Secrets Manager development guidelines

GitLab Secrets Manager stores secrets in OpenBao, an open source secrets management engine. GitLab runs OpenBao as a backing service and manages it through a Rails service layer. All Secrets Manager code is GitLab Enterprise Edition (EE) only and lives under ee/. The feature category is secrets_management.

Read the page that matches your task:

If you change behavior that a page describes, update the page in the same merge request.

How it fits together

GitLab does not store secret values in PostgreSQL. Values live in OpenBao. PostgreSQL holds the ProjectSecretsManager and GroupSecretsManager records, which track provisioning state and the OpenBao mount configuration.

Each project or group that has Secrets Manager enabled gets its own OpenBao namespace. For example, a namespace path might look like org_1/group_2/project_1. There is no inheritance between OpenBao namespaces. A token for one namespace can only read that namespace. Cross-level access in CI, for example a project reading a group’s secrets, is handled explicitly by GitLab, not by OpenBao.

GitLab authenticates to OpenBao with JWTs. OpenBao validates these JWTs by fetching the GitLab OpenID Connect (OIDC) discovery document. This means GitLab must be reachable over HTTP for OpenBao operations to work.

Availability gates

Two conditions must both be true before Secrets Manager is available. All checks go through SecretsManagement::Availability at ee/lib/secrets_management/availability.rb.

  • License. The native_secrets_management licensed feature, which is available on Premium and above.
  • Enrollment. On GitLab.com, enrollment is per top-level group (SecretsManagement::NamespaceEnrollment). On GitLab Self-Managed, including the GDK, enrollment is the instance-wide secrets_manager_instance_enrolled application setting, which defaults to false.

The gates are combined with AND. Turning off enrollment makes Secrets Manager unavailable right away. CI jobs picked up after that get no secrets, and jobs that are already running keep the secrets they received.

Entitlement is a separate, additional gate based on trial and billing status. For details, see Entitlement states.

Code map

Use this table to find where a given kind of Secrets Manager code lives.

WhatWhere
Modelsee/app/models/secrets_management/
Servicesee/app/services/secrets_management/
Service concernsee/app/services/concerns/secrets_management/
GraphQL types, mutations, resolversee/app/graphql/{types,mutations,resolvers}/secrets_management/
Workersee/app/workers/secrets_management/
Policiesee/app/policies/secrets_management/
OpenBao client, JWT, availabilityee/lib/secrets_management/
Rake tasksee/lib/tasks/gitlab/secrets_management/
Helpersee/app/helpers/secrets_management/
Frontendee/app/assets/javascripts/ci/secrets/
Backend testsee/spec/{services,models,workers,requests,lib,helpers}/secrets_management/
Frontend testsee/spec/frontend/ci/secrets/

Patterns to know before you write code

These patterns repeat across the Secrets Manager codebase. Knowing them before you write code helps you avoid the most common bugs.

Secrets are not ActiveRecord

Secret objects are ActiveModel::Model instances backed by OpenBao. Do not expect find_by, scopes, or database joins to work on them.

Two authorization layers

A request must pass both the Rails policy check and the OpenBao ACL policy. Passing one is not enough.

Provision before you read or write

The secrets manager record must be in the active state. Services fail if it is still provisioning.

Exclusive leases on writes

Write operations take an exclusive lease and fail immediately rather than retry. Concurrent writes to the same project return an error.

Metadata check-and-set

Updates pass a metadata_cas value that must match the current metadata version. This stops two updates from silently overwriting each other, a pattern known as optimistic concurrency control.

Two-phase writes

Writes are split into a start step and a complete step, so a failure leaves the secret closed rather than half-written.

Sanitize errors

Never pass a raw OpenBao error back to the user. Map it to a safe message instead.

Groups are not projects

Project secrets take a branch parameter, and group secrets take a protected parameter. The two sides also differ in default role grants and in how transfer works. Check the group code path separately when you change project code. For the full list, see Project and group differences.

Deprovision is task-driven and asynchronous

Deleting a secrets manager row fires a database trigger that records a deprovision task. A worker reads the task and runs the OpenBao cleanup. Cleanup can then finish after the parent project or group is already gone.