OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

keystone — Interactions

Interactions with other components — keystone — branch stable/2026.2

Branch stable/2026.2 @ e6dc5ae079e6 — repository https://opendev.org/openstack/keystone — analysed on 2026-10-28

Keystone talks to the following systems:

SystemProtocolLibraryDirectionMandatory?
HTTP clients (users, services, keystonemiddleware in every other service)HTTP/JSONFlask (keystone/server/flask/)inboundyes
SQL databaseSQLSQLAlchemy and oslo.db enginefacadeoutboundyes
Key files on disk (Fernet tokens, credentials, receipts, JWS)file systemcryptography (Fernet, ES256)read; written by keystone-manageyes
Cache (memcached, Redis…)memcache/redisoslo.cache (dogpile)outboundno ([cache]/enabled)
Message bus (RabbitMQ…)AMQPoslo.messaging Notifieroutbound, notifications onlyno ([oslo_messaging_notifications]/driver)
LDAP directoryLDAP v3python-ldap, ldappooloutboundno (identity backend per domain)
xmlsec1 binaryprocesssubprocesslocalonly for Keystone as SAML IdP

There is no outbound REST call to another OpenStack service and no RPC. Keystone listens to no queue: it only publishes notifications.

1. Inbound HTTP

  • Transport: WSGI application keystone.wsgi.api:application (keystone/wsgi/api.py), behind uwsgi or mod_wsgi. The configuration files are found with OS_KEYSTONE_CONFIG_DIR / OS_KEYSTONE_CONFIG_FILES (keystone/server/flask/core.py:73-90).
  • Authentication headers:
    • X-Auth-Token: the caller's token;
    • X-Subject-Token: the token to validate, in requests and responses;
    • Openstack-Auth-Receipt: MFA receipt;
    • OpenStack-Identity-Access-Rules: version of access rules the middleware understands; all defined in keystone/common/authorization.py:22-38.
  • Tokenless authentication: a client certificate (mTLS), when [tokenless_auth] is configured, is handled by AuthContextMiddleware (keystone/server/flask/request_processing/middleware/auth_context.py).

Sample frame [reconstructed]: password authentication scoped to a project.

POST /v3/auth/tokens HTTP/1.1
Host: keystone:5000
Content-Type: application/json

{"auth": {"identity": {"methods": ["password"],
   "password": {"user": {"name": "demo", "domain": {"id": "default"}, "password": "…"}}},
  "scope": {"project": {"name": "demo", "domain": {"id": "default"}}}}}

HTTP/1.1 201 Created
X-Subject-Token: gAAAAABl…   (Fernet token, about 180 to 250 characters)
Vary: X-Auth-Token
Content-Type: application/json

{"token": {"methods": ["password"], "user": {"id": "…", "name": "demo", "domain": {…}, "password_expires_at": null},
  "audit_ids": ["3T2dc1CGQxyJsHdDu1xkcw"], "expires_at": "2026-10-28T12:00:00.000000Z",
  "issued_at": "2026-10-28T11:00:00.000000Z", "project": {"id": "…", "name": "demo", "domain": {…}},
  "is_domain": false, "roles": [{"id": "…", "name": "member"}, {"id": "…", "name": "reader"}],
  "catalog": [{"type": "compute", "name": "nova", "id": "…", "endpoints": [{"interface": "public", "region": "RegionOne", "url": "…"}]}]}}

2. Tokens: format and keys (the main contract for a rewrite)

Fernet (default provider, [token]/provider = fernet)

  • Token = Fernet(MultiFernet(keys)).encrypt(msgpack(payload)), with the trailing = removed (keystone/token/token_formatters.py:39-270). It is not stored: validation decrypts it and rebuilds the context from the database.
  • Payload = (version,) + fields, in 11 versions (keystone/token/token_formatters.py:445-1206):
VersionTypeFields (in order)
0unscopeduser_id, methods, expires_at, audit_ids
1domainuser_id, methods, domain_id, expires_at, audit_ids
2projectuser_id, methods, project_id, expires_at, audit_ids
3trustuser_id, methods, project_id, expires_at, audit_ids, trust_id
4federated unscopeduser_id, methods, group_ids, idp_id, protocol_id, expires_at, audit_ids
5 / 6federated project / domainuser_id, methods, scope_id, group_ids, idp_id, protocol_id, expires_at, audit_ids
7OAuth1user_id, methods, project_id, access_token_id, expires_at, audit_ids
8systemuser_id, methods, system, expires_at, audit_ids
9application credentialuser_id, methods, project_id, expires_at, audit_ids, app_cred_id
10OAuth2 mTLSuser_id, methods, project_id, domain_id, expires_at, audit_ids, thumbprint

The field order is the one returned by each assemble (keystone/token/token_formatters.py:470-1160).

  • Field encodings:
    • UUID IDs are compressed into 16 bytes, as a pair (True, bytes); non-UUID IDs are kept as (False, str) (attempt_convert_uuid_hex_to_bytes);
    • expires_at is a float of seconds since the epoch;
    • audit_ids are the bytes of URL-safe base64 strings;
    • methods is an integer bitmask whose bit i is the i-th method of [auth]/methods (keystone/auth/plugins/core.py:33-95). Consequence: changing the order of [auth]/methods makes every existing token unreadable. A rewrite must keep the same order.
  • Keys: files 0, 1, … in [fernet_tokens]/key_repository (default /etc/keystone/fernet-keys/).
    • The highest number is the primary key, which encrypts.
    • 0 is the staged key.
    • Every key decrypts.
    • keystone-manage fernet_rotate promotes 0 and keeps max_active_keys (keystone/common/fernet_utils.py:202-266). The repository must be identical on every node: the operator synchronises it (rsync, secrets).
  • **issued_at** is not in the payload: it is the Fernet timestamp (bytes 1 to 8 of the token, TokenFormatter.creation_time).

JWS ([token]/provider = jws)

An ES256-signed token (public and private keys in [jwt_tokens]), with the same information as JSON claims (keystone/token/providers/jws/core.py). The keys are created by keystone-manage create_jws_keypair.

Other Fernet repositories

  • [credential]/key_repository: encryption of credential blobs (credential.encrypted_blob, with key_hash = hash of the primary key used, keystone/credential/providers/fernet/core.py:54-104). Rotation is done by credential_rotate and re-encryption by credential_migrate.
  • [fernet_receipts]/key_repository: MFA receipts.

3. Database

  • Connection: [database]/connection (oslo.db). Transactions go through sql.session_for_read() / session_for_write() (keystone/common/sql/core.py).
  • Schema: 47 tables (_rewrite/schema-mysql_stable-2026.2.sql).
  • Migrations: Alembic in two branches, expand and contract (keystone/common/sql/migrations/versions/). keystone-manage db_sync --expand, --migrate and --contract allow a rolling upgrade; plain db_sync runs all of them (keystone/cmd/cli.py:361-494).
AreaTables (model)
Identityuser, local_user, password, nonlocal_user, federated_user, user_option, group, user_group_membership, expiring_user_group_membership, id_mapping (keystone/identity/backends/sql_model.py:34-497, keystone/identity/mapping_backends/sql.py:21)
Resourceproject (also holds the domains, is_domain), project_tag, project_option, whitelisted_config, sensitive_config, config_register (keystone/resource/backends/sql_model.py:56-135, keystone/resource/config_backends/sql.py:20-46)
Assignmentrole, role_option, implied_role, assignment, system_assignment (keystone/assignment/role_backends/sql_model.py:52-105, keystone/assignment/backends/sql.py:418-452)
Catalogregion, service, endpoint, endpoint_group, project_endpoint, project_endpoint_group (keystone/catalog/backends/sql.py:31-584)
Policiespolicy, policy_association
Credentialscredential, application_credential, application_credential_role, application_credential_access_rule, access_rule
Delegationtrust, trust_role, consumer, request_token, access_token (OAuth1)
Federationidentity_provider, idp_remote_ids, federation_protocol, mapping, service_provider
Limitsregistered_limit, limit
Revocationrevocation_event

**There is no token table**: tokens are not persisted. The revocation_event table is what makes revocation possible.

Checking revocation [reconstructed] (keystone/revoke/backends/sql.py:112-155)

On every validation (unless cached):

SELECT * FROM revocation_event
 WHERE issued_before >= :token_issued_at
   AND (user_id IS NULL OR user_id IN (:user_id, :trustor_id, :trustee_id))
   AND (project_id IS NULL OR project_id = :project_id)
   AND (audit_id IS NULL OR audit_id = :audit_id)
   AND (trust_id IS NULL OR trust_id = :trust_id);

The rows found are then compared in memory on the other fields (domain, expires_at, consumer, audit_chain_id, role) by matches() (keystone/models/revoke_model.py:160-222). Every write of an event also purges the events older than the token lifetime plus [token]/allow_expired_window (_prune_expired_events, keystone/revoke/backends/sql.py:88-110), in batches on MySQL. There is no periodic purge task.

Password authentication [reconstructed] (keystone/identity/backends/sql.py:63-123)

SELECT user.*, local_user.*, password.* FROM user JOIN local_user … WHERE user.id = :id;   -- read
-- if it fails:
UPDATE local_user SET failed_auth_count = failed_auth_count + 1, failed_auth_at = now() WHERE user_id = :id;
-- if it succeeds after failures:
UPDATE local_user SET failed_auth_count = 0 … ;
UPDATE user SET last_active_at = :today WHERE id = :id;   -- shadow_users_api.set_last_active_at

Hashing uses scrypt, bcrypt, bcrypt_sha256 or pbkdf2_sha512, chosen by [identity]/password_hash_algorithm. The algorithm is recognised from the hash prefix, so old hashes stay valid (keystone/common/password_hashing.py:33-60).

4. Cache (oslo.cache)

Regions, each with its own [<group>]/caching and cache_time options:

Region / decoratorContentInvalidated byDefined at
tokensvalidated tokens (_validate_token, check_revocation_v3)any revocation of a trust, user or domain; user, domain or project disabled; cascade token revocationkeystone/token/provider.py:39-130
revokerevocation eventsevery revoke()keystone/revoke/core.py:28-29
computed assignmentseffective rolesany change to an assignment, project or domainkeystone/assignment/core.py:47-48
computed catalog regioncatalog per projectany catalog changekeystone/catalog/core.py:38-39
id mappingpublic ID ↔ local ID—keystone/identity/core.py:52-53
receiptsMFA receiptssame events as the tokenskeystone/receipt/provider.py:36-37
memoize identity, role, catalog, credential, federation, unified_limitget / list resultsthe manager on writesthe respective core.py files

Distributed invalidation: region.invalidate() does not flush the cache. It replaces a region identifier stored under the key <<<region>>>:<name> in a dedicated region, and that identifier is appended to every key (keystone/common/cache/core.py:29-110). All processes sharing the same memcached therefore see the invalidation. With a per-process cache (dogpile.cache.memory), an invalidation in one worker is not seen by the others [inferred].

5. Notifications (AMQP, outbound only)

  • Publisher: identity.<[DEFAULT]/default_publisher_id or hostname> (keystone/notifications.py:487-506). Priority info.
  • Format ([DEFAULT]/notification_format, keystone/conf/default.py:150-168):
    • cadf (default): a CADF event (eventType: activity, action, outcome, initiator with the user, project and address, target, observer = the identity service), with event_type = identity.<resource>.<operation> (keystone/notifications.py:944-1020);
    • basic: {"resource_info": <id>} plus the request IDs (keystone/notifications.py:582-636).
  • Events:
    • identity.<resource>.created|updated|deleted|disabled for user, group, project, domain, role, region, service, endpoint, policy, trust, OAuth consumer, application credential…;
    • identity.role_assignment.created|deleted;
    • identity.authenticate with outcome success, failure or pending.
  • Opt-out: by default identity.authenticate.success and identity.authenticate.pending are not sent ([DEFAULT]/notification_opt_out).
  • Not sent: disabled events are often marked public=False. They only feed the internal callbacks (see async processing).

Sample envelope [reconstructed] (CADF, user created):

{"event_type": "identity.user.created", "publisher_id": "identity.controller1", "priority": "INFO",
 "payload": {"typeURI": "http://schemas.dmtf.org/cloud/audit/1.0/event", "eventType": "activity",
   "action": "created.user", "outcome": "success",
   "initiator": {"typeURI": "service/security/account/user", "id": "…", "host": {"address": "10.0.0.5", "agent": "python-openstackclient"}, "project_id": "…"},
   "target": {"typeURI": "data/security/account/user", "id": "<new user id>"},
   "observer": {"typeURI": "service/security", "id": "<identity service id>"},
   "resource_info": "<new user id>"}}

6. LDAP

  • A read-only identity backend (users and groups): creating, updating or deleting a user or a group answers 403 READ_ONLY_LDAP_ERROR_MESSAGE (keystone/identity/backends/ldap/core.py:161-190). Keystone only reads LDAP, plus the bind for password authentication.
  • Connections: ldap.initialize(url), or a pool ldappool.ConnectionManager ([ldap]/use_pool, keystone/identity/backends/ldap/common.py:579-921).
  • Errors: LDAPServerConnectionError (500), LDAPInvalidCredentialsError, LDAPSizeLimitExceeded (500).
  • One LDAP per domain is possible ([identity]/domain_specific_drivers_enabled, with configuration files or /v3/domains/{id}/config). The public IDs then go through id_mapping.

7. External process: xmlsec1

To sign a SAML assertion (Keystone as identity provider for Keystone-to-Keystone), Keystone writes the assertion to a temporary file, then runs xmlsec1 --sign --privkey-pem <key>,<cert> --id-attr:ID Assertion … (keystone/federation/idp.py:447-541). A failure gives 500 SAMLSigningError.

Source: reverse-spec-keystone/03-interactions_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.