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

Reverse-spec · Services

barbican — REST API

REST API — barbican — branch stable/2026.2

Branch stable/2026.2 @ de32745a7a4f — repository https://opendev.org/openstack/barbican — analysed on 2026-10-29

How routing works (Pecan)

Barbican does not declare routes as data. The paste composite main maps / to the version application, /healthcheck to oslo.middleware and /v1 to the API pipeline (etc/barbican/barbican-api-paste.ini:1). The API application is a pecan.Pecan whose root is V1Controller (barbican/api/app.py:105, barbican/api/app.py:47). Pecan dispatches on objects:

  • an attribute is a URL segment: self.secrets, self.orders… and project-quotas / secret-stores, set with setattr because they are not identifiers (barbican/api/controllers/versions.py:139);
  • _lookup(self, id, *remainder) consumes a dynamic segment, loads the entity and returns the next controller (barbican/api/controllers/secrets.py:358);
  • @pecan.expose(generic=True) def index plus @index.when(method='GET') selects the handler by HTTP method; the generic index itself answers 405 for any other method (barbican/api/controllers/secrets.py:374).

The full table, with the real handler of each method (decorator closures unwrapped), policy rule, accepted Content-Type, body schema and the source line of each tree edge, is extracted by _rewrite/tools/barbican_routes.py into _rewrite/routes_stable-2026.2.json; _rewrite/openapi_stable-2026.2.json is built from it.

Every handler is wrapped, outermost first, by handle_exceptions (exception → status and message, barbican/api/controllers/__init__.py:100), enforce_rbac (policy check, then the caller's project id becomes the first argument, barbican/api/controllers/__init__.py:74) and, for bodies, enforce_content_types (415, barbican/api/controllers/__init__.py:132). Request bodies are read with a size cap and validated by load_body (barbican/api/__init__.py:40).

Common rules

  • Project: the caller's project comes from the token (X-Project-Id set by keystonemiddleware, barbican/api/middleware/context.py:80). The first call of a project creates its projects row (barbican/common/resources.py:34).
  • References: entities are returned as absolute URLs (secret_ref, container_ref, order_ref…), built from [DEFAULT] host_href or, when unset, from the request (Host, X-Forwarded-Proto, path prefix without /v1) (barbican/common/utils.py:67, barbican/common/hrefs.py:65).
  • Paging: offset (default 0, negative → 0) and limit (default default_limit_paging=10, clamped to 1…max_limit_paging=100; non-numeric → default) (barbican/model/repositories.py:244). Lists return total and next/previous URLs when applicable; an empty page returns {"<name>": [], "total": n} without links (barbican/common/hrefs.py:132).
  • Identifiers: secret, container, consumer and transport-key ids must be UUID version 4, otherwise 404 "Not Found. Provided … id is invalid." (barbican/common/utils.py:210). Order ids are not checked: an unknown id gives 404 Order not found. (barbican/api/controllers/orders.py:116).
  • Body: JSON at most max_allowed_request_size_in_bytes (25,000) bytes, read truncated at that size (barbican/api/__init__.py:51, barbican/common/config.py:36); string values are stripped recursively (barbican/api/__init__.py:140).
  • Microversions: header OpenStack-API-Version: key-manager 1.x, parsed by microversion-parse (barbican/api/middleware/microversion.py:30); absent means 1.0. 1.1 adds consumers to secret representations; 1.2 refuses to delete a secret that has consumers unless ?force=true (doc/source/api/microversion_history.rst:22).

Operations

Statuses in the last column were observed in the functional capture (_rewrite/exchanges-functional_stable-2026.2.json) or the API unit tests (_rewrite/exchanges-api-tests_stable-2026.2.json); 401 applies to every /v1 call without a valid token (keystonemiddleware) and is not repeated.

MethodPathHandlerPolicySuccessErrors (observed or by code)
GET/barbican/api/controllers/versions.py:182—300406
GET/v1barbican/api/controllers/versions.py:156—200406
GET/healthcheckoslo.middleware (etc/barbican/barbican-api-paste.ini:59)—200503
GET/v1/secretsbarbican/api/controllers/secrets.py:380secrets:get200400, 403
POST/v1/secretsbarbican/api/controllers/secrets.py:473secrets:post201400, 403, 404, 413, 415
GET/v1/secrets/{secret_id}barbican/api/controllers/secrets.py:117secret:get (secret:decrypt if Accept ≠ JSON)200403, 404, 406
PUT/v1/secrets/{secret_id}barbican/api/controllers/secrets.py:221secret:put204400, 403, 404, 409, 413, 415
DELETE/v1/secrets/{secret_id}barbican/api/controllers/secrets.py:258secret:delete204400, 403, 404
GET/v1/secrets/{secret_id}/payloadbarbican/api/controllers/secrets.py:203secret:decrypt200400, 403, 404, 405, 406
GET/v1/secrets/{secret_id}/aclbarbican/api/controllers/acls.py:62secret_acls:get200403, 404
PATCH/v1/secrets/{secret_id}/aclbarbican/api/controllers/acls.py:72secret_acls:put_patch200400, 403, 404, 415
PUT/v1/secrets/{secret_id}/aclbarbican/api/controllers/acls.py:124secret_acls:put_patch200400, 403, 404, 415
DELETE/v1/secrets/{secret_id}/aclbarbican/api/controllers/acls.py:186secret_acls:delete200403, 404
GET/v1/secrets/{secret_id}/metadatabarbican/api/controllers/secretmeta.py:52secret_meta:get200403, 404
PUT/v1/secrets/{secret_id}/metadatabarbican/api/controllers/secretmeta.py:67secret_meta:put201400, 403, 404, 415
POST/v1/secrets/{secret_id}/metadatabarbican/api/controllers/secretmeta.py:85secret_meta:post201400, 403, 404, 409, 415
GET/v1/secrets/{secret_id}/metadata/{key}barbican/api/controllers/secretmeta.py:126secret_meta:get200403, 404
PUT/v1/secrets/{secret_id}/metadata/{key}barbican/api/controllers/secretmeta.py:145secret_meta:put200400, 403, 404, 409, 415
DELETE/v1/secrets/{secret_id}/metadata/{key}barbican/api/controllers/secretmeta.py:172secret_meta:delete204403, 404
GET/v1/secrets/{secret_id}/consumersbarbican/api/controllers/consumers.py:279secret_consumers:get200403, 404
POST/v1/secrets/{secret_id}/consumersbarbican/api/controllers/consumers.py:318secret_consumers:post200400, 403, 404, 415
DELETE/v1/secrets/{secret_id}/consumersbarbican/api/controllers/consumers.py:347secret_consumers:delete200400, 403, 404, 415
GET/v1/secrets/{secret_id}/consumers/{consumer_id}barbican/api/controllers/consumers.py:235consumer:get200403, 404
GET/v1/containersbarbican/api/controllers/containers.py:139containers:get200403
POST/v1/containersbarbican/api/controllers/containers.py:181containers:post201400, 403, 404, 415
GET/v1/containers/{container_id}barbican/api/controllers/containers.py:65container:get200403, 404
DELETE/v1/containers/{container_id}barbican/api/controllers/containers.py:81container:delete204403, 404
POST/v1/containers/{container_id}/secretsbarbican/api/controllers/containers.py:243container_secret:post201400, 403, 404, 409, 415
DELETE/v1/containers/{container_id}/secretsbarbican/api/controllers/containers.py:294container_secret:delete204400, 403, 404
GET/PATCH/PUT/DELETE/v1/containers/{container_id}/aclbarbican/api/controllers/acls.py:223 … barbican/api/controllers/acls.py:351container_acls:get / :put_patch / :delete200400, 403, 404, 415
GET/v1/containers/{container_id}/consumersbarbican/api/controllers/consumers.py:109container_consumers:get200403, 404
POST/v1/containers/{container_id}/consumersbarbican/api/controllers/consumers.py:148container_consumers:post200400, 403, 404, 415
DELETE/v1/containers/{container_id}/consumersbarbican/api/controllers/consumers.py:173container_consumers:delete200400, 403, 404, 415
GET/v1/containers/{container_id}/consumers/{consumer_id}barbican/api/controllers/consumers.py:64consumer:get200403, 404
GET/v1/ordersbarbican/api/controllers/orders.py:128orders:get200403
POST/v1/ordersbarbican/api/controllers/orders.py:163orders:post202400, 403, 415
PUT/v1/ordersbarbican/api/controllers/orders.py:156orders:put—405 always
GET/v1/orders/{order_id}barbican/api/controllers/orders.py:80order:get200403, 404
DELETE/v1/orders/{order_id}barbican/api/controllers/orders.py:87order:delete204403, 404
GET/v1/transport_keysbarbican/api/controllers/transportkeys.py:104transport_keys:get200403
POST/v1/transport_keysbarbican/api/controllers/transportkeys.py:143transport_keys:post201400, 403, 415
GET/v1/transport_keys/{transport_key_id}barbican/api/controllers/transportkeys.py:57transport_key:get200403, 404
DELETE/v1/transport_keys/{transport_key_id}barbican/api/controllers/transportkeys.py:69transport_key:delete204403, 404
GET/v1/quotasbarbican/api/controllers/quotas.py:50quotas:get200403
GET/v1/project-quotasbarbican/api/controllers/quotas.py:133project_quotas:get200403
GET/v1/project-quotas/{project_id}barbican/api/controllers/quotas.py:75project_quotas:get200403, 404
PUT/v1/project-quotas/{project_id}barbican/api/controllers/quotas.py:87project_quotas:put204400, 403, 415
DELETE/v1/project-quotas/{project_id}barbican/api/controllers/quotas.py:102project_quotas:delete204403, 404
GET/v1/secret-storesbarbican/api/controllers/secretstores.py:165secretstores:get200403, 404
any/v1/secret-stores/global-defaultbarbican/api/controllers/secretstores.py:187secretstores:get_global_default200403, 404
any/v1/secret-stores/preferredbarbican/api/controllers/secretstores.py:202secretstores:get_preferred200403, 404
GET/v1/secret-stores/{secret_store_id}barbican/api/controllers/secretstores.py:124secretstore:get200403, 404, 405
POST/v1/secret-stores/{secret_store_id}/preferredbarbican/api/controllers/secretstores.py:90secretstore_preferred:post204403, 404
DELETE/v1/secret-stores/{secret_store_id}/preferredbarbican/api/controllers/secretstores.py:71secretstore_preferred:delete204403, 404

Undocumented in doc/source/api/reference (_rewrite/api-ref-gaps_stable-2026.2.json): /, /v1, /healthcheck, the four transport-key operations, GET …/consumers/{consumer_id} (secret and container), and PUT /v1/orders.

Versions — GET / and GET /v1

Purpose: version discovery for keystoneauth and the clients. Actor: anyone; the / pipeline has no authentication (etc/barbican/barbican-api-paste.ini:8).

  • GET / always answers 300. With microversion 1.0 (no header) the body is the legacy {"versions": {"values": [{"id": "v1", "status": "stable", "links": […], "media-types": […]}]}}; with 1.1+ it is {"versions": [{"id": "v1", "status": "CURRENT", "min_version": "1.0", "max_version": "1.2", "links": […]}]} (barbican/api/controllers/versions.py:189, barbican/api/controllers/versions.py:94). python-barbicanclient negotiates by sending key-manager 1.1 here (python-barbicanclient:barbicanclient/v1/client.py:131).
  • GET /?build returns {"build": "<version>"} (barbican/api/controllers/versions.py:186).
  • Accept: application/json-home redirects (302) to /v1/ (barbican/api/controllers/versions.py:209). Other Accept values than JSON / JSON-home give 406 (Pecan content negotiation, barbican/api/controllers/versions.py:181).
  • The updated field of the legacy representation is never set: version['updated']: cls.last_updated is an annotation, not an assignment (barbican/api/controllers/versions.py:116).

Secrets

POST /v1/secrets — store a secret (one or two steps)

Purpose: create a secret, with or without its payload. Actor: project member (creator/admin in the legacy roles). Preconditions: quota secrets not reached (barbican/api/controllers/secrets.py:480); Content-Type: application/json (415 otherwise, barbican/api/controllers/secrets.py:472).

Business rules (barbican/common/validators.py:139):

  • name ≤ 255, algorithm, mode, bit_length (integer ≥ 0 [see schema]), secret_type ∈ symmetric, public, private, passphrase, certificate, opaque (default opaque), expiration ISO 8601 in the future (barbican/common/validators.py:254).
  • payload given ⇒ payload_content_type mandatory and one of text/plain, text/plain;charset=utf-8, application/octet-stream, application/pkcs8 (barbican/plugin/util/mime_types.py:25). Binary types require payload_content_encoding: "base64" and a decodable payload; text/plain forbids an encoding (barbican/plugin/util/mime_types.py:58, barbican/plugin/util/translations.py:62).
  • payload larger than max_allowed_secret_in_bytes (20,000) → 413 (barbican/common/validators.py:44).
  • payload_content_type without payload → 400 (only allowed inside an order, barbican/common/validators.py:213).
  • creator_id is forced to the token user (barbican/api/controllers/secrets.py:486).
  • transport_key_needed: "true" with no payload asks for a transport key reference in the answer (two-step upload with client-side wrapping, barbican/plugin/resources.py:83).

Flow (synchronous): validate → create/find project → quota → pick a secret-store plugin that store_secret_supports(key_spec) (barbican/plugin/interface/secret_store.py:573) → normalise the payload to base64 (barbican/plugin/util/translations.py:25) → store (for store_crypto: find or create the project KEK, encrypt, insert encrypted_data; 07 § 3) → insert secrets and secret_store_metadata (plugin_name, content_type) (barbican/plugin/resources.py:338).

Outcome: 201, Location and {"secret_ref": "<url>"} (+ transport_key_ref) (barbican/api/controllers/secrets.py:503). Status ACTIVE immediately (barbican/model/models.py:377).

GET /v1/secrets — list

Filters (barbican/api/controllers/secrets.py:393, barbican/model/repositories.py:590): name, alg, mode (SQL LIKE, % wildcards pass through), bits (non-numeric ignored), secret_type, created / updated / expiration ([gt|gte|lt|lte:]ISO-date, comma-separated, contradictory combinations → 400, barbican/api/controllers/secrets.py:303), sort (key[:asc|desc] among algorithm, bit_length, created, expiration, mode, name, secret_type, status, updated, barbican/api/controllers/secrets.py:335), acl_only=true (secrets whose ACL lists the user, any project, barbican/model/repositories.py:640). Expired and deleted secrets are never listed (barbican/model/repositories.py:611). Items carry content_types ({"default": "<type>"}), and from 1.1 consumers.

GET /v1/secrets/{id} — metadata (or deprecated payload)

With Accept: application/json (or none, or */*) returns the secret's fields: secret_ref, name, secret_type, status, algorithm, bit_length, mode, expiration, creator_id, created, updated, content_types and, from 1.1, consumers (barbican/api/controllers/secrets.py:134, barbican/model/models.py:403). ?transport_key_needed=true adds transport_key_id when the store supports transport keys (barbican/api/controllers/secrets.py:149). Any other Accept returns the decrypted payload, as /payload does, but logs a deprecation warning (barbican/api/controllers/secrets.py:128).

GET /v1/secrets/{id}/payload — decrypt

Purpose: return the secret in clear. Actor: secret:decrypt (the legacy audit role cannot). Rules: Accept absent → application/octet-stream; */* → the stored content type; an unsupported Accept → 406 Wrong payload content-type (barbican/api/controllers/secrets.py:165, barbican/plugin/resources.py:123). text/plain returns UTF-8 text (406 if the bytes are not UTF-8), binary types return the raw bytes (barbican/plugin/util/translations.py:89). No payload yet → 404 Not Found. Sorry but your secret has no payload. (barbican/api/controllers/secrets.py:173). Optional transport-key wrapping: trans_wrapped_session_key + transport_key_id query parameters (barbican/api/controllers/secrets.py:176). Any method other than GET → 405 (barbican/api/controllers/secrets.py:204).

PUT /v1/secrets/{id} — second step: upload the payload

Content-Type must be text/plain or application/octet-stream (415 otherwise, including JSON); body = raw payload (Content-Encoding: base64 for base64 text); empty → 400 No data supplied to process.; too big → 413; the secret already has data → 409 Secret already has data, cannot modify it. (barbican/api/controllers/secrets.py:221). Answer 204.

DELETE /v1/secrets/{id}

From 1.2, refuses with 400 Secret cannot be deleted as it has consumers. unless force is a true boolean string (true, 1, yes…; anything else → 400) (barbican/api/controllers/secrets.py:264). Deletes the external-store object if any (barbican/plugin/resources.py:251), soft-deletes the secret and its children (store metadata, user metadata, encrypted data, container links; ACLs are hard-deleted) (barbican/model/models.py:379), then deletes the consumer rows (barbican/api/controllers/secrets.py:285). The project used for the deletion is the secret's project, not the caller's (barbican/api/controllers/secrets.py:281).

stateDiagram-v2
  [*] --> ACTIVE_no_payload: POST /v1/secrets without payload
  [*] --> ACTIVE: POST /v1/secrets with payload / order completed
  ACTIVE_no_payload --> ACTIVE: PUT /v1/secrets/{id}
  ACTIVE --> expired: expiration reached (hidden from GET/list)
  ACTIVE --> deleted: DELETE (soft delete; 1.2 needs no consumers or force)
  expired --> deleted: barbican-manage db clean --soft-delete-expired-secrets
  deleted --> [*]: barbican-manage db clean (hard delete after min-days)

Secret metadata (user key/value pairs)

  • GET …/metadata → {"metadata": {k: v}}; PUT …/metadata replaces all pairs ({"metadata": {…}}, values strings) → 201 {"metadata_ref": …} (barbican/api/controllers/secretmeta.py:67).
  • POST …/metadata {"key", "value"} adds one pair; existing key → 409; key must match ^[A-Za-z0-9_-]*$ (400 Invalid Key. Key must be URL safe.) and is lower-cased (barbican/common/validators.py:400). Answer 201, Location: …/metadata/<key>.
  • PUT …/metadata/{key} requires the body key to equal the URL key (409, barbican/api/controllers/secretmeta.py:155); unknown key → 404 Secret metadata not found.; DELETE …/metadata/{key} → 204, also for a key that does not exist (bulk DELETE … WHERE key=… without a check, barbican/model/repositories.py:898).

Secret consumers (from 1.1 in representations)

POST …/consumers {"service", "resource_type", "resource_id"} registers a use; it is idempotent (an existing, even soft-deleted, triple is revived, barbican/model/repositories.py:2517) and counts against the consumers quota. It answers 200 with the secret representation (not 201) (barbican/api/controllers/consumers.py:318). DELETE …/consumers with the same body removes it; only the consumer's project or the secret's project may do it (403, barbican/api/controllers/consumers.py:366). Unknown triple → 404 Consumer not found.

ACLs (secrets and containers)

Format: {"read": {"users": [...], "project-access": true|false}}; only read is accepted by the schema (barbican/common/validators.py:569). GET returns the ACLs with created/updated, or the implicit default {"read": {"project-access": true}} (barbican/api/controllers/acls.py:42). PUT replaces (missing project-access → true, operations not given are deleted), PATCH merges (only given fields change); both answer 200 {"acl_ref": "<secret_ref>/acl"} (barbican/api/controllers/acls.py:72, barbican/api/controllers/acls.py:124). DELETE removes all ACLs → 200 (barbican/api/controllers/acls.py:186). project-access: false makes the entity private: only its creator and the listed users can read it (policy secret_non_private_read, barbican/common/policies/base.py:106). The ACL data feeding the policy is built per request (barbican/api/controllers/__init__.py:198).

Containers

POST /v1/containers {"name", "type": generic|rsa|certificate, "secret_refs": [{"name", "secret_ref"}]} (barbican/common/validators.py:648): reference names unique, secret ids unique, every secret_ref must start with this service's base URL (400 … does not match the configured hostname, barbican/common/validators.py:713); rsa allows public_key, private_key (required) and private_key_passphrase; certificate requires certificate and allows private_key, private_key_passphrase, intermediates (barbican/common/validators.py:732). Each secret must exist in the caller's project (404 Secret provided for '<name>' doesn't exist., barbican/api/controllers/containers.py:198). Quota containers. Answer 201 {"container_ref"}.

  • GET returns container_ref, name, type, status, secret_refs[{name, secret_ref}], consumers[{name, URL}], creator_id, created, updated (barbican/model/models.py:878).
  • DELETE soft-deletes the container and deletes its consumer rows (barbican/api/controllers/containers.py:81). PUT → 405.
  • POST/DELETE …/secrets add or remove a {name, secret_ref} link, only for generic containers (400, barbican/api/controllers/containers.py:246); duplicate → 409.
  • Container consumers: {"name", "URL"}, same idempotency and ownership rules as secret consumers; the answer is the container representation with 200 and a wrong Location header (…/v1/consumers/<container_id>/consumers, barbican/common/hrefs.py:46).

Orders — asynchronous key generation

Purpose: ask Barbican to generate a symmetric key (type: key) or an RSA/DSA key pair (type: asymmetric). Body {"type", "meta": {"name", "algorithm", "bit_length", "mode", "payload_content_type", "expiration", "passphrase"}} (barbican/common/validators.py:420): algorithm and bit_length required; bit_length multiple of 8 (EC: 256/384/521); payload forbidden; certificate orders → 400 Feature not implemented … (barbican/common/validators.py:560).

Flow: quota orders → insert order PENDING → commit → cast process_type_order(order_id, project_id, request_id) (barbican/api/controllers/orders.py:197) → 202 {"order_ref"}. The worker (or the API itself when [queue] enable=False) generates the key through the secret-store plugin (barbican/tasks/resources.py:245): a key order creates one secret; an asymmetric order creates private, public and optional passphrase secrets plus an rsa/dsa container (barbican/plugin/resources.py:191). Then the order becomes ACTIVE with secret_ref or container_ref, or ERROR with error_status_code and error_reason (barbican/tasks/resources.py:189).

stateDiagram-v2
  [*] --> PENDING: POST /v1/orders (202)
  PENDING --> ACTIVE: worker generated the secret(s)
  PENDING --> ERROR: generation failed (error_status_code, error_reason)
  PENDING --> PENDING: retry scheduled (order_retry_tasks; unused by key/asymmetric)
  ACTIVE --> deleted: DELETE /v1/orders/{id} (generated secrets stay)
  ERROR --> deleted: DELETE /v1/orders/{id}

GET /v1/orders?meta=… filters with a substring match on the serialized JSON meta column (LIKE '%…%', barbican/model/repositories.py:1073). Order lookup uses the caller's project: another project's order is 404 even for an admin (barbican/api/controllers/orders.py:113).

Transport keys

Used with plugins that support client-side wrapping (Dogtag). POST /v1/transport_keys {"plugin_name", "transport_key"} (admin, 201); GET lists references (?plugin_name=), GET/{id} returns the stored key as a raw string with Content-Type: application/json (barbican/api/controllers/transportkeys.py:63); DELETE/{id} → 204.

Quotas

  • GET /v1/quotas → effective quotas of the caller's project {"quotas": {"secrets", "orders", "containers", "consumers", "cas"}}: project values, falling back to [quotas] quota_* (default -1 = unlimited) (barbican/common/quota.py:76).
  • /v1/project-quotas[/{project_id}] (service admin): list {"project_quotas": [{"project_id", "project_quotas": {…}}], "total"}, get one (404 Project quotas not found. when none configured), PUT {"project_quotas": {"secrets": n, …}} → 204 (body read by Pecan into kwargs, barbican/api/controllers/quotas.py:93), DELETE → 204. Any project id string is accepted and auto-creates a projects row (barbican/common/quota.py:107). Value semantics: <0 unlimited, 0 disabled, n cap (barbican/common/quota.py:178).

Secret stores (multiple back ends)

Only with [secretstore] enable_multiple_secret_stores = True; otherwise every operation answers 404 Not Found. Multiple backends support is not enabled … (barbican/api/controllers/secretstores.py:168). Stores are configured in [secretstore:<suffix>] sections (secret_store_plugin, crypto_plugin, global_default) and synchronised to the secret_stores table at API start (barbican/plugin/util/multiple_backends.py:106). A project may set a preferred store (POST …/{id}/preferred, 204); new secrets of the project then go to that store (barbican/plugin/util/multiple_backends.py:262). Representation: {"secret_store_ref", "name", "secret_store_plugin", "crypto_plugin", "global_default", "status", "created", "updated"} (barbican/api/controllers/secretstores.py:45).

Source: reverse-spec-barbican/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.