eigenstate-ipa

IdM Cert Capabilities

Related docs:

  IDM CERT PLUGIN     IDM VAULT CAPABILITIES     ROTATION CAPABILITIES     AAP INTEGRATION     DOCS MAP  

Purpose

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.

Contents

Request Model

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:

1. Service Certificate Request

Use 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:

2. Certificate Retrieval By Serial Number

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:

3. Pre-Expiry Maintenance: Find By Expiry Window

Use 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:

[!NOTE] The valid_not_after_from and valid_not_after_to filters use the CA’s own validity fields. They do not require parsing the certificate on the controller. The metadata comes back from ipalib directly.

4. Multi-Principal Batch Request

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:

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

5. Certificate Deployment: PEM File On Target

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:

6. Certificate Plus Vault Key Bundle

Use 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:

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

7. Expiry Audit Integrated With IdM Inventory

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:

8. Full Cert Lifecycle Workflow

This 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

Step 1 — Generate Key And CSR

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.

Step 2 — Request The Signed Certificate

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

Step 3 — Deploy Cert And Key To Target

- 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

Step 4 — Maintain: Find Before Expiry

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 separate ipa CLI task or via the IdM API directly when needed.

Quick Decision Matrix

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.