diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..4dc6823 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,8 @@ +--- + +skip_list: + - meta-runtime[unsupported-version] + - galaxy[no-changelog] + - galaxy[version-incorrect] + - name[casing] + - var-naming[no-role-prefix] diff --git a/.forgejo/workflows/ansible-galaxy.yml b/.forgejo/workflows/ansible-galaxy.yml new file mode 100644 index 0000000..d5b5efc --- /dev/null +++ b/.forgejo/workflows/ansible-galaxy.yml @@ -0,0 +1,29 @@ +--- + +name: Ansible Galaxy + +on: # noqa yaml[truthy] + push: + tags: + - 'v*' + +jobs: + deploy: + runs-on: docker + steps: + + - uses: actions/checkout@v4 + + - name: Set version in galaxy.yml + run: | + VERSION=${GITHUB_REF#refs/tags/v} + sed -re "s/^version:.*$/version: ${VERSION}/" -i galaxy.yml + + - name: Upload collection to Ansible Galaxy + env: + GALAXY_API_KEY: ${{ secrets.GALAXY_API_KEY }} + run: | + apt update; apt install --yes python3-pip + pip3 install --break-system-packages ansible + ansible-galaxy collection build + ansible-galaxy collection publish --api-key=${GALAXY_API_KEY} s3lph-nextcloud*tar.gz diff --git a/.forgejo/workflows/ansible-lint.yml b/.forgejo/workflows/ansible-lint.yml new file mode 100644 index 0000000..b42b17b --- /dev/null +++ b/.forgejo/workflows/ansible-lint.yml @@ -0,0 +1,17 @@ +--- + +name: Ansible Lint +on: [push, pull_request] # noqa yaml[truthy] + +jobs: + build: + runs-on: docker + + steps: + + - uses: actions/checkout@v4 + + - run: | + apt update; apt install --yes python3-pip + pip3 install --break-system-packages ansible-lint + ansible-lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f47e11 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +s3lph-nextcloud*.tar.gz \ No newline at end of file diff --git a/docs/group_vars/all/zones/zone.example.org.yml b/docs/group_vars/all/zones/zone.example.org.yml index b35d91c..3fb7e1d 100644 --- a/docs/group_vars/all/zones/zone.example.org.yml +++ b/docs/group_vars/all/zones/zone.example.org.yml @@ -1,7 +1,7 @@ --- -# Replace example.org with your zone name -knot_zone_example.org: +# Replace example_org/example.org with your zone name +knot_zone_example_org: masters: - ns1.example.org diff --git a/docs/playbook.yml b/docs/playbook.yml index e00aafa..36e61a7 100644 --- a/docs/playbook.yml +++ b/docs/playbook.yml @@ -1,5 +1,6 @@ --- -- hosts: nameserver +- name: Install and configure Knot + hosts: nameserver roles: - s3lph.nameserver.knot diff --git a/galaxy.yml b/galaxy.yml index 1858f33..c8a454b 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -7,7 +7,7 @@ namespace: s3lph name: nameserver # The version of the collection. Must be compatible with semantic versioning -version: "0.3.7" +version: "0.4.0" # The path to the Markdown (.md) readme file. This path is relative to the root of the collection readme: README.md @@ -15,8 +15,7 @@ readme: README.md # A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) # @nicks:irc/im.site#channel' authors: -- s3lph <1375407-s3lph@users.noreply.gitlab.com> - + - s3lph ### OPTIONAL but strongly recommended # A short summary description of the collection @@ -25,15 +24,12 @@ description: Authoritative nameserver setup using knot # Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only # accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' license: -- MIT - -# The path to the license file for the collection. This path is relative to the root of the collection. This key is -# mutually exclusive with 'license' -license_file: '' + - MIT # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character # requirements as 'namespace' and 'name' tags: + - application - dns - knot - nameserver @@ -46,20 +42,19 @@ tags: dependencies: {} # The URL of the originating SCM repository -repository: https://gitlab.com/s3lph/ansible-collection-nameserver +repository: https://git.kabelsalat.ch/s3lph/ansible-collection-nameserver # The URL to any online docs -documentation: https://gitlab.com/s3lph/ansible-collection-nameserver +documentation: https://git.kabelsalat.ch/s3lph/ansible-collection-nameserver # The URL to the homepage of the collection/project -homepage: https://gitlab.com/s3lph/ansible-collection-nameserver +homepage: https://git.kabelsalat.ch/s3lph/ansible-collection-nameserver # The URL to the collection issue tracker -issues: https://gitlab.com/s3lph/ansible-collection-nameserver/-/issues +issues: https://git.kabelsalat.ch/s3lph/ansible-collection-nameserver/issues # A list of file glob-like patterns used to filter any files or directories that should not be included in the build # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This # uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', # and '.git' are always filtered build_ignore: [] - diff --git a/meta/runtime.yml b/meta/runtime.yml new file mode 100644 index 0000000..c8a010b --- /dev/null +++ b/meta/runtime.yml @@ -0,0 +1,52 @@ +--- +# Collections must specify a minimum required ansible version to upload +# to galaxy +requires_ansible: '>=2.10' + +# Content that Ansible needs to load from another location or that has +# been deprecated/removed +# plugin_routing: +# action: +# redirected_plugin_name: +# redirect: ns.col.new_location +# deprecated_plugin_name: +# deprecation: +# removal_version: "4.0.0" +# warning_text: | +# See the porting guide on how to update your playbook to +# use ns.col.another_plugin instead. +# removed_plugin_name: +# tombstone: +# removal_version: "2.0.0" +# warning_text: | +# See the porting guide on how to update your playbook to +# use ns.col.another_plugin instead. +# become: +# cache: +# callback: +# cliconf: +# connection: +# doc_fragments: +# filter: +# httpapi: +# inventory: +# lookup: +# module_utils: +# modules: +# netconf: +# shell: +# strategy: +# terminal: +# test: +# vars: + +# Python import statements that Ansible needs to load from another location +# import_redirection: +# ansible_collections.ns.col.plugins.module_utils.old_location: +# redirect: ansible_collections.ns.col.plugins.module_utils.new_location + +# Groups of actions/modules that take a common set of options +# action_groups: +# group_name: +# - module1 +# - module2 diff --git a/roles/knot/README.md b/roles/knot/README.md new file mode 100644 index 0000000..d592a9d --- /dev/null +++ b/roles/knot/README.md @@ -0,0 +1,5 @@ +# Role s3lph.nameserver.knot + +Documentation in `meta/argument_specs.yml`. + +A usage example can be found in the `docs` folder of the collection. diff --git a/roles/knot/defaults/main.yml b/roles/knot/defaults/main.yml index 1278fa2..f46c4ab 100644 --- a/roles/knot/defaults/main.yml +++ b/roles/knot/defaults/main.yml @@ -1,15 +1,15 @@ --- -knot_repository_install: no +knot_repository_install: false knot_repository_url: https://deb.knot-dns.cz/knot/ -knot_repository_distribution: "{{ ansible_distribution_release }}" +knot_repository_distribution: "{{ ansible_facts.distribution_release }}" knot_server_rundir: /run/knot knot_server_user: knot knot_server_group: knot -knot_server_identity: "{{ ansible_hostname }}" -knot_server_nsid: "{{ ansible_hostname }}" -knot_server_version: "{{ ansible_hostname }}" +knot_server_identity: "{{ ansible_facts.hostname }}" +knot_server_nsid: "{{ ansible_facts.hostname }}" +knot_server_version: "{{ ansible_facts.hostname }}" knot_server_listen: - "::@53" - "0.0.0.0@53" diff --git a/roles/knot/handlers/main.yml b/roles/knot/handlers/main.yml index 44462d5..9781632 100644 --- a/roles/knot/handlers/main.yml +++ b/roles/knot/handlers/main.yml @@ -1,11 +1,11 @@ --- - name: reload knot - service: + ansible.builtin.service: name: knot state: reloaded - name: restart knot - service: + ansible.builtin.service: name: knot state: restarted diff --git a/roles/knot/meta/argument_specs.yml b/roles/knot/meta/argument_specs.yml new file mode 100644 index 0000000..8e3f2bb --- /dev/null +++ b/roles/knot/meta/argument_specs.yml @@ -0,0 +1,365 @@ +--- + +argument_specs: + + main: + version_added: "0.0.1" + short_description: Install and configure Knot. + description: + - Install and configure the L(Knot,https://knot.readthedocs.io/en/latest/index.html) authoritative namesever. + - Zones are configured through YAML hostvars, rather than simply deploying zone files. + - "Execution of this role can be limited using the following tags:" + - "C(role::knot:install): Install knot from distribution packages or upstream repositories." + - "C(role::knot:config): Render the Knot configuration file." + - "C(role::knot:zones): Render the zone files for which Knot is authoritative." + - "C(role::knot): Apply all of the above." + author: s3lph + options: + knot_repository_install: + description: + - If true, install knot from the L(upstream repositories,https://deb.knot-dns.cz/knot/). + - If false, install knot from the default system repositories. + type: bool + default: false + knot_repository_url: + description: URL of the upstream repository. + type: str + default: https://deb.knot-dns.cz/knot/ + knot_repository_distribution: + description: Suite name (distribution codename) of the upstream repository. + type: str + default: "C(ansible_facts.distribution_release)" + + knot_server_rundir: + description: Runtime directory where Knot should e.g. write its control socket to. + type: str + default: /run/knot + knot_server_user: + description: The user to run knot as. + type: str + default: knot + knot_server_group: + description: The group to run knot as. + type: str + default: knot + knot_server_identity: + description: + - Response to a C(id.server. CH TXT) or C(hostname.bind. CH TXT) query. + - Set to an empty value to disable. + type: str + default: "C(ansible_facts.hostname)" + knot_server_nsid: + description: + - The RFC 5001 NSID to include in responses when requested by the resolver. + - Set to an empty value to disable. + type: str + default: "C(ansible_facts.hostname)" + knot_server_version: + description: + - Response to a C(version.server. CH TXT) or C(version.bind. CH TXT) query. + - Set to an empty value to disable. + type: str + default: "C(ansible_facts.hostname)" + knot_server_listen: + description: + - The list of interfaces to listen on. + - Host and port are separated with an C(@) sign. + type: list + elements: str + default: ["::@53", "0.0.0.0@53"] + knot_dns_addresses: + description: + - Addresses under which the nameserver is reachable externally. + - Used for zone replication and notification and for KSK submission checks. + - Required to be set for all members of a replicated setup. + type: list + elements: str + default: [] + + knot_log_targets: + description: + - Logging configuration of Knot. + - Every entry is a dict consisting of at least a C(target) key. + - >- + Details on logging configuration can be found in the + U(upstream documentation,https://knot.readthedocs.io/en/latest/reference.html#log-section) + type: list + elements: dict + default: + - target: syslog + level: info + + knot_zone_master_storage_path: + description: Location from where Knot should read zonefiles for which it is the primary server. + type: str + default: /var/lib/knot/master + knot_zone_replica_storage_path: + description: Location where Knot should store replicated zonefiles. + type: str + default: /var/lib/knot/replica + knot_zone_semantic_checks: + description: If set to C(on), Knot will perform additional zonefile checks. + type: str + default: 'on' + knot_zone_dnssec_signing: + description: + - If set to C(on), Knot will automatically configure DNSSEC zone signing. + - If set to C(off), Knot will not sign zones automatically. + type: str + default: 'on' + + knot_dnssec_policy_algorithm: + description: The DNSSEC signing algorithm to use. + type: str + default: ed25519 + knot_dnssec_policy_nsec3: + description: + - If set to C(on), C(NSEC3) is used instead of C(NSEC). + - If set to C(off), C(NSEC) is used, which allows full zone enumeration. + type: str + default: 'on' + knot_dnssec_policy_ksk_size: + description: + - Size (in bits) of the KSK. + - >- + Permitted values in combination with O(knot_dnssec_policy_algorithm) are documented in the + U(upstream documentation,https://knot.readthedocs.io/en/latest/reference.html#ksk-size). + type: int + default: 256 + knot_dnssec_policy_zsk_size: + description: + - Size (in bits) of the KSK. + - >- + Permitted values in combination with O(knot_dnssec_policy_algorithm) are documented in the + U(upstream documentation,https://knot.readthedocs.io/en/latest/reference.html#ksk-size). + type: int + default: 256 + knot_dnssec_policy_zsk_lifetime: + description: Time after which the ZSK should be rotated automatically. + type: str + default: 30d + knot_dnssec_policy_ksk_lifetime: + description: Time after which the KSK should be rotated automatically. + type: str + default: "0" + knot_dnssec_policy_cds_publish: + description: + - If and when to publish C(CDS) and C(CDNSKEY) records. + - >- + Supported values and their meaning are documented in the + U(upstream documentation,https://knot.readthedocs.io/en/latest/reference.html#cds-cdnskey-publish) + - Do not use the C(double-ds) policy when performing automated KSK rollovers. It will break the chain of trust. + type: str + default: 'always' + knot_dnssec_policy_propagation_delay: + description: Additional time to wait before continuing with each step of a key rollover. + type: str + default: 1h + + knot_dnssec_submission_check_interval: + description: How often during a KSK rollover Knot should check submission nameservers for the new DS RRSet. + type: str + default: 1h + knot_dnssec_submission_timeout: + description: + - >- + Time after which a KSK submission to the parent nameserver should automatically be considered successful, + even if the new DS RRSet has not been found on the submussion nameserver. + type: str + default: "0" + + knot_tsig_key: + description: + - The TSIG key used by this host for zone transfers or updates. + - >- + This shared key will be configured automatically for all zones involving this host in + O(replicas) or O(updaters). + type: dict + default: null + options: + name: + description: + - The name of the key. + - Should be a FQDN including the trailing C(.), e.g. C(tsig.hostname.example.org.). + type: str + required: true + algorithm: + description: The key algorithm, e.g. C(hmac-sha384). + type: str + required: true + secret: + description: + - The shared secret of this key. + - Generate a new key with e.g. C(keymgr -t tsig.foo.example.org. hmac-sha384). + - This is a secret. Protect it e.g. with C(ansible-vault)! + type: str + required: true + + knot_zone_*: + description: + - Zone configurations, one top-level dict per zone. + - >- + Recomendation: You can use an arbitrary string after C(knot_zone_), but we recommend to use the zone name + with C(.) replaced by (_). + - "Recommendation: Keep one file per zone in C(group_vars/nameservers/zones/zone_.yml)." + type: dict + required: true + options: + name: + description: Fully qualified name of the zone, including the trailing C(.). + type: str + required: true + soa: + description: Contains the values required to synthesize the C(SOA) record of the zone apex. + type: dict + required: true + options: + class: + description: Class for the records in this zone, usually C(IN). + type: str + required: true + primary: + description: + - FQDN of the autoritative nameserver of the zone. + - Also known as C(MNAME). + type: str + required: true + rname: + description: + - Email address of the administrator. + - The C(@) must be replaced with a C(.). + - C(.) in the localpart must be escaled as C(\.). + type: str + required: true + refresh: + description: + - How often (in seconds) replicas should query the primary for SOA changes. + type: int + required: true + retry: + description: + - How long (in seconds) replicas should wait to retry a failed zone transfer. + type: int + required: true + expire: + description: + - How long after the last update (in seconds) replicas should stop serving a zone. + type: int + required: true + ttl: + description: + - Default TTL (in seconds) for all entries in the zone, and TTL of the SOA record. + type: int + required: true + min_ttl: + description: + - TTL (in seconds) of negative responses. + type: int + required: true + records: + description: + - All the records in this zone go here. + type: list + elements: dict + options: + name: + description: + - Name of the record. + - Records without a trailing C(.) are relative to the zone apex. + - Use C(@) to refer to the zone apex itself. + type: str + required: true + ttl: + description: + - TTL of this record. + - If omitted, defaults to the zone TTL set in the O(soa) section. + type: int + default: O(soa.ttl) + class: + description: + - Class of this record. + - If omitted, defaults to the class set in the O(soa) section. + type: str + default: O(soa.class) + type: + description: Type of the record, e.g. C(AAAA), C(A) or C(CNAME). + type: str + required: true + value: + description: + - Value of the record. + - >- + Length restrictions for TXT records apply. Subdivide them with single-quoted double quotes + e.g. C('"first part of the txt record " "second part of the txt record"') + type: str + required: true + masters: + description: + - Hostnames of servers which should act as a primary for this zone. + - Zonefile will be deployed to hosts whose C(inventory_hostname) is contained in this list. + type: list + default: [] + replicas: + description: + - Hostnames of servers which should at as a replica for this zone. + - >- + Hosts whose C(inventory_hostname) is contained in this list are automatically configured + to replicate this zone. + updaters: + description: + - Hostnames of servers which should be permitted to submit TSIG zone updates for this zone. + - >- + The O(knot_tsig_key)s of hosts whose C(inventory_hostname) is contained in this list are configured as + permitted TSIG updaters for this zone. + - The inventory entry for this host can be a dummy. Its hostvars are only used to fetch the key. + parents: + description: + - Hostnames of servers which should be checked for KSK submissions. + - >- + The O(knot_dns_addresses) of hosts whose C(inventory_hostname) is contained in this list are configured + as KSK submission servers, which are regularly checked to verify the upstream DS RRSet. + - The inventory entry for this host can be a dummy. Its hostvars are only used to hold its IPs. + algorithm: + description: Zone-specific override for O(knot_dnssec_policy_algorithm). + type: str + default: O(knot_dnssec_policy_algorithm) + ksk_size: + description: Zone-specific override for O(knot_dnssec_policy_ksk_size). + type: str + default: O(knot_dnssec_policy_ksk_size) + zsk_size: + description: Zone-specific override for O(knot_dnssec_policy_zsk_size). + type: str + default: O(knot_dnssec_policy_zsk_size) + ksk_lifetime: + description: Zone-specific override for O(knot_dnssec_policy_ksk_lifetime). + type: str + default: O(knot_dnssec_policy_ksk_lifetime) + zsk_lifetime: + description: Zone-specific override for O(knot_dnssec_policy_zsk_lifetime). + type: str + default: O(knot_dnssec_policy_zsk_lifetime) + propagation_delay: + description: Zone-specific override for O(knot_dnssec_policy_propagation_delay). + type: str + default: O(knot_dnssec_policy_propagation_delay) + cds_cdnskey_publish: + description: Zone-specific override for O(knot_dnssec_policy_cds_publish). + type: str + default: O(knot_dnssec_policy_cds_publish) + sign_on_secondary: + description: + - Whether Knot should sign this zone even if it is not the primary nameserver. + - Useful if Knot is used with a hidden primary that does not support DNSSEC. + type: bool + default: false + replicate: + description: + - >- + Note: This option is used for more complex replication hierarchies. Chances are you want to use + O(replicas) instead. + - Configure further replication to other nameservers even if the server is already a replica itself. + - This works in addition to the replication configured through O(masters)/O(replicas). + - Takes a dict where each upstream is mapped to a list of downstreams. + type: dict + default: {} diff --git a/roles/knot/tasks/config.yml b/roles/knot/tasks/config.yml index ef8eb4b..8d46efb 100644 --- a/roles/knot/tasks/config.yml +++ b/roles/knot/tasks/config.yml @@ -1,12 +1,12 @@ --- -- name: render knot master config - template: +- name: Render knot master config + ansible.builtin.template: src: etc/knot/knot.conf.j2 dest: /etc/knot/knot.conf owner: knot group: knot - mode: 0640 + mode: "0640" vars: zones: "{{ hostvars[inventory_hostname] | dict2items | selectattr('key', 'match', '^knot_zone_.+$') | map(attribute='value') | list }}" notify: restart knot diff --git a/roles/knot/tasks/install.yml b/roles/knot/tasks/install.yml index 2faede8..6b8ae29 100644 --- a/roles/knot/tasks/install.yml +++ b/roles/knot/tasks/install.yml @@ -1,26 +1,24 @@ --- -- name: install knot repo key - ansible.builtin.apt_key: - url: https://deb.knot-dns.cz/apt.gpg - keyring: /etc/apt/trusted.gpg.d/knot.gpg +- name: Install knot repository + ansible.builtin.deb822_repository: + name: knot + types: deb + uris: "{{ knot_repository_url }}" + suites: "{{ knot_repository_distribution }}" + components: main + signed_by: https://deb.knot-dns.cz/apt.gpg when: knot_repository_install -- name: install knot repository - ansible.builtin.apt_repository: - repo: "deb {{ knot_repository_url }} {{ knot_repository_distribution }} main" - filename: knot - when: knot_repository_install - -- name: install dependencies +- name: Install dependencies ansible.builtin.package: name: - knot - knot-dnsutils - knot-dnssecutils -- name: start and enable knot +- name: Start and enable knot ansible.builtin.service: name: knot state: started - enabled: yes + enabled: true diff --git a/roles/knot/tasks/main.yml b/roles/knot/tasks/main.yml index 85e1063..d7ef7fd 100644 --- a/roles/knot/tasks/main.yml +++ b/roles/knot/tasks/main.yml @@ -1,19 +1,19 @@ --- -- name: install knot - import_tasks: install.yml +- name: Install knot + ansible.builtin.import_tasks: install.yml tags: - "role::knot" - "role::knot:install" -- name: render zonefiles - import_tasks: zones.yml +- name: Render zonefiles + ansible.builtin.import_tasks: zones.yml tags: - "role::knot" - "role::knot:zones" -- name: configure knot - import_tasks: config.yml +- name: Configure knot + ansible.builtin.import_tasks: config.yml tags: - "role::knot" - "role::knot:config" diff --git a/roles/knot/tasks/zones.yml b/roles/knot/tasks/zones.yml index c1fd06e..ea10c70 100644 --- a/roles/knot/tasks/zones.yml +++ b/roles/knot/tasks/zones.yml @@ -1,26 +1,26 @@ --- -- name: create knot zone directories - file: +- name: Create knot zone directories + ansible.builtin.file: path: "{{ item }}" state: directory owner: knot group: knot - mode: 0750 + mode: "0750" loop: - "{{ knot_zone_master_storage_path }}" - "{{ knot_zone_replica_storage_path }}" -- name: make sure all zones have a name - assert: +- name: Make sure all zones have a name + ansible.builtin.assert: that: - "'name' in item.value" - "item.value.name | type_debug == 'str'" fail_msg: "{{ item.key }} does not have a name" loop: "{{ hostvars[inventory_hostname] | dict2items | selectattr('key', 'match', '^knot_zone_.+$') | list }}" -- name: make sure all zones have at least one master defined - assert: +- name: Make sure all zones have at least one master defined + ansible.builtin.assert: that: - "'masters' in item.value" - "item.value.masters | type_debug == 'list'" @@ -28,13 +28,13 @@ fail_msg: "{{ item.key }} does not have a zone master" loop: "{{ hostvars[inventory_hostname] | dict2items | selectattr('key', 'match', '^knot_zone_.+$') | list }}" -- name: render knot zone files - template: +- name: Render knot zone files + ansible.builtin.template: src: var/lib/knot/master/zone.j2 dest: "{{ knot_zone_master_storage_path }}/{{ item.name }}zone" owner: knot group: knot - mode: 0640 + mode: "0640" validate: /usr/bin/kzonecheck -v %s vars: zone: "{{ item }}"