Related docs:
IDM CERT PLUGIN IDM VAULT CAPABILITIES ROTATION CAPABILITIES AAP INTEGRATION DOCS MAP
Use this guide to choose the major IdM PKI automation pattern exposed by
eigenstate.ipa.cert.
It is the cert-side companion to the vault capabilities guide. Where the vault guide covers secret retrieval, this guide covers certificate lifecycle operations: signing new certs, retrieving existing ones, and finding certs by expiry or principal.
IdM already runs Dogtag CA. This plugin gives Ansible a direct signing and retrieval interface without certmonger running interactively on the target.
flowchart LR
csr["CSR input"]
principal["Service or host principal"]
auth["Kerberos auth"]
lookup["eigenstate.ipa.cert"]
ca["IdM CA\nDogtag PKI"]
result["Signed cert\nPEM or base64"]
csr --> lookup
principal --> lookup
auth --> lookup
lookup --> ca
ca --> result
Choose the cert operation that matches what you actually need at runtime:
request when the cert does not exist yet or should be re-issuedretrieve when the cert already exists and you need it by serialfind when you need to discover certificates by principal or expiry dateUse operation='request' when a service principal needs a signed certificate
and you are supplying the CSR from the controller or a file.
Typical cases:
flowchart LR
csr["CSR on controller"] --> request["operation=request"] --> signed["Signed PEM from IdM CA"]
Example:
- name: Request a signed service certificate from IdM
ansible.builtin.set_fact:
api_cert_pem: "{{ lookup('eigenstate.ipa.cert',
'HTTP/api.example.com@EXAMPLE.COM',
operation='request',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
csr_file='/runner/env/csr/api.example.com.csr',
verify='/etc/ipa/ca.crt') }}"
Why this pattern fits:
Use operation='retrieve' when you already know the certificate serial number
and need to pull the signed cert without re-issuing it.
Typical cases:
flowchart LR
serial["Serial number"] --> retrieve["operation=retrieve"] --> cert["Existing signed cert from CA"]
Example:
- name: Retrieve certificate by serial number
ansible.builtin.set_fact:
cert_record: "{{ lookup('eigenstate.ipa.cert',
'12345',
operation='retrieve',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
result_format='record',
verify='/etc/ipa/ca.crt') }}"
Why this pattern fits:
find stepresult_format='record' carries the expiry and subject alongside the cert so
the playbook can make a validity check before deployingUse operation='find' with a valid_not_after_to date to discover all
certificates expiring before a target date. Pair with valid_not_after_from
to scope the window.
Typical cases:
flowchart LR
window["Expiry window\nvalid_not_after_from + valid_not_after_to"] --> find["operation=find"] --> expiring["List of expiring certs with metadata"]
Example:
- name: Find certificates expiring within 60 days
ansible.builtin.set_fact:
expiring_certs: "{{ lookup('eigenstate.ipa.cert',
operation='find',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
valid_not_after_from='2026-04-05',
valid_not_after_to='2026-06-05',
result_format='map_record',
verify='/etc/ipa/ca.crt') }}"
- name: Report expiring certificates
ansible.builtin.debug:
msg: "Cert {{ item.key }} for {{ item.value.metadata.subject }} expires {{ item.value.metadata.valid_not_after }}"
loop: "{{ expiring_certs | dict2items }}"
when: expiring_certs | length > 0
Why this pattern fits:
valid_not_after_to maps directly onto a renewal deadlineresult_format='map_record' keys results by serial number so the consuming
task can act on each cert independently without caring about list positionsubject and san, giving the playbook
enough context to re-request the right CSR without a separate IdM query[!NOTE] The
valid_not_after_fromandvalid_not_after_tofilters use the CA’s own validity fields. They do not require parsing the certificate on the controller. The metadata comes back fromipalibdirectly.
Use terms with multiple principals when you need certificates for several
service principals in one lookup call.
Typical cases:
flowchart LR
principals["Multiple principals in terms"] --> request["operation=request"] --> certs["One signed cert per principal"]
Example:
- name: Request certificates for a set of service principals
ansible.builtin.set_fact:
service_certs: "{{ lookup('eigenstate.ipa.cert',
'HTTP/web-01.example.com@EXAMPLE.COM',
'HTTP/web-02.example.com@EXAMPLE.COM',
'HTTP/web-03.example.com@EXAMPLE.COM',
operation='request',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
csr_file='/runner/env/csr/cluster.csr',
result_format='map',
verify='/etc/ipa/ca.crt') }}"
- name: Write each cert to its target
ansible.builtin.copy:
content: "{{ service_certs['HTTP/' + inventory_hostname + '@EXAMPLE.COM'] }}"
dest: /etc/pki/tls/certs/service.pem
mode: "0644"
Why this pattern fits:
result_format='map' keys results by principal so the consuming task can
index directly by host without positional lookups[!CAUTION] When the same CSR is reused for multiple principals, the CN in the CSR only matches one of them. IdM validates the CSR subject against the principal. Use a wildcard or SAN-based CSR if the CA profile allows it, or generate one CSR per principal and call the lookup individually.
Use the result of a request or retrieve lookup to write a .crt file to a
target host in the same play.
Typical cases:
flowchart LR
req["operation=request or retrieve"] --> pem["PEM string in set_fact"] --> copy["ansible.builtin.copy to target"]
Example:
- name: Issue and deploy service certificate
hosts: api.example.com
gather_facts: false
tasks:
- name: Request signed certificate from IdM
ansible.builtin.set_fact:
service_cert: "{{ lookup('eigenstate.ipa.cert',
'HTTP/api.example.com@EXAMPLE.COM',
operation='request',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
csr_file='/runner/env/csr/api.csr',
verify='/etc/ipa/ca.crt') }}"
delegate_to: localhost
run_once: true
- name: Write certificate to target
ansible.builtin.copy:
content: "{{ service_cert }}"
dest: /etc/pki/tls/certs/api.crt
mode: "0644"
owner: root
group: root
- name: Reload dependent service
ansible.builtin.systemd:
name: httpd
state: reloaded
Why this pattern fits:
copy can write without any decodingdelegate_to: localhost keeps the CA interaction on the controller while
the file write lands on the targetencoding='pem' and the default result_format='value' give
the simplest possible output for file-write use casesUse eigenstate.ipa.cert alongside eigenstate.ipa.vault when a service
needs both a signed certificate and the matching private key at deploy time.
The private key lives in an IdM vault. The certificate comes from the IdM CA. Both are assembled on the controller and delivered together.
Typical cases:
flowchart LR
vault["Private key in vault"] --> key_lookup["Vault lookup"]
ca["Signed cert in IdM CA"] --> cert_lookup["Cert lookup"]
key_lookup --> bundle["Assemble bundle"]
cert_lookup --> bundle
bundle --> target["Deploy to target"]
Example:
- name: Deploy service TLS cert and key from IdM
hosts: api.example.com
gather_facts: false
tasks:
- name: Retrieve private key from IdM vault
ansible.builtin.set_fact:
service_key: "{{ lookup('eigenstate.ipa.vault',
'api.example.com-private-key',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
shared=true,
verify='/etc/ipa/ca.crt') }}"
delegate_to: localhost
run_once: true
no_log: true
- name: Request signed certificate from IdM CA
ansible.builtin.set_fact:
service_cert: "{{ lookup('eigenstate.ipa.cert',
'HTTP/api.example.com@EXAMPLE.COM',
operation='request',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
csr_file='/runner/env/csr/api.csr',
verify='/etc/ipa/ca.crt') }}"
delegate_to: localhost
run_once: true
- name: Write private key to target
ansible.builtin.copy:
content: "{{ service_key }}"
dest: /etc/pki/tls/private/api.key
mode: "0600"
owner: root
group: root
no_log: true
- name: Write certificate to target
ansible.builtin.copy:
content: "{{ service_cert }}"
dest: /etc/pki/tls/certs/api.crt
mode: "0644"
owner: root
group: root
- name: Reload service
ansible.builtin.systemd:
name: httpd
state: reloaded
Why this pattern fits:
no_log: true on the key tasks keeps the private key off controller logs[!CAUTION] Never pass the private key through a CSR or cert request parameter. The CSR should contain only the public key. The private key stays in the vault and moves directly to the target via the vault lookup.
Use operation='find' with an expiry window alongside eigenstate.ipa.idm
dynamic inventory to scope renewal plays to only the affected hosts.
Typical cases:
flowchart LR
inventory["Dynamic inventory"]
find["Find expiring certs"]
intersect["Match principals\nto hosts"]
renew["Renew affected hosts"]
inventory --> intersect
find --> intersect
intersect --> renew
Example:
- name: Identify and renew expiring service certs
hosts: all
gather_facts: false
tasks:
- name: Find certs expiring in the next 30 days
ansible.builtin.set_fact:
expiring: "{{ lookup('eigenstate.ipa.cert',
operation='find',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
valid_not_after_from='2026-04-05',
valid_not_after_to='2026-05-05',
result_format='map_record',
verify='/etc/ipa/ca.crt') }}"
delegate_to: localhost
run_once: true
- name: Report what is expiring
ansible.builtin.debug:
msg: >
Serial {{ item.key }}
subject={{ item.value.metadata.subject }}
expires={{ item.value.metadata.valid_not_after }}
loop: "{{ expiring | dict2items }}"
run_once: true
when: expiring | length > 0
- name: Request renewal for principals matching this host
ansible.builtin.set_fact:
renewed_cert: "{{ lookup('eigenstate.ipa.cert',
'HTTP/' + inventory_hostname + '@EXAMPLE.COM',
operation='request',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
csr_file='/runner/env/csr/' + inventory_hostname + '.csr',
verify='/etc/ipa/ca.crt') }}"
delegate_to: localhost
when: >
expiring.values() |
selectattr('metadata.subject', 'search', inventory_hostname) |
list | length > 0
Why this pattern fits:
result_format='map_record' gives the loop both the serial and the full
metadata without a second queryThis section covers the end-to-end path from CSR generation through cert expiry, for operators who own the full cert lifecycle in automation.
flowchart LR
key["Generate key + CSR"]
vault["Optional: archive key"]
request["Request signed cert"]
deploy["Deploy cert + key"]
maintain["Find expiring certs"]
renew["Renew or revoke"]
key --> request --> deploy --> maintain --> renew
key -.-> vault
vault -.-> deploy
On the controller or a dedicated PKI host:
# Generate private key
openssl genrsa -out /runner/env/csr/api.example.com.key 2048
chmod 0600 /runner/env/csr/api.example.com.key
# Generate CSR
openssl req -new \
-key /runner/env/csr/api.example.com.key \
-out /runner/env/csr/api.example.com.csr \
-subj "/CN=api.example.com" \
-addext "subjectAltName=DNS:api.example.com"
If the key should be stored in IdM vault for later retrieval, archive it before proceeding. See the vault capabilities guide for the archive workflow.
- name: Request signed certificate
ansible.builtin.set_fact:
signed_cert: "{{ lookup('eigenstate.ipa.cert',
'HTTP/api.example.com@EXAMPLE.COM',
operation='request',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
csr_file='/runner/env/csr/api.example.com.csr',
result_format='record',
verify='/etc/ipa/ca.crt') }}"
delegate_to: localhost
run_once: true
Record the metadata.serial_number from the result. It is the stable
identifier for retrieval and revocation.
- name: Write certificate
ansible.builtin.copy:
content: "{{ signed_cert.value }}"
dest: /etc/pki/tls/certs/api.crt
mode: "0644"
- name: Write private key (from vault if stored there)
ansible.builtin.copy:
content: "{{ service_key }}"
dest: /etc/pki/tls/private/api.key
mode: "0600"
no_log: true
- name: Reload service
ansible.builtin.systemd:
name: httpd
state: reloaded
Run this on a schedule to catch expiring certs before they cause outages:
- name: Find certs expiring in 30 days
ansible.builtin.set_fact:
expiring: "{{ lookup('eigenstate.ipa.cert',
operation='find',
server='idm-01.corp.example.com',
kerberos_keytab='/runner/env/ipa/admin.keytab',
valid_not_after_from='{{ ansible_date_time.date }}',
valid_not_after_to='{{ (ansible_date_time.date | to_datetime(\"%Y-%m-%d\") + timedelta(days=30)) | strftime(\"%Y-%m-%d\") }}',
result_format='map_record',
verify='/etc/ipa/ca.crt') }}"
delegate_to: localhost
run_once: true
To renew, re-run the request operation for the same principal with the
same or a freshly generated CSR. The IdM CA issues a new cert with a new
serial. The previous cert remains valid until its original expiry date unless
explicitly revoked.
[!NOTE] Revocation (
ipa cert-revoke) is out of scope for the lookup plugin. The plugin is a retrieval and issuance interface. Revocation requires write access to the CA and is best handled as a separateipaCLI task or via the IdM API directly when needed.
| Need | Best pattern |
|---|---|
| Issue a new cert for a service principal | operation='request' with csr_file or csr |
| Pull an existing cert by serial | operation='retrieve' with serial number term |
| Discover certs expiring within a date range | operation='find' with valid_not_after_from + valid_not_after_to |
| Find all certs for a specific principal | operation='find' with principal filter |
| Deploy cert file to target host | result_format='value' (default) + ansible.builtin.copy |
| Keep expiry and subject alongside the cert | result_format='record' |
| Index multiple certs by principal or serial | result_format='map' or result_format='map_record' |
| Cert + private key bundle delivery | eigenstate.ipa.cert request + eigenstate.ipa.vault key retrieval |
| Targeted renewal from dynamic inventory | operation='find' expiry window intersected with IdM inventory groups |
| Auto-create principal if missing | operation='request' with add=true |
| Override the signing profile | operation='request' with profile=<profile_id> |
For option-level behavior, failure modes, and exact lookup syntax, return to IDM CERT PLUGIN.