Skip to content

Allow enabling group secrets manager

What does this MR do and why?

This MR implements the foundational infrastructure for Group-level Secrets Management, enabling groups to provision and manage their own secrets manager instances. This is the first backend implementation in the Group-level Secrets Management epic.

I initially planned to include disabling the secrets manager in this same MR, but with the amount of changes needed because of the overlap with existing project secrets manager, I decided to only do enabling here.

Related to #577340

Architecture & Key Design Decisions

IMPORTANT NOTE: While some of the features added here are not tied to enabling the secrets manager, for example, the user CEL program, I decided to do them here mainly to avoid breaking changes and having to deprovision and re-provision secrets managers in production later on as we add more of the features.

1. Security Model - project_group_ids Validation

Critical Security Feature: Group secrets use project_group_ids claim (from [#563038 (closed)](#479575 (comment 2810775868))) for validating project membership in the group hierarchy.

Why this matters:

  • Fork MR Protection: Prevents fork merge request pipelines from accessing target group secrets
  • Inheritance Validation: Projects can access secrets from their group and all parent groups
  • Top-down Security: Projects in Group B cannot access Group A's secrets (sibling isolation)

Implementation:

  • Pipeline authentication uses CEL (Common Expression Language) to validate project_group_ids contains the expected group ID
  • JWT claim added to PipelineJwt class: project_group_ids: project.group&.self_and_ancestors&.pluck(:id)&.map(&:to_s)

Example:

Root Group (100)
  └─ Subgroup A (200)
      └─ Project (300)

JWT project_group_ids: ["200", "100"]
✅ Can access Subgroup A secrets (200 in list)
✅ Can access Root Group secrets (100 in list)
❌ Cannot access unrelated group secrets

2. Namespace Structure - Flat Hierarchy

Design Decision: All groups within a hierarchy are organized flat under the root group's namespace, not nested.

Structure:

group_123/                    # Root namespace (encryption/org-mover boundary)
├── group_123/               # Root group's secrets
│   ├── secrets/kv/
│   └── pipeline_jwt/
├── group_456/               # Direct child (flat)
│   ├── secrets/kv/
│   └── pipeline_jwt/
└── group_789/               # Grandchild (also flat, not nested!)
    ├── secrets/kv/
    └── pipeline_jwt/

3. Policy Model - Protected/Unprotected Based on Branch Protection + Environment scope

Design Decision: Group secrets support environment scope and protected/unprotected flag based on ref_protected claim (whether the branch is protected), similar to CI variables.

Policy Structure:

pipelines/protected/global              # Protected branches, any environment
pipelines/unprotected/global            # Unprotected branches, any environment
pipelines/protected/env/{hex}           # Protected branches + specific environment
pipelines/unprotected/env/{hex}         # Unprotected branches + specific environment

4. Code Architecture - Refactored for Maintainability

There was a lot of overlap of common behavior between project secrets manager so I had to refactor. We don't cover existing SecretsManagement::Permission model yet here but we might do something similar.

New Structure:

BaseSecretsManager (base class)
  include SecretsManagers::PipelineHelper  # Shared CI related methods
  include SecretsManagers::UserHelper      # Shared user auth methods
  # State machine, server config, common methods

ProjectSecretsManager < BaseSecretsManager
  include ProjectSecretsManagers::PipelineHelper  # CI related methods but specific to project
  include ProjectSecretsManagers::UserHelper      # Project-specific user CEL

GroupSecretsManager < BaseSecretsManager
  include GroupSecretsManagers::PipelineHelper   # CI related methods but specific to group
  include GroupSecretsManagers::UserHelper       # Group-specific user CEL

Benefits:

  • DRY: No duplication between project and group implementations
  • Readable: Methods organized by concern (Pipeline vs User)

5. Authentication - CEL for Both Pipeline and User Auth

Design Decision: Groups use CEL for both pipeline and user authentication, not regular JWT roles.

Why CEL:

  • Complex validation: Required for project_group_ids array validation
  • Dynamic policy assignment: Assigns policies based on ref_protected and environment claims

Pipeline CEL validates:

  • project_group_ids contains the expected group ID
  • Standard claims (aud, sub, scope, etc.)
  • Dynamically assigns: pipelines/{protected|unprotected}/global + environment-specific policy

User CEL validates:

  • namespace_id matches the group
  • User's groups claim contains the target group ID (validates user is a member of the group)
  • Dynamically assigns policies based on user_id, role_id, member_role_id, and group memberships

access_control_spec.rb has been updated to ensure the CEL program works as expected.

6. Service Architecture - Separate Base Services

Created:

  • ProjectBaseService - For all services that work with project related resources
  • GroupBaseService - For all services that work with project related resources

Separate JWTs:

  • SecretsManagerJwt - For projects (includes project_id claim)
  • GroupSecretsManagerJwt - For groups (includes namespace_id, NO project claims)
  • UserJwt - For projects (includes project_id claim)
  • GroupUserJwt - For groups (includes groups, NO project claims)

7. Rails-side Permissions

We are now following the updated convention documented in https://docs.gitlab.com/development/permissions/conventions/

What's NOT in This MR (Comes Later)

To not further increase the size of this MR, the following will be done separately:

  • A separate MR for disabling the project secrets manager
Edited by Erick Bajao

Merge request reports

Loading