Skip to main content
RunBook Academy

AnsibleXLIX · Compliance, Validation and CertificatesCompliance, validation and certificates

Certificates, end to end

Advanced⏱ ~30 minansible-playbookansible-galaxy

What you'll learn

  • Build a certificate with community.crypto in the correct order and with correct permissions
  • Deploy a key and certificate so that only the consuming service can read the key
  • Choose reload over restart, and know when reload is not enough
  • Read expiry with x509_certificate_info so renewal is scheduled rather than reactive

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

A certificate has four failure modes and automation addresses them in descending order of how often they are handled:

  1. It is missing or wrong — handled by everyone.
  2. Its private key is readable by users who should not read it — handled by some.
  3. The service is serving the old one because nothing reloaded — handled by fewer.
  4. It expires on a Saturday — handled by almost nobody until it happens once.

This lesson is the sequence that addresses all four.

community.crypto is a collection, not part of ansible-core. It has to be installed before any of this runs:

Read-only / Safethe collection has to be present
ansible-galaxy collection list community.crypto
ansible-galaxy collection install community.crypto

The sequence

Three modules in order, each producing the input to the next. The order is not negotiable — a CSR needs a key, a certificate needs a CSR.

Configuration changekey, CSR, certificate
- name: Generate the private key
community.crypto.openssl_privatekey:
  path: /etc/ssl/private/{{ cert_common_name }}.key
  type: RSA
  size: 4096
  owner: root
  group: "{{ service_group }}"
  mode: '0640'

- name: Generate the certificate signing request
community.crypto.openssl_csr:
  path: /etc/ssl/csr/{{ cert_common_name }}.csr
  privatekey_path: /etc/ssl/private/{{ cert_common_name }}.key
  common_name: "{{ cert_common_name }}"
  organization_name: Example Organisation
  subject_alt_name: "{{ cert_sans }}"
  key_usage:
    - digitalSignature
    - keyEncipherment
  extended_key_usage:
    - serverAuth
  owner: root
  group: root
  mode: '0644'

- name: Sign the certificate against the internal CA
community.crypto.x509_certificate:
  path: /etc/ssl/certs/{{ cert_common_name }}.crt
  csr_path: /etc/ssl/csr/{{ cert_common_name }}.csr
  provider: ownca
  ownca_path: /etc/ssl/ca/internal-ca.crt
  ownca_privatekey_path: /etc/ssl/ca/internal-ca.key
  ownca_not_after: "+365d"
  owner: root
  group: root
  mode: '0644'
notify: reload web server

provider is required and takes acme, ownca or selfsigned. For a public certificate the shape is the same with provider: acme, preceded by acme_account and acme_certificate to complete the challenge; the deployment, permissions and reload half of this lesson is identical either way.

With subject_alt_name, values carry a prefix — DNS:, IP:, URI::

cert_sans:
  - "DNS:{{ cert_common_name }}"
  - 'DNS:app.example.com'
  - 'IP:192.0.2.10'

Permissions are the part that goes wrong

A validation assertion, so the mode is checked rather than assumed:

Read-only / Safeassert the permissions, do not assume them
- name: Read the private key metadata
ansible.builtin.stat:
  path: /etc/ssl/private/{{ cert_common_name }}.key
register: key_stat
check_mode: false

- name: The private key is not readable beyond its group
ansible.builtin.assert:
  that:
    - key_stat.stat.exists
    - key_stat.stat.pw_name == 'root'
    - key_stat.stat.mode is match('06[04]0')
  fail_msg: >-
    Private key on {{ inventory_hostname }} is
    {{ key_stat.stat.mode | default('missing') }} owned by
    {{ key_stat.stat.pw_name | default('nobody') }}
  quiet: true

Reload, not restart

The handler reloads. It matters, and the reason generalises.

Service impact possiblehandlers/main.yml
- name: reload web server
ansible.builtin.systemd_service:
  name: nginx
  state: reloaded

A restart terminates the process and every connection it holds. Across a fleet of 40 web servers on a certificate renewal, that is 40 simultaneous connection drops for a change that did not require any of them.

Two caveats keep this from being a rule you apply blindly.

Not every service picks up a certificate on reload. Some read certificate material only at startup. Confirm it for the specific service rather than assuming; the validation step below is what tells you, because it checks the certificate the service is actually serving.

A reload does not survive a bad certificate. If the new file is malformed the reload fails and the service continues with the previous one — which is the safe outcome, and it means the play must notice the failed reload rather than treating it as a no-op.

Expiry is a fact, not a calendar reminder

The fourth failure mode. x509_certificate_info reads a certificate and returns not_after as an ASN.1 time, expired as a boolean, and — the useful one — valid_at, which the documentation describes as returning, for each named time specification, “a boolean whether the certificate is valid at that point in time or not”.

Read-only / Saferenewal scheduled from the certificate itself
- name: Read the deployed certificate
community.crypto.x509_certificate_info:
  path: /etc/ssl/certs/{{ cert_common_name }}.crt
  valid_at:
    in_two_weeks: "+14d"
    in_a_month: "+30d"
register: cert_info
check_mode: false

- name: The certificate is valid now
ansible.builtin.assert:
  that:
    - not cert_info.expired
  fail_msg: >-
    EXPIRED certificate on {{ inventory_hostname }}:
    not_after {{ cert_info.not_after }}
  quiet: true

- name: The certificate is not inside the renewal window
ansible.builtin.assert:
  that:
    - cert_info.valid_at.in_a_month
  fail_msg: >-
    {{ inventory_hostname }} certificate expires within 30 days
    (not_after {{ cert_info.not_after }}) - schedule renewal
  quiet: true

The two assertions are deliberately different findings. The first is an incident. The second is a work item with a month of lead time, and it is the one that prevents the first.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A scheduled play regenerates certificates using the community.crypto sequence. A certificate expires in four days and the play reports ok on every task, changing nothing. Why?

  2. Q2. A renewal play writes the new certificate and notifies a reload handler. The service continues serving the old certificate. Which are plausible causes? Select all that apply.

  3. Q3. Asserting on the certificate file with x509_certificate_info is necessary but not sufficient - proving the service serves a valid certificate requires connecting to it.

  4. Q4. Which permission arrangement is correct for a TLS private key read by a service that does not run as root?

Passing score: 75%. Answers are checked in this browser.