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