eigenstate-ipa

OTP Plugin

Related docs:

  OTP CAPABILITIES     OTP USE CASES     IDM VAULT PLUGIN     AAP INTEGRATION     DOCS MAP  

Purpose

eigenstate.ipa.otp generates and manages OTP tokens and host enrollment passwords in FreeIPA/IdM from Ansible.

This reference covers:

The plugin generates credentials only. It does not perform host enrollment. Consume host enrollment passwords with freeipa.ansible_freeipa.ipaclient or freeipa.ansible_freeipa.ipahost.

Contents

Token Model

flowchart LR
    ans["Ansible task"]
    lookup["eigenstate.ipa.otp"]
    krb["Kerberos ticket\nexisting, password-derived,\nor keytab-derived"]
    ipa["ipalib OTP APIs\notptoken_add / find / show / del\nhost_mod random=True"]
    out["URI or password\nor state record"]

    ans --> lookup
    lookup --> krb
    krb --> ipa
    ipa --> out

TOTP and HOTP tokens are stored as IdM objects. When operation=add succeeds, IdM returns an otpauth:// URI that encodes the shared secret. This URI is the authenticator seed — it is only present in the add response and is never returned by find or show.

URI structure (TOTP example):

otpauth://totp/REALM:username?secret=BASE32SECRET&issuer=REALM&algorithm=SHA1&digits=6&period=30

The secret= parameter is the raw HMAC key. Treat the full URI as a credential. Use no_log: true on any task that registers or displays it.

Host Enrollment Model

When token_type=host, the plugin calls host_mod with random=True. IdM sets a one-time enrollment password on the host record and returns it in randompassword. This password is consumed exactly once by ipa-client-install or by the freeipa.ansible_freeipa.ipaclient role.

After the host uses the password to enroll, it is consumed and cannot be reused. Calling token_type=host again generates a fresh password.

The host record must already exist in IdM before calling this plugin. To create the host record, use the freeipa.ansible_freeipa.ipahost module.

Authentication Model

The lookup always operates with a Kerberos credential cache.

It can get there in three ways:

[!IMPORTANT] This plugin requires python3-ipalib and python3-ipaclient on the Ansible controller or execution environment. Install with dnf install python3-ipalib python3-ipaclient.

TLS behavior:

Operations

Operation _terms meaning Result
add (default) Usernames (totp/hotp) or host FQDNs (host) New token or enrollment password
find Optional substring filter (omit for all tokens) List of token metadata records
show Token unique IDs (ipatokenuniqueid) Token metadata record or exists=false
revoke Token unique IDs (ipatokenuniqueid) List of revoked token IDs

add: Creates a new token for each term. For token_type=totp and token_type=hotp, terms are IdM usernames. For token_type=host, terms are host FQDNs. Returns the URI (user tokens) or one-time password (host). Non-idempotent — each call creates a new token even if the user already has one.

find: Returns metadata for all tokens visible to the authenticated principal. An optional search string in _terms[0] filters by token ID or description substring. Use the owner option to restrict to a specific user. The URI is never included in find results.

show: Returns the metadata record for each token ID given. If a token ID does not exist, the record includes exists=false rather than raising an error. The URI is never included in show results.

revoke: Permanently deletes each token by ID. Raises an error if a token ID does not exist — revocation is not idempotent. To revoke all tokens for a user, combine find with owner filter and loop over the result.

Options Reference

Standard auth options

Option Type Default Notes
server str Required. IPA server FQDN. Env: IPA_SERVER.
ipaadmin_principal str admin Kerberos principal for authentication.
ipaadmin_password str Password for obtaining a Kerberos ticket. Env: IPA_ADMIN_PASSWORD. Secret.
kerberos_keytab str Path to a keytab file for non-interactive authentication. Env: IPA_KEYTAB.
verify str Path to the IPA CA certificate for TLS verification. Env: IPA_CERT.

OTP-specific options

Option Type Default Choices Notes
_terms list[str] Identifiers. See Operations table for per-operation meaning.
operation str add add, find, show, revoke Which operation to perform.
token_type str totp totp, hotp, host Token type. Only used by add. type remains an accepted compatibility alias.
algorithm str sha1 sha1, sha256, sha384, sha512 HMAC algorithm. Only applies to totp and hotp.
digits int 6 6, 8 OTP length. Only applies to totp and hotp.
interval int 30 TOTP time step in seconds. Only meaningful for token_type=totp; ignored with a warning for hotp and host.
owner str Filter find results to this user. Ignored with a warning for other operations.
description str Token description for add.
result_format str value (add), record (find/show) value, record, map, map_record Output container shape.

Result Record Fields

User token record (add, find, show)

Field Type Nullable Notes
owner str no IdM username
token_id str no ipatokenuniqueid — use this for show and revoke
type str no totp or hotp
uri str yes otpauth:// URI; only present on add; null in find/show records
algorithm str no sha1, sha256, sha384, or sha512
digits int yes 6 or 8
interval int or null yes TOTP time step; null for HOTP tokens
disabled bool no true when token is administratively disabled
description str or null yes Token description if set
exists bool no false only when show hits a missing token ID

Host enrollment record (add, token_type=host)

Field Type Notes
fqdn str Host FQDN as given in _terms
type str Always host
password str One-time enrollment password
exists bool Always true on successful add

Not-found record (show only)

When operation=show is given a token ID that does not exist in IdM, it returns a record with exists=false and all other fields set to null. No error is raised.

Return Shapes

value (default for add)

Returns a list of bare secret strings — URI for user tokens, password for host tokens.

uris: "{{ lookup('eigenstate.ipa.otp', 'alice', 'bob',
           server='idm-01.example.com',
           kerberos_keytab='/etc/admin.keytab') }}"
# uris[0] == 'otpauth://totp/...'
# uris[1] == 'otpauth://totp/...'

record (default for find and show)

Returns a list of result dictionaries, one per term.

token_records: "{{ lookup('eigenstate.ipa.otp', 'tok-abc123',
                    operation='show',
                    server='idm-01.example.com',
                    kerberos_keytab='/etc/admin.keytab') }}"
# token_records[0].token_id  == 'tok-abc123'
# token_records[0].exists    == true
# token_records[0].owner     == 'alice'

map

Returns a dictionary keyed by the primary identifier — owner for user token adds, fqdn for host adds, token_id for find/show — with bare secret values.

token_map: "{{ query('eigenstate.ipa.otp', 'alice', 'bob',
                server='idm-01.example.com',
                kerberos_keytab='/etc/admin.keytab',
                result_format='map') | first }}"
# token_map['alice'] == 'otpauth://totp/EXAMPLE.COM:alice?secret=...'

map_record

Returns a dictionary keyed by primary identifier with full result dictionaries as values.

token_map: "{{ query('eigenstate.ipa.otp', 'alice',
                server='idm-01.example.com',
                kerberos_keytab='/etc/admin.keytab',
                result_format='map_record') | first }}"
# token_map['alice'].token_id == 'tok-abc123'
# token_map['alice'].uri      == 'otpauth://totp/...'

Security Considerations

The OTP URI contains the shared secret. The otpauth:// URI returned by operation=add encodes the raw HMAC key in the secret= parameter. Anyone with the URI can generate valid OTP codes for the token.

Operational requirements:

Host enrollment passwords have a narrower risk window — they are one-time use and are consumed immediately by ipa-client-install. However, between generation and consumption they should be handled as sensitive values (use no_log: true, do not log, do not display).

Minimal Examples

Provision a TOTP token for a user:

- ansible.builtin.set_fact:
    totp_uri: "{{ lookup('eigenstate.ipa.otp', 'alice',
                   server='idm-01.corp.example.com',
                   kerberos_keytab='/runner/env/ipa/admin.keytab',
                   verify='/etc/ipa/ca.crt') | first }}"
  no_log: true

Generate a host enrollment password:

- ansible.builtin.set_fact:
    enroll_pass: "{{ lookup('eigenstate.ipa.otp', 'web-01.corp.example.com',
                     token_type='host',
                     server='idm-01.corp.example.com',
                     kerberos_keytab='/runner/env/ipa/admin.keytab',
                     verify='/etc/ipa/ca.crt') | first }}"
  no_log: true

Find all tokens owned by a user:

- ansible.builtin.set_fact:
    alice_tokens: "{{ lookup('eigenstate.ipa.otp',
                      operation='find',
                      owner='alice',
                      server='idm-01.corp.example.com',
                      kerberos_keytab='/runner/env/ipa/admin.keytab',
                      verify='/etc/ipa/ca.crt') }}"

Check whether a token exists by ID:

- ansible.builtin.set_fact:
    token_state: "{{ lookup('eigenstate.ipa.otp', 'tok-abc123',
                     operation='show',
                     server='idm-01.corp.example.com',
                     kerberos_keytab='/runner/env/ipa/admin.keytab',
                     verify='/etc/ipa/ca.crt') | first }}"

Revoke a token by ID:

- ansible.builtin.set_fact:
    revoked: "{{ lookup('eigenstate.ipa.otp', 'tok-abc123',
                  operation='revoke',
                  server='idm-01.corp.example.com',
                  kerberos_keytab='/runner/env/ipa/admin.keytab',
                  verify='/etc/ipa/ca.crt') }}"

Failure Boundaries

Common failure classes:

[!NOTE] operation=show with a missing token ID returns exists=false rather than raising an error. Use this for conditional logic without ignore_errors.

When To Read The Scenario Guide

Use OTP CAPABILITIES when you need operator patterns rather than option-by-option reference: