Client contract and Go rewrite — keystonemiddleware — branch stable/2026.2
Branch
stable/2026.2@3a8a835bb573— repository https://opendev.org/openstack/keystonemiddleware — analysed on 2026-10-28 Service used for the proof: Keystonestable/2026.2@e6dc5ae079e6(../reverse-spec-keystone/), in-process; oslo.context 6.5.0 (oslo-context:) for the reading of headers
This file has two purposes:
- state what keystonemiddleware requires from Keystone, so that OpenKeel Keystone is accepted by the Python services that remain;
- state what an equivalent Go middleware (for the OpenKeel services) must reproduce, so that the identity handed to the services stays the same.
1. Contents of _rewrite/
| File | Content |
|---|---|
[exchanges_stable-2026.2.json](_rewrite/exchanges_stable-2026.2.json) | 10 scenarios (expected, observed, statuses and headers seen by the service) and the 44 calls of the middleware to Keystone |
[response-schemas_stable-2026.2.json](_rewrite/response-schemas_stable-2026.2.json), [response-schemas_stable-2026.2.md](_rewrite/response-schemas_stable-2026.2.md) | schemas of the Keystone responses read by the middleware (GET /v3/auth/tokens…) |
[observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md) | errors seen on the Keystone side (404 for invalid tokens) |
How it was produced: conformance_capture.py scenarios with the keystonemiddleware adapter.
- The same throw-away Keystone as for keystoneauth.
- auth_token in front of an "echo" application that returns the headers received.
- Ordinary users (
member,service) are created through the API.
Result: 10/10 as expected.
2. Scenarios
| # | Scenario | Observed |
|---|---|---|
| 1 | valid-user-token-headers | 200; X-Identity-Status: Confirmed; X-Roles: member,reader (implied role expanded); X-Is-Admin-Project: True (no admin project configured in Keystone, so every project counts as one) |
| 2 | no-token-rejected | 401, WWW-Authenticate: Keystone uri="http://keystone.test/v3", JSON body |
| 3 | invalid-token-rejected | Keystone 404, then 401 to the caller |
| 4 | delay-auth-decision-marks-invalid | 200 to the service with X-Identity-Status: Invalid |
| 5 | spoofed-identity-headers-replaced | X-Roles: admin, X-Service-Roles and X-Is-Admin-Project sent by the client are removed or replaced |
| 6 | spoofed-system-scope-header-with-delay-auth | **OpenStack-System-Scope: all sent by the client reaches the service** when the token is invalid and delay_auth_decision = true (see § 5) |
| 7 | service-token-headers | X-Service-Identity-Status: Confirmed, X-Service-Roles: service |
| 8 | service-token-without-service-role | 401 (service_token_roles_required = true) |
| 9 | revoked-token-and-middleware-cache | before revocation 200; after, same middleware 200 (cache); fresh middleware 401 |
| 10 | application-credential-access-rules | authorised path 200; other path 401; without service_type 401 |
3. What keystonemiddleware requires from Keystone
| Requirement | Source |
|---|---|
GET /v3/auth/tokens with X-Subject-Token: 200 + token body, 404 for every invalid token. A 401 means "your own token is invalid" (the middleware re-authenticates once, then answers 503 to its callers) | keystonemiddleware/auth_token/_identity.py:130-170 |
supports ?allow_expired=1 and ?nocatalog | keystonemiddleware/auth_token/_identity.py:48-58 |
honours OpenStack-Identity-Access-Rules: 1: tokens from application credentials with access rules are only returned to middlewares that send it | keystonemiddleware/auth_token/_identity.py:21; Keystone side: keystone:keystone/token/provider.py:256-275 |
token body: roles already expanded (implied roles), application_credential.access_rules, catalog (for access rules and X-Service-Catalog), is_admin_project, system, expires_at, user, project | keystonemiddleware/auth_token/__init__.py:543-590, keystonemiddleware/auth_token/_request.py:61-201 |
version discovery on /v3, and POST /v3/auth/tokens for its own authentication (keystoneauth contract) | ../reverse-spec-keystoneauth/07-rewrite-pack_stable-2026.2.md |
4. What a Go middleware for the OpenKeel services must reproduce
- Remove every client-supplied identity header before validating (the list of
keystonemiddleware/auth_token/_request.py:206-227), **includingOpenStack-System-Scope** (see § 5). - Read
X-Auth-Token, orX-Storage-Tokenfor Swift, andX-Service-Token. Validate them throughGET /v3/auth/tokenswith the middleware's own token,OpenStack-Identity-Access-Rules: 1, andallow_expired=1when a service token with theservicerole is present. - Cache the valid validations (TTL
token_cache_time, 300 s), keyed bysha256(token), with optional HMAC or encryption. Never cache an invalid token. Document the revocation delay this implies, or make it configurable (0 = no cache). - Check expiry (unless
allow_expired), access rules (same*,**and{x}grammar), and optionally the bind. - Inject the headers of the table in 01 § 2, with the same names and formats: comma-separated roles,
True/False,Confirmed/Invalid, the catalog as JSON with the v3 catalog normalised as_normalize_catalogdoes (keystonemiddleware/auth_token/_request.py). - On rejection: 401 with the same JSON body and
WWW-Authenticate: Keystone uri="…". If Keystone is unavailable: 503 "The Keystone service is temporarily unavailable.". Withdelay_auth_decision: let the request through, markedInvalid. - Read the same
[keystone_authtoken]options (01 § 3) so that deployment tools keep working. - Optionally: audit (CADF
audit.http.request/audit.http.response), OAuth2 Bearer, and mTLS (x5t#S256thumbprint).
Conformance: run the 10 scenarios against the Go middleware placed in front of the same echo application, with the same Keystone. Expected: the same statuses and the same headers, except § 5 where the Go version must not let the header through.
5. Security point found: OpenStack-System-Scope not removed
remove_auth_headers removes the X-* identity headers, but **not OpenStack-System-Scope**: _all_auth_headers does not list it (keystonemiddleware/auth_token/_request.py:206-227), although _set_auth_headers writes it for valid tokens (keystonemiddleware/auth_token/_request.py:158-164).
- With a valid token, the value is overwritten by the token's scope.
- With an invalid token and
delay_auth_decision = true, the header sent by the client reaches the service (scenario 6, observed).
oslo.context builds system_scope from HTTP_OPENSTACK_SYSTEM_SCOPE (oslo-context:oslo_context/context.py:53). A service using delay_auth_decision that builds its context **without checking X-Identity-Status** would therefore see system_scope = all for an anonymous request.
- The impact is limited:
X-Rolesis removed, so rules likerole:admin and system_scope:allfail. - A rule based only on
system_scope:allwould pass.
[inferred: impact depends on each service; to report upstream.]
OpenKeel requirement: remove this header like the others.
6. Gaps
| Gap | Impact |
|---|---|
external_oauth2_token, oauth2_token, oauth2_mtls_token and audit not run | requirements read from the code; need an OAuth2 server, mTLS and a bus |
memcached and ENCRYPT/MAC modes not exercised (per-process cache) | same contract, other storage |
expired user token with a service token: allow_expired=1 observed only once, the token was not actually expired | test with a short [token]/expiration in Keystone |
Source: reverse-spec-keystonemiddleware/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.