Skip to content

KeystoneIdentityBackend CRD API Reference

Reference documentation for the KeystoneIdentityBackend Custom Resource Definition. One CR attaches to a Keystone CR via spec.keystoneRef and describes one external identity backend:

  • type: LDAP — an LDAP/AD-backed domain: connection and bind credentials, the user/group tree layout and attribute mapping, read-only mode, TLS, and an extraOptions escape hatch.
  • type: OIDC — an OpenID Connect federation backend: the issuer and discovery shape, the relying-party client, typed keystone mapping rules, and declarative target groups with role assignments. The controller provisions the keystone federation API objects (identity provider, mapping, protocol) and the keystone-side projection renders the mod_auth_openidc reverse-proxy sidecar configuration.
  • type: SAML — a SAML 2.0 federation backend: the IdP entityID and metadata (inline, by SecretRef, or by URL), an operator-generated or operator-consumed service-provider keypair, and the same typed mapping rules and declarative groups as OIDC. It reuses the same federation objects and sidecar; the keystone-side projection renders mod_auth_mellon configuration alongside mod_auth_openidc. At most one SAML backend per Keystone (webhook-enforced).

For the task-oriented walkthroughs, see the LDAP Domain Backend guide, the OIDC Federation guide, and the SAML Federation guide. For the controller topology (dedicated backend controller + keystone-side projection), see Keystone Reconciler Architecture.

API Group and Version

FieldValue
Groupkeystone.openstack.c5c3.io
Versionv1alpha1
KindKeystoneIdentityBackend
List KindKeystoneIdentityBackendList
ScopeNamespaced

Scheme registration: the init() function in keystoneidentitybackend_types.go registers both Kinds with the shared SchemeBuilder, so AddToScheme covers Keystone and KeystoneIdentityBackend alike.

Example

yaml
apiVersion: keystone.openstack.c5c3.io/v1alpha1
kind: KeystoneIdentityBackend
metadata:
  name: corp-ldap
  namespace: openstack
spec:
  keystoneRef:
    name: keystone
  domain:
    name: corp
    mode: Manage
    deletionPolicy: Retain
    description: Corporate directory
  type: LDAP
  ldap:
    url: ldaps://ldap.corp.example.com:636
    bindCredentialsSecretRef:
      name: corp-ldap-bind
    suffix: dc=corp,dc=example,dc=com
    readOnly: true
    users:
      treeDN: ou=people,dc=corp,dc=example,dc=com
      objectClass: inetOrgPerson
      idAttribute: uid
      nameAttribute: uid
      mailAttribute: mail
    groups:
      treeDN: ou=groups,dc=corp,dc=example,dc=com
    tls:
      caBundleSecretRef:
        name: corp-ldap-ca
status:
  conditions:
  - type: DomainReady
    status: "True"
    reason: DomainProvisioned
  - type: ConfigProjected
    status: "True"
    reason: ConfigProjected
  - type: Ready
    status: "True"
    reason: AllReady
  domainID: 8c2f1d3e5a794b0f9a1c

An OIDC federation backend:

yaml
apiVersion: keystone.openstack.c5c3.io/v1alpha1
kind: KeystoneIdentityBackend
metadata:
  name: keycloak-cobaltcore
  namespace: openstack
spec:
  keystoneRef:
    name: keystone
  domain:
    name: cobaltcore
    mode: Manage
    deletionPolicy: Delete
  type: OIDC
  oidc:
    issuer: https://keycloak.example.com/realms/cobaltcore
    clientID: keystone
    clientSecretRef:
      name: keycloak-cobaltcore-client
    oauth2Introspection:
      enabled: true
  mappings:
  - remote:
    - type: HTTP_OIDC_ISS
      anyOneOf:
      - https://keycloak.example.com/realms/cobaltcore
    - type: HTTP_OIDC_PREFERRED_USERNAME
    local:
    - user:
        name: "{1}"
      group:
        name: federated-users
        domain:
          name: cobaltcore
  groups:
  - name: federated-users
    roleAssignments:
    - role: member
      domain: true
status:
  conditions:
  - type: DomainReady
    status: "True"
    reason: DomainProvisioned
  - type: FederationObjectsReady
    status: "True"
    reason: FederationObjectsProvisioned
  - type: MappingsReady
    status: "True"
    reason: MappingsApplied
  - type: ConfigProjected
    status: "True"
    reason: ConfigProjected
  - type: Ready
    status: "True"
    reason: AllReady
  domainID: 8c2f1d3e5a794b0f9a1c

Printer columns: kubectl get keystoneidentitybackends shows Ready (.status.conditions[?(@.type=='Ready')].status), Domain (.spec.domain.name), Keystone (.spec.keystoneRef.name), and Age.

Spec

KeystoneIdentityBackendSpec

FieldTypeRequiredDefaultDescription
keystoneRefKeystoneRefSpecYesNames the Keystone CR in the same namespace this backend attaches to. Immutable (CEL transition rule): re-pointing a backend at a different Keystone would strand the provisioned domain; delete and recreate instead. The referenced CR does not have to exist at admission time (GitOps ordering) — a dangling reference surfaces as DomainReady=False/KeystoneNotFound.
domainDomainSpecYesThe Keystone domain this backend provides.
typeIdentityBackendTypeYesBackend driver: LDAP, OIDC, or SAML. Immutable (CEL transition rule).
ldapLDAPBackendSpecWhen type: LDAPLDAP/AD connection, tree layout, and attribute mapping. The union rule ((self.type == 'LDAP') == has(self.ldap)) enforces exactly one backend block matching spec.type at the schema layer.
oidcOIDCBackendSpecWhen type: OIDCOpenID Connect federation configuration. The mirror union rule ((self.type == 'OIDC') == has(self.oidc)) applies.
samlSAMLBackendSpecWhen type: SAMLSAML 2.0 federation configuration. The mirror union rule ((self.type == 'SAML') == has(self.saml)) applies.
mappings[]MappingRuleSpecNo (federation only)Keystone federation mapping rules applied to the backend's protocol; type-gated to federation backends (OIDC or SAML) at the schema layer. Federation cannot provision its protocol without at least one rule (keystone rejects rule-less mappings) — an empty list parks the backend at MappingsReady=False/NoMappingRules. Max 32 rules.
groups[]FederationGroupSpecNo (federation only)Declarative local groups (in this backend's domain) the mapping rules target, plus their role assignments; type-gated to federation backends. Max 32.
extraOptionsmap[string]stringNo (LDAP only)Free-form [ldap] section options not covered by typed fields, keyed by bare option name (e.g. page_size); type-gated to LDAP. See the denylist.

KeystoneRefSpec

FieldTypeRequiredDescription
namestringYesName of the Keystone CR in the same namespace (MinLength=1).

DomainSpec

FieldTypeRequiredDefaultDescription
namestringYesKeystone domain name (1–64 chars, pattern ^[A-Za-z0-9][A-Za-z0-9_.-]*$ — the grammar shared by Secret data keys and keystone's keystone.<name>.conf domain-config file naming). Immutable. default (any case) is rejected: the Default domain hosts the SQL-backed service users and the bootstrap admin and must never be backed by an external directory. Domain names must additionally be unique (case-insensitively) per referenced Keystone — webhook-enforced.
modeManage | AdoptNoManageManage: the controller creates the domain (and reconciles description/enabled drift on its own domain). Adopt: the controller resolves a pre-existing domain by name and never mutates it. Immutable.
deletionPolicyRetain | DeleteNoRetainWhat happens to a managed domain when this CR is deleted: Retain leaves it in place, Delete disables it and then deletes it (keystone forbids deleting an enabled domain). Adopted domains are always retained regardless of this field. Mutable, so operators decide at teardown time.
descriptionstringNoProjected onto the domain in Manage mode.

LDAPBackendSpec

Only user-set optional fields are rendered into the per-domain config, so upstream keystone defaults apply for everything left unset — with one deliberate exception: unless extraOptions carries any user_enabled_* key, the projection renders user_enabled_invert = true and user_enabled_default = false, which makes every user read as enabled. Directories without an "enabled" concept (plain inetOrgPerson / posixAccount trees — no standard LDAP attribute exists for it) otherwise yield user models without the enabled key, and keystone's response-schema validation rejects its own reply with HTTP 400 ('enabled' is a required property) on every user listing. Deployments whose directory does model enabled semantics (e.g. Active Directory's userAccountControl mask) set the matching user_enabled_* options via extraOptions, which suppresses both defaults.

FieldTypeRequiredDefaultDescription
urlstringYesLDAP server URL; pattern ^ldaps?://.
bindCredentialsSecretRefSecretRefSpecYesSecret holding the bind credentials under the fixed data keys username (the bind DN) and password. The key field must stay empty (webhook-enforced).
suffixstringYesLDAP suffix (base DN), e.g. dc=example,dc=com.
usersLDAPUserSpecYesUser tree layout and attribute mapping.
groupsLDAPGroupSpecNoGroup tree layout and attribute mapping; when unset no group_* options are rendered.
readOnly*boolNotrueForces user_allow_create/update/delete and group_allow_create/update/delete to false so keystone can never write into the corporate directory. Setting false is an explicit opt-in to a writable backend.
tlsLDAPTLSSpecNoCertificate verification for ldaps:// / STARTTLS connections.
poolLDAPPoolSpecNoKeystone-side LDAP connection pool.

LDAPUserSpec

FieldTypeRequiredRendered [ldap] optionKeystone default when unset
treeDNstringYesuser_tree_dn
filterstringNouser_filternone
objectClassstringNouser_objectclassinetOrgPerson
idAttributestringNouser_id_attributecn
nameAttributestringNouser_name_attributesn
mailAttributestringNouser_mail_attributemail

LDAPGroupSpec

FieldTypeRequiredRendered [ldap] optionKeystone default when unset
treeDNstringYesgroup_tree_dn
filterstringNogroup_filternone
objectClassstringNogroup_objectclassgroupOfNames
idAttributestringNogroup_id_attributecn
nameAttributestringNogroup_name_attributeou
memberAttributestringNogroup_member_attributemember

LDAPTLSSpec

FieldTypeRequiredDescription
caBundleSecretRefSecretRefSpecYesSecret holding the CA bundle under the fixed data key ca.crt (the canonical cert-manager file name, mirroring the database TLS contract). The projection writes the PEM to /etc/keystone/domains/<domain>-ca.pem and points tls_cacertfile at it.

LDAPPoolSpec

FieldTypeRequiredRendered [ldap] option
enabledboolNouse_pool
size*int32 (Minimum=1)Nopool_size (only when set)

OIDCBackendSpec

Configures an OpenID Connect federation backend for one domain. Discovery is either metadata-driven (providerMetadataURL, defaulted to <issuer>/.well-known/openid-configuration by the webhook) or fully explicit (endpoints); the two shapes are mutually exclusive (CEL rule plus webhook defense-in-depth). The operator fetches the discovery document at reconcile time and pre-provisions it into the sidecar's read-only OIDCMetadataDir — the module cannot self-cache there. The fetch client is SSRF-guarded: it refuses non-public metadata addresses unless the operator deployment allowlists them via --federation-metadata-allow-cidrs (operator-level configuration, never a CR field), so discovery against a trusted in-cluster IdP is opt-in and cannot be widened from this CR.

FieldTypeRequiredDefaultDescription
issuerstringYesThe OIDC issuer URL exactly as the IdP asserts it (the iss claim); pattern ^https?://. Registered as the keystone identity provider's remote ID and encoded (scheme-stripped, URL-escaped) into the metadata file basenames.
providerMetadataURLstringNoderived from issuerThe OIDC discovery document URL; pattern ^https?://. Mutually exclusive with endpoints. A fetch failure or a document whose issuer mismatches the spec parks the backend pending with a Warning event. Non-public metadata addresses are refused unless the operator deployment allowlists them via --federation-metadata-allow-cidrs (operator configuration, not a CR field); use explicit endpoints otherwise.
endpointsOIDCEndpointsSpecNoExplicit provider endpoints for air-gapped operators or IdPs unreachable from the operator pod.
clientIDstringYesThe relying-party client ID registered at the IdP.
clientSecretRefSecretRefSpecYesSecret holding the relying-party client secret under the fixed data key clientSecret. The key field must stay empty (webhook-enforced), mirroring the LDAP bind Secret contract.
protocolIDstringNoopenidKeystone federation protocol ID embedded in the websso/auth URLs (…/protocols/<protocolID>/websso); pattern ^[A-Za-z0-9_-]+$.
identityProviderNamestringNothe CR nameKeystone identity provider ID this backend provisions; pattern ^[A-Za-z0-9_-]+$. Unique per referenced Keystone (webhook-enforced).
remoteIDAttributestringNoHTTP_OIDC_ISSWSGI environ key keystone reads the asserted issuer from ([openid] remote_id_attribute). Must be uniform across every OIDC backend of one Keystone (webhook-enforced) because it renders into the single [openid] section.
scopes[]stringNo["openid", "email", "profile"]OIDC scopes requested from the IdP. Max 16.
responseTypestringNocodeOIDC response type (the authorization-code flow).
oauth2IntrospectionOIDCIntrospectionSpecNoTurns the proxy into an OAuth2 resource server so CLI clients present IdP-issued bearer tokens directly. At most one OIDC backend per Keystone may enable this (webhook-enforced): the module's OIDCOAuth* directives are server-scoped.
sessionTypeclient-cookie | client-cookie-persistentNoclient-cookiemod_auth_openidc session storage. The default keeps the whole session in the browser cookie — HA-safe across replicas, no shared server-side cache.
stateInputHeadersnone | user-agent | x-forwarded-for | bothNononeRequest headers folded into the state cookie hash. none is the HA-safe default: replicas behind one Service must not bind state to per-connection headers.

OIDCEndpointsSpec

All URL fields share the ^https?:// scheme guard.

FieldTypeRequiredDescription
authorizationEndpointstringYesOAuth2 authorization endpoint.
tokenEndpointstringYesOAuth2 token endpoint.
jwksURIstringYesJSON Web Key Set document.
userinfoEndpointstringNoOIDC userinfo endpoint.
endSessionEndpointstringNoRP-initiated logout endpoint.
introspectionEndpointstringNoOAuth2 token introspection endpoint. Required — and required to be https:// — when oauth2Introspection is enabled with explicit endpoints (webhook-enforced; see OIDCIntrospectionSpec).

OIDCIntrospectionSpec

FieldTypeRequiredDescription
enabledboolYesTurns bearer-token introspection on for this backend. The resolved introspection endpoint must be https — mod_auth_openidc rejects http introspection endpoints at Apache config-parse time, so the webhook fails explicit http endpoints at admission and the render skips a backend whose metadata-derived endpoint is http (both fail-visible, never a crash-looping sidecar).
tlsVerify*boolNo (default true)Certificate verification on the introspection call. false renders OIDCOAuthSSLValidateServer Off so self-signed / private-CA endpoints work — an explicit opt-out for test and lab environments until a CA-bundle field ships.

SAMLBackendSpec

FieldTypeRequiredDefaultDescription
idpEntityIDstringYesThe SAML IdP's entityID exactly as the IdP asserts it (1–512 chars; entityIDs may be URNs, so no scheme pattern). Registered as the keystone identity provider's remote ID and cross-checked against the entityID inside the IdP metadata.
idpMetadataSAMLIdPMetadataSpecYesThe IdP's EntityDescriptor metadata; exactly one of inline / secretRef / url (CEL rule plus webhook defense-in-depth).
spSAMLSPSpecNoThe service-provider signing key/certificate. When omitted, the operator generates a self-signed keypair once and never rotates it.
protocolIDstringNomappedKeystone federation protocol ID the websso/auth URLs embed (pattern ^[A-Za-z0-9_-]+$, ≤64). Must not collide with an operator-owned keystone.conf section (webhook-enforced).
identityProviderNamestringNoCR nameKeystone identity provider ID this backend provisions (pattern ^[A-Za-z0-9_-]+$, ≤64). Unique across every federation backend (OIDC and SAML) of one Keystone (webhook-enforced).
remoteIDAttributestringNoHTTP_MELLON_IDPThe WSGI environ key keystone reads the asserted IdP entityID from (the per-protocol [<protocolID>] remote_id_attribute). Must keep the HTTP_ prefix — it is conveyed to keystone as a request header across the sidecar hop (webhook-enforced, ≤128).
forwardAttributes[]stringNoSAML assertion attribute names the proxy forwards to keystone as MELLON-<attr> request headers (items pattern ^[A-Za-z0-9_]+$, max 32, unique). Case-sensitive; the header hop uppercases them, so mapping rules reference HTTP_MELLON_<ATTR>.

SAMLIdPMetadataSpec

Exactly one of the three sources must be set (CEL rule plus webhook defense-in-depth).

FieldTypeRequiredDescription
inlinestringNoRaw IdP EntityDescriptor XML (≤65536). The operator validates its entityID against idpEntityID at admission.
secretRefSecretRefSpecNoSecret holding the IdP metadata under the fixed data key idp-metadata.xml. The key field must stay empty (webhook-enforced).
urlstringNoIdP metadata URL (pattern ^https://, ≤512); operator-fetched through the hardened SSRF-guarded client. https:// is mandatory — the metadata carries the assertion-signing certificate, so a plaintext fetch would let an on-path attacker substitute the trust anchor. Non-public addresses are refused unless the operator deployment allowlists them via --federation-metadata-allow-cidrs (operator configuration, not a CR field).

SAMLSPSpec

FieldTypeRequiredDescription
certificateSecretRefSecretRefSpecNoA kubernetes.io/tls-shaped Secret holding the SP certificate/key under the fixed data keys tls.crt / tls.key (exactly what a cert-manager Certificate emits). The key field must stay empty (webhook-enforced). When omitted, the operator generates a self-signed keypair once (in the stable-named <keystone>-saml-sp Secret) and never rotates it.

MappingRuleSpec

One keystone federation mapping rule: remote assertion matchers plus the local objects they map to. The typed shape mirrors keystone's mapping-rule JSON one-to-one — the rule grammar is closed, so no free-form escape hatch exists. The camelCase field names map to the snake_case JSON keys:

CRD fieldKeystone mapping JSON key
mappings[].local / mappings[].remoterules[].local / rules[].remote
remote[].typetype
remote[].regexregex
remote[].anyOneOfany_one_of
remote[].notAnyOfnot_any_of
remote[].blacklistblacklist
remote[].whitelistwhitelist
local[].user.{id,name,email,type}user.{id,name,email,type}
local[].user.domain.{id,name}user.domain.{id,name}
local[].group.{id,name}group.{id,name}
local[].group.domain.{id,name}group.domain.{id,name}
local[].groupIdsgroup_ids
local[].groupsgroups
local[].projects[].nameprojects[].name
local[].projects[].roles[].nameprojects[].roles[].name
local[].domain.{id,name}domain.{id,name}
FieldTypeRequiredDescription
local[]MappingLocalRuleSpecYes (1–16)Local Identity API objects the matched remote user maps to. Every rule needs at least one local entry (webhook-enforced).
remote[]MappingRemoteRuleSpecYes (1–16)Assertion matchers. type is the full WSGI environ key (e.g. HTTP_OIDC_ISS, HTTP_OIDC_PREFERRED_USERNAME) — keystone's assertion_prefix stays empty so the claim headers arrive unmodified. Every rule needs at least one remote entry with a non-empty type (webhook-enforced).

FederationGroupSpec

Declares one local keystone group (created if missing in the backend's domain, never mutated afterwards — keystone cascades group deletion with the domain per the deletion policy) plus the role assignments granting its members access.

FieldTypeRequiredDescription
namestringYesGroup name inside the backend's domain (1–128 chars).
descriptionstringNoProjected onto the group at creation.
roleAssignments[]FederationRoleAssignmentSpecNo (max 32)Role grants for the group.

FederationRoleAssignmentSpec

Exactly one of project or domain must be set (CEL rule plus webhook defense-in-depth). The role and project are resolved by name at reconcile time; a missing role or project parks the backend at MappingsReady=False/RoleOrProjectNotFound on a bounded poll.

FieldTypeRequiredDescription
rolestringYesRole name to grant (e.g. member).
projectFederationProjectScopeSpecNoScopes the assignment to a project: name (required) and domainName (defaults to the backend's own domain).
domainboolNoScopes the assignment to the backend's domain.

extraOptions Denylist

The validating webhook rejects extraOptions keys the projection owns, so the escape hatch cannot silently contradict the typed spec:

GroupRejected keys
Rendered from typed fieldsurl, suffix, user, password, tls_cacertfile, use_pool, pool_size, user_tree_dn, user_filter, user_objectclass, user_id_attribute, user_name_attribute, user_mail_attribute, group_tree_dn, group_filter, group_objectclass, group_id_attribute, group_name_attribute, group_member_attribute
Operator-owned wiringdriver, domain_config_dir
readOnly-forced (only when readOnly is true, the default)user_allow_create, user_allow_update, user_allow_delete, group_allow_create, group_allow_update, group_allow_delete

Keys are bare [ldap] option names; per-domain [identity]-section tuning is not expressible in Phase 1.

Status

KeystoneIdentityBackendStatus

The dedicated KeystoneIdentityBackendReconciler is the single writer of this status. The keystone-side identitybackends sub-reconciler only reads DomainReady (it gates config projection) and writes the aggregated IdentityBackendsReady condition onto the Keystone CR instead.

FieldTypeDescription
conditions[]metav1.ConditionDomainReady, ConfigProjected, for federation backends (OIDC and SAML) additionally FederationObjectsReady and MappingsReady, and the aggregate Ready (see below). The aggregate derives from the backend type's own sub-condition set, so LDAP backends are unaffected by the federation-only types.
observedGenerationint64The .metadata.generation the controller last reconciled.
domainIDstringThe Keystone domain ID this backend provisioned (Manage) or resolved (Adopt). The deletion path uses it to disable+delete exactly the domain this CR created, never a same-named foreign one.
samlSPMetadataSecretNamestringFor a SAML backend, the Secret carrying the generated SP metadata (data keys sp-metadata.xml and entityID) for out-of-band IdP registration. Empty for non-SAML backends and until the SP material first resolves.

Conditions

TypeStatusReasonMeaning
DomainReadyTrueDomainProvisionedManage mode created (or re-recognized) the domain.
DomainReadyTrueDomainAdoptedAdopt mode resolved the pre-existing domain by name.
DomainReadyFalseKeystoneNotFoundspec.keystoneRef does not resolve (yet); the Keystone watch re-wakes the backend when it appears.
DomainReadyFalseWaitingForKeystoneAPIThe referenced Keystone's KeystoneAPIReady condition is not True yet.
DomainReadyFalseAdminSecretUnavailableThe bootstrap admin password Secret is missing or has no password key.
DomainReadyFalseDomainNotFoundAdopt mode: no domain with the requested name exists (Adopt never creates).
DomainReadyFalseDomainAlreadyExistsManage mode: a same-named domain exists that this CR did not create — never silently seized; switch to mode: Adopt to attach to it.
DomainReadyFalseIdentityAPIErrorA domain lookup/create/update call failed.
FederationObjectsReadyTrueFederationObjectsProvisionedFederation (OIDC/SAML): the identity provider and protocol are upserted and drift-free.
FederationObjectsReadyFalseNoMappingRulesFederation: spec.mappings is empty — keystone cannot represent a rule-less mapping and the protocol needs one.
FederationObjectsReadyFalseIdentityAPIErrorFederation: an identity-provider/protocol call failed.
MappingsReadyTrueMappingsAppliedFederation: the mapping rules, declarative groups, and role assignments are applied.
MappingsReadyFalseNoMappingRulesFederation: spec.mappings is empty.
MappingsReadyFalseRoleOrProjectNotFoundFederation: a role assignment references a role, project, or project domain that does not exist (yet); retried on a bounded poll.
MappingsReadyFalseIdentityAPIErrorFederation: a mapping/group/assignment call failed.
ConfigProjectedTrueConfigProjectedLDAP: the Keystone Deployment's domains volume Secret carries this backend's keystone.<domain>.conf. OIDC: the federation Secret mounted by the sidecar carries this backend's client document. SAML: the federation Secret carries this backend's IdP-metadata document. In every case the Deployment has also finished rolling that template out.
ConfigProjectedFalseWaitingForProjectionThe projection has not landed in the Deployment's pod template yet.
ConfigProjectedFalseWaitingForRolloutThe pod template mounts this backend's config, but replicas of the previous template still serve; the condition flips once every replica runs the projected config.
ReadyTrueAllReadyBoth sub-conditions are True.
ReadyFalseNotAllReadyAt least one sub-condition is not True.

Once DomainReady, FederationObjectsReady, or MappingsReady has reached True, the transient-observation reasons (WaitingForKeystoneAPI, AdminSecretUnavailable, IdentityAPIError) never demote it back to False: they report a failed observation — the API or a credential was temporarily unreachable — not a de-provisioning. This matters on every OIDC attach, where projecting the sidecar rolls the Keystone Deployment and briefly takes the API offline: demoting DomainReady in that window would de-project the backend and re-trigger the same rollout in a loop. Transient failures surface as Events and reconcile retries; only authoritative findings (domain gone, foreign same-named domain, missing mapping rules, unresolvable role/project) flip a provisioned condition back to False.

WaitingForRollout is deliberately not on that transient list: a pending rollout demotes ConfigProjected, and with it the aggregate Ready. The aggregate therefore only turns True once every replica of the Keystone Deployment runs this backend's projected config — a consumer gating on Ready (the ControlPlane websso projection, for example) never observes a backend whose config only the pod template carries.

Deletion Semantics

Deleting a backend CR runs the keystone.openstack.c5c3.io/identitybackend finalizer:

  1. De-projection first. The keystone-side sub-reconciler drops the backend's config from the projection (the domains Secret for LDAP, the federation Secret — and with the last OIDC backend, the whole sidecar — for OIDC) and rolls the Deployment; the finalizer waits for this so keystone never runs with config pointing at a dead domain.
  2. Federation-object teardown (OIDC). The protocol, mapping, and identity provider are removed in reverse dependency order — unconditionally, regardless of the domain deletion policy — tolerating objects already gone. Declarative groups follow the domain (keystone cascades domain contents).
  3. Deletion policy. Only for mode: Manage + deletionPolicy: Delete with a recorded status.domainID: the domain is disabled and then deleted (keystone forbids deleting an enabled domain). Retain (the default) and adopted domains always leave the domain in place.
  4. Fail open. When the referenced Keystone CR is gone (stack teardown), or the admin credential is no longer available, the finalizer releases with a Warning event instead of holding the backend hostage; the domain is retained.

Immutability and Validation Summary

Schema-layer rules (CEL / kubebuilder markers, enforced by the API server even when the webhook is down): keystoneRef / type / domain.name / domain.mode transition rules, the type-vs-ldap, type-vs-oidc, and type-vs-saml unions, the type-gating of mappings/groups (federation) and extraOptions (LDAP), the Default-domain rejection, the URL scheme patterns, the providerMetadataURL-vs-endpoints exclusivity, the SAML metadata exactly-one-source rule, the project-vs-domain exclusivity on role assignments, and the domain-name / identity-provider-name / forwardAttributes grammars.

Webhook rules (defense-in-depth plus the rules CEL cannot express): domain-name uniqueness per referenced Keystone (case-insensitive, via a direct-reader List that skips Terminating siblings), the fixed data-key contract on the bind/CA Secret references, the extraOptions denylist, and an INI-injection guard that rejects a newline or carriage-return in any rendered value (every typed ldap field and every extraOptions value) — such a character would inject arbitrary [ldap] lines through the verbatim config render, defeating the readOnly forcing and the denylist. The projection re-validates the fully assembled [ldap] values as the last line of defense: it is the only gate that sees the Secret-sourced bind username/password and the only gate that still runs when a CR bypassed admission, and it skips (with a Warning event) any backend whose value carries a control character rather than emitting a corrupted config.

OIDC-specific webhook rules: the fixed clientSecret data-key contract on clientSecretRef, per-rule mapping completeness (at least one local and one remote matcher with a non-empty type), identityProviderName uniqueness per referenced Keystone, remoteIDAttribute uniformity across the OIDC siblings of one Keystone, at most one introspection-enabled sibling, the https-only introspection endpoint, and a config-injection guard over every value the proxy configuration or metadata documents embed. The federation render re-validates those values (plus the Secret-sourced client secret path) as the last line of defense, mirroring the LDAP contract.

SAML-specific webhook rules: the fixed data-key contracts on the idpMetadata.secretRef (idp-metadata.xml) and sp.certificateSecretRef (tls.crt / tls.key) references, the exactly-one metadata source, the metadata URL scheme, the HTTP_-prefixed remoteIDAttribute (it must be header-conveyable across the sidecar hop), the protocolID section-collision guard (it becomes a [<protocolID>] keystone.conf section), the forwardAttributes charset/uniqueness, inline-metadata entityID matching, the identity-provider-name uniqueness across every federation sibling (OIDC and SAML), and the at-most-one-SAML-backend-per-Keystone rule. The SAML render re-validates the config-embedded values and re-checks the resolved IdP-metadata entityID as the last line of defense.

Chainsaw E2E Tests

The rejection corpus lives in tests/e2e/keystone/invalid-identitybackend-cr/ (generated from _generate.py, guarded by make verify-invalid-cr-fixtures); the end-to-end LDAP flow — in-suite OpenLDAP fixture, domain provisioning, projection, token issuance for an LDAP user, and clean detach — lives in tests/e2e/keystone/ldap-domain-backend/. The OIDC federation flow — in-suite two-realm Keycloak fixture, federation-object provisioning, the sidecar rollout, the bearer/CLI and browser websso flows, the multi-realm proof, and clean detach — lives in tests/e2e/keystone/oidc-federation/; chaos coverage (sidecar container-kill, IdP outage) lives in tests/e2e-chaos/keystone-federation/. The SAML federation flow — in-suite Keycloak SAML realm, federation-object provisioning, the mellon sidecar rollout, the SP-metadata export, OIDC/SAML coexistence, and clean detach — lives in tests/e2e/keystone/saml-federation/. See Keystone E2E Test Suites.

Retained Artefacts

Like the config ConfigMaps, up to 3 historical content-hashed <keystone>-domains-<hash> and <keystone>-federation-<hash> Secrets are retained while backends of the matching type exist (fast rollback); when the last backend of a type detaches every historical Secret of that type is removed (no bind password, client secret, or crypto-passphrase copy may linger), and all of them are garbage-collected with the owning Keystone CR. The stable-named <keystone>-oidc-crypto-passphrase Secret holds the operator-generated OIDCCryptoPassphrase; it is regenerable by design (a rotation invalidates in-flight login sessions only) and therefore has no OpenBao backup.