Vault

Vault-Powered CI/CD Patterns: Secret Retrieval at Build and Deploy Time

2026-04-19
NicheeLab Editorial Team

The fundamental rule is to leave no secrets behind in repositories or runners, and to consume short-lived tokens and dynamic secrets on the fly. Vault fits CI/CD naturally and handles authentication, retrieval, and auditing end to end.

This article focuses on stable patterns grounded in the official documentation. We prioritize designs that hold up across providers and versions, and the points most often probed on operations and certification exams.

Pipeline Overview and Separation of Duties

CI runners authenticate to Vault with short-lived credentials, fetch only the minimum required secrets on demand, and consume them in place. Never persist secrets in repositories, artifacts, or logs. When you need to hand off between stages, use response wrapping and keep the wrap token TTL short.

Separate retrieval paths and permissions between build (dependency fetch, container build) and deploy (Kubernetes/Helm, etc.). Build is mostly read; deploy injects into environments. Split their policies and enforce least privilege on each.

  • Treat secrets as pull-based (fetched on demand by the runner/Pod) and avoid push-based distribution
  • Use short-lived tokens with an explicit max_ttl, and keep renewability to the bare minimum
  • Always enable an audit device and correlate entries with the pipeline's request_id

Data flow: CI → Vault → Secret Engine → runtime environment

JWT/OIDC/AppRolesecretsauto-auth/sidecar/CSICI RunnerBuild / DeployVaultAuth + PolicyBuild Stagee.g. image buildKV v2DynamicDB / CloudDeploy StageHelm / ArgoK8s Workload (Pod)Vault Agent / InjectorCI Runner authenticates with Vault and feeds KV/dynamic secrets to Build/Deploy

Hand off between stages with response wrapping (for example, wrap KV data for 5 minutes)

vault write -field=wrapping_token -wrap-ttl=5m sys/wrapping/wrap \
  secret=@<(vault kv get -format=json kv/build/npmrc | jq -c '.data.data')

# 受け取り側ステージ(ラップ解除は 1 回のみ有効)
vault write sys/wrapping/unwrap wrapping_token=$WRAPPING_TOKEN > unwrapped.json
cat unwrapped.json | jq '.'

Choosing an Auth Pattern (AppRole / OIDC(JWT) / Kubernetes)

Pick the entry point from CI into Vault based on operability and trust boundaries. For SaaS CI (GitHub Actions and similar), OIDC(JWT) is the first choice. AppRole fits self-hosted runners, and Kubernetes auth is natural for Pods inside the cluster.

In every case, define least-privilege policies and keep token TTL and max_ttl short. Federation via OIDC removes the need for any long-lived shared secret, which also helps auditing.

  • OIDC: Vault verifies a signed JWT from an external IdP (role-level claim constraints)
  • AppRole: server-to-server with a RoleID and SecretID. Response wrapping is a prerequisite
  • Kubernetes: Vault verifies a ServiceAccount JWT; roles are constrained by Namespace and SA name
PatternPrimary use caseStrengthsWatch-outs
OIDC (JWT)SaaS CI (GitHub Actions, GitLab)No long-lived shared key; easy to auditHinges on IdP claim design and role constraints
AppRoleSelf-hosted runners / on-premSimple, with few dependenciesRequires careful SecretID protection and wrapped distribution
KubernetesPods (Injector / CSI / Agent)Per-Pod least privilege with automatic rotationManaging the scope of ServiceAccount/JWT

Log in to Vault via GitHub Actions OIDC and fetch KV

jobs:
  build:
    permissions:
      id-token: write   # OIDC 発行に必要
      contents: read
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Get OIDC token
        id: oidc
        run: echo "token=$(curl -H 'Authorization: bearer $ACTIONS_ID_TOKEN' 2>/dev/null)" >> $GITHUB_OUTPUT
        env:
          ACTIONS_ID_TOKEN: ${{ steps.generate_token.outputs.token }}
      - name: Vault Login (JWT)
        env:
          VAULT_ADDR: https://vault.example.com
          JWT: ${{ steps.oidc.outputs.token }}
        run: |
          vault write -format=json auth/jwt/login role=gha-build jwt="$JWT" > login.json
          export VAULT_TOKEN=$(jq -r .auth.client_token login.json)
          vault kv get -format=json kv/build/npmrc | jq -r .data.data.authToken >> $GITHUB_ENV

Build-Stage Secret Retrieval (Dependency Fetch and Container Build)

Emit npm/pip/maven credentials to a temporary file and delete them when the job ends. For container builds, use Docker BuildKit secrets so nothing lands in the Dockerfile in plaintext.

CI tokens should be non-renewable with a short TTL. Account for retries, but keep max_ttl tight so leaks via build caches or re-runs are unlikely.

  • Avoid embedding via ARG in the Dockerfile; use --secret instead
  • Do not write npmrc/pypirc outside the workspace and never echo them to logs
  • If needed, issue and revoke dynamic secrets (for example, ephemeral DB users) for every test run

Example of safely passing Vault-fetched secrets through BuildKit

# 1) CI で Vault から取得しファイル化(短命)
vault kv get -format=json kv/build/npmrc | jq -r .data.data.npmrc > .npmrc.tmp

# 2) BuildKit 経由で Dockerfile に渡す(イメージには残らない)
DOCKER_BUILDKIT=1 docker build \
  --secret id=npmrc,src=.npmrc.tmp \
  -t app:build .

# Dockerfile (抜粋)
# syntax=docker/dockerfile:1.6
RUN --mount=type=secret,id=npmrc,dst=/root/.npmrc \
    npm ci && rm -f /root/.npmrc

# 3) 後始末
shred -u .npmrc.tmp

Deploy Stage: Safe Delivery into Kubernetes

On Kubernetes, use the Vault Agent Injector (sidecar) or the CSI Driver to deliver secrets as files inside the Pod. Nothing lands in etcd in plaintext, which is safer than relying on Kubernetes Secrets alone.

Tie a ServiceAccount to a role through the Kubernetes auth method to enforce per-Pod least privilege. Let the Agent rotate credentials automatically and design the app to reload them via file watching — that is the practical pattern.

  • Injector: easy to adopt, renders files from templates, refreshes automatically via sidecar
  • CSI: mounted as a volume; easier to manage in environments with many secret types
  • Never put raw secrets into Helm values (use templates or external references)

Deployment example using Vault Agent Injector (template rendering)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "k8s-web"
    vault.hashicorp.com/agent-inject-secret-config.yaml: "kv/app/config"
    vault.hashicorp.com/agent-inject-template-config.yaml: |
      {{- with secret "kv/app/config" -}}
      DB_URL={{ .Data.data.DB_URL }}
      API_KEY={{ .Data.data.API_KEY }}
      {{- end -}}
spec:
  replicas: 2
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web }
    spec:
      serviceAccountName: web-sa
      containers:
        - name: app
          image: ghcr.io/acme/web:1.0
          envFrom:
            - secretRef: { name: dummy } # 実際の値はファイルから読み込む
          volumeMounts:
            - name: config
              mountPath: /run/secrets
      volumes:
        - name: config
          emptyDir: {}

Hardening and Auditing (TTL, Token Types, Wrapping, Leases)

Vault offers service tokens (renewable) and batch tokens (lightweight, non-renewable); batch tokens fit CI's one-shot use perfectly. Keep TTL and max_ttl short, and use periodic tokens when planned renewal is necessary.

Dynamic secrets are managed through leases — revoke them explicitly at job end or let them expire by TTL. Always enable an audit device so you have a record of who accessed which path under which policy.

  • At a minimum, run `audit enable file file_path=/var/log/vault_audit.log`
  • Use sys/wrapping for one-shot handoffs to shrink the exposure surface
  • Use `lease revoke -prefix` to reliably tear down dynamic users created for testing

Common operational commands (audit, tokens, leases)

# 監査ログを有効化(ファイル例)
vault audit enable file file_path=/var/log/vault_audit.log

# バッチトークン(軽量・非更新)を短 TTL で発行
vault token create -type=batch -policy=ci-readonly -ttl=15m -explicit-max-ttl=30m

# 動的シークレットの強制失効(接頭辞でまとめて)
vault lease revoke -prefix database/creds/ci-test

# レスポンスラッピング(5 分)で安全に受け渡し
vault write -wrap-ttl=5m sys/wrapping/wrap [email protected]

Operational Pitfalls and SLO Design

Heavy concurrency in CI spikes traffic to Vault. Use rate limiting, connection reuse, and the Agent cache to absorb it. Design HA (with an officially recommended storage backend) and auto-unseal to match your availability targets.

Define behavior under Vault outages. The Injector/Agent cache buys you some headroom, but when strong consistency is required, failing closed (halting the deploy) is the safer choice.

  • Configure exponential backoff and an overall timeout on the CI side
  • Smooth out load with the Agent cache and template min_refresh allocations
  • Align secret TTLs with your release window (the deploy slot)

Minimal Vault Agent configuration (auto_auth + cache)

auto_auth {
  method {
    type = "kubernetes"
    config = {
      role = "k8s-web"
    }
  }
  sink {
    type = "file"
    config = {
      path = "/home/vault/.token"
    }
  }
}
cache {
  use_auto_auth_token = true
}
template {
  source      = "/vault/templates/config.tpl"
  destination = "/run/secrets/config.env"
  # 更新間隔の下限を指定して過剰な再読込を抑制
  min_refresh_interval = "15s"
}

Check Your Understanding

Ops

問題 1

A CI build needs database credentials for unit tests. Requirements: no long-lived shared keys, automatic invalidation after the job ends, and a safe handoff between stages. Which approach fits best?

  1. Issue dynamic database secrets with a short TTL, and use response wrapping for stage handoff when needed
  2. Store credentials in an encrypted file in the Git repository and decrypt them at build time
  3. Reuse a 1-month periodic token across tests
  4. Bake them into the Docker image as environment variables and delete them after the build

正解: A

Vault dynamic secrets shine with short TTLs and automatic expiry, making them ideal for one-shot CI use. Handoffs between stages can be done safely via sys/wrapping response wrapping. The other options rely on long-lived keys or image embedding, which are inappropriate from both an exposure and auditing standpoint.

Frequently Asked Questions

Should I use OIDC or AppRole with GitHub Actions?

OIDC is generally preferred. Vault verifies a signed JWT from the IdP, so you do not need a long-lived shared secret and auditing is straightforward. On self-hosted runners or in environments without OIDC support, use AppRole and distribute the SecretID via response wrapping.

Should secrets be passed as environment variables or as files?

Both at build time and runtime, file delivery is preferred. Environment variables can leak via the process tree, crash dumps, or logs. Render them to files via Vault Agent/Injector templates and design the app to reload them.

How should deployments behave when Vault is down?

If confidentiality matters most, fail closed (abort deploys when Vault is unreachable). If availability matters more and brief staleness is acceptable, lean on Agent/Injector caching and reconcile with the real rotation later. Either way, leave an audit trail.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Vault

Vault Core Concepts: Sealed/Unsealed, Auth, Secrets (2026)

Vault fundamentals — sealed/unsealed state, auth methods, se...

Vault

Vault Operations Professional (VOP-003): Complete Guide (2026)

Pass the Vault Operations Professional exam — enterprise pat...

Vault

Vault Path-Based Routing: API URL Structure (2026)

How Vault's path-based routing works — mount points, sub-pat...

Vault

Vault Tokens: Auth Token Mechanics (2026)

Token fundamentals — service vs. batch tokens, accessor, ren...

Vault

Vault Token Types: Service, Batch, Periodic (2026)

Service vs. batch tokens compared — performance, ACL behavio...

Browse all Vault articles (101)
© 2026 NicheeLab All rights reserved.