From bd90a7e16fad66025a494b90c5d09c60bb8ffdc2 Mon Sep 17 00:00:00 2001 From: Matthias Johnson Date: Sat, 28 Feb 2026 19:06:24 -0700 Subject: [PATCH] Automate DKIM sync and add Hetzner resource labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add dkim_sync.yml: generates DKIM keys for all mail_domains, writes keys to stack config (group_vars/all/dkim.yml), and publishes mail._domainkey TXT records via dns.yml — replaces manual vault editing - Remove dkim_keys from vault.yml.setup (public keys don't need encryption) - Add hcloud_labels to config.yml.setup and apply to server + SSH key in provision role, enabling project-level tagging of Hetzner resources - Fix setup.sh next steps: add missing bootstrap step, replace manual DKIM instructions with dkim_sync.yml - Update CLAUDE.md and README.md accordingly Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 + README.md | 17 +----- inventory/group_vars/all/config.yml.setup | 6 ++ inventory/group_vars/all/vault.yml.setup | 6 -- playbooks/dkim_sync.yml | 72 +++++++++++++++++++++++ playbooks/dns.yml | 8 +-- roles/provision/tasks/hetzner.yml | 2 + setup.sh | 6 +- 8 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 playbooks/dkim_sync.yml diff --git a/CLAUDE.md b/CLAUDE.md index bef5a44..b1be529 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,12 +40,14 @@ Note: Inventory and vault password are set via `ANSIBLE_INVENTORY` and `ANSIBLE_ - `provision.yml` - Provision a cloud VM (Hetzner) - `dns.yml` - Manage DNS zones/records via Hetzner DNS API - `bootstrap.yml` - First-time server setup (run once as root before site.yml) +- `dkim_sync.yml` - Fetch DKIM keys from mailserver and publish to DNS (run once after first mail deploy) **Full deployment order** (fresh server): 1. `provision.yml` - create server, auto-writes IP to hosts.yml and config.yml 2. `dns.yml` - create DNS records 3. `bootstrap.yml` - users, SSH hardening, packages, Docker (connects as root) 4. `site.yml` - deploy all services +5. `dkim_sync.yml` - generate DKIM keys, write to stack config, publish to DNS **Playbook Execution Order** (via `site.yml`): 1. networks.yml - Pre-create all Docker networks (must run before any service) diff --git a/README.md b/README.md index 71f0851..959020b 100644 --- a/README.md +++ b/README.md @@ -213,24 +213,13 @@ ansible-vault edit $LINDERHOF_DIR/group_vars/all/vault.yml ## after first mail deployment — DKIM -retrieve the generated DKIM public key and add it to the vault: +run `dkim_sync.yml` once after the first mail deployment — it generates DKIM keys for all mail domains, writes them to your stack config, and publishes the `mail._domainkey` DNS records automatically: ```bash -docker exec mailserver cat /tmp/docker-mailserver/rspamd/dkim//mail.pub -ansible-vault edit $LINDERHOF_DIR/group_vars/all/vault.yml +ansible-playbook playbooks/dkim_sync.yml ``` -add under `dkim_keys`: -```yaml -dkim_keys: - example.com: "v=DKIM1; k=rsa; p=..." -``` - -then re-run DNS — the `mail._domainkey` record is created automatically: - -```bash -ansible-playbook playbooks/dns.yml -``` +keys are stored in `$LINDERHOF_DIR/group_vars/all/dkim.yml` (plain file — DKIM public keys are not secret). safe to re-run; only generates keys for domains that don't have one yet. ## common operations diff --git a/inventory/group_vars/all/config.yml.setup b/inventory/group_vars/all/config.yml.setup index ef052ee..082d4ae 100644 --- a/inventory/group_vars/all/config.yml.setup +++ b/inventory/group_vars/all/config.yml.setup @@ -34,6 +34,12 @@ enable_radicale: true # ============================================================ domain: $domain server_name: $server_name + +# Labels applied to all Hetzner cloud resources (server, SSH key). +# DNS resources do not support labels. +hcloud_labels: + managed-by: linderhof + stack: $stack_name server_ip: $server_ip admin_user: $admin_user admin_shell: /bin/zsh diff --git a/inventory/group_vars/all/vault.yml.setup b/inventory/group_vars/all/vault.yml.setup index d08713f..e9bc1cc 100644 --- a/inventory/group_vars/all/vault.yml.setup +++ b/inventory/group_vars/all/vault.yml.setup @@ -54,9 +54,3 @@ restic_password: "$restic_password" # fail2ban (optional — IPs/CIDRs to whitelist) # fail2ban_ignoreip: "your-home-ip/32" - -# DKIM public keys — add after first mail deployment: -# docker exec mailserver cat /tmp/docker-mailserver/rspamd/dkim/$domain/mail.pub -# Format: "v=DKIM1; k=rsa; p=" -# dkim_keys: -# $domain: "v=DKIM1; k=rsa; p=..." diff --git a/playbooks/dkim_sync.yml b/playbooks/dkim_sync.yml new file mode 100644 index 0000000..c63d6ff --- /dev/null +++ b/playbooks/dkim_sync.yml @@ -0,0 +1,72 @@ +--- +# Fetch DKIM public keys from the running mailserver and publish them to DNS. +# +# Safe to re-run — only generates keys for domains that don't have one yet. +# Run after the first mail deployment: +# ansible-playbook playbooks/dkim_sync.yml +# +# What it does: +# 1. Generates DKIM keys for any domain missing one (skips existing keys) +# 2. Discovers each domain's key file at runtime via find — the algorithm prefix +# (rsa-2048-..., ed25519-...) may vary across docker-mailserver versions, +# but the *.public.dns.txt suffix is stable +# 3. Writes $LINDERHOF_DIR/group_vars/all/dkim.yml (plain file — DKIM keys are public) +# 4. Runs dns.yml to create/update mail._domainkey TXT records for all domains + +- name: Fetch DKIM keys from mailserver + hosts: all + gather_facts: false + + tasks: + - name: Check for existing DKIM key files + # /srv/mail/config is the host-side mount of /tmp/docker-mailserver in the container + command: find /srv/mail/config/rspamd/dkim -name "*{{ item }}.public.dns.txt" -type f + loop: "{{ mail_domains }}" + register: dkim_existing + changed_when: false + failed_when: false + + - name: Generate DKIM keys for domains without existing keys + command: docker exec mailserver setup config dkim + when: dkim_existing.results | selectattr('stdout', 'equalto', '') | list | length > 0 + changed_when: true + + - name: Find DKIM DNS TXT key file for each domain + command: find /srv/mail/config/rspamd/dkim -name "*{{ item }}.public.dns.txt" -type f + loop: "{{ mail_domains }}" + register: dkim_file_paths + changed_when: false + + - name: Read DKIM public key for each domain + slurp: + src: "{{ item.stdout | trim }}" + loop: "{{ dkim_file_paths.results }}" + loop_control: + label: "{{ item.item }}" + register: dkim_keys_raw + + - name: Build dkim_keys dict + set_fact: + dkim_keys_collected: >- + {{ dkim_keys_collected | default({}) | combine({item.item.item: item.content | b64decode | trim}) }} + loop: "{{ dkim_keys_raw.results }}" + loop_control: + label: "{{ item.item.item }}" + + - name: Write dkim.yml to stack config directory + delegate_to: localhost + become: false + # Written to a separate dkim.yml rather than config.yml so this playbook can safely + # overwrite it without touching the hand-edited config. Ansible loads all files under + # group_vars/all/ automatically, so dkim_keys is available to all roles either way. + copy: + content: | + --- + # DKIM public keys — written automatically by dkim_sync.yml, do not edit manually + dkim_keys: + {% for domain_name, key in dkim_keys_collected.items() %} + {{ domain_name }}: "{{ key }}" + {% endfor %} + dest: "{{ lookup('env', 'ANSIBLE_INVENTORY') | dirname }}/group_vars/all/dkim.yml" + +- import_playbook: dns.yml diff --git a/playbooks/dns.yml b/playbooks/dns.yml index e464379..46be005 100644 --- a/playbooks/dns.yml +++ b/playbooks/dns.yml @@ -4,13 +4,7 @@ # Zone definitions live in $LINDERHOF_DIR/group_vars/all/dns.yml # (generated from inventory/group_vars/all/dns.yml.setup by setup.sh). # -# To add DKIM keys after first mail deployment: -# docker exec mailserver cat /tmp/docker-mailserver/rspamd/dkim//mail.pub -# Then add to vault.yml: -# ansible-vault edit $LINDERHOF_DIR/group_vars/all/vault.yml -# dkim_keys: -# example.com: "v=DKIM1; k=rsa; p=..." -# And uncomment the mail._domainkey record in dns.yml. +# DKIM records are managed automatically by dkim_sync.yml — do not add manually. # # Usage: ansible-playbook playbooks/dns.yml - name: Manage DNS zones on Hetzner Cloud diff --git a/roles/provision/tasks/hetzner.yml b/roles/provision/tasks/hetzner.yml index a54ae54..2b0af11 100644 --- a/roles/provision/tasks/hetzner.yml +++ b/roles/provision/tasks/hetzner.yml @@ -3,6 +3,7 @@ hetzner.hcloud.ssh_key: name: "{{ admin_user }}" public_key: "{{ admin_ssh_key }}" + labels: "{{ hcloud_labels }}" api_token: "{{ hcloud_token }}" state: present @@ -14,6 +15,7 @@ location: "{{ hcloud_location }}" ssh_keys: - "{{ admin_user }}" + labels: "{{ hcloud_labels }}" api_token: "{{ hcloud_token }}" state: present register: server_result diff --git a/setup.sh b/setup.sh index 42026ac..6d72c3a 100755 --- a/setup.sh +++ b/setup.sh @@ -242,6 +242,6 @@ echo " 2. Review $VAULT (ansible-vault edit)" echo " 3. Review $DNS_CONFIG" echo " 4. Provision a server: ansible-playbook playbooks/provision.yml" echo " 5. Update DNS: ansible-playbook playbooks/dns.yml" -echo " 6. Deploy: ansible-playbook playbooks/site.yml" -echo " 7. After mail deploys, retrieve DKIM keys and add to vault.yml:" -echo " docker exec mailserver cat /tmp/docker-mailserver/rspamd/dkim/$domain/mail.pub" +echo " 6. Bootstrap server: ansible-playbook playbooks/site.yml --tags bootstrap" +echo " 7. Deploy: ansible-playbook playbooks/site.yml" +echo " 8. Sync DKIM keys to DNS: ansible-playbook playbooks/dkim_sync.yml"