diff --git a/defaults/main.yml b/defaults/main.yml index 68e7a08..b3c35fe 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -41,7 +41,8 @@ bind9_options: allow_update_forwarding: [] auth_nxdomain: false blackhole: [] - dnssec_validation: true + dnssec_accept_expired: false + dnssec_validation: "auto" forwarders: - ip: "8.8.8.8" # Google IPv4 port: "53" @@ -60,6 +61,7 @@ bind9_options: - ip: "2620:0:ccd::2" # OpenDNS IPv6 port: "53" interface_interval: 0 + key_directory: "/var/named/dnssec-keys" listen_on_ipv4: - "127.0.0.1" listen_on_ipv6: @@ -83,6 +85,23 @@ bind9_rndc_key: algorithm: "" secret: "" +bind9_dnssec_keys: [] +# - origin: "hellenthal.cryptic.systems" +# key_signing_key: +# private: +# filename: "{{ bind9_options.key_directory }}/example.com.private" +# content: "private key" +# public: +# filename: "{{ bind9_options.key_directory }}/example.com.private" +# content: "public key" +# zone_signing_key: +# private: +# filename: "{{ bind9_options.key_directory }}/example.com.private" +# content: "private key" +# public: +# filename: "{{ bind9_options.key_directory }}/example.com.private" +# content: "public key" + bind9_statics: enabled: true channels: @@ -103,41 +122,47 @@ bind9_views: [] # - "!internalnets" # - "any" # zones: -# - allow_notify: [] -# allow_query: -# - "any" -# allow_query_on: [] -# allow_update: [] -# allow_update_forwarding: [] -# allow_transfer: [] +# - config: +# allow_notify: [] +# allow_query: +# - "any" +# allow_query_on: [] +# allow_update: [] +# allow_update_forwarding: [] +# allow_transfer: [] +# file: zones/external/db.local.example +# origin: "example.local." +# type: master +# notify: true # file: zones/external/db.local.example -# origin: "example.local." -# type: master -# notify: true # - name: internal # match_clients: # - "!192.168.178.1" # - "internalnets" # - "127.0.0.0/8" # zones: -# - allow_notify: [] -# allow_query: -# - "any" -# allow_query_on: [] -# allow_update: [] -# allow_update_forwarding: [] -# allow_transfer: [] +# - config: +# allow_notify: [] +# allow_query: +# - "any" +# allow_query_on: [] +# allow_update: [] +# allow_update_forwarding: [] +# allow_transfer: [] +# file: zones/internal/db.local.example +# origin: "example.local." +# type: master # file: zones/internal/db.local.example -# origin: "example.local." -# type: master -# - allow_notify: [] -# allow_query: [] -# allow_query_on: [] -# allow_update: [] -# allow_update_forwarding: [] -# allow_transfer: [] -# forward: only -# forwarders: -# - 192.168.175.1 -# origin: "gitlab-runner.external.local." -# type: forward +# - config: +# allow_notify: [] +# allow_query: [] +# allow_query_on: [] +# allow_update: [] +# allow_update_forwarding: [] +# allow_transfer: [] +# forward: only +# forwarders: +# - 192.168.175.1 +# origin: "gitlab-runner.external.local" +# type: forward +# file: "gitlab-runner.external.local" diff --git a/tasks/create_dnssec_files.yml b/tasks/create_dnssec_files.yml new file mode 100644 index 0000000..1ab79e2 --- /dev/null +++ b/tasks/create_dnssec_files.yml @@ -0,0 +1,25 @@ +--- + +- name: "Create private DNSSEC: {{ bind9_dnssec_key.origin }}" + ansible.builtin.copy: + dest: "{{ item.private.filename }}" + content: "{{ item.private.content }}" + owner: "{{ bind_unix_user }}" + group: "{{ bind_unix_group }}" + mode: "0600" + no_log: true + with_items: + - "{{ bind9_dnssec_key.key_signing_key }}" + - "{{ bind9_dnssec_key.zone_signing_key }}" + +- name: "Create public DNSSEC: {{ bind9_dnssec_key.origin }}" + ansible.builtin.copy: + dest: "{{ item.public.filename }}" + content: "{{ item.public.content }}" + owner: "{{ bind_unix_user }}" + group: "{{ bind_unix_group }}" + mode: "0644" + no_log: true + with_items: + - "{{ bind9_dnssec_key.key_signing_key }}" + - "{{ bind9_dnssec_key.zone_signing_key }}" diff --git a/tasks/main.yml b/tasks/main.yml index 23bf025..ebf139f 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -41,6 +41,47 @@ state: absent with_items: "{{ files_to_delete.files }}" +- name: Remove existing signed zone files + block: + - name: Find existing signed zone files + ansible.builtin.find: + path: "{{ bind_config_directory }}" + recurse: true + patterns: "*.signed" + register: files_to_delete + - name: Delete existing signed zone files + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + with_items: "{{ files_to_delete.files }}" + +- name: Remove existing DNSSEC key directory + block: + - name: Check if DNSSEC key directory exists + ansible.builtin.stat: + path: "{{ bind9_options.key_directory }}" + register: _stat_bind9_options_key_directory + - name: Remove DNSSEC key directory + ansible.builtin.file: + path: "{{ bind9_options.key_directory }}" + state: "absent" + when: _stat_bind9_options_key_directory.stat.exists + +- name: Create DNSSEC key directory + ansible.builtin.file: + path: "{{ bind9_options.key_directory }}" + owner: "{{ bind_unix_user }}" + group: "{{ bind_unix_group }}" + mode: "0700" + state: directory + +- name: Create DNSSEC files + ansible.builtin.include_tasks: create_dnssec_files.yml + with_items: "{{ bind9_dnssec_keys }}" + no_log: true + loop_control: + loop_var: bind9_dnssec_key + - name: Create DNS-Zone files ansible.builtin.include_tasks: template_zone_files.yml with_items: diff --git a/tasks/sign_zone_file.yml b/tasks/sign_zone_file.yml new file mode 100644 index 0000000..445a31a --- /dev/null +++ b/tasks/sign_zone_file.yml @@ -0,0 +1,28 @@ +--- + +- name: "Sign DNS Zone {{ zone.config.origin }}" + vars: + dnssec_cmd: + - dnssec-signzone + - -N + - INCREMENT + - -S + - -K + - "{{ bind9_options.key_directory }}" + block: + - name: "Extend dnssec command of ORIGIN" + ansible.builtin.set_fact: + _dnssec_cmd: "{{ dnssec_cmd + ['-o', zone.config.origin] }}" + - name: "Extend dnssec command of zone file" + ansible.builtin.set_fact: + _dnssec_cmd: "{{ _dnssec_cmd + [bind_config_directory + '/' + zone.file] }}" + - name: "Sign zone {{ zone.config.origin }}" + ansible.builtin.command: + argv: "{{ _dnssec_cmd }}" + creates: "{{ bind_config_directory + '/' + zone.file }}.signed" + - name: Adapt signed zone file permissions + ansible.builtin.file: + path: "{{ bind_config_directory + '/' + zone.file }}.signed" + owner: "{{ bind_unix_user }}" + group: "{{ bind_unix_group }}" + mode: "0644" diff --git a/tasks/template_zone_files.yml b/tasks/template_zone_files.yml index c7fd080..6d42421 100644 --- a/tasks/template_zone_files.yml +++ b/tasks/template_zone_files.yml @@ -1,6 +1,6 @@ --- -- name: "Create directory for zone {{ zone.file | dirname }}" +- name: "Create config directory of DNS zone {{ zone.config.origin }}" ansible.builtin.file: path: "{{ bind_config_directory }}/{{ zone.file | dirname }}" owner: "{{ bind_unix_user }}" @@ -11,7 +11,8 @@ - "{{ view.zones }}" loop_control: loop_var: zone - when: zone.file is defined and zone.file | length > 0 + when: zone.file is defined and + zone.file | length > 0 - name: "Template view {{ view.name }}" ansible.builtin.template: @@ -24,7 +25,7 @@ - "{{ view.zones }}" loop_control: loop_var: zone - when: zone.type == 'master' + when: zone.config.type == 'master' notify: Restart named - name: Check if last character in zone files is a newline @@ -33,4 +34,15 @@ - "{{ view.zones }}" loop_control: loop_var: zone - when: zone.type == 'master' + when: zone.config.type == 'master' + +- name: Sign Zones + ansible.builtin.include_tasks: sign_zone_file.yml + with_items: + - "{{ view.zones }}" + loop_control: + loop_var: zone + when: zone.config.type == 'master' and + bind9_dnssec_keys | selectattr('origin', 'in', zone.config.origin) | map(attribute='zone_signing_key') | length > 0 and + (bind9_dnssec_keys | selectattr('origin', 'in', zone.config.origin) | map(attribute='zone_signing_key'))[0].private | length > 0 and + (bind9_dnssec_keys | selectattr('origin', 'in', zone.config.origin) | map(attribute='zone_signing_key'))[0].public | length > 0 diff --git a/tasks/verify_zone_file.yml b/tasks/verify_zone_file.yml index 893c3d7..02854d1 100644 --- a/tasks/verify_zone_file.yml +++ b/tasks/verify_zone_file.yml @@ -1,6 +1,6 @@ --- -- name: "Read the last character of DNS Zonefile: {{ bind_config_directory + '/' + zone.file }}" +- name: "Read the last character of DNS zone: {{ zone.config.origin }}" ansible.builtin.command: cmd: "tail --bytes 1 {{ bind_config_directory + '/' + zone.file }}" register: _bind9_zone_last_character diff --git a/templates/etc/named/named.conf.options.j2 b/templates/etc/named/named.conf.options.j2 index 6e0d5b9..bc08613 100644 --- a/templates/etc/named/named.conf.options.j2 +++ b/templates/etc/named/named.conf.options.j2 @@ -186,9 +186,24 @@ options { directory "{{ bind_config_directory }}"; - dnssec-validation {{ 'yes' if bind9_options.dnssec_validation else 'no' }}; + # This accepts expired signatures when verifying DNSSEC signatures. The default is no. Setting this option to yes + # leaves named vulnerable to replay attacks. + dnssec-accept-expired {{ "yes" if bind9_options.dnssec_accept_expired else "no" }}; - # dump-file "/var/bind/named.dump"; + # Enables DNSSEC validation in named. + # + # auto: If set to auto, DNSSEC validation is enabled and a default trust anchor for the DNS root zone is used. This + # trust anchor is provided as part of BIND and is kept up-to-date + # + # yes: If set to yes, DNSSEC validation is enabled, but a trust anchor must be manually configured using a + # trust-anchors statement (or the managed-keys or trusted-keys statements, both deprecated). If trust-anchors is not + # configured, it is a configuration error. If trust-anchors does not include a valid root key, then validation does + # not take place for names which are not covered by any of the configured trust anchors. + # + # no: If set to no, DNSSEC validation is disabled. + # + # https://bind9.readthedocs.io/en/latest/reference.html#namedconf-statement-dnssec-validation + dnssec-validation {{ bind9_options.dnssec_validation | default('auto') }}; {% if bind9_options.forwarders is defined and bind9_options.forwarders | length > 0 %} forwarders { @@ -231,6 +246,13 @@ options { }; {% endif %} + # Indicates the directory where public and private DNSSEC key files are found. + # + # This is the directory where the public and private DNSSEC key files should be found when performing a dynamic update + # of secure zones, if different than the current working directory. + # https://bind9.readthedocs.io/en/latest/reference.html#namedconf-statement-key-directory + key-directory "{{ bind9_options.key_directory }}"; + # managed-keys-directory "/var/named/dynamic"; # memstatistics-file "/var/bind/named.memstats"; minimal-responses {{ bind9_options.minimal_responses }}; diff --git a/templates/etc/named/named.conf.views.j2 b/templates/etc/named/named.conf.views.j2 index 7fc8d93..926783d 100644 --- a/templates/etc/named/named.conf.views.j2 +++ b/templates/etc/named/named.conf.views.j2 @@ -13,7 +13,7 @@ view "{{ view.name }}" { }; {% for zone in view.zones %} - zone "{{ zone.origin }}" { + zone "{{ zone.config.origin }}" { # Hosts which are allowed to issue queries to the server. If not specified all # hosts are allowed to make queries (defaults to allow-query {any;}; @@ -21,9 +21,9 @@ view "{{ view.name }}" { # NOTE: # - The statements may be used in a zone, view or a global options # clause. -{% if zone.allow_query is defined and zone.allow_query | length > 0 %} +{% if zone.config.allow_query is defined and zone.config.allow_query | length > 0 %} allow-query { -{% for entry in zone.allow_query %} +{% for entry in zone.config.allow_query %} {{ entry }}; {% endfor %} }; @@ -40,9 +40,9 @@ view "{{ view.name }}" { # NOTE: # - The statements may be used in a zone, view or a global options # clause. -{% if zone.allow_query_on is defined and zone.allow_query_on | length > 0 %} +{% if zone.config.allow_query_on is defined and zone.config.allow_query_on | length > 0 %} allow-query { -{% for entry in zone.allow_query_on %} +{% for entry in zone.config.allow_query_on %} {{ entry }}; {% endfor %} }; @@ -63,9 +63,9 @@ view "{{ view.name }}" { # # NOTE: # - This statement may be used in a zone, view or global options clause. -{% if zone.allow_transfer is defined and zone.allow_transfer | length > 0 %} +{% if zone.config.allow_transfer is defined and zone.config.allow_transfer | length > 0 %} allow-transfer { -{% for entry in zone.allow_transfer %} +{% for entry in zone.config.allow_transfer %} key {{ entry }}; {% endfor %} }; @@ -86,9 +86,9 @@ view "{{ view.name }}" { # # NOTE: # - This statement may be used in a zone, view or an options clause. -{% if zone.allow_update is defined and zone.allow_update | length > 0 %} +{% if zone.config.allow_update is defined and zone.config.allow_update | length > 0 %} allow-update { -{% for entry in zone.allow_update %} +{% for entry in zone.config.allow_update %} key {{ entry }}; {% endfor %} }; @@ -102,9 +102,9 @@ view "{{ view.name }}" { # # NOTE: # - This statement may be used in zone, view or an options clause. -{% if zone.allow_update_forwarding is defined and zone.allow_update_forwarding | length > 0 %} +{% if zone.config.allow_update_forwarding is defined and zone.config.allow_update_forwarding | length > 0 %} allow-update-forwarding { -{% for entry in zone.allow_update_forwarding %} +{% for entry in zone.config.allow_update_forwarding %} {{ entry }}; {% endfor %} }; @@ -129,9 +129,9 @@ view "{{ view.name }}" { # contact the Master, ffor whatever reason, the zone may be left with # no effective Authoritative Name Servers. {% if zone.file is defined and zone.file | length > 0 and not zone.file.startswith('/') %} - file "{{ bind_config_directory }}/{{ zone.file }}"; + file "{{ bind_config_directory }}/{{ zone.config.file }}"; {% elif zone.file is defined and zone.file | length > 0 and zone.file.startswith('/')%} - file "{{ zone.file }}"; + file "{{ zone.config.file }}"; {% else %} # file "{{ bind_config_directory }}/..."; {% endif %} @@ -141,8 +141,8 @@ view "{{ view.name }}" { # forwarders first; if that does not answer the question, the server then # looks for the answer itself. If only is specified, the server only queries # the forwarders. -{% if zone.forward is defined and zone.forward | length > 0 %} - forward {{ zone.forward }}; +{% if zone.config.forward is defined and zone.config.forward | length > 0 %} + forward {{ zone.config.forward }}; {% else %} # forward first; {% endif %} @@ -152,9 +152,9 @@ view "{{ view.name }}" { # associated with an optional port number and/or DSCP value, and a default # port number and DSCP value can be set for the entire list. # https://bind9.readthedocs.io/en/latest/reference.html#forwarding -{% if zone.forwarders is defined and zone.forwarders | length > 0 %} +{% if zone.config.forwarders is defined and zone.config.forwarders | length > 0 %} forwarders { -{% for forwarder in zone.forwarders %} +{% for forwarder in zone.config.forwarders %} {{ forwarder }}; {% endfor %} }; @@ -164,9 +164,9 @@ view "{{ view.name }}" { # master servers # https://bind9.readthedocs.io/en/latest/manpages.html?highlight=masters#masters -{% if zone.masters is defined and zone.masters | length > 0 %} +{% if zone.config.masters is defined and zone.config.masters | length > 0 %} masters { -{% for master in zone.masters %} +{% for master in zone.config.masters %} {{ master.ip }} key {{ master.tsigkey}}; {% endfor %} }; @@ -190,9 +190,9 @@ view "{{ view.name }}" { # NOTE: # - This statement may be specified in zone, view clauses or in a # global options clause. -{% if zone.notify is defined and zone.notify %} +{% if zone.config.notify is defined and zone.config.notify %} notify yes; -{% elif zone.notify is defined and not zone.notify %} +{% elif zone.config.notify is defined and not zone.config.notify %} notify no; {% else %} # notify yes | no; @@ -216,13 +216,13 @@ view "{{ view.name }}" { # is the current date in the form “YYYYMMDD”, followed by two # zeroes, unless the existing serial number is already greater than # or equal to that value, in which case it is incremented by one. -{% if zone.serial_update_method is defined %} - serial-update-method {{ zone.serial_update_method }}; +{% if zone.config.serial_update_method is defined %} + serial-update-method {{ zone.config.serial_update_method }}; {% else %} # serial-update-method [date | increment | unixtime ]; {% endif %} - type {{ zone.type }}; + type {{ zone.config.type }}; # The update-policy clause allows more fine-grained control over which # updates are allowed. It specifies a set of rules, in which each rule @@ -230,9 +230,9 @@ view "{{ view.name }}" { # updated by one or more identities. Identity is determined by the key that # signed the update request, using either TSIG or SIG(0). # https://bind9.readthedocs.io/en/v9_16_5/reference.html#dynamic-update-policies -{% if zone.update_policies is defined and zone.update_policies | length > 0 %} +{% if zone.config.update_policies is defined and zone.config.update_policies | length > 0 %} update-policy { -{% for update_policy in zone.update_policies %} +{% for update_policy in zone.config.update_policies %} {{ update_policy.action }} {{ update_policy.identity }} {{ update_policy.ruletype }} {{ update_policy.name | default('') }} {{ update_policy.types | default('') | join(' ') }}; {% endfor %} };