From 4bdd36b8f7c87c023090792471ca7c4edddfd36f Mon Sep 17 00:00:00 2001
From: lilly
Date: Fri, 11 Sep 2026 10:09:48 +0200
Subject: [PATCH] update knot role to support more operational variety
Now secondary zones, dynamic zones, interpeted catalog zones, etc.
are supported by our knot role
---
.../chaosknoten/host_vars/auth-dns.sops.yaml | 15 +-
.../chaosknoten/host_vars/auth-dns.yaml | 11 +-
roles/knot/defaults/main.yaml | 9 ++
roles/knot/meta/argument_specs.yaml | 132 ++++++++++++++++--
roles/knot/tasks/02-configure.yaml | 16 +--
roles/knot/tasks/main.yaml | 9 +-
roles/knot/templates/knot.conf.j2 | 120 ++++++++++++++--
roles/knot/templates/netplan-disable-ra.yaml | 13 --
8 files changed, 264 insertions(+), 61 deletions(-)
delete mode 100644 roles/knot/templates/netplan-disable-ra.yaml
diff --git a/inventories/chaosknoten/host_vars/auth-dns.sops.yaml b/inventories/chaosknoten/host_vars/auth-dns.sops.yaml
index 78832249..fbf06b66 100644
--- a/inventories/chaosknoten/host_vars/auth-dns.sops.yaml
+++ b/inventories/chaosknoten/host_vars/auth-dns.sops.yaml
@@ -1,9 +1,11 @@
ansible_pull__age_private_key: ENC[AES256_GCM,data:2kBG8j8JHa/dlXgWMdbSobulFdVunf052T1QQfm1X2vpEZx2HPCL87fWea+O0WOg7+eoMYbiShu0Vw1eTjb+687LjU8l4cj2JWIajnYfDGH+ipWXojxj613C3RZV3JfDOclVTwP8fCHu7z7P3fKrsKWb5d3t2ohTT+sGdVdimakAOf192CkufcVIthq2imiWbntiMTOdMGJxyIjqT2Io2H89nSbJXkONsuHCF/PbxhryB2LZbl8aZV32knk=,iv:hpscVc7iO4r/h31vS6Zno2pkEsgA2uR7wD/1PjH1znM=,tag:ypiwFtgeXuj4gOsgTCRTBw==,type:str]
-knot__dnssec_key_secret: ENC[AES256_GCM,data:WPFTLyJIttFtqqTZV2fGN0Tt1vRS318TGmd2YqNzYisE3TBi6Z2aClxuYh56Q+j7TUQwCvga3jd5w017sEz3kA==,iv:umaFHBCy9AZgNFv7uXLCtO0o/NZDAZ1QNg5DcGHWEW8=,tag:oR92C1Uj5iXU9L02MqzGSQ==,type:str]
+knot__keys:
+ - id: ENC[AES256_GCM,data:ff7Z1SHmKVseX4hguwcfrqLKyDKu3Imw1Q==,iv:Ds5T3cvi2XYAa3C8mbgYjN8AHM4FsKR2RTPvykWgy2E=,tag:7Gveno0zsLf3bDOYgkQkbw==,type:str]
+ secret: ENC[AES256_GCM,data:dJcVnqQ6hlA/xp4NQ5s9by+z9cm5oWD7KAlPX+kDsvSJn33YiZ/A/wf0jfT+FeVLaBerQ6WipxZs90rp8rD+dg==,iv:JVby6vORdqChps6cRx1EJPNBC7+M0Vgp1ji1nQtv3K4=,tag:X0KggJahlTOAowTNnpPs2g==,type:str]
+ algorithm: ENC[AES256_GCM,data:r7uojDN+o1yYgaY=,iv:KBWKZbd+5qZstuMh6gseq/vC86UB/l9m4wVZpA4usPk=,tag:gfU3asmwOIgxJQsqPG712g==,type:str]
sops:
age:
- - recipient: age18zgt4y2sd75hxnpe333zz39048ctxpr0q8a3uqh3jajjkyawsdrq8yg5ve
- enc: |
+ - enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArVUxoOFhZdi9PSkZlU3pw
VW0vNVRTSG9EamUvVGR0dnN6ajcwTXdlQkdZCkFGVWxPcTNSOGtJdlpSMXJxOFBX
@@ -11,8 +13,9 @@ sops:
SHhROG5nMGp2d3dwWVJsZk55TW93dU0KnW2ZhU6OhwwPBIxcO1xP+W8DV5Obj6ov
Hgb8MQ6i9FlhAN/P8onBsqvbh0ttBEFQ6aRJj5njKs/MMfKUk9Q25g==
-----END AGE ENCRYPTED FILE-----
- lastmodified: "2026-05-01T17:08:09Z"
- mac: ENC[AES256_GCM,data:TaMWf1ESs8nYzxkElMYtsz+/Be0PtI7FA0q6IFK+ob4dl/EN+AeTD7Pp0MZF8zcRvZ4hF0Ybimet5bwVR+d7UIXlXz3qP//pX68JDCvcLMQuhNtm6Ws+mwVxkpxEvBr1PtxlSvcQ76vH3ryEsXkP84gmlCDEdX1GAZYZ9ZS3Cfk=,iv:g3tzUfTPNUQyOAxWJEFPHg0IAPAzQgwYABHm4mFOOrI=,tag:C6KE/bg/3jS7Wc56y6YOJQ==,type:str]
+ recipient: age18zgt4y2sd75hxnpe333zz39048ctxpr0q8a3uqh3jajjkyawsdrq8yg5ve
+ lastmodified: "2026-09-11T08:40:23Z"
+ mac: ENC[AES256_GCM,data:UrncHXwMWUzs5pciDVUGJ885EBsPfrOFswzDOej2pQOTrp6+YomluHivy/L5niSYb05HaGy+9puw35+d+9SXQbgoI5WbwYo2LQ2MggMWSchnnZoy3O5S6LLjBhcvebDv4/1JGvftyBS1LnU7mmJaMekV5KASXaGyuwKumsdTojA=,iv:HJqCXtqQErP/50tR7LL2eAN574NkVO/3QfYsK1q9tt4=,tag:BniLURhadzSliPYz0cRgkA==,type:str]
pgp:
- created_at: "2026-05-20T02:08:47Z"
enc: |-
@@ -185,4 +188,4 @@ sops:
-----END PGP MESSAGE-----
fp: 41FFAF3D519CF5C039FBD8414BCC213729AF0E49
unencrypted_suffix: _unencrypted
- version: 3.12.1
+ version: 3.13.3
diff --git a/inventories/chaosknoten/host_vars/auth-dns.yaml b/inventories/chaosknoten/host_vars/auth-dns.yaml
index 7d220bcb..035e0328 100644
--- a/inventories/chaosknoten/host_vars/auth-dns.yaml
+++ b/inventories/chaosknoten/host_vars/auth-dns.yaml
@@ -3,18 +3,21 @@ deploy_systemd_resolved_config__enable: false
alloy_config_additional: "{{ lookup('ansible.builtin.template', 'resources/chaosknoten/auth-dns/alloy/knot-exporter.alloy') }}"
-knot__dnssec_key_id: "auth-dns.hamburg.ccc.de-1"
knot__remotes:
- id: erfadns.ber.ccc.de
address: [ "2a02:8000:1000:101::196", "185.106.84.196" ]
+ via: [ "2a00:14b0:4200:3000:124::1", "212.12.48.124" ]
- id: ns.vie.ccc.de
address: [ "2a02:1b8:10:31::228", "146.255.57.228" ]
+ via: [ "2a00:14b0:4200:3000:124::1", "212.12.48.124" ]
+ # - id: cccfr-de-hidden-primary
+ # address: [ "89.163.204.252" ]
knot__catalog_zones:
- domain: "hamburg.ccc.de.catalog."
notify_targets: [ "erfadns.ber.ccc.de" ]
-knot__zones:
+knot__primary_zones:
- domain: "hh.ccc.de."
catalog_member: "hamburg.ccc.de.catalog."
notify_targets: [ "erfadns.ber.ccc.de" ]
@@ -48,3 +51,7 @@ knot__zones:
- domain: "3.2.0.0.0.0.0.f.0.b.4.1.0.0.a.2.ip6.arpa."
notify_targets: [ "erfadns.ber.ccc.de" ]
content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/auth-dns/zones/3.2.0.0.0.0.0.f.0.b.4.1.0.0.a.2.ip6.arpa.zone') }}"
+
+# knot__catalog_secondary_zones:
+# - domain: "cccfr.catalog.invalid"
+# primary: "cccfr-de-hidden-primary"
diff --git a/roles/knot/defaults/main.yaml b/roles/knot/defaults/main.yaml
index 50a3ffb5..572abe28 100644
--- a/roles/knot/defaults/main.yaml
+++ b/roles/knot/defaults/main.yaml
@@ -1,2 +1,11 @@
---
+knot__deploy_prometheus_exporter: true
+knot__log_level: info
knot__remotes: [ ]
+knot__keys: [ ]
+knot__catalog_zones: [ ]
+knot__primary_zones: [ ]
+knot__catalog_secondary_zones: [ ]
+knot__secondary_zones: [ ]
+knot__dynamic_zones: [ ]
+knot__acls: [ ]
diff --git a/roles/knot/meta/argument_specs.yaml b/roles/knot/meta/argument_specs.yaml
index 40a58238..61a529b1 100644
--- a/roles/knot/meta/argument_specs.yaml
+++ b/roles/knot/meta/argument_specs.yaml
@@ -2,14 +2,15 @@
argument_specs:
main:
options:
- knot__dnssec_key_id:
- description: The id of the TSIG key which knot will use for zone transfer signing
+ knot__log_level:
type: str
- required: true
- knot__dnssec_key_secret:
- description: The secret value of the TSIG key which knot will use for zone transfer signing
- type: str
- required: true
+ required: false
+
+ knot__deploy_prometheus_exporter:
+ type: bool
+ required: false
+ description: Whether a prometheus exporter for knot should also be installed
+
knot__remotes:
description:
- A list of definitions for remote nameservers that are used for different purposes
@@ -25,11 +26,19 @@ argument_specs:
type: list
required: true
elements: str
+ key:
+ type: str
+ required: false
+ via:
+ type: list
+ elements: str
+ required: false
+
knot__catalog_zones:
description: A list of catalog zones that will be served by knot
type: list
elements: dict
- required: true
+ required: false
options:
domain:
type: str
@@ -38,11 +47,12 @@ argument_specs:
type: list
elements: str
required: false
- knot__zones:
+
+ knot__primary_zones:
description: A list of user zones that will be served by knot
type: list
elements: dict
- required: true
+ required: false
options:
domain:
type: str
@@ -57,3 +67,105 @@ argument_specs:
content:
type: str
required: true
+
+ knot__dynamic_zones:
+ description: A list of user zones operated via DDNS that will be served by knot
+ type: list
+ elements: dict
+ required: false
+ options:
+ domain:
+ type: str
+ required: true
+ notify_targets:
+ type: list
+ elements: str
+ required: false
+ catalog_member:
+ type: str
+ required: false
+ acl:
+ type: list
+ elements: str
+ required: true
+
+ knot__secondary_zones:
+ description: A list of secondary zones that will be served by knot
+ required: false
+ type: list
+ elements: dict
+ options:
+ domain:
+ type: str
+ required: true
+ notify_targets:
+ type: list
+ elements: str
+ required: false
+ catalog_member:
+ type: str
+ required: false
+ primary:
+ type: str
+ required: true
+
+ knot__catalog_secondary_zones:
+ description: A list of zones which are catalog secondaries and which will generate new zones based on the catalog content
+ type: list
+ elements: dict
+ required: false
+ options:
+ domain:
+ type: str
+ required: true
+ notify_targets:
+ type: list
+ elements: str
+ required: false
+ primary:
+ type: str
+ required: true
+
+ knot__keys:
+ description: TSIG Key stanzas used by knot
+ required: false
+ type: list
+ elements: dict
+ options:
+ id:
+ type: str
+ required: true
+ algorithm:
+ type: str
+ required: false
+ secret:
+ type: str
+ required: true
+
+ knot__acls:
+ description: ACL stanzas
+ required: false
+ type: list
+ elements: dict
+ options:
+ id:
+ type: str
+ required: true
+ address:
+ type: list
+ elements: str
+ required: false
+ key:
+ type: str
+ required: false
+ remote:
+ type: list
+ elements: str
+ required: false
+ action:
+ type: str
+ required: false
+ update_type:
+ type: list
+ elements: str
+ required: false
diff --git a/roles/knot/tasks/02-configure.yaml b/roles/knot/tasks/02-configure.yaml
index e79143fc..fe4be107 100644
--- a/roles/knot/tasks/02-configure.yaml
+++ b/roles/knot/tasks/02-configure.yaml
@@ -22,7 +22,7 @@
- name: Deploy configured zones
become: true
notify: reload knot
- loop: "{{ knot__zones }}"
+ loop: "{{ knot__primary_zones }}"
loop_control:
label: "{{ item.domain }}"
vars:
@@ -34,17 +34,3 @@
group: knot
mode: u=rw,g=r
validate: "kzonecheck -v -o '{{ item.domain }}' %s"
-
-# this seems weird but hear me out:
-# if we don't disable SLAAC, the node automatically gets an address based on IPv6 Router-Advertisements
-# this results in outgoing zone transfers failing because knot will prefer to use the dynamic address over the statically configured one.
-# so because we are configuring a DNS Nameserver where known IP-Addresses are actually important for ACL reasons, SLAAC is disabled
-- name: Disable IPv6 SLAAC
- become: true
- notify: netplan apply
- ansible.builtin.template:
- src: "netplan-disable-ra.yaml"
- dest: "/etc/netplan/10-disable-ra.yaml"
- owner: root
- group: root
- mode: u=rw,g=,o=
diff --git a/roles/knot/tasks/main.yaml b/roles/knot/tasks/main.yaml
index bdf5cf78..145dc582 100644
--- a/roles/knot/tasks/main.yaml
+++ b/roles/knot/tasks/main.yaml
@@ -1,4 +1,7 @@
---
-- ansible.builtin.import_tasks: 01-install.yaml # noqa: name[missing]
-- ansible.builtin.import_tasks: 02-configure.yaml # noqa: name[missing]
-- ansible.builtin.import_tasks: 03-configure-exporter.yaml # noqa: name[missing]
+- tags: [ knot ]
+ block:
+ - ansible.builtin.import_tasks: 01-install.yaml # noqa: name[missing]
+ - ansible.builtin.import_tasks: 02-configure.yaml # noqa: name[missing]
+ - ansible.builtin.include_tasks: 03-configure-exporter.yaml # noqa: name[missing]
+ when: knot__deploy_prometheus_exporter
diff --git a/roles/knot/templates/knot.conf.j2 b/roles/knot/templates/knot.conf.j2
index 45a0f8d9..03c46ffe 100644
--- a/roles/knot/templates/knot.conf.j2
+++ b/roles/knot/templates/knot.conf.j2
@@ -9,15 +9,17 @@ server:
log:
- target: syslog
- any: info
+ any: {{ knot__log_level }}
database:
storage: "/var/lib/knot"
key:
- - id: {{ knot__dnssec_key_id }}
- algorithm: hmac-sha512
- secret: "{{ knot__dnssec_key_secret }}"
+ {% for i_key in knot__keys -%}
+ - id: "{{ i_key.id }}"
+ algorithm: "{{ i_key.algorithm | default("hmac-sha256") }}"
+ secret: "{{ i_key.secret }}"
+ {% endfor %}
remote:
# static, external and public remote used for DNSSEC KSK checking
@@ -28,6 +30,13 @@ remote:
{% for i_remote in knot__remotes -%}
- id: "{{ i_remote.id }}"
address: [ {% for i_addr in i_remote.address %}"{{ i_addr}}"{% if not loop.last %},{% endif %} {% endfor %} ]
+ {% if i_remote.via | default(None) -%}
+ via: [ {% for i_via in i_remote.via %}"{{ i_via }}"{% if not loop.last %}, {% endif %}{% endfor %} ]
+ {% endif -%}
+ {% if i_remote.key | default(None) -%}
+ key: "{{ i_remote.key }}"
+ {% endif %}
+
{% endfor %}
{% endif %}
@@ -46,10 +55,33 @@ policy:
nsec3: true
nsec3-salt-length: 0
+# explicit acl configurations
+# we mostly rely on automatic ACLs but for some dynamic zones, explicit allow stanzas are required
+acl:
+ {% for i_acl in knot__acls -%}
+ - id: "{{ i_acl.id }}"
+ {% if i_acl.address | default(None) -%}
+ address: [ {% for i_addr in i_acl.address %}"{{ i_addr}}"{% if not loop.last %}, {% endif %}{% endfor %} ]
+ {% endif -%}
+ {% if i_acl.key | default(None) -%}
+ key: "{{ i_acl.key }}"
+ {% endif -%}
+ {% if i_acl.remote | default(None) -%}
+ remote: "{{ i_acl.remote }}"
+ {% endif -%}
+ {% if i_acl.action | default(None) -%}
+ action: "{{ i_acl.action }}"
+ {% endif -%}
+ {% if i_acl.update_type | default(None) -%}
+ update-type: [ {% for i_addr in i_acl.update_type %}"{{ i_addr}}"{% if not loop.last %}, {% endif %}{% endfor %} ]
+ {% endif %}
+
+ {% endfor %}
+
# define default settings that apply to all zones
template:
- # template for general-purpose user zones
- - id: default
+ # template for general-purpose primary zones
+ - id: primary
storage: "/etc/knot/zones"
file: "%s.zone"
semantic-checks: on
@@ -57,17 +89,45 @@ template:
zonefile-load: difference-no-serial
serial-policy: dateserial
journal-content: all
- default-ttl: 7200
dnssec-signing: on
dnssec-policy: default
- {# catalog-role: member #}
- {# catalog-zone: hamburg.ccc.de.catalog. #}
+ # template for generic secondary zones
+ - id: secondary
+ storage: "/var/lib/knot/secondary_zones/"
+ dnssec-signing: off
+
+ # template for zones that support dynamic updates
+ - id: dynamic-primary
+ storage: "/var/lib/knot/dynamic_zones/"
+ zonefile-load: whole
+ journal-content: changes
+ dnssec-signing: on
+ dnssec-policy: default
# template for automatically created special zones
- id: catalog
catalog-role: generate
dnssec-signing: off
+ storage: "/var/lib/knot/catalog_zones/"
+
+ # template for secondary catalog zones (they are interpreted and generate secondary zones based on their content)
+ - id: catalog-secondary
+ catalog-role: interpret
+ dnssec-signing: off
+ storage: "/var/lib/knot/secondary_zones/"
+
+ # templates for secondary catalog member zones
+ # these templates are for zones which get spawned by catalog zones
+ # unfortunately we need one template per interpreted catalog zone because it must include all information required for that zone to work as secondary
+ # it reuses master and notify values from the catalog zones
+ {% for i_zone in knot__catalog_secondary_zones -%}
+ - id: catalog-secondary-member-{{ i_zone.domain }}
+ storage: "/var/lib/knot/secondary_zones/"
+ dnssec-signing: off
+ master: {{ i_zone.primary }}
+
+ {% endfor %}
# define zones on this server
@@ -80,10 +140,10 @@ zone:
notify: [ {% for i_notif in i_zone.notify_targets | default([]) %}"{{ i_notif }}"{% if not loop.last %}, {% endif %}{% endfor %} ]
{% endfor %}
- # normal zones
- {% for i_zone in knot__zones -%}
+ # primary zones
+ {% for i_zone in knot__primary_zones -%}
- domain: "{{ i_zone.domain }}"
- template: default
+ template: primary
notify: [ {% for i_notif in i_zone.notify_targets | default([]) %}"{{ i_notif }}"{% if not loop.last %}, {% endif %}{% endfor %} ]
{% if i_zone.catalog_member | default(False) -%}
catalog-role: member
@@ -92,3 +152,39 @@ zone:
{% endfor %}
+ # dynamic primary zones
+ {% for i_zone in knot__dynamic_zones -%}
+ - domain: "{{ i_zone.domain }}"
+ template: "dynamic-primary"
+ notify: [ {% for i_notif in i_zone.notify_targets | default([]) %}"{{ i_notif }}"{% if not loop.last %}, {% endif %}{% endfor %} ]
+ acl: [ {% for i_acl in i_zone.acl %}"{{ i_acl }}"{% if not loop.last %}, {% endif %}{% endfor %} ]
+ {% if i_zone.catalog_member | default(False) -%}
+ catalog-role: member
+ catalog-zone: "{{ i_zone.catalog_member }}"
+ {% endif %}
+
+ {% endfor %}
+
+ # secondary zones
+ {% for i_zone in knot__secondary_zones -%}
+ - domain: "{{ i_zone.domain }}"
+ template: secondary
+ master: {{ i_zone.primary }}
+ notify: [ {% for i_notif in i_zone.notify_targets | default([]) %}"{{ i_notif }}"{% if not loop.last %}, {% endif %}{% endfor %} ]
+ {% if i_zone.catalog_member | default(False) -%}
+ catalog-role: member
+ catalog-zone: "{{ i_zone.catalog_member }}"
+ {% endif %}
+
+ {% endfor %}
+
+ # secondary catalog zones
+ {% for i_zone in knot__catalog_secondary_zones -%}
+ - domain: "{{ i_zone.domain }}"
+ template: catalog-secondary
+ catalog-template: catalog-secondary-member-{{ i_zone.domain }}
+ master: {{ i_zone.primary }}
+ notify: [ {% for i_notif in i_zone.notify_targets | default([]) %}"{{ i_notif }}"{% if not loop.last %}, {% endif %}{% endfor %} ]
+
+ {% endfor %}
+
diff --git a/roles/knot/templates/netplan-disable-ra.yaml b/roles/knot/templates/netplan-disable-ra.yaml
deleted file mode 100644
index bad31a5a..00000000
--- a/roles/knot/templates/netplan-disable-ra.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-# {{ ansible_managed }}
-network:
- ethernets:
- {%- for i_iface_name in ansible_facts["interfaces"] -%}
- {%- if i_iface_name != "lo" -%}
- {%- set i_iface = ansible_facts[i_iface_name] %}
-
- {{ i_iface_name }}:
- match:
- macaddress: "{{ i_iface.macaddress }}"
- accept-ra: false
- {% endif %}
- {% endfor %}