Add Secrets Manager direct API access for non-CI/CD workloads
What does this MR do and why?
Adds a beta REST endpoint that lets non-CI/CD workloads read GitLab Secrets Manager secrets, closing the gap where only CI/CD jobs (via Runner JWT) and the UI (via the Rails GraphQL proxy) could reach secrets.
POST /api/v4/projects/:id/secrets_manager/access_token
POST /api/v4/groups/:id/secrets_manager/access_tokenThe endpoint mints a short-lived JWT scoped to the caller and returns the OpenBao
connection details. The client then reads secrets directly from OpenBao, the same
direct-access pattern Runners already use. The response is shaped like
external-secrets.io/v1.VaultProvider so clients (External Secrets Operator,
Terraform, scripts) can consume it with standard Vault-compatible tooling.
The endpoint mints a token; it never reads a secret value. The value read happens later, client-side, against OpenBao.
Example
Request (no body). The token can be a regular PAT, a service account token,
or a group/project access token, as long as the caller is Reporter or above
on the namespace. A standalone granular token also works if it carries the
create_secrets_manager_api_jwt permission. Service accounts use a regular PAT
(granular tokens are not extended to service accounts yet).
curl --request POST \
--header "PRIVATE-TOKEN: <access-token>" \
"https://gitlab.example.com/api/v4/projects/99/secrets_manager/access_token"Response:
{
"expires_at": "2026-06-12T10:35:00Z",
"provider": {
"vault": {
"server": "https://openbao.example.com",
"namespace": "org_5/ns_42/project_99",
"path": "secrets/kv",
"version": "v2",
"auth": {
"jwt": {
"path": "api_jwt",
"role": "all_api",
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
}
}The client takes provider.vault and logs in to OpenBao with the JWT (auth.jwt),
then reads from the KV path. The response is shaped like
external-secrets.io/v1.VaultProvider, so it maps onto a Vault provider config
with the token inlined.
Authorization model and why Reporter access to the endpoint is fine
Access is enforced in two independent layers:
- Minting a token (this endpoint) is gated by a new, dedicated permission
create_secrets_manager_api_jwt, granted to Reporter and above on an SM-enabled namespace. - Reading a secret value is gated separately in OpenBao by the per-principal
read_valuepermission. The minted JWT, presented to the newapi_jwtmount, only attaches a read-only policy for secrets the principal was explicitly grantedread_valueon.
Why Reporter access to minting is fine: the token is useless on its own. A Reporter
(or any caller) with no read_value grant gets a token whose OpenBao policies do not
exist, so every read is denied. Minting gives them nothing. The real check is
read_value, done in OpenBao. This is covered end to end in
ee/spec/requests/secrets_management/api_secret_access_spec.rb (granted reads work;
metadata-only, revoked, and ungranted callers are denied).
We allow minting for Reporter and above because read_value can be granted to any
Reporter+ member, so the mint has to be open to the same people. If we made it stricter,
a Developer who was granted read_value could not get a token to use it. We also cannot
check "has read_value somewhere" when minting, because read_value lives in OpenBao,
not the GitLab database. So the people who can mint are the same ones who already have
:read_project_secrets.
Permission naming
The permission is named for what the endpoint does. It creates a JWT for the api_jwt
OpenBao role, so it is create_secrets_manager_api_jwt, not read_project_secrets
(which would imply reading values the endpoint never performs). The api_jwt suffix
also distinguishes it from the other Secrets Manager JWTs (pipeline_jwt, user_jwt).
It is registered as a granular token (GPAT) permission and assignable on project and
group boundaries.
Maturity and rollout
- Ships as beta (
route_setting :lifecycle, :beta), aligned with SM's Open Beta. - Behind the
secrets_manager_api_accessfeature flag, disabled by default. - Excluded from the OpenAPI spec until general availability.
Databases
No migrations. No schema or query changes. The new OpenBao api_jwt mount and
all_api CEL role are additive and provisioned alongside the existing
pipeline_jwt and user_jwt mounts. Existing SM-enabled namespaces are backfilled
separately (see References).
Backward compatibility
- The endpoint is off by default behind the feature flag.
- The OpenBao change is additive; existing
pipeline_jwtanduser_jwtmounts and roles are untouched. - The existing
readsecret-permission action is kept as a deprecated alias ofread_metadata(accepted on input, still emitted on readback), so the frontend can migrate on its own clock. No data migration.
Testing
- Unit: permission model (path-aware capability mapping,
read_valuevalidation), JWT classes (TTL,apiscope,auth_via). - Integration (
:gitlab_secrets_manager, against a real OpenBao): CEL boundaries for theapi_jwtmount (project and group), provisioning of the mount and role, and the end-to-endread_valuevalue-gating behaviour. - Request: the endpoint contract, authorization (Reporter+ allowed, Guest/non-member denied, FF off, inactive manager), and the granular-token shared example.
- Policy specs for
create_secrets_manager_api_jwton project and group.
Documentation
- New API reference page and a "non-CI/CD access" guide.
- Needs a Technical Writer review before merge.
Deferred (tracked separately)
- "Read value" permission UI and GraphQL exposure (so users can grant it).
- Backfill of the
api_jwtmount/role into existing SM-enabled namespaces. - QA end-to-end spec for the deployed read path.
- Workload identity federation (exchange external OIDC for an SM JWT).
- Usage tracking and billing.