Compare commits

..

3 commits

Author SHA1 Message Date
20225bac4b Update docker.io/library/redis Docker tag to v8
All checks were successful
/ Ansible Lint (push) Successful in 3m10s
/ Ansible Lint (pull_request) Successful in 6m46s
2026-03-31 15:01:04 +00:00
73ed238a28
sunders(host): move to dns-01-acme-dns
All checks were successful
/ Ansible Lint (push) Successful in 4m37s
2026-03-31 16:59:02 +02:00
8bf6dfbefb
certbot(role): support DNS-01 certs using acme-dns
All checks were successful
/ Ansible Lint (push) Successful in 3m36s
Introduce new configuration structure called certbot__certs, which
allows for different challenge types per cert with the first challenge
type supported being dns-01-acme-dns.
2026-03-31 16:48:00 +02:00
12 changed files with 199 additions and 24 deletions

View file

@ -2,6 +2,7 @@ ansible_pull__age_private_key: ENC[AES256_GCM,data:tP84jDYh2zeWjf7wqDoefm9zaeg/Q
secret__sunders_db_root_password: ENC[AES256_GCM,data:m3Xt6dOKibRflon/rWG9KmdBPHEBbqE/GIpKdFI1Di7Lpl/THxzrgx12mTK6aZnwDrM=,iv:hD/UGwo88ye9CxyTCEQ0SVon2+ipPjeA9NF2/OhYwmc=,tag:DRdQ5hvTgUO5FVae/ul7kQ==,type:str]
secret__sunders_db_camera_password: ENC[AES256_GCM,data:tOt4ImpedgfGvRpcThPO30YyEl/bP244ruJQzAYodJIsEhFuk5LxHpPASEnsqlN6m3M=,iv:rQXBjiYWZlzeUdaqDdTlrdbSSqGaPDeZOPhUaMjgcjU=,tag:lkSlIdJWFowyPfWEjpC/Zg==,type:str]
secret__sunders_db_camera_select_password: ENC[AES256_GCM,data:PveGcD2WmvpMc8bafGY1c45aQ2XH/ym2yj5YacauQPeZO6Xem3kaxU0kwjs0Wd26ugc=,iv:tk288L9i0lxsJbTFq5ET5IiKkJfMQwc6uKNFXILcD7o=,tag:hOIivp3mOtDNBCsKvrSrBw==,type:str]
secret__acme_dns_api_key_sunders_hamburg_ccc_de: ENC[AES256_GCM,data:OXGasHc1qf4r8wj0N2qqjZzWO3zg5L5ZGRjAwmlEXi9/gaTpUWT1xw==,iv:pKaNajQ9JxxUwuv/scJZhP6UdSob3GrODShk2t75580=,tag:3atM6IddvYhoAmNlqooooQ==,type:str]
sops:
age:
- recipient: age1na0nh9ndnr9cxpnlvstrxskr4fxf4spnkw48ufl7m43f98y40y7shhnvgd
@ -13,8 +14,8 @@ sops:
KzBPS0Y3OXBUSjFWRVQwaGQ0M2M4ajgKYGgcfiDsBsJQmSodfPejhAcAekatqkJ9
Xa6Y7mWUiAKcbYY9to7/7/u5FUW3JYdv1GX7vAC2ZtZv6WXGLtjdwg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-11-01T16:32:10Z"
mac: ENC[AES256_GCM,data:8Q6DBSFtzwHuVxduRlZYxlRWO0trSoesNGUR8r/dWnp9ashFBSZqVyffXb4Vq6DB5thANJ6/b3PCNsHdiAKn6Ai2UT8G0HimFjUUgNpZxo4xoNGmDhDvfdBgUL6O2pHhY+ojjguUXDYeYc99+eaxfKqZ3w+PAPaySltKm99foz8=,iv:ILOErdiWbUjk9kovXXZYcAqZFQp2Wo1Tm14sgK3niWg=,tag:Q2gT6wbQyhDXjoQEG2Lngw==,type:str]
lastmodified: "2026-03-31T14:49:48Z"
mac: ENC[AES256_GCM,data:Ln//LDvcSqRknJGQvdzwoRvZOK/ZBqfTJqnxm9fhQsfZudUchZ5u7PXQf/0y3GnrYVx9agGGoKQN3VFehrZfpUE4ygjvmLHiqJSZVREaD4Yq0PuaY54/wN4YX8H1nE7ILdm1QvFRzpMUpSnTM1Yrr6cMIFMHcRRpL73LfG1nwPY=,iv:NXT/cPTnbg7ESiI7Aj3OOpCLMM4j355NAMVJNsIppzY=,tag:iLFTfiFdDxpwvC88fLf3Mw==,type:str]
pgp:
- created_at: "2026-03-05T19:17:55Z"
enc: |-
@ -216,4 +217,4 @@ sops:
-----END PGP MESSAGE-----
fp: 41FFAF3D519CF5C039FBD8414BCC213729AF0E49
unencrypted_suffix: _unencrypted
version: 3.11.0
version: 3.12.1

View file

@ -1,8 +1,13 @@
docker_compose__compose_file_content: "{{ lookup('ansible.builtin.template', 'resources/chaosknoten/sunders/docker_compose/compose.yaml.j2') }}"
certbot__acme_account_email_address: le-admin@hamburg.ccc.de
certbot__certificate_domains:
- "sunders.hamburg.ccc.de"
certbot__certs:
- commonName: "sunders.hamburg.ccc.de"
challengeType: "dns-01-acme-dns"
dns_01_acme_dns:
subdomain: a5ee8a99-3cdf-4212-972e-c0b6fda1242f
apiUser: 45e4ea1c-8322-4d92-9f52-adf52b521131
apiKey: "{{ secret__acme_dns_api_key_sunders_hamburg_ccc_de }}"
certbot__new_cert_commands:
- "systemctl reload nginx.service"

View file

@ -29,7 +29,6 @@ map $host $upstream_acme_challenge_host {
wiki.hamburg.ccc.de wiki.hosts.hamburg.ccc.de:31820;
www.hamburg.ccc.de 172.31.17.151:31820;
tickets.hamburg.ccc.de tickets.hosts.hamburg.ccc.de:31820;
sunders.hamburg.ccc.de sunders.hosts.hamburg.ccc.de:31820;
zammad.hamburg.ccc.de zammad.hosts.hamburg.ccc.de:31820;
eh03.easterhegg.eu 172.31.17.151:31820;
eh05.easterhegg.eu 172.31.17.151:31820;

View file

@ -6,6 +6,24 @@ Note: This role doesn't take care of deleting certificates.
Also see the following documentation for a full How-to on how to get certificates using this role in the context of our infra: <https://wiki.ccchh.net/infrastructure:zertifikate>.
## Challenge Types
For the `certbot__certs` configuration, depending on the challenge type, different preparation needs to be done.
### `dns-01-acme-dns`
For the `dns-01-acme-dns` challenge type, ensure that:
1. An acme-dns entry got registered, so you have access to the `subdomain`, `apiUser` and `apiKey` for the configuration. The `apiKey` should be stored in some kind of secret, which then gets referenced in the relevant `certbot__certs` configuration.
For our acme-dns instance, go to <https://acmedns.hamburg.ccc.de/>, sign-in and register a new entry.
2. CNAME the `_acme-challenge` domain of the domain you want to obtain a certificate for (`certbot__certs.*.commonName`) to the full domain from the registration.
It should look something like this: `_acme-challenge.domainiwantcertfor.tld. CNAME 3ed25037-79f1-4a89-8934-db3e162fe2bc.auth.acmedns.hamburg.ccc.de.`
For more info see:
- [The ACME DNS hamburg.ccc.de Wiki page](https://wiki.hamburg.ccc.de/infrastructure:services:acme_dns)
- [The acme-dns GitHub](https://github.com/acme-dns/acme-dns)
## Required Arguments
- `certbot__acme_account_email_address`: The E-Mail address to use for the ACME account.
@ -15,6 +33,18 @@ Also see the following documentation for a full How-to on how to get certificate
- `certbot__certificate_domains`: The domains for which to obtain a certificate using the HTTP-01 challenge.
- `certbot__http_01_port`: The port number the bot listens on. Should be `80` if directly exposed to the internet.
Defaults to `31820` (for the public-reverse-proxy setup).
- `certbot__certs`: Certificates to create.
Defaults to the empty list (`[ ]`).
- `certbot__certs.*.commonName`: The common name to issue the certificate for.
- `certbot__certs.*.challengeType`: The challenge type to use for getting the certificate. Challenge type-specific configuration must be provided as well.
Should be one of:
- `dns-01-acme-dns`
- `certbot__certs.*.dns_01_acme_dns`: Configuration for the `dns-01-acme-dns` challenge type.
- `certbot__certs.*.dns_01_acme_dns.serverUrl`: The acme-dns server API URL.
Defaults to `https://acmedns.hamburg.ccc.de`.
- `certbot__certs.*.dns_01_acme_dns.subdomain`: The acme-dns subdomain to use.
- `certbot__certs.*.dns_01_acme_dns.apiUser`: The acme-dns API user to use.
- `certbot__certs.*.dns_01_acme_dns.apiKey`: The acme-dns API key to use.
- `certbot__new_cert_commands`: A list of commands to execute after getting a new certificate. Will be added into a bash script.
Defaults to the empty list (`[ ]`).

View file

@ -1,3 +1,4 @@
certbot__certificate_domains: [ ]
certbot__http_01_port: 31820
certbot__certs: [ ]
certbot__new_cert_commands: [ ]

View file

@ -0,0 +1,14 @@
# #!/usr/bin/env bash
CERT_CONFIG_FILE="/etc/ansible_certbot/cert_configs/$CERTBOT_DOMAIN.json"
ACME_DNS_SERVER_URL=$( jq -er '.dns_01_acme_dns.serverUrl' "$CERT_CONFIG_FILE" )
export ACME_DNS_SUBDOMAIN=$( jq -er '.dns_01_acme_dns.subdomain' "$CERT_CONFIG_FILE" )
ACME_DNS_API_USER=$( jq -er '.dns_01_acme_dns.apiUser' "$CERT_CONFIG_FILE" )
ACME_DNS_API_KEY=$( jq -er '.dns_01_acme_dns.apiKey' "$CERT_CONFIG_FILE" )
jq -nec '{ "subdomain": env.ACME_DNS_SUBDOMAIN, "txt": env.CERTBOT_VALIDATION }' | curl "$ACME_DNS_SERVER_URL/update" \
--request POST \
--fail-with-body \
--header "X-Api-User: $ACME_DNS_API_USER" \
--header "X-Api-Key: $ACME_DNS_API_KEY" \
--json @-

View file

@ -13,6 +13,25 @@ argument_specs:
type: str
required: false
default: 31820
certbot__certs:
type: list
elements: dict
required: false
default: [ ]
options:
commonName:
type: str
required: true
# ToDo: subjectAlternativeNames:
challengeType:
type: str
required: true
choices:
- dns-01-acme-dns
dns_01_acme_dns:
type: dict
required: false
# Further checking done in tasks/validate_cert.yaml
certbot__new_cert_commands:
type: list
elements: str

View file

@ -1,24 +1,46 @@
- name: get expiry date before
ansible.builtin.command: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/{{ item }}/fullchain.pem
ansible.builtin.command: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/{{ item.commonName }}/fullchain.pem
ignore_errors: true
become: true
changed_when: false
register: certbot__cert_expiry_before
- name: obtain the certificate using certbot
ansible.builtin.command: /usr/bin/certbot certonly --keep-until-expiring --agree-tos --non-interactive --email "{{ certbot__acme_account_email_address }}" --no-eff-email --standalone --http-01-port "{{ certbot__http_01_port }}" -d "{{ item }}"
- name: ensure directory for cert configs exists
ansible.builtin.file:
path: "/etc/ansible_certbot/cert_configs/"
state: directory
owner: root
group: root
mode: "0750"
become: true
- name: ensure cert config is stored
ansible.builtin.copy:
content: "{{ cert_config_defaults[item.challengeType] | combine(item, recursive=True) | ansible.builtin.to_nice_json }}"
dest: "/etc/ansible_certbot/cert_configs/{{ item.commonName }}.json"
owner: root
group: root
mode: "0640"
become: true
vars:
cert_config_defaults:
dns-01-acme-dns:
dns_01_acme_dns:
serverUrl: "https://acmedns.hamburg.ccc.de"
# # https://eff-certbot.readthedocs.io/en/stable/using.html#manual
- name: obtain the certificate using certbot and the manual auth hook
ansible.builtin.command: /usr/bin/certbot certonly --keep-until-expiring --agree-tos --non-interactive --email "{{ certbot__acme_account_email_address }}" --no-eff-email --manual --preferred-challenge dns --manual-auth-hook "/usr/local/lib/ansible_certbot/manual_auth_scripts/{{ item.challengeType }}.sh" -d "{{ item.commonName }}"
become: true
changed_when: false
- name: get expiry date after
ansible.builtin.command: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/{{ item }}/fullchain.pem
ansible.builtin.command: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/{{ item.commonName }}/fullchain.pem
become: true
changed_when: false
register: certbot__cert_expiry_after
# Doesn't work anymore. Dunno why.
# TODO: Fix
# - name: potentially report changed
# ansible.builtin.debug:
# msg: "If this reports changed, then the certificate expiry date and therefore the certificate changed."
# changed_when: certbot__cert_expiry_before.stdout != certbot__cert_expiry_after.stdout
- name: potentially report changed
ansible.builtin.debug:
msg: "If this reports changed, then the certificate expiry date and therefore the certificate changed."
changed_when: certbot__cert_expiry_before.stdout != certbot__cert_expiry_after.stdout

View file

@ -1,4 +1,14 @@
- name: obtain certificates
- name: obtain http-01 challenge certificates
loop: "{{ certbot__certificate_domains }}"
ansible.builtin.include_tasks:
file: main/http_01_cert.yaml
- name: validate certs config
loop: "{{ certbot__certs }}"
ansible.builtin.include_tasks:
file: main/validate_cert.yaml
- name: obtain certs
loop: "{{ certbot__certs }}"
ansible.builtin.include_tasks:
file: main/cert.yaml

View file

@ -0,0 +1,24 @@
- name: get expiry date before
ansible.builtin.command: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/{{ item }}/fullchain.pem
ignore_errors: true
become: true
changed_when: false
register: certbot__cert_expiry_before
- name: obtain the certificate using certbot
ansible.builtin.command: /usr/bin/certbot certonly --keep-until-expiring --agree-tos --non-interactive --email "{{ certbot__acme_account_email_address }}" --no-eff-email --standalone --http-01-port "{{ certbot__http_01_port }}" -d "{{ item }}"
become: true
changed_when: false
- name: get expiry date after
ansible.builtin.command: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/{{ item }}/fullchain.pem
become: true
changed_when: false
register: certbot__cert_expiry_after
# Doesn't work anymore. Dunno why.
# TODO: Fix
# - name: potentially report changed
# ansible.builtin.debug:
# msg: "If this reports changed, then the certificate expiry date and therefore the certificate changed."
# changed_when: certbot__cert_expiry_before.stdout != certbot__cert_expiry_after.stdout

View file

@ -1,11 +1,30 @@
- name: make sure the `openssl` package is installed
- name: ensure relevant packages are installed
ansible.builtin.apt:
name: openssl
name:
- openssl
- certbot
- jq
state: present
become: true
- name: make sure the `certbot` package is installed
ansible.builtin.apt:
name: certbot
state: present
become: true
- name: ensure manual auth scripts are deployed
block:
- name: ensure manual auth scripts directory exists
ansible.builtin.file:
path: "/usr/local/lib/ansible_certbot/manual_auth_scripts"
state: directory
owner: root
group: root
mode: "0755"
become: true
- name: ensure manual auth scripts are deployed
ansible.builtin.copy:
src: "manual_auth_scripts/{{ item }}.sh"
dest: "/usr/local/lib/ansible_certbot/manual_auth_scripts/{{ item }}.sh"
owner: root
group: root
mode: "0754"
become: true
loop:
- "dns-01-acme-dns"

View file

@ -0,0 +1,31 @@
- name: validate dns-01-acme-dns challenge type config
when: item.challengeType == "dns-01-acme-dns"
block:
- name: assert dns_01_acme_dns config exists
ansible.builtin.assert:
that: item.dns_01_acme_dns is defined
- name: assert dns_01_acme_dns config is valid
ansible.builtin.validate_argument_spec:
argument_spec: "{{ required_data }}"
provided_arguments:
dns_01_acme_dns: "{{ item.dns_01_acme_dns }}"
vars:
required_data:
dns_01_acme_dns:
type: dict
required: true
options:
serverUrl:
type: str
required: false
default: https://acmedns.hamburg.ccc.de
subdomain:
type: str
required: true
apiUser:
type: str
required: true
apiKey:
type: str
required: true