update knot role to support more operational variety

Now secondary zones, dynamic zones, interpeted catalog zones, etc.
are supported by our knot role
This commit is contained in:
lilly 2026-09-11 10:09:48 +02:00
commit 4bdd36b8f7
Signed by: lilly
SSH key fingerprint: SHA256:y9T5GFw2A20WVklhetIxG1+kcg/Ce0shnQmbu1LQ37g
8 changed files with 264 additions and 61 deletions

View file

@ -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: [ ]

View file

@ -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

View file

@ -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=

View file

@ -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

View file

@ -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 %}

View file

@ -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 %}