Adding DNS role

This commit is contained in:
Ian Roddis
2026-04-21 10:07:06 -04:00
parent 3a873051e7
commit 043d315b80
14 changed files with 464 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
---
dns_data_dir: "{{ sovereign_base_dir }}/dns"
+7
View File
@@ -0,0 +1,7 @@
---
- name: Restart BIND9
community.docker.docker_compose_v2:
project_src: "{{ dns_data_dir }}"
state: present
recreate: always
when: not (molecule_test_mode | default(false))
+8
View File
@@ -0,0 +1,8 @@
---
- name: Converge
hosts: localhost
gather_facts: false
vars_files:
- ../../../molecule/shared/vars.yml
roles:
- role: dns
+21
View File
@@ -0,0 +1,21 @@
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: default
platforms:
- name: localhost
groups:
- sovereign
provisioner:
name: ansible
env:
ANSIBLE_ROLES_PATH: "${MOLECULE_PROJECT_DIRECTORY}/.."
inventory:
host_vars:
localhost:
ansible_connection: local
verifier:
name: ansible
+194
View File
@@ -0,0 +1,194 @@
---
- name: Verify dns role
hosts: localhost
gather_facts: false
vars:
dns_data_dir: /tmp/sovereign_test/dns
tasks:
- name: Check dns data directory exists
ansible.builtin.stat:
path: /tmp/sovereign_test/dns
register: data_dir_stat
- name: Assert dns data directory is present
ansible.builtin.assert:
that: data_dir_stat.stat.isdir
fail_msg: "Data directory /tmp/sovereign_test/dns was not created"
- name: Check dns config directory exists
ansible.builtin.stat:
path: /tmp/sovereign_test/dns/config
register: config_dir_stat
- name: Assert dns config directory is present
ansible.builtin.assert:
that: config_dir_stat.stat.isdir
fail_msg: "Directory /tmp/sovereign_test/dns/config was not created"
- name: Check dns zones directory exists
ansible.builtin.stat:
path: /tmp/sovereign_test/dns/zones
register: zones_dir_stat
- name: Assert dns zones directory is present
ansible.builtin.assert:
that: zones_dir_stat.stat.isdir
fail_msg: "Directory /tmp/sovereign_test/dns/zones was not created"
- name: Check dns cache directory exists
ansible.builtin.stat:
path: /tmp/sovereign_test/dns/cache
register: cache_dir_stat
- name: Assert dns cache directory is present
ansible.builtin.assert:
that: cache_dir_stat.stat.isdir
fail_msg: "Directory /tmp/sovereign_test/dns/cache was not created"
- name: Check named.conf exists
ansible.builtin.stat:
path: /tmp/sovereign_test/dns/config/named.conf
register: named_conf_stat
- name: Assert named.conf was rendered
ansible.builtin.assert:
that: named_conf_stat.stat.exists
fail_msg: "named.conf was not rendered"
- name: Read named.conf
ansible.builtin.slurp:
src: /tmp/sovereign_test/dns/config/named.conf
register: named_conf_raw
- name: Set named.conf content fact
ansible.builtin.set_fact:
named_conf: "{{ named_conf_raw.content | b64decode }}"
- name: Assert zone declaration for base domain in named.conf
ansible.builtin.assert:
that: "'zone \"test.example.com\"' in named_conf"
fail_msg: "Expected zone declaration for test.example.com not found in named.conf"
- name: Assert recursion disabled in named.conf
ansible.builtin.assert:
that: "'recursion no' in named_conf"
fail_msg: "Expected 'recursion no' not found in named.conf"
- name: Check zone file exists
ansible.builtin.stat:
path: /tmp/sovereign_test/dns/zones/test.example.com.zone
register: zone_stat
- name: Assert zone file was rendered
ansible.builtin.assert:
that: zone_stat.stat.exists
fail_msg: "Zone file test.example.com.zone was not rendered"
- name: Read zone file
ansible.builtin.slurp:
src: /tmp/sovereign_test/dns/zones/test.example.com.zone
register: zone_raw
- name: Set zone content fact
ansible.builtin.set_fact:
zone: "{{ zone_raw.content | b64decode }}"
- name: Assert SOA record in zone
ansible.builtin.assert:
that: "'SOA' in zone"
fail_msg: "SOA record not found in zone file"
- name: Assert NS record in zone
ansible.builtin.assert:
that: "'IN NS' in zone"
fail_msg: "NS record not found in zone file"
- name: Assert server IP in zone for ns1 A record
ansible.builtin.assert:
that: "'192.0.2.1' in zone"
fail_msg: "Expected dns_server_ip 192.0.2.1 not found in zone file"
- name: Assert mail A record in zone
ansible.builtin.assert:
that: "'mail IN A' in zone"
fail_msg: "mail A record not found in zone file"
- name: Assert MX record in zone
ansible.builtin.assert:
that: "'IN MX' in zone"
fail_msg: "MX record not found in zone file"
- name: Assert SPF TXT record in zone
ansible.builtin.assert:
that: "'v=spf1 mx ~all' in zone"
fail_msg: "SPF record not found in zone file"
- name: Assert DMARC TXT record in zone
ansible.builtin.assert:
that: "'v=DMARC1' in zone"
fail_msg: "DMARC record not found in zone file"
- name: Assert DMARC policy in zone
ansible.builtin.assert:
that: "'p=quarantine' in zone"
fail_msg: "Expected DMARC policy 'quarantine' not found in zone file"
- name: Assert DMARC rua address in zone
ansible.builtin.assert:
that: "'mailto:dmarc-reports@test.example.com' in zone"
fail_msg: "Expected DMARC rua address not found in zone file"
- name: Assert auth A record in zone
ansible.builtin.assert:
that: "'auth IN A' in zone"
fail_msg: "auth A record not found in zone file"
- name: Assert git A record in zone
ansible.builtin.assert:
that: "'git IN A' in zone"
fail_msg: "git A record not found in zone file"
- name: Check docker-compose.yml exists
ansible.builtin.stat:
path: /tmp/sovereign_test/dns/docker-compose.yml
register: compose_stat
- name: Assert docker-compose.yml was rendered
ansible.builtin.assert:
that: compose_stat.stat.exists
fail_msg: "docker-compose.yml was not rendered for dns"
- name: Read docker-compose.yml
ansible.builtin.slurp:
src: /tmp/sovereign_test/dns/docker-compose.yml
register: compose_raw
- name: Set compose content fact
ansible.builtin.set_fact:
compose: "{{ compose_raw.content | b64decode }}"
- name: Assert bind9 image in compose
ansible.builtin.assert:
that: "'ubuntu/bind9:9.18-22.04_beta' in compose"
fail_msg: "Expected bind9 image not found in docker-compose.yml"
- name: Assert port 53 TCP in compose
ansible.builtin.assert:
that: "'53:53/tcp' in compose"
fail_msg: "Expected port 53/tcp mapping not found in docker-compose.yml"
- name: Assert port 53 UDP in compose
ansible.builtin.assert:
that: "'53:53/udp' in compose"
fail_msg: "Expected port 53/udp mapping not found in docker-compose.yml"
- name: Assert GELF logging address in compose
ansible.builtin.assert:
that: "'udp://127.0.0.1:12201' in compose"
fail_msg: "Expected GELF address udp://127.0.0.1:12201 not found in docker-compose.yml"
- name: Assert sovereign network is external in compose
ansible.builtin.assert:
that: "'external: true' in compose"
fail_msg: "Expected 'external: true' not found in docker-compose.yml"
+49
View File
@@ -0,0 +1,49 @@
---
- name: Create BIND9 directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- "{{ dns_data_dir }}"
- "{{ dns_data_dir }}/config"
- "{{ dns_data_dir }}/zones"
- "{{ dns_data_dir }}/cache"
- name: Set DNS zone serial from current timestamp
ansible.builtin.set_fact:
dns_zone_serial: "{{ lookup('pipe', 'date +%Y%m%d%H') | int }}"
- name: Deploy named.conf
ansible.builtin.template:
src: named.conf.j2
dest: "{{ dns_data_dir }}/config/named.conf"
mode: '0644'
notify: Restart BIND9
- name: Deploy zone file
ansible.builtin.template:
src: zone.j2
dest: "{{ dns_data_dir }}/zones/{{ base_domain }}.zone"
mode: '0644'
notify: Restart BIND9
- name: Deploy BIND9 docker-compose
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ dns_data_dir }}/docker-compose.yml"
mode: '0644'
notify: Restart BIND9
- name: Start BIND9
community.docker.docker_compose_v2:
project_src: "{{ dns_data_dir }}"
state: present
when: not (molecule_test_mode | default(false))
- name: Wait for BIND9 to be ready
ansible.builtin.wait_for:
host: 127.0.0.1
port: 53
timeout: 30
when: not (molecule_test_mode | default(false))
+25
View File
@@ -0,0 +1,25 @@
services:
bind9:
image: ubuntu/bind9:{{ bind_version }}
container_name: bind9
restart: unless-stopped
environment:
TZ: UTC
ports:
- "53:53/tcp"
- "53:53/udp"
volumes:
- {{ dns_data_dir }}/config/named.conf:/etc/bind/named.conf:ro
- {{ dns_data_dir }}/zones:/var/lib/bind:ro
- {{ dns_data_dir }}/cache:/var/cache/bind
networks:
- {{ sovereign_network_name }}
logging:
driver: gelf
options:
gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}"
tag: "bind9"
networks:
{{ sovereign_network_name }}:
external: true
+29
View File
@@ -0,0 +1,29 @@
// named.conf — authoritative-only configuration for {{ base_domain }}
// Managed by Ansible — do not edit manually.
options {
directory "/var/cache/bind";
// Authoritative only — no recursion to prevent DNS amplification attacks
recursion no;
allow-recursion { none; };
// Accept queries from any source
allow-query { any; };
// Only allow zone transfers to trusted hosts (none by default)
allow-transfer { none; };
// Listen on all interfaces
listen-on { any; };
listen-on-v6 { any; };
dnssec-validation no;
};
// Authoritative zone for the base domain
zone "{{ base_domain }}" IN {
type master;
file "/var/lib/bind/{{ base_domain }}.zone";
allow-update { none; };
};
+77
View File
@@ -0,0 +1,77 @@
; Zone file for {{ base_domain }}
; Managed by Ansible — do not edit manually.
; Serial: {{ dns_zone_serial }} (YYYYMMDDHH — increment on manual edits)
$ORIGIN {{ base_domain }}.
$TTL {{ dns_ttl | default(3600) }}
; ---------------------------------------------------------------------------
; SOA record
; ---------------------------------------------------------------------------
@ IN SOA {{ dns_ns_hostname | default('ns1.' + base_domain) }}. hostmaster.{{ base_domain }}. (
{{ dns_zone_serial }} ; Serial
3600 ; Refresh (1 hour)
900 ; Retry (15 min)
604800 ; Expire (7 days)
300 ; Negative TTL (5 min)
)
; ---------------------------------------------------------------------------
; Name servers
; ---------------------------------------------------------------------------
@ IN NS {{ dns_ns_hostname | default('ns1.' + base_domain) }}.
ns1 IN A {{ dns_server_ip }}
; ---------------------------------------------------------------------------
; Root domain
; ---------------------------------------------------------------------------
@ IN A {{ dns_server_ip }}
; ---------------------------------------------------------------------------
; Service A records
; ---------------------------------------------------------------------------
traefik IN A {{ dns_server_ip }}
logs IN A {{ dns_server_ip }}
auth IN A {{ dns_server_ip }}
s3 IN A {{ dns_server_ip }}
minio IN A {{ dns_server_ip }}
cloud IN A {{ dns_server_ip }}
mail IN A {{ dns_server_ip }}
webmail IN A {{ dns_server_ip }}
matrix IN A {{ dns_server_ip }}
chat IN A {{ dns_server_ip }}
meet IN A {{ dns_server_ip }}
headscale IN A {{ dns_server_ip }}
wazuh IN A {{ dns_server_ip }}
vault IN A {{ dns_server_ip }}
git IN A {{ dns_server_ip }}
; ---------------------------------------------------------------------------
; Mail exchange
; ---------------------------------------------------------------------------
@ IN MX 10 mail.{{ base_domain }}.
; ---------------------------------------------------------------------------
; SPF — authorise the mail server to send on behalf of the domain
; ---------------------------------------------------------------------------
@ IN TXT "v=spf1 mx ~all"
; ---------------------------------------------------------------------------
; DMARC — policy: {{ dmarc_policy | default('quarantine') }}
; ---------------------------------------------------------------------------
_dmarc IN TXT "v=DMARC1; p={{ dmarc_policy | default('quarantine') }}; rua={{ dmarc_rua | default('mailto:dmarc-reports@' + base_domain) }}; ruf={{ dmarc_ruf | default('mailto:dmarc-forensics@' + base_domain) }}; fo=1; adkim=r; aspf=r"
; ---------------------------------------------------------------------------
; DKIM — public key from the Stalwart mail server
; Set stalwart_dkim_public_key in group_vars/all.yml (retrieve from the
; Stalwart admin UI at mail.{{ base_domain }} → Settings → DKIM keys).
; RSA-2048 keys exceed the 255-byte TXT string limit so they are split into
; multiple quoted strings, which compliant resolvers concatenate.
; ---------------------------------------------------------------------------
{% if stalwart_dkim_public_key | default('') != '' %}
{% set dkim_full = "v=DKIM1; k=rsa; p=" + stalwart_dkim_public_key %}
{% set dkim_chunks = dkim_full | regex_findall('.{1,255}') %}
{{ stalwart_dkim_selector | default('default') }}._domainkey IN TXT ( {% for chunk in dkim_chunks %}"{{ chunk }}" {% endfor %})
{% else %}
; DKIM record not configured — set stalwart_dkim_public_key to enable.
{% endif %}