Skip to content

Certificate Authority Role — Ansible-internal CA

This page covers the use case where the role manages its own self-signed CA. See the overview for guidance on choosing between use cases and the full tag reference.

Tags Used

Tag Description
generate_ca Generate a new self-signed CA and save it to an encrypted vault file — see Generating the Ansible Root CA
trust_root_ca_windows Install the CA certificate into the Windows Trusted Root store
generate_csr_windows Generate a CSR on a Windows host using certreq.exe
sign_certificate Sign a CSR using the Ansible-managed CA
import_certificate_windows Import a signed certificate on a Windows host
export_certificate_windows Export the imported certificate from Cert:\LocalMachine\My as a password-protected PFX file
deploy_certificate_windows Copy an exported PFX from a Windows delegate host to a Windows target and import it
deploy_certificate_linux Fetch a PFX from a Windows delegate host, extract cert and key on the controller, and push them to a Linux target
import_pfx_windows Import a PFX file that already exists on the Windows host into LocalMachine\My
install_iis Install the IIS web server Windows feature
bind_iis_certificate Bind an issued certificate to an IIS web site

Prerequisites

A root CA must already exist as the encrypted vault file extra_vars/certificate_authority.yml. If it does not, generate it first — see Generating the Ansible Root CA. The vault file can come from another environment.

For every signing or issuing tag below, the vault file must be loaded so that ca_certificate and ca_private_key are available. Pass it using -e @extra_vars/certificate_authority.yml when running the issue playbook. The vault password must also be supplied — either via ansible_vault.txt (configured in ansible.cfg), --ask-vault-pass, or --vault-password-file=....

Usage

Step 1 — Trust the CA on Windows hosts

Install the CA certificate into the Trusted Root Certification Authorities store on each Windows host. This only needs to be done once per host, or whenever the CA is regenerated.

ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags trust_root_ca_windows -e @extra_vars/certificate_authority.yml

Step 2 — Issue a certificate (Windows)

The full certificate issuance workflow generates a CSR on the target host, signs it with the CA, and imports the result. These tags can be combined into a single playbook run:

ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags generate_csr_windows,sign_certificate,import_certificate_windows -e @extra_vars/certificate_authority.yml

Replacing an Existing Certificate

Set certificate_authority_cert_keep_copies: 0 in host_vars (or pass -e certificate_authority_cert_keep_copies=0) to remove all previous copies after import. Set it to a positive integer to keep that many of the most recently issued copies and remove the rest. Cleanup targets only certificates whose Windows friendly name matches the role's managed pattern (<prefix> - <cert_name> - <inventory_hostname>), so unrelated certificates in the store are never touched. The removal only runs after the new certificate is already present, so IIS automatic certificate rebinding is not interrupted.


Running Steps Individually

Each tag can be run independently when needed:

# Generate a CSR only (CSR content is shown in task output)
ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags generate_csr_windows -e @extra_vars/certificate_authority.yml

# Sign a CSR that was generated externally (set certificate_authority_sign_csr before running)
ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags sign_certificate -e @extra_vars/certificate_authority.yml -e certificate_authority_sign_csr="$(cat request.csr)"

# Import a certificate that was already signed (pass the cert directly via certificate_authority_cert_content)
ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags import_certificate_windows -e certificate_authority_cert_content="$(cat signed.pem)"

# Export an imported certificate as a PFX file (requires the cert to have been issued with exportable=true)
ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags export_certificate_windows -e certificate_authority_cert_thumbprint=<HEX>

Exporting a certificate as PFX

The export_certificate_windows tag exports a certificate from Cert:\LocalMachine\My to a password-protected .pfx file. The file name is <CN>_<inventory_hostname>.pfx (sanitized for Windows file name rules), e.g. C:\Temp\kuiper.sapphire.dev_epic-kpr-sapph1.sapphire.dev.pfx. Including inventory_hostname keeps per-target exports unique when several inventory hosts share a delegate and a CN, so one target's export can't overwrite another's. Existing files at the same path are silently overwritten on re-runs of the same target. The export directory is created if it does not exist.

After the export, certificate_authority_cert_export_path holds the full path to the PFX on the host where the certificate lives.

Exportable flag must be set at request time

The private key associated with a certificate can only be exported if the original CSR was generated with certificate_authority_cert_exportable: true. Set this before generate_csr_windows runs. There is no way to make an already-issued non-exportable cert exportable; you must re-issue.

PFX Password

The PFX is encrypted with certificate_authority_cert_export_pwd. The role's default for this is a placeholder — override it via host_vars/group_vars (preferably from an Ansible Vault) before exporting anything you intend to keep.


Deploying via a delegate host

When certificate_authority_delegate_host is set, all issue/export tasks run on the delegate — the cert and PFX live there, not on the inventory target.

Deploying to a Windows target

The deploy_certificate_windows tag closes the loop: it pulls the PFX from the delegate to the controller, pushes it to the actual target host, and imports it via import_pfx_windows. Predecessor cleanup (controlled by cert_keep_copies) runs on the target too, with the same friendly-name match as the delegate side.

ansible-playbook --limit=<targets> --forks=1 playbooks/certificate-authority-issue.yml --tags trust_root_ca_windows,generate_csr_windows,sign_certificate,import_certificate_windows,export_certificate_windows,deploy_certificate_windows -e @extra_vars/certificate_authority.yml

When no delegate is set (or when the delegate happens to equal the inventory host), deploy_certificate_windows short-circuits — the cert is already on the right host. Including the tag unconditionally in your chain is safe.

Deploying to a Linux target

The deploy_certificate_linux tag handles delivery to Linux targets. It fetches the PFX from the Windows delegate to the controller, extracts the certificate and private key using the Python cryptography library, and pushes them as PEM files to the Linux target. The delegate store entry and all controller staging files are cleaned up afterward.

ansible-playbook --limit=<linux-targets> --forks=1 playbooks/certificate-authority-issue.yml \
  --tags generate_csr_windows,sign_certificate,import_certificate_windows,export_certificate_windows,deploy_certificate_linux \
  -e @extra_vars/certificate_authority.yml \
  -e certificate_authority_cert_exportable=true \
  -e certificate_authority_linux_key_path=/etc/ssl/private/myhost.key \
  -e certificate_authority_linux_cert_path=/etc/ssl/certs/myhost.crt \
  --become

See the Linux Hosts page for the full variable reference and additional details on this workflow.

Importing a pre-built PFX

The import_pfx_windows tag imports a PFX file that already exists on the Windows host (built externally, copied by hand, restored from backup). It runs win_certificate_store on the target's LocalMachine\My, stamps the friendly name, publishes certificate_authority_cert_thumbprint, and runs the same predecessor cleanup as the rest of the chain.

ansible-playbook --limit=<targets> playbooks/certificate-authority-issue.yml --tags import_pfx_windows -e certificate_authority_cert_pfx_path='C:\path\to\my.pfx' -e certificate_authority_cert_export_pwd='<pfx password>'

When deploy_certificate_windows runs in the same play, it sets a marker fact so a subsequent import_pfx_windows invocation is detected and skipped with a clear message — preventing accidental double imports if you combine both tags.

Variables

All variables use the full certificate_authority_* name. They can be set in host_vars, group_vars, or via -e on the command line.

CA Variables

certificate_authority_vault_file is the only CA variable consumed during certificate issuance — it tells the role where the encrypted vault file is loaded from (default extra_vars/certificate_authority.yml). The variables that control CA generation itself (common name, key size, validity) are documented on the Generating the Ansible Root CA page.

Certificate Variables

Variable Default Description
certificate_authority_cert_validity_days 825 Issued certificate validity in days
certificate_authority_cert_not_before +0s Certificate not-before offset
certificate_authority_cert_subject (first DNS SAN) Certificate subject (CN). Defaults to the first DNS-type entry in the resolved SAN list if not set. User-supplied entries in certificate_authority_cert_sans come first in that list, so cert_subject defaults to your first explicit DNS SAN when one is provided.
certificate_authority_cert_key_length 2048 RSA key length for the CSR
certificate_authority_cert_exportable true Whether the private key is exportable. Must be true if you intend to use export_certificate_windows.
certificate_authority_cert_sans [] Additional SANs. Each entry uses the prefix-string form "TYPE:VALUE" (OpenSSL-style): "DNS:foo.example.com", "IP:10.0.0.5", "EMAIL:admin@example.com", "UPN:user@example.com", "URI:https://...", "DN:...", "OID:...", "GUID:...". Bare values (no recognized prefix) are treated as DNS for backward compatibility. Order: these entries are placed first in the resolved SAN list — before any auto-added default names or auto-detected IP — so the subject default (cert_subject = first DNS SAN) prefers your explicit values.
certificate_authority_cert_include_default_names true Include FQDN, inventory hostname, and short hostname as DNS SANs. When gather_facts is false, falls back to inventory_hostname + inventory_hostname_short only.
certificate_authority_cert_include_ip_san false Auto-add the host's primary IPv4 (first non-loopback, non-link-local entry from ansible_ip_addresses) as an IP: SAN. Without gather_facts, add an explicit "IP:<addr>" entry to certificate_authority_cert_sans instead.
certificate_authority_cert_key_usage [digitalSignature, keyEncipherment] Key usage values
certificate_authority_cert_key_usage_critical true Mark Key Usage extension as critical
certificate_authority_cert_extended_key_usage [serverAuth] Extended key usage OIDs
certificate_authority_cert_extended_key_usage_critical false Mark Extended Key Usage extension as critical
certificate_authority_cert_name default Logical name for this certificate. Combined with the prefix and target hostname to form the Windows friendly name (<prefix> - <cert_name> - <inventory_hostname>).
certificate_authority_cert_friendly_name_prefix Ansible Prefix for the Windows certificate friendly name. Full format: <prefix> - <cert_name> - <inventory_hostname> (e.g. Ansible - kuiper - epic-kpr-sapph1.sapphire.dev).
certificate_authority_cert_keep_copies (unset — no cleanup) How many older copies to keep after a successful issue. Windows: copies matched by friendly name in LocalMachine\My; 0 removes all, N keeps the N most recently issued. Linux: numbered file backups (.1 = most recent); 0 removes current and all backups before writing, N keeps N backup files. Leave unset to disable cleanup/backup on both platforms.
certificate_authority_cert_temp_dir C:\Windows\Temp Temp directory on the Windows host for intermediate files
certificate_authority_cert_content (set by sign tasks) Signed certificate PEM. Set automatically by sign_certificate; supply directly via -e for a standalone import.
certificate_authority_cert_thumbprint (set by sign or import tasks) SHA-1 thumbprint of the certificate being managed (uppercase hex, no separators). Set by sign_certificate and import_certificate_windows. Required input for export_certificate_windows.
certificate_authority_cert_export_dir C:\Temp Destination directory on the Windows host for export_certificate_windows. Created if missing.
certificate_authority_cert_export_pwd (role default; override) Password used to encrypt the PFX produced by export_certificate_windows. Always override in host_vars/group_vars (preferably via Ansible Vault).
certificate_authority_cert_export_path (set by export task) Full path to the exported PFX on the host where the cert lives. Set by export_certificate_windows.
certificate_authority_cert_pfx_path (set by deploy task; or provide explicitly) Local Windows path to a PFX file to import. Set by deploy_certificate_windows before it includes import_pfx_windows; supply directly via -e to use import_pfx_windows standalone.
certificate_authority_delegate_host (none, runs on each target) When set, generate_csr_windows, sign_certificate, import_certificate_windows, and export_certificate_windows all run on this single host for every inventory target. The CSR, signed certificate, private key, and exported PFX end up in the delegate host's machine store — distribute to the target separately if needed.

Serialize parallel runs when using a delegate host

With certificate_authority_delegate_host set and multiple inventory targets, Ansible's default forks=5 will issue concurrent certreq.exe and cert-store operations against the same Windows host. They can interleave and corrupt each other's pending requests. Always pass --forks=1 (or set serial: 1 on the play) so the per-target chains run one at a time on the delegate.


Binding the certificate to IIS

After a certificate has been issued and imported (import_certificate_windows), install IIS and bind the certificate in one run:

ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags install_iis,bind_iis_certificate -e @extra_vars/certificate_authority.yml

bind_iis_certificate requires certificate_authority_cert_thumbprint to be set — either carried forward from an earlier import step in the same play or passed explicitly via -e certificate_authority_cert_thumbprint=<HEX>.

# Bind an already-imported certificate by thumbprint
ansible-playbook --limit=epic-kpr-sapph1.sapphire.dev playbooks/certificate-authority-issue.yml --tags bind_iis_certificate -e certificate_authority_cert_thumbprint=<HEX>

IIS Variables

Variable Default Description
certificate_authority_iis_bindings see below List of IIS binding objects to configure

Each entry in certificate_authority_iis_bindings supports:

Key Default Description
name Default Web Site IIS site name
protocol https Binding protocol (http or https)
ip * Binding listen IP
port 443 Binding port
certificate_store_name MY Certificate store that holds the certificate (the LocalMachine\My store)
hostname (omitted) Hostname (DNS) for SNI/CCS bindings; required when use_sni or use_ccs is true
use_sni (omitted) Require Server Name Indication for SSL; requires hostname
use_ccs (omitted) Use Centralized Certificate Store for SSL; requires hostname

Sample host_vars

Kuiper

# host_vars/epic-kpr-sapph1.sapphire.dev.yml
certificate_authority_cert_sans: ["kuiper.sapphire.dev"]
certificate_authority_cert_name: kuiper
# certificate_authority_cert_include_ip_san: true
certificate_authority_cert_keep_copies: 2

System Pulse

# host_vars/epic-sp-sapph.sapphire.dev.yml
certificate_authority_cert_keep_copies: 2
certificate_authority_cert_name: "system pulse"
# certificate_authority_cert_include_ip_san: true
certificate_authority_cert_sans: ["systempulse.sapphire.dev"]

Sample Playbook Run

ansible-playbook playbooks/certificate-authority-issue.yml --limit='epic-kpr-sapph*,epic-sp-sapph*' -e @extra_vars/certificate_authority.yml --tags generate_csr_windows,sign_certificate,trust_root_ca_windows,import_certificate_windows,install_iis,bind_iis_certificate

Sample Playbook Run (Delegate Host)

ansible-playbook playbooks/certificate-authority-issue.yml --limit='epic-kpr-sapph*,epic-sp-sapph*' -e @extra_vars/certificate_authority.yml -e certificate_authority_cert_exportable=true --tags generate_csr_windows,sign_certificate,trust_root_ca_windows,import_certificate_windows,export_certificate_windows,deploy_certificate_windows,install_iis,bind_iis_certificate