InfoGrab DocsInfoGrab Docs

Secrets Manager 서비스

요약

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/ 아래).

  1. 프로젝트 또는 그룹의 배타적 리스를 획득합니다.
  2. 시크릿 모델(ProjectSecret 또는 GroupSecret)을 만들고, rotation_interval_days가 주어지면 로테이션 정보 레코드도 만듭니다.
  3. Secrets Manager가 활성 상태가 아니거나 시크릿 개수 한도를 초과하면 일찍 반환합니다.
  4. 시크릿 모델을 검증합니다. ReadMetadataService를 호출하여 시크릿이 이미 있는지 확인합니다.
  5. 시크릿 값 크기를 검증합니다(최대 10,000바이트, Secrets::CreateServiceHelpers::MAX_SECRET_SIZE).
  6. OpenBao에 쓰기 전에 로테이션 정보 레코드를 upsert합니다. 이렇게 하면 나중에 OpenBao 쓰기가 실패해도, 로테이션 추적이 없는 시크릿이 남는 대신 로테이션 알림 cron이 정리하는 고아 행이 남습니다.
  7. start_secret_creation!. cas: 0으로 값을 쓴 다음 metadata_cas: 0으로 메타데이터를 씁니다.
  8. refresh_secret_ci_policies. 파이프라인 읽기 액세스를 위한 OpenBao ACL 정책을 업데이트합니다.
  9. complete_secret_creation!. create_completed_at을 설정하고 metadata_cas: 1로 메타데이터를 다시 씁니다.
  10. 메모리에서 secret.metadata_version = 2로 설정하여 GraphQL 응답과 다음 업데이트 호출이 올바른 버전을 사용하게 합니다.
  11. 네임스페이스 시크릿 개수 갱신을 큐에 넣습니다.

값과 메타데이터 쓰기는 의도적으로 정책 갱신보다 먼저 일어납니다. 정책 생성이 실패하면 시크릿은 OpenBao에 존재하지만 아직 어떤 파이프라인도 읽을 수 없습니다. 이후 재시도에서는 시크릿이 이미 있는 것으로 확인되어 "Secret already exists."를 반환합니다. 오래된 불완전한 시크릿은 수동으로 또는 유지 관리 job으로 정리해야 합니다.

업데이트#

파일: ee/app/services/secrets_management/project_secrets/update_service.rb(그룹 대응 파일은 group_secrets/ 아래).

  1. 배타적 리스를 획득합니다.
  2. ReadMetadataService를 통해 현재 시크릿을 읽습니다.
  3. 주어진 속성 변경을 적용하고 valid_for_update?를 검증합니다.
  4. rotation_interval_days가 변경되었으면 로테이션 정보를 만들거나 upsert합니다.
  5. Secrets Manager가 활성 상태가 아니면 일찍 반환합니다.
  6. 낙관적 잠금을 위해 metadata_cas와 함께 메타데이터를 먼저 씁니다. 값을 쓰기 전에 실행되므로 CAS 불일치를 값 쓰기가 일어나기 전에 잡을 수 있습니다.
  7. 값이 주어졌으면 값을 씁니다.
  8. 값을 쓴 뒤 refresh_policies_after_update를 호출합니다. 이제 프로젝트와 그룹 업데이트 서비스 모두 같은 이름의 이 메서드를 구현하며, 쓰기 전이 아니라 쓰기 후에 실행합니다.
  9. 증가한 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/ 아래).

  1. 리소스의 배타적 리스를 획득합니다.
  2. Secrets Manager가 활성 상태가 아니면 일찍 반환합니다.
  3. read_secret을 통해 현재 시크릿을 읽습니다. 시크릿이 없으면 읽기 오류를 반환합니다.
  4. OpenBao에서 값과 메타데이터를 삭제합니다(delete_kv_secret).
  5. refresh_secret_ci_policies(secret, delete_operation: true)를 호출하여 정책에서 시크릿을 제거합니다.
  6. 네임스페이스 시크릿 개수 갱신을 큐에 넣습니다.

목록#

파일: 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을 포함합니다:

  1. super(include_rotation_info: true)를 호출하여 로테이션 정보가 붙은 모든 시크릿을 가져옵니다.
  2. rotation_info.needs_attention?가 true인 시크릿으로 필터링합니다.
  3. 긴급도순으로 정렬합니다. 기한이 지난 시크릿이 먼저이며 가장 먼저 생성된 순서로 정렬하고, 그다음 다가오는 시크릿을 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을 참고합니다.

새 서비스 체크리스트#

  1. 범위를 선택합니다: project_secrets/ 또는 group_secrets/.
  2. ee/app/services/secrets_management/{scope}/{action}_service.rb에 파일을 만듭니다.
  3. ProjectBaseService 또는 GroupBaseService를 상속합니다.
  4. 작업에 맞는 공유 concern이 있으면 ee/app/services/concerns/secrets_management/secrets/에서 포함합니다.
  5. 서비스가 시크릿을 쓴다면 범위별 refresher 헬퍼(ProjectSecrets::SecretRefresherHelper 또는 GroupSecrets::SecretRefresherHelper)를 포함합니다.
  6. secrets_manager를 상위 리소스에 위임합니다.
  7. concern이 기대하는 모든 추상 메서드를 구현합니다. 이 페이지 앞부분의 표를 참고합니다.
  8. 쓰기 작업을 with_exclusive_lease_for(secrets_manager_parent)로 감쌉니다.
  9. ServiceResponse.success 또는 ServiceResponse.error를 일관되게 반환합니다. 예상되는 실패 사례에서 처리되지 않은 예외를 발생시키지 않습니다.
  10. ee/spec/services/secrets_management/{scope}/{action}_service_spec.rb에 스펙을 추가합니다. Secrets Manager 테스트를 참고합니다.
  11. 프로젝트와 그룹 서비스가 같은 로직을 필요로 하면 중복하지 말고 ee/app/services/concerns/secrets_management/secrets/ 아래의 concern으로 추출합니다.

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/ 아래).

  1. 프로젝트 또는 그룹의 배타적 리스를 획득합니다.
  2. 시크릿 모델(ProjectSecret 또는 GroupSecret)을 만들고, rotation_interval_days가 주어지면 로테이션 정보 레코드도 만듭니다.
  3. Secrets Manager가 활성 상태가 아니거나 시크릿 개수 한도를 초과하면 일찍 반환합니다.
  4. 시크릿 모델을 검증합니다. ReadMetadataService를 호출하여 시크릿이 이미 있는지 확인합니다.
  5. 시크릿 값 크기를 검증합니다(최대 10,000바이트, Secrets::CreateServiceHelpers::MAX_SECRET_SIZE).
  6. OpenBao에 쓰기 전에 로테이션 정보 레코드를 upsert합니다. 이렇게 하면 나중에 OpenBao 쓰기가 실패해도, 로테이션 추적이 없는 시크릿이 남는 대신 로테이션 알림 cron이 정리하는 고아 행이 남습니다.
  7. start_secret_creation!. cas: 0으로 값을 쓴 다음 metadata_cas: 0으로 메타데이터를 씁니다.
  8. refresh_secret_ci_policies. 파이프라인 읽기 액세스를 위한 OpenBao ACL 정책을 업데이트합니다.
  9. complete_secret_creation!. create_completed_at을 설정하고 metadata_cas: 1로 메타데이터를 다시 씁니다.
  10. 메모리에서 secret.metadata_version = 2로 설정하여 GraphQL 응답과 다음 업데이트 호출이 올바른 버전을 사용하게 합니다.
  11. 네임스페이스 시크릿 개수 갱신을 큐에 넣습니다.

값과 메타데이터 쓰기는 의도적으로 정책 갱신보다 먼저 일어납니다. 정책 생성이 실패하면 시크릿은 OpenBao에 존재하지만 아직 어떤 파이프라인도 읽을 수 없습니다. 이후 재시도에서는 시크릿이 이미 있는 것으로 확인되어 "Secret already exists."를 반환합니다. 오래된 불완전한 시크릿은 수동으로 또는 유지 관리 job으로 정리해야 합니다.

업데이트#

파일: ee/app/services/secrets_management/project_secrets/update_service.rb(그룹 대응 파일은 group_secrets/ 아래).

  1. 배타적 리스를 획득합니다.
  2. ReadMetadataService를 통해 현재 시크릿을 읽습니다.
  3. 주어진 속성 변경을 적용하고 valid_for_update?를 검증합니다.
  4. rotation_interval_days가 변경되었으면 로테이션 정보를 만들거나 upsert합니다.
  5. Secrets Manager가 활성 상태가 아니면 일찍 반환합니다.
  6. 낙관적 잠금을 위해 metadata_cas와 함께 메타데이터를 먼저 씁니다. 값을 쓰기 전에 실행되므로 CAS 불일치를 값 쓰기가 일어나기 전에 잡을 수 있습니다.
  7. 값이 주어졌으면 값을 씁니다.
  8. 값을 쓴 뒤 refresh_policies_after_update를 호출합니다. 이제 프로젝트와 그룹 업데이트 서비스 모두 같은 이름의 이 메서드를 구현하며, 쓰기 전이 아니라 쓰기 후에 실행합니다.
  9. 증가한 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/ 아래).

  1. 리소스의 배타적 리스를 획득합니다.
  2. Secrets Manager가 활성 상태가 아니면 일찍 반환합니다.
  3. read_secret을 통해 현재 시크릿을 읽습니다. 시크릿이 없으면 읽기 오류를 반환합니다.
  4. OpenBao에서 값과 메타데이터를 삭제합니다(delete_kv_secret).
  5. refresh_secret_ci_policies(secret, delete_operation: true)를 호출하여 정책에서 시크릿을 제거합니다.
  6. 네임스페이스 시크릿 개수 갱신을 큐에 넣습니다.

목록#

파일: 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을 포함합니다:

  1. super(include_rotation_info: true)를 호출하여 로테이션 정보가 붙은 모든 시크릿을 가져옵니다.
  2. rotation_info.needs_attention?가 true인 시크릿으로 필터링합니다.
  3. 긴급도순으로 정렬합니다. 기한이 지난 시크릿이 먼저이며 가장 먼저 생성된 순서로 정렬하고, 그다음 다가오는 시크릿을 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을 참고합니다.

새 서비스 체크리스트#

  1. 범위를 선택합니다: project_secrets/ 또는 group_secrets/.
  2. ee/app/services/secrets_management/{scope}/{action}_service.rb에 파일을 만듭니다.
  3. ProjectBaseService 또는 GroupBaseService를 상속합니다.
  4. 작업에 맞는 공유 concern이 있으면 ee/app/services/concerns/secrets_management/secrets/에서 포함합니다.
  5. 서비스가 시크릿을 쓴다면 범위별 refresher 헬퍼(ProjectSecrets::SecretRefresherHelper 또는 GroupSecrets::SecretRefresherHelper)를 포함합니다.
  6. secrets_manager를 상위 리소스에 위임합니다.
  7. concern이 기대하는 모든 추상 메서드를 구현합니다. 이 페이지 앞부분의 표를 참고합니다.
  8. 쓰기 작업을 with_exclusive_lease_for(secrets_manager_parent)로 감쌉니다.
  9. ServiceResponse.success 또는 ServiceResponse.error를 일관되게 반환합니다. 예상되는 실패 사례에서 처리되지 않은 예외를 발생시키지 않습니다.
  10. ee/spec/services/secrets_management/{scope}/{action}_service_spec.rb에 스펙을 추가합니다. Secrets Manager 테스트를 참고합니다.
  11. 프로젝트와 그룹 서비스가 같은 로직을 필요로 하면 중복하지 말고 ee/app/services/concerns/secrets_management/secrets/ 아래의 concern으로 추출합니다.