{"id":50804878,"url":"https://github.com/mikysal78/ansible-dns","last_synced_at":"2026-07-05T01:02:04.610Z","repository":{"id":363197946,"uuid":"1261746514","full_name":"mikysal78/ansible-dns","owner":"mikysal78","description":"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.","archived":false,"fork":false,"pushed_at":"2026-06-25T17:31:44.000Z","size":164,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-25T19:13:39.879Z","etag":null,"topics":["acme","ansible","bind9","ddns","debian","devops","dns","dnssec","grafana","infrastructure-as-code","nftables","openwrt","prometheus"],"latest_commit_sha":null,"homepage":"","language":"Jinja","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mikysal78.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-07T05:10:39.000Z","updated_at":"2026-06-25T17:31:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mikysal78/ansible-dns","commit_stats":null,"previous_names":["mikysal78/ansible-dns"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/mikysal78/ansible-dns","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikysal78%2Fansible-dns","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikysal78%2Fansible-dns/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikysal78%2Fansible-dns/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikysal78%2Fansible-dns/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mikysal78","download_url":"https://codeload.github.com/mikysal78/ansible-dns/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikysal78%2Fansible-dns/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35140189,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["acme","ansible","bind9","ddns","debian","devops","dns","dnssec","grafana","infrastructure-as-code","nftables","openwrt","prometheus"],"created_at":"2026-06-13T00:01:31.002Z","updated_at":"2026-07-05T01:02:04.572Z","avatar_url":"https://github.com/mikysal78.png","language":"Jinja","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🌐 ansible-dns — Infrastruttura DNS Professionale\n\n[![CI](https://github.com/mikysal78/ansible-dns/actions/workflows/ci.yml/badge.svg)](https://github.com/mikysal78/ansible-dns/actions/workflows/ci.yml)\n[![ansible-lint](https://img.shields.io/badge/ansible--lint-passing-brightgreen)](https://github.com/ansible/ansible-lint)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Debian Trixie](https://img.shields.io/badge/Debian-Trixie-red)](https://www.debian.org/)\n[![BIND9](https://img.shields.io/badge/BIND-9.20-blue)](https://www.isc.org/bind/)\n[![Proxmox](https://img.shields.io/badge/Proxmox-VE-orange)](https://www.proxmox.com/)\n\nPlaybook 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 con deploy automatico ai CT Proxmox, DDNS per router OpenWrt e monitoring con Prometheus/Grafana.\n\n---\n\n## 📋 Indice\n\n- [Architettura](#architettura)\n- [Funzionalità](#funzionalità)\n- [Prerequisiti](#prerequisiti)\n- [Struttura del progetto](#struttura-del-progetto)\n- [Makefile — comandi rapidi](#makefile--comandi-rapidi)\n- [Proxmox — Provisioning VM](#proxmox--provisioning-vm)\n- [OVH — VM secondarie](#ovh--vm-secondarie)\n- [Setup iniziale](#setup-iniziale)\n- [Configurazione](#configurazione)\n- [Deploy DNS](#deploy-dns)\n- [Tunnel WireGuard](#tunnel-wireguard)\n- [Gestione zone](#gestione-zone)\n- [DNSSEC](#dnssec)\n- [DDNS — Router OpenWrt](#ddns--router-openwrt)\n- [Monitoring](#monitoring)\n- [Hardening](#hardening)\n- [Firewall nftables](#firewall-nftables)\n- [Certificati ACME](#certificati-acme)\n- [CI/CD](#cicd)\n- [Operazioni giornaliere](#operazioni-giornaliere)\n- [Troubleshooting](#troubleshooting)\n- [Sicurezza](#sicurezza)\n\n---\n\n## Architettura\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                    PROXMOX VE (rete locale)                         │\n│                                                                     │\n│  ┌────────────────────────────────────────────────────────────┐    │\n│  │  VM dns-primary (VMID 200) — Debian Trixie                 │    │\n│  │  192.168.1.10 — 2 vCPU host — 2GB RAM — 40GB VirtIO       │    │\n│  │                                                            │    │\n│  │  • BIND9 Hidden Master (non esposto a internet)            │    │\n│  │  • DNSSEC inline signing (Ed25519, dnssec-policy)          │    │\n│  │  • acme.sh wildcard via DNS-01                             │    │\n│  │  • Prometheus + Grafana + Alertmanager                     │    │\n│  │  • fail2ban + nftables + auditd + rkhunter                 │    │\n│  └──────────────────────┬─────────────────────────────────────┘    │\n└─────────────────────────┼───────────────────────────────────────────┘\n                          │  Tunnel WireGuard cifrato (10.99.0.0/24)\n                          │  AXFR/IXFR (TSIG) + NOTIFY viaggiano qui dentro\n           ┌──────────────┼──────────────┬─────────────┐\n           ▼              ▼              ▼             ▼\n    ┌────────────┐ ┌────────────┐ ┌────────────┐  fino a 5\n    │ ns1 (VPS)  │ │ ns2 (VPS)  │ │ ns3 (VPS)  │  secondari\n    │Debian Trixie│ │Debian Trixie│ │Debian Trixie│\n    │ wg 10.99.0.2│ │ wg 10.99.0.3│ │ wg 10.99.0.4│\n    │ Query DNS  │ │ Query DNS  │ │ Query DNS  │\n    │ pubbliche  │ │ pubbliche  │ │ pubbliche  │\n    └────────────┘ └────────────┘ └────────────┘\n           ▲              ▲              ▲\n           └──────────────┼──────────────┘\n                    UDP/TCP 53 pubblico\n                    (rate limiting + anti-amplification)\n\n  Il primary (dietro NAT) inizia il tunnel verso i secondari (endpoint\n  pubblici); PersistentKeepalive mantiene aperto il percorso. Così il\n  primary resta nascosto e il transfer di zona è cifrato end-to-end.\n\n    ┌─────────────────────────────────────────┐\n    │  Router OpenWrt (DDNS)                  │\n    │  nsupdate TSIG → dyn.example.com        │\n    │  router-home.dyn.example.com → WAN IP   │\n    └─────────────────────────────────────────┘\n```\n\n---\n\n## Funzionalità\n\n### Proxmox VE\n- Provisioning VM primary via **API Proxmox** (`community.general.proxmox_kvm`)\n- Creazione automatica **template Debian Trixie** genericcloud con `virt-customize`\n- Clone template → VM con **cloud-init** (IP statico, utente, chiave SSH, pacchetti)\n- Hardware ottimizzato: `q35`, `UEFI`, `CPU host` (AES-NI + rdrand), VirtIO, balloon disabilitato\n- **Snapshot automatico** post-creazione come baseline pre-deploy\n- Playbook dedicati per gestione snapshot (crea, lista, rollback, elimina)\n\n### DNS Core\n- **Hidden Primary** — il master non è mai esposto a internet\n- **N secondari pubblici** — da 2 a 5+ VPS, configurazione automatizzata\n- **Zone in YAML** — formato leggibile con supporto a tutti i record professionali\n- **AXFR/IXFR autenticato** — chiave TSIG `hmac-sha256`\n- **Record supportati** — A, AAAA, CNAME, MX, TXT, SRV, CAA, TLSA, SSHFP, PTR\n\n### Tunnel WireGuard (primary ↔ secondari ↔ Proxmox)\n- **Trasferimento di zona cifrato** — AXFR/IXFR e NOTIFY viaggiano dentro un tunnel WireGuard, mai in chiaro su internet\n- **Primary dietro NAT** — pensato per il caso reale di un hidden primary in LAN (senza IP pubblico) e secondari su VPS cloud\n- **Topologia roaming peer** — il primary inizia la connessione verso i secondari (endpoint pubblici fissi) con `PersistentKeepalive`, mantenendo aperto il percorso attraverso il NAT\n- **Subnet dedicata** `10.99.0.0/24` — gli IP del tunnel diventano gli indirizzi che BIND usa per il transfer\n- Chiavi generate per host e distribuite automaticamente via `hostvars`\n- **Peer extra opzionali** (es. l'host Proxmox stesso, gruppo `proxmox` in inventory con `wg_address` impostato): stessa logica dei secondari (endpoint fisso + keepalive, il primary dialoga verso di loro), usati per far raggiungere BIND a client nsupdate/RFC2136 esterni sulla LAN senza mai esporre BIND stesso fuori da loopback+WireGuard — vedi [ACME built-in di Proxmox](#acme-built-in-di-proxmox-certificato-interfaccia-web) più sotto\n\n### DNSSEC\n- **Inline signing automatico** — `dnssec-policy` BIND 9.20, zero intervento manuale\n- **Ed25519** — algoritmo moderno, chiavi compatte e veloci\n- **KSK** rotazione annuale, **ZSK** ogni 90 giorni — entrambe automatiche\n- **NSEC3** con `iterations=0` (RFC 9276)\n- Compatibile con zone DDNS\n\n### DDNS — Router OpenWrt\n- Aggiornamento record A tramite `nsupdate` con TSIG\n- Rilevamento automatico CGNAT\n- Configurazione UCI automatizzata via Ansible\n\n### Certificati ACME\n- **acme.sh** con DNS-01 challenge via `nsupdate` (plugin ufficiale `dns_nsupdate`, RFC 2136)\n- Certificati **wildcard** `*.example.com` + root, versione acme.sh pinned\n- Rinnovo automatico via cron (02:30 ogni notte)\n- **Deploy automatico ai CT Proxmox**: il primary genera una chiave SSH ed25519 dedicata, la distribuisce ai CT consumer e copia i certificati rinnovati via rsync (porta configurabile)\n- Deploy **best-effort**: se un CT è irraggiungibile, il rinnovo sul primary non fallisce\n\n### ACME built-in di Proxmox (certificato interfaccia web)\nPer rinnovare il certificato di `pveproxy` (l'interfaccia web di Proxmox VE) con l'ACME nativo di Proxmox (non acme.sh), invece di aprire BIND sulla LAN si aggiunge l'host Proxmox come **peer WireGuard** — coerente con l'architettura hidden-primary, nessuna esposizione di BIND fuori da loopback+tunnel:\n\n1. In `inventory/hosts.yml`, il gruppo `proxmox` ha già `wg_address: 10.99.0.4` per l'host `pve`; `make deploy` (o il play \"Setup tunnel WireGuard\") genera le chiavi e configura il peer sia sul primary che su Proxmox.\n2. `ddns_allowed_sources` in `inventory/group_vars/all/main.yml` include `10.99.0.4` (l'IP del tunnel, non un IP di LAN): il firewall del primary accetta update DDNS/nsupdate da quell'indirizzo, autenticati comunque dalla chiave TSIG `ddns-key`.\n3. Configura il DNS plugin `nsupdate` di Proxmox (`Datacenter → ACME` oppure `pvenode acme plugin`) con:\n   - **Server**: `10.99.0.1` (IP WireGuard del primary, porta 53)\n   - **Key name**: `ddns-key`, **algoritmo**: `hmac-sha256`\n   - **Secret**: `ansible-vault view inventory/group_vars/all/vault.yml` (variabile `vault_ddns_secret`)\n4. Registra il dominio/hostname della GUI Proxmox come dominio ACME sull'host, con il plugin DNS appena creato.\n\nNota: `ddns-key` non è ristretta al solo record `_acme-challenge` — chi la possiede può aggiornare qualsiasi record delle zone DDNS. L'unica vera barriera aggiuntiva del peer WireGuard è l'autenticazione crittografica del tunnel stesso.\n\n### Hardening OS\n- SSH con cifrari moderni (chacha20, AES-GCM, curve25519)\n- Sysctl kernel: anti-spoofing, TCP syncookies, ASLR, kptr_restrict\n- Filesystem: `/tmp`, `/var/tmp`, `/dev/shm` con `noexec,nosuid,nodev`\n- auditd con regole CIS (identity, network, file DNS, syscall critiche)\n- sudo con log completo, requiretty, timeout 5 min\n- rkhunter scan notturno + unattended-upgrades solo sicurezza\n- MOTD dinamico con stato BIND, nftables e IP bannati\n\n### Firewall nftables\n- Primary: porta 53 solo da localhost e secondari\n- Secondari: rate limiting per IP, ban automatico set dinamico `dns_flood`\n- Anti-amplification: throttle risposte UDP \u003e 512B in OUTPUT\n- Anti-spoofing bogon su tabella raw, bypass conntrack UDP/53\n- fail2ban integrato con nftables\n\n### Monitoring\n- Prometheus + bind_exporter + node_exporter su tutti i nodi\n- Gli exporter dei secondari sono raggiunti dal primary **via tunnel WireGuard** (non esposti su internet)\n- Grafana 13 con dashboard DNS overview + system health\n- Alertmanager con 14 alert preconfigurati (email/webhook)\n- Alert: BIND down, zone transfer failure, DDoS detection, DNSSEC key expiry\n- Grafana ascolta solo su `127.0.0.1`: accesso via tunnel SSH (vedi sezione Accesso)\n\n### CI/CD\n- ansible-lint profilo `production` + yamllint\n- Molecule con driver Docker (Debian Trixie) + verify assertions\n- GitHub Actions: lint + syntax + molecule + trivy + release automatica\n\n---\n\n## Prerequisiti\n\n### Controller Ansible\n```bash\npython3 --version   # 3.10+\n\npip install \\\n  ansible-core\u003e=2.16 \\\n  ansible-lint \\\n  yamllint \\\n  molecule \\\n  molecule-docker\n\nansible-galaxy collection install -r requirements.yml\n```\n\n### Proxmox VE\n- Versione 7.x o 8.x\n- Utente API con token (vedi [Configurazione token API](#configurazione-token-api-proxmox))\n- Storage con supporto snippet cloud-init abilitato\n- Accesso SSH al nodo Proxmox per la creazione del template\n- Connettività di rete tra VM primary e VPS secondari (porta 53 TCP)\n\n### Server DNS\n- **OS**: Debian Trixie (13)\n- **Primary**: 2 vCPU, 2GB RAM, 40GB disco (VM Proxmox)\n- **Secondari**: 1 vCPU, 512MB RAM, 10GB (VPS pubblici — OVH, Hetzner, Contabo, ecc.)\n\n### Router OpenWrt\n- OpenWrt 23.x o superiore\n- `opkg install bind-client`\n\n---\n\n## Struttura del progetto\n\n```\nansible-dns/\n├── .ansible-lint\n├── .yamllint\n├── .gitignore\n├── ansible.cfg\n├── requirements.yml           # community.general, ansible.posix, community.proxmox\n├── README.md\n├── LICENSE\n├── CHANGELOG.md\n│\n├── inventory/\n│   ├── hosts.yml              # inventory reale (escluso da git)\n│   ├── hosts.yml.example      # template inventory (committato)\n│   └── group_vars/\n│       └── all/\n│           ├── main.yml           # configurazione globale (escluso da git)\n│           ├── main.yml.example   # template configurazione (committato)\n│           ├── vault.yml.example  # template secret (committato)\n│           └── vault.yml          # secret reali cifrati (escluso da git)\n│\n├── zones/\n│   ├── example.com.yml\n│   ├── dyn.example.com.yml\n│   └── 203.0.113.reverse.yml\n│\n├── roles/\n│   ├── proxmox_vm/            # ← NUOVO: provisioning VM su Proxmox\n│   │   ├── defaults/main.yml\n│   │   ├── tasks/\n│   │   │   ├── main.yml\n│   │   │   ├── cloudinit_snippet.yml\n│   │   │   ├── clone_vm.yml\n│   │   │   ├── configure_vm.yml\n│   │   │   ├── start_and_wait.yml\n│   │   │   └── snapshot.yml\n│   │   └── templates/\n│   │       ├── cloudinit-user-data.yml.j2\n│   │       └── cloudinit-network-config.yml.j2\n│   ├── packages/\n│   ├── hardening/\n│   │   └── tasks/\n│   │       ├── main.yml\n│   │       ├── user.yml\n│   │       ├── ssh.yml\n│   │       ├── sysctl.yml\n│   │       ├── filesystem.yml\n│   │       ├── services.yml\n│   │       ├── sudo.yml\n│   │       ├── auditd.yml\n│   │       ├── banner.yml\n│   │       ├── fail2ban.yml\n│   │       ├── rkhunter.yml\n│   │       └── unattended_upgrades.yml\n│   ├── nftables/\n│   ├── wireguard/             # tunnel cifrato primary \u003c-\u003e secondari\n│   ├── bind9_primary/\n│   ├── bind9_secondary/\n│   ├── dnssec/\n│   ├── acme_dns/\n│   ├── ddns_openwrt/\n│   └── monitoring/\n│\n├── Makefile                           # comandi rapidi (deploy, zones, acme, ...)\n├── playbooks/\n│   ├── proxmox.yml                    # provisioning VM primary\n│   ├── proxmox-prepare-template.yml   # crea template Debian Trixie\n│   ├── proxmox-snapshot.yml           # gestione snapshot\n│   ├── site.yml                       # deploy DNS completo\n│   ├── update-zones.yml               # aggiorna zone con serial auto\n│   ├── acme-only.yml                  # emissione cert + deploy ai CT\n│   ├── cert-deploy.yml                # copia cert dal primary ai CT (via control node)\n│   ├── renew-certs.yml                # rinnovo manuale forzato\n│   └── dnssec-status.yml\n│\n├── molecule/\n│   └── default/\n│\n└── .github/\n    └── workflows/\n        ├── ci.yml\n        └── release.yml\n```\n\n---\n\n## Makefile — comandi rapidi\n\nIl `Makefile` alla radice del progetto evita di ricordare i path dei playbook.\n\n```bash\nmake deploy        # deploy completo (site.yml)\nmake zones         # aggiorna solo le zone DNS\nmake acme          # emetti/rinnova cert e distribuiscili ai CT\nmake cert-deploy   # ricopia cert esistenti ai CT (senza re-emettere)\nmake renew         # rinnovo manuale forzato certificati ACME\nmake dnssec        # stato DNSSEC e prossime rotazioni chiavi\nmake vault-summary # riepilogo variabili vault\nmake ping          # verifica connettività a tutti gli host\nmake syntax        # syntax check di site.yml\nmake snapshot      # snapshot Proxmox del CT primary\n```\n\nDi default usa `--ask-vault-pass`. Per un file password:\n\n```bash\nmake deploy VAULT=\"--vault-password-file=/git/.vault_pass\"\n```\n\n---\n\n## Proxmox — Provisioning VM\n\n### Configurazione token API Proxmox\n\n1. Accedi all'interfaccia web Proxmox (`https://proxmox.lan:8006`)\n2. Vai in **Datacenter → Permissions → API Tokens → Add**\n3. Configura:\n   ```\n   User:       ansible@pam\n   Token ID:   ansible\n   Privilege Separation: NO  ← importante\n   ```\n4. Copia il **Token Secret** mostrato (visibile solo una volta)\n5. Aggiungi i permessi necessari:\n   ```\n   Datacenter → Permissions → Add → API Token Permission\n   Path:       /\n   Token:      ansible@pam!ansible\n   Role:       PVEVMAdmin\n   Propagate:  ✓\n   ```\n\n6. Salva il secret nel vault:\n   ```bash\n   ansible-vault edit inventory/group_vars/all/vault.yml\n   # Aggiorna: vault_proxmox_token_secret: \"il-tuo-token-secret\"\n   ```\n\n### Abilitare lo storage snippets\n\nLo storage `local` su Proxmox deve avere i **Content: Snippets** abilitati:\n\n1. **Datacenter → Storage → local → Edit**\n2. **Content**: aggiungi `Snippets`\n3. Salva\n\n### Requisiti VM Proxmox consigliati\n\n| Risorsa | Minimo | Consigliato | Note |\n|---|---|---|---|\n| CPU | 1 vCPU | **2 vCPU** | `host` passthrough per AES-NI |\n| RAM | 1 GB | **2 GB** | Grafana (~300MB) + Prometheus (~200MB) |\n| Disco | 20 GB | **40 GB** | 15GB `/` + 20GB `/var` (Prometheus) |\n| Rete | 1 NIC | 1 NIC LAN | Bridge `vmbr0`, IP statico |\n| Macchina | q35 | q35 | UEFI + TPM opzionale |\n| BIOS | OVMF | OVMF | UEFI |\n\n\u003e **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.\n\n### Step 1 — Crea il template Debian Trixie\n\nIl playbook si connette **via SSH al nodo Proxmox**, scarica l'immagine cloud Debian Trixie, installa `qemu-guest-agent` e la converte in template:\n\n```bash\n# Prima aggiungi il nodo Proxmox all'inventory\n# inventory/hosts.yml:\n#   all:\n#     hosts:\n#       proxmox.lan:\n#         ansible_user: root\n\nansible-playbook playbooks/proxmox-prepare-template.yml --ask-vault-pass\n```\n\nIl playbook è **idempotente**: se il template VMID 9000 esiste già, non fa nulla.\n\n**Cosa viene creato:**\n- Template VMID `9000`, nome `debian-trixie-cloudinit`\n- Immagine ottimizzata con `virt-customize`: qemu-guest-agent, cloud-init, python3\n- Machine type q35, CPU host, VirtIO, drive cloud-init\n\n### Step 2 — Provisioning VM primary\n\n```bash\nansible-playbook playbooks/proxmox.yml --ask-vault-pass\n```\n\n**Flusso completo:**\n\n```\n1. Verifica esistenza template (VMID 9000)\n2. Genera snippet cloud-init (user-data + network-config)\n3. Carica snippet su Proxmox storage\n4. Clona template → VM (VMID 200, clone completo)\n5. Configura hardware:\n   - CPU: host passthrough, 2 core\n   - RAM: 2048MB, balloon disabilitato\n   - Disco: ridimensiona a 40GB\n   - Rete: VirtIO su vmbr0\n   - Tags: dns, primary, ansible-managed\n6. Configura cloud-init:\n   - IP statico, gateway, DNS\n   - Utente ansible con chiave SSH\n   - Snippet custom con pacchetti\n7. Avvia VM e attende SSH (120s timeout)\n8. Attende completamento cloud-init\n9. Verifica qemu-guest-agent via API\n10. Crea snapshot baseline \"post-cloudinit-base\"\n11. Stampa checklist VPS secondari\n```\n\n### Step 3 — Gestione snapshot\n\n```bash\n# Lista tutti gli snapshot\nansible-playbook playbooks/proxmox-snapshot.yml \\\n  --ask-vault-pass -e \"snap_action=list\"\n\n# Crea snapshot manuale (prima del deploy DNS)\nansible-playbook playbooks/proxmox-snapshot.yml \\\n  --ask-vault-pass -e \"snap_action=create snap_name=pre-deploy-dns\"\n\n# Rollback (con conferma interattiva)\nansible-playbook playbooks/proxmox-snapshot.yml \\\n  --ask-vault-pass -e \"snap_action=rollback snap_name=pre-deploy-dns\"\n\n# Elimina snapshot\nansible-playbook playbooks/proxmox-snapshot.yml \\\n  --ask-vault-pass -e \"snap_action=delete snap_name=pre-deploy-dns\"\n```\n\n### Configurazione Proxmox (`inventory/group_vars/all/main.yml`)\n\n```yaml\n# --- Connessione API ---\nproxmox_host: \"proxmox.lan\"         # IP o hostname Proxmox\nproxmox_user: \"ansible@pam\"\nproxmox_token_id: \"ansible\"\nproxmox_node: \"pve\"                 # nome nodo (pvesh nodes)\n\n# --- Template ---\nproxmox_template_vmid: 9000\nproxmox_storage: \"local-lvm\"\nproxmox_iso_storage: \"local\"        # deve avere Content: Snippets\nproxmox_bridge: \"vmbr0\"\n\n# --- VM Primary ---\nproxmox_primary_vmid: 200\nproxmox_primary_name: \"dns-primary\"\nproxmox_primary_cores: 2\nproxmox_primary_memory: 2048\nproxmox_primary_disk_size: \"40G\"\nproxmox_primary_ip: \"192.168.1.10\"\nproxmox_primary_gw: \"192.168.1.1\"\nproxmox_primary_netmask: \"24\"\n\n# --- Cloud-init ---\ncloudinit_user: \"ansible\"\ncloudinit_timezone: \"Europe/Rome\"\ncloudinit_ssh_authorized_keys:\n  - \"ssh-ed25519 AAAA... tua-chiave-pubblica\"\n```\n\n---\n\n---\n\n## OVH — VM secondarie\n\nLe 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.\n\n\u003e **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.\n\n### 1. Ordina la VM OVH\n\nProdotti adatti come secondario DNS:\n- **OVH VPS** (da ~3,50€/mese) — sufficiente: 1 vCPU, 2GB RAM\n- **OVH Public Cloud** (istanze a consumo)\n- **OVH Bare Metal / Eco** (overkill per un secondario, ma valido)\n\nDurante l'ordine seleziona **Debian Trixie (13)** come sistema operativo e carica la tua **chiave SSH pubblica**.\n\n### 2. Configura il firewall OVH (Edge Network Firewall)\n\nQuesto è il punto più importante e specifico di OVH. L'Edge Network Firewall di OVH ha tre caratteristiche che vanno comprese:\n\n- È **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.\n- **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).\n- 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.\n\nConfigurazione consigliata nell'Edge Network Firewall (pannello OVH → IP → firewall):\n\n| Priorità | Azione | Protocollo | Porta | Opzione | Note |\n|---|---|---|---|---|---|\n| 0 | Authorize | TCP | 22 | — | SSH (meglio se da IP fisso) |\n| 1 | Authorize | UDP | 53 | — | query DNS |\n| 2 | Authorize | TCP | 53 | — | query DNS grandi + AXFR |\n| 3 | Authorize | TCP | — | established | risposte sessioni TCP |\n| 4 | Authorize | ICMP | — | — | ping / traceroute |\n| 19 | Deny | IPv4 | — | — | **blocco finale obbligatorio** |\n\n\u003e 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.\n\n\u003e **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.\n\n### 3. Aggiungi la VM all'inventory\n\n```yaml\n# inventory/hosts.yml\ndns_secondary:\n  hosts:\n    ns1:\n      ansible_host: 203.0.113.10        # es. Hetzner\n      ansible_user: ansible\n      dns_secondary_index: 1\n    ns2-ovh:\n      ansible_host: 51.91.x.x           # IP pubblico VM OVH\n      ansible_user: ansible\n      dns_secondary_index: 2\n```\n\n```yaml\n# inventory/group_vars/all/main.yml\ndns_secondary_ips:\n  - \"203.0.113.10\"\n  - \"51.91.x.x\"          # VM OVH\n```\n\n### 4. Deploy\n\n```bash\nansible-playbook playbooks/site.yml --limit dns_secondary --ask-vault-pass\n```\n\n### 5. Verifica\n\n```bash\n# Query diretta alla VM OVH\ndig @51.91.x.x example.com SOA\n\n# Verifica che il firewall nftables del server sia attivo\nansible ns2-ovh -m command -a \"nft list ruleset\" --ask-vault-pass\n\n# Verifica zone transfer ricevuto dal primary\nansible ns2-ovh -m command -a \"rndc zonestatus example.com\" --ask-vault-pass\n```\n\n### Primary su OVH (sconsigliato ma possibile)\n\nSe 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:\n- Le regole nftables del role primary già limitano la porta 53 a localhost e secondari\n- Apri nell'Edge Firewall OVH solo SSH + la porta 53 verso gli IP dei secondari\n- Perdi il vantaggio principale dell'architettura hidden primary (master non esposto)\n\nL'approccio consigliato resta: **primary locale su Proxmox** + **secondari pubblici su OVH/Hetzner/ecc.**\n\n\n## Setup iniziale\n\n### 1. Clona il repository\n\n```bash\ngit clone https://github.com/mikysal78/ansible-dns.git\ncd ansible-dns\npip install ansible-core\u003e=2.16\nansible-galaxy collection install -r requirements.yml\n```\n\n### 2. Genera le chiavi TSIG\n\n```bash\n# Chiave per trasferimenti zona (AXFR)\ntsig-keygen -a hmac-sha256 axfr-key\n\n# Chiave per DDNS (router OpenWrt + acme.sh)\ntsig-keygen -a hmac-sha256 ddns-key\n```\n\n### 3. Configura il vault\n\nCopia il template `vault.yml.example` e compilalo con i valori reali:\n\n```bash\ncp inventory/group_vars/all/vault.yml.example inventory/group_vars/all/vault.yml\n\n# Modifica con i tuoi secret (TSIG, password, token Proxmox)\n$EDITOR inventory/group_vars/all/vault.yml\n\n# Cifra il file (non sarà mai committato in chiaro grazie a .gitignore)\nansible-vault encrypt inventory/group_vars/all/vault.yml\n```\n\nLe chiavi richieste sono documentate in `vault.yml.example`:\n`vault_tsig_secret`, `vault_ddns_secret`, `vault_acme_email`,\n`vault_grafana_admin_password`, `vault_alertmanager_smtp_password`,\n`vault_proxmox_token_secret`.\n\n### 4. Configura l'inventory\n\n```yaml\n# inventory/hosts.yml\nall:\n  children:\n    dns_primary:\n      hosts:\n        ns-primary:\n          ansible_host: 192.168.1.10\n          ansible_user: ansible\n\n    dns_secondary:\n      hosts:\n        ns1:\n          ansible_host: 203.0.113.10\n          ansible_user: ansible\n        ns2:\n          ansible_host: 203.0.113.20\n          ansible_user: ansible\n\n    openwrt_routers:\n      hosts:\n        router-home:\n          ansible_host: 192.168.1.1\n          ansible_user: root\n          ddns_hostname: \"router-home.dyn.example.com\"\n```\n\n### 5. Aggiungi la chiave SSH pubblica\n\n```yaml\n# inventory/group_vars/all/main.yml\ncloudinit_ssh_authorized_keys:\n  - \"ssh-ed25519 AAAA... tua-chiave\"\n\nhardening_ssh_authorized_keys:\n  - key: \"ssh-ed25519 AAAA... tua-chiave\"\n    user: ansible\n```\n\n---\n\n## Configurazione\n\n### Variabili principali (`inventory/group_vars/all/main.yml`)\n\n| Variabile | Default | Descrizione |\n|---|---|---|\n| `dns_domain_base` | `example.com` | Dominio principale |\n| `dns_primary_ip` | `192.168.1.10` | IP privato hidden primary |\n| `dns_secondary_ips` | lista | IP VPS secondari pubblici |\n| `dns_tsig_key_name` | `axfr-key` | Nome chiave TSIG AXFR |\n| `ddns_zone` | `dyn.example.com` | Zona record DDNS |\n| `proxmox_host` | `proxmox.lan` | Host Proxmox |\n| `proxmox_primary_vmid` | `200` | VMID VM primary |\n| `proxmox_primary_ip` | `192.168.1.10` | IP statico VM |\n| `prometheus_retention` | `30d` | Retention dati Prometheus |\n| `alertmanager_smtp_enabled` | `false` | Abilita notifiche email |\n\n### Formato zone YAML\n\n```yaml\nzone:\n  name: \"example.com\"\n  ttl: 3600\n  soa:\n    primary_ns: \"ns1.example.com.\"\n    admin: \"hostmaster.example.com.\"\n  ns:\n    - \"ns1.example.com.\"\n    - \"ns2.example.com.\"\n  a:\n    - { name: \"@\",   ip: \"203.0.113.10\" }\n    - { name: \"www\", ip: \"203.0.113.10\" }\n    - { name: \"mail\", ip: \"203.0.113.10\" }\n  mx:\n    - { priority: 10, host: \"mail.example.com.\" }\n  txt:\n    - { name: \"@\",     value: \"v=spf1 mx a ~all\" }\n    - { name: \"_dmarc\", value: \"v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com\" }\n  caa:\n    - { name: \"@\", flag: 0, tag: \"issue\",     value: \"letsencrypt.org\" }\n    - { name: \"@\", flag: 0, tag: \"issuewild\",  value: \"letsencrypt.org\" }\n```\n\n---\n\n## Deploy DNS\n\n### Flusso completo consigliato\n\n```bash\n# 1. Crea template Debian Trixie su Proxmox (una volta sola)\nansible-playbook playbooks/proxmox-prepare-template.yml --ask-vault-pass\n\n# 2. Provisioning VM primary\nansible-playbook playbooks/proxmox.yml --ask-vault-pass\n\n# 3. Snapshot pre-deploy (sicurezza)\nansible-playbook playbooks/proxmox-snapshot.yml \\\n  --ask-vault-pass -e \"snap_action=create snap_name=pre-deploy-dns\"\n\n# 4. Deploy infrastruttura DNS completa\nansible-playbook playbooks/site.yml --ask-vault-pass\n\n# 5. Verifica e pubblica DS record DNSSEC presso il registrar\nansible-playbook playbooks/dnssec-status.yml --ask-vault-pass\n```\n\n### Opzioni deploy parziale\n\n```bash\n# Solo primary\nansible-playbook playbooks/site.yml --limit dns_primary --ask-vault-pass\n\n# Solo secondari\nansible-playbook playbooks/site.yml --limit dns_secondary --ask-vault-pass\n\n# Solo hardening\nansible-playbook playbooks/site.yml --tags hardening --ask-vault-pass\n\n# Dry run\nansible-playbook playbooks/site.yml --check --diff --ask-vault-pass\n```\n\n### Riepilogo a fine deploy\n\n`site.yml` termina con un play di riepilogo che mostra:\n\n- **INFRASTRUTTURA** — IP primary, NS1/NS2 pubblici, indirizzi WireGuard\n- **ZONE DNS** — zone attive con tipo e stato DDNS\n- **MONITORING** — URL Grafana/Prometheus/Alertmanager con credenziali e comando SSH tunnel pronto per il notebook\n- **MONITORING SMTP** — stato notifiche email/webhook\n- **CERTIFICATI ACME** — file per dominio e CT destinatari\n- **CT CONSUMER** — elenco CT con cert e reload command\n- **VAULT** — valori delle variabili cifrate\n\n```bash\n# nasconde i valori sensibili (utile su shell condivise o in CI)\nansible-playbook playbooks/site.yml --ask-vault-pass -e reveal_secrets=false\n```\n\n\u003e ⚠️ Con `reveal_secrets=true` (default) le password appaiono in chiaro nello stdout.\n\u003e Non eseguire con output visibile ad altri. Per ispezionare il vault:\n\u003e `ansible-vault view inventory/group_vars/all/vault.yml`.\n\n### Ordine roles in `site.yml`\n\n```\npackages → hardening → nftables → bind9_primary → dnssec → acme_dns → monitoring\n                                → bind9_secondary (sui secondari)\n```\n\n---\n\n## Tunnel WireGuard\n\nIl 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.\n\n### Topologia\n\nIl 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.\n\n```\nPrimary (NAT)              Secondari (IP pubblici)\n10.99.0.1   ──connette──►  10.99.0.2  (ns1, ascolta :51820)\n            ──connette──►  10.99.0.3  (ns2, ascolta :51820)\n            keepalive 25s mantiene aperti i buchi NAT\n```\n\n### Configurazione\n\nOgni host DNS ha un indirizzo nel tunnel, assegnato nell'inventory:\n\n```yaml\n# inventory/hosts.yml\nns-primary:\n  ansible_host: 10.0.0.14\n  wg_address: 10.99.0.1      # IP nel tunnel\nns1:\n  ansible_host: 203.0.113.10\n  wg_address: 10.99.0.2\nns2:\n  ansible_host: 203.0.113.20\n  wg_address: 10.99.0.3\n```\n\nGli IP DNS usati per il transfer puntano al tunnel:\n\n```yaml\n# inventory/group_vars/all/main.yml\ndns_primary_ip: \"10.99.0.1\"\ndns_secondary_ips:\n  - \"10.99.0.2\"\n  - \"10.99.0.3\"\n```\n\nIl 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`).\n\n### Verifica\n\n```bash\n# handshake attivo con entrambi i peer?\nssh -p 2400 root@10.0.0.14 \"wg show\"\n\n# il primary raggiunge i secondari nel tunnel?\nssh -p 2400 root@10.0.0.14 \"ping -c2 10.99.0.2\"\n\n# AXFR funziona via tunnel?\nssh -p 2400 root@10.0.0.14 \"dig @127.0.0.1 example.com AXFR | head\"\n```\n\n---\n\n## Gestione zone\n\n### Aggiornamento con serial automatico\n\nIl playbook calcola il serial nel formato `YYYYMMDDnn`:\n- Data odierna + serial esistente → incrementa `nn`\n- Data passata → `YYYYMMDD01`\n- Aggiorna **solo le zone cambiate** (diff prima del deploy)\n- Verifica sintassi con `named-checkzone`\n- Controlla propagazione sui secondari\n\n```bash\n# Tutte le zone\nansible-playbook playbooks/update-zones.yml --ask-vault-pass\n\n# Zona specifica\nansible-playbook playbooks/update-zones.yml --ask-vault-pass \\\n  -e \"zone_name=example.com\"\n\n# Forza reload\nansible-playbook playbooks/update-zones.yml --ask-vault-pass \\\n  -e \"force_serial=true\"\n```\n\n### Aggiungere una zona\n\n1. Crea `zones/nuova-zona.com.yml`\n2. Aggiungi in `inventory/group_vars/all/main.yml`:\n   ```yaml\n   dns_zones:\n     - name: \"nuova-zona.com\"\n       file: \"zones/nuova-zona.com.yml\"\n       type: master\n       ddns_enabled: false\n   ```\n3. `ansible-playbook playbooks/update-zones.yml --ask-vault-pass`\n\n### TLSA (DANE)\n\nOgni zona supporta una lista `tlsa` (vedi `zones/example.com.yml`). Con `usage: 3, selector: 1, matching: 1` (DANE-EE della chiave pubblica) l'hash si ricava dal certificato acme.sh in uso:\n\n```bash\nopenssl x509 -in /etc/ssl/acme/\u003cdominio\u003e.fullchain.pem -noout -pubkey \\\n  | openssl pkey -pubin -outform DER \\\n  | openssl dgst -sha256\n```\n\nRecord consigliati per un host web+mail (stesso certificato, stesso hash su tutte le porte):\n\n| Owner name              | Servizio          | Perché |\n|--------------------------|-------------------|--------|\n| `_443._tcp.www`          | HTTPS             | validazione standard del certificato web |\n| `_25._tcp.mail`          | SMTP (MTA-to-MTA) | STARTTLS è opportunistico e vulnerabile a downgrade/stripping; il TLSA (RFC 7672) forza i mittenti che supportano DANE (Postfix `smtp_tls_security_level=dane`, Exim, ecc.) a rifiutare la consegna in chiaro invece di degradare silenziosamente |\n| `_587._tcp.mail`         | Submission        | TLS implicito/mandatorio |\n| `_465._tcp.mail`         | SMTPS             | TLS implicito |\n| `_993._tcp.mail`         | IMAPS             | TLS implicito |\n| `_995._tcp.mail`         | POP3S             | TLS implicito |\n\nNon pubblicare TLSA su porte STARTTLS lato client (`_110._tcp`, `_143._tcp`, POP3/IMAP in chiaro): i client mail non validano DANE su queste porte, a differenza degli MTA in consegna SMTP — sarebbe un record senza alcun effetto pratico.\n\nSe la chiave del certificato cambia (nuovo keypair, non solo rinnovo con stessa chiave), l'hash va rigenerato e ridistribuito su tutti gli owner name che lo referenziano.\n\n---\n\n## DNSSEC\n\n### Configurazione automatica\n\nBIND 9.20 con `dnssec-policy` gestisce tutto:\n\n| Parametro | Valore | Note |\n|---|---|---|\n| Algoritmo | Ed25519 | Chiavi da 32 byte |\n| KSK lifetime | 1 anno | Rotazione automatica |\n| ZSK lifetime | 90 giorni | Rotazione automatica |\n| NSEC3 iterations | 0 | RFC 9276 |\n| Signature validity | 14 giorni | Rinnovo 3gg prima |\n\n### DS record — cosa sono e dove vanno\n\nIl **DS record** (Delegation Signer) è un hash della tua KSK (Key Signing Key). Va inserito nel **pannello del registrar** dove hai registrato il dominio (Aruba, Register.it, Namecheap, ecc.) nella sezione \"DNSSEC\" o \"DS Records\". Crea la **chain of trust**: senza di esso DNSSEC è configurato ma non verificabile dai resolver pubblici.\n\n\u003e **Eccezione**: le zone subdomain (es. `dyn.example.com`) non vanno al registrar. Il loro DS record è gestito automaticamente da BIND nella zona padre (`example.com`).\n\n### Come ottenere i DS record\n\n```bash\nmake dnssec\n```\n\nIl playbook stampa per ogni zona:\n\n```\nninux-nnxx.it. IN DS 24729 15 2 B73CAF4FE07BE64E...\n```\n\nI campi da inserire al registrar sono:\n\n| Campo registrar | Dove trovarlo nell'output |\n|---|---|\n| **Key Tag** (o Key ID) | primo numero dopo `DS` — es. `24729` |\n| **Algorithm** | secondo numero — `15` = Ed25519 |\n| **Digest Type** | terzo numero — `2` = SHA-256 |\n| **Digest** | stringa esadecimale finale |\n\n### Inserimento al registrar\n\nOgni registrar ha un'interfaccia diversa, ma i campi sono sempre gli stessi. Esempio per un dominio con algoritmo Ed25519:\n\n| Campo | Valore esempio |\n|---|---|\n| Key Tag | `24729` |\n| Algorithm | `15` |\n| Digest Type | `2` |\n| Digest | `B73CAF4FE07BE64E29EB7A57ABBC791D757CC5B8B0790B4DCE532352765EF729` |\n\nLa propagazione richiede in genere 1–24 ore (dipende dal TTL del registro TLD).\n\n### Verifica chain of trust\n\n```bash\n# Chain of trust end-to-end (dopo propagazione DS al registrar)\ndelv @8.8.8.8 example.com SOA +rtrace\n# Output atteso: \"; fully validated\"\n\n# Firma locale (senza aspettare la propagazione)\ndig +dnssec example.com SOA @ns1.example.com\n\n# Stato chiavi DNSSEC e prossime rotazioni\nmake dnssec\n```\n\n### Rotazione chiavi (automatica)\n\nBIND ruota KSK e ZSK automaticamente secondo la policy. Quando avviene una **rotazione KSK** devi aggiornare il DS record al registrar con il nuovo Key Tag. Il playbook `make dnssec` mostra sempre il DS record attivo da pubblicare.\n\n---\n\n## DDNS — Router OpenWrt\n\nIl router aggiorna automaticamente il proprio record A ogni 5 minuti:\n\n```\nrouter-home.dyn.example.com.  60  IN  A  \u003cIP WAN pubblico\u003e\n```\n\n```yaml\n# inventory/hosts.yml\nopenwrt_routers:\n  hosts:\n    router-home:\n      ansible_host: 192.168.1.1\n      ansible_user: root\n      ddns_hostname: \"router-home.dyn.example.com\"\n      ddns_interface: \"wan\"\n```\n\n```bash\nansible-playbook playbooks/site.yml --limit openwrt_routers --ask-vault-pass\n```\n\nLo script rileva automaticamente CGNAT e ottiene l'IP pubblico reale.\n\n---\n\n## Monitoring\n\n### Accesso via SSH tunnel\n\nGrafana, Prometheus e Alertmanager ascoltano solo su `127.0.0.1` del primary (IP privato LAN, non esposto su internet). Il role hardening abilita `PermitOpen` solo per le porte di monitoraggio.\n\n```bash\n# Apri il tunnel dal tuo notebook (rimane in background con -N)\nssh -p 2400 -N \\\n    -L 3000:127.0.0.1:3000 \\\n    -L 9090:127.0.0.1:9090 \\\n    -L 9093:127.0.0.1:9093 \\\n    root@\u003cprimary-ip\u003e\n\n# Grafana:      http://localhost:3000   (admin / vault_grafana_admin_password)\n# Prometheus:   http://localhost:9090\n# Alertmanager: http://localhost:9093\n```\n\nIl comando SSH tunnel preciso (con IP reale) viene stampato a fine di ogni `make deploy` nella sezione **MONITORING — accesso**.\n\n\u003e Se il primary non è raggiungibile direttamente dal notebook, fai il jump via Proxmox:\n\u003e ```bash\n\u003e ssh -J root@\u003cproxmox-ip\u003e -p 2400 -N \\\n\u003e     -L 3000:127.0.0.1:3000 -L 9090:127.0.0.1:9090 -L 9093:127.0.0.1:9093 \\\n\u003e     root@\u003cprimary-ip\u003e\n\u003e ```\n\n\u003e **Firewall**: se non riesci a connetterti anche con il tunnel aperto, aggiungi il tuo IP a `monitoring_allowed_sources` in `group_vars/all/main.yml` e rilancia `make deploy`.\n\n### Alert preconfigurati\n\n| Alert | Severità | Condizione |\n|---|---|---|\n| `BINDDown` | critical | bind_exporter non risponde 2+ min |\n| `BINDQueryRateCritical` | critical | \u003e 20.000 query/s |\n| `BINDSerialMismatch` | warning | serial non allineato tra nodi |\n| `DNSSECKeyExpiredCritical` | critical | chiave DNSSEC scade in \u003c 24h |\n| `NodeDown` | critical | server non raggiungibile |\n| `DiskSpaceCritical` | critical | \u003c 5% spazio libero |\n| `NTPOffsetHigh` | warning | offset \u003e 100ms |\n| *(+7 altri)* | | |\n\n### Notifiche email\n\n```yaml\n# inventory/group_vars/all/main.yml\nalertmanager_smtp_enabled: true\nalertmanager_smtp_host: \"smtp.gmail.com:587\"\nalertmanager_smtp_from: \"alerts@example.com\"\nalertmanager_smtp_to: \"admin@example.com\"\n```\n\n```yaml\n# inventory/group_vars/all/vault.yml\nvault_alertmanager_smtp_password: \"app_password\"\n```\n\n---\n\n## Hardening\n\n### Moduli attivi\n\n| Modulo | Descrizione |\n|---|---|\n| `user` | Crea utente ansible, carica chiavi SSH, blocca root |\n| `ssh` | chacha20/AES-GCM, curve25519, no password auth |\n| `sysctl` | 25+ parametri kernel hardening |\n| `filesystem` | `/tmp` noexec, no core dump, permessi file sensibili |\n| `services` | Disabilita 10+ demoni inutili |\n| `sudo` | Log completo, use_pty, requiretty |\n| `auditd` | Regole CIS per file DNS, syscall critiche |\n| `banner` | Banner pre-login + MOTD dinamico |\n| `fail2ban` | SSH jail + DNS flood jail con nftables |\n| `rkhunter` | Scan notturno, aggiornamento DB settimanale |\n| `unattended_upgrades` | Solo patch sicurezza, bind9 in blacklist |\n\n---\n\n## Firewall nftables\n\n### Primary (hidden master)\n\n```\nINPUT:  loopback, established, ICMP rate-limited, SSH, UDP/53 solo secondari+localhost\nOUTPUT: throttle UDP \u003e 512B per IP (anti-amplification)\nRAW:    anti-spoofing bogon, bypass conntrack UDP/53\n```\n\n### Secondari (VPS pubblici)\n\n```\nINPUT:  UDP/53 pubblico rate-limited (30pps/IP, ban 120s), TCP/53 rate-limited,\n        AXFR TCP illimitato dal primary, SSH rate-limited\nOUTPUT: throttle UDP \u003e 512B (anti-amplification, set dinamico amp_targets)\n```\n\n### Comandi utili\n\n```bash\n# IP bannati per DNS flood\nnft list set inet filter dns_flood\n\n# Sblocca IP manualmente\nnft delete element inet filter dns_flood { 1.2.3.4 }\n\n# Ruleset completo\nnft list ruleset\n```\n\n---\n\n## Certificati ACME\n\n### Come funziona\n\n1. acme.sh sul primary ottiene i certificati wildcard (`*.example.com` + root) via DNS-01 challenge usando `nsupdate` con la TSIG key `ddns-key`\n2. Al rinnovo (cron 02:30) o al primo deploy, il primary copia i certificati ai CT consumer via SSH (chiave ed25519 dedicata)\n3. Dopo la copia, il CT esegue il `reload_cmd` configurato (nginx, postfix, dovecot…)\n\n### Configurare i domini e i CT destinatari\n\n```yaml\n# inventory/group_vars/all/main.yml\nacme_deploy_key: \"/root/.ssh/acme_deploy_id_ed25519\"\nacme_deploy_ssh_port: 2400   # porta SSH dei CT\n\nacme_domains:\n  - domain: \"example.com\"\n    keylength: \"ec-256\"\n    deploy:\n      - host: \"10.0.0.16\"          # CT nginx\n        reload_cmd: \"systemctl reload nginx\"\n  - domain: \"altro.com\"\n    keylength: \"ec-256\"\n    deploy:\n      - host: \"10.0.0.6\"           # CT mail\n        reload_cmd: \"systemctl reload postfix \u0026\u0026 systemctl reload dovecot\"\n```\n\n```yaml\n# inventory/hosts.yml — gruppo cert_consumers\ncert_consumers:\n  hosts:\n    ct-web:\n      ansible_host: 10.0.0.16\n      ansible_user: root\n      ansible_port: 2400\n      cert_domain: \"example.com\"\n      cert_reload_cmd: \"systemctl reload nginx\"\n    ct-mail:\n      ansible_host: 10.0.0.6\n      ansible_user: root\n      ansible_port: 2400\n      cert_domain: \"altro.com\"\n      cert_reload_cmd: \"systemctl reload postfix \u0026\u0026 systemctl reload dovecot\"\n```\n\n### Comandi\n\n```bash\n# Emette/rinnova cert E distribuisce ai CT (tutto in una run)\nmake acme\n\n# Ricopia solo i cert già emessi ai CT (senza re-emettere)\nmake cert-deploy\n\n# Rinnovo manuale forzato\nmake renew\n\n# Controlla i log di rinnovo automatico sul primary\nssh -p 2400 root@\u003cprimary-ip\u003e \"tail -50 /var/log/acme-renew.log\"\n```\n\n### Troubleshooting\n\n```bash\n# Forza rinnovo manuale di un singolo dominio\nssh -p 2400 root@\u003cprimary-ip\u003e \\\n  \"/opt/acme.sh/acme.sh --renew -d example.com --force --home /opt/acme.sh\"\n\n# Verifica che i cert siano arrivati sul CT\nssh -p 2400 root@\u003cct-ip\u003e \"ls -la /etc/ssl/acme/\"\nssh -p 2400 root@\u003cct-ip\u003e \\\n  \"openssl x509 -noout -subject -enddate -in /etc/ssl/acme/example.com.fullchain.pem\"\n```\n\n---\n\n## CI/CD\n\n### Pipeline GitHub Actions\n\n```\npush → lint (yamllint + ansible-lint)\n     → syntax (tutti i playbook)\n     → molecule (Docker Debian Trixie: prepare → converge → idempotency → verify)\n     → validate-zones (valida YAML zone files)\n     → security (trivy CVE + verifica vault cifrato)\n\ntag vX.Y.Z → release (archivio + changelog automatico)\n```\n\n### Test in locale\n\n```bash\nyamllint .\nansible-lint\nansible-playbook playbooks/site.yml --syntax-check \\\n  -i inventory/hosts.yml -e @inventory/group_vars/all/main.yml \\\n  -e \"vault_tsig_secret=test vault_ddns_secret=test vault_proxmox_token_secret=test\"\nmolecule test\n```\n\n---\n\n## Operazioni giornaliere\n\n```bash\n# Aggiorna zone DNS\nmake zones\n\n# Stato DNSSEC + DS records\nmake dnssec\n\n# Snapshot prima di un'operazione rischiosa\nmake snapshot\n\n# Ricopia certificati ai CT (dopo un rinnovo manuale o una nuova VM)\nmake cert-deploy\n\n# Verifica connettività a tutti gli host\nmake ping\n```\n\nComandi diretti utili:\n\n```bash\n# Stato BIND su tutti i nodi\nansible all -m command -a \"systemctl status bind9\" --ask-vault-pass\n\n# Serial zone correnti\nansible dns_primary -m command \\\n  -a \"rndc zonestatus example.com\" --ask-vault-pass\n\n# Forza zone transfer sui secondari\nansible dns_secondary -m command \\\n  -a \"rndc retransfer example.com\" --ask-vault-pass\n\n# IP bannati per DNS flood\nansible dns_secondary -m command \\\n  -a \"nft list set inet filter dns_flood\" --ask-vault-pass\n\n# Log rinnovo certificati\nssh -p 2400 root@\u003cprimary-ip\u003e \"tail -50 /var/log/acme-renew.log\"\n\n# Snapshot con nome personalizzato\nansible-playbook playbooks/proxmox-snapshot.yml \\\n  --ask-vault-pass -e \"snap_action=create snap_name=pre-manutenzione\"\n```\n\n### Aggiornare BIND9\n\n```bash\n# Testa prima su un secondario\nansible ns1 -m apt -a \"name=bind9 state=latest\" --ask-vault-pass\nansible ns1 -m command -a \"systemctl restart bind9\" --ask-vault-pass\ndig @203.0.113.10 example.com SOA\n\n# Poi primary (crea snapshot prima)\nansible-playbook playbooks/proxmox-snapshot.yml \\\n  --ask-vault-pass -e \"snap_action=create snap_name=pre-bind9-upgrade\"\nansible dns_primary -m apt -a \"name=bind9 state=latest\" --ask-vault-pass\nansible dns_primary -m command -a \"systemctl restart bind9\" --ask-vault-pass\n\n# Infine gli altri secondari\nansible dns_secondary -m apt -a \"name=bind9 state=latest\" --ask-vault-pass\n```\n\n---\n\n## Troubleshooting\n\n### BIND9 non si avvia\n\n```bash\njournalctl -u bind9 -n 50 --no-pager\nnamed-checkconf /etc/bind/named.conf\nnamed-checkzone example.com /var/lib/bind/zones/db.example.com\n```\n\n### Zone transfer non funziona\n\n```bash\ndig @192.168.1.10 example.com AXFR\ngrep \"transfer\" /var/log/named/named.log\nrndc retransfer example.com\n```\n\n### DNSSEC validation errors\n\n```bash\nrndc dnssec -status example.com\nrndc sign example.com\ndnssec-verify -z example.com /var/lib/bind/zones/db.example.com\n```\n\n### Certificati ACME non si rinnovano\n\n```bash\n/opt/acme.sh/acme.sh --renew -d example.com --force\ncat /var/log/acme-renew.log\n```\n\n### Proxmox — clone fallisce\n\n```bash\n# Verifica che il template esista\nqm status 9000\n\n# Verifica permessi token API\npvesh get /access/acl\n\n# Log Proxmox\njournalctl -u pvedaemon -n 50\n```\n\n### Proxmox — cloud-init non applica IP\n\n```bash\n# Controlla lo status cloud-init sulla VM\nssh -p 2400 root@10.0.0.14 \"cloud-init status\"\nssh -p 2400 root@10.0.0.14 \"cat /var/log/cloud-init.log | tail -30\"\n\n# Rigenera immagine cloud-init e riavvia\nqm set 200 --cicustom \"\"\nqm cloudinit update 200\nqm reboot 200\n```\n\n### VM non risponde dopo creazione\n\n```bash\n# Verifica stato VM\nqm status 200\n\n# Console VM su Proxmox\nqm terminal 200\n\n# Log avvio\nqm showcmd 200\n```\n\n---\n\n## Sicurezza\n\n### Gestione secret\n\nTutti 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.\n\n```bash\n# Verifica che il vault sia cifrato\nhead -1 inventory/group_vars/all/vault.yml\n# Output atteso: $ANSIBLE_VAULT;1.1;AES256\n```\n\n### Rotazione chiavi TSIG\n\n```bash\ntsig-keygen -a hmac-sha256 axfr-key-new\nansible-vault edit inventory/group_vars/all/vault.yml\nansible-playbook playbooks/site.yml --ask-vault-pass\ndig @192.168.1.10 example.com AXFR   # verifica\n```\n\n### Rotazione token API Proxmox\n\n1. Crea nuovo token in Proxmox\n2. Aggiorna `vault_proxmox_token_secret` nel vault\n3. Esegui `ansible-playbook playbooks/proxmox.yml --ask-vault-pass` per verificare\n4. Revoca il vecchio token\n\n---\n\n## Licenza\n\nMIT — vedi [LICENSE](LICENSE)\n\n---\n\n## Contribuire\n\n1. Fork del repository\n2. Branch: `git checkout -b feature/nome`\n3. Test: `yamllint . \u0026\u0026 ansible-lint \u0026\u0026 molecule test`\n4. Commit: `git commit -m \"feat: descrizione\"`\n5. Pull request su `main`\n\nI contributi devono passare l'intera pipeline CI prima del merge.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikysal78%2Fansible-dns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmikysal78%2Fansible-dns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikysal78%2Fansible-dns/lists"}