Skip to content

Commit

Permalink
Adding nsupdate DNS challenge (#169)
Browse files Browse the repository at this point in the history
* Adding nsupdate DNS challenge

* Update after feedback

* Update after feedback

* Fix linting and some cosmetics

Signed-off-by: Martin Schurz <[email protected]>

* Fix linting and some cosmetics

Signed-off-by: Martin Schurz <[email protected]>

* Fix typo

Signed-off-by: Martin Schurz <[email protected]>

---------

Signed-off-by: Martin Schurz <[email protected]>
Co-authored-by: Michael Kluge <[email protected]>
Co-authored-by: Martin Schurz <[email protected]>
  • Loading branch information
3 people authored Feb 14, 2025
1 parent 0c6233a commit ae971d0
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .config/ansible-lint.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
exclude_paths:
- .cache/ # implicit unless exclude_paths is defined in config
- .ansible/ # somehow someone decided that the cache directory should be renamed
- .github/
skip_list:
- meta-runtime[unsupported-version]
106 changes: 106 additions & 0 deletions docs/dns-challenge/nsupdate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Variables for nsupdate dns-challenge

| Variable | Required | Default | Description |
|-----------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------------------------------------------|
| | | | |
| acme_nsupdate_server | yes | | The IPv4/IPv6 address of the DNS server where nsupdate manages the _acme-challenge TXT records. (can also be a DNS name, see below) |
| acme_nsupdate_dns_key: | yes | | The acme_nsupdate_dns_key dictionary mirrors the settings of a bind DNS keyfile. |
| acme_nsupdate_dns_key: name: | no | nsupdate_key | Name of the |
| acme_nsupdate_dns_key: algorithm: | no | hmac-sha512 | Hash algo of the key (i.e. hmac-sha512, hmac-sha256) |
| acme_nsupdate_dns_key: secret: | yes | | The key |
| acme_nsupdate_replication_delay | no | 2 | Wait time after the TXT record is issued, before the certificate is fetched via ACME |

## Usage

### wildcard certificate

```yaml
- name: create the certificate for *.example.org
hosts: localhost
collections:
- telekom_mms.acme
roles:
- acme
vars:
acme_domain:
certificate_name: "wildcard.example.org"
zone: "example.org"
email_address: "[email protected]"
subject_alt_name:
- "*.example.org"
acme_challenge_provider: nsupdate
acme_use_live_directory: false
acme_account_email: "[email protected]"
acme_nsupdate_server: "{{ lookup('community.general.dig', 'primary.nsforexample.org', qtype='A') }}"
acme_nsupdate_dns_key:
name: 'nsupdate_key'
algorithm: hmac-sha512
secret: ""
```
### SAN certificate
```yaml
- name: create the certificate for example.org
hosts: localhost
collections:
- telekom_mms.acme
roles:
- acme
vars:
acme_domain:
certificate_name: "test.example.org"
zone: "example.org"
email_address: "[email protected]"
subject_alt_name:
- "test.example.org"
- "domain1.example.org"
- "domain2.example.org"
acme_challenge_provider: nsupdate
acme_use_live_directory: false
acme_account_email: "[email protected]"
acme_nsupdate_server: "{{ lookup('community.general.dig', 'primary.nsforexample.org', qtype='A') }}"
acme_nsupdate_dns_key:
name: 'nsupdate_key'
algorithm: hmac-sha512
secret: ""
```
### acme_nsupdate_server
The acme_nsupdate_server MUST be a IPv4/IPv6 address (limitation of the nsupdate ansible module). To work with a DNS
name, you can use the dig lookup:
```
acme_nsupdate_server: "{{ lookup('community.general.dig', 'primary.nsforexample.org', qtype='A') }}"
```
### acme_domain.zone
In some cases your DNS server may not be authoritative for a subdomain but the parent domain. In such cases you can
override which zone is used when the nsupdate is issued. For example:
* certificate zone (acme_domain.zone) = mysub.example.org
* DNS is authoritative for example.org and the zonefile should contain the following entry
```
_acme-challenge.mysub.example.org. 120 IN TXT "nsupdate-test123"
```
In this scenario the following dictionary should be placed in acme_nsupdate_override_domain
```
acme_domain:
certificate_name: "mysub.example.org"
zone: "example.org"
subject_alt_name:
- "test.example.org"
- "domain1.example.org"
- "domain2.example.org"
```

The same is true for SAN certificates. Please note, that SAN certificates can have multiple subdomain names but
are limited to one zone.
```
acme_domain:
certificate_name: "mysub.example.org"
zone: "example.org"
```

### acme_nsupdate_replication_delay
If you are using a primary/secondary DNS server setup it might be a good idea to wait a second or two after the
nsupdate on the primary was issued.
1 change: 1 addition & 0 deletions galaxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies:
openstack.cloud: ">=1.2.1"
amazon.aws: ">=5.0.0"
azure.azcollection: ">=1.14.0"
community.general: ">10.3.0"
repository: https://github.com/telekom-mms/ansible-collection-acme
documentation: https://github.com/telekom-mms/ansible-collection-acme
homepage: https://github.com/telekom-mms/ansible-collection-acme
Expand Down
8 changes: 8 additions & 0 deletions roles/acme/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,13 @@ acme_s3_install_prerequisites: true
acme_local_validation_path: /var/www/html
acme_azure_purge_state: absent

### DNS nsupdate challenge parameters
acme_nsupdate_replication_delay: 2
acme_nsupdate_server: 192.168.1.1
acme_nsupdate_dns_key:
name: 'nsupdate_key'
algorithm: hmac-sha512
secret: ""

### certificate download for non-persistent environments
acme_download_cert: false
76 changes: 76 additions & 0 deletions roles/acme/tasks/challenge/dns-01/nsupdate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
# include/role 3 - validate challenge
- name: Validate challenge only if it is created or changed # noqa no-handler
when: acme_challenge is changed
connection: local
delegate_to: localhost
block:
- name: Add a new TXT record to the relevant domains
vars:
record_name: "_acme-challenge.{{ domain | replace('*.', '') }}." # replace '*.' in the case of a wildcard
record_data: "{{ acme_challenge['challenge_data'][domain]['dns-01']['resource_value'] }}"
community.general.nsupdate:
key_name: "{{ acme_nsupdate_dns_key.name | default('nsupdate_key') }}"
key_secret: "{{ acme_nsupdate_dns_key.secret }}"
key_algorithm: "{{ acme_nsupdate_dns_key.algorithm | default('hmac-sha512') }}"
server: "{{ acme_nsupdate_server }}"
zone: "{{ acme_domain.zone | default(domain) }}"
record: "{{ record_name }}"
value: "{{ record_data }}"
type: "TXT"
ttl: "120"
loop: "{{ acme_domain.subject_alt_name }}"
loop_control:
label: "zone={{ domain }} rr={{ record_name }} (TXT) {{ record_data }}"
loop_var: "domain"
when:
- acme_domain.subject_alt_name is defined
# only runs if the challenge is run the first time, because then there is challenge_data
- acme_challenge['challenge_data'][domain] is defined

- name: Wait for DNS replication to catch up
ansible.builtin.pause:
seconds: "{{ acme_nsupdate_replication_delay }}"

- name: Let the challenge be validated and retrieve the cert and intermediate certificate
community.crypto.acme_certificate:
account_key_src: "{{ acme_account_key_path }}"
account_email: "{{ acme_account_email }}"
csr: "{{ acme_csr_path }}"
cert: "{{ acme_cert_path }}"
fullchain: "{{ acme_fullchain_path }}"
chain: "{{ acme_intermediate_path }}"
challenge: dns-01
force: "{{ acme_force_renewal | default(false) }}"
acme_directory: "{{ acme_directory }}"
acme_version: 2
terms_agreed: true
remaining_days: "{{ acme_remaining_days }}"
data: "{{ acme_challenge }}"
when:
- acme_certificate_enabled | default(true)

always:
- name: Remove the TXT record from the relevant domains
vars:
record_name: "_acme-challenge.{{ domain | replace('*.', '') }}." # replace '*.' in the case of a wildcard
record_data: "{{ acme_challenge['challenge_data'][domain]['dns-01']['resource_value'] }}"
community.general.nsupdate:
key_name: "{{ acme_nsupdate_dns_key.name | default('nsupdate_key') }}"
key_secret: "{{ acme_nsupdate_dns_key.secret }}"
key_algorithm: "{{ acme_nsupdate_dns_key.algorithm | default('hmac-sha512') }}"
server: "{{ acme_nsupdate_server }}"
zone: "{{ acme_domain.zone | default(domain) }}"
record: "{{ record_name }}"
value: "{{ record_data }}"
type: "TXT"
ttl: "120"
state: absent
loop: "{{ acme_domain.subject_alt_name }}"
loop_control:
label: "zone={{ domain }} rr={{ record_name }} (TXT) {{ record_data }}"
loop_var: "domain"
when:
- acme_domain.subject_alt_name is defined
# only runs if the challenge is run the first time, because then there is challenge_data
- acme_challenge['challenge_data'][domain] is defined

0 comments on commit ae971d0

Please sign in to comment.