CI/CD 외 워크로드에서 시크릿 접근하기

CI/CD 외 워크로드에서 시크릿 접근하기 (Access secrets from non-CI/CD workloads)

CI/CD job은 GitLab Runner를 통해 GitLab Secrets Manager 시크릿을 읽어요. 그런데 CI/CD 밖의 워크로드, 예를 들어 Kubernetes 애플리케이션이나 인프라 코드(IaC) 도구는 Secrets Manager API를 통해 시크릿을 읽습니다. 읽기 요청이 바로 OpenBao 백엔드로 가기 때문에, 시크릿 사용 가능 여부가 GitLab 애플리케이션에 의존하지 않아요.

출처: 문서

본문

액세스 토큰 흐름 (Access token flow)

  1. 클라이언트가 api 스코프가 있는 개인 액세스 토큰, 서비스 계정 토큰, 또는 프로젝트·그룹 액세스 토큰으로 GitLab에 인증합니다.
  2. 클라이언트가 Secrets Manager API를 호출해서 수명이 짧은 액세스 토큰을 발급(mint)받아요. 응답에는 토큰과 OpenBao 연결 정보가 포함됩니다.
  3. 클라이언트가 토큰을 OpenBao 백엔드에 제시해서 시크릿 값을 읽어요.

액세스 토큰은 5분 후 만료됩니다. OpenBao가 Vault API를 구현하기 때문에, HashiCorp Vault 호환 클라이언트라면 무엇이든 토큰을 제시할 수 있어요. 모든 OpenBao 연결 정보는 발급 응답에서 오므로, 네임스페이스·마운트·인증 경로를 직접 구성할 필요가 없습니다.

사전 요구사항 (Prerequisites)

  • 프로젝트 또는 그룹에 대해 Secrets Manager가 활성화되어 있어야 해요.
  • Secrets Manager가 GitLab 19.2 이상에서 프로비저닝되어 있어야 해요.
  • api 스코프가 있는 개인 액세스 토큰, 프로젝트·그룹 액세스 토큰, 또는 서비스 계정 토큰으로 인증해야 해요.
  • 역할이 최소 Reporter여야 해요.
  • 시크릿 값을 읽으려면 해당 시크릿에 대한 읽기 값(read value) 권한이 부여되어 있어야 해요. Reporter 역할만으로는 시크릿 값이 노출되지 않습니다.

[!NOTE] GitLab 19.2 이전에 프로비저닝된 Secrets Manager는 외부 요청이나 서비스의 접근을 지원하지 않아요. 외부 접근을 활성화하려면 외부 요청에서 시크릿 접근 활성화 문서를 참고하세요.

시크릿 읽기 (Read a secret)

이 예시는 프로젝트용 액세스 토큰을 발급하고, OpenBao에 인증한 뒤 시크릿 값을 읽는 과정을 보여줘요.

  1. 액세스 토큰을 발급합니다. 인증에 쓰는 토큰은 반드시 api 스코프가 있어야 해요.
RESPONSE=$(curl --silent --request POST \
  --header "PRIVATE-TOKEN: <your_access_token>" \
  --url "https://gitlab.example.com/api/v4/projects/<project_id>/secrets_manager/access_token")

응답에는 provider.vault 객체에 server, namespace, path, secrets_path, auth.jwt 정보가, 그리고 수명이 짧은 token이 포함됩니다.

  1. 반환된 토큰으로 OpenBao에 인증하고 값을 읽습니다.
SERVER=$(echo "$RESPONSE" | jq --raw-output .provider.vault.server)
NAMESPACE=$(echo "$RESPONSE" | jq --raw-output .provider.vault.namespace)
MOUNT=$(echo "$RESPONSE" | jq --raw-output .provider.vault.path)
SECRETS_PATH=$(echo "$RESPONSE" | jq --raw-output .provider.vault.secrets_path)
AUTH_PATH=$(echo "$RESPONSE" | jq --raw-output .provider.vault.auth.jwt.path)
ROLE=$(echo "$RESPONSE" | jq --raw-output .provider.vault.auth.jwt.role)
JWT=$(echo "$RESPONSE" | jq --raw-output .provider.vault.auth.jwt.token)

# Exchange the JWT for a short-lived OpenBao token.
VAULT_TOKEN=$(curl --silent --request POST \
  --header "X-Vault-Namespace: $NAMESPACE" \
  --data "{\"role\":\"$ROLE\",\"jwt\":\"$JWT\"}" \
  "$SERVER/v1/auth/$AUTH_PATH/login" | jq --raw-output .auth.client_token)

# Read the secret value.
curl --silent \
  --header "X-Vault-Token: $VAULT_TOKEN" \
  --header "X-Vault-Namespace: $NAMESPACE" \
  "$SERVER/v1/$MOUNT/data/$SECRETS_PATH/<secret_name>"

GitLab.com에서 serverhttps://secrets.gitlab.com이에요. GitLab Self-Managed에서는 server가 인스턴스에 구성된 OpenBao URL입니다. 전체 요청·응답 형식은 Secrets Manager API 문서를 참고하세요.

Vault CLI와 함께 사용 (Use with the Vault CLI)

OpenBao가 Vault API를 구현하므로 응답에서 얻은 값으로 Vault CLI를 쓸 수 있어요.

export VAULT_ADDR="<server>"
export VAULT_NAMESPACE="<namespace>"

# Exchange the minted JWT for an OpenBao token, then export it.
vault write "auth/<auth_jwt_path>/login" role=<role> jwt=<token>
export VAULT_TOKEN="<client_token>"

# Read the secret value.
vault kv get -mount=<path> "<secrets_path>/<secret_name>"

External Secrets Operator와 함께 사용 (Use with the External Secrets Operator)

External Secrets Operator는 HashiCorp Vault 공급자를 통해 GitLab 시크릿을 Kubernetes 시크릿으로 동기화해요. 클러스터의 CronJob 같은 워크로드는 Kubernetes 시크릿에 신선한 액세스 토큰을 유지하고, 운영자는 그 토큰을 읽어 OpenBao에 인증합니다. 발급 응답을 SecretStore에 매핑하고 각 시크릿을 <secrets_path>/<secret_name>으로 참조하세요.

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: gitlab-secrets-manager
  namespace: my-app
spec:
  provider:
    vault:
      server: https://secrets.gitlab.com     # provider.vault.server
      path: secrets/kv                        # provider.vault.path
      version: v2
      namespace: org_5/group_42/project_99    # provider.vault.namespace
      auth:
        jwt:
          path: api_jwt/cel                   # provider.vault.auth.jwt.path
          role: all_api                       # provider.vault.auth.jwt.role
          secretRef:
            name: gitlab-access-token         # Kubernetes secret holding the minted token
            key: token
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: my-secret
  namespace: my-app
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: gitlab-secrets-manager
    kind: SecretStore
  target:
    name: synced-secret
  data:
    - secretKey: value
      remoteRef:
        key: explicit/<secret_name>           # <secrets_path>/<secret_name>
        property: value

액세스 토큰은 5분 후 만료되므로, 워크로드는 gitlab-access-token Kubernetes 시크릿을 만료 전에 갱신해야 해요. 네이티브 Kubernetes 통합은 epic 20382에 제안되어 있습니다.

인증 흐름 (Authentication flow)

External Secrets Operator와 함께 쓸 때의 인증 흐름을 다이어그램으로 보면 이렇습니다.

sequenceDiagram
    accTitle: Access token flow between Workload refresher, Secrets Manager API, ESO, and OpenBao
    accDescr: A workload refresher mints a short-lived access token and stores it in a Kubernetes secret. ESO reads the token, authenticates to OpenBao with it, reads the secret value, and writes it to a synced Kubernetes secret on each sync interval.
    participant W as Workload <br/> refresher
    participant API as Secrets Manager API
    participant KAuth as K8s Secret <br/> auth token
    participant ESO as ESO
    participant OB as OpenBao
    participant KApp as K8s Secret <br/> synced

    W->>API: Mint access token
    API-->>W: Short-lived token
    W->>KAuth: Store token

    loop Sync interval
        ESO->>KAuth: Read token
        ESO->>OB: JWT login
        OB-->>ESO: Client token
        ESO->>OB: Read KV path
        OB-->>ESO: Secret value
        ESO->>KApp: Write value
    end

    Note over W,API: Access token expires in 5 min

Terraform과 함께 사용 (Use with Terraform)

Terraform 또는 OpenTofu 구성은 데이터 소스로 GitLab 시크릿을 읽을 수 있어요. Terraform이 액세스 토큰을 직접 발급할 수는 없으므로, external 데이터 소스가 Secrets Manager API를 호출해서 provider.vault 연결 정보를 반환하는 스크립트를 실행해요. 그다음 Vault 공급자가 발급된 JWT로 인증하고 시크릿을 읽습니다. 이 스크립트는 토큰을 발급하고 연결 정보를 JSON으로 출력하며, GITLAB_TOKEN 환경 변수에서 GitLab 토큰을 읽어요.

#!/usr/bin/env bash
# scripts/mint_token.sh
set -euo pipefail
eval "$(jq --raw-output '@sh "PROJECT_ID=\(.project_id)"')"

curl --silent --request POST \
  --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
  --url "https://gitlab.example.com/api/v4/projects/${PROJECT_ID}/secrets_manager/access_token" \
  | jq '{
      server:       .provider.vault.server,
      namespace:    .provider.vault.namespace,
      mount:        .provider.vault.path,
      secrets_path: .provider.vault.secrets_path,
      auth_path:    .provider.vault.auth.jwt.path,
      role:         .provider.vault.auth.jwt.role,
      jwt:          .provider.vault.auth.jwt.token
    }'

external 데이터 소스에서 스크립트를 참조하고, Vault 공급자를 구성한 뒤 시크릿을 읽습니다.

data "external" "gitlab_secrets_token" {
  program = ["bash", "${path.module}/scripts/mint_token.sh"]

  query = {
    project_id = var.gitlab_project_id
  }
}

provider "vault" {
  address   = data.external.gitlab_secrets_token.result.server
  namespace = data.external.gitlab_secrets_token.result.namespace

  auth_login_jwt {
    mount = data.external.gitlab_secrets_token.result.auth_path
    role  = data.external.gitlab_secrets_token.result.role
    jwt   = data.external.gitlab_secrets_token.result.jwt
  }
}

data "vault_kv_secret_v2" "my_secret" {
  mount = data.external.gitlab_secrets_token.result.mount
  name  = "${data.external.gitlab_secrets_token.result.secrets_path}/<secret_name>"
}

output "secret_value" {
  value     = data.vault_kv_secret_v2.my_secret.data["value"]
  sensitive = true
}

발급된 토큰은 5분 동안 유효하므로 terraform apply는 그 시간 안에 실행되어야 해요. 네이티브 GitLab Terraform 공급자 통합은 epic 21177에 제안되어 있습니다.

더 알아보기

핵심은 '발급(5분 유효 토큰) → OpenBao 인증 → KV 경로 읽기'라는 흐름이에요. 연결 정보가 발급 응답에 다 들어오므로 경로를 직접 조립할 필요가 없습니다. Vault CLI, External Secrets Operator, Terraform 등 환경에 맞는 클라이언트를 골라 쓰면 CI/CD 밖의 워크로드에서도 시크릿을 안전하게 읽을 수 있어요. 토큰 만료 시점을 잘 관리해서 워크로드가 갱신 타이밍을 놓치지 않도록 하는 게 중요합니다.