HashiCorp Vault를 사용한 인증 및 비밀 가져오기

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

경고: CI_JOB_JWT로의 인증은 GitLab 15.9에서 사용 중단되었으며 해당 토큰은 GitLab 17.0에서 제거될 예정입니다. 대신 HashiCorp Vault로 자동 ID 토큰 인증을 사용하세요. 이 페이지에서 보여지는 대로 사용하세요.

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

전제 조건

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

따라오기 위해서는 다음이 있어야 합니다:

  • GitLab 계정.
  • 구동 중인 Vault 서버에 액세스 (최소 v1.2.0)하여 인증 구성 및 역할 및 정책 생성. HashiCorp Vaults의 경우, 오픈 소스 또는 엔터프라이즈 버전이 가능합니다.

참고: 아래의 vault.example.com URL을 귀하의 Vault 서버의 URL로, 그리고 gitlab.example.com을 귀하의 GitLab 인스턴스의 URL로 대체해야 합니다.

작동 방식

ID 토큰은 제3자 서비스와의 OIDC 인증에 사용되는 JSON Web Token (JWT)입니다. 작업에 적어도 하나의 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 작업이 환경을 지정함 이 작업이 지정하는 환경 (등장 in GitLab 13.9)
environment_protected 작업이 환경을 지정함 지정한 환경이 보호되어있으면 true, 그렇지 않으면 false (등장 in GitLab 13.9)
deployment_tier 작업이 환경을 지정함 이 작업이 지정한 환경의 배포 티어 (등장 in GitLab 15.2)
environment_action 작업이 환경을 지정함 작업에서 지정한 환경 액션 (environment:action) (등장 in 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",
  "environment": "production",
  "environment_protected": "true",
  "environment_action": "start"
}

이 JWT는 RS256을 사용하여 인코딩되고 전용 프라이빗 키로 서명됩니다. 토큰의 만료 시간은 작업의 타임아웃이 지정된 경우 해당 시간으로 설정되며, 그렇지 않은 경우 5분입니다. 이 토큰을 서명하는 데 사용된 키는 언제든지 변경될 수 있습니다. 그런 경우 작업을 다시 시도하면 현재 서명 키를 사용하여 새 JWT를 생성합니다.

이 JWT를 사용하여 구성된 Vault 서버로의 인증에 사용할 수 있습니다. 귀하의 GitLab 인스턴스의 기본 URL을 Vault 서버에 oidc_discovery_url로 제공합니다 (예: https://gitlab.example.com). 그러면 서버가 귀하의 인스턴스에서 토큰을 유효성 검사하기 위한 키를 검색할 수 있습니다.

Vault에서 역할을 구성할 때 bound claims을 사용하여 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
# Policy name: myproject-staging
#
# 'secret/myproject/staging/*' 경로에 대한 읽기 전용 권한
path "secret/myproject/staging/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-staging

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

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

스테이징을 위한 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_claims": {
    "project_id": "22",
    "ref": "master",
    "ref_type": "branch"
  }
}
EOF

프로덕션을 위한 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_claims_type": "glob",
  "bound_claims": {
    "project_id": "22",
    "ref_protected": "true",
    "ref_type": "branch",
    "ref": "auto-deploy-*"
  }
}
EOF

이 예시에서는 bound claims을 사용하여 특정 claim 값과 일치하는 JWT만 인증할 수 있도록 지정합니다.

protected branches와 함께 사용하면 누가 인증하고 비밀을 읽을 수 있는지를 제한할 수 있습니다.

JWT에 포함된 claim 중 어떤 것이든 bound 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만 사용되면 해당 namespace의 모든 프로젝트가 허용됩니다.
  • namespace_idproject_id 모두 사용되면, Vault는 먼저 프로젝트의 namespace가 namespace_id에 있는지 확인합니다. 그렇지 않으면 프로젝트가 project_id에 있는지 확인합니다.

token_explicit_max_ttl은 Vault에서 성공적으로 인증된 후 발급된 토큰이 하드 만료 시간 제한이 60초라는 것을 지정합니다.

user_claim은 Vault에서 성공적인 로그인 후 생성된 Identity alias의 이름을 지정합니다.

bound_claims_typebound_claims 값의 해석을 구성합니다. glob로 설정되면 값은 glob으로 해석되며, *는 모든 문자에 일치합니다.

위의 표에 나와있는 claim 필드는 Vault의 정책 경로 템플릿 작성을 위해 JWT auth 내의 accessor 이름으로 액세스할 수 있습니다. (예: ACCESSOR_NAME의 케이스 액세서 이름)은 vault auth list를 실행하여 검색할 수 있습니다.

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

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

위의 템플릿에 대한 지원을 위한 역할 예제는, claim_mappings 구성을 통해 claim 필드 project_path를 메타데이터 필드로 매핑합니다.

전체 옵션 목록은 Vault의 역할 생성 문서를 참조하십시오.

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

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

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

bound_issuer는 인증에 사용 가능한 발급자(iss claim)가 gitlab.example.com으로 설정된 JWT만 이 방법을 사용하여 인증할 수 있음을 지정하며, 토큰을 유효성 검사하기 위해 oidc_discovery_url(https://gitlab.example.com)을 사용해야 함을 지정합니다.

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

GitLab에서 CI/CD 변수를 만들어 Vault 서버에 대한 세부 정보를 제공합니다:

  • VAULT_SERVER_URL - Vault 서버의 URL, 예: https://vault.example.com:8200.
  • VAULT_AUTH_ROLE - 선택 사항. 인증을 시도할 때 사용할 역할. 역할이 지정되지 않은 경우, Vault는 인증 방법이 구성된 기본 역할(default role)을 사용합니다.
  • VAULT_AUTH_PATH - 선택 사항. 인증 방법이 마운트된 경로. 기본값은 jwt입니다.
  • VAULT_NAMESPACE - 선택 사항. 비밀 및 인증을 위해 사용할 Vault Enterprise namespace. 네임스페이스가 지정되지 않은 경우, Vault는 루트(/) 네임스페이스를 사용합니다. 이 설정은 Vault 오픈 소스에서 무시됩니다.

기본 브랜치에서 실행되면 다음 작업은 secret/myproject/staging/에 있는 비밀을 읽을 수 있지만, secret/myproject/production/에 있는 비밀은 읽을 수 없습니다:

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

이 예에서:

  • @secrets - Secrets Engines가 활성화된 Vault 이름입니다.
  • secret/myproject/staging/db - Vault에서 비밀의 위치 경로입니다.
  • password - 참조된 비밀 내에서 가져올 필드입니다.

Vault 시크릿에 대한 토큰 액세스 제한

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

  • group_claim를 사용하여 특정 그룹에 대한 Vault 바운드 클레임 사용
  • 특정 사용자의 user_loginuser_email에 기반하여 Vault 바운드 클레임의 값을 하드코딩
  • 토큰의 TTL에 대한 Vault 시간 제한 설정으로, 인증 후 토큰이 만료되도록 token_explicit_max_ttl에 명시된 대로
  • 프로젝트 사용자의 일부에 제한된 GitLab 보호 브랜치로 JWT를 범위 지정
  • 프로젝트 사용자의 일부에 제한된 GitLab 보호 태그로 JWT를 범위 지정