Adding DNS role
This commit is contained in:
@@ -25,7 +25,12 @@
|
||||
"Bash(ansible-playbook:*)",
|
||||
"Bash(just test:*)",
|
||||
"Bash(pip show:*)",
|
||||
"Bash(molecule test:*)"
|
||||
"Bash(molecule test:*)",
|
||||
"Bash(python3 -m molecule test -s default)",
|
||||
"Bash(ansible-lint roles/dns/)",
|
||||
"Bash(ansible-lint roles/graylog/)",
|
||||
"Bash(ansible-lint .)",
|
||||
"Bash(grep -v \"^WARNING\\\\|^$\\\\|^A new\\\\|^Upgrade\\\\|^Read\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,3 +136,23 @@ smtp_from: "noreply@{{ base_domain }}"
|
||||
smtp_user: "noreply@{{ base_domain }}"
|
||||
smtp_password: "changeme_smtp"
|
||||
smtp_tls: "starttls"
|
||||
|
||||
# DNS / BIND9 — authoritative nameserver
|
||||
bind_version: "9.18-22.04_beta"
|
||||
# dns_server_ip must be the public IPv4 address of this server.
|
||||
# Register ns1.{{ base_domain }} as a glue record at your domain registrar
|
||||
# pointing to this IP, then set your domain's nameservers to ns1.{{ base_domain }}.
|
||||
dns_server_ip: "changeme_server_public_ip"
|
||||
dns_ns_hostname: "ns1.{{ base_domain }}"
|
||||
dns_ttl: 3600
|
||||
|
||||
# DKIM — retrieve the public key from the Stalwart admin UI at
|
||||
# mail.{{ base_domain }} → Settings → DKIM keys after first deployment.
|
||||
# Leave empty to skip the DKIM TXT record until the key is available.
|
||||
stalwart_dkim_selector: "default"
|
||||
stalwart_dkim_public_key: "" # e.g. "MIGfMA0GCSqGSIb3DQEB..."
|
||||
|
||||
# DMARC — email authentication policy
|
||||
dmarc_policy: "quarantine" # none | quarantine | reject
|
||||
dmarc_rua: "mailto:dmarc-reports@{{ base_domain }}"
|
||||
dmarc_ruf: "mailto:dmarc-forensics@{{ base_domain }}"
|
||||
|
||||
@@ -139,3 +139,16 @@ forgejo_data_dir: /tmp/sovereign_test/forgejo
|
||||
# Website
|
||||
website_nginx_version: "alpine"
|
||||
website_data_dir: /tmp/sovereign_test/website
|
||||
|
||||
# DNS / BIND9
|
||||
bind_version: "9.18-22.04_beta"
|
||||
dns_server_ip: "192.0.2.1"
|
||||
dns_ns_hostname: "ns1.test.example.com"
|
||||
dns_ttl: 3600
|
||||
dkim_selector: "default"
|
||||
stalwart_dkim_selector: "default"
|
||||
stalwart_dkim_public_key: ""
|
||||
dmarc_policy: "quarantine"
|
||||
dmarc_rua: "mailto:dmarc-reports@test.example.com"
|
||||
dmarc_ruf: "mailto:dmarc-forensics@test.example.com"
|
||||
dns_data_dir: /tmp/sovereign_test/dns
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
roles:
|
||||
- role: common
|
||||
tags: [common, traefik]
|
||||
- role: dns
|
||||
tags: [dns, nameserver]
|
||||
- role: graylog
|
||||
tags: [graylog, logging]
|
||||
- role: authentik
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
---
|
||||
dns_data_dir: "{{ sovereign_base_dir }}/dns"
|
||||
@@ -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))
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Converge
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
vars_files:
|
||||
- ../../../molecule/shared/vars.yml
|
||||
roles:
|
||||
- role: dns
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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; };
|
||||
};
|
||||
@@ -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 %}
|
||||
@@ -110,6 +110,17 @@ forgejo_admin_password: "test_forgejo_admin"
|
||||
forgejo_admin_email: "admin@test.example.com"
|
||||
forgejo_ssh_port: 2222
|
||||
|
||||
# DNS / BIND9
|
||||
bind_version: "9.18-22.04_beta"
|
||||
dns_server_ip: "192.0.2.1"
|
||||
dns_ns_hostname: "ns1.test.example.com"
|
||||
dns_ttl: 3600
|
||||
stalwart_dkim_selector: "default"
|
||||
stalwart_dkim_public_key: ""
|
||||
dmarc_policy: "quarantine"
|
||||
dmarc_rua: "mailto:dmarc-reports@test.example.com"
|
||||
dmarc_ruf: "mailto:dmarc-forensics@test.example.com"
|
||||
|
||||
# Website
|
||||
website_nginx_version: "alpine"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user