튜토리얼: HashiCorp Vault로 인증하고 비밀 읽기

튜토리얼: HashiCorp Vault로 인증하고 비밀 읽기

GitLab CI/CD에서 HashiCorp의 Vault를 사용해 인증하고, 설정하고, 비밀(secret)을 읽는 방법을 보여주는 튜토리얼이에요. CI/CD 작업이 스테이징·프로덕션 데이터베이스 비밀번호 같은 민감 정보를 안전하게 꺼내 쓰도록 구성하는 전체 흐름을 옆에서 설명해 주는 방식으로 진행할게요.

ID 토큰(ID token) 기반 인증을 이용해 Vault의 역할(role)과 정책(policy)을 매핑하는 방식이 핵심이에요. Vault 1.2.0 이상이 필요하고, GitLab Premium·Ultimate 요금제에서 사용할 수 있어요.

출처: 문서

본문

이 튜토리얼은 GitLab CI/CD와 Vault에 익숙하다고 가정해요. 따라 하려면 다음이 필요해요.

  • GitLab 계정.
  • 실행 중인 Vault 서버(최소 v1.2.0)에 접근해 인증을 구성하고 역할과 정책을 만들 수 있어야 해요. HashiCorp Vault 기준으로 오픈 소스 또는 엔터프라이즈 버전이면 됩니다.
  • 아래 예시의 vault.example.com URL을 여러분의 Vault 서버 URL로, gitlab.example.com을 여러분의 GitLab 인스턴스 URL로 바꿔야 해요.

Vault 구성하기

JWT는 리소스에 접근 권한을 부여할 수 있는 자격 증명이에요. 붙여 넣는 곳을 조심하세요!

여러분이 스테이징 및 프로덕션 데이터베이스의 비밀번호를 Vault 서버에 저장하는 시나리오를 생각해볼게요. 이 시나리오는 KV v2 시크릿 엔진을 사용한다고 가정해요. KV v1을 사용한다면 아래 정책 경로에서 /data/를 제거하고, CI/CD 작업을 구성하는 방법을 확인하세요.

vault kv get 명령으로 비밀번호를 조회할 수 있어요.

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

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

스테이징 비밀번호는 pa$$w0rd, 프로덕션 비밀번호는 real-pa$$w0rd예요.

Vault 서버를 구성하려면 먼저 JWT Auth 메서드를 활성화하세요.

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

그런 다음 이 비밀들을 읽을 수 있는 정책을 (비밀 하나당 하나씩) 만드세요.

$ vault policy write myproject-staging - # Policy name: myproject-staging
#
# Read-only permission on 'secret/data/myproject/staging/*' path
path "secret/data/myproject/staging/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-staging

$ vault policy write myproject-production - # Policy name: myproject-production
#
# Read-only permission on 'secret/data/myproject/production/*' path
path "secret/data/myproject/production/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-production

JWT를 이 정책들과 연결하는 역할(role)도 필요해요.

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

$ vault write auth/jwt/role/myproject-staging -
{
  "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 -
{
  "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 별칭(alias)의 이름을 지정해요.
  • bound_claims_typebound_claims 값의 해석 방식을 구성해요. glob으로 설정하면 값들이 glob으로 해석되며, *는 임의 개수의 문자와 일치합니다.

클레임 필드는 Vault의 JWT 인증의 액세서(accessor) 이름을 사용해 Vault의 정책 경로 템플릿 목적으로도 접근할 수 있어요. 마운트 액세서 이름(아래 예시의 ACCESSOR_NAME)은 vault auth list를 실행해 얻을 수 있어요.

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

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

claim_mappings 구성을 통해 클레임 필드 project_path를 메타데이터 필드로 매핑하는 이전 템플릿 정책을 지원하는 역할 예시:

{
  "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는 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 JWT 인증 역할의 이름. 이 튜토리얼에서는 이미 myproject-stagingmyproject-production이라는 두 역할을 만들었어요. 역할을 지정하지 않으면 Vault는 인증 메서드 구성 시 지정된 기본 역할을 사용해요.
  • VAULT_AUTH_PATH: 선택 사항. 인증 메서드가 마운트된 경로. 기본값은 jwt예요.
  • VAULT_NAMESPACE: 선택 사항. 비밀 읽기와 인증에 사용할 Vault Enterprise 네임스페이스. 네임스페이스를 지정하지 않으면 Vault는 루트(/) 네임스페이스를 사용해요. 이 설정은 Vault 오픈 소스에서는 무시됩니다.

자동 ID 토큰 인증

다음 작업은 기본 브랜치에서 실행될 때 secret/myproject/staging/ 아래의 비밀은 읽을 수 있지만 secret/myproject/production/ 아래의 비밀은 읽을 수 없어요.

job_with_secrets:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.com
  secrets:
    STAGING_DB_PASSWORD:
      vault: myproject/staging/db/password@secret  # translates to a path of 'secret/myproject/staging/db' and field 'password'. Authenticates using $VAULT_ID_TOKEN.
  script:
    - access-staging-db.sh --token $STAGING_DB_PASSWORD

이 예시에서:

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

ID 토큰이 두 개 이상 정의되면 token 키워드로 어떤 토큰을 사용할지 지정하세요. 예를 들어:

job_with_secrets:
  id_tokens:
    FIRST_ID_TOKEN:
      aud: https://first.service.com
    SECOND_ID_TOKEN:
      aud: https://second.service.com
  secrets:
    FIRST_DB_PASSWORD:
      vault: first/db/password
      token: $FIRST_ID_TOKEN
    SECOND_DB_PASSWORD:
      vault: second/db/password
      token: $SECOND_ID_TOKEN
  script:
    - access-first-db.sh --token $FIRST_DB_PASSWORD
    - access-second-db.sh --token $SECOND_DB_PASSWORD

Vault 1.17부터, JWT에 aud 클레임이 포함되어 있으면 JWT 인증 로그인은 역할에 바운드 오디언스가 있어야 해요. aud 클레임은 단일 문자열 또는 문자열 목록일 수 있어요.

수동 인증

ID 토큰을 사용해 HashiCorp Vault에 수동으로 인증할 수도 있어요. 예를 들어:

manual_authentication:
  variables:
    VAULT_ADDR: http://vault.example.com:8200
  image: vault:latest
  id_tokens:
    VAULT_ID_TOKEN:
      aud: http://vault.example.com
  script:
    - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-example jwt=$VAULT_ID_TOKEN)"
    - export PASSWORD="$(vault kv get -field=password secret/myproject/example/db)"
    - my-authentication-script.sh $VAULT_TOKEN $PASSWORD

Vault 비밀에 대한 토큰 접근 제한

Vault 보호 기능과 GitLab 기능을 사용해 Vault 비밀에 대한 ID 토큰 접근을 제어할 수 있어요. 예를 들어 토큰을 다음으로 제한하세요.

문제 해결

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

HashiCorp Vault에 접근하도록 구성된 작업을 시작하려 할 때 이 오류를 받을 수 있어요.

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

필요한 변수가 정의되지 않아 작업을 만들 수 없어요.

  • VAULT_SERVER_URL

api error: status code 400: missing role 오류

HashiCorp Vault에 접근하도록 구성된 작업을 시작하려 할 때 missing role 오류를 받을 수 있어요. VAULT_AUTH_ROLE 변수가 정의되지 않아 작업이 Vault 서버로 인증할 수 없기 때문일 수 있어요.

audience claim does not match any expected audience 오류

YAML 파일에 지정된 ID 토큰의 aud: 클레임 값과 JWT 인증에 사용되는 rolebound_audiences 매개변수 값이 일치하지 않으면 이 오류를 받을 수 있어요.

invalid audience (aud) claim: audience claim does not match any expected audience

이 값들이 동일한지 확인하세요.

더 알아보기

인증 방식을 더 세밀하게 조정하고 싶다면 ID 토큰 인증 문서를 살펴보세요. 기존 CI_JOB_JWT 방식에서 ID 토큰으로 마이그레이션하는 법은 HashiCorp Vault 구성을 ID 토큰으로 업데이트하기 튜토리얼에서 다루고 있으니 함께 보시면 좋아요.