← Tous les projets
🔒

Synology Network Hardening

Segmentation, pare-feu, VPN et automatisation autour de Synology SRM.

PHPBashSynology SRMFirewallVPNJSONNginxYunoHost

Ce que le projet démontre

Pare-feu

Règles ordonnées et contrôle des flux entre Internet et les réseaux internes.

Segmentation

Séparation des usages et réflexion autour de l’isolation sur le réseau principal.

VPN

Gestion des accès distants et contrôle des routes autorisées.

Audit

Comparaison entre l’état attendu et la configuration réellement appliquée.

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.

BASH
00-requetes-cli.txt · lignes 1–18
# 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.sh

Authentification SRM, 2FA et SID par Cookie

Détection de la version d’authentification, login SRM, gestion OTP et transmission du SID dans le cookie.

BASH
01-srm-auth-2fa.txt · lignes 95–147
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.

BASH
02-explore-srm-api.txt · lignes 12–56
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_logout

Diagnostic 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.

BASH
03-diagnose-api-versions.txt · lignes 29–108
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"  donedone

Durcissement appliqué via API

Désactivation de l’administration WAN, renforcement AutoBlock et configuration globale du pare-feu.

BASH
04-apply-hardening.txt · lignes 37–97
# ── 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.

BASH
05-firewall-rules-batch.txt · lignes 67–144
# ── 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}")PYEOF

Application 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.

BASH
05-firewall-rules-batch.txt · lignes 160–208
# ── 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

Architecture technique

Routeur
Synology SRM
Pare-feu, réseau local, VPN et services du routeur.
Automatisation
PHP + Bash
Interrogation, application et vérification de paramètres.
Configuration
JSON
Description structurée de l’état attendu.
Sécurité
Firewall + VPN
Réduction de la surface d’exposition et contrôle des flux.

Technologies utilisées

PHPBashSynology SRMFirewallVPNJSONNginxYunoHost

Liens