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

Tier: Premium, Ultimate 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 인증 로그인은 역할에 바인딩된 청중을 요구합니다
JWT에 aud 클레임이 포함된 경우. aud 클레임은 단일 문자열 또는 문자열 목록이 될 수 있습니다.

이 튜토리얼에서는 GitLab CI/CD에서 HashiCorp Vault로 인증, 구성 및 비밀을 읽는 방법을 보여줍니다.

사전 요구 사항

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

따라서 다음이 필요합니다:

  • GitLab 계정.
  • 인증을 구성하고 역할 및 정책을 생성하기 위해 실행 중인 Vault 서버에 대한 액세스 (최소 v1.2.0).
    HashiCorp Vault의 경우 오픈 소스 또는 엔터프라이즈 버전일 수 있습니다.
note
아래의 vault.example.com URL을 Vault 서버의 URL로 교체해야 하며,
gitlab.example.com을 GitLab 인스턴스의 URL로 교체해야 합니다.

작동 방식

ID 토큰은 제3자 서비스와의 OIDC 인증에 사용되는 JSON 웹 토큰(JWT)입니다.
작업에 적어도 하나의 ID 토큰이 정의되어 있으면 secrets 키워드는 자동으로 해당 토큰을 사용하여 Vault에 인증합니다.

JWT에 포함된 필드는 다음과 같습니다:

Field When Description
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
ref_type 항상 Git ref 유형, branch 또는 tag 중 하나
ref_path 항상 작업을 위한 완전한 자격을 갖춘 ref. 예를 들어, refs/heads/main. 소개됨 GitLab 16.0에서.
ref_protected 항상 이 Git ref가 보호된 경우 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에서)

Example JWT payload:

{
  "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를 사용하여 JWT 인증 방법을 허용하도록 구성된 Vault 서버에 인증할 수 있습니다.
GitLab 인스턴스의 기본 URL(예: https://gitlab.example.com)을 Vault 서버에 oidc_discovery_url로 제공하십시오.
서버는 그러면 귀하의 인스턴스에서 토큰을 검증하기 위한 키를 검색할 수 있습니다.

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와 이 정책을 연결하는 역할도 필요합니다.

예를 들어, myproject-staging이라는 이름의 스테이징 역할이 있습니다.
바운드 클레임
정확히 22라는 ID를 가진 프로젝트의 main 브랜치에서만 정책을 사용할 수 있도록 설정됩니다:

$ 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

그리고 myproject-production이라는 이름의 프로덕션 역할이 있습니다.
이 역할의 bound_claims 섹션은 auto-deploy-* 패턴과 일치하는
보호된 브랜치만 비밀에 접근할 수 있도록 허용합니다.

$ 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에 포함된 클레임 중 어떤 것도 바운드 클레임 값 목록과 일치시킬 수 있습니다.
예를 들어:

"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_type
bound_claims 값의 해석을 구성합니다. glob으로 설정하면, 값들이 glob으로 해석되며
*는 임의의 수의 문자를 매치합니다.

위의 표에 나열된 클레임 필드는
Vault의 정책 경로 템플릿화에도 접근할 수 있습니다.
JWT 인증의 접근자 이름을 사용하여 이를 수행합니다.
마운트 접근자 이름
(ACCESSOR_NAME, 아래 예시)을 얻으려면 vault auth list를 실행하세요.

명명된 메타데이터 필드 project_path를 사용하는 정책 템플릿 예시:

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

위의 템플릿화된 정책을 지원하기 위한 역할 예시로,
클레임 필드 project_pathclaim_mappings
구성을 통해 메타데이터 필드로 매핑합니다:

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

전체 옵션 목록은 Vault의 Create Role 문서를 참조하세요.

경고:
항상 제공된 클레임 중 하나를 사용하여 역할을 프로젝트 또는 네임스페이스로 제한하세요
(예: project_id 또는 namespace_id). 그렇지 않으면 이 인스턴스에서 생성된 모든 JWT가
이 역할을 사용하여 인증될 수 있습니다.

이제 JWT 인증 방법을 구성합니다:

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

bound_issuer
오직 발급자(즉, 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 Open Source에서는 무시됩니다.

기본 브랜치에서 실행될 때, 다음 작업은 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에서 지정된 Vault 시간 제한 설정하기, 여기서 토큰은 인증 후 만료됩니다.

  • JWT를 GitLab 보호된 브랜치에 스코프 한정하기, 이는 프로젝트 사용자 집합에 제한됩니다.

  • JWT를 GitLab 보호된 태그에 스코프 한정하기, 이는 프로젝트 사용자 집합에 제한됩니다.

문제 해결

비밀 제공자를 찾을 수 없습니다. CI/CD 변수 확인 후 다시 시도하세요. 메시지

HashiCorp Vault에 접근하도록 구성된 작업을 시작할 때 이 오류가 발생할 수 있습니다:

비밀 제공자를 찾을 수 없습니다. CI/CD 변수를 확인 후 다시 시도하세요.

필요한 변수가 정의되지 않았기 때문에 작업을 생성할 수 없습니다:

  • VAULT_SERVER_URL