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_idscontains the expected group ID - JWT claim added to
PipelineJwtclass: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_idsarray validation -
Dynamic policy assignment: Assigns policies based on
ref_protectedandenvironmentclaims
Pipeline CEL validates:
-
project_group_idscontains the expected group ID - Standard claims (aud, sub, scope, etc.)
- Dynamically assigns:
pipelines/{protected|unprotected}/global+ environment-specific policy
User CEL validates:
-
namespace_idmatches 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 (includesproject_idclaim) -
GroupSecretsManagerJwt- For groups (includesnamespace_id, NO project claims) -
UserJwt- For projects (includesproject_idclaim) -
GroupUserJwt- For groups (includesgroups, 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