Ansible acme module
Ansible is a suite of tools to enable the automatization of software configuration. It can be used in conjunction with an ACME server running on the CZERTAINLY platform to automate certificate management, especially in situations where a target entity isn't capable of certificate management operations and direct communication with an ACME server through available ACME client implementations.
CZERTAINLY platform supports ACME implementation according to the RFC 8555. This guide shows, how you can use Ansible acme
module to manage certificates using ACME protocol and certificate management services controlled by the platform.
For more information about Ansible, refer to the Ansible documentation.
This guide assumes you have at least basic knowledge about Ansible. If you are new to Ansible, we recommend you to start with the Getting started with Ansible.
Prerequisites
To use Ansible with CZERTAINLY, you need to have the following:
- Ansible installed
- Configured at least one
RA Profile
certificate service in CZERTAINLY - Access to HTTP or DNS resources that will be used to validate ACME challenges
- ACME protocol enabled according to the Enable ACME
To install Ansible, follow the installation instructions.
Steps
To get a certificate with ACME protocol, you need to do several tasks typically using Ansible:
- register an account on the ACME server
- prepare CSR to issue new certificate
- request challenge on the ACME server
- transport challenge data to HTTP or DNS server
- request validation of the challenge on the ACME server
- install the new certificate
Ansible playbook preparation
To demonstrate all steps, we will create a playbook file playbook-czertainly-acme-demo.yml
.
The playbook uses the following modules:
community.crypto.acme_account
for registration ACME accountcommunity.crypto.acme_certificate
for certificate management operations on ACME servercommunity.crypto.openssl_privatekey
to generate unique key paircommunity.crypto.openssl_csr
to sign certificate signing requestcommunity.general.nsupdate
to distribute ACME challenge to DNS server
Even though Ansible community manages those modules, they are part of the standard Ansible packages.
At the beginning of the playbook file, we define default values that are used across tasks. The example playbook assumes that it is running on an HTTP server and that user running it has write permissions to /var/www/html
directory. All working files and resulting certificate is stored in the tmp
directory, which must be created before executing the playbook.
- name: CZERTAINLY Ansible ACME example
hosts: localhost
tasks:
- name: Set vars
ansible.builtin.set_fact:
acme_directory: "https://[domain]:[port]/api/v1/protocols/acme/czertainly/directory"
acme_version: 2
acme_register_account: true
acme_web_dir: "/var/www/html"
acme_force: false
acme_account_email: "email@example.com"
bind: "{{ lookup('file', 'vars/czertainly.private') | from_yaml }}"
- name: Set default ACME method
ansible.builtin.set_fact:
acme_method: 'http-01'
when: acme_method is not defined
Register ACME account
First, we will create private RSA key pair which will be associated with later registered account. The key can be used for revoking the issued certificate even when the private key of the certificate isn't available.
- name: Generate private key for ACME account
community.crypto.openssl_privatekey:
path: "tmp/acme_account_key"
type: RSA
size: 2048
mode: 0600
- name: Register ACME Account without external binding
community.crypto.acme_account:
account_key_src: "tmp/acme_account_key"
acme_directory: "{{ acme_directory }}"
acme_version: "{{ acme_version }}"
state: present
terms_agreed: true
contact:
- "mailto:{{ acme_account_email }}"
register: acme_account
when: acme_register_account
Generate private key and prepare CSR
The step to generate key pair and certificate signing request is optional. You can also you externally generated and signed certificate signing request.
Note that this key pair is not associated with the ACME account key pair.
- name: Generate private key for server
community.crypto.openssl_privatekey:
path: "tmp/{{ acme_domain }}.key"
type: RSA
size: 2048
mode: 0600
- name: Generate certificate signing request (CSR)
community.crypto.openssl_csr:
path: "tmp/{{ acme_domain }}.csr"
privatekey_path: "tmp/{{ acme_domain }}.key"
common_name: "{{ acme_domain }}"
subject_alt_name: "DNS:{{ acme_domain }}"
Request verification challenge
Now, we need to submit the certificate signing request to ACME server and register validation challenge.. The argument dest
can be pointing to a non-existent file. But if it points to an existing certificate, the Ansible will check if it is below the default value 10 days and request renewal instead of new certificate (default behaviour). This can be modified by arguments remaining_days
and force
. The second argument, force
requests new certificate every time the playbook is executed, instead of checking for renewal.
- name: Create a challenge
community.crypto.acme_certificate:
account_key_src: "tmp/acme_account_key"
account_email: "{{ acme_account_email }}"
csr: "tmp/{{ acme_domain }}.csr"
dest: "tmp/{{ acme_domain }}}.crt"
acme_directory: "{{ acme_directory }}"
acme_version: "{{ acme_version }}"
terms_agreed: true
force: "{{ acme_force }}"
modify_account: false
challenge: "{{ acme_method }}"
register: acme_challenge
- name: Show challenge
ansible.builtin.debug: var=acme_challenge
The following is an example of challenge data generated by CZERTAINLY for submitted CSR containing with two domain name identifiers demo.czertainly.test
and www.czertainly.test
:
"challenge_data": {
"demo.czertainly.test": {
"dns-01": {
"record": "_acme-challenge.demo.czertainly.test",
"resource": "_acme-challenge",
"resource_value": "A1RnPLUigrrhd32tDtnF3yH_mgmuDLIfxKvBVrWefs4"
},
"http-01": {
"resource": ".well-known/acme-challenge/wXKY9aI5ChnUFGgUzG-dUDe85-Grq4Ub1MGZ2cYVtL4",
"resource_value": "wXKY9aI5ChnUFGgUzG-dUDe85-Grq4Ub1MGZ2cYVtL4.YS1M_PeBjPbe78-TEhNKOWuckVy7xf04IG0HpKijyPw"
},
},
"www.czertainly.test": {
"dns-01": {
"record": "_acme-challenge.www.czertainly.test",
"resource": "_acme-challenge",
"resource_value": "_UuzUAyxd1tFA3qx8k94HLmbLwV1Hsoqgef3YxhkWss"
},
"http-01": {
"resource": ".well-known/acme-challenge/OSu7SStl_kwYUOQ1dg0jzptGuaHf5VVcPiJyt-8oUMA",
"resource_value": "OSu7SStl_kwYUOQ1dg0jzptGuaHf5VVcPiJyt-8oUMA.YS1M_PeBjPbe78-TEhNKOWuckVy7xf04IG0HpKijyPw"
},
}
}
Publishing verification challenge
In case you decide to use http-01
challenge validation method, you need to publish generated challenge in /var/www/html/.well-known/acme-challenge/
directory of the web server:
- name: Copy http-01 challenge data
ansible.builtin.copy:
dest: "{{ acme_web_dir }}/{{ item.value['http-01']['resource'] }}"
content: "{{ item.value['http-01']['resource_value'] }}"
with_dict:
- "{{ acme_challenge['challenge_data'] }}"
when: acme_method == 'http-01'
Note a loop using with_dict
to iterate over all identifiers.
In case you decide to use dns-01
challenge validation method, you need to publish TXT record to the DNS server responsible for the respective domain. Typically, access is granted based on the secret key that needs to be configured to sign request to write DNS records. Put your DNS credentials into file vars/czertainly.private
with the following content:
server: 123.123.123.123
key_algorithm: "hmac-sha512"
key_name: "name"
key_secret: "base64 data of secret"
The file vars/czertainly.private
gets loaded every time the playbook is executed. The following Ansible task is responsible for publishing the challenge to desired DNS resolver:
community.general.nsupdate:
key_algorithm: "{{ bind.key_algorithm }}"
key_name: "{{ bind.key_name }}"
key_secret: "{{ bind.key_secret }}"
server: "{{ bind.server }}"
record: "{{ item.value['dns-01']['record'] }}."
type: "TXT"
value: "{{ item.value['dns-01']['resource_value'] | regex_replace('^(.*)$', '\"\\1\"') }}"
state: present
with_dict:
- "{{ acme_challenge['challenge_data'] }}"
when: acme_method == 'dns-01'
Note dot .
after record
(DNS name) - it is required for some DNS server implementations, for example Bind9.
Request validation
Once the challenges are successfully published, Ansible can request CZERTAINLY to validate them:
- name: Let the challenge(s) be validated and retrieve the cert and intermediate certificate
community.crypto.acme_certificate:
account_key_src: "tmp/acme_account_key"
csr: "tmp/{{ acme_domain }}.csr"
dest: "tmp/{{ acme_domain }}.crt"
fullchain_dest: "tmp/{{ acme_domain }}-fullchain.crt"
chain_dest: "tmp/{{ acme_domain }}-intermediate.crt"
data: "{{ acme_challenge }}"
acme_directory: "{{ acme_directory }}"
acme_version: "{{ acme_version }}"
terms_agreed: true
force: "{{ acme_force }}"
modify_account: false
challenge: "{{ acme_method }}"
async: 120
This tasks is blocking and can take be time-consuming depending on how many challenges and how often are validated. Therefore, it is a good practice to set timeout for this task. With async: 120
, Ansible will wait for 2 minutes for challenge validation, and if it is not done even after this timeout, it will fail with an error.
Delete validation challenges
After successful challenge validation and obtaining issued certificate, we are going to do the cleanup:
- name: Clean http-01 challenge data from drive
ansible.builtin.file:
path: "{{ acme_web_dir }}/{{ item.value['http-01'].resource }}"
state: absent
with_dict:
- "{{ acme_challenge['challenge_data'] }}"
when: acme_method == 'http-01'
- name: Clean dns-01 challenge data from DNS
community.general.nsupdate:
key_algorithm: "{{ bind.key_algorithm }}"
key_name: "{{ bind.key_name }}"
key_secret: "{{ bind.key_secret }}"
server: "{{ bind.server }}"
record: "{{ item.value['dns-01'].record }}."
type: "TXT"
state: absent
with_dict:
- "{{ acme_challenge['challenge_data'] }}"
when: acme_method == 'dns-01'
Issued certificate
The issued certificate is placed into file "tmp/{{ acme_domain }}.crt"
. Placing it in the right location is specific and different for various end entities. This is out of the scope of this guide.