GitLab CI/CD에서 HashiCorp Vault 비밀을 사용하기


Tier: 프리미엄, 얼티메이트
Offering: GitLab.com, Self-managed, GitLab Dedicated
caution

CI_JOB_JWT를 사용한 인증은 GitLab 15.9에서 폐기되었습니다 그리고 해당 토큰은 GitLab 18.0에서 제거 예정입니다. 대신 ID 토큰을 사용하여 HashiCorp Vault로 인증 을 진행하세요. 이 페이지에서 설명된 대로.
note

Vault 1.17부터 JWT auth 로그인은 역할에 대한 바운드 오디언스를 요구합니다 JWT에 aud 클레임이 포함되어 있다면. aud 클레임은 하나의 문자열 또는 여러 문자열일 수 있습니다.

이 튜토리얼에서는 GitLab CI/CD로부터 HashiCorp의 Vault에서 인증, 구성, 및 비밀을 읽는 방법을 설명합니다.

전제 조건

이 튜토리얼은 GitLab CI/CD와 Vault에 익숙하다고 가정합니다.

따라오기 위해서, 다음이 필요합니다:

  • GitLab 계정.
  • 구성 및 역할과 정책을 만들기 위해 실행되고 있는 Vault 서버에 액세스 (최소 v1.2.0).
    HashiCorp Vaults의 경우, 오픈소스 또는 엔터프라이즈 버전이 될 수 있습니다.
note

아래의 URL인 vault.example.com을 Vault 서버의 URL로, gitlab.example.com을 GitLab 인스턴스의 URL로 교체해야 합니다.

작동 방식

ID 토큰은 서드 파티 서비스와의 OIDC 인증에 사용되는 JSON Web Tokens(JWTs)입니다. 하나 이상의 ID 토큰이 정의된 경우, secrets 키워드는 자동으로 해당 토큰을 사용하여 Vault로 인증합니다.

다음 필드가 JWT에 포함됩니다:

필드 시점 설명
jti 항상 이 토큰의 고유 식별자
iss 항상 발급자, 당신의 GitLab 인스턴스의 도메인
iat 항상 발행 시간
nbf 항상 유효하지 않은 시간 이전
exp 항상 만료 시간
sub 항상 주제 (작업 ID)
namespace_id 항상 ID에 의해 그룹이나 사용자 수준 네임스페이스로 범위를 지정하는 데 사용
namespace_path 항상 경로에 의해 그룹이나 사용자 수준 네임스페이스로 범위를 지정하는 데 사용
project_id 항상 ID에 의해 프로젝트로 범위를 지정하는 데 사용
project_path 항상 경로에 의해 프로젝트로 범위를 지정하는 데 사용
user_id 항상 작업을 실행하는 사용자의 ID
user_login 항상 작업을 실행하는 사용자의 사용자 이름
user_email 항상 작업을 실행하는 사용자의 이메일
pipeline_id 항상 해당 파이프라인의 ID
pipeline_source 항상 파이프라인 소스
job_id 항상 해당 작업의 ID
ref 항상 해당 작업의 Git 참조
ref_type 항상 Git 참조 유형, branch 또는 tag
ref_path 항상 작업을 위한 완전히 정규화 된 참조. 예를 들어, refs/heads/main. GitLab 16.0에서 소개됨.
ref_protected 항상 이 Git 참조가 보호되어 있으면 true, 그렇지 않으면 false
environment 작업이 환경을 지정하는 경우 해당 작업이 지정하는 환경
groups_direct 사용자가 직접 소속된 0부터 200개의 그룹 사용자의 직접 소속 그룹의 경로. 사용자가 200개 이상의 그룹에 직접 소속되어 있으면 생략됨 (GitLab 16.11에서 소개됨).
environment_protected 작업이 환경을 지정하는 경우 지정된 환경이 보호되어 있으면 true, 그렇지 않으면 false
deployment_tier 작업이 환경을 지정하는 경우 환경의 배포 티어 (GitLab 15.2에서 소개됨)
environment_action 작업이 환경을 지정하는 경우 작업에서 지정된 환경 액션(environment:action). (GitLab 16.5에서 소개됨)

예시 JWT 페이로드:

{
  "jti": "c82eeb0c-5c6f-4a33-abf5-4c474b92b558",
  "iss": "gitlab.example.com",
  "iat": 1585710286,
  "nbf": 1585798372,
  "exp": 1585713886,
  "sub": "job_1212",
  "namespace_id": "1",
  "namespace_path": "mygroup",
  "project_id": "22",
  "project_path": "mygroup/myproject",
  "user_id": "42",
  "user_login": "myuser",
  "user_email": "myuser@example.com",
  "pipeline_id": "1212",
  "pipeline_source": "web",
  "job_id": "1212",
  "ref": "auto-deploy-2020-04-01",
  "ref_type": "branch",
  "ref_path": "refs/heads/auto-deploy-2020-04-01",
  "ref_protected": "true",
  "groups_direct": ["mygroup/mysubgroup", "myothergroup/myothersubgroup"],
  "environment": "production",
  "environment_protected": "true",
  "environment_action": "start"
}

해당 JWT는 RS256을 사용하여 인코딩되고 전용 개인 키로 서명됩니다. 토큰의 만료 시간은 작업의 제한 시간으로 설정되거나, 그렇지 않으면 5분입니다. 이 토큰을 서명하는 데 사용된 키는 어떠한 안내없이 변경될 수 있습니다. 그럴 경우 작업을 다시 시도하면 현재의 서명 키를 사용하여 새로운 JWT를 생성합니다.

이 JWT를 사용하여 Vault 서버와 통신할 수 있으며, Vault 서버가 JWT 인증 방법을 허용하도록 구성되어 있어야 합니다.
Vault에서 역할을 구성할 때, 바운드 클레임을 사용하여 JWT 클레임과 일치시켜 각 CI/CD 작업이 액세스할 수 있는 비밀을 제한할 수 있습니다.

Vault와 통신하기 위해 CLI 클라이언트를 사용하거나 API 요청을 수행할 수 있습니다 (curl 또는 다른 클라이언트를 사용하여).

예시

경고: JWT는 자격 증명으로, 리소스 접근을 허용할 수 있는데요. 붙여 넣는 자리를 신중히 선택하세요!

예를 들어, http://vault.example.com:8200에서 실행 중인 Vault 서버에 스테이징 및 프로덕션 데이터베이스의 비밀번호가 저장되어 있다고 가정해봅시다. 여기서 스테이징 비밀번호는 pa$$w0rd이고, 프로덕션 비밀번호는 real-pa$$w0rd입니다.

$ vault kv get -field=password secret/myproject/staging/db
pa$$w0rd

$ vault kv get -field=password secret/myproject/production/db
real-pa$$w0rd

Vault 서버를 구성하려면 먼저 JWT Auth 메소드를 활성화합니다.

$ vault auth enable jwt
Success! Enabled jwt auth method at: jwt/

그런 다음, 이러한 시크릿을 읽을 수 있는 정책을 만듭니다(각 시크릿에 대해 하나씩):

$ vault policy write myproject-staging - <<EOF
# 정책 이름: myproject-staging
#
# 'secret/myproject/staging/*' 경로에 대한 읽기 전용 허가
path "secret/myproject/staging/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-staging

$ vault policy write myproject-production - <<EOF
# 정책 이름: myproject-production
#
# 'secret/myproject/production/*' 경로에 대한 읽기 전용 허가
path "secret/myproject/production/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-production

또한, JWT를 이러한 정책과 연결하는 역할도 필요합니다.

예를 들어, 프로젝트 ID가 22인 프로젝트의 main 브랜치에서만 정책을 사용할 수 있도록 바운드 클레임이 구성된 스테이징용 myproject-staging 역할을 하나 만듭니다.

$ vault write auth/jwt/role/myproject-staging - <<EOF
{
  "role_type": "jwt",
  "policies": ["myproject-staging"],
  "token_explicit_max_ttl": 60,
  "user_claim": "user_email",
  "bound_audiences": "https://vault.example.com",
  "bound_claims": {
    "project_id": "22",
    "ref": "main",
    "ref_type": "branch"
  }
}
EOF

또한, auto-deploy-* 패턴과 일치하는 보호된 브랜치만 액세스할 수 있도록 이 역할에 대한 프로덕션용 myproject-production 역할을 또 하나 만듭니다.

$ vault write auth/jwt/role/myproject-production - <<EOF
{
  "role_type": "jwt",
  "policies": ["myproject-production"],
  "token_explicit_max_ttl": 60,
  "user_claim": "user_email",
  "bound_audiences": "https://vault.example.com",
  "bound_claims_type": "glob",
  "bound_claims": {
    "project_id": "22",
    "ref_protected": "true",
    "ref_type": "branch",
    "ref": "auto-deploy-*"
  }
}
EOF

보호된 브랜치와 결합하여 인증 및 시크릿 읽기를 허용할 수 있는 사용자를 제한할 수 있습니다.

JWT에 포함된 클레임(claims) 중 어느 것이든 바운드 클레임에 포함된 목록과 일치시킬 수 있습니다. 예를 들어:

"bound_claims": {
  "user_login": ["alice", "bob", "mallory"]
}

"bound_claims": {
  "ref": ["main", "develop", "test"]
}

"bound_claims": {
  "namespace_id": ["10", "20", "30"]
}

"bound_claims": {
  "project_id": ["12", "22", "37"]
}
  • namespace_id만 사용하는 경우, 해당 네임스페이스의 모든 프로젝트가 허용됩니다. 중첩 프로젝트는 포함되지 않으므로 필요한 경우 해당 네임스페이스 ID도 목록에 추가되어야 합니다.
  • namespace_idproject_id 둘 다 사용하는 경우, Vault는 먼저 프로젝트의 네임스페이스가 namespace_id에 있는지 확인한 후 project_id에 있는지 확인합니다.

token_explicit_max_ttl은 인증에 성공한 후 Vault에 의해 발급된 토큰의 하드 수명 한도를 60초로 지정합니다.

user_claim은 성공적인 로그인 시 Vault에서 생성된 Identity 별칭에 대한 이름을 지정합니다.

bound_claims_typebound_claims 값의 해석을 구성합니다. glob로 설정하면 *이 임의의 문자와 일치하도록 glob으로 해석됩니다.

위의 표(섹션 how-it-works)에 나열된 클레임 필드는 Vault의 정책 경로 템플릿 작성을 위해 JWT 인증의 액세서 이름을 사용하여 액세스할 수도 있습니다. 템플릿에 지정된 이름을 포함하는 마운트 액세서 이름(ACCESSOR_NAME)은 vault auth list를 실행하여 검색할 수 있습니다.

프로젝트 경로라는 명명된 메타데이터 필드를 활용한 정책 템플릿 예제:

path "secret/data/{{identity.entity.aliases.ACCESSOR_NAME.metadata.project_path}}/staging/*" {
  capabilities = [ "read" ]
}

이 템플릿에 대한 지원을 위한 역할 예제는 별칭을 통해 클레임 필드 project_path를 메타데이터 필드로 매핑하는 클레임 매핑(configuration)을 사용하여 작성됩니다(claim_mappings 구성).

{
  "role_type": "jwt",
  ...
  "claim_mappings": {
    "project_path": "project_path"
  }
}

전체 옵션 리스트는 Vault의 역할 생성 문서에서 확인할 수 있습니다.

경고: 제공된 클레임 중 하나를 사용하여 역할을 프로젝트 또는 네임스페이스에 제한하는 것이 좋습니다(예: project_id 또는 namespace_id). 그렇지 않으면 해당 인스턴스에서 생성된 어떤 JWT도 이 역할을 사용하여 인증할 수 있습니다.

이제 JWT Authentication 메소드를 구성하세요:

$ vault write auth/jwt/config \
    oidc_discovery_url="https://gitlab.example.com" \
    bound_issuer="https://gitlab.example.com"

bound_issuer를 통해 이 메소드를 사용하여 인증할 수 있는 JWT는 발급자(즉, iss 클레임)가 gitlab.example.com으로 설정된 JWT만 해당되며, 토큰을 검증하기 위해 oidc_discovery_url(https://gitlab.example.com)을 사용해야 합니다.

사용 가능한 구성 옵션 전체 목록은 Vault의 API 문서를 참조하세요.

GitLab에서 Vault 서버에 대한 다음 CI/CD 변수를 만들어서 관련 정보를 제공하세요:

  • VAULT_SERVER_URL - Vault 서버의 URL. 예: https://vault.example.com:8200.
  • VAULT_AUTH_ROLE - 선택 사항. 인증을 시도할 때 사용할 역할입니다. 역할이 지정되지 않은 경우, Vault는 인증 메소드가 구성된 기본 역할을 사용합니다.
  • VAULT_AUTH_PATH - 선택 사항. 인증 메소드가 장착되어 있는 경로입니다. 기본값은 jwt입니다.
  • VAULT_NAMESPACE - 선택 사항. 시크릿 읽기 및 인증에 사용할 Vault Enterprise 네임스페이스입니다. 네임스페이스가 지정되지 않으면 Vault는 루트(/) 네임스페이스를 사용합니다. 이 설정은 Vault 오픈 소스에서는 무시됩니다.

다음 default 브랜치에 대해 실행되는 작업은 secret/myproject/staging/의 시크릿을 읽을 수 있지만, secret/myproject/production/의 시크릿은 읽을 수 없습니다:

job_with_secrets:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.com
  secrets:
    STAGING_DB_PASSWORD:
      vault: secret/myproject/staging/db/password@secrets # $VAULT_ID_TOKEN을 사용하여 인증
  script:
    - access-staging-db.sh --token $STAGING_DB_PASSWORD

이 예제에서:

  • id_tokens - OIDC 인증에 사용되는 JSON 웹 토큰(JWT)입니다. aud 클레임은 Vault JWT 인증 메소드의 bound_audiences 매개변수와 일치하도록 설정됩니다.
  • @secrets - 보안 엔진이 활성화된 Vault 이름.
  • secret/myproject/staging/db - Vault에서의 시크릿 경로 위치.
  • password - 참조된 시크릿에서 가져올 필드입니다.

Vault 시크릿의 토큰 액세스 제한

Vault 보호 및 GitLab 기능을 사용하여 Vault 시크릿의 ID 토큰 액세스를 제어할 수 있습니다. 예를 들어 다음과 같이 토큰을 제한할 수 있습니다:

  • 특정 ID 토큰 aud 클레임을 위한 Vault 바운드 오디언스 사용.
  • group_claim을 사용하여 특정 그룹을 위한 Vault 바운드 클레임 사용.
  • 특정 사용자의 user_loginuser_email에 기반하여 Vault 바운드 클레임의 값을 하드코딩.
  • 토큰의 TTL을 지정된대로 token_explicit_max_ttl에 설정하여 토큰이 인증 후 만료되도록 함.
  • GitLab 보호 브랜치에 JWT의 범위를 지정함으로써 프로젝트 사용자의 하위 집합으로 제한함.
  • GitLab 보호 태그에 JWT의 범위를 지정함으로써 프로젝트 사용자의 하위 집합으로 제한함.

문제 해결

The secrets provider can not be found. Check your CI/CD variables and try again. 메시지

해시코프 Vault에 액세스하도록 구성된 작업을 시작하려고 시도할 때 다음 오류 메시지를 받을 수 있습니다:

The secrets provider can not be found. Check your CI/CD variables and try again.

필수 변수가 정의되지 않아 작업을 만들 수 없습니다:

  • VAULT_SERVER_URL