eigenstate-ipa

IdM Vault Capabilities

Related docs:

  IDM VAULT PLUGIN     INVENTORY CAPABILITIES     AAP INTEGRATION     DOCS MAP  

Purpose

Use this guide to choose the major IdM vault retrieval pattern exposed by eigenstate.ipa.vault.

It is the IdM-vault-side companion to the inventory capabilities guide.

Contents

Retrieval Model

flowchart LR
    vaults["IdM vaults"]
    scopes["user / service / shared scope"]
    auth["Kerberos auth"]
    lookup["eigenstate.ipa.vault"]
    tasks["Tasks, templates,\ncredential injection"]

    vaults --> lookup
    scopes --> lookup
    auth --> lookup
    lookup --> tasks

Choose the vault pattern that matches both:

1. Shared Standard Vaults: Estate-Wide Secret Injection

Use a shared standard vault when the same secret must be consumed by automation across many hosts or jobs and no extra decryption material should be required at lookup time.

Typical cases:

flowchart LR
    V["Shared standard vault"] --> L["lookup(..., shared=true)"] --> U["utf-8 secret for task/template use"]

Example:

- name: Read a shared database password
  ansible.builtin.set_fact:
    db_password: "{{ lookup('eigenstate.ipa.vault',
                     'database-password',
                     server='idm-01.corp.example.com',
                     ipaadmin_password=lookup('env', 'IPA_ADMIN_PASSWORD'),
                     shared=true,
                     verify='/etc/ipa/ca.crt') }}"

Why this pattern fits:

2. Shared Symmetric Vaults: Centrally Owned Secret With Extra Unlock Material

Use a symmetric vault when the automation should retrieve a centrally owned secret, but the vault itself should still require a second password for decryption.

Typical cases:

flowchart LR
    V["Shared symmetric vault"] --> P["vault_password\nor vault_password_file"] --> L["lookup"] --> U["decrypted secret"]

Example:

- name: Read a symmetric vault using a password file
  ansible.builtin.set_fact:
    api_key: "{{ lookup('eigenstate.ipa.vault',
                 'partner-api-key',
                 server='idm-01.corp.example.com',
                 kerberos_keytab='/runner/env/ipa/admin.keytab',
                 shared=true,
                 vault_password_file='/runner/env/ipa/partner-api.pass',
                 verify='/etc/ipa/ca.crt') }}"

Why this pattern fits:

3. Shared Asymmetric Vaults: Private-Key-Gated Retrieval

Use an asymmetric vault when the consuming automation should only succeed if it also possesses a corresponding private key.

Typical cases:

flowchart LR
    V["Shared asymmetric vault"] --> K["private_key_file"] --> L["lookup"] --> U["decrypted output"]

Example:

- name: Read a TLS private key from an asymmetric vault
  ansible.builtin.copy:
    content: "{{ lookup('eigenstate.ipa.vault',
                  'wildcard-tls-key',
                  server='idm-01.corp.example.com',
                  kerberos_keytab='/runner/env/ipa/admin.keytab',
                  shared=true,
                  private_key_file='/runner/env/ipa/tls-recovery.pem',
                  verify='/etc/ipa/ca.crt') }}"
    dest: /etc/pki/tls/private/wildcard.key
    mode: "0600"

Why this pattern fits:

4. User Vaults: Principal-Scoped Secrets

Use a user vault when the secret belongs to one named user principal rather than to a shared automation domain.

Typical cases:

flowchart LR
    V["User vault"] --> S["username='appuser'"] --> L["lookup"] --> U["principal-scoped secret"]

Example:

- name: Read a user-owned secret
  ansible.builtin.debug:
    msg: "{{ lookup('eigenstate.ipa.vault',
             'my-bootstrap-token',
             server='idm-01.corp.example.com',
             kerberos_keytab='/runner/env/ipa/admin.keytab',
             username='appuser',
             verify='/etc/ipa/ca.crt') }}"

Why this pattern fits:

5. Service Vaults: Service-Principal-Scoped Secrets

Use a service vault when the secret belongs to a service principal boundary.

Typical cases:

flowchart LR
    V["Service vault"] --> S["service='HTTP/app.example.com'"] --> L["lookup"] --> U["service-scoped secret"]

Example:

- name: Read a service-principal secret
  ansible.builtin.set_fact:
    service_secret: "{{ lookup('eigenstate.ipa.vault',
                         'oidc-client-secret',
                         server='idm-01.corp.example.com',
                         ipaadmin_password=lookup('env', 'IPA_ADMIN_PASSWORD'),
                         service='HTTP/app.corp.example.com',
                         verify='/etc/ipa/ca.crt') }}"

Why this pattern fits:

6. Binary Retrieval: Keytabs And Opaque Artifacts

Use encoding='base64' when the vault content is not reliably text.

Typical cases:

flowchart LR
    V["Binary vault data"] --> E["encoding='base64'"] --> L["lookup"] --> D["b64decode in consuming task"]

Example:

- name: Install service keytab from IdM vault
  ansible.builtin.copy:
    content: "{{ lookup('eigenstate.ipa.vault',
                  'service-keytab',
                  server='idm-01.corp.example.com',
                  kerberos_keytab='/runner/env/ipa/admin.keytab',
                  shared=true,
                  encoding='base64',
                  verify='/etc/ipa/ca.crt') | b64decode }}"
    dest: /etc/krb5.keytab
    mode: "0600"

Why this pattern fits:

[!CAUTION] Do not rely on the default utf-8 return mode for binary payloads. Use base64 intentionally when the content is a keytab, archive, bundle, or any other opaque byte sequence.

7. Rotation And Incident Response

Use IdM vault lookups when you need automation to consume the latest rotated secret without changing repository content.

Typical cases:

The operational advantage is:

That keeps secret version changes out of Git and out of static inventory.

8. Metadata Inspection Before Retrieval

Use operation='show' or operation='find' when the caller needs to inspect vault metadata before deciding whether payload retrieval is appropriate.

Typical cases:

flowchart LR
    find["operation=find"] --> inspect["metadata records"]
    show["operation=show"] --> inspect
    inspect --> retrieve["optional later retrieve"]

Why this pattern fits:

9. Brokered Sealed Artifact Metadata Convention

Use a consistent metadata shape when the vault is holding an opaque artifact that will be brokered by Ansible and consumed elsewhere.

Recommended fields:

Example convention:

sealed_artifact:
  vault_name: payments-bootstrap-bundle
  description: sealed bootstrap bundle for payments-agent
  consumer: payments-agent
  final_boundary: target-host
  format: base64

Why this pattern fits:

10. Brokered Sealed Artifact Delivery

Use this pattern when the vault is storing an encrypted or sealed artifact that Ansible should transport, but not interpret.

Typical cases:

flowchart LR
    vault["IdM vault with sealed payload"] --> lookup["lookup(..., encoding='base64', include_metadata=true)"]
    lookup --> broker["Ansible brokers payload plus metadata"]
    broker --> consumer["downstream agent, controller, or target host"]

Why this pattern fits:

This is not a HashiCorp Vault-style sealing controller or an Ansible Vault replacement. The collection retrieves and brokers the artifact. The downstream system is still responsible for final consumption or unseal.

11. Sealed Artifact Full Workflow

The sections above describe the brokered delivery pattern conceptually. This section covers the full end-to-end workflow with commands, for both the IPA admin path and a delegated operator path where admin rights are not required after initial vault setup.

The Key Distribution Problem

A sealed artifact can only be opened by the target host’s private key. That key never moves. The sealer only needs the target’s public certificate — which IdM can issue for an enrolled host and which is not sensitive material.

In practice, enrollment and certificate materialization are separate concerns:

Do not assume the target already has a decryptable host cert just because it is enrolled. Verify with getcert list or ipa-getcert list first.

flowchart LR
    hostpair["Host cert + private key\nstay on target"]
    seal["Seal blob with host cert"]
    store["Archive sealed blob to vault"]
    broker["Vault lookup returns opaque blob"]
    deliver["Deliver blob to target"]
    unseal["Target decrypts locally"]

    hostpair --> seal --> store --> broker --> deliver --> unseal

Prerequisite — Confirm A Target Cert/Key Pair Exists

Before sealing anything, confirm the target already has a CMS-usable host certificate and matching private key. If not, request one explicitly.

Example on the target host:

getcert list

getcert request \
  -I appserv-01-sealed-artifact \
  -c IPA \
  -k /etc/pki/tls/private/appserv-01-sealed-artifact.key \
  -f /etc/pki/tls/certs/appserv-01-sealed-artifact.pem \
  -K host/appserv-01.idm.corp.lan@IDM.CORP.LAN \
  -D appserv-01.idm.corp.lan \
  -w

Use the resulting cert/key paths consistently in both the seal verification and the final decrypt step.

IPA Admin Path

flowchart LR
    prereq["Verify target cert/key"]
    export["Export public cert"]
    seal["Seal payload"]
    archive["Create vault + archive blob"]
    retrieve["Vault lookup retrieves blob"]
    copy["Copy blob to target"]
    unseal["Target decrypts"]
    cleanup["Remove blob"]

    prereq --> export --> seal --> archive --> retrieve --> copy --> unseal --> cleanup

Use this when the operator has full IdM admin rights and owns the vault directly.

Step 1 — Ensure the target has a usable host cert/key pair

getcert list

If no suitable cert exists yet, request one on the target host:

getcert request \
  -I appserv-01-sealed-artifact \
  -c IPA \
  -k /etc/pki/tls/private/appserv-01-sealed-artifact.key \
  -f /etc/pki/tls/certs/appserv-01-sealed-artifact.pem \
  -K host/appserv-01.idm.corp.lan@IDM.CORP.LAN \
  -D appserv-01.idm.corp.lan \
  -w

Step 2 — Export the host certificate

kinit admin

# Use whichever export path your environment supports.
# Example: copy the cert from the target after certmonger issued it.
scp root@appserv-01.idm.corp.lan:/etc/pki/tls/certs/appserv-01-sealed-artifact.pem \
  appserv-01.pem

openssl x509 -in appserv-01.pem -noout -subject -dates

The host certificate is not sensitive. Any authenticated principal can read it.

Step 3 — Seal the artifact

openssl cms -encrypt \
  -in artifact.bin \
  -out sealed.bin \
  -recip appserv-01.pem \
  -aes256 \
  -binary \
  -outform DER

Only appserv-01 can decrypt this. The operator cannot unseal what they just sealed.

Step 4 — Create the vault and archive the sealed blob

ipa vault-add appserv-01-bootstrap \
  --type standard \
  --shared

ipa vault-archive appserv-01-bootstrap \
  --in sealed.bin \
  --shared

ipa vault-show appserv-01-bootstrap --shared

Step 5 — Retrieve and deliver via Ansible

- name: Deliver sealed bootstrap artifact
  hosts: appserv-01.idm.corp.lan
  gather_facts: false

  tasks:
    - name: Retrieve sealed blob from IdM vault
      ansible.builtin.set_fact:
        sealed_artifact: "{{ lookup('eigenstate.ipa.vault',
                             'appserv-01-bootstrap',
                             server='idm-01.idm.corp.lan',
                             kerberos_keytab='/runner/env/ipa/admin.keytab',
                             shared=true,
                             encoding='base64',
                             result_format='record',
                             include_metadata=true,
                             verify='/etc/ipa/ca.crt') }}"
      delegate_to: localhost
      run_once: true

    - name: Write sealed blob to target
      ansible.builtin.copy:
        content: "{{ sealed_artifact.value | b64decode }}"
        dest: /var/lib/bootstrap/sealed.bin
        mode: "0600"
        owner: root
        group: root

    - name: Unseal artifact on target
      ansible.builtin.command:
        cmd: >
          openssl cms -decrypt
          -in /var/lib/bootstrap/sealed.bin
          -recip /etc/pki/tls/certs/appserv-01-sealed-artifact.pem
          -inkey /etc/pki/tls/private/appserv-01-sealed-artifact.key
          -inform DER
          -out /var/lib/bootstrap/artifact.bin

    - name: Remove sealed blob
      ansible.builtin.file:
        path: /var/lib/bootstrap/sealed.bin
        state: absent

Do not assume a cert at /etc/pki/tls/certs/ exists automatically just because the host is enrolled. Verify the exact cert/key paths with getcert list or ipa-getcert list and use those paths consistently.

If you are validating the decrypt step directly on the automation controller instead of on the target host, make sure the play has permission to read the certificate and private key. In the lab, controller-side decrypt on the example controller required become: true because the host key material was root-readable only.

Delegated Operator Path

flowchart LR
    admin["One-time vault setup"]
    prereq["Target cert/key exists"]
    seal["Seal artifact"]
    archive["Archive blob"]
    retrieve["Automation retrieves blob"]
    decrypt["Target decrypts"]

    admin --> seal
    prereq --> seal
    seal --> archive --> retrieve --> decrypt

Use this when the operator does not have IdM admin rights. An admin creates the vault and delegates access once. All subsequent operations run under the operator’s own principal.

Step Requires IdM admin? Who acts
Read host cert No Any authenticated principal
Seal artifact No Any operator
Create vault Yes IdM admin, one time
Delegate vault access Yes IdM admin, one time
Archive sealed blob No Delegated operator
Retrieve via Ansible No Service principal with vault membership
Unseal on host No Target host or local process

Admin one-time setup

kinit admin

ipa vault-add appserv-01-bootstrap \
  --type standard \
  --shared

# Grant the delegated operator write access
ipa vault-add-member appserv-01-bootstrap \
  --shared \
  --users operator-principal

# Grant the automation service principal read access
ipa vault-add-member appserv-01-bootstrap \
  --shared \
  --services "HTTP/automation.idm.corp.lan"

Operator steps — no admin rights required after this point

# Authenticate as the delegated operator
kinit operator-principal

# Ensure the target cert/key pair exists first
getcert list

# Export or copy the host certificate using a method your environment supports
scp root@appserv-01.idm.corp.lan:/etc/pki/tls/certs/appserv-01-sealed-artifact.pem \
  appserv-01.pem

# Seal the artifact locally
openssl cms -encrypt \
  -in artifact.bin \
  -out sealed.bin \
  -recip appserv-01.pem \
  -aes256 \
  -binary \
  -outform DER

# Archive to the vault — operator is a member, no admin ticket required
ipa vault-archive appserv-01-bootstrap \
  --in sealed.bin \
  --shared

The Ansible retrieval and unseal steps are identical to the admin path. The only difference is which principal the service keytab belongs to.

In this pattern, the collection returns the sealed artifact as a base64-encoded structured record with:

and the resulting blob is only decryptable by the intended target that holds the matching private key.

[!NOTE] For a tighter ownership boundary, use a service-scoped vault tied to host/appserv-01.idm.corp.lan rather than a shared vault. This limits retrieval to principals explicitly granted access to that service scope.

[!CAUTION] If the host certificate is renewed or replaced after the artifact is sealed, the sealed blob cannot be decrypted with the new key pair. Re-seal against the current cert and re-archive before the next delivery run.

12. Structured Text Payloads

Use the text-normalization helpers when the vault payload is text, but the consumer really wants structured data or newline-free values.

Typical cases:

flowchart LR
    text["UTF-8 vault payload"] --> helper["decode_json or strip_trailing_newline"] --> task["task or template consumes normalized value"]

Why this pattern fits:

Quick Decision Matrix

Need Best pattern
Simple centrally shared text secret shared standard vault
Centrally shared secret with extra unlock material shared symmetric vault
Secret gated by private-key possession shared asymmetric vault
Principal-owned secret user vault
Service-identity-owned secret service vault
Binary artifact any of the above with encoding='base64'
Inspect before retrieving operation='show' or operation='find'
Brokered sealed artifact consistent metadata convention plus include_metadata=true
Opaque encrypted artifact with routing context encoding='base64' plus include_metadata=true
Parsed JSON or trimmed text decode_json=true or strip_trailing_newline=true

For option-level behavior, failure modes, and exact lookup syntax, return to IDM VAULT PLUGIN.