- Python 54.7%
- Shell 39.4%
- Makefile 5.9%
- zones: replace placeholder IPs with the live staging infra; add the staging.* record set (account/imap/smtp/jmap/pop3/webmail/autoconfig/mta-sts.staging, mxN.staging, node-N.staging, idp-1.staging, admin/api/ca.mail.staging), real ed25519 DKIM, SPF/DMARC/TLS-RPT/SRV; drop apex NS (ClouDNS owns delegation) and placeholder AAAA (no v6 ingress). _mta-sts deferred (enforce mode). - config.yaml/config.ci.yaml: octodns-cloudns 0.0.16 takes a numeric sub-auth-id via auth_id + sub_auth:true (no sub_auth_user kwarg); ownership txt_name must be a single label (_owner) — a dotted value silently filtered every real record out of the plan - roles/octodns: pass the CLOUDNS_* env to the validate step too (prod config instantiates the ClouDNS provider) - ptr-manifest: real staging node IPs -> mxN.staging.albient.cloud + idp-1 |
||
|---|---|---|
| .github/workflows | ||
| artifacts/dns | ||
| config | ||
| docs | ||
| inventories | ||
| playbooks | ||
| roles/octodns | ||
| scripts | ||
| zones | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .sops.yaml | ||
| .yamllint | ||
| ansible.cfg | ||
| Makefile | ||
| ptr-manifest.yaml | ||
| README.md | ||
| requirements.txt | ||
| requirements.yml | ||
ansible-dns
DNS-as-code for Albient — a UK hosting provider that prioritises privacy, data sovereignty and open source. This repo manages all of Albient's DNS (infrastructure zones + customer mail domains) declaratively with octoDNS, publishing to ClouDNS, orchestrated and gated by Ansible.
octoDNS is the source of truth. ClouDNS is just a publish target — isolated to a single provider block so it can be swapped (Route53, PowerDNS, Knot, deSEC, …) without touching any zone data.
Principles
- Dry-run first. Every apply is preceded by a visible plan.
octodns-syncruns without--doitby default; applying requires an explicit, double-confirmed gate. - Idempotent & reviewable. Zones are plain YAML; the git diff is the change record. CI posts the computed DNS diff on every PR.
- Sovereign / open-source. No proprietary control plane; provider-specific bits are isolated and replaceable.
- Secrets never in plaintext. ClouDNS credentials live SOPS-encrypted (age), decrypted only at runtime and exported as env vars.
The seam with ansible-mailstack
The sibling albient.mailstack collection deploys the
Stalwart-based HA mail platform. It does not manage DNS — it emits the DNS records
the mail platform needs as a YAML artifact. This repo consumes that artifact and
publishes it.
ansible-mailstack ──emit──► artifacts/dns/<env>-mailstack-records.yml
│ scripts/ingest_contract.py (idempotent merge)
▼
ansible-dns zones/*.yaml ──► octoDNS ──► ClouDNS
The full schema is in docs/record-contract.md (reconciled
with the mailstack's docs/dns-contract.md, v1). It covers MX, SPF, DKIM (with rotation
selectors), DMARC, MTA-STS, TLS-RPT, autoconfig/autodiscover + the full SRV set, CAA,
optional DANE/TLSA, ACME DNS-01 challenge handling, and PTR/rDNS auditing.
Layout
config/
config.yaml octoDNS: ClouDNS target (prod) + no-op dump + processors
config.ci.yaml same zones, no-op target ONLY (no creds) — used by CI
zones/
albient.cloud.yaml infra zone (mx1-3, mail floating IP, mta-sts, egress…)
example.com.yaml worked customer mail zone — every contract record
roles/octodns/ venv install, SOPS decrypt, validate, gated octodns-sync
playbooks/
sync.yml dry-run (default) / gated apply
ingest.yml merge the mailstack contract into zones/
scripts/
ingest_contract.py idempotent contract → zones/ + ptr-manifest merge
acme_dns01.py ClouDNS DNS-01 present/cleanup hook (ephemeral)
audit_rdns.py live PTR vs ptr-manifest.yaml drift audit
inventories/{prod,staging,dev}/ localhost + per-env SOPS secrets
ptr-manifest.yaml authoritative expected-PTR list (rDNS lives at Hetzner)
docs/ record-contract, architecture, runbooks, samples
Quick start
make venv # build the isolated octoDNS venv
make lint # yamllint
make validate # octodns-validate (no network)
make plan # dry-run plan against the no-op target (CI-safe, no creds)
Dry-run vs apply
Default is always dry-run. Two equivalent paths:
# octoDNS directly
make plan-prod # dry-run against ClouDNS (needs CLOUDNS_* env)
make apply ENV=prod CONFIRM=1 # APPLY (octodns-sync --doit)
# Ansible wrapper (recommended — handles SOPS + the apply interlock)
ansible-playbook playbooks/sync.yml -e octodns_environment=prod # DRY-RUN
ansible-playbook playbooks/sync.yml \
-e octodns_environment=prod -e octodns_apply=true -e octodns_apply_confirm_env=prod
The role refuses to apply unless both octodns_apply=true and
octodns_apply_confirm_env equals the active environment — a typo-proof interlock against
changing the wrong environment. Staging uses config.ci.yaml (no-op target, no creds).
Add / update a customer mail domain
ansible-mailstackemitsartifacts/dns/<env>-mailstack-records.yml.make ingest ENV=prod(oransible-playbook playbooks/ingest.yml) merges it intozones/<domain>.yaml— idempotent, no hand-editing.- Review the
zones/git diff, open a PR; CI shows the DNS plan. - Merge, then
make apply ENV=prod CONFIRM=1(or the gated playbook).
See docs/add-a-domain.md.
Secrets (SOPS + age)
ClouDNS auth-id / auth-password (and optional sub-auth-user) live in
inventories/<env>/group_vars/all/secrets.sops.yaml, SOPS-encrypted with age. The
.example files show the plaintext shape. Decrypted only at runtime by the octodns
role; never logged. Key handling/rotation: docs/runbooks/secret-rotation.md.
pre-commit runs gitleaks, detect-private-key, and a local
sops-encrypted-check that blocks committing any *.sops.yaml that isn't encrypted.
CI/CD
.github/workflows/ci.yml on every PR: yamllint → octodns-validate →
octodns-sync dry-run against the no-op target → posts the DNS diff as a PR comment,
plus an idempotency check of the ingest script and a full pre-commit run. No ClouDNS
credentials and no live API calls in CI.
ClouDNS specifics, DNSSEC & gotchas
See docs/architecture.md for TTLs, ALIAS/ANAME at apex, API rate
limits, DNSSEC (provider-managed DS), and how to swap providers.
Status
| Area | State |
|---|---|
| octoDNS config (prod + CI), processors | implemented, validated |
| Infra zone + worked customer zone | implemented, validated |
| Ansible role + sync/ingest playbooks | implemented |
ingest_contract.py (idempotent, contract v1) |
implemented, tested |
acme_dns01.py, audit_rdns.py |
implemented (no live calls without creds) |
| CI, pre-commit, SOPS rules | implemented (placeholder age recipients) |
| Live ClouDNS apply / real credentials | not done — no creds yet, by design |