Règles ordonnées et contrôle des flux entre Internet et les réseaux internes.
Synology Network Hardening
Segmentation, pare-feu, VPN et automatisation autour de Synology SRM.
Ce que le projet démontre
Séparation des usages et réflexion autour de l’isolation sur le réseau principal.
Gestion des accès distants et contrôle des routes autorisées.
Comparaison entre l’état attendu et la configuration réellement appliquée.
Réseau & sécurité en images
Les captures SRM et audit seront ajoutées automatiquement dès leur présence dans le dossier synology.

Description
Synology Network Hardening formalise la configuration réseau autour d’un routeur Synology afin de rendre les règles de sécurité plus cohérentes et plus faciles à auditer. Les scripts PHP/Bash servent à interroger ou appliquer des éléments de configuration via les interfaces disponibles dans SRM.
Le travail couvre notamment les règles de pare-feu, les accès VPN, la séparation logique des usages et la vérification de l’état réel par rapport à une configuration attendue. L’objectif est de réduire les manipulations répétitives dans l’interface graphique et de conserver une trace de ce qui est appliqué.
Ce projet s’inscrit dans l’infrastructure personnelle Softcloud, qui regroupe également des services Linux auto-hébergés. Un lien vers softcloud.fr permet de voir la partie visible de cet environnement réseau et services.
Fonctionnalités & points techniques
- ▹Gestion et audit des règles de pare-feu Synology SRM.
- ▹Contrôle des flux WAN/LAN et des équipements internes.
- ▹Configuration et suivi des accès VPN.
- ▹Scripts PHP/Bash pour automatiser les contrôles répétitifs.
- ▹Configuration déclarative en JSON pour les éléments automatisés.
- ▹Journalisation des opérations et détection de dérive.
- ▹Approche idempotente autant que possible.
- ▹Intégration dans l’écosystème auto-hébergé Softcloud.
Requêtes CLI & scripts SRM
Extraits réels des commandes et scripts utilisés pour explorer l’API SRM, diagnostiquer les versions et méthodes disponibles, gérer l’authentification 2FA, durcir l’accès WAN et appliquer les règles de pare-feu.
Requêtes CLI utilisées
Commandes de diagnostic réseau et séquence des scripts utilisés pendant le durcissement.
# Requêtes CLI et scripts utilisés — Synology Network Hardening# Diagnostic réseauip routetraceroute -n 1.1.1.1 | headtcpdump -ni eth0 'tcp port 5349 or udp port 5349 or udp port 8095'# Pare-feu / portsyunohost firewall listnft list ruleset | grep -E -C2 '3478|5349|5350|49152|65535'# Scripts SRM / APIbash 05-explore-srm-api-v2.shbash 08-diagnose-api-versions.shbash 09-test-create-rules.shbash 10-apply-what-works.shbash 11-retry-cookie-auth.shbash 13-set-rules-fixed.shAuthentification SRM, 2FA et SID par Cookie
Détection de la version d’authentification, login SRM, gestion OTP et transmission du SID dans le cookie.
detect_auth_version() { local ver ver=$(curl -sk "$BASE/query.cgi" \ --data-urlencode "api=SYNO.API.Info" \ --data-urlencode "version=1" \ --data-urlencode "method=query" \ --data-urlencode "query=SYNO.API.Auth" \ | python3 -c "import sys,jsontry: print(json.load(sys.stdin)['data']['SYNO.API.Auth']['maxVersion'])except: print('')" 2>/dev/null) [[ -n "$ver" && "$ver" =~ ^[0-9]+$ ]] && echo "$ver" || echo "3"}load_device_id() { [[ ! -f "$AUTH_FILE" ]] && { echo ""; return; } local host user did host=$(grep "^host=" "$AUTH_FILE" 2>/dev/null | cut -d= -f2) user=$(grep "^user=" "$AUTH_FILE" 2>/dev/null | cut -d= -f2) did=$(grep "^device_id=" "$AUTH_FILE" 2>/dev/null | cut -d= -f2) [[ "$host" == "$SRM_HOST" && "$user" == "$SRM_USER" && -n "$did" ]] && echo "$did" || echo ""}save_device_id() { local did="$1" cat > "$AUTH_FILE" << EOFhost=$SRM_HOSTuser=$SRM_USERdevice_id=$didsaved_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")EOF chmod 600 "$AUTH_FILE" echo -e " ${GREEN}✓${NC} device_id sauvegardé → $AUTH_FILE"}_do_login() { local pass="$1" otp="${2:-}" did="${3:-}" local args=( --data-urlencode "api=SYNO.API.Auth" --data-urlencode "version=${AUTH_VER}" --data-urlencode "method=Login" --data-urlencode "account=${SRM_USER}" --data-urlencode "passwd=${pass}" --data-urlencode "session=RouterManager" --data-urlencode "format=sid" --data-urlencode "enable_device_token=yes" --data-urlencode "device_name=${DEVICE_NAME}" ) [[ -n "$otp" ]] && args+=(--data-urlencode "otp_code=${otp}") [[ -n "$did" ]] && args+=(--data-urlencode "device_id=${did}") curl -sk "$BASE/auth.cgi" "${args[@]}"}Exploration des API de sécurité SRM
Inventaire des API Firewall, Forward, GeoIP, AutoBlock et Port Forwarding après authentification.
srm_login # Gère 2FA automatiquementapi() { srm_api "$1" "${2:-1}" "${3:-list}" "${@:4}" | python3 -m json.tool 2>/dev/null; }section "Versions APIs firewall"curl -sk "$BASE/query.cgi" \ --data-urlencode "api=SYNO.API.Info" \ --data-urlencode "version=1" --data-urlencode "method=query" \ --data-urlencode "query=SYNO.Core.Security.Firewall,SYNO.Core.Security.AutoBlock" \ --data-urlencode "_sid=$SID" \ | python3 -c "import sys,jsonfor k,v in sorted(json.load(sys.stdin).get('data',{}).items()): print(f' {k} v{v.get(\"minVersion\")}..{v.get(\"maxVersion\")} path={v.get(\"path\")}')" 2>/dev/nullsection "SYNO.Core.Security.Firewall.Conf"api "SYNO.Core.Security.Firewall.Conf" 1 getsection "SYNO.Core.Security.Firewall.Rules (INPUT WAN)"api "SYNO.Core.Security.Firewall.Rules" 1 listsection "SYNO.Core.Security.Firewall.Forward.Conf"api "SYNO.Core.Security.Firewall.Forward.Conf" 1 getsection "SYNO.Core.Security.Firewall.Forward.Rules (FORWARD NAS)"api "SYNO.Core.Security.Firewall.Forward.Rules" 1 listsection "SYNO.Core.Security.Firewall.Geoip"api "SYNO.Core.Security.Firewall.Geoip" 1 getapi "SYNO.Core.Security.Firewall.Geoip" 1 listsection "SYNO.Core.Security.Firewall.AllowSrmFromWan"api "SYNO.Core.Security.Firewall.AllowSrmFromWan" 1 getsection "SYNO.Core.Security.Firewall.Rules.HitCount"api "SYNO.Core.Security.Firewall.Rules.HitCount" 1 listsection "SYNO.Core.Security.AutoBlock.Rules"api "SYNO.Core.Security.AutoBlock.Rules" 1 listsection "SYNO.Core.PortForwarding.Rules"api "SYNO.Core.PortForwarding.Rules" 1 listsrm_logoutDiagnostic des versions et méthodes API
Détection des versions maximales et test empirique des méthodes list/get/set/create/save des API SRM.
echo -e "\n${CYAN}══ Étape 1 : versions max de chaque API ══${NC}"declare -A API_VERSIONSALL_QUERY=$(IFS=','; echo "${APIS[*]}")VERS_RESP=$(curl -sk "$BASE/query.cgi" \ --data-urlencode "api=SYNO.API.Info" \ --data-urlencode "version=1" \ --data-urlencode "method=query" \ --data-urlencode "query=$ALL_QUERY" \ --data-urlencode "_sid=$SID")for api in "${APIS[@]}"; do info=$(echo "$VERS_RESP" | python3 -c "import sys,jsond=json.load(sys.stdin).get('data',{})if '$api' in d: v=d['$api'] print(f\"{v.get('minVersion',1)}-{v.get('maxVersion','?')} path={v.get('path','?')}\")else: print('NOT FOUND')" 2>/dev/null) if [[ "$info" == "NOT FOUND" ]]; then fail "$api → non trouvée dans ce SRM" else ok "$api → v$info" maxver=$(echo "$info" | grep -oP '(?<=-)\d+' | head -1) API_VERSIONS[$api]="${maxver:-1}" fidoneecho -e "\n${CYAN}══ Étape 2 : méthodes valides par API ══${NC}"for api in "${APIS[@]}"; do [[ -z "${API_VERSIONS[$api]+x}" ]] && continue ver="${API_VERSIONS[$api]}" echo -e "\n ${CYAN}$api${NC} (version $ver) :" for method in "${METHODS[@]}"; do resp=$(srm_api "$api" "$ver" "$method" 2>/dev/null) code=$(echo "$resp" | python3 -c "import sys,jsontry: d=json.load(sys.stdin) if d.get('success'): print('OK') else: print(d.get('error',{}).get('code','?'))except: print('parse_err')" 2>/dev/null) case "$code" in OK) # Afficher les clés du retour keys=$(echo "$resp" | python3 -c "import sys,jsond=json.load(sys.stdin).get('data',{})if isinstance(d,dict): print(', '.join(list(d.keys())[:5]))elif isinstance(d,list): print(f'liste de {len(d)} éléments')else: print(type(d).__name__)" 2>/dev/null) ok "method=$method → OK [data: $keys]" ;; 103) ;; # Méthode inconnue — silencieux 104) warn "method=$method → version $ver non supportée (essayez v1)" ;; *) ;; # Autre erreur — silencieux esac donedoneecho -e "\n${CYAN}══ Étape 3 : structure détaillée des APIs qui marchent ══${NC}"# Firewall.Rules — essai de toutes les versions de 1 à maxVersionecho -e "\n ${CYAN}SYNO.Core.Security.Firewall.Rules${NC} — test versions :"for v in 1 2 3 4 5 6; do for m in list get; do r=$(srm_api "SYNO.Core.Security.Firewall.Rules" "$v" "$m" 2>/dev/null) c=$(echo "$r" | python3 -c "import sys,json; d=json.load(sys.stdin); print('OK' if d.get('success') else d.get('error',{}).get('code','?'))" 2>/dev/null) [[ "$c" == "OK" ]] && ok "v$v method=$m → OK" && echo "$r" | python3 -m json.tool 2>/dev/null | head -10 | sed 's/^/ /' [[ "$c" != "103" && "$c" != "OK" ]] && echo -e " v$v/$m → code $c" donedoneDurcissement appliqué via API
Désactivation de l’administration WAN, renforcement AutoBlock et configuration globale du pare-feu.
# ── 1. Désactiver accès admin depuis Internet ─────────────────section "1. Accès SRM depuis WAN"echo " Avant :"srm_api "SYNO.Core.Security.Firewall.AllowSrmFromWan" 1 get \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' enable = {d[\"data\"][\"enable\"]}')" 2>/dev/nullapi_ok "AllowSrmFromWan → false (admin inaccessible depuis Internet)" \ "SYNO.Core.Security.Firewall.AllowSrmFromWan" 1 set \ --data-urlencode "enable=false"echo " Après :"srm_api "SYNO.Core.Security.Firewall.AllowSrmFromWan" 1 get \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' enable = {d[\"data\"][\"enable\"]}')" 2>/dev/null# ── 2. AutoBlock — durcir la configuration ───────────────────section "2. AutoBlock brute-force"echo " Config actuelle :"CURRENT=$(srm_api "SYNO.Core.Security.AutoBlock" 1 get)show "$CURRENT"echo " Application config renforcée (5 tentatives / 1 min / blocage permanent) :"# Noms de paramètres exacts issus de la réponse get :# attempts, within_mins, expire_day, enableapi_ok "AutoBlock configuré" \ "SYNO.Core.Security.AutoBlock" 1 set \ --data-urlencode "enable=true" \ --data-urlencode "attempts=5" \ --data-urlencode "within_mins=1" \ --data-urlencode "expire_day=0"echo " Config après modification :"srm_api "SYNO.Core.Security.AutoBlock" 1 get \ | python3 -c "import sys,jsond=json.load(sys.stdin).get('data',{})print(f' enable = {d.get(\"enable\")}')print(f' attempts = {d.get(\"attempts\")} (was 10)')print(f' within_mins= {d.get(\"within_mins\")} min (was 5)')print(f' expire_day = {d.get(\"expire_day\")} (0=permanent, was 30)')" 2>/dev/null# ── 3. Firewall.Conf ─────────────────────────────────────────section "3. Configuration pare-feu globale"api_ok "Firewall.Conf → enable_port_check=true" \ "SYNO.Core.Security.Firewall.Conf" 1 set \ --data-urlencode "enable_port_check=true"# ── 4. Forward.Conf — auto-règles port-forward ───────────────section "4. Règles forward automatiques"echo " Config actuelle :"srm_api "SYNO.Core.Security.Firewall.Forward.Conf" 1 get \ | python3 -m json.tool 2>/dev/null | sed 's/^/ /'# pf_auto_fw_rules=true : les redirections de port créent auto des règles firewall# Vérifions que c'est bien le cas (déjà true selon l'explore)api_ok "Forward.Conf → pf_auto_fw_rules=true confirmé" \ "SYNO.Core.Security.Firewall.Forward.Conf" 1 set \ --data-urlencode "pf_auto_fw_rules=true" \ --data-urlencode "dmz_auto_fw_rules=true" \ --data-urlencode "upnp_auto_fw_rules=false" 2>/dev/null \ || ok "Forward.Conf déjà correctement configuré (lecture seule)"Construction et application du jeu de règles
Lecture des règles SRM, modification GeoIP, ajout WireGuard et application batch via method=set.
# ── 3. Construction des nouvelles règles (via fichier) ────────section "3. Construction nouveau jeu de règles (fix Python)"python3 << PYEOFimport json, sysGEO_ALLOW = "A1,A2,DE,EU,FR,GF,GP,MF,MQ,NC,PF,PM,RE,TF,YT,BE,LU,CH,MC"GEO_DENY = "BY,RU,CN,KP,IR,VN,MM,SY,IQ,YE,AF,CU,PK,NG,UA,KZ,TJ,TM,UZ,AZ"with open('$TMPDIR_WORK/current.json') as f: data = json.load(f)rules = data['data']['rules']seen_ui_ids = {r.get('ui_id', -1) for r in rules}next_id = max(i for i in seen_ui_ids if isinstance(i, int)) + 1modified = []wireguard_exists = Falsefor rule in rules: uid = rule.get('ui_id', -1) name = rule.get('name', '') if uid == 7: # AUTHORISER PAYS rule['enabled'] = True rule['src_ip_value'] = GEO_ALLOW print(f" Modifié: AUTHORISER PAYS → {GEO_ALLOW}", file=sys.stderr) elif uid == 8: # Ban RUSSIE rule['enabled'] = True rule['name'] = 'Ban Pays Risques' rule['src_ip_value'] = GEO_DENY print(f" Modifié: Ban Pays Risques → {GEO_DENY}", file=sys.stderr) if '51820' in str(rule.get('dst_port_value', '')) or 'wireguard' in name.lower(): wireguard_exists = True modified.append(rule)if not wireguard_exists: wg = { "action": "allow", "chain": "input", "created_by": "user", "dst_ip_type": "all", "dst_ip_value": "", "dst_port_type": "port", "dst_port_value": "51820", "enabled": True, "in_interface_type": "all", "in_interface_value": [], "ip_version": "ipv4", "logged": False, "name": "WireGuard VPN", "out_interface_type": "all", "out_interface_value": [], "profile": "custom", "protocol": "udp", "src_ip_type": "all", "src_ip_value": "", "src_port_type": "all", "src_port_value": "", "ui_id": next_id } modified.append(wg) print(f" Ajouté: WireGuard VPN UDP 51820 (ui_id={next_id})", file=sys.stderr)# Écrire dans fichierwith open('$TMPDIR_WORK/new_rules.json', 'w') as f: json.dump(modified, f)print(f" Total: {len(modified)} règles générées", file=sys.stderr)# Afficher résuméfor r in modified: en = 'ON ' if r.get('enabled') else 'off' geo = '[GEO]' if r.get('src_ip_type') == 'geo' else '' print(f" [{en}] {r.get('name','?'):<30} {geo}")PYEOFApplication et vérification finale des règles
Envoi JSON du jeu de règles, gestion de la réponse et relecture de l’état final.
# ── 4. Envoi via set ──────────────────────────────────────────section "4. Envoi via set (JSON correct cette fois)"NEW_RULES=$(cat "$TMPDIR_WORK/new_rules.json")echo " Taille JSON : ${#NEW_RULES} caractères"echo " Début : ${NEW_RULES:0:80}..."SET_RESP=$(srm_api "SYNO.Core.Security.Firewall.Rules" 2 set \ --data-urlencode "rules=$NEW_RULES")SET_CODE=$(echo "$SET_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin)print('OK' if d.get('success') else d.get('error',{}).get('code','?'))" 2>/dev/null)echo -e " set → $SET_CODE"show "$SET_RESP"if [[ "$SET_CODE" == "OK" ]]; then ok "SET réussi ! Toutes les règles appliquées"else warn "$(errcode_label "$SET_CODE")" # Essai avec le paramètre 'firewall_rules' au lieu de 'rules' echo " Essai paramètre firewall_rules=..." SET2=$(srm_api "SYNO.Core.Security.Firewall.Rules" 2 set \ --data-urlencode "firewall_rules=$NEW_RULES") CODE2=$(echo "$SET2" | python3 -c "import sys,json; d=json.load(sys.stdin)print('OK' if d.get('success') else d.get('error',{}).get('code','?'))" 2>/dev/null) echo -e " firewall_rules → $CODE2" [[ "$CODE2" == "OK" ]] && ok "Réussi avec firewall_rules !" && show "$SET2"fi# ── 5. État final ─────────────────────────────────────────────section "5. État final"srm_api "SYNO.Core.Security.Firewall.Rules" 2 get \ | python3 -c "import sys,jsond=json.load(sys.stdin)['data']print(f' {len(d.get(\"rules\",[]))} règles actives :')for r in d.get('rules',[]): en='ON ' if r.get('enabled') else 'off' geo='[GEO '+r.get('src_ip_value','')[:20]+']' if r.get('src_ip_type')=='geo' else '' print(f' [{en}] {r.get(\"name\",\"?\"):<30} {geo}')" 2>/dev/nullsrm_logout