Secrets Manager OpenBao 클라이언트와 인증
GitLab v19.5요약
GitLab은 SecretsManagement::SecretsManagerClient를 통해 HTTP로 OpenBao와 통신합니다. 두 가지 인가 계층(Rails 정책과 OpenBao ACL)과 OpenBao 네임스페이스 구성은 Secrets Manager 개발 가이드라인을 참고합니다.
GitLab은 SecretsManagement::SecretsManagerClient를 통해 HTTP로 OpenBao와 통신합니다.
모든 요청은 GitLab이 직접 서명한 JWT를 함께 보냅니다.
OpenBao는 CEL(Common Expression Language) 프로그램으로 각 JWT를 검사하고, 클레임을 해당 요청에 대한 ACL 정책 집합으로 변환합니다.
두 가지 인가 계층(Rails 정책과 OpenBao ACL)과 OpenBao 네임스페이스 구성은 Secrets Manager 개발 가이드라인을 참고합니다.
SecretsManagerClient#
파일: ee/lib/secrets_management/secrets_manager_client.rb
클라이언트는 Faraday를 감싸며 미리 인증하지 않습니다. 모든 요청이 OpenBao의 인라인 인증 헤더를 포함하므로, 각 호출이 한 번의 왕복으로 인증하고 실행합니다.
X-Vault-Inline-Auth-Path: 사용할 로그인 경로. 예를 들어auth/user_jwt/cel/login입니다.X-Vault-Inline-Auth-Parameter-token: base64로 인코딩한 JWT.X-Vault-Inline-Auth-Parameter-role: base64로 인코딩한 역할 이름(비어 있으면 생략).
SecretsManagerClient.new는 다음을 받습니다.
| 매개변수 | 기본값 | 용도 |
|---|---|---|
jwt |
없음 | 인라인 인증으로 보낼 인코딩된 JWT. 필수입니다. |
role |
'app' |
인증할 때 사용하는 JWT 역할. |
auth_namespace |
"" |
로그인 경로의 기준이 되는 OpenBao 네임스페이스. |
auth_mount |
'gitlab_rails_jwt' |
인증 대상 JWT 인증 마운트. |
use_cel_auth |
false |
login 대신 마운트의 cel/login 경로에서 인증합니다. |
namespace |
"" |
데이터 플레인 요청의 기준이 되는 OpenBao 네임스페이스. |
timeout |
nil |
요청별 연결 및 읽기 타임아웃(초). |
클라이언트 호스트와 기본 경로는 ee/config/initializers/secrets_manager.rb에서 설정하는 SecretsManagerClient.configure에서 가져옵니다.
네임스페이스 지정#
with_namespace는 다른 OpenBao 네임스페이스로 범위가 지정된 새 클라이언트를 반환합니다.
with_auth_namespace는 작업하는 네임스페이스와 다른 네임스페이스에 로그인하는 클라이언트를 반환합니다.
client = SecretsManagerClient.new(jwt: jwt)
org_client = client.with_namespace(secrets_manager.org_path)
project_client = client.with_namespace(secrets_manager.full_project_namespace_path)
with_namespace는 클라이언트의 네임스페이스에 덧붙이지 않고 교체하므로, 항상 루트부터 슬래시로 연결한 전체 경로를 전달합니다.
클라이언트 생성 패턴 두 가지#
- 권한이 높은 관리 클라이언트:
GlobalSecretsManagerJwt하위 클래스로 만들며 기본값인role: 'app'과auth_mount: 'gitlab_rails_jwt'를 사용한 다음, 작업 대상인 org, 루트 또는 엔티티 수준 네임스페이스의 전체 경로로with_namespace범위를 지정합니다.ProjectBaseService#base_secrets_manager_client와GroupBaseService#base_secrets_manager_client가 이 클라이언트를 만듭니다. - 특정 마운트 하나로 범위가 지정된 CEL 클라이언트:
*UserJwt,*ApiJwt또는PipelineJwt로 만들며 해당 마운트의role,auth_mount,auth_namespace를 명시적으로 전달하고use_cel_auth: true를 사용합니다.ProjectBaseService#user_client가 한 예입니다.
메서드 그룹#
| 그룹 | 메서드 |
|---|---|
| 네임스페이스 관리 | enable_namespace, disable_namespace |
| 시크릿 엔진 관리 | enable_secrets_engine, disable_secrets_engine |
| 인증 엔진 관리 | enable_auth_engine, disable_auth_engine, configure_jwt |
| JWT 역할 구성 | update_gitlab_rails_jwt_role, update_jwt_role, read_jwt_role, delete_jwt_role |
| CEL 역할 구성 | update_jwt_cel_role, read_jwt_cel_role, delete_jwt_cel_role, cel_login_jwt |
| 시크릿 값과 메타데이터 | update_kv_secret, update_kv_secret_metadata, read_secret_metadata, delete_kv_secret, list_secrets, count_secrets |
| ACL 정책 | get_policy, set_policy, delete_policy, list_policies, list_project_policies |
| 기능 해석 | capabilities_self |
| 복구 및 상태 | init_rotate_recovery, cancel_rotate_recovery, server_available?, generate_root_token_status, init_generate_root_token, update_generate_root_token, cancel_generate_root_token |
capabilities_self는 인라인 인증을 사용하지 않는 유일한 메서드입니다.
sys/capabilities-self는 저장된 토큰을 조회하여 호출자를 해석하지만, 인라인 인증 토큰은 토큰 저장소에 기록되지 않습니다.
그래서 클라이언트는 먼저 명시적으로 인증하고(login_with_jwt), 그 결과 토큰을 X-Vault-Token으로 사용해 capabilities-self를 호출한 다음 그 토큰을 폐기합니다.
오류 유형#
클라이언트는 OpenBao나 연결이 반환한 내용에 따라 네 가지 오류 클래스 중 하나를 발생시킵니다.
| 클래스 | 발생 조건 |
|---|---|
ApiError |
errors 배열이 있는 응답 본문, 또는 OpenBao의 raise_error 미들웨어가 클라이언트 오류로 바꾸는 4xx. |
AuthenticationError |
인라인 인증 실패. 항상 별도의 상태 코드로 나타나는 것이 아니라 X-Vault-Inline-Auth-Failed 응답 헤더로 알립니다. |
ServiceUnavailableError |
5xx 응답 또는 요청 타임아웃. |
ConnectionError |
그 밖의 모든 Faraday 연결 실패. |
JWT 클래스 계층#
Secrets Manager는 상황마다 다른 JWT 하위 클래스를 발급하며, 다음 계층으로 구성됩니다.
디렉터리: ee/lib/secrets_management/
Gitlab::Ci::JwtBase
GlobalSecretsManagerJwt (privileged, system-level)
ProjectSecretsManagerJwt
ProjectUserJwt
ProjectApiJwt
GroupSecretsManagerJwt
GroupUserJwt
GroupApiJwt
Gitlab::Ci::JwtV2
PipelineJwt
| 클래스 | 파일 | 사용 용도 | 구별 클레임 |
|---|---|---|---|
GlobalSecretsManagerJwt |
global_secrets_manager_jwt.rb |
프로젝트나 그룹으로 범위가 지정되지 않은 시스템 수준 작업. 예를 들어 복구 키 생성. | sub: 'gitlab_secrets_manager', secrets_manager_scope: 'privileged'. TTL 30초. |
ProjectSecretsManagerJwt |
project_secrets_manager_jwt.rb |
프로젝트 범위의 권한이 높은 작업(프로비저닝, 관리 클라이언트)을 위한 기본 클래스. | JSONWebToken::UserProjectTokenClaims로 프로젝트 및 사용자 감사 클레임을 추가합니다. |
ProjectUserJwt |
project_user_jwt.rb |
UI와 GraphQL을 통한 시크릿 CRUD. | sub: "user:{username}", secrets_manager_scope: 'user', role_id, member_role_id. |
ProjectApiJwt |
project_api_jwt.rb |
프로젝트 시크릿에 대한 비 CI API 액세스. | ProjectUserJwt와 같은 클레임에 secrets_manager_scope: 'api'와 auth_via(API 호출에 사용한 토큰 유형)를 더합니다. |
GroupSecretsManagerJwt |
group_secrets_manager_jwt.rb |
그룹 범위의 권한이 높은 작업을 위한 기본 클래스. | group_id, group_path, root_group_id, organization_id, organization_path. |
GroupUserJwt |
group_user_jwt.rb |
UI와 GraphQL을 통한 그룹의 시크릿 CRUD. | ProjectUserJwt와 같은 형태. |
GroupApiJwt |
group_api_jwt.rb |
그룹 시크릿에 대한 비 CI API 액세스. | ProjectApiJwt와 같은 형태. |
PipelineJwt |
pipeline_jwt.rb |
파이프라인 job 중에 시크릿을 가져오는 CI 러너. | secrets_manager_scope: 'pipeline', project_group_ids(그룹 수준 시크릿 액세스를 위한 프로젝트의 그룹 상위 계층). |
role_id는 사용자의 유효 액세스 수준이며, 프로젝트에서는 max_member_access_for_project로, 그룹에서는 max_member_access_for_group(공유를 고려하며 only_concrete_membership: true)으로 읽습니다.
member_role_id는 프로젝트나 그룹의 상위 계층을 따라가며 가장 가까운 사용자 지정 권한 할당을 찾습니다.
PipelineJwt는 GlobalSecretsManagerJwt 계열이 아니라 Gitlab::Ci::JwtV2를 확장하며 verify_path_not_burned!를 아무 동작도 하지 않도록 재정의합니다. OpenBao가 토큰 subject 접두사가 아니라 project_id와 project_group_ids 클레임으로 프로젝트 ID를 검증하기 때문입니다.
인증 마운트#
프로비저닝된 각 Secrets Manager 네임스페이스에는 JWT 인증 마운트가 세 개 있으며, 모두 CEL 역할로 인증합니다.
| 마운트 | 역할 | 사용처 | 해석되는 정책 |
|---|---|---|---|
pipeline_jwt |
all_pipelines |
CI 러너(PipelineJwt) |
pipelines/global, 그리고 환경, 브랜치 또는 결합 정책. |
user_jwt |
all_users |
UI 및 GraphQL CRUD(*UserJwt) |
users/direct/user_{id}, users/direct/member_role_{id}, users/roles/{access_level}. |
api_jwt |
all_api |
비 CI API 액세스(*ApiJwt) |
user_jwt와 같은 주체 정책이며 api/ 접두사 아래에 있고 읽기 전용입니다. |
네 번째 마운트인 gitlab_rails_jwt(역할 app)는 클라이언트의 기본값이며, CEL로 검증되는 주체가 아니라 권한이 높은 관리 작업(프로비저닝, 네임스페이스 및 정책 관리)에 사용합니다.
CEL 마운트 세 개는 프로비저닝 중에 모두 같은 방식으로 구성됩니다(ProjectSecretsManagers::ProvisionService#enable_auth이며 그룹에도 동일하게 구현). 인증 엔진을 활성화하고, configure_jwt를 호출해 GitLab OIDC 발급자를 지정한 다음, 해당 범위의 CEL 프로그램으로 update_jwt_cel_role을 호출합니다.
ApiAuthConfigurator가 api_jwt 마운트에 대한 이 로직을 담당합니다.
프로비저닝 흐름과 API 마운트 백필 태스크가 모두 이를 호출하므로 둘이 서로 어긋날 수 없습니다.
CEL 프로그램#
CEL 프로그램은 ee/app/models/secrets_management/{project,group}_secrets_managers/ 아래의 모델 concern이 범위(프로젝트 또는 그룹)별로 생성합니다.
| Concern | 메서드 | 마운트 |
|---|---|---|
PipelineHelper |
pipeline_auth_cel_program |
pipeline_jwt |
UserHelper |
user_auth_cel_program |
user_jwt |
ApiHelper |
api_auth_cel_program |
api_jwt |
각 프로그램은 CEL variables의 해시와 마지막 expression으로 이루어집니다.
표현식은 클레임(subject 접두사, secrets_manager_scope, 프로젝트 또는 그룹 ID, audience)을 검증하고, 오류 문자열이나 연결할 OpenBao 정책과 기록할 메타데이터를 나열한 pb.Auth 객체를 반환합니다.
정책 할당은 범위에 따라 다릅니다.
- 파이프라인 CEL(프로젝트): 항상
pipelines/global이고, 해당 클레임이 설정되어 있으면pipelines/env/{hex(environment)},pipelines/branch/{hex(ref)},pipelines/combined/env/{hex}/branch/{hex}가 추가됩니다. - 파이프라인 CEL(그룹): 그룹 수준 CI 액세스는 특정 브랜치 하나로 범위가 지정되지 않으므로, 브랜치 대신 보호 수준으로 이름을 붙인 정책을
ci_policy_name_for_environment(environment, protected:)로 만듭니다. - 사용자 및 API CEL(프로젝트와 그룹):
users/direct/user_{id}(또는api/users/direct/user_{id}),users/direct/member_role_{id},users/roles/{access_level}이며, 클레임에 있는uid,mrid,rid중 존재하는 것으로 만듭니다.
사용자 및 API CEL 프로그램은 선택 사항인 secrets_manager_token_ttl 클레임(초)도 읽어 발급하는 OpenBao 토큰의 리스를 설정합니다.
클레임이 없거나 null이거나 양수가 아니면 기본값은 5분입니다.
ACL 정책#
파일: ee/lib/secrets_management/acl_policy.rb, ee/lib/secrets_management/acl_policy_path.rb
두 클래스가 Ruby에서 OpenBao ACL 정책을 모델링합니다.
AclPolicy는 정책 name과 경로에서 AclPolicyPath로의 맵을 가집니다.
AclPolicyPath는 capabilities(집합이며 예를 들어 create, read, update, delete, list)와 선택 사항인 allowed_parameters, denied_parameters, required_parameters, granted_by(OpenBao 주석 필드이며 사용자 ID로 설정), expired_at을 가집니다.
AclPolicy#to_openbao_attributes는 OpenBao의 ACL 정책 API가 기대하는 JSON 형태로 직렬화합니다.
SecretsManagerClient#get_policy와 #set_policy가 이름이 지정된 정책을 읽고 씁니다.
정책 이름 지정#
| 메서드 | 위치 | 생성되는 이름 |
|---|---|---|
policy_name_for_principal(principal_type:, principal_id:) |
SecretsManagers::UserHelper |
관리(UI) 마운트용 users/direct/user_{id}, users/direct/member_role_{id} 또는 users/roles/{access_level}. |
api_policy_name_for_principal(principal_type:, principal_id:) |
SecretsManagers::ApiHelper |
읽기 전용 API 마운트용으로, 같은 이름 앞에 api/를 붙인 이름. |
ci_policy_name(environment, branch)와 _global, _env, _branch, _combined 변형 |
ProjectSecretsManagers::PipelineHelper |
pipelines/global, pipelines/env/{hex}, pipelines/branch/{hex} 또는 pipelines/combined/env/{hex}/branch/{hex}. |
ci_policy_name_for_environment(environment, protected:) |
GroupSecretsManagers::PipelineHelper |
pipelines/combined/{protected or unprotected}/{global or env/{hex}}. |
OpenBao 정책 이름에는 임의의 문자를 안전하게 넣을 수 없으므로, 환경과 브랜치 값은 정책 이름에 들어가기 전에 BaseSecretsManager#hex(value.unpack1('H*'))로 16진수 인코딩합니다.
정책을 쓰는 시점#
- 프로비저닝은 기본 역할마다 정책을 하나씩 씁니다(
SecretsManagers::DefaultRolePoliciesHelper#create_default_role_policies). 프로젝트 프로비저닝은 관리 마운트의 경로에 대해 Owner(read,write,delete), Maintainer(read,write), Developer(read,create)를 부여합니다. Developer는write가 아니라create를 가지므로 시크릿을 추가할 수는 있지만 이미 있는 시크릿을 변경할 수는 없습니다. 그룹 프로비저닝은 그룹 부여가 모든 하위 프로젝트의 파이프라인에 미치므로 Owner에게만 부여합니다. - 시크릿 권한을 부여하거나 업데이트하면(
SecretsPermissions::UpdateServiceHelpers#store_permission) 정책 두 개를 씁니다. 관리 정책(메타데이터 읽기, 쓰기, 삭제이며 값 읽기는 절대 포함하지 않음)과,read_value가 부여된 경우에만api/접두사 아래의 읽기 전용 API 정책입니다. API 기능이 남아 있지 않으면 API 정책을 빈 채로 쓰지 않고 삭제합니다. - 모든 정책 쓰기는 정책 경로의 전체 기능 집합을 교체한 뒤 현재 집합을 다시 추가하므로, 기능을 철회하면 오래된 부여가 남지 않고 지워집니다.
비 CI API 액세스#
파일: ee/lib/api/secrets_management/access_tokens.rb, ee/app/services/secrets_management/api_access/issue_token_service.rb
REST 엔드포인트는 클라이언트가 읽을 때마다 GitLab을 거치지 않고 OpenBao에 직접 제시하는 수명이 짧은 JWT를 발급합니다.
POST /projects/:id/secrets_manager/access_tokenPOST /groups/:id/secrets_manager/access_token
두 경로 모두 기능 플래그와 create_secrets_manager_api_jwt 인가 권한 뒤에 있으며, Secrets Manager가 active 상태여야 합니다.
액세스는 토큰 기반 인증(개인 액세스 토큰, 프로젝트 또는 그룹 액세스 토큰, 서비스 계정 토큰, OAuth)으로 제한됩니다.
브라우저 세션은 폐기할 수 있는 자격 증명을 남기지 않으므로 거부합니다.
ApiAccess::IssueTokenService는 ProjectApiJwt 또는 GroupApiJwt(TTL 5분)를 만듭니다.
이 서비스는 external-secrets.io Vault 공급자 형식에 맞춘 연결 정보를 반환하므로 External Secrets Operator 같은 클라이언트가 응답을 그대로 사용할 수 있습니다. 서버 URL, 네임스페이스, KV 마운트 경로, 시크릿 기본 경로(explicit), CEL 인증 경로(api_jwt/cel), 역할(all_api), 토큰 자체가 포함됩니다.
이 액세스 토큰 발급 외에 비 CI REST 읽기 또는 쓰기 엔드포인트를 제공하겠다는 공개적인 약속은 아직 없습니다. 쓰기는 여전히 기존 GraphQL 뮤테이션을 거칩니다.