Secrets Manager 서비스
GitLab v19.5요약
Secrets Manager 비즈니스 로직은 ee/app/services/secrets_management/ 아래의 일반 Ruby 서비스 객체에 있습니다. 두 기본 서비스는 모두 Gitlab::Utils::StrongMemoize, Helpers::ExclusiveLeaseHelper, Helpers::ErrorResponseHelper, EntitlementGate를 포함합니다.
Secrets Manager 비즈니스 로직은 ee/app/services/secrets_management/ 아래의 일반 Ruby 서비스 객체에 있습니다.
공유 동작은 ee/app/services/concerns/secrets_management/ 아래의 모듈과 두 개의 작은 헬퍼 위치에 있습니다.
아래 절에서는 기본 서비스 클래스, 공유 concern, 시크릿 CRUD 흐름, CI 정책 갱신, 개수 및 로테이션 목록 서비스, 오류 처리를 다룹니다.
기본 서비스 클래스#
| 클래스 | 파일 | 제공 항목 |
|---|---|---|
SecretsManagement::ProjectBaseService |
ee/app/services/secrets_management/project_base_service.rb |
base_secrets_manager_client, namespace_secrets_manager_client, project_secrets_manager_client, user_client |
SecretsManagement::GroupBaseService |
ee/app/services/secrets_management/group_base_service.rb |
같은 클라이언트 메서드이며, 프로젝트 클라이언트 대신 group_secrets_manager_client를 범위로 사용합니다 |
SecretsManagement::BaseDeprovisionService |
ee/app/services/secrets_management/base_deprovision_service.rb |
공유 비동기 프로비저닝 해제 흐름. Secrets Manager 상태와 부수 효과를 참고합니다 |
두 기본 서비스는 모두 Gitlab::Utils::StrongMemoize, Helpers::ExclusiveLeaseHelper, Helpers::ErrorResponseHelper, EntitlementGate를 포함합니다.
또한 둘 다 생성자에서 프로젝트 또는 그룹을 받습니다.
각 클라이언트 메서드는 JWT(ProjectSecretsManagerJwt, GroupSecretsManagerJwt 또는 사용자 JWT)를 만들어 SecretsManagerClient로 감쌉니다.
user_client는 사용자 JWT와 CEL(Common Expression Language) 인가로 인증합니다. 시크릿 CRUD 서비스가 사용하는 클라이언트입니다.
생성은 예외입니다. 애플리케이션 클라이언트로 메타데이터를 쓰므로 create 권한 부여만으로도 update 없이 생성을 끝낼 수 있습니다.
자격 확인과 과금 상태는 Secrets Manager 이행을 참고합니다.
서비스 concern#
공유 로직은 ee/app/services/concerns/secrets_management/에 있습니다.
| Concern | 파일 | 사용처 |
|---|---|---|
EntitlementGate |
entitlement_gate.rb |
자격 때문에 쓰기가 차단될 때 이를 거부하기 위해 두 기본 서비스가 사용 |
InitializeServiceHelpers |
initialize_service_helpers.rb |
프로젝트 및 그룹 Secrets Manager 초기화 서비스 |
InitiateDeprovisionServiceHelpers |
initiate_deprovision_service_helpers.rb |
프로젝트 및 그룹 프로비저닝 해제 시작 서비스 |
Secrets::CreateServiceHelpers |
secrets/create_service_helpers.rb |
시크릿 생성 서비스 |
Secrets::UpdateServiceHelpers |
secrets/update_service_helpers.rb |
시크릿 업데이트 서비스 |
Secrets::DeleteServiceHelpers |
secrets/delete_service_helpers.rb |
시크릿 삭제 서비스 |
Secrets::ListNeedingRotationServiceHelpers |
secrets/list_needing_rotation_service_helpers.rb |
로테이션 목록 서비스 |
ProjectSecrets::SecretRefresherHelper |
project_secrets/secret_refresher_helper.rb |
프로젝트 시크릿 생성, 업데이트, 삭제 서비스 |
SecretsManagers::DefaultRolePoliciesHelper |
secrets_managers/default_role_policies_helper.rb |
기본 역할 ACL 정책을 만들기 위해 프로비저닝 서비스가 사용합니다. 프로젝트는 Owner, Maintainer, Developer이고 그룹은 Owner뿐입니다 |
SecretsPermissions::UpdateServiceHelpers |
secrets_permissions/update_service_helpers.rb |
권한 업데이트 서비스 |
SecretsPermissions::DeleteServiceHelpers |
secrets_permissions/delete_service_helpers.rb |
권한 삭제 서비스 |
SecretsPermissions::ListServiceHelpers |
secrets_permissions/list_service_helpers.rb |
권한 목록 서비스 |
그룹 refresher 헬퍼는 이 디렉터리 아래에 있지 않습니다.
그룹 시크릿 서비스 바로 옆인 ee/app/services/secrets_management/group_secrets/secret_refresher_helper.rb에 있습니다.
그룹 전용 헬퍼를 추가할 때는 모든 것이 concerns/ 아래에 들어간다고 가정하지 말고 이 기존 위치를 따릅니다.
두 개의 작은 위치에 concern이 아닌 헬퍼가 있습니다:
ee/app/services/secrets_management/helpers/. 기본 서비스에 믹스인되는 작은 모듈입니다.ExclusiveLeaseHelper(with_exclusive_lease_for)와ErrorResponseHelper(secrets_manager_inactive_response,deprovision_in_progress_response)가 있습니다.ee/app/services/secrets_management/concerns/secrets_count_service.rb. 아래의 개수 서비스에서만 사용하는 단일 concern인SecretsCountService입니다.
각 concern이 기대하는 추상 메서드#
일부 메서드는 concern에서 raise NotImplementedError로 강제됩니다.
다른 메서드는 직접 호출되며, 명시적인 가드 없이 이를 포함하는 클래스 어딘가에 정의되어 있어야 합니다.
둘 다 서비스가 작동하는 데 필요하므로 여기에 모두 나열합니다.
EntitlementGate는 메서드 하나, entitlement_root_namespace를 기대합니다. 이 메서드는 자격 상태가 쓰기를 제어하는 네임스페이스를 반환합니다.
InitializeServiceHelpers와 InitiateDeprovisionServiceHelpers는 프로비저닝 및 프로비저닝 해제 서비스를 뒷받침합니다.
이들이 구동하는 프로비저닝 및 프로비저닝 해제 흐름은 상태 전환의 부수 효과를 참고합니다.
Secrets::CreateServiceHelpers는 다음을 기대합니다:
| 메서드 | 용도 |
|---|---|
secrets_manager |
활성 Secrets Manager. 보통 delegate :secrets_manager, to: :project(또는 :group) |
read_secret(secret) |
ReadMetadataService를 호출하여 시크릿이 이미 있는지 확인 |
secrets_count_service |
한도 확인에 사용하는 개수 서비스 인스턴스를 반환 |
refresh_secret_ci_policies(secret) |
생성 후 OpenBao ACL 정책을 갱신 |
user_client |
기본 서비스에서 제공 |
Secrets::UpdateServiceHelpers는 다음을 기대합니다:
| 메서드 | 용도 |
|---|---|
secrets_manager |
활성 Secrets Manager |
user_client |
기본 서비스에서 제공 |
refresh_policies_after_update(secret) |
메타데이터와 값을 쓴 뒤 OpenBao ACL 정책을 갱신. 선택 사항이며, 포함하는 서비스가 정의한 경우에만 호출 |
error_response(secret) |
시크릿의 검증 오류로 ServiceResponse.error를 생성 |
Secrets::DeleteServiceHelpers는 다음을 기대합니다:
| 메서드 | 용도 |
|---|---|
secrets_manager |
활성 Secrets Manager |
user_client |
기본 서비스에서 제공 |
read_secret(name) |
삭제하기 전에 시크릿을 읽음 |
refresh_secret_ci_policies(secret, delete_operation: true) |
정책에서 시크릿을 제거 |
SecretsManagers::DefaultRolePoliciesHelper는 secrets_manager, client(Secrets Manager의 네임스페이스로 범위가 지정된 OpenBao 클라이언트), permission_class, default_roles(기본 정책을 부여할 액세스 수준이며 ROLE_ACTIONS 키의 부분 집합)를 기대합니다.
SecretsPermissions::UpdateServiceHelpers, DeleteServiceHelpers, ListServiceHelpers는 각각 이를 포함하는 서비스로부터 resource, client, permission_class를 기대합니다. 권한 서비스를 참고합니다.
시크릿 CRUD 서비스#
각 범위(project_secrets/, group_secrets/)에는 자체 생성, 업데이트, 삭제, 목록, 메타데이터 읽기 서비스가 있습니다.
이들은 위의 concern을 공유하므로, 별도로 명시하지 않는 한 이 절의 로직은 두 범위에서 같습니다.
생성#
파일: ee/app/services/secrets_management/project_secrets/create_service.rb(그룹 대응 파일은 group_secrets/ 아래).
- 프로젝트 또는 그룹의 배타적 리스를 획득합니다.
- 시크릿 모델(
ProjectSecret또는GroupSecret)을 만들고,rotation_interval_days가 주어지면 로테이션 정보 레코드도 만듭니다. - Secrets Manager가 활성 상태가 아니거나 시크릿 개수 한도를 초과하면 일찍 반환합니다.
- 시크릿 모델을 검증합니다.
ReadMetadataService를 호출하여 시크릿이 이미 있는지 확인합니다. - 시크릿 값 크기를 검증합니다(최대 10,000바이트,
Secrets::CreateServiceHelpers::MAX_SECRET_SIZE). - OpenBao에 쓰기 전에 로테이션 정보 레코드를 upsert합니다. 이렇게 하면 나중에 OpenBao 쓰기가 실패해도, 로테이션 추적이 없는 시크릿이 남는 대신 로테이션 알림 cron이 정리하는 고아 행이 남습니다.
start_secret_creation!.cas: 0으로 값을 쓴 다음metadata_cas: 0으로 메타데이터를 씁니다.refresh_secret_ci_policies. 파이프라인 읽기 액세스를 위한 OpenBao ACL 정책을 업데이트합니다.complete_secret_creation!.create_completed_at을 설정하고metadata_cas: 1로 메타데이터를 다시 씁니다.- 메모리에서
secret.metadata_version = 2로 설정하여 GraphQL 응답과 다음 업데이트 호출이 올바른 버전을 사용하게 합니다. - 네임스페이스 시크릿 개수 갱신을 큐에 넣습니다.
값과 메타데이터 쓰기는 의도적으로 정책 갱신보다 먼저 일어납니다. 정책 생성이 실패하면 시크릿은 OpenBao에 존재하지만 아직 어떤 파이프라인도 읽을 수 없습니다. 이후 재시도에서는 시크릿이 이미 있는 것으로 확인되어 "Secret already exists."를 반환합니다. 오래된 불완전한 시크릿은 수동으로 또는 유지 관리 job으로 정리해야 합니다.
업데이트#
파일: ee/app/services/secrets_management/project_secrets/update_service.rb(그룹 대응 파일은 group_secrets/ 아래).
- 배타적 리스를 획득합니다.
ReadMetadataService를 통해 현재 시크릿을 읽습니다.- 주어진 속성 변경을 적용하고
valid_for_update?를 검증합니다. rotation_interval_days가 변경되었으면 로테이션 정보를 만들거나 upsert합니다.- Secrets Manager가 활성 상태가 아니면 일찍 반환합니다.
- 낙관적 잠금을 위해
metadata_cas와 함께 메타데이터를 먼저 씁니다. 값을 쓰기 전에 실행되므로 CAS 불일치를 값 쓰기가 일어나기 전에 잡을 수 있습니다. - 값이 주어졌으면 값을 씁니다.
- 값을 쓴 뒤
refresh_policies_after_update를 호출합니다. 이제 프로젝트와 그룹 업데이트 서비스 모두 같은 이름의 이 메서드를 구현하며, 쓰기 전이 아니라 쓰기 후에 실행합니다. - 증가한
metadata_cas로 완료 메타데이터(update_completed_at)를 씁니다.
메타데이터 쓰기에서 check-and-set 불일치로 SecretsManagerClient::ApiError가 발생하면 Secrets::UpdateServiceHelpers가 이를 잡아 "This secret has been modified recently. Please refresh the page and try again."를 반환합니다.
삭제#
파일: ee/app/services/secrets_management/project_secrets/delete_service.rb(그룹 대응 파일은 group_secrets/ 아래).
- 리소스의 배타적 리스를 획득합니다.
- Secrets Manager가 활성 상태가 아니면 일찍 반환합니다.
read_secret을 통해 현재 시크릿을 읽습니다. 시크릿이 없으면 읽기 오류를 반환합니다.- OpenBao에서 값과 메타데이터를 삭제합니다(
delete_kv_secret). refresh_secret_ci_policies(secret, delete_operation: true)를 호출하여 정책에서 시크릿을 제거합니다.- 네임스페이스 시크릿 개수 갱신을 큐에 넣습니다.
목록#
파일: ee/app/services/secrets_management/project_secrets/list_service.rb(그룹 대응 파일은 group_secrets/ 아래).
- Secrets Manager가 활성 상태가 아니면 일찍 반환합니다.
- 상세 메타데이터 엔드포인트에 대해
user_client.list_secrets를 호출합니다. - OpenBao 응답의 각 항목마다 시크릿 모델을 하나씩 만듭니다.
include_rotation_info:(기본값true)를 받습니다. 설정하면 참조된ProjectSecretRotationInfo또는GroupSecretRotationInfo레코드를 시크릿마다 쿼리하는 대신 ID로 한 번의 쿼리에서 일괄 로드합니다.
메타데이터 읽기#
파일: ee/app/services/secrets_management/project_secrets/read_metadata_service.rb(그룹 대응 파일은 group_secrets/ 아래).
- OpenBao를 호출하기 전에 시크릿 이름을
/\A[a-zA-Z0-9_]+\z/와 대조해 검증합니다. - OpenBao의 해당 경로에 메타데이터가 없으면
ServiceResponse.error(reason: :not_found)를 반환합니다. include_rotation_info: true(기본값)이면 현재metadata_version의 로테이션 정보를 조회하고, 없으면metadata_version - 1로 대체합니다. 이는 메타데이터 쓰기와 로테이션 정보 upsert 반영 사이의 경쟁 상태를 처리합니다.
CI 정책 갱신#
시크릿을 만들거나 업데이트하거나 삭제하면 CI 정책도 업데이트되어, 파이프라인이 올바른 시크릿을 읽을 수 있습니다.
파일: ee/app/services/secrets_management/ci_policies/base_secret_refresher.rb이며, 같은 디렉터리에 ProjectSecretRefresher와 GroupSecretRefresher 하위 클래스가 있습니다.
생성, 업데이트, 삭제는 모두 해당 범위의 refresher 헬퍼를 통해 refresh_ci_policies_for(secret, delete_operation:)를 호출합니다.
refresher는 시크릿을 어느 정책에서 제거하고 어느 정책에 추가해야 하는지 판단합니다:
- 삭제: 현재 정책에서만 제거합니다.
- 범위 속성이 변경된 업데이트(프로젝트:
environment또는branch, 그룹:environment또는protected): 이전 정책에서 제거하고 새 정책에 추가합니다. - 생성이거나 범위 변경이 없는 업데이트: 현재 정책에만 추가합니다.
시크릿을 정책에서 제거할 때 refresher는 list_secrets를 스캔하여 같은 정책 이름으로 해석되는 다른 시크릿이 몇 개 남아 있는지 셉니다.
개수가 0이면 정책을 삭제합니다. 그렇지 않으면 해당 시크릿의 읽기 기능만 제거합니다.
정책 이름은 Secrets Manager 모델이 계산합니다(프로젝트는 ci_policy_name, 그룹은 ci_policy_name_for_environment).
명명 체계와 ACL 정책 및 기능의 구조는 Secrets Manager OpenBao 클라이언트를 참고합니다.
배타적 리스#
생성, 업데이트, 삭제는 프로젝트 또는 그룹을 키로 하여 Helpers::ExclusiveLeaseHelper#with_exclusive_lease_for를 통해 배타적 리스를 획득합니다.
같은 프로젝트 또는 그룹에 대한 두 번째 작업은 기다리지 않고 "Another secret operation in progress"와 함께 즉시 실패합니다.
프로비저닝과 프로비저닝 해제도 이 같은 리스를 공유합니다. 리스 규칙은 결합할 수 있는 상태를 참고합니다.
권한 서비스#
권한 서비스는 누가 시크릿을 읽고, 쓰고, 삭제할 수 있는지 제어합니다.
파일: ee/app/services/secrets_management/project_secrets_permissions/ 및 group_secrets_permissions/.
Concern: ee/app/services/concerns/secrets_management/secrets_permissions/.
각 서비스 클래스는 resource, client, permission_class만 정의합니다.
나머지는 concern이 처리합니다. 권한 객체를 만들고, 부여된 작업을 OpenBao 기능으로 변환하고, ACL 정책을 씁니다.
권한 부여는 두 개의 별도 OpenBao 정책에 기록됩니다.
관리 정책은 메타데이터 읽기, 쓰기, 삭제를 다루며 값 읽기는 다루지 않습니다.
읽기 전용 API 정책은 값 읽기를 다룹니다. GitLab은 read_value가 부여될 때만 이 두 번째 정책을 쓰고, read_value가 부여되지 않으면 삭제합니다.
권한은 데이터베이스가 아니라 OpenBao ACL 정책으로 저장됩니다.
ProjectSecretsPermission과 GroupSecretsPermission은 ApplicationRecord가 아니라 ActiveModel::Model 객체입니다.
개수 서비스와 한도 적용#
이 서비스는 생성이 진행되기 전에 프로젝트 또는 그룹이 시크릿 한도에 도달했는지 확인합니다.
파일: ee/app/services/secrets_management/project_secrets_count_service.rb, group_secrets_count_service.rb, 그리고 ee/app/services/secrets_management/concerns/secrets_count_service.rb의 공유 Concerns::SecretsCountService.
secrets_limit_exceeded?는 Secrets Manager에 구성된 secrets_limit를 읽습니다.
한도가 0이면 무제한으로 취급합니다.
그렇지 않으면 OpenBao에 limit + 1로 상한을 둔 개수를 요청하고 한도와 비교하므로, 개수 호출이 질문에 답하는 데 필요한 범위를 넘어 스캔할 필요가 없습니다.
Secrets::CreateServiceHelpers#secrets_limit_exceeded?는 새 시크릿을 쓰기 전에 이 서비스를 호출하며, 각 생성 서비스는 올바른 서비스를 반환하도록 secrets_count_service를 구현해야 합니다:
def secrets_count_service
SecretsManagement::ProjectSecretsCountService.new(project, current_user)
end
두 번째 별도 메커니즘은 과금을 위해 네임스페이스별 개수를 유지합니다. SecretsManagement::NamespaceSecretCounts::RefreshService(ee/app/services/secrets_management/namespace_secret_counts/refresh_service.rb)입니다.
이 개수는 secrets_stored 과금 이벤트만 읽습니다. GraphQL 개수 필드는 여전히 요청마다 OpenBao의 시크릿을 셉니다.
생성과 삭제는 쓰기에 성공한 뒤 ReconcileNamespaceSecretCountWorker를 큐에 넣고, 이 워커는 RefreshService.execute_for_namespace_id를 호출하여 새 개수를 NamespaceSecretCount에 upsert합니다(Secrets Manager가 없어졌거나 비활성 상태이면 행을 삭제합니다).
쓰기 시점의 한도 적용도 이 개수를 사용하지 않습니다.
로테이션 목록 서비스#
이 서비스는 로테이션 기한이 되었거나 다가오는 시크릿을 나열합니다.
파일: ee/app/services/secrets_management/project_secrets/list_needing_rotation_service.rb, group_secrets/list_needing_rotation_service.rb, 그리고 공유 Secrets::ListNeedingRotationServiceHelpers concern.
각 서비스는 해당 범위의 ListService를 상속하고 execute를 재정의하는 concern을 포함합니다:
super(include_rotation_info: true)를 호출하여 로테이션 정보가 붙은 모든 시크릿을 가져옵니다.rotation_info.needs_attention?가 true인 시크릿으로 필터링합니다.- 긴급도순으로 정렬합니다. 기한이 지난 시크릿이 먼저이며 가장 먼저 생성된 순서로 정렬하고, 그다음 다가오는 시크릿을
next_reminder_at이 빠른 순서로 정렬합니다.
그룹과 프로젝트 서비스의 차이#
| 항목 | 프로젝트 | 그룹 |
|---|---|---|
| 범위 속성 | branch(문자열) |
protected(불리언) |
| 시크릿 클래스 | ProjectSecret |
GroupSecret |
| 로테이션 정보 클래스 | ProjectSecretRotationInfo |
GroupSecretRotationInfo |
| Refresher 헬퍼 위치 | ee/app/services/concerns/secrets_management/project_secrets/secret_refresher_helper.rb |
ee/app/services/secrets_management/group_secrets/secret_refresher_helper.rb(concerns/ 아래가 아님) |
| 클라이언트 메서드 | project_secrets_manager_client |
group_secrets_manager_client |
오류 처리#
서비스는 ServiceResponse.success(payload: ...) 또는 ServiceResponse.error(message:, reason:, payload:)를 반환합니다.
몇 가지 reason은 호출자가 확인할 만큼 일관되게 사용됩니다. :not_found, :entitlement_blocked, :secrets_limit_exceeded, :already_initialized입니다.
OpenBao 클라이언트는 OpenBao의 원시 오류 텍스트와 함께 SecretsManagement::SecretsManagerClient::ApiError를 발생시킵니다.
서비스는 이를 사용자에게 보여 줄 메시지로 바꿀 수 있는 특정 지점에서 rescue합니다. 예를 들어 Secrets::UpdateServiceHelpers의 CAS 불일치 rescue가 있습니다.
서비스가 rescue하지 않는 곳에서는 ApiError가 GraphQL 계층까지 전파됩니다.
SecretsManagement::ErrorMapping(ee/lib/secrets_management/error_mapping.rb)은 그 경계에서 쓰는 공유 새니타이저입니다.
패턴으로 권한 오류와 check-and-set 오류를 인식하고, 로깅하기 전에 따옴표로 묶인 OpenBao 경로(시크릿 이름이 포함될 수 있음)를 가리며, 그 밖의 모든 것은 일반적인 "Internal server error."로 매핑합니다.
서비스 계층 자체가 아니라 뮤테이션 및 리졸버 오류 처리 concern이 이를 적용합니다.
이 concern이 이를 연결하는 방식은 Secrets Manager GraphQL을 참고합니다.
새 서비스 체크리스트#
- 범위를 선택합니다:
project_secrets/또는group_secrets/. ee/app/services/secrets_management/{scope}/{action}_service.rb에 파일을 만듭니다.ProjectBaseService또는GroupBaseService를 상속합니다.- 작업에 맞는 공유 concern이 있으면
ee/app/services/concerns/secrets_management/secrets/에서 포함합니다. - 서비스가 시크릿을 쓴다면 범위별 refresher 헬퍼(
ProjectSecrets::SecretRefresherHelper또는GroupSecrets::SecretRefresherHelper)를 포함합니다. secrets_manager를 상위 리소스에 위임합니다.- concern이 기대하는 모든 추상 메서드를 구현합니다. 이 페이지 앞부분의 표를 참고합니다.
- 쓰기 작업을
with_exclusive_lease_for(secrets_manager_parent)로 감쌉니다. ServiceResponse.success또는ServiceResponse.error를 일관되게 반환합니다. 예상되는 실패 사례에서 처리되지 않은 예외를 발생시키지 않습니다.ee/spec/services/secrets_management/{scope}/{action}_service_spec.rb에 스펙을 추가합니다. Secrets Manager 테스트를 참고합니다.- 프로젝트와 그룹 서비스가 같은 로직을 필요로 하면 중복하지 말고
ee/app/services/concerns/secrets_management/secrets/아래의 concern으로 추출합니다.