https://github.com/mikysal78/ansible-dns
Ansible playbook per infrastruttura DNS production-ready: BIND9 hidden primary, N secondari pubblici, DNSSEC inline signing, hardening OS, nftables, ACME wildcard, DDNS OpenWrt, Prometheus/Grafana.
https://github.com/mikysal78/ansible-dns
acme ansible bind9 ddns debian devops dns dnssec grafana infrastructure-as-code nftables openwrt prometheus
Last synced: 15 days ago
JSON representation
Ansible playbook per infrastruttura DNS production-ready: BIND9 hidden primary, N secondari pubblici, DNSSEC inline signing, hardening OS, nftables, ACME wildcard, DDNS OpenWrt, Prometheus/Grafana.
- Host: GitHub
- URL: https://github.com/mikysal78/ansible-dns
- Owner: mikysal78
- License: mit
- Created: 2026-06-07T05:10:39.000Z (20 days ago)
- Default Branch: main
- Last Pushed: 2026-06-07T21:52:44.000Z (20 days ago)
- Last Synced: 2026-06-07T22:23:39.677Z (20 days ago)
- Topics: acme, ansible, bind9, ddns, debian, devops, dns, dnssec, grafana, infrastructure-as-code, nftables, openwrt, prometheus
- Language: Jinja
- Homepage:
- Size: 135 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# π ansible-dns β Infrastruttura DNS Professionale
[](https://github.com/mikysal78/ansible-dns/actions/workflows/ci.yml)
[](https://github.com/ansible/ansible-lint)
[](LICENSE)
[](https://www.debian.org/)
[](https://www.isc.org/bind/)
[](https://www.proxmox.com/)
Playbook Ansible completo per deployare un'infrastruttura DNS **production-ready** con hidden primary su Proxmox VE, N secondari pubblici su VPS, DNSSEC inline signing, hardening OS, firewall nftables, certificati ACME wildcard, DDNS per router OpenWrt e monitoring con Prometheus/Grafana.
---
## π Indice
- [Architettura](#architettura)
- [FunzionalitΓ ](#funzionalitΓ )
- [Prerequisiti](#prerequisiti)
- [Struttura del progetto](#struttura-del-progetto)
- [Proxmox β Provisioning VM](#proxmox--provisioning-vm)
- [OVH β VM secondarie](#ovh--vm-secondarie)
- [Setup iniziale](#setup-iniziale)
- [Configurazione](#configurazione)
- [Deploy DNS](#deploy-dns)
- [Tunnel WireGuard](#tunnel-wireguard)
- [Gestione zone](#gestione-zone)
- [DNSSEC](#dnssec)
- [DDNS β Router OpenWrt](#ddns--router-openwrt)
- [Monitoring](#monitoring)
- [Hardening](#hardening)
- [Firewall nftables](#firewall-nftables)
- [Certificati ACME](#certificati-acme)
- [CI/CD](#cicd)
- [Operazioni giornaliere](#operazioni-giornaliere)
- [Troubleshooting](#troubleshooting)
- [Sicurezza](#sicurezza)
---
## Architettura
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PROXMOX VE (rete locale) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β VM dns-primary (VMID 200) β Debian Trixie β β
β β 192.168.1.10 β 2 vCPU host β 2GB RAM β 40GB VirtIO β β
β β β β
β β β’ BIND9 Hidden Master (non esposto a internet) β β
β β β’ DNSSEC inline signing (Ed25519, dnssec-policy) β β
β β β’ acme.sh wildcard via DNS-01 β β
β β β’ Prometheus + Grafana + Alertmanager β β
β β β’ fail2ban + nftables + auditd + rkhunter β β
β ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββ
β Tunnel WireGuard cifrato (10.99.0.0/24)
β AXFR/IXFR (TSIG) + NOTIFY viaggiano qui dentro
ββββββββββββββββΌβββββββββββββββ¬ββββββββββββββ
βΌ βΌ βΌ βΌ
ββββββββββββββ ββββββββββββββ ββββββββββββββ fino a 5
β ns1 (VPS) β β ns2 (VPS) β β ns3 (VPS) β secondari
βDebian Trixieβ βDebian Trixieβ βDebian Trixieβ
β wg 10.99.0.2β β wg 10.99.0.3β β wg 10.99.0.4β
β Query DNS β β Query DNS β β Query DNS β
β pubbliche β β pubbliche β β pubbliche β
ββββββββββββββ ββββββββββββββ ββββββββββββββ
β² β² β²
ββββββββββββββββΌβββββββββββββββ
UDP/TCP 53 pubblico
(rate limiting + anti-amplification)
Il primary (dietro NAT) inizia il tunnel verso i secondari (endpoint
pubblici); PersistentKeepalive mantiene aperto il percorso. Così il
primary resta nascosto e il transfer di zona Γ¨ cifrato end-to-end.
βββββββββββββββββββββββββββββββββββββββββββ
β Router OpenWrt (DDNS) β
β nsupdate TSIG β dyn.example.com β
β router-home.dyn.example.com β WAN IP β
βββββββββββββββββββββββββββββββββββββββββββ
```
---
## FunzionalitΓ
### Proxmox VE
- Provisioning VM primary via **API Proxmox** (`community.general.proxmox_kvm`)
- Creazione automatica **template Debian Trixie** genericcloud con `virt-customize`
- Clone template β VM con **cloud-init** (IP statico, utente, chiave SSH, pacchetti)
- Hardware ottimizzato: `q35`, `UEFI`, `CPU host` (AES-NI + rdrand), VirtIO, balloon disabilitato
- **Snapshot automatico** post-creazione come baseline pre-deploy
- Playbook dedicati per gestione snapshot (crea, lista, rollback, elimina)
### DNS Core
- **Hidden Primary** β il master non Γ¨ mai esposto a internet
- **N secondari pubblici** β da 2 a 5+ VPS, configurazione automatizzata
- **Zone in YAML** β formato leggibile con supporto a tutti i record professionali
- **AXFR/IXFR autenticato** β chiave TSIG `hmac-sha256`
- **Record supportati** β A, AAAA, CNAME, MX, TXT, SRV, CAA, TLSA, SSHFP, PTR
### Tunnel WireGuard (primary β secondari)
- **Trasferimento di zona cifrato** β AXFR/IXFR e NOTIFY viaggiano dentro un tunnel WireGuard, mai in chiaro su internet
- **Primary dietro NAT** β pensato per il caso reale di un hidden primary in LAN (senza IP pubblico) e secondari su VPS cloud
- **Topologia roaming peer** β il primary inizia la connessione verso i secondari (endpoint pubblici fissi) con `PersistentKeepalive`, mantenendo aperto il percorso attraverso il NAT
- **Subnet dedicata** `10.99.0.0/24` β gli IP del tunnel diventano gli indirizzi che BIND usa per il transfer
- Chiavi generate per host e distribuite automaticamente via `hostvars`
### DNSSEC
- **Inline signing automatico** β `dnssec-policy` BIND 9.20, zero intervento manuale
- **Ed25519** β algoritmo moderno, chiavi compatte e veloci
- **KSK** rotazione annuale, **ZSK** ogni 90 giorni β entrambe automatiche
- **NSEC3** con `iterations=0` (RFC 9276)
- Compatibile con zone DDNS
### DDNS β Router OpenWrt
- Aggiornamento record A tramite `nsupdate` con TSIG
- Rilevamento automatico CGNAT
- Configurazione UCI automatizzata via Ansible
### Certificati ACME
- **acme.sh** con DNS-01 challenge via `nsupdate`
- Certificati **wildcard** `*.example.com` + root
- Rinnovo automatico via cron
### Hardening OS
- SSH con cifrari moderni (chacha20, AES-GCM, curve25519)
- Sysctl kernel: anti-spoofing, TCP syncookies, ASLR, kptr_restrict
- Filesystem: `/tmp`, `/var/tmp`, `/dev/shm` con `noexec,nosuid,nodev`
- auditd con regole CIS (identity, network, file DNS, syscall critiche)
- sudo con log completo, requiretty, timeout 5 min
- rkhunter scan notturno + unattended-upgrades solo sicurezza
- MOTD dinamico con stato BIND, nftables e IP bannati
### Firewall nftables
- Primary: porta 53 solo da localhost e secondari
- Secondari: rate limiting per IP, ban automatico set dinamico `dns_flood`
- Anti-amplification: throttle risposte UDP > 512B in OUTPUT
- Anti-spoofing bogon su tabella raw, bypass conntrack UDP/53
- fail2ban integrato con nftables
### Monitoring
- Prometheus + bind_exporter + node_exporter su tutti i nodi
- Gli exporter dei secondari sono raggiunti dal primary **via tunnel WireGuard** (non esposti su internet)
- Grafana 13 con dashboard DNS overview + system health
- Alertmanager con 14 alert preconfigurati (email/webhook)
- Alert: BIND down, zone transfer failure, DDoS detection, DNSSEC key expiry
- Grafana ascolta solo su `127.0.0.1`: accesso via tunnel SSH (vedi sezione Accesso)
### CI/CD
- ansible-lint profilo `production` + yamllint
- Molecule con driver Docker (Debian Trixie) + verify assertions
- GitHub Actions: lint + syntax + molecule + trivy + release automatica
---
## Prerequisiti
### Controller Ansible
```bash
python3 --version # 3.10+
pip install \
ansible-core>=2.16 \
ansible-lint \
yamllint \
molecule \
molecule-docker
ansible-galaxy collection install -r requirements.yml
```
### Proxmox VE
- Versione 7.x o 8.x
- Utente API con token (vedi [Configurazione token API](#configurazione-token-api-proxmox))
- Storage con supporto snippet cloud-init abilitato
- Accesso SSH al nodo Proxmox per la creazione del template
- ConnettivitΓ di rete tra VM primary e VPS secondari (porta 53 TCP)
### Server DNS
- **OS**: Debian Trixie (13)
- **Primary**: 2 vCPU, 2GB RAM, 40GB disco (VM Proxmox)
- **Secondari**: 1 vCPU, 512MB RAM, 10GB (VPS pubblici β OVH, Hetzner, Contabo, ecc.)
### Router OpenWrt
- OpenWrt 23.x o superiore
- `opkg install bind-client`
---
## Struttura del progetto
```
ansible-dns/
βββ .ansible-lint
βββ .yamllint
βββ .gitignore
βββ ansible.cfg
βββ requirements.yml # community.general, ansible.posix, community.proxmox
βββ README.md
βββ LICENSE
βββ CHANGELOG.md
β
βββ inventory/
β βββ hosts.yml # inventory reale (escluso da git)
β βββ hosts.yml.example # template inventory (committato)
β βββ group_vars/
β βββ all/
β βββ main.yml # configurazione globale (escluso da git)
β βββ main.yml.example # template configurazione (committato)
β βββ vault.yml.example # template secret (committato)
β βββ vault.yml # secret reali cifrati (escluso da git)
β
βββ zones/
β βββ example.com.yml
β βββ dyn.example.com.yml
β βββ 203.0.113.reverse.yml
β
βββ roles/
β βββ proxmox_vm/ # β NUOVO: provisioning VM su Proxmox
β β βββ defaults/main.yml
β β βββ tasks/
β β β βββ main.yml
β β β βββ cloudinit_snippet.yml
β β β βββ clone_vm.yml
β β β βββ configure_vm.yml
β β β βββ start_and_wait.yml
β β β βββ snapshot.yml
β β βββ templates/
β β βββ cloudinit-user-data.yml.j2
β β βββ cloudinit-network-config.yml.j2
β βββ packages/
β βββ hardening/
β β βββ tasks/
β β βββ main.yml
β β βββ user.yml
β β βββ ssh.yml
β β βββ sysctl.yml
β β βββ filesystem.yml
β β βββ services.yml
β β βββ sudo.yml
β β βββ auditd.yml
β β βββ banner.yml
β β βββ fail2ban.yml
β β βββ rkhunter.yml
β β βββ unattended_upgrades.yml
β βββ nftables/
β βββ wireguard/ # tunnel cifrato primary <-> secondari
β βββ bind9_primary/
β βββ bind9_secondary/
β βββ dnssec/
β βββ acme_dns/
β βββ ddns_openwrt/
β βββ monitoring/
β
βββ playbooks/
β βββ proxmox.yml # β NUOVO: provisioning VM primary
β βββ proxmox-prepare-template.yml # β NUOVO: crea template Debian Trixie
β βββ proxmox-snapshot.yml # β NUOVO: gestione snapshot
β βββ site.yml # deploy DNS completo
β βββ update-zones.yml # aggiorna zone con serial auto
β βββ renew-certs.yml
β βββ dnssec-status.yml
β
βββ molecule/
β βββ default/
β
βββ .github/
βββ workflows/
βββ ci.yml
βββ release.yml
```
---
## Proxmox β Provisioning VM
### Configurazione token API Proxmox
1. Accedi all'interfaccia web Proxmox (`https://proxmox.lan:8006`)
2. Vai in **Datacenter β Permissions β API Tokens β Add**
3. Configura:
```
User: ansible@pam
Token ID: ansible
Privilege Separation: NO β importante
```
4. Copia il **Token Secret** mostrato (visibile solo una volta)
5. Aggiungi i permessi necessari:
```
Datacenter β Permissions β Add β API Token Permission
Path: /
Token: ansible@pam!ansible
Role: PVEVMAdmin
Propagate: β
```
6. Salva il secret nel vault:
```bash
ansible-vault edit inventory/group_vars/all/vault.yml
# Aggiorna: vault_proxmox_token_secret: "il-tuo-token-secret"
```
### Abilitare lo storage snippets
Lo storage `local` su Proxmox deve avere i **Content: Snippets** abilitati:
1. **Datacenter β Storage β local β Edit**
2. **Content**: aggiungi `Snippets`
3. Salva
### Requisiti VM Proxmox consigliati
| Risorsa | Minimo | Consigliato | Note |
|---|---|---|---|
| CPU | 1 vCPU | **2 vCPU** | `host` passthrough per AES-NI |
| RAM | 1 GB | **2 GB** | Grafana (~300MB) + Prometheus (~200MB) |
| Disco | 20 GB | **40 GB** | 15GB `/` + 20GB `/var` (Prometheus) |
| Rete | 1 NIC | 1 NIC LAN | Bridge `vmbr0`, IP statico |
| Macchina | q35 | q35 | UEFI + TPM opzionale |
| BIOS | OVMF | OVMF | UEFI |
> **Nota disco**: il role hardening configura automaticamente `/tmp`, `/var/tmp` e `/dev/shm` come `tmpfs noexec`. Per separare `/var` (consigliato per Prometheus), aggiungi un secondo disco nella VM e configura il mount prima del deploy.
### Step 1 β Crea il template Debian Trixie
Il playbook si connette **via SSH al nodo Proxmox**, scarica l'immagine cloud Debian Trixie, installa `qemu-guest-agent` e la converte in template:
```bash
# Prima aggiungi il nodo Proxmox all'inventory
# inventory/hosts.yml:
# all:
# hosts:
# proxmox.lan:
# ansible_user: root
ansible-playbook playbooks/proxmox-prepare-template.yml --ask-vault-pass
```
Il playbook Γ¨ **idempotente**: se il template VMID 9000 esiste giΓ , non fa nulla.
**Cosa viene creato:**
- Template VMID `9000`, nome `debian-trixie-cloudinit`
- Immagine ottimizzata con `virt-customize`: qemu-guest-agent, cloud-init, python3
- Machine type q35, CPU host, VirtIO, drive cloud-init
### Step 2 β Provisioning VM primary
```bash
ansible-playbook playbooks/proxmox.yml --ask-vault-pass
```
**Flusso completo:**
```
1. Verifica esistenza template (VMID 9000)
2. Genera snippet cloud-init (user-data + network-config)
3. Carica snippet su Proxmox storage
4. Clona template β VM (VMID 200, clone completo)
5. Configura hardware:
- CPU: host passthrough, 2 core
- RAM: 2048MB, balloon disabilitato
- Disco: ridimensiona a 40GB
- Rete: VirtIO su vmbr0
- Tags: dns, primary, ansible-managed
6. Configura cloud-init:
- IP statico, gateway, DNS
- Utente ansible con chiave SSH
- Snippet custom con pacchetti
7. Avvia VM e attende SSH (120s timeout)
8. Attende completamento cloud-init
9. Verifica qemu-guest-agent via API
10. Crea snapshot baseline "post-cloudinit-base"
11. Stampa checklist VPS secondari
```
### Step 3 β Gestione snapshot
```bash
# Lista tutti gli snapshot
ansible-playbook playbooks/proxmox-snapshot.yml \
--ask-vault-pass -e "snap_action=list"
# Crea snapshot manuale (prima del deploy DNS)
ansible-playbook playbooks/proxmox-snapshot.yml \
--ask-vault-pass -e "snap_action=create snap_name=pre-deploy-dns"
# Rollback (con conferma interattiva)
ansible-playbook playbooks/proxmox-snapshot.yml \
--ask-vault-pass -e "snap_action=rollback snap_name=pre-deploy-dns"
# Elimina snapshot
ansible-playbook playbooks/proxmox-snapshot.yml \
--ask-vault-pass -e "snap_action=delete snap_name=pre-deploy-dns"
```
### Configurazione Proxmox (`inventory/group_vars/all/main.yml`)
```yaml
# --- Connessione API ---
proxmox_host: "proxmox.lan" # IP o hostname Proxmox
proxmox_user: "ansible@pam"
proxmox_token_id: "ansible"
proxmox_node: "pve" # nome nodo (pvesh nodes)
# --- Template ---
proxmox_template_vmid: 9000
proxmox_storage: "local-lvm"
proxmox_iso_storage: "local" # deve avere Content: Snippets
proxmox_bridge: "vmbr0"
# --- VM Primary ---
proxmox_primary_vmid: 200
proxmox_primary_name: "dns-primary"
proxmox_primary_cores: 2
proxmox_primary_memory: 2048
proxmox_primary_disk_size: "40G"
proxmox_primary_ip: "192.168.1.10"
proxmox_primary_gw: "192.168.1.1"
proxmox_primary_netmask: "24"
# --- Cloud-init ---
cloudinit_user: "ansible"
cloudinit_timezone: "Europe/Rome"
cloudinit_ssh_authorized_keys:
- "ssh-ed25519 AAAA... tua-chiave-pubblica"
```
---
---
## OVH β VM secondarie
Le VM OVH funzionano come **secondari pubblici** insieme (o al posto) di Hetzner/Contabo. I ruoli `bind9_secondary`, `nftables`, `hardening`, `packages` e `monitoring` non dipendono da Proxmox: agiscono su qualsiasi Debian Trixie raggiungibile via SSH.
> **Provisioning**: a differenza del primary su Proxmox (creazione VM automatizzata via API), le VM OVH vengono ordinate manualmente dal pannello OVH. Ansible automatizza solo la configurazione successiva. Per OVH Public Cloud (OpenStack) Γ¨ teoricamente possibile automatizzare anche la creazione con la collection `openstack.cloud`, ma non Γ¨ incluso in questo progetto.
### 1. Ordina la VM OVH
Prodotti adatti come secondario DNS:
- **OVH VPS** (da ~3,50β¬/mese) β sufficiente: 1 vCPU, 2GB RAM
- **OVH Public Cloud** (istanze a consumo)
- **OVH Bare Metal / Eco** (overkill per un secondario, ma valido)
Durante l'ordine seleziona **Debian Trixie (13)** come sistema operativo e carica la tua **chiave SSH pubblica**.
### 2. Configura il firewall OVH (Edge Network Firewall)
Questo Γ¨ il punto piΓΉ importante e specifico di OVH. L'Edge Network Firewall di OVH ha tre caratteristiche che vanno comprese:
- Γ **stateless** e integrato nell'infrastruttura Anti-DDoS: filtra solo il traffico proveniente da **fuori** dalla rete OVH. Il traffico interno OVH raggiunge comunque il server su qualsiasi porta.
- **Non sostituisce** il firewall a livello server: per questo il role `nftables` resta indispensabile (protegge anche dal traffico interno OVH e applica rate limiting + anti-amplification).
- La logica delle **prioritΓ Γ¨ invertita**: numeri piΓΉ bassi hanno prioritΓ piΓΉ alta, e serve **sempre** una regola finale di blocco esplicita, altrimenti le sole regole di autorizzazione sono inefficaci.
Configurazione consigliata nell'Edge Network Firewall (pannello OVH β IP β firewall):
| PrioritΓ | Azione | Protocollo | Porta | Opzione | Note |
|---|---|---|---|---|---|
| 0 | Authorize | TCP | 22 | β | SSH (meglio se da IP fisso) |
| 1 | Authorize | UDP | 53 | β | query DNS |
| 2 | Authorize | TCP | 53 | β | query DNS grandi + AXFR |
| 3 | Authorize | TCP | β | established | risposte sessioni TCP |
| 4 | Authorize | ICMP | β | β | ping / traceroute |
| 19 | Deny | IPv4 | β | β | **blocco finale obbligatorio** |
> Essendo stateless, il firewall OVH non tiene traccia delle connessioni: la regola `TCP established` (prioritΓ 3) Γ¨ necessaria per le risposte. Per il DNS su UDP non serve, perchΓ© ogni pacchetto Γ¨ indipendente.
> **Attenzione Anti-DDoS**: durante un attacco la mitigazione automatica OVH puΓ² temporaneamente limitare il traffico DNS verso la VM. Avere piΓΉ secondari su provider diversi (OVH + Hetzner + ...) mitiga questo rischio: se un secondario Γ¨ sotto mitigazione, gli altri continuano a rispondere.
### 3. Aggiungi la VM all'inventory
```yaml
# inventory/hosts.yml
dns_secondary:
hosts:
ns1:
ansible_host: 203.0.113.10 # es. Hetzner
ansible_user: ansible
dns_secondary_index: 1
ns2-ovh:
ansible_host: 51.91.x.x # IP pubblico VM OVH
ansible_user: ansible
dns_secondary_index: 2
```
```yaml
# inventory/group_vars/all/main.yml
dns_secondary_ips:
- "203.0.113.10"
- "51.91.x.x" # VM OVH
```
### 4. Deploy
```bash
ansible-playbook playbooks/site.yml --limit dns_secondary --ask-vault-pass
```
### 5. Verifica
```bash
# Query diretta alla VM OVH
dig @51.91.x.x example.com SOA
# Verifica che il firewall nftables del server sia attivo
ansible ns2-ovh -m command -a "nft list ruleset" --ask-vault-pass
# Verifica zone transfer ricevuto dal primary
ansible ns2-ovh -m command -a "rndc zonestatus example.com" --ask-vault-pass
```
### Primary su OVH (sconsigliato ma possibile)
Se vuoi mettere anche il **primary** su OVH rinunciando all'hidden primary locale, funziona ma cambia il modello di sicurezza: il primary diventa raggiungibile da internet. In tal caso:
- Le regole nftables del role primary giΓ limitano la porta 53 a localhost e secondari
- Apri nell'Edge Firewall OVH solo SSH + la porta 53 verso gli IP dei secondari
- Perdi il vantaggio principale dell'architettura hidden primary (master non esposto)
L'approccio consigliato resta: **primary locale su Proxmox** + **secondari pubblici su OVH/Hetzner/ecc.**
## Setup iniziale
### 1. Clona il repository
```bash
git clone https://github.com/mikysal78/ansible-dns.git
cd ansible-dns
pip install ansible-core>=2.16
ansible-galaxy collection install -r requirements.yml
```
### 2. Genera le chiavi TSIG
```bash
# Chiave per trasferimenti zona (AXFR)
tsig-keygen -a hmac-sha256 axfr-key
# Chiave per DDNS (router OpenWrt + acme.sh)
tsig-keygen -a hmac-sha256 ddns-key
```
### 3. Configura il vault
Copia il template `vault.yml.example` e compilalo con i valori reali:
```bash
cp inventory/group_vars/all/vault.yml.example inventory/group_vars/all/vault.yml
# Modifica con i tuoi secret (TSIG, password, token Proxmox)
$EDITOR inventory/group_vars/all/vault.yml
# Cifra il file (non sarΓ mai committato in chiaro grazie a .gitignore)
ansible-vault encrypt inventory/group_vars/all/vault.yml
```
Le chiavi richieste sono documentate in `vault.yml.example`:
`vault_tsig_secret`, `vault_ddns_secret`, `vault_acme_email`,
`vault_grafana_admin_password`, `vault_alertmanager_smtp_password`,
`vault_proxmox_token_secret`.
### 4. Configura l'inventory
```yaml
# inventory/hosts.yml
all:
children:
dns_primary:
hosts:
ns-primary:
ansible_host: 192.168.1.10
ansible_user: ansible
dns_secondary:
hosts:
ns1:
ansible_host: 203.0.113.10
ansible_user: ansible
ns2:
ansible_host: 203.0.113.20
ansible_user: ansible
openwrt_routers:
hosts:
router-home:
ansible_host: 192.168.1.1
ansible_user: root
ddns_hostname: "router-home.dyn.example.com"
```
### 5. Aggiungi la chiave SSH pubblica
```yaml
# inventory/group_vars/all/main.yml
cloudinit_ssh_authorized_keys:
- "ssh-ed25519 AAAA... tua-chiave"
hardening_ssh_authorized_keys:
- key: "ssh-ed25519 AAAA... tua-chiave"
user: ansible
```
---
## Configurazione
### Variabili principali (`inventory/group_vars/all/main.yml`)
| Variabile | Default | Descrizione |
|---|---|---|
| `dns_domain_base` | `example.com` | Dominio principale |
| `dns_primary_ip` | `192.168.1.10` | IP privato hidden primary |
| `dns_secondary_ips` | lista | IP VPS secondari pubblici |
| `dns_tsig_key_name` | `axfr-key` | Nome chiave TSIG AXFR |
| `ddns_zone` | `dyn.example.com` | Zona record DDNS |
| `proxmox_host` | `proxmox.lan` | Host Proxmox |
| `proxmox_primary_vmid` | `200` | VMID VM primary |
| `proxmox_primary_ip` | `192.168.1.10` | IP statico VM |
| `prometheus_retention` | `30d` | Retention dati Prometheus |
| `alertmanager_smtp_enabled` | `false` | Abilita notifiche email |
### Formato zone YAML
```yaml
zone:
name: "example.com"
ttl: 3600
soa:
primary_ns: "ns1.example.com."
admin: "hostmaster.example.com."
ns:
- "ns1.example.com."
- "ns2.example.com."
a:
- { name: "@", ip: "203.0.113.10" }
- { name: "www", ip: "203.0.113.10" }
- { name: "mail", ip: "203.0.113.10" }
mx:
- { priority: 10, host: "mail.example.com." }
txt:
- { name: "@", value: "v=spf1 mx a ~all" }
- { name: "_dmarc", value: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com" }
caa:
- { name: "@", flag: 0, tag: "issue", value: "letsencrypt.org" }
- { name: "@", flag: 0, tag: "issuewild", value: "letsencrypt.org" }
```
---
## Deploy DNS
### Flusso completo consigliato
```bash
# 1. Crea template Debian Trixie su Proxmox (una volta sola)
ansible-playbook playbooks/proxmox-prepare-template.yml --ask-vault-pass
# 2. Provisioning VM primary
ansible-playbook playbooks/proxmox.yml --ask-vault-pass
# 3. Snapshot pre-deploy (sicurezza)
ansible-playbook playbooks/proxmox-snapshot.yml \
--ask-vault-pass -e "snap_action=create snap_name=pre-deploy-dns"
# 4. Deploy infrastruttura DNS completa
ansible-playbook playbooks/site.yml --ask-vault-pass
# 5. Verifica e pubblica DS record DNSSEC presso il registrar
ansible-playbook playbooks/dnssec-status.yml --ask-vault-pass
```
### Opzioni deploy parziale
```bash
# Solo primary
ansible-playbook playbooks/site.yml --limit dns_primary --ask-vault-pass
# Solo secondari
ansible-playbook playbooks/site.yml --limit dns_secondary --ask-vault-pass
# Solo hardening
ansible-playbook playbooks/site.yml --tags hardening --ask-vault-pass
# Dry run
ansible-playbook playbooks/site.yml --check --diff --ask-vault-pass
```
### Ordine roles in `site.yml`
```
packages β hardening β nftables β bind9_primary β dnssec β acme_dns β monitoring
β bind9_secondary (sui secondari)
```
---
## Tunnel WireGuard
Il primary Γ¨ un **hidden master in LAN dietro NAT**, senza IP pubblico. I secondari sono su VPS pubblici. Senza un percorso tra i due, l'AXFR non potrebbe funzionare (un IP privato non Γ¨ instradabile su internet). La soluzione Γ¨ un tunnel WireGuard cifrato.
### Topologia
Il primary (dietro NAT) **inizia** la connessione verso i secondari, che hanno endpoint pubblici fissi. `PersistentKeepalive` tiene aperto il percorso attraverso il NAT. Una volta su, il tunnel Γ¨ bidirezionale e AXFR/NOTIFY ci viaggiano dentro cifrati.
```
Primary (NAT) Secondari (IP pubblici)
10.99.0.1 ββconnetteβββΊ 10.99.0.2 (ns1, ascolta :51820)
ββconnetteβββΊ 10.99.0.3 (ns2, ascolta :51820)
keepalive 25s mantiene aperti i buchi NAT
```
### Configurazione
Ogni host DNS ha un indirizzo nel tunnel, assegnato nell'inventory:
```yaml
# inventory/hosts.yml
ns-primary:
ansible_host: 10.0.0.14
wg_address: 10.99.0.1 # IP nel tunnel
ns1:
ansible_host: 203.0.113.10
wg_address: 10.99.0.2
ns2:
ansible_host: 203.0.113.20
wg_address: 10.99.0.3
```
Gli IP DNS usati per il transfer puntano al tunnel:
```yaml
# inventory/group_vars/all/main.yml
dns_primary_ip: "10.99.0.1"
dns_secondary_ips:
- "10.99.0.2"
- "10.99.0.3"
```
Il play WireGuard in `site.yml` gira su primary e secondari **insieme**, perchΓ© il template ha bisogno delle chiavi pubbliche di tutti gli host (via `hostvars`).
### Verifica
```bash
# handshake attivo con entrambi i peer?
ssh -p 2400 root@10.0.0.14 "wg show"
# il primary raggiunge i secondari nel tunnel?
ssh -p 2400 root@10.0.0.14 "ping -c2 10.99.0.2"
# AXFR funziona via tunnel?
ssh -p 2400 root@10.0.0.14 "dig @127.0.0.1 example.com AXFR | head"
```
---
## Gestione zone
### Aggiornamento con serial automatico
Il playbook calcola il serial nel formato `YYYYMMDDnn`:
- Data odierna + serial esistente β incrementa `nn`
- Data passata β `YYYYMMDD01`
- Aggiorna **solo le zone cambiate** (diff prima del deploy)
- Verifica sintassi con `named-checkzone`
- Controlla propagazione sui secondari
```bash
# Tutte le zone
ansible-playbook playbooks/update-zones.yml --ask-vault-pass
# Zona specifica
ansible-playbook playbooks/update-zones.yml --ask-vault-pass \
-e "zone_name=example.com"
# Forza reload
ansible-playbook playbooks/update-zones.yml --ask-vault-pass \
-e "force_serial=true"
```
### Aggiungere una zona
1. Crea `zones/nuova-zona.com.yml`
2. Aggiungi in `inventory/group_vars/all/main.yml`:
```yaml
dns_zones:
- name: "nuova-zona.com"
file: "zones/nuova-zona.com.yml"
type: master
ddns_enabled: false
```
3. `ansible-playbook playbooks/update-zones.yml --ask-vault-pass`
---
## DNSSEC
### Configurazione automatica
BIND 9.20 con `dnssec-policy` gestisce tutto:
| Parametro | Valore | Note |
|---|---|---|
| Algoritmo | Ed25519 | Chiavi da 32 byte |
| KSK lifetime | 1 anno | Rotazione automatica |
| ZSK lifetime | 90 giorni | Rotazione automatica |
| NSEC3 iterations | 0 | RFC 9276 |
| Signature validity | 14 giorni | Rinnovo 3gg prima |
### Pubblicazione DS record
```bash
ansible-playbook playbooks/dnssec-status.yml --ask-vault-pass
# Output: DS record da incollare nel pannello del registrar
```
### Verifica
```bash
# Firma locale
dig +dnssec example.com SOA @ns1.example.com
# Chain of trust end-to-end
delv @8.8.8.8 example.com SOA +rtrace
# Stato DNSSEC
ansible dns_primary -m command \
-a "rndc dnssec -status example.com" --ask-vault-pass
```
---
## DDNS β Router OpenWrt
Il router aggiorna automaticamente il proprio record A ogni 5 minuti:
```
router-home.dyn.example.com. 60 IN A
```
```yaml
# inventory/hosts.yml
openwrt_routers:
hosts:
router-home:
ansible_host: 192.168.1.1
ansible_user: root
ddns_hostname: "router-home.dyn.example.com"
ddns_interface: "wan"
```
```bash
ansible-playbook playbooks/site.yml --limit openwrt_routers --ask-vault-pass
```
Lo script rileva automaticamente CGNAT e ottiene l'IP pubblico reale.
---
## Monitoring
### Accesso via SSH tunnel
Grafana, Prometheus e Alertmanager ascoltano solo su `127.0.0.1` del primary.
Il role hardening abilita `PermitOpen` solo per le porte di monitoraggio, così
il forwarding Γ¨ ristretto a queste e nient'altro.
```bash
ssh -p 2400 \
-L 9090:127.0.0.1:9090 \
-L 3000:127.0.0.1:3000 \
-L 9093:127.0.0.1:9093 \
root@10.0.0.14
# Grafana: http://localhost:3000
# Prometheus: http://localhost:9090
# Alertmanager: http://localhost:9093
```
> Login Grafana: utente `admin`, password dal vault (`vault_grafana_admin_password`).
### Alert preconfigurati
| Alert | SeveritΓ | Condizione |
|---|---|---|
| `BINDDown` | critical | bind_exporter non risponde 2+ min |
| `BINDQueryRateCritical` | critical | > 20.000 query/s |
| `BINDSerialMismatch` | warning | serial non allineato tra nodi |
| `DNSSECKeyExpiredCritical` | critical | chiave DNSSEC scade in < 24h |
| `NodeDown` | critical | server non raggiungibile |
| `DiskSpaceCritical` | critical | < 5% spazio libero |
| `NTPOffsetHigh` | warning | offset > 100ms |
| *(+7 altri)* | | |
### Notifiche email
```yaml
# inventory/group_vars/all/main.yml
alertmanager_smtp_enabled: true
alertmanager_smtp_host: "smtp.gmail.com:587"
alertmanager_smtp_from: "alerts@example.com"
alertmanager_smtp_to: "admin@example.com"
```
```yaml
# inventory/group_vars/all/vault.yml
vault_alertmanager_smtp_password: "app_password"
```
---
## Hardening
### Moduli attivi
| Modulo | Descrizione |
|---|---|
| `user` | Crea utente ansible, carica chiavi SSH, blocca root |
| `ssh` | chacha20/AES-GCM, curve25519, no password auth |
| `sysctl` | 25+ parametri kernel hardening |
| `filesystem` | `/tmp` noexec, no core dump, permessi file sensibili |
| `services` | Disabilita 10+ demoni inutili |
| `sudo` | Log completo, use_pty, requiretty |
| `auditd` | Regole CIS per file DNS, syscall critiche |
| `banner` | Banner pre-login + MOTD dinamico |
| `fail2ban` | SSH jail + DNS flood jail con nftables |
| `rkhunter` | Scan notturno, aggiornamento DB settimanale |
| `unattended_upgrades` | Solo patch sicurezza, bind9 in blacklist |
---
## Firewall nftables
### Primary (hidden master)
```
INPUT: loopback, established, ICMP rate-limited, SSH, UDP/53 solo secondari+localhost
OUTPUT: throttle UDP > 512B per IP (anti-amplification)
RAW: anti-spoofing bogon, bypass conntrack UDP/53
```
### Secondari (VPS pubblici)
```
INPUT: UDP/53 pubblico rate-limited (30pps/IP, ban 120s), TCP/53 rate-limited,
AXFR TCP illimitato dal primary, SSH rate-limited
OUTPUT: throttle UDP > 512B (anti-amplification, set dinamico amp_targets)
```
### Comandi utili
```bash
# IP bannati per DNS flood
nft list set inet filter dns_flood
# Sblocca IP manualmente
nft delete element inet filter dns_flood { 1.2.3.4 }
# Ruleset completo
nft list ruleset
```
---
## Certificati ACME
```bash
# Rinnovo manuale
ansible-playbook playbooks/renew-certs.yml --ask-vault-pass
# Aggiungere dominio: roles/acme_dns/defaults/main.yml
acme_domains:
- domain: "example.com"
keylength: "ec-256"
- domain: "altro.com"
keylength: "ec-256"
```
---
## CI/CD
### Pipeline GitHub Actions
```
push β lint (yamllint + ansible-lint)
β syntax (tutti i playbook)
β molecule (Docker Debian Trixie: prepare β converge β idempotency β verify)
β validate-zones (valida YAML zone files)
β security (trivy CVE + verifica vault cifrato)
tag vX.Y.Z β release (archivio + changelog automatico)
```
### Test in locale
```bash
yamllint .
ansible-lint
ansible-playbook playbooks/site.yml --syntax-check \
-i inventory/hosts.yml -e @inventory/group_vars/all/main.yml \
-e "vault_tsig_secret=test vault_ddns_secret=test vault_proxmox_token_secret=test"
molecule test
```
---
## Operazioni giornaliere
```bash
# Stato BIND su tutti i nodi
ansible all -m command -a "systemctl status bind9" --ask-vault-pass
# Serial zone correnti
ansible dns_primary -m command \
-a "rndc zonestatus example.com" --ask-vault-pass
# Forza zone transfer sui secondari
ansible dns_secondary -m command \
-a "rndc retransfer example.com" --ask-vault-pass
# IP bannati per DNS flood
ansible dns_secondary -m command \
-a "nft list set inet filter dns_flood" --ask-vault-pass
# Stato DNSSEC + DS records
ansible-playbook playbooks/dnssec-status.yml --ask-vault-pass
# Snapshot prima di un'operazione rischiosa
ansible-playbook playbooks/proxmox-snapshot.yml \
--ask-vault-pass -e "snap_action=create snap_name=pre-manutenzione"
```
### Aggiornare BIND9
```bash
# Testa prima su un secondario
ansible ns1 -m apt -a "name=bind9 state=latest" --ask-vault-pass
ansible ns1 -m command -a "systemctl restart bind9" --ask-vault-pass
dig @203.0.113.10 example.com SOA
# Poi primary (crea snapshot prima)
ansible-playbook playbooks/proxmox-snapshot.yml \
--ask-vault-pass -e "snap_action=create snap_name=pre-bind9-upgrade"
ansible dns_primary -m apt -a "name=bind9 state=latest" --ask-vault-pass
ansible dns_primary -m command -a "systemctl restart bind9" --ask-vault-pass
# Infine gli altri secondari
ansible dns_secondary -m apt -a "name=bind9 state=latest" --ask-vault-pass
```
---
## Troubleshooting
### BIND9 non si avvia
```bash
journalctl -u bind9 -n 50 --no-pager
named-checkconf /etc/bind/named.conf
named-checkzone example.com /var/lib/bind/zones/db.example.com
```
### Zone transfer non funziona
```bash
dig @192.168.1.10 example.com AXFR
grep "transfer" /var/log/named/named.log
rndc retransfer example.com
```
### DNSSEC validation errors
```bash
rndc dnssec -status example.com
rndc sign example.com
dnssec-verify -z example.com /var/lib/bind/zones/db.example.com
```
### Certificati ACME non si rinnovano
```bash
/opt/acme.sh/acme.sh --renew -d example.com --force
cat /var/log/acme-renew.log
```
### Proxmox β clone fallisce
```bash
# Verifica che il template esista
qm status 9000
# Verifica permessi token API
pvesh get /access/acl
# Log Proxmox
journalctl -u pvedaemon -n 50
```
### Proxmox β cloud-init non applica IP
```bash
# Controlla lo status cloud-init sulla VM
ssh -p 2400 root@10.0.0.14 "cloud-init status"
ssh -p 2400 root@10.0.0.14 "cat /var/log/cloud-init.log | tail -30"
# Rigenera immagine cloud-init e riavvia
qm set 200 --cicustom ""
qm cloudinit update 200
qm reboot 200
```
### VM non risponde dopo creazione
```bash
# Verifica stato VM
qm status 200
# Console VM su Proxmox
qm terminal 200
# Log avvio
qm showcmd 200
```
---
## Sicurezza
### Gestione secret
Tutti i secret sono in `inventory/group_vars/all/vault.yml` cifrato con ansible-vault. Il file in chiaro non deve mai essere committato. Il `.gitignore` esclude il vault non cifrato.
```bash
# Verifica che il vault sia cifrato
head -1 inventory/group_vars/all/vault.yml
# Output atteso: $ANSIBLE_VAULT;1.1;AES256
```
### Rotazione chiavi TSIG
```bash
tsig-keygen -a hmac-sha256 axfr-key-new
ansible-vault edit inventory/group_vars/all/vault.yml
ansible-playbook playbooks/site.yml --ask-vault-pass
dig @192.168.1.10 example.com AXFR # verifica
```
### Rotazione token API Proxmox
1. Crea nuovo token in Proxmox
2. Aggiorna `vault_proxmox_token_secret` nel vault
3. Esegui `ansible-playbook playbooks/proxmox.yml --ask-vault-pass` per verificare
4. Revoca il vecchio token
---
## Licenza
MIT β vedi [LICENSE](LICENSE)
---
## Contribuire
1. Fork del repository
2. Branch: `git checkout -b feature/nome`
3. Test: `yamllint . && ansible-lint && molecule test`
4. Commit: `git commit -m "feat: descrizione"`
5. Pull request su `main`
I contributi devono passare l'intera pipeline CI prima del merge.