OPNsense — Config automatique
Suite logique de
07-template-opnsense(template Packer + VM clone Terraform). Cette fiche déploie tout le contenu fonctionnel d’OPNsense via Ansible : users, aliases, firewall, NAT, port forwards, DHCP Kea, DNS Unbound, WireGuard, services.1 seule commande :
task opnsense:config(idempotent, 7 étapes auto-skip).
Contexte
Convention identification automation IaC :
Toutes les règles, NAT, port forwards, aliases déployés par Ansible portent
le préfixe 🤖 minfra: dans leur description. Visible directement dans
l’UI OPNsense (Firewall → Rules / NAT) → pas de confusion avec règles
manuelles éventuelles.
⚠ Cleanup automatique : avant chaque upsert, les rules legacy minfra:
(sans 🤖) sont supprimées par le rôle Ansible pour éviter les doublons
après renommage du préfixe. Idempotent, no-op si rien à cleaner.
Architecture déployée :
WAN (vtnet0 192.168.1.2) ←→ Box SFR (DMZ → 192.168.1.2)
│
├─ Port forward → endor Traefik (10.0.1.15:80/443)
├─ NAT outbound : LAN + DMZ → WAN (masquerade)
├─ WireGuard listen UDP 51820
│
LAN (vtnet1 10.0.1.1/24) — gateway + Unbound DNS + DHCP Kea
│
DMZ (vtnet2 10.0.2.1/24) — gateway + Unbound DNS + DHCP Kea
│
WG (wg0 10.10.10.1/24) — clients VPN nomades
Lancer le déploiement
# === Windows (PowerShell) ===
cd D:\git\minfra-v2
task opnsense:config
Verify: task opnsense:postcheck
7 étapes auto-skip (cf script infra/ansible/scripts/opnsense_smart_config.sh):
| # | Étape | Skip si |
|---|---|---|
| 1 | SSH key minfra | ssh root@opnsense OK |
| 2 | Python311 OPNsense | /usr/local/bin/python3 exists |
| 3 | API HTTPS up | poll 60s max, break dès code != 000 |
| 4 | API key SOPS valide | curl HTTP 200 avec key SOPS |
| 5 | WireGuard server keys | présentes dans SOPS |
| 6 | Config Ansible role | toujours (idempotent) |
| 7 | Postcheck sanity | toujours |
Règles Firewall déployées
WAN (interface vtnet0)
| Description (UI) | Action | Proto | Source | Destination | Port |
|---|---|---|---|---|---|
| 🤖 minfra: Allow ICMP on WAN | pass | ICMP | any | wanip | - |
| 🤖 minfra: GeoIP deny (Block_COUNTRIES) | block | any | Block_COUNTRIES | any | - |
| 🤖 minfra: WireGuard inbound | pass | UDP | any | wanip | 51820 |
| 🤖 minfra: NAT Traefik HTTPS inbound (post-rdr → endor) | pass | TCP | any | 10.0.1.15 | 443 |
| 🤖 minfra: NAT Traefik HTTP inbound (post-rdr → endor) | pass | TCP | any | 10.0.1.15 | 80 |
| 🤖 minfra: NAT Plex inbound | pass | TCP | any | wanip | 32400 |
⚠ Les règles post-rdr → endor ont destination_net=10.0.1.15, pas wanip.
Raison : pf évalue les rules filter APRÈS le NAT redirect. Le paquet a
déjà sa nouvelle destination (10.0.1.15) au moment du filter check.
LAN (interface vtnet1)
| Description | Action | Proto | Source | Destination | Port |
|---|---|---|---|---|---|
| 🤖 minfra: LAN DNS to Unbound only | pass | TCP/UDP | lan | 10.0.1.1 | 53 |
| 🤖 minfra: Block external DNS | block | TCP/UDP | lan | NOT lanip | 53 |
| 🤖 minfra: LAN to DMZ web | pass | TCP | lan | 10.0.2.0/24 | PORTS_WEB (80/443) |
| 🤖 minfra: LAN to DMZ OPNsense GUI | pass | TCP | lan | 10.0.2.1 | 443 |
| 🤖 minfra: Allow all LAN (admin) | pass | any | lan | any | any |
DMZ (interface opt1)
| Description | Action | Proto | Source | Destination | Port |
|---|---|---|---|---|---|
| 🤖 minfra: Allow ICMP on DMZ | pass | ICMP | any | 10.0.2.1 | - |
| 🤖 minfra: DMZ DNS to OPNsense | pass | TCP/UDP | 10.0.2.0/24 | 10.0.2.1 | 53 |
| 🤖 minfra: DMZ NTP to OPNsense | pass | UDP | 10.0.2.0/24 | 10.0.2.1 | 123 |
| 🤖 minfra: DMZ NFS to jedha (scoped) | pass | TCP | 10.0.2.0/24 | HOST_NAS | PORT_NFS (111/2049) |
| 🤖 minfra: DMZ SMB to jedha (scoped) | pass | TCP | HOST_JAKKU,HOST_HOTH | HOST_NAS | PORT_SAMBA (445) |
| 🤖 minfra: DMZ Plex backend to jedha | pass | TCP | HOST_Kalevala,Mandalore | HOST_NAS | PORT_PLEX (32400) |
| 🤖 minfra: DMZ Ollama API to coruscant | pass | TCP | HOST_Kalevala | HOST_Ollama | PORT_OLLAMA (11434) |
| 🤖 minfra: DMZ vCenter API to utapau | pass | TCP | HOST_Mandalore,JAKKU,Tatooine,Kamino | HOST_vCenter,ESXi | 443 |
| 🤖 minfra: DMZ Packer HTTP to naboo | pass | TCP | 10.0.2.0/24 | HOST_NABOO | 8275 |
| 🤖 minfra: DMZ outbound web | pass | TCP | 10.0.2.0/24 | any | PORTS_WEB |
| 🤖 minfra: Block DMZ to LAN (catch-all) | block | any | 10.0.2.0/24 | 10.0.1.0/24 | - |
VPN (interface WireGuard)
| Description | Action | Source | Destination |
|---|---|---|---|
| 🤖 minfra: VPN to LAN full | pass | 10.10.10.0/24 | 10.0.1.0/24 |
| 🤖 minfra: VPN to DMZ full | pass | 10.10.10.0/24 | 10.0.2.0/24 |
| 🤖 minfra: VPN to Internet | pass | 10.10.10.0/24 | any |
NAT déployé
Source NAT (outbound — masquerade)
| Description | Source | Destination | Target | Interface |
|---|---|---|---|---|
| 🤖 minfra: LAN → WAN outbound NAT | lan | any | wanip | wan |
| 🤖 minfra: DMZ → WAN outbound NAT | opt1 (DMZ) | any | wanip | wan |
Port forwards (DNAT WAN → backend interne)
| Description | Proto | WAN port | Target IP | Target port |
|---|---|---|---|---|
| 🤖 minfra: WAN HTTPS → endor Traefik | TCP | 443 | 10.0.1.15 | 443 |
| 🤖 minfra: WAN HTTP → endor Traefik | TCP | 80 | 10.0.1.15 | 80 |
DHCP Kea
Plages dynamiques (.150 → .250 sur chaque subnet, statiques .2 → .149).
| Subnet | Range | Gateway | DNS | Domain |
|---|---|---|---|---|
| 10.0.1.0/24 LAN | 10.0.1.150–250 | 10.0.1.1 | 10.0.1.1 | minfra.in |
| 10.0.2.0/24 DMZ | 10.0.2.150–250 | 10.0.2.1 | 10.0.2.1 | minfra.in |
DNS Unbound — Host overrides
13 entrées internes *.minfra.in (cf vars/main.yml):
opnsense → 10.0.1.1,naboo → 10.0.1.10,dagobah → 10.0.1.12endor → 10.0.1.15,jedha → 10.0.1.20,coruscant → 10.0.1.30yavin4-kvm → 10.0.1.4,utapau → 10.0.1.100,yavin4 → 10.0.1.101- DMZ :
tatooine → 10.0.2.10(platform),kamino → 10.0.2.11(GitLab CE),kalevala → 10.0.2.12(monitoring)
Aliases Firewall
Host aliases : HOST_NABOO, HOST_NAS, HOST_vCenter, HOST_ESXi,
HOST_Ollama, HOST_HOTH, HOST_Kamino, HOST_Mandalore, HOST_JAKKU,
HOST_Kalevala, HOST_Tatooine.
Port aliases : PORT_NFS (111/2049), PORT_SAMBA (445), PORT_PLEX (32400),
PORT_OLLAMA (11434), PORTS_WEB (80/443).
WireGuard server + peers
Server (instance wg0):
- Tunnel
10.10.10.1/24, listen UDP51820 - Clés générées auto par smart-config si absentes du SOPS
(
opnsense_wireguard_server_pubkey/privkey)
Peers (clients VPN) — persistance SOPS:
Ajout d’un peer :
task vpn:add-peer NAME=mobile-sacha IP=10.10.10.5
Effectue 3 actions:
- Génère keypair Curve25519 (priv/pub) côté localhost
- Push API OPNsense : addClient nom =
iac-mobile-sacha(visible UI). ⚠ Le champnameWG OPNsense n’accepte QUE alphanumeric + dash + underscore — pas d’emoji/espace/colon. Prefixeiac-(Infrastructure as Code) identifie l’origine automation. - Persist SOPS : append à
opnsense_wireguard_clientsdansinventory/secrets/opnsense.yml(chiffré age) avec name/pubkey/privkey/IP - Génère
.conf+ QR PNG dans~/minfra-output/wg-peers/ - Reconfigure WG OPNsense
➡ Au prochain task opnsense:config, le rôle wireguard.yml ré-itère
opnsense_wireguard_clients SOPS → tous les peers re-poussés auto sur OPNsense.
Survit aux rebuilds:
- ✅ Wipe + reflash template OPNsense → peers restaurés
- ✅ Terraform destroy + apply VM 200 → peers restaurés
- ✅ Disaster Recovery complet → SOPS git = source de vérité
Suppression :
task vpn:delete-peer NAME=mobile-sacha
- Retire de OPNsense API (lookup par
iac-mobile-sacha) - Retire de SOPS
opnsense_wireguard_clients
Structure SOPS pour peers (déchiffré):
opnsense_wireguard_clients:
- name: "iac-mobile-sacha"
enabled: "1"
pubkey: "..."
privkey: "..." # garde priv pour regen .conf si fichier local perdu
tunneladdress: "10.10.10.5/32"
persistentkeepalive: "25"
Helper script : infra/ansible/scripts/wg_sops_peer.sh add|del NAME ...
Services force-start
Après config push, le rôle force régénération + start de services:
configctl template reload OPNsense/Kea # régénère /usr/local/etc/kea/*.conf
configctl kea restart # démarre daemon Kea DHCPv4
configctl hostwatch restart # démarre host discovery service
Sans template reload, Kea n’a pas de config file → start silencieux no-op.
Sécurité — cleanup automatique avant upsert
Chaque section (firewall.yml, nat.yml, port_forward_patch.php.j2)
exécute un cleanup en début de tâche:
# Supprime toute rule dont description commence par "minfra:" SANS 🤖
- name: "Supprimer rules legacy 'minfra:' (sans 🤖)"
...
loop: rules where description matches '^minfra:' AND NOT '^🤖'
⚠ Conséquences :
- ✅ Renommer un préfixe (
minfra:→🤖 minfra:) ne crée jamais de doublons - ✅ Les rules manuelles UI sans préfixe minfra: sont préservées
- ⚠ Si tu ajoutes manuellement une rule UI avec
minfra: X(sans 🤖), elle sera supprimée au prochaintask opnsense:config
Convention : pour règles manuelles, n’utilise jamais le préfixe minfra:.
Mets un autre identifiant (custom: X, manual: X, etc.) → préservé.
Postcheck — vérifications fonctionnelles
Le smart-config termine par opnsense-postcheck.yml qui valide:
- LAN SSH :
10.0.1.1:22accessible depuis naboo - LAN HTTPS :
10.0.1.1:443accessible depuis naboo - API REST :
GET /api/core/firmware/runningretourne HTTP 200 - DMZ HTTPS :
10.0.2.1:443écoute (test via SSH OPNsense, contourne routing naboo) - WAN NAT :
ping 8.8.8.8depuis OPNsense réussi (NAT outbound OK) - DNS Unbound :
opnsense.minfra.inrésout →10.0.1.1
Tasks utiles
task opnsense:config # full chain idempotent (recommandé)
task opnsense:postcheck # vérif seule (sans changement)
task opnsense:fw:sync # sync rapide FW/NAT seulement (skip user/aliases/wg/dhcp)
task opnsense:get-rules # GET toutes les rules actuelles (FW + NAT + port forwards)
task vpn:add-peer NAME=mobile-sacha IP=10.10.10.5 # ajoute client WG + QR
task vpn:delete-peer NAME=mobile-sacha # retire client WG
Hors périmètre (Phase 2)
- CARP / HA secondaire OPNsense — différé
- Multi-WAN — un seul lien SFR actuellement
- VLAN 40 IoT + VLAN 50 Invités — différés
- Suricata IDS — RAM upgrade requis avant
- VPN site-à-site WireGuard — différé
- Backup config.xml auto cron
scp /conf/config.xml jedha:— TODO