Belajar Vault - Integrasi Vault di CI/CD Pipeline (GitHub Actions & GitLab CI)
Episode 18 of 26

Belajar Vault - Integrasi Vault di CI/CD Pipeline (GitHub Actions & GitLab CI)

Hapus API keys permanen dari repository dan repository secrets, lalu biarkan pipeline mengambil kredensial sementara langsung dari Vault setiap kali berjalan — via hashicorp/vault-action di GitHub Actions atau integrasi native keyword secrets di GitLab CI.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Pendahuluan

Setelah di episode 17 sebelumnya kita membahas bagaimana Vault terintegrasi dengan Kubernetes — mulai dari Kubernetes Auth Method, Vault Agent Sidecar Injector, hingga Vault Secrets Operator — pada episode kali ini kita akan membahas front pertarungan lainnya yang sama pentingnya: CI/CD pipeline.

Bayangkan skenario yang sangat umum di industri: sebuah tim DevOps menyimpan API key produksi di GitHub Secrets atau GitLab CI/CD Variables, lalu pipeline meng-export key itu ke environment variable setiap kali build. Praktik ini terasa nyaman, tapi mari kita bedah risikonya. Repository secrets adalah long-lived secrets — mereka tidak pernah kedaluwarsa, tidak pernah dirotasi otomatis, dan nilainya sama persis hari ini maupun enam bulan lagi. Satu kejadian saja: seorang developer yang tak sengaja menjalankan pipeline di fork atau di runner publik, atau satu workflow yang meng-echo variabel rahasia ke log, maka kredensial produksi itu bocor permanen dan harus dirotasi manual — yang seringkali tidak pernah terjadi.

Di dunia nyata, serangan terhadap CI/CD kini menjadi salah satu vektor utama peretasan supply chain software. Menurut berbagai laporan keamanan, CI/CD system adalah target favorit penyerang karena di dalamnya berkumpul banyak kredensial sekali jalan. Maka prinsip yang kita bangun di episode ini adalah: CI/CD pipeline tidak boleh menyimpan kredensial panjang, ia hanya boleh meminjamnya dalam waktu singkat. Pipeline akan login ke Vault setiap kali berjalan, menerima token dengan TTL pendek, mengambil secret yang dibutuhkan, lalu "lupa" semuanya setelah selesai.

Pembahasan Utama

Mengapa Long-Lived Secrets di CI/CD Adalah Bom Waktu

Sebelum masuk ke implementasi, mari kita pahami dulu kenapa pola lama ini bermasalah secara fundamental. Kuncinya ada di perbedaan antara kredensial statis dan dinamis:

AspekRepository Secrets (Statis)Vault (Dinamis/Short-Lived)
Masa hidupPermanen sampai dihapus manualBeberapa menit hingga beberapa jam (TTL)
RotasiManual, sering dilupakanOtomatis, setiap login mendapat kredensial baru
Jika bocorBerbahaya permanen, perlu rotasi daruratBerbahaya hanya selama TTL; otomatis tidak valid lagi
Audit trailTidak ada jejak siapa memakai kapanSetiap akses tercatat di audit log Vault
ScopingSatu secret dipakai semua jobToken dan policy bisa di-scope per job/role

Analoginya seperti memakai kunci gudang permanen yang disalin ke semua karyawan, dibandingkan memakai kartu akses hotel yang otomatis mati setelah masa menginap selesai. Kunci permanen yang jatuh ke tangan orang lain berarti gudang kalian tidak aman lagi; kartu hotel yang hilang otomatis menjadi tidak berguna setelah beberapa jam.

Dengan Vault, pola yang benar adalah:

Alur fetch secret di CI/CD (konseptual)
Pipeline mulai


Vault mendanai identitas pipeline (AppRole / OIDC / JWT)
 Vault menerbitkan token dengan TTL pendek

Pipeline membaca secret yang diizinkan policy (KV / DB / PKI)
 Secret punya lease & TTL sendiri

Pipeline selesai token & lease hangus dengan sendirinya

GitHub Actions: Integrasi dengan hashicorp/vault-action

Untuk GitHub Actions, alat utama kita adalah hashicorp/vault-action — action resmi dari HashiCorp yang menangani login ke Vault, membaca secret, dan meletakkannya ke environment variable job. Action ini menangani detail yang merepotkan: renew token saat TTL mendekati habis, dan membersihkan token di akhir job.

Ada dua cara otentikasi yang umum dipakai: AppRole (mudah, cocok untuk self-hosted runner) dan OIDC (tanpa kredensial statis sama sekali, cocok untuk GitHub-hosted runner). Kita bahas keduanya.

Menyiapkan Role AppRole untuk CI

AppRole adalah mekanisme machine-to-machine yang sudah kita kenal dari episode 10. Untuk CI, kita membuat role khusus yang hanya memiliki policy minimal — prinsip least privilege. Pertama, tulis policy yang hanya mengizinkan membaca data yang dibutuhkan pipeline:

ci-policy.hcl
path "kv/data/ci/*" {
  capabilities = ["read", "list"]
}
 
path "database/creds/deploy-reader" {
  capabilities = ["read"]
}

Perhatikan: policy ini hanya membaca, tidak menulis. Pipeline tidak boleh punya akses untuk mengubah data di Vault. Setelah itu, daftarkan policy dan buat role AppRole-nya:

Setup AppRole untuk CI/CD
vault policy write ci-app ci-policy.hcl
 
vault write auth/approle/role/ci-app \
  token_policies="ci-app" \
  token_period="30m" \
  token_ttl="10m" \
  token_max_ttl="1h" \
  secret_id_ttl="5m" \
  secret_id_num_uses="1" \
  bound_cidr_list="10.0.0.0/8"
 
vault read -field=role_id auth/approle/role/ci-app/role-id
vault write -f -field=secret_id auth/approle/role/ci-app/secret-id

Mari bedah parameter pentingnya:

  • token_period="30m" — menjadikan token bersifat periodic; vault-action bisa memperbarui (renew) token selama pipeline berjalan, sehingga job yang berjalan lebih dari 10 menit tidak putus di tengah jalan.
  • token_ttl="10m" / token_max_ttl="1h" — token pertama valid 10 menit, dan meskipun direnew berkala, tidak akan pernah melewati batas 1 jam. Ini memastikan pipeline hanya "meminjam" akses dalam jangka pendek.
  • secret_id_ttl="5m" dan secret_id_num_uses="1" — SecretID yang di-generate hanya berlaku 5 menit dan hanya untuk satu kali login. Pola ini paling aman: kalian push SecretID segar ke runner setiap pipeline akan berjalan, bukan menyimpannya permanen.
  • bound_cidr_list — membatasi alamat IP sumber yang boleh login. Untuk self-hosted runner di jaringan internal, ini pertahanan yang bagus.

Note

Pola secret_id_num_uses="1" mengharuskan kalian mem-push SecretID segar ke setiap pipeline (misalnya via API Vault ke runner, atau memanggil vault write -f auth/approle/role/ci-app/secret-id pada step sebelum login). Jika kalian memilih menyimpan SecretID statis di GitHub Secrets, set secret_id_ttl="0" dan secret_id_num_uses="0" agar tidak kedaluwarsa — tapi ingat, itu kembali menciptakan kredensial long-lived yang harus dirotasi manual. Untuk hasil terbaik, gunakan OIDC yang dibahas di bawah.

Workflow GitHub Actions Lengkap dengan vault-action

Sekarang kita tulis workflow deploy yang mengambil secret dari Vault setiap kali berjalan, bukan dari repository secrets:

.github/workflows/deploy.yml
name: Deploy Application
 
on:
  push:
    branches: [main]
 
permissions:
  contents: read
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Import secrets from Vault
        uses: hashicorp/vault-action@v3
        with:
          url: https://vault.example.com:8200
          method: approle
          roleId: ${{ secrets.VAULT_ROLE_ID }}
          secretId: ${{ secrets.VAULT_SECRET_ID }}
          secrets: |
            kv/data/ci/app DB_USERNAME | DB_USERNAME ;
            kv/data/ci/app DB_PASSWORD | DB_PASSWORD ;
            database/creds/deploy-reader POSTGRES_PASSWORD | password
 
      - name: Build application
        run: |
          npm ci
          npm run build
 
      - name: Deploy to server
        run: |
          ./scripts/deploy.sh "$DB_USERNAME" "$DB_PASSWORD"

Beberapa hal penting dari workflow di atas:

  • Sintaks secrets: vault-action adalah <path_vault> <nama_env_tujuan> | <key_secret>, dipisahkan ;. Perhatikan path KV v2 menggunakan kv/data/ci/app — segmen data/ wajib ada, ini kesalahan paling umum (dibahas di bagian pitfalls).
  • database/creds/deploy-reader POSTGRES_PASSWORD | password — di sini kita mengambil dynamic credentials dari database secrets engine (episode 5). Setiap pipeline run mendapat username/password database baru yang otomatis hangus sesuai TTL. Ini adalah puncak dari filosofi "short-lived".
  • ${{ secrets.VAULT_ROLE_ID }} dan ${{ secrets.VAULT_SECRET_ID }} — hanya dua kredensial bootstrap yang tersimpan di GitHub Secrets. Itu pun harus dipilih minimal dan dirotasi berkala.
  • Setelah job selesai, vault-action otomatis revoke token sehingga tidak ada yang tertinggal di runner.

Warning

Jangan pernah menulis echo "$DB_PASSWORD" atau mencetak secret ke log hanya untuk "debugging". Log GitHub Actions bersifat persisten dan bisa dibaca siapa pun yang punya akses ke repository — itu sama saja dengan mempublikasikan secret. Jika harus memastikan nilai ada, cetak panjangnya saja: echo "panjang password: ${#DB_PASSWORD}".

OIDC: Alternatif Tanpa Kredensial Statis

Pola AppRole di atas sudah jauh lebih baik daripada repository secrets, tetapi masih menyisakan dua kredensial bootstrap di GitHub Secrets. Solusi paling bersih adalah OIDC: GitHub Actions menerbitkan ID token berisi klaim seperti repository dan ref, lalu Vault memvalidasi token tersebut. Tidak ada RoleID, tidak ada SecretID, tidak ada yang perlu disimpan.

Pertama, aktifkan JWT auth di Vault dan buat role yang mengikat ke repo spesifik:

Setup JWT/OIDC auth untuk GitHub Actions
vault auth enable jwt
 
vault write auth/jwt/role/gha-deploy \
  bound_issuer="https://token.actions.githubusercontent.com" \
  bound_audiences="https://vault.example.com" \
  user_claim="sub" \
  token_policies="ci-app" \
  token_ttl="10m" \
  claim_mappings=repository=repository

Lalu di workflow, gunakan method: oidc dan izinkan permission id-token: write:

Workflow dengan OIDC
name: Deploy Application (OIDC)
 
on:
  push:
    branches: [main]
 
permissions:
  id-token: write
  contents: read
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Import secrets from Vault
        uses: hashicorp/vault-action@v3
        with:
          url: https://vault.example.com:8200
          method: oidc
          role: gha-deploy
          secrets: |
            kv/data/ci/app DB_PASSWORD | DB_PASSWORD
 
      - name: Build application
        run: npm run build

Yang menakjubkan dari pola ini: repository kalian tidak menyimpan satu pun kredensial Vault. Vault mempercayai token yang diterbitkan oleh GitHub karena token itu sudah ditandatangani dan klaimnya (repo, branch) diperiksa terhadap role. Perubahan kredensial? Tidak ada. Rotasi? Tidak perlu. Inilah standar industri untuk integrasi CI/CD dengan secret manager saat ini.

Tip

vault-action juga mendukung namespace, tlsSkipVerify (jangan pernah dipakai di produksi!), dan exportToken: true untuk mengekspor VAULT_TOKEN ke job bila ada tool lain yang perlu akses langsung ke Vault. Simpan default yang aman: url selalu HTTPS, dan sertakan role saat memakai method: oidc agar Vault tahu klaim aud mana yang harus divalidasi.

GitLab CI: Integrasi Native dengan Keyword secrets:

GitLab menawarkan integrasi yang lebih dalam karena GitLab sudah terhubung langsung dengan Vault sebagai CI/CD secret resolver. Tidak ada action eksternal — GitLab runner akan login ke Vault menggunakan JWT job, mengambil secret yang dideklarasikan, dan menyuntikkannya ke environment job.

Menyiapkan JWT Auth Role di Vault untuk GitLab

GitLab runner mengirim CI_JOB_JWT_V2 ke Vault, dan Vault memvalidasinya lewat JWT auth method. Role-nya harus di-bind ke project GitLab tertentu:

Setup JWT auth untuk GitLab CI
vault auth enable jwt
 
vault write auth/jwt/role/gitlab-ci \
  bound_issuer="https://gitlab.example.com" \
  bound_audiences="https://gitlab.example.com" \
  user_claim="sub" \
  bound_subject="project_path:myorg/myapp:*" \
  token_policies="ci-app" \
  token_ttl="10m"

Penjelasan singkat:

  • bound_issuer dan bound_audiences — GitLab menerbitkan JWT dengan issuer dan audience berupa URL instance GitLab.
  • bound_subject — mengikat role ke project tertentu lewat project_path:group/project. Tanda * di akhir memperbolehkan semua branch project tersebut; kalian bisa persempit ke ref:main bila perlu.
  • token_policies="ci-app" — memakai policy yang sama dengan contoh GitHub di atas.

Konfigurasi Server Vault di GitLab

Satu langkah administrasi yang tidak boleh terlewat: beri tahu GitLab di mana Vault berada dan role mana yang dipakai. Ini dilakukan di Project → Settings → CI/CD → Variables → Vault server:

FieldNilai Contoh
Vault server URLhttps://vault.example.com:8200
Vault auth rolegitlab-ci
Vault namespacekosong (untuk Vault Open Source)

Setelah itu, tulis .gitlab-ci.yml dengan keyword secrets::

.gitlab-ci.yml
stages:
  - build
  - deploy
 
secrets:
  DB_PASSWORD:
    vault: kv/data/ci/app DB_PASSWORD@kv
  API_KEY:
    vault: kv/data/ci/app API_KEY@kv
    file: false
  POSTGRES_DYNAMIC:
    vault: database/creds/deploy-reader password@database
    file: true
 
build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
 
deploy:
  stage: deploy
  image: alpine:3
  script:
    - apk add --no-cache postgresql-client
    - psql "postgres://app:$POSTGRES_DYNAMIC@db.example.com/appdb" -c "SELECT 1"

Bedah sintaks keyword vault::

  • Formatnya: vault: <path_secret> <field_secret>@<mount_point>. Untuk KV v2, path lengkapnya kv/data/ci/app, field-nya DB_PASSWORD, dan mount-nya kv.
  • database/creds/deploy-reader password@database — mengambil field password dari dynamic database credentials (mount database). GitLab akan memperoleh lease baru setiap pipeline, dan Vault akan mencabutnya setelah TTL.
  • file: true — menulis secret ke file sementara ($DB_PASSWORD_FILE-style) alih-alih environment variable. Ini mencegah secret muncul di halaman variabel job dan mengurangi risiko ter-print ke log.

Note

GitLab juga mendukung id_tokens: untuk menciptakan OIDC token dengan klaim aud khusus, berguna bila ingin menyambungkan GitLab ke Vault dengan mekanisme yang lebih eksplisit (mirip OIDC GitHub) atau ke sistem lain yang menerima OIDC. Native secrets: tetap yang paling sederhana karena tidak butuh step tambahan.

Perbandingan: GitHub Actions vs GitLab CI

Kedua platform memecahkan masalah yang sama dengan filosofi berbeda. Tabel berikut meringkas perbandingannya:

AspekGitHub ActionsGitLab CI
MekanismeStep hashicorp/vault-actionKeyword native secrets:
Auth defaultAppRole atau OIDC (method: oidc)JWT job (CI_JOB_JWT_V2auth/jwt/login)
Konfigurasi serverPer step di workflow (url, method)Project settings → Vault server
Inject ke jobEnvironment variable dari action outputsEnvironment variable atau file (file: true)
Dynamic secretsAmbil lewat path di secrets:Ambil lewat path di vault:
Kredensial bootstrapRoleID/SecretID (atau tanpa apa pun via OIDC)Tidak ada — hanya butuh URL + role
Renewal tokenOtomatis oleh vault-actionDitangani GitLab/Vault JWT login

Secara singkat: GitLab lebih "native" dan zero-config dari sisi pipeline; GitHub Actions lebih fleksibel karena kalian mengontrol penuh proses login di dalam workflow.

Kesalahan Umum (Common Pitfalls) CI/CD + Vault

Terakhir, mari kita bicarakan jebakan yang paling sering menghantui tim yang baru berintegrasi dengan Vault:

KesalahanGejalaSolusi
Path KV v2 tanpa segmen data/Permission denied atau secret not foundGunakan kv/data/ci/app, bukan kv/ci/app
RoleID/SecretID di-commit ke repoKredensial bocor permanen ke git historySimpan di Secrets/Variables; rotasi jika pernah tercommit
TTL token terlalu pendekJob yang lama gagal di tengah jalanGunakan token_period + renewal, atau token_max_ttl yang cukup
Service token abadi dipakai pipelineKembali ke masalah long-lived secretsSelalu AppRole/JWT dengan TTL pendek
Secret di-print ke logBocor ke log viewer yang persistenMasking, file: true, jangan echo
bound_cidr_list di GitHub-hosted runnerLogin ditolak karena IP runner berubahPakai OIDC, atau gunakan CIDR khusus self-hosted runner
Lupa id-token: write (OIDC)vault-action error 401Tambahkan permission id-token: write di workflow
Satu role untuk semua pipelineSerangan pada satu project = akses semuaSatu role per project/pipeline dengan policy minimal

Important

Sekalipun pipeline hanya "meminjam" kredensial dalam waktu singkat, prinsip least privilege tetap wajib. Role CI hanya boleh membaca path yang dibutuhkan job tersebut — jangan pernah memberi policy * pada path kv/* untuk pipeline. Karena jika runner atau workflow disusupi, batas itu adalah garis pertahanan terakhir kalian.

Penutup

Pada episode 18 ini kita telah membahas bagaimana menghentikan kebiasaan menyimpan API keys permanen di repository secrets dan menggantinya dengan kredensial sementara yang diambil langsung dari Vault saat pipeline berjalan. Kita sudah melihat implementasi lengkap dengan hashicorp/vault-action di GitHub Actions — baik lewat AppRole maupun OIDC tanpa kredensial statis — serta integrasi native keyword secrets: di GitLab CI yang menyuntikkan secret dari Vault ke job secara otomatis.

Poin kunci yang perlu kalian bawa pulang:

  • Repository secrets menciptakan long-lived credentials; Vault memberikan short-lived credentials dengan TTL, audit log, dan rotasi otomatis.
  • Di GitHub Actions, vault-action mengelola login, renewal, dan revocation token; OIDC menghilangkan kredensial bootstrap sama sekali.
  • Di GitLab CI, keyword secrets: + JWT auth role membuat integrasi native tanpa action tambahan.
  • Path KV v2 harus menyertakan data/, TTL token harus cukup untuk durasi job, dan secret tidak boleh pernah di-print ke log.

Di episode 19 selanjutnya, kita akan naik satu tingkat: integrasi Vault dengan Infrastructure as Code (IaC) menggunakan Terraform Vault Provider dan Ansible lookup plugin — bagaimana infrastruktur kalian bisa menerbitkan kredensial database dinamis atau sertifikat PKI langsung saat provisioning, dan bagaimana menghindari jebakan secret yang bocor ke tfstate dan log. Pastikan tetap semangat!

Belajar Vault - Integrasi Vault di CI/CD Pipeline (GitHub Actions & GitLab CI) | Belajar Secret Management dengan HashiCorp Vault