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:
| System | Protocol | Library | Direction | Mandatory? |
|---|---|---|---|---|
| HTTP clients (users, services, keystonemiddleware in every other service) | HTTP/JSON | Flask (keystone/server/flask/) | inbound | yes |
| SQL database | SQL | SQLAlchemy and oslo.db enginefacade | outbound | yes |
| Key files on disk (Fernet tokens, credentials, receipts, JWS) | file system | cryptography (Fernet, ES256) | read; written by keystone-manage | yes |
| Cache (memcached, Redis…) | memcache/redis | oslo.cache (dogpile) | outbound | no ([cache]/enabled) |
| Message bus (RabbitMQ…) | AMQP | oslo.messaging Notifier | outbound, notifications only | no ([oslo_messaging_notifications]/driver) |
| LDAP directory | LDAP v3 | python-ldap, ldappool | outbound | no (identity backend per domain) |
xmlsec1 binary | process | subprocess | local | only 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 withOS_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 inkeystone/common/authorization.py:22-38.
- Tokenless authentication: a client certificate (mTLS), when
[tokenless_auth]is configured, is handled byAuthContextMiddleware(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):
| Version | Type | Fields (in order) |
|---|---|---|
| 0 | unscoped | user_id, methods, expires_at, audit_ids |
| 1 | domain | user_id, methods, domain_id, expires_at, audit_ids |
| 2 | project | user_id, methods, project_id, expires_at, audit_ids |
| 3 | trust | user_id, methods, project_id, expires_at, audit_ids, trust_id |
| 4 | federated unscoped | user_id, methods, group_ids, idp_id, protocol_id, expires_at, audit_ids |
| 5 / 6 | federated project / domain | user_id, methods, scope_id, group_ids, idp_id, protocol_id, expires_at, audit_ids |
| 7 | OAuth1 | user_id, methods, project_id, access_token_id, expires_at, audit_ids |
| 8 | system | user_id, methods, system, expires_at, audit_ids |
| 9 | application credential | user_id, methods, project_id, expires_at, audit_ids, app_cred_id |
| 10 | OAuth2 mTLS | user_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_atis a float of seconds since the epoch;audit_idsare the bytes of URL-safe base64 strings;methodsis 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]/methodsmakes every existing token unreadable. A rewrite must keep the same order.
- UUID IDs are compressed into 16 bytes, as a pair
- Keys: files
0,1, … in[fernet_tokens]/key_repository(default/etc/keystone/fernet-keys/).- The highest number is the primary key, which encrypts.
0is the staged key.- Every key decrypts.
keystone-manage fernet_rotatepromotes0and keepsmax_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, withkey_hash= hash of the primary key used,keystone/credential/providers/fernet/core.py:54-104). Rotation is done bycredential_rotateand re-encryption bycredential_migrate.[fernet_receipts]/key_repository: MFA receipts.
3. Database
- Connection:
[database]/connection(oslo.db). Transactions go throughsql.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,--migrateand--contractallow a rolling upgrade; plaindb_syncruns all of them (keystone/cmd/cli.py:361-494).
| Area | Tables (model) |
|---|---|
| Identity | user, 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) |
| Resource | project (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) |
| Assignment | role, role_option, implied_role, assignment, system_assignment (keystone/assignment/role_backends/sql_model.py:52-105, keystone/assignment/backends/sql.py:418-452) |
| Catalog | region, service, endpoint, endpoint_group, project_endpoint, project_endpoint_group (keystone/catalog/backends/sql.py:31-584) |
| Policies | policy, policy_association |
| Credentials | credential, application_credential, application_credential_role, application_credential_access_rule, access_rule |
| Delegation | trust, trust_role, consumer, request_token, access_token (OAuth1) |
| Federation | identity_provider, idp_remote_ids, federation_protocol, mapping, service_provider |
| Limits | registered_limit, limit |
| Revocation | revocation_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 / decorator | Content | Invalidated by | Defined at |
|---|---|---|---|
tokens | validated tokens (_validate_token, check_revocation_v3) | any revocation of a trust, user or domain; user, domain or project disabled; cascade token revocation | keystone/token/provider.py:39-130 |
revoke | revocation events | every revoke() | keystone/revoke/core.py:28-29 |
computed assignments | effective roles | any change to an assignment, project or domain | keystone/assignment/core.py:47-48 |
computed catalog region | catalog per project | any catalog change | keystone/catalog/core.py:38-39 |
id mapping | public ID ↔ local ID | — | keystone/identity/core.py:52-53 |
receipts | MFA receipts | same events as the tokens | keystone/receipt/provider.py:36-37 |
memoize identity, role, catalog, credential, federation, unified_limit | get / list results | the manager on writes | the 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). Priorityinfo. - Format (
[DEFAULT]/notification_format,keystone/conf/default.py:150-168):cadf(default): a CADF event (eventType: activity,action,outcome,initiatorwith the user, project and address,target,observer= the identity service), withevent_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|disabledfor user, group, project, domain, role, region, service, endpoint, policy, trust, OAuth consumer, application credential…;identity.role_assignment.created|deleted;identity.authenticatewith outcomesuccess,failureorpending.
- Opt-out: by default
identity.authenticate.successandidentity.authenticate.pendingare not sent ([DEFAULT]/notification_opt_out). - Not sent:
disabledevents are often markedpublic=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 poolldappool.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 throughid_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.