Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration Reference

The configuration file is TOML. Its path is read from the AHDAPA_CONFIG environment variable (default: /etc/ahdapa/config.toml).

[server]

KeyTypeRequiredDescription
issuerstringyes*OAuth2 issuer URI. Appears in all JWT iss claims and in the discovery documents. Must be an https:// URL. Example: "https://idp.example.com". *When [gssapi] initiator_principal is set this field is optional — ahdapa auto-derives the issuer from the principal’s FQDN as https://{fqdn}/idp. Set explicitly to use the stable ipa-ca.<domain> DNS alias (recommended for multi-node IPA clusters).
realmstringyesKerberos realm. Used to construct full principal names for SPNEGO authentication. Example: "EXAMPLE.COM"
listenstringnoAddress to listen on. Accepts host:port for TCP or unix:/path/to/socket (and the bare /path/to/socket shorthand) for a Unix domain socket. Default: "0.0.0.0:8080". Overridden by the AHDAPA_LISTEN environment variable.
display_namestringnoHuman-readable name shown in the WebUI login page title and the admin panel header. Defaults to the issuer URI when not set. Example: "Acme Corp Identity"
registration_tokenstringnoWhen set, enables the pre-shared-token path of POST /register (RFC 7591 dynamic client registration): requests must carry Authorization: Bearer <token> matching this value. When absent, that path is unavailable. The endpoint is also reachable — independently of this key — by a session cookie whose sub is a Kerberos service principal from the server’s own realm (see Dynamic Client Registration). The endpoint returns 404 only when registration_token is absent and no valid service-principal session is present.
auth_rate_limitintegernoMaximum number of authentication attempts per source IP in any five-minute rolling window. Applies to password login, SPNEGO, and passkey assertion endpoints. Default: 20.
node_idstringnoStable identifier for this cluster node. Used as the CRDT causality actor and for gossip peer matching. The value is compared against the hostname extracted from peer URLs (e.g. "node1.example.com" matches the peer URL "https://node1.example.com:8080"). Must be the exact hostname component of the peer URL that refers to this node — prefix collisions (e.g. "ipa1" matching "ipa10") are not possible. Defaults to the HOSTNAME environment variable or the system hostname from /proc/sys/kernel/hostname.
issuer_aliaseslist of strings[]Additional issuer URLs this node also answers to. Each entry must be a full issuer URL (e.g. "https://ipa1.example.com/idp"). Tokens are always minted with the canonical issuer; aliases are accepted transparently for WebAuthn passkey origin validation, backchannel-logout aud verification, and client_assertion JWT aud validation (RFC 7521). When [gssapi] initiator_principal is set, the node’s own FQDN and the ipa-ca.<domain> DNS alias are derived automatically — no explicit entry is needed for standard IPA deployments.
jwt_signing_algorithmstringnoJWS algorithm used for JWT signing keys. Default: "ES256". Allowed values: ES256, ES384, ES512, EdDSA, ML-DSA-44, ML-DSA-65, ML-DSA-87. Changing this value on an existing deployment triggers automatic key rotation on the next startup; the old public key remains in the cluster CRDT so that tokens minted before the rotation are still verifiable until they expire.

[db]

KeyTypeDefaultDescription
urlstringsqlx connection URL. Selects the database backend: sqlite:///path/to/file.db, postgres://user:pass@host/dbname, or mariadb://user:pass@host/dbname.
max_connectionsinteger10Maximum number of connections in the pool.
require_tlsboolfalseWhen true, refuse to start if the database URL does not include TLS parameters. Applies to Postgres and MariaDB; SQLite ignores this flag.

[gssapi]

KeyTypeRequiredDescription
servicestringyesGSSAPI service name component of the server principal. Almost always "HTTP".
keytabstringif not gssproxyPath to the Kerberos keytab file for the server principal. The file must be readable by the process user. Omit when gssproxy = true.
gssproxyboolnoWhen true, ahdapa acquires its GSSAPI credential via the gssproxy daemon instead of reading a keytab directly. The process must have a matching entry in /etc/gssproxy/conf.d/. Default: false.
initiator_principalstringnoKerberos principal name in the keytab used to acquire an initiator (client) credential. Enables two distinct credential modes depending on context: (1) Service credential — used for pre-authentication LDAP lookups (e.g. GET /api/auth/federated-hint slow path, the ipauserauthtype gate before any token flow). The HTTP service principal binds to LDAP directly without impersonating the user, because FreeIPA LDAP ACLs prevent users from reading their own ipaidpconfiglink and related IdP attributes. (2) S4U2Self — used for post-authentication operations (OTP token management, passkey writes, per-user attribute lookups). Ahdapa impersonates the already-authenticated user via S4U2Self so that FreeIPA’s standard self-service ACIs apply. Additionally, when gossip.ipa_topology = true, ahdapa uses this credential to obtain Kerberos service tickets for newly-discovered gossip peers and call POST /api/gossip/register-kem to pre-seed this node’s ML-KEM-768 public key before the first gossip round. When absent, both the service-credential pre-auth path and gossip self-registration are disabled; S4U2Self for post-auth operations is also disabled.
ccachestringnoPath to a Kerberos credential cache for the GSSAPI initiator credential (e.g. "FILE:/run/ahdapa/ahdapa.ccache"). Required when gssproxy = true and initiator_principal is set: passed as the ccache entry in the credential store alongside keytab and client_keytab so that gssproxy has a persistent location to store the TGT it acquires. In IPA co-deployments the path should reside in /run/ahdapa/ and must match the ccache entry in the gssproxy drop-in (ahdapa-gssproxy.conf).

Exactly one of keytab or gssproxy = true should be set. If neither is reachable at startup, the server logs a warning and continues with SPNEGO disabled. Password-based authentication via LDAP still works.

[ipa]

KeyTypeDefaultDescription
uristringLDAP server URI. Use ldaps:// for encrypted TCP, ldap:// for plain TCP (combine with starttls = true), or ldapi:///path/to/socket for a Unix domain socket (on-server only). Example: "ldaps://ipa.example.com"
base_dnstringoptionalDomain suffix used as the root for all LDAP searches and distinguished-name construction. Example: "dc=example,dc=com". When absent, ahdapa queries the server’s rootDSE at startup to discover the suffix automatically (defaultNamingContext / namingContexts attribute). Set this only if rootDSE discovery fails or if you need to force a specific naming context on a multi-suffix server. Do not include cn=accounts or cn=users — those sub-containers are appended automatically from the discovered suffix.
gssapibooltrueWhen true, authenticate to LDAP using GSSAPI (Kerberos machine credential from the server’s keytab). When false, use anonymous or simple bind. Setting this to true also enables the kerberos_client_auth token endpoint authentication method and causes it to appear in OAuth2/OIDC discovery documents.
tls_ca_certstringPath to a PEM CA certificate file used to verify the LDAP server’s TLS certificate. When absent, the system trust store is used. Ignored for ldapi:// connections (no TLS).
starttlsboolfalseWhen true, upgrade a plain ldap:// connection to TLS using STARTTLS before sending any bind or search. Has no effect on ldaps:// (already TLS) or ldapi:// (no TLS).
s4u_cache_ttl_secsinteger300Seconds to cache per-user S4U2Self Kerberos credentials. Acquiring a S4U2Self credential requires a KDC round-trip; caching avoids that overhead on every LDAP request. Set to 0 to disable caching and always acquire a fresh credential.
passkey_rp_idstringWebAuthn Relying Party ID for passkey authentication. Must match the RP ID used when passkeys were registered. FreeIPA derives the RP ID from the Kerberos realm lowercased (e.g. IPA.TESTipa.test); when absent ahdapa applies the same derivation from [server] realm, so this key can be omitted for standard IPA deployments. Set explicitly to override, e.g. "localhost" for local development.
cache_ttl_secsinteger60Seconds to cache per-user attribute lookups (name, email, groups, passkey credentials) in memory. Set to 0 to disable caching and always fetch from the backend. The cache is keyed on the bare uid (the part before @), so alice@REALM and alice share the same entry. Entries are evicted when the cache exceeds 200 entries.
use_ldapboolfalseForce direct LDAP for attribute lookups and OTP/passkey management even when a non-ldapi:// URI is configured. When false (default), a non-ldapi:// URI with a GSSAPI initiator uses the FreeIPA JSON-RPC API, which caches session cookies and requires only HTTP → HTTP constrained delegation instead of HTTP → ldap. Set to true only if the IPA API is not reachable or for debugging.
ipa_urlstringderivedIPA JSON-RPC API base URL. When absent, derived from the uri hostname as https://{host}/ipa. Set this only when the IPA API is served on a different host than the LDAP server. Only used when use_ldap = false and the URI is not ldapi://.
ipa_session_ttl_secsinteger1200Seconds to cache per-user IPA session cookies obtained via POST /ipa/session/login_kerberos. A cached session cookie avoids a GSSAPI round-trip on every request. Set to 0 to disable caching.

Required IPA privileges

When [ipa] gssapi = true and [gssapi] initiator_principal is set, the HTTP service principal must hold the following IPA privileges (granted via the Ahdapa Services role):

PrivilegeIPA permissionWhy
Ahdapa Topology ReadSystem: Read Topology Segmentsgossip.ipa_topology = true peer discovery
Ahdapa IdP ReadAhdapa: Read user IdP attributesPre-auth ipauserauthtype / ipaidpconfiglink lookups (service credential, not S4U2Self)
Ahdapa IdP ReadAhdapa: Read IdP configurationsAutomatic ipaIdP discovery (fetch_ipa_idps) including ipaidpclientsecret

See FreeIPA Co-deployment — Multi-node HA for the ipa privilege-add / ipa permission-add commands and the Ansible playbook that provisions all of these idempotently.

ipauserauthtype enforcement

When [ipa] gssapi = true, ahdapa reads the ipauserauthtype LDAP attribute for each user at login time and enforces it as a gate on all non-Kerberos token flows. The per-user value overrides the global default stored in cn=ipaconfig,cn=etc,<suffix>; if neither is set, any method is permitted (backward-compatible, fail-open behaviour).

Recognised values (case-insensitive, may be combined):

ValueMeaning
passwordPassword (static file, PAM, or LDAP simple bind) is allowed.
otpPassword + OTP (TOTP/HOTP) is allowed.
pkinitPKINIT / certificate-based authentication is allowed.
hardenedAuthentication-policy hardening flag (IPA internal; treated as a method selector in this context).
idpThe user must authenticate via an external IdP. All other token flows return federated_login_required (see Error responses below).
passkeyWebAuthn passkey authentication is allowed.

An empty effective set (no per-user value and no global default) means no restriction — all methods are allowed.

ipauserauthtype error responses

When the ipauserauthtype gate rejects a token request, the token endpoint returns 401 Unauthorized with a JSON body:

Method not in allowed set:

{
  "error": "invalid_credentials",
  "error_description": "Authentication method not allowed by user policy."
}

idp in effective set — user must use an external IdP:

{
  "error": "federated_login_required",
  "error_description": "This account must authenticate via an external identity provider.",
  "redirect_to": "/auth/external/ipa-google-workspace"
}

The redirect_to field contains the path to initiate the upstream IdP login flow. The upstream IdP identifier (ipa-google-workspace in the example) is derived from the ipaidpconfiglink attribute on the user’s LDAP entry.

The global default auth types are fetched at startup and refreshed alongside the IPA IdP list every 300 seconds.

[webui]

KeyTypeRequiredDescription
static_dirstringyesPath to the built Preact SPA dist/ directory. The server serves files from this directory under /ui/.
logo_urlstringnoURL for a custom logo image. When set, the WebUI uses this instead of the built-in /ui/logo.svg on login, consent, device verification, profile, and admin pages. May be an absolute URL ("https://cdn.corp.example.com/logo.png") or a path served by a reverse proxy ("/branding/logo.png"). When the URL is an external origin, ahdapa automatically expands the Content Security Policy img-src directive to include that origin. Returned in the GET /api/auth/info response as logo_url.
custom_css_urlstringnoURL for a custom CSS stylesheet injected as a <link rel="stylesheet"> tag into the SPA’s HTML before </head>. Use this to override PatternFly 6 design tokens (--pf-t--global--*) for brand colours, fonts, etc. The CSS file should be served by a reverse proxy or an external CDN. When the URL is an external origin, ahdapa automatically expands the Content Security Policy style-src directive to include that origin.
default_themestringnoDefault colour theme for unauthenticated users who have not yet set a preference in localStorage. Accepted values: "light" (default when absent) or "dark". Invalid values are rejected at startup. The value is injected into the HTML as a data-default-theme attribute on the <html> element and returned in the GET /api/auth/info response as default_theme. The frontend theme fallback chain is: localStorage preference > data-default-theme attribute > OS prefers-color-scheme media query.

Branding example

[webui]
static_dir     = "/usr/share/ahdapa/webui"
logo_url       = "/branding/logo.png"
custom_css_url = "/branding/custom.css"
default_theme  = "dark"

The logo and custom CSS files should be served by the reverse proxy (e.g. an nginx location /branding/ block) or an external CDN. When external origins are used (e.g. "https://cdn.corp.example.com/logo.png"), ahdapa automatically adds the origin to the Content Security Policy so browsers do not block the resources.

[tokens]

All keys are optional. Lifetimes are in seconds.

KeyDefaultDescription
access_token_ttl900 (15 min)Lifetime of JWT access tokens.
refresh_token_ttl86400 (24 h)Lifetime of refresh tokens.
auth_code_ttl60 (1 min)Lifetime of authorization codes. RFC 6749 recommends ≤10 minutes; 60 seconds is conservative.
session_ttl3600 (1 h)Lifetime of the user session cookie issued after SPNEGO or password login.

[federation]

All keys are optional. When this section is absent, the server publishes a valid Entity Statement but rejects all incoming federated assertions.

KeyTypeDefaultDescription
trusted_issuerslist of strings[]Issuer URIs of upstream IdPs whose JWT bearer assertions and ID tokens are accepted statically. Used as a fast-path before trust-chain validation. IPA-sourced IdPs (auto-discovered via cn=idp,<suffix>) are added to this trust list automatically — no manual entry is needed for them.
trust_anchorslist of strings[]Issuer URIs of OIDC Federation 1.0 trust anchors. When an incoming assertion’s iss is not in trusted_issuers, the server attempts trust-chain validation up to these anchors.
max_clock_skew_secsinteger30Maximum clock skew (seconds) tolerated when validating iat and nbf in both incoming federated bearer assertions (urn:ietf:params:oauth:grant-type:jwt-bearer) and client authentication assertions (private_key_jwt, client_secret_jwt).
max_assertion_lifetime_secsinteger3600Maximum assertion lifetime (seconds): the server rejects any incoming JWT assertion whose exp − now exceeds this value. Applies to both jwt-bearer grant assertions and private_key_jwt/client_secret_jwt client auth assertions at all endpoints (token, device, PAR). Set to 0 to disable the upper-bound check.

[[federation.upstream_idps]]

Repeat this section once per upstream IdP for §6 authentication delegation. When configured, the login page redirects federated users to the upstream instead of showing a local password field.

IPA auto-discovery: When [ipa] gssapi = true, ahdapa automatically reads all ipaIdP LDAP objects from cn=idp,<suffix> at startup and after every 300-second refresh cycle, and converts them into upstream IdP registrations without requiring any [[federation.upstream_idps]] entries. Each IPA-sourced IdP gets id = "ipa-{slug}" (the CN lowercased with spaces replaced by -) and callback_path = "/internal/callback/ipa-{slug}". Entries in the static TOML take precedence over IPA-sourced entries that share the same id. Manual [[federation.upstream_idps]] sections are still needed for IdPs that are not registered in FreeIPA (e.g. GitHub, Google, or custom providers without an ipaIdP object).

Client authentication

Exactly one authentication method must be configured:

token_endpoint_auth_methodRequired fieldDescription
private_key_jwt (default)private_key_pathRFC 7523 §2.2 — signed JWT assertion. Supported by Keycloak, Okta, Auth0, Entra ID, Apple, and most enterprise providers.
client_secret_postclient_secretRFC 6749 §2.3.1 — client_id + client_secret in the POST body. Supported by Google, GitLab, LinkedIn, Twitch, and most social providers.
client_secret_basicclient_secretRFC 6749 §2.3.1 — credentials in Authorization: Basic header.
none(none)Public client — only client_id is sent; PKCE is used for security. IPA-sourced IdPs without an ipaIdpClientSecret use this method automatically.

Core fields

KeyTypeRequiredDescription
idstringyesShort identifier used in route paths (/auth/external/{id}, /internal/callback/{id}). URL-safe characters only.
issuerstringyesUpstream IdP issuer URI. Must match the iss claim in tokens from that IdP. For plain OAuth2 providers (GitHub etc.) this is the provider’s base URL.
client_idstringyesThis server’s client_id at the upstream IdP.
token_endpoint_auth_methodstring"private_key_jwt"How to authenticate at the upstream token endpoint. See table above.
private_key_pathstringif private_key_jwtPath to PKCS#8 PEM private key (EC, RSA, or Ed25519).
client_secretstringif client_secret_*Shared secret issued by the upstream IdP.
scopeslist of strings["openid"]Scopes to request in the upstream authorization redirect.
callback_pathstringyesAbsolute path for the callback route. Must match a redirect URI registered at the upstream IdP. Example: "/internal/callback/corp-sso"
default_groupslist of strings[]Groups automatically granted to any local user linked to this upstream IdP. Applied during group resolution when the user is not present in the static users file and LDAP returns no memberships. For users resolved via FreeIPA LDAP, real memberOf group memberships are returned instead of this static list. IPA-sourced IdP entries always have an empty default_groups.
default_acrstringACR (Authentication Context Class Reference) value stamped into ahdapa tokens when the upstream IdP does not include one in its response. If absent and the upstream also omits acr, the token carries urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified. For IPA-sourced IdPs this can be set via the admin panel (PUT /api/admin/federation/ipa-idps/{id}) without modifying the TOML.
default_amrlist of strings[]AMR (Authentication Method References, RFC 8176) values stamped into ahdapa tokens when the upstream IdP does not include any. Typical values: pwd (password), otp (OTP), mfa (MFA), hwk (hardware key), swk (software key), fed (external IdP hop). For IPA-sourced IdPs this can be set via the admin panel without modifying the TOML.
display_namestringHuman-readable display name for the IdP shown in the admin panel. For TOML-configured IdPs this is optional; for IPA-sourced IdPs it is set automatically from the LDAP CN of the ipaIdP object.

Static endpoint overrides (non-OIDC providers)

By default the server fetches {issuer}/.well-known/openid-configuration to discover endpoints. For providers that don’t publish OIDC Discovery (GitHub, Discord, Slack, etc.) set these overrides instead:

KeyTypeDescription
authorization_endpointstringFull URL of the authorization endpoint. When set, OIDC Discovery is skipped.
token_endpointstringFull URL of the token endpoint. Required when authorization_endpoint is set.
userinfo_endpointstringFull URL of the userinfo API. Required for non-OIDC providers (no ID token).
jwks_uristringJWKS endpoint for ID token signature verification. Omit for plain OAuth2 providers.
subject_claimstringField name in the userinfo response that carries the subject. Default "sub". Use "id" for GitHub (returns a numeric ID), "login" for username-based providers.

§5 claims backing

KeyTypeDefaultDescription
claims_providerboolfalseWhen true, this upstream also supplies §5 authorization-backing claims for any locally-linked user.
claims_scopeslist of strings[]Scopes to request when fetching claims via Token Exchange.
claims_pattern"aggregated" or "distributed""aggregated"How claims are returned in the UserInfo response. aggregated embeds a signed JWT; distributed embeds a claims endpoint reference.

Account linkage

After setting up an [[federation.upstream_idps]], link accounts via the admin API:

POST /api/admin/federated-accounts
Content-Type: application/json

{
  "upstream_iss": "https://partner.example.com",
  "upstream_sub": "bob@PARTNER.LOCAL",
  "local_sub":    "alice@CORP.LOCAL"
}

Accounts can also be resolved automatically from FreeIPA’s ipaIdpUser LDAP schema when LDAP is configured.

Examples

Another ahdapa / Keycloak / Okta / Auth0 (OIDC + private_key_jwt):

[[federation.upstream_idps]]
id               = "corp-sso"
issuer           = "https://partner.example.com"
client_id        = "abc123"
private_key_path = "/etc/ahdapa/upstream-corp-sso.pem"
scopes           = ["openid", "profile", "email"]
callback_path    = "/internal/callback/corp-sso"

Google / GitLab / LinkedIn (OIDC Discovery + client_secret_post):

[[federation.upstream_idps]]
id                         = "google"
issuer                     = "https://accounts.google.com"
client_id                  = "12345.apps.googleusercontent.com"
client_secret              = "GOCSPX-..."
token_endpoint_auth_method = "client_secret_post"
scopes                     = ["openid", "profile", "email"]
callback_path              = "/internal/callback/google"

GitHub (plain OAuth2 — no OIDC Discovery, no ID token):

[[federation.upstream_idps]]
id                         = "github"
issuer                     = "https://github.com"
client_id                  = "Iv1.abc123"
client_secret              = "ghp_..."
token_endpoint_auth_method = "client_secret_post"
authorization_endpoint     = "https://github.com/login/oauth/authorize"
token_endpoint             = "https://github.com/login/oauth/access_token"
userinfo_endpoint          = "https://api.github.com/user"
subject_claim              = "id"
scopes                     = ["read:user", "user:email"]
callback_path              = "/internal/callback/github"

Apple Sign In (private_key_jwt required by Apple):

[[federation.upstream_idps]]
id               = "apple"
issuer           = "https://appleid.apple.com"
client_id        = "com.example.your-service-id"
private_key_path = "/etc/ahdapa/apple-authkey.pem"
scopes           = ["openid", "name", "email"]
callback_path    = "/internal/callback/apple"

[users]

Optional. Configures a static users file for local authentication without a live FreeIPA/LDAP server. When set, username and password authentication checks this file before PAM and LDAP. Users not found here fall through to PAM (if [pam] is configured and the binary was built with --features pam) and then to LDAP. Profile attribute and group lookup follows a three-step chain: static users → varlink (if [varlink] is configured) → LDAP.

KeyTypeDefaultDescription
filestringPath to a TOML file listing static users. When absent, password authentication skips the static-user check and proceeds to PAM (if configured) and then LDAP.

Static users file format

The file is TOML with one [[user]] section per user. Passwords are stored in plain text — use this only for local development and CI.

FieldTypeRequiredDescription
usernamestringyesLogin name. The @REALM suffix is stripped automatically when matching against Kerberos UPNs.
passwordstringyesPlain-text password.
namestringnoFull display name (name OIDC claim).
given_namestringnoGiven name (given_name OIDC claim).
family_namestringnoFamily name (family_name OIDC claim).
emailstringnoEmail address (email OIDC claim).
groupslist of stringsnoGroup memberships. Used by RBAC group resolution and identity API group queries. Defaults to [].
uid_numberintegernoPOSIX UID number. Returned by the identity API (/api/identity/users) as uid_number.
gid_numberintegernoPOSIX primary GID number. Returned by the identity API as gid_number.
home_directorystringnoPOSIX home directory path. Returned by the identity API as home_directory.
login_shellstringnoPOSIX login shell. Returned by the identity API as login_shell.
gecosstringnoPOSIX GECOS field (informational). Returned by the identity API as gecos.

Example

[users]
file = "/etc/ahdapa/users.toml"
# /etc/ahdapa/users.toml
[[user]]
username       = "alice"
password       = "changeme"
name           = "Alice Admin"
given_name     = "Alice"
family_name    = "Admin"
email          = "alice@example.com"
groups         = ["admins"]
uid_number     = 10001
gid_number     = 10001
home_directory = "/home/alice"
login_shell    = "/bin/bash"

[[user]]
username = "bob"
password = "changeme"
email    = "bob@example.com"
groups   = ["viewers"]

[clients]

Optional. Seeds a fixed set of OAuth2 clients from a TOML file at startup. Clients defined here are upserted into the CRDT with the current timestamp (so the file always wins after a restart), persisted to the database, and gossiped to cluster peers. The admin API rejects PUT and DELETE requests for static clients with 403 Forbidden.

If the file is configured but cannot be read or parsed, startup fails with a fatal error (unlike [users], which warns and continues). This is intentional: static clients are typically critical infrastructure (e.g. an SSSD machine-auth template).

KeyTypeDefaultDescription
filestringPath to a TOML file listing static OAuth2 clients. When absent, no clients are pre-seeded.

Static clients file format

The file is TOML with one [[client]] section per client.

FieldTypeRequiredDescription
client_idstringyesStable client identifier. Must be unique across all clients.
client_namestringyesHuman-readable display name.
token_endpoint_auth_methodstringyesAuthentication method at the token endpoint (e.g. none, client_secret_basic, private_key_jwt, tls_client_auth, kerberos_client_auth).
scopeslist of stringsnoAllowed scopes. Defaults to [].
grant_typeslist of stringsnoAllowed grant types. Defaults to null (any).
redirect_urislist of stringsnoAllowed redirect URIs (required for authorization_code flow).
subject_typestringnopublic or pairwise.
client_secretstringnoPre-shared secret (for client_secret_* methods).
jwks_uristringnoURL of the client’s JWKS (for private_key_jwt).
tls_client_auth_subject_dnstringnoExpected Subject DN for mTLS client auth.
tls_client_certificate_thumbprintstringnoSHA-256 thumbprint (hex) of the client certificate.
kerberos_principalstringnoExact Kerberos service principal (single-machine mode).
kerberos_principal_patternstringnoGlob pattern matching multiple Kerberos principals (template mode). * matches any characters except @.
kerberos_hbac_servicestringnoIPA HBAC service name used to gate access in template mode.

Exactly one of kerberos_principal / kerberos_principal_pattern must be set when token_endpoint_auth_method = "kerberos_client_auth".

Example

[clients]
file = "/etc/ahdapa/clients.toml"
# /etc/ahdapa/clients.toml

[[client]]
client_id   = "sssd-template"
client_name = "SSSD Machine Template"
token_endpoint_auth_method = "kerberos_client_auth"
scopes      = ["openid", "directory.read"]
grant_types = ["client_credentials"]
kerberos_principal_pattern = "host/*@EXAMPLE.COM"
kerberos_hbac_service      = "sssd-idp"

[[client]]
client_id   = "ci-pipeline"
client_name = "CI Pipeline"
token_endpoint_auth_method = "private_key_jwt"
scopes      = ["openid", "profile"]
grant_types = ["client_credentials"]
jwks_uri    = "https://ci.example.com/.well-known/jwks.json"

Optional. When present, ahdapa queries the local system user database via the io.systemd.UserDatabase varlink interface for profile attributes and group memberships before falling back to FreeIPA LDAP. This is useful on systemd-based hosts where users are managed by systemd-homed, sssd, or another NSS/userdb provider registered with the systemd multiplexer.

Requires the varlink Cargo feature to be enabled at compile time (--features varlink). If the binary was built without this feature, the [varlink] section is accepted in the config file but silently ignored.

Password authentication is not supported through varlink: the privileged section of a userdb record (which contains hashed passwords) is not accessible to non-root processes. Password authentication uses the static users file, then PAM (if [pam] is configured), and finally LDAP.

Requires systemd 246 or later with /run/systemd/userdb/io.systemd.Multiplexer present.

KeyTypeDefaultDescription
servicestring"io.systemd.Multiplexer"Varlink service name to query. The default multiplexer queries all registered userdb services (NSS, systemd-homed, sssd, etc.). Set to a specific service name to bypass the multiplexer.
timeout_secsinteger5Seconds to wait for a varlink response. If the socket does not respond in time, ahdapa falls through to LDAP as if varlink were not configured.

Example

[varlink]
service      = "io.systemd.Multiplexer"
timeout_secs = 5

[pam]

Optional. When present, ahdapa delegates password verification to the system PAM stack before falling back to FreeIPA LDAP. This covers any PAM-integrated backend: SSSD (FreeIPA, Active Directory via SSSD), winbindd, systemd-homed, or any other PAM module. When PAM returns PAM_NEW_AUTHTOK_REQD (expired password), the user is redirected to /login/change-password to complete a password change before their session is established.

Requires the pam-devel package at build time. Enable at compile time with --features pam. If the binary was built without this feature, the [pam] section is accepted in the config file but silently ignored.

The password authentication lookup chain is: static users file → PAM (if [pam] configured) → LDAP simple bind.

Install contrib/systemd/ahdapa-pam.conf as /etc/pam.d/ahdapa to provide the PAM service definition. The default includes system-auth; replace it with sssd or winbind as appropriate for your environment.

KeyTypeDefaultDescription
servicestring"ahdapa"Name of the PAM service file in /etc/pam.d/. Must match the filename of the installed PAM configuration.
timeout_secsinteger30Seconds to wait for PAM to respond. If PAM does not return within this time, ahdapa falls through to LDAP as if [pam] were not configured.

Example

[pam]
service      = "ahdapa"
timeout_secs = 30

[rbac] / [[rbac.role]] / [[rbac.group_role]]

Role-based access control for the admin API. When this section is absent, all admin API requests return 403 — access is denied to everyone. Add it to grant admin access to specific groups.

Groups are resolved in order: (1) the static users file, (2) the varlink system userdb if [varlink] is configured and the binary was built with the varlink feature, (3) FreeIPA LDAP memberOf if LDAP is configured. The first source that returns a non-empty result is used.

[[rbac.role]]

Repeat once per role.

KeyTypeDescription
namestringRole name referenced by [[rbac.group_role]].
permissionslist of stringsPermissions granted by this role. See table below.

[[rbac.group_role]]

Maps a group name to a role. Repeat once per (group, role) pair.

KeyTypeDescription
groupstringGroup name as it appears in the users file or LDAP memberOf.
rolestringRole name from [[rbac.role]].

Permission strings

PermissionAdmin API endpoints covered
clients:readGET /clients, GET /clients/{id}, GET /refresh-families
clients:writePOST /clients, PUT /clients/{id}, DELETE /clients/{id}, DELETE /refresh-families/{id}
keys:readGET /keys, GET /keys/cluster
keys:rotatePOST /keys/rotate, DELETE /keys/{kid}, PUT /keys/cluster
federation:readGET /federated-accounts, GET /federation/ipa-idps, GET /federation/ipa-idps/{id}
federation:writePOST /federated-accounts, DELETE /federated-accounts/{id}, PUT /federation/ipa-idps/{id}, DELETE /federation/ipa-idps/{id}
users:readGET /users, GET /users/{u}, GET /groups, GET /groups/{g}
nodes:readGET /nodes
audit:readGET /audit
scopes:readGET /scopes
scopes:writePUT /scopes/{name}, DELETE /scopes/{name}
hbac:readGET /hbac, GET /hbac/{id}, GET /hbac/lookup/*, GET /clients/{id}/hbac
hbac:writePOST /hbac, PUT /hbac/{id}, DELETE /hbac/{id}
spiffe:readGET /spiffe/entries, GET /spiffe/entries/{id}, GET /spiffe/status, GET /spiffe/lookup/users, GET /spiffe/lookup/groups, GET /spiffe/lookup/hostgroups
spiffe:writePOST /spiffe/entries, PUT /spiffe/entries/{id}, DELETE /spiffe/entries/{id}
*All of the above.

Example

[[rbac.role]]
name        = "admin"
permissions = ["*"]

[[rbac.role]]
name        = "viewer"
permissions = [
  "clients:read", "keys:read", "federation:read",
  "users:read", "nodes:read", "audit:read", "scopes:read",
]

[[rbac.group_role]]
group = "admins"
role  = "admin"

[[rbac.group_role]]
group = "helpdesk"
role  = "viewer"

[gossip]

KeyDefaultDescription
peers[]List of peer node base URLs. Gossip is disabled when this list is empty and ipa_topology is false. Example: ["https://node2.example.com:8080", "https://node3.example.com:8080"]. These URLs also define the set of trusted issuers for cross-node token validation: the iss claim in an incoming JWT is accepted if it matches the local [server] issuer, any URL in this list, or any URL discovered via ipa_topology. This allows /userinfo and /introspect to accept tokens issued by any peer node without requiring session forwarding.
interval_secs5How often (in seconds) the gossip loop runs. Each round contacts each peer and sends a delta of changed CRDT entries (or a full-state push on first contact or after an error).
allowed_node_ids[]Allowlist of node_id values permitted to self-register via gossip. The allowlist is always enforced — when both this list and the topology-derived allowlist are empty, no node can self-register (fail-closed). Any NodeEntry received for an unlisted node_id is silently dropped before merge. When ipa_topology = true, discovered replica hostnames are added to the allowlist automatically in addition to any entries listed here. Use network ACLs for additional defense-in-depth.
tombstone_ttl_secs604800Seconds to retain tombstoned OR-Map entries (deleted clients, decommissioned nodes, and revoked signing keys) before permanently removing them. Also the maximum age of accepted gossip envelopes — envelopes with issued_at older than this value are rejected. Must exceed the longest expected node downtime: a node offline longer than this TTL may re-gossip entries that were since deleted. Default: 7 days.
ipa_topologyfalseWhen true, ahdapa reads the FreeIPA replication topology from LDAP (cn=topology,cn=ipa,cn=etc,<suffix>, objectClass ipaReplTopoSegment) and gossips with all directly connected IPA replica peers automatically. Peer URLs are constructed as https://<hostname><issuer-path> (e.g. https://ipa2.example.com/idp). Discovered replica hostnames are also added to the gossip admission allowlist. Gossip is not disabled when peers = [] and this key is true. Requires the HTTP service principal to have the “System: Read Topology Segments” IPA permission — see the IPA setup commands in contrib/demo/ipa/ahdapa.toml. When gssapi.initiator_principal is also set, after each topology refresh ahdapa additionally calls POST /api/gossip/register-kem on newly-discovered peers to pre-seed this node’s ML-KEM-768 public key and gossip signing key via Kerberos authentication. Dynamically discovered peer URLs are also trusted for cross-node iss validation (see peers above).
ipa_topology_interval_secs300How often (in seconds) to re-query the IPA topology for new or removed peers. Only used when ipa_topology = true. Minimum effective value: 30 seconds. Default: 5 minutes.
kerberos_realmExpected Kerberos realm for POST /api/gossip/register-kem callers. When set, cross-realm principals are rejected with 403 Forbidden, preventing cross-realm trust escalation. Example: "IPA.EXAMPLE.COM". When unset, the realm component of the authenticated principal is not checked.

[cluster]

Optional. Controls multi-node coordination behaviour. When this section is absent or distributed_mode = "off", ahdapa operates in single-node mode: auth codes can only be exchanged on the issuing node, and session revocation is node-local only.

All keys are optional. The section itself is optional — omitting it is equivalent to distributed_mode = "off".

KeyTypeDefaultDescription
distributed_modestring"off"Coordination level. One of off, eventual, forwarding, or strict. Modes are ordered: each higher mode is a superset of the features below it. See Distributed modes for full semantics.
quorum_kinteger0Minimum number of peer approvals required before issuing a token in strict mode. 0 means majority: floor(live_peer_count / 2) + 1. Only used when distributed_mode = "strict".
quorum_timeout_msinteger500Milliseconds to wait for quorum votes before failing in strict mode.
quorum_fallbackboolfalseWhen true and quorum cannot be reached in strict mode, log a warning and issue the token anyway. When false (default), the token request is rejected.

Mode summary

distributed_modeAuth code exchangeSession revocationQuorum
offIssuing node onlyNode-localNo
eventualAny node (~gossip-interval window)CRDT (cross-node)No
forwardingForwarded to origin (zero window)CRDT (cross-node)No
strictForwarded to origin (zero window)CRDT (cross-node)k-of-n peers

Example

# Recommended HA default — cross-node session revocation, any-node code exchange.
[cluster]
distributed_mode = "eventual"
# Forwarding mode — zero replay window for auth code exchanges.
[cluster]
distributed_mode = "forwarding"
# Strict mode — quorum pre-approval before every token issuance.
[cluster]
distributed_mode  = "strict"
quorum_k          = 0       # majority: floor(peers/2)+1
quorum_timeout_ms = 500
quorum_fallback   = false   # hard-reject on quorum failure

[tls]

When this section is absent or enabled = false, ahdapa listens on plain HTTP. Use this section when ahdapa terminates TLS itself; omit it when a reverse proxy handles TLS.

KeyTypeDefaultDescription
enabledboolfalseEnable TLS on the listening socket. When true, cert_file and key_file must also be set.
cert_filestringPath to a PEM file containing the server certificate chain (leaf certificate first, followed by any intermediates).
key_filestringPath to a PEM file containing the server private key (PKCS#8 or SEC1, unencrypted). Also accepts a PKCS#11 URI (pkcs11:token=…;object=…;type=private) for hardware-backed keys.
protocolslist of strings["TLSv1.2", "TLSv1.3"]TLS protocol versions to accept.
ca_certstringPath to a PEM file containing a CA certificate to add to the trust store for outbound TLS connections (used when verifying gossip peer certificates). When absent, the system trust store is used. Use this when gossip peers present certificates signed by a private CA.
client_cert_headerstringHTTP header name that a trusted reverse proxy uses to forward the client TLS certificate (PEM-encoded, optionally URL-encoded with %xx escaping as produced by nginx $ssl_client_escaped_cert). When set, ahdapa reads the client certificate from this header instead of from the TLS handshake, enabling mTLS client authentication (RFC 8705) behind a TLS-terminating proxy. When absent, mTLS via proxy headers is disabled (only native TLS handshake certificates are accepted).
trusted_proxy_cidrslist of strings["127.0.0.0/8", "::1/128"]CIDR ranges whose source IP is allowed to supply client_cert_header. Requests from IPs outside these ranges that carry the header are treated as if the header were absent. Restricts certificate spoofing to the defined proxy tier.

mTLS client authentication (RFC 8705)

When a client registers with token_endpoint_auth_method set to tls_client_auth or self_signed_tls_client_auth, the token endpoint authenticates the client by comparing the SHA-256 thumbprint of the presented TLS certificate against the thumbprint stored for that client. Two certificate delivery paths are supported:

Native TLS (ahdapa terminates TLS): the client certificate is extracted from the TLS handshake by the accept loop and injected into the request as an extension. No additional configuration is needed beyond enabling [tls].

Reverse proxy (a proxy terminates TLS and forwards the cert): set client_cert_header to the name of the header the proxy uses (e.g. "X-Client-Cert" for HAProxy, or the nginx $ssl_client_escaped_cert variable forwarded as "X-SSL-Client-Cert"). Also set trusted_proxy_cidrs to the CIDR(s) of your proxy tier. Requests from outside those CIDRs will not have the header honoured.

To register an mTLS client via the admin API, supply the client certificate as PEM in the tls_client_certificate field of the create/update request. The server computes the SHA-256 thumbprint and stores only that; the full certificate is not persisted.

Certificate-bound access tokens (RFC 8705 §3): every access token issued to an mTLS client contains a cnf claim with x5t#S256 set to the base64url-encoded SHA-256 thumbprint of the client certificate presented at the token endpoint. Resource servers that receive such a token must verify that the cnf.x5t#S256 value matches the certificate the client used to establish the mTLS connection for the resource request, preventing token theft by a party without the certificate’s private key. The discovery documents (/.well-known/oauth-authorization-server and /.well-known/openid-configuration) advertise "tls_client_certificate_bound_access_tokens": true (RFC 8705 §7.3) so that clients know to expect this binding.

When a client authenticates with both mTLS and DPoP simultaneously, the access token cnf object contains both x5t#S256 (mTLS binding) and jkt (DPoP key thumbprint binding).

POST /api/admin/clients
Content-Type: application/json

{
  "client_name": "my-service",
  "redirect_uris": ["https://my-service.example.com/callback"],
  "scopes": ["openid", "profile"],
  "token_endpoint_auth_method": "tls_client_auth",
  "tls_client_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
}

Example

[tls]
enabled   = true
cert_file = "/etc/pki/tls/certs/idp.example.com.pem"
key_file  = "/etc/pki/tls/private/idp.example.com.key"

For gossip between nodes that use a private CA:

[tls]
enabled   = true
cert_file = "/etc/pki/tls/certs/node.pem"
key_file  = "/etc/pki/tls/private/node.key"
ca_cert   = "/etc/pki/tls/certs/cluster-ca.pem"

[spiffe]

Optional. When trust_domain is set, ahdapa acts as a SPIFFE trust domain authority and Workload API server. All other keys have safe defaults. When this section is absent or trust_domain is unset, no SPIFFE functionality is activated.

See SPIFFE Integration for a setup guide.

KeyTypeDefaultDescription
trust_domainstringSPIFFE trust domain (e.g. "example.org"). Must be a bare domain name — not a spiffe:// URI and not a path. Setting this key activates SPIFFE features. When absent, all other [spiffe] keys are ignored.
ca_ttl_daysinteger365Lifetime in days of the self-signed SPIFFE CA certificate.
svid_ttl_secondsinteger3600Lifetime in seconds of issued X.509-SVIDs and JWT-SVIDs.
workload_socketstring"/run/spiffe/workload.sock"Filesystem path for the SPIFFE Workload API Unix domain socket (gRPC).
workload_socket_modeinteger432 (= 0o660)Unix permission bits for the workload socket, expressed as a decimal integer. Default 432 (= octal 0o660, owner + group read-write). Use 438 (octal 0o666) to allow any local process — for development only.
bundle_refresh_hintinteger300Suggested polling interval in seconds for trust bundle consumers. Returned as spiffe_refresh_hint in the JWK Set at /.well-known/spiffe-bundle.
ca_algorithmstring"EC-P256"Key algorithm for a generated CA. Accepted values: "EC-P256" or "EC-P384".
ca_subject_cnstringtrust_domain valueCN for the generated CA certificate Subject DN. Defaults to trust_domain when absent.
ca_subject_ostringO (Organization) for the generated CA certificate Subject DN.
ca_cert_filestringPath to an external CA certificate PEM file. Required when ca_key_file is set to a PEM private key path.
ca_key_filestringPath to an external CA private key PEM file, or a PKCS#11 URI (pkcs11:token=…;object=…;type=private) for HSM-backed keys. When set, ahdapa loads this key instead of generating one.
accepted_proxiesarray of strings["host", "HTTP"]Kerberos service name components that are permitted to call POST /spiffe/issue-svid. Only OAuth2 clients whose token_endpoint_auth_method is kerberos_client_auth and whose stored Kerberos principal’s service component (the part before the first /) appears in this list are accepted. The default ["host", "HTTP"] covers standard IPA host principals (host/myhost@REALM) and HTTP service principals (HTTP/myhost@REALM). Set to [] to disable proxy SVID issuance entirely.
oidc_issuerstringOIDC issuer URL for SPIFFE OIDC Federation. When set, enables GET /spiffe/oidc/openid-configuration and GET /spiffe/oidc/jwks. Must be an HTTPS URL (e.g. "https://spiffe.example.org"). See OIDC Federation.

CA key loading priority

  1. CRDT contains an encrypted CA key blob (from a previous startup or gossip) → decrypt with the cluster wrapping key and use.
  2. ca_key_file starts with pkcs11: → load from HSM; ca_cert_file is required. The HSM key is not stored in the CRDT.
  3. Both ca_key_file and ca_cert_file are set → load PEM files; encrypt the private key and gossip via CRDT.
  4. Nothing set → generate a new ECDSA CA (algorithm from ca_algorithm); encrypt and gossip the key.

Example

[spiffe]
trust_domain         = "example.org"
workload_socket      = "/run/spiffe/workload.sock"
workload_socket_mode = 432   # 0o660 — owner + group read-write
svid_ttl_seconds     = 3600
ca_ttl_days          = 365
ca_algorithm         = "EC-P256"
ca_subject_cn        = "example.org SPIFFE CA"
ca_subject_o         = "Example Corp"
bundle_refresh_hint  = 300
# accepted_proxies is optional; the default shown below is applied when absent
accepted_proxies     = ["host", "HTTP"]

HSM-backed CA:

[spiffe]
trust_domain    = "example.org"
ca_cert_file    = "/etc/ahdapa/spiffe-ca.pem"
ca_key_file     = "pkcs11:token=spiffe-ca;object=ca-key;type=private"

Environment variables

VariableDescription
AHDAPA_CONFIGPath to the TOML configuration file. Defaults to /etc/ahdapa/ahdapa.toml. Overridden by a positional argument: ahdapa /path/to/config.toml.
AHDAPA_LISTENOverrides server.listen from the config file. Accepts the same formats: host:port, unix:/path, or /path. Ignored when systemd socket activation is in use (LISTEN_FDS is set).
LISTEN_FDS / LISTEN_PIDSet by systemd for socket activation. When present, ahdapa uses the pre-bound socket passed by systemd instead of binding its own. Do not set these manually.
HOSTNAMEUsed as the node identifier within the cluster. Automatically set by the OS or container runtime; override if needed for a stable identity across restarts.
RUST_LOGLog level filter (e.g. ahdapa=debug,info). See tracing-subscriber documentation.

Minimal example

[server]
issuer = "https://idp.example.com"
realm  = "EXAMPLE.COM"

[db]
url = "sqlite:///var/lib/ahdapa/ahdapa.db"

[gssapi]
service = "HTTP"
keytab  = "/etc/ahdapa/ahdapa.keytab"

[ipa]
uri = "ldaps://ipa.example.com"

[webui]
static_dir = "/usr/share/ahdapa/webui"

Three-node cluster example

[server]
issuer = "https://idp.example.com"
realm  = "EXAMPLE.COM"

[db]
url             = "sqlite:///var/lib/ahdapa/ahdapa.db"
max_connections = 20

[gssapi]
service = "HTTP"
keytab  = "/etc/ahdapa/ahdapa.keytab"

[ipa]
uri    = "ldaps://ipa.example.com"
gssapi = true

[webui]
static_dir = "/usr/share/ahdapa/webui"

[tokens]
access_token_ttl  = 600
refresh_token_ttl = 43200

[gossip]
peers        = ["https://node2.example.com:8080", "https://node3.example.com:8080"]
interval_secs = 5

[cluster]
distributed_mode = "eventual"

Each of the three nodes lists the other two as peers. A full mesh is not required: each node only needs to reach any one other node for state to eventually propagate to the entire cluster.

Access control for gossip and internal endpoints: The /api/gossip/* and /api/internal/* endpoints are served on the same port as the public OAuth2 endpoints and are protected at the application layer. /api/gossip/sync, /api/gossip/wrapping-key, and the two /api/internal/ endpoints are authenticated by CMS (ECDSA P-256 signature + ML-KEM-768 encryption) and the allowed_node_ids allowlist. /api/gossip/register-kem is authenticated by Kerberos SPNEGO. Port-level firewall rules cannot selectively block these paths without also blocking public OAuth2 clients. A reverse proxy can add an optional path-level restriction — see Reverse Proxy Setup. The internal endpoints return 404 Not Found when distributed_mode = "off" or "eventual".