Skip to content

Certificate Authority Role — Microsoft AD-integrated CA

This page covers the use case where the role submits certificate requests to an existing Microsoft Enterprise CA. See the overview for guidance on choosing between use cases and the full tag reference.

Tags Used

Tag Description
generate_csr_windows Generate a CSR on a Windows host using certreq.exe -new
sign_certificate_microsoft_ca Submit the CSR to a Microsoft CA via certreq.exe -submit
import_certificate_windows Bind the signed certificate to the pending request via certreq.exe -accept
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

  • The target host is domain-joined so the machine account can authenticate against the CA.
  • An Active Directory Enterprise CA publishes the template you want to use. The role auto-discovers a CA by querying pKIEnrollmentService objects in the Configuration NC over LDAP and selecting one whose certificateTemplates attribute lists certificate_authority_cert_template. When more than one CA publishes the template, the role picks one at random (seeded by inventory_hostname, so the same target consistently lands on the same CA across runs).
  • The machine account (or a group it belongs to) has Enroll permission on the certificate template you intend to use.

The role runs the AD-facing tasks as SYSTEM (become_user: SYSTEM) so the host's machine account is used for both AD lookups and the certreq submission. This is the correct enrollment identity for a machine certificate and avoids the WinRM/NTLM double-hop problem when querying AD.

No CA vault file or --ask-vault-pass is needed for this flow.

Usage

Issue a certificate (full chain)

The full workflow generates a CSR with certreq.exe -new, submits it to the CA with certreq.exe -submit, and binds the signed result with certreq.exe -accept. Combine the tags in one run:

ansible-playbook --limit=vm-lspiehler.lcmchealth.org playbooks/certificate-authority-issue.yml --tags generate_csr_windows,sign_certificate_microsoft_ca,import_certificate_windows -e 'certificate_authority_cert_template=SANWebServer(90Days)'

Required Variable

certificate_authority_cert_template must be set to the Microsoft CA template's CN (not display name). Quote the value if it contains parentheses.

Selecting a Specific CA

When unset, certificate_authority_cert_ca_config is auto-discovered by querying AD for CAs that publish the requested template. If exactly one CA publishes it, that CA is used. If multiple do, the role picks one at random (seeded by inventory_hostname, so a given target picks the same CA across runs) and logs the candidate list. Set certificate_authority_cert_ca_config to <server FQDN>\<CA CN> to pin a specific CA. If no CA publishes the template, the role fails with a hint to publish it, override the config, or pick a different template.

Replacing an Existing Certificate

Set 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.


Discover available templates

The role exposes two failure paths that double as diagnostics:

Template not published anywhere. If certificate_authority_cert_template names a template no CA publishes, LDAP discovery fails immediately:

No CA in AD publishes the template 'NoSuchTemplate'. Either publish the
template on a CA, set certificate_authority_cert_ca_config explicitly to
'<server>\<CAName>', or pick a different template via
certificate_authority_cert_template.

Template published but this machine cannot enroll. If the template exists on the resolved CA but the machine account has no Enroll permission, the role fails and prints the list of templates this machine can enroll for, annotated with (Auto-Enroll) / (Access is denied) and similar status from certutil -CATemplates. Useful for spotting templates you have access to:

- SANWebServer(90Days) (Auto-Enroll)
- WebServerAuto-Renewal (Access is denied)
- LCMCClientComputerCert (Auto-Enroll: Access is denied)

To browse all templates in AD without running the role, query directly on a domain-joined host:

# Templates published in AD (forest-wide)
certutil -ADTemplate

# Templates this machine can enroll for on a specific CA
certutil -CATemplates -config "server.example.com\CAName"

Discover available CAs

If certificate_authority_cert_ca_config is left unset and multiple CAs publish the requested template, the role picks one at random and prints the full candidate list for transparency:

Multiple CAs publish template SANWebServer(90Days). Picking one (seeded by inventory_hostname so the same host always gets the same CA). Candidates:
  - epic-dc1-ica01.example.com\EPIC-ICA01
  - epic-dc2-ica02.example.com\EPIC-ICA02

If you need to pin a specific CA, copy one of those strings into -e certificate_authority_cert_ca_config=... (single-quote it in bash because of the backslash). To browse all CAs in AD regardless of which templates they publish, run certutil -ADCA on a domain-joined host.


Running Steps Individually

Each tag can be run independently:

# Generate a CSR only (CSR content is shown in task output and stored at certificate_authority_sign_csr)
ansible-playbook --limit=vm-lspiehler.lcmchealth.org playbooks/certificate-authority-issue.yml --tags generate_csr_windows

# Submit an externally-generated CSR to the CA (no -new on this host)
ansible-playbook --limit=vm-lspiehler.lcmchealth.org playbooks/certificate-authority-issue.yml --tags sign_certificate_microsoft_ca -e 'certificate_authority_cert_template=SANWebServer(90Days)' -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=vm-lspiehler.lcmchealth.org 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=vm-lspiehler.lcmchealth.org 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_lansweeper-app.lcmchealth.org.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.

# Issue, import, and export in one chain
ansible-playbook --limit=vm-lspiehler.lcmchealth.org playbooks/certificate-authority-issue.yml --tags generate_csr_windows,sign_certificate_microsoft_ca,import_certificate_windows,export_certificate_windows -e 'certificate_authority_cert_template=SANWebServer(90Days)' -e certificate_authority_cert_exportable=true

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 generate_csr_windows,sign_certificate_microsoft_ca,import_certificate_windows,export_certificate_windows,deploy_certificate_windows -e 'certificate_authority_cert_template=SANWebServer(90Days)'

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 on the controller using the Python cryptography library, and pushes them as PEM files to the Linux target. The certificate is then removed from the delegate's LocalMachine\My store and all temporary controller files are cleaned up.

ansible-playbook --limit=<linux-targets> --forks=1 playbooks/certificate-authority-issue.yml \
  --tags generate_csr_windows,sign_certificate_microsoft_ca,import_certificate_windows,export_certificate_windows,deploy_certificate_linux \
  -e 'certificate_authority_cert_template=SANWebServer(90Days)' \
  -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.

Cleanup on Failure

certreq -new leaves a pending request in Cert:\LocalMachine\REQUEST. If sign_certificate_microsoft_ca was run in the same play as generate_csr_windows and the submission fails, the role automatically removes the exact pending request created in that run by its thumbprint. Repeated troubleshooting runs do not accumulate stale entries.

Pending requests created in previous runs (different thumbprints) are not touched by the automatic cleanup. Remove them manually with:

Get-ChildItem Cert:\LocalMachine\REQUEST | Where-Object { $_.Subject -like '*<your subject>*' } | Remove-Item

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.

Microsoft-CA-specific Variables

Variable Default Description
certificate_authority_cert_template (none, required) Microsoft CA template CN. List options with certutil -CATemplates -config "<server>\<CAName>" or certutil -ADTemplate.
certificate_authority_cert_ca_config (auto-discovered) CA -config string in <server FQDN>\<CA CN> form. When unset, the role queries AD for a CA that publishes the requested template and picks one at random (seeded by inventory_hostname) if multiple match. Set explicitly only when you need to pin a specific CA. List all CAs in AD with certutil -ADCA.
certificate_authority_delegate_host (none, runs on each target) When set, generate_csr_windows, sign_certificate_microsoft_ca, 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. Useful when only one server has enrollment permissions.

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.

Certificate Variables (CSR generation)

These variables shape the CSR generated by certreq.exe -new. The Microsoft CA template usually overrides most of them on the issued certificate (validity, key usage, EKU, etc. are template-controlled), but the values affect the CSR itself.

Variable Default Description
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 requested in the CSR
certificate_authority_cert_key_usage_critical true Mark Key Usage extension as critical
certificate_authority_cert_extended_key_usage [serverAuth] Extended key usage OIDs requested in the CSR
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 (deploy_certificate_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_microsoft_ca; 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_microsoft_ca 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.

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=vm-lspiehler.lcmchealth.org playbooks/certificate-authority-issue.yml --tags install_iis,bind_iis_certificate -e 'certificate_authority_cert_template=SANWebServer(90Days)'

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>.

# Full chain: issue, import, and bind to IIS
ansible-playbook --limit=vm-lspiehler.lcmchealth.org playbooks/certificate-authority-issue.yml --tags generate_csr_windows,sign_certificate_microsoft_ca,import_certificate_windows,install_iis,bind_iis_certificate -e 'certificate_authority_cert_template=SANWebServer(90Days)'

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_template: SANWebServer(90Days)
certificate_authority_cert_ca_config: 'epic-dc1-ica01.lcmchealth.org\EPIC-ICA01'
certificate_authority_cert_keep_copies: 2
# certificate_authority_cert_include_ip_san: true
# certificate_authority_delegate_host: epic-kpr-sapph1.sapphire.dev

System Pulse

# host_vars/epic-sp-sapph.sapphire.dev.yml
certificate_authority_cert_sans: ["systempulse.sapphire.dev"]
certificate_authority_cert_name: system pulse
certificate_authority_cert_template: SANWebServer(90Days)
certificate_authority_cert_ca_config: 'epic-dc1-ica01.lcmchealth.org\EPIC-ICA01'
certificate_authority_cert_keep_copies: 2
# certificate_authority_cert_include_ip_san: true
# certificate_authority_delegate_host: epic-kpr-sapph1.sapphire.dev

Sample Playbook Run

ansible-playbook --limit='epic-kpr-sapph*,epic-sp-sapph*' playbooks/certificate-authority-issue.yml --tags generate_csr_windows,sign_certificate_microsoft_ca,import_certificate_windows,install_iis,bind_iis_certificate

Sample Playbook Run (Delegate Host)

ansible-playbook --forks=1 --limit='epic-kpr-sapph*,epic-sp-sapph*' playbooks/certificate-authority-issue.yml -e certificate_authority_cert_exportable=true --tags generate_csr_windows,sign_certificate_microsoft_ca,import_certificate_windows,export_certificate_windows,deploy_certificate_windows,install_iis,bind_iis_certificate