//BAGIAN 10 DARI 10
Bagian 10: CI Mengambil Token Dokploy dari Vault lewat GitHub OIDC, dan Penutup Seri
· 21 menit baca
English//TUTORIAL · 10 DARI 10 — Secure CI/CD Pipeline dengan Go, GitHub Actions, Vault, dan Dokploy
- 01Bagian 1: Kenapa Saya Bikin Secure CI/CD Pipeline, dan Persiapan Tools-nya
- 02Bagian 2: Vault, Secret, Policy, dan AppRole per Environment
- 03Bagian 3: Aplikasi Go Kecil yang Membaca Secret dari File dan Menolak Jalan Kalau Config Salah
- 04Bagian 4: Vault Agent Sidecar dan Docker Compose, Secret Masuk Tanpa App Tahu Vault Ada
- 05Bagian 5: Repo GitHub dan Workflow PR sebagai Security Gate
- 06Bagian 6: Ruleset yang Memblokir Merge, dan Lima Demo PR yang Harus Gagal
- 07Bagian 7: Menyiapkan Environment Dev dan Production di Dokploy
- 08Bagian 8: Workflow Deploy, Build Sekali, Scan, SBOM, GHCR, lalu Promosi ke Production
- 09Bagian 9: Proses Exception untuk Temuan Security yang Belum Bisa Diperbaiki
- 10Bagian 10: CI Mengambil Token Dokploy dari Vault lewat GitHub OIDC, dan Penutup Seri
Sampai Bagian 9, pipeline-nya sudah lengkap. PR dicek, merge ke development deploy ke dev, merge ke main dipromosikan ke production setelah approval, dan exception punya tanggal kedaluwarsa. Tapi ada satu hal yang masih mengganjal sejak Bagian 8: API key Dokploy tersimpan sebagai GitHub Environment Secret DOKPLOY_API_KEY.
Itu yang saya sebut Tahap A di Bagian 1. Sengaja dibuat dulu supaya deploy jalan. Di bagian terakhir ini saya ganti dengan Tahap B: job deploy minta identitas sementara ke GitHub (OIDC token), menukarnya ke Vault, lalu membaca key Dokploy untuk environment-nya sendiri. Setelah itu DOKPLOY_API_KEY dihapus dari GitHub. Nggak ada lagi kredensial Dokploy yang tersimpan di CI.
Di akhir artikel, saya tutup seri ini: apa saja yang sudah dibangun dari Bagian 1 sampai 10, risiko apa yang sengaja diterima, dan apa yang bisa dikerjakan berikutnya.
Kenapa key statis di GitHub itu masalah
Secret environment di GitHub sebenarnya sudah lumayan aman. Hanya job yang masuk environment production yang bisa membacanya, dan environment itu cuma boleh dipakai main setelah approval. Lalu apa masalahnya?
Masalahnya ada di sifat key-nya: permanen. Key Dokploy berlaku 90 hari. Kalau bocor sekali saja, dari log, cache, artifact, atau dari akun GitHub yang dicuri, key itu bisa dipakai siapa pun sampai kedaluwarsa. Dan nggak ada catatan siapa mengambilnya kapan.
Skenario | Tahap A (secret GitHub) | Tahap B (Vault lewat OIDC) |
|---|---|---|
Key bocor dari log, cache, atau artifact | Bisa dipakai sampai 90 hari | Key tetap di-mask. Token Vault mati dalam 5 menit |
Workflow baru di branch fitur mencoba membaca key | Aman kalau proteksi environment benar | Ditolak Vault: |
Job dev mencoba key prod | Beda secret, tapi dua-duanya tersimpan permanen | Ditolak: role dev hanya punya policy dev |
Admin GitHub atau token GitHub dicuri | Bisa membaca dan mengganti secret | Nggak ada secret yang bisa dibaca. Akses diatur di Vault |
Audit "siapa ambil key kapan" | Nggak ada | Tercatat di Vault (entity login, audit log) |
Catatan
Yang nggak berubah
Key Dokploy-nya sendiri tetap key biasa, karena Dokploy belum punya token per-service atau token berumur pendek. Yang dikecilkan di bagian ini adalah tempat key disimpan dan siapa yang bisa mengambilnya, bukan kekuatan key-nya.
Cara kerjanya: OIDC token GitHub ditukar ke Vault
Setiap job GitHub Actions yang punya izin id-token: write bisa meminta JWT ke GitHub. JWT ini:
ditandatangani GitHub dengan kunci privatnya. Vault mengecek tanda tangannya pakai kunci publik GitHub, jadi nggak bisa dipalsukan.
berumur pendek, sekitar 5 menit.
berisi klaim: fakta tentang job yang meminta (repo, branch, environment, file workflow). Klaim ini ditulis GitHub sendiri, bukan oleh isi workflow. Jadi workflow nggak bisa mengaku-ngaku.
Analogi yang saya pakai: ini KTP sementara yang dicetak GitHub untuk satu job. Vault itu satpam yang mengecek KTP itu, lalu memberi kartu tamu 5 menit untuk satu pintu.
Yang penting: nggak ada rahasia bersama antara GitHub dan Vault. Kepercayaannya murni dari tanda tangan digital. Alurnya:
Runner GitHub GitHub OIDC Vault (vault.example.com)───────────── ─────────── ─────────────────────────[0] job punya id-token: write[1] minta JWT, audience=https://vault.example.com ──▶ buat JWT bertanda tangan ◀── JWT (umur ±5 menit)[2] POST /v1/auth/jwt-github/login {role, jwt} ──────────────────▶ [3] cek tanda tangan (kunci publik GitHub) cek iss, aud, sub, repository_id, job_workflow_ref ◀────────────────────────────────────────────── [4] token Vault, TTL 5 menit, 1 policy[5] GET /v1/static-secret/data/secure-cicd-go/ci/<env>/dokploy ──▶ [6] policy: boleh read path ini? ◀── api_key (di-mask jadi *** di log)[7] step deploy memakai key → Dokploy[8] job selesai, VM runner dibuang, token Vault mati sendiriSyarat jaringannya cuma dua, dan dua-duanya koneksi keluar biasa lewat HTTPS 443: runner ke Vault, dan Vault ke token.actions.githubusercontent.com untuk mengambil kunci publik GitHub. Artinya Vault kamu harus bisa dijangkau dari internet. Kalau Vault cuma bisa dibuka lewat VPN, runner GitHub-hosted nggak akan bisa login.
curl -sS https://vault.example.com/v1/sys/health | jq '{initialized, sealed, version}'Endpoint ini publik dan nggak butuh token. Harus keluar "initialized": true, "sealed": false.
Klaim yang dipakai
Klaim | Contoh nilai | Dipakai untuk |
|---|---|---|
|
| Memastikan penerbitnya GitHub |
|
| Token memang dibuat untuk Vault ini. Token untuk layanan lain (misalnya AWS) nggak bisa dipakai ulang |
|
| Siapa yang login: repo dan environment |
|
| ID angka repo. Tetap sama walau repo di-rename, beda kalau repo dihapus lalu dibuat ulang |
|
| File workflow dan branch. Workflow lain di repo yang sama tetap ditolak |
Kenapa sub berisi environment:production itu kuat? Di Bagian 8, environment production sudah diatur: hanya branch main yang boleh memakainya, dan butuh approval manusia. GitHub hanya menulis environment:production di sub kalau job benar-benar masuk environment itu. Jadi token dengan sub itu hanya bisa didapat dari main, setelah di-approve. Vault mendapat jaminan itu tanpa perlu tahu apa-apa soal approval.
Langkah 1: policy dan role Vault disimpan di repo
Sama seperti policy aplikasi di Bagian 2, policy dan role untuk CI saya simpan di repo (vault/policies/, vault/roles/). Dengan begitu, mengubah akses CI harus lewat PR, ikut di-review, dan tercatat di Git.
Policy-nya sesempit mungkin. Satu path persis, tanpa *, hanya read:
# CI deploy job (development): read the Dokploy API key, nothing else.path "static-secret/data/secure-cicd-go/ci/development/dokploy" { capabilities = ["read"]}File production sama persis, cuma development diganti production. Perhatikan folder ci/: sengaja di luar secure-cicd-go/<env>/*. Akibatnya policy aplikasi (yang dipakai Vault Agent) nggak bisa membaca key CI, dan policy CI nggak bisa membaca secret JWT/AES aplikasi. Dua dunia yang terpisah.
Lalu role-nya. Ini file dari repo, dengan ID owner dan repo saya ganti placeholder:
{ "role_type": "jwt", "user_claim": "sub", "bound_audiences": ["https://vault.example.com"], "bound_subject": "repo:dimss113@<OWNER_ID>/secure-cicd-go@<REPO_ID>:environment:production", "bound_claims_type": "string", "bound_claims": { "repository_id": "<REPO_ID>", "job_workflow_ref": "dimss113/secure-cicd-go/.github/workflows/deploy.yml@refs/heads/main" }, "token_policies": ["secure-cicd-go-ci-production"], "token_ttl": "5m", "token_max_ttl": "10m"}Field | Artinya |
|---|---|
| Login pakai JWT yang dikirim langsung, bukan alur OIDC lewat browser |
| Klaim yang jadi "nama" login di Vault. Muncul di log dan di entity Vault |
|
|
|
|
| Pencocokan persis, bukan pola |
| ID repo ( |
| Hanya |
| Token hasil login hanya membawa policy ini (plus |
| Mati sendiri dalam 5 menit, nggak bisa diperpanjang lewat 10 menit. Deploy cuma butuh beberapa detik |
Saya nggak menulis awalan sub dengan tangan. Saya ambil langsung dari API GitHub, karena di situlah jebakan terbesar fase ini (ceritanya di bawah):
SUB_PREFIX=$(gh api repos/dimss113/secure-cicd-go/actions/oidc/customization/sub --jq .sub_claim_prefix)echo "$SUB_PREFIX" # repo:dimss113@<OWNER_ID>/secure-cicd-go@<REPO_ID>Dari situ dua file role dan dua file policy dibuat lewat loop for ENV in development production dengan heredoc. Satu detail kecil kalau kamu pakai zsh: tulis ${SUB_PREFIX}:environment:... pakai kurung kurawal. Tanpa kurung, zsh membaca $SUB_PREFIX:e sebagai modifier :e, dan hasilnya jadi "nvironment:...". Rusak, dan nggak kelihatan sampai login gagal.
Cek hasil:
for f in vault/roles/*.json; do jq -e . "$f" >/dev/null && echo "$f ok"; doneHarus keluar ok dua kali.
Langkah 2: pasang auth JWT, policy, dan role di Vault
Auth method jwt di path jwt-github cukup dipasang sekali. Cek dulu, karena kalau sudah ada (misalnya dipasang proyek lain), vault auth enable akan gagal dengan path is already in use.
export VAULT_ADDR=https://vault.example.comvault loginvault auth list | grep jwt-github || echo "belum ada"Kalau belum ada:
vault auth enable -path=jwt-github jwtvault write auth/jwt-github/config \ oidc_discovery_url="https://token.actions.githubusercontent.com" \ bound_issuer="https://token.actions.githubusercontent.com"oidc_discovery_url adalah tempat Vault mengambil kunci publik GitHub. Vault memperbaruinya sendiri kalau GitHub mengganti kunci. bound_issuer memastikan token wajib punya iss itu.
Lalu unggah policy dan role dari file di repo:
for ENV in development production; do vault policy write "secure-cicd-go-ci-$ENV" "vault/policies/secure-cicd-go-ci-$ENV.hcl" vault write "auth/jwt-github/role/secure-cicd-go-ci-$ENV" "@vault/roles/secure-cicd-go-ci-$ENV.json"done@file.json artinya "kirim isi file ini sebagai data". Jadi yang ada di Vault selalu sama dengan yang ada di Git.
Cek hasil:
vault read -format=json auth/jwt-github/role/secure-cicd-go-ci-production \ | jq '.data | {bound_subject, bound_claims, bound_audiences, token_policies, token_ttl}'bound_subject harus berakhiran environment:production, job_workflow_ref berakhiran refs/heads/main, dan "token_ttl": 300 (detik).
Langkah 3: rotasi key Dokploy, simpan hanya di Vault
Key lama pernah tersimpan di GitHub, jadi saya anggap pernah terekspos. Saya buat key baru di Dokploy (Profile → API/CLI → Generate, expiration 90 hari, rate limit nyala), satu untuk dev dan satu untuk prod. Key lama jangan dihapus dulu: masih dipakai deploy sampai perubahan workflow di-merge.
read -rs DOKPLOY_KEY # tempel key DEV baru (nggak tampil), Enterprintf '%s' "$DOKPLOY_KEY" | vault kv put -mount=static-secret secure-cicd-go/ci/development/dokploy api_key=-unset DOKPLOY_KEYread -rs membaca input tanpa menampilkannya, jadi nggak masuk history shell. api_key=- artinya nilainya diambil dari stdin, jadi key nggak muncul di argumen perintah dan nggak kelihatan di ps. Ulangi untuk ci/production/dokploy.
Saya juga menandai pemilik dan tanggal rotasi sebagai metadata. Metadata bisa dilihat tanpa membuka nilai key:
ROTATE=$(date -v+90d +%F)for ENV in development production; do vault kv metadata put -mount=static-secret \ -custom-metadata=owner=@dimss113 -custom-metadata=rotate_before="$ROTATE" \ "secure-cicd-go/ci/$ENV/dokploy"doneLangkah 4: uji isolasi sebelum menyentuh workflow
Sebelum mengubah apa pun di GitHub, saya buktikan dulu policy-nya benar. Buat token uji yang hanya punya policy CI dev:
T=$(vault token create -policy=secure-cicd-go-ci-development -ttl=2m -field=token)VAULT_TOKEN=$T vault kv get -mount=static-secret -field=api_key secure-cicd-go/ci/development/dokploy | wc -cVAULT_TOKEN=$T vault kv get -mount=static-secret secure-cicd-go/ci/production/dokploy # harus 403VAULT_TOKEN=$T vault kv get -mount=static-secret secure-cicd-go/development/secrets/jwt # harus 403vault token revoke "$T"; unset Twc -c menghitung jumlah karakter key tanpa menampilkannya. Ulangi dengan -policy=secure-cicd-go-development (policy aplikasi): membaca ci/development/dokploy juga harus 403.
Cek hasil: baris pertama mencetak angka lebih dari 0, sisanya Code: 403 ... permission denied.
Langkah 5: ubah deploy.yml
Perubahannya kecil, dan cuma di dua job deploy (deploy-dev dan promote). Ini deploy-dev dari repo, step retag image saya potong karena sama dengan Bagian 8:
env: IMAGE: ghcr.io/dimss113/secure-cicd-go # The deploy jobs log in to Vault with their GitHub OIDC token; no Dokploy key is stored in GitHub. VAULT_ADDR: https://vault.example.com jobs: # ... build (sama seperti Bagian 8) deploy-dev: needs: build runs-on: ubuntu-24.04 timeout-minutes: 15 environment: name: development url: ${{ vars.APP_URL }} permissions: contents: read packages: write id-token: write steps: # ... checkout + "Point the development tag at this image" (sama seperti Bagian 8) # The role only accepts this workflow, on development, in the development environment. - name: Read the Dokploy key from Vault id: vault uses: hashicorp/vault-action@892a26828f195e65540a40b4768ae4571f51ebfc # v4.0.0 with: url: ${{ env.VAULT_ADDR }} method: jwt path: jwt-github role: secure-cicd-go-ci-development jwtGithubAudience: ${{ env.VAULT_ADDR }} exportEnv: false secrets: static-secret/data/secure-cicd-go/ci/development/dokploy api_key | DOKPLOY_API_KEY - name: Deploy and smoke test env: DOKPLOY_URL: ${{ vars.DOKPLOY_URL }} DOKPLOY_API_KEY: ${{ steps.vault.outputs.DOKPLOY_API_KEY }} DOKPLOY_COMPOSE_ID: ${{ vars.DOKPLOY_COMPOSE_ID }} APP_URL: ${{ vars.APP_URL }} EXPECTED_COMMIT: ${{ github.sha }} run: scripts/dokploy-deploy.shJob promote identik, dengan role: secure-cicd-go-ci-production dan path ci/production/dokploy.
Bagian | Kenapa |
|---|---|
| Izin meminta OIDC token. Hanya di dua job deploy. |
| Dipin ke SHA commit, sama seperti semua action lain di repo. Tag bisa dipindah, SHA nggak |
| Default-nya |
| Isi |
| Key nggak dijadikan env var untuk semua step berikutnya. Hanya step deploy yang menerimanya lewat |
| Path KV v2 pakai |
| Key diambil dari output step Vault, bukan dari |
vault-action otomatis me-mask key dan token Vault di log. Token Vault-nya juga nggak di-export, dan mati dalam 5 menit.
Step Vault sengaja ditaruh setelah retag image, tepat sebelum deploy. Key baru ada di job sesaat sebelum dipakai. Kalau retag gagal, Vault nggak pernah dihubungi.
Cek hasil: sebelum commit, jalankan actionlint dan scripts/check-exceptions.sh dari Bagian 9. Lalu commit lima file (workflow, dua policy, dua role), buka PR ke development, tunggu check hijau, dan merge. PR ini sendiri nggak menjalankan deploy, karena deploy.yml hanya jalan saat push ke development/main.
Langkah 6: buktikan, lalu hapus DOKPLOY_API_KEY
Merge tadi memicu deploy di development. Tonton run-nya, lalu lihat log step Vault:
RUN_ID=$(gh run list --workflow deploy.yml --branch development --limit 1 --json databaseId --jq '.[0].databaseId')gh run watch "$RUN_ID" --exit-statusgh run view "$RUN_ID" --log | grep -A6 "Read the Dokploy key from Vault" | head -20Harus ada login jwt yang berhasil dan nggak ada nilai key (paling jauh ***). Smoke test /version menunjukkan commit merge ini.
Bukti dari sisi Vault: login pertama membuat entity dengan nama dari sub.
ACC=$(vault auth list -format=json | jq -r '."jwt-github/".accessor')vault write -format=json identity/lookup/entity \ alias_name="${SUB_PREFIX}:environment:development" alias_mount_accessor="$ACC" \ | jq '.data | {name, alias_created: .aliases[0].creation_time}'Sekarang dev sudah nggak butuh secret GitHub. Hapus:
gh secret delete DOKPLOY_API_KEY --env developmentgh secret list --env developmentLalu hapus key dev yang lama di Dokploy, dan jalankan ulang run tadi dengan gh run rerun "$RUN_ID". Kalau tetap hijau, terbukti deploy dev hanya bergantung pada Vault.
Untuk production prosesnya sama seperti promosi biasa di Bagian 8: PR development → main, check hijau, merge, approve deployment production. Step Vault di promote memakai role prod. Setelah hijau, gh secret delete DOKPLOY_API_KEY --env production, dan hapus key prod lama di Dokploy.
Cek hasil:
for ENV in development production; do echo "== $ENV"; gh secret list --env "$ENV"; donegh secret listNggak boleh ada DOKPLOY_API_KEY di mana pun. DOKPLOY_URL dan DOKPLOY_COMPOSE_ID tetap ada sebagai variable, dan memang bukan rahasia.
Yang sempat bikin saya bingung: format sub yang immutable
Deploy dev pertama setelah merge gagal. Pesannya:
vaultResponse: {"errors":["error validating token: invalid subject (sub) claim"]}Padahal semuanya sudah saya uji di Vault dev server lokal, dengan token palsu yang saya tanda tangani sendiri: token dev diterima, token PR ditolak, audience salah ditolak, workflow lain ditolak. Semua sesuai harapan.
Ternyata masalahnya di format sub. Panduan Vault, vault-action, dan GitHub memberi contoh format lama: repo:dimss113/secure-cicd-go:environment:development. Tapi sejak 2026 GitHub punya format immutable subject yang menyertakan ID angka owner dan repo: repo:dimss113@<OWNER_ID>/secure-cicd-go@<REPO_ID>:.... Repo baru (dibuat setelah pertengahan Juli 2026) otomatis memakainya, termasuk repo ini.
Uji lokal saya memakai token palsu berformat lama. Jadi uji itu cuma membuktikan bahwa asumsi saya cocok dengan role saya, bukan bahwa role saya cocok dengan token GitHub yang asli.
Kenapa GitHub mengganti format? Nama bisa dipakai ulang, ID nggak. Kalau akun atau repo dihapus lalu orang lain membuat ulang dengan nama yang sama, dengan format lama sub-nya identik. Dengan format immutable, ID-nya pasti beda, jadi sub-nya beda dan ditolak. (Role kita sudah punya bound_claims.repository_id, jadi serangan ini sebenarnya sudah tertutup. Format immutable menambah lapisan kedua.)
Perbaikannya: ambil awalan dari API (oidc/customization/sub, seperti di Langkah 1), ganti bound_subject di dua file role, vault write ulang, lalu gh run rerun "$RUN_ID" --failed supaya hanya deploy-dev yang diulang. Setelah itu perbaikan file role-nya di-commit lewat PR, supaya isi vault/roles/ tetap sama dengan isi Vault.
Tips
Pelajaran
Uji integrasi identitas dengan token dari penerbit aslinya, atau baca formatnya dari sumber resmi (di sini API oidc/customization/sub). Jangan dari contoh dokumentasi. Demo di bagian berikut mencetak sub asli. Kalau demo itu saya jalankan sebelum membuat role, perbedaannya langsung kelihatan.
Demo negatif: job dari PR nggak bisa mengambil key
Ini bukti terkuat bagian ini. Skenarionya: seseorang membuka PR yang menambah workflow untuk mencuri key Dokploy. Workflow itu boleh meminta OIDC token, dan bahkan memakai audience yang benar. Vault tetap harus menolak.
PR ini cuma demo, ditutup tanpa merge.
name: oidc-demo # Demo only, never merged: a pull request job asks Vault for the Dokploy keys and must be refused.on: pull_request permissions: {} jobs: vault-denies-pull-requests: runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: id-token: write env: VAULT_ADDR: https://vault.example.com steps: - name: Ask GitHub for an OIDC token and try both deploy roles run: | jwt="$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$VAULT_ADDR" | jq -r .value)" echo "::add-mask::$jwt" # The payload is not a credential without its signature, so printing these claims is safe. payload="$(cut -d. -f2 <<<"$jwt" | tr '_-' '/+')" while [ $(( ${#payload} % 4 )) -ne 0 ]; do payload="$payload="; done base64 -d <<<"$payload" | jq '{sub, aud, ref, environment, job_workflow_ref}' for role in secure-cicd-go-ci-production secure-cicd-go-ci-development; do echo "== $role" resp="$(jq -n --arg role "$role" --arg jwt "$jwt" '{role: $role, jwt: $jwt}' | curl -sS -X POST "$VAULT_ADDR/v1/auth/jwt-github/login" --data @-)" if jq -e '.auth.client_token' <<<"$resp" >/dev/null; then echo "::error::Vault accepted a pull request token for $role" exit 1 fi jq -c '.errors' <<<"$resp" done echo "Both roles refused this pull request, as expected."Script ini melakukan hal yang sama persis dengan vault-action, tapi manual dengan curl, jadi setiap langkahnya terlihat di log. ACTIONS_ID_TOKEN_REQUEST_URL dan _TOKEN disuntik GitHub karena job punya id-token: write. Token utuh di-mask. Bagian tengah JWT (payload) di-decode supaya kita bisa melihat klaim yang dilihat Vault. Kalau Vault sampai memberi token, job dibuat gagal, karena itu artinya ada celah.
Cek hasil: di log job oidc-demo:
"sub": "repo:dimss113@<OWNER_ID>/secure-cicd-go@<REPO_ID>:pull_request","environment": null,"job_workflow_ref": "dimss113/secure-cicd-go/.github/workflows/oidc-demo.yml@refs/pull/<n>/merge"== secure-cicd-go-ci-production["error validating token: invalid subject (sub) claim"]== secure-cicd-go-ci-development["error validating token: invalid subject (sub) claim"]Both roles refused this pull request, as expected.Job-nya hijau, karena hijau di sini artinya "Vault menolak". sub untuk job PR berakhiran :pull_request, bukan :environment:<env>, jadi nggak cocok dengan role mana pun.
Bagaimana kalau penyerang menambahkan environment: production ke workflow demo? GitHub menolak job itu sebelum jalan, karena production hanya boleh dipakai branch main. Kalaupun entah bagaimana lolos, job_workflow_ref (oidc-demo.yml@refs/pull/...) tetap nggak cocok dengan deploy.yml@refs/heads/main. Dua lapis.
Setelah screenshot, tutup PR-nya dengan gh pr close --delete-branch.
Keterbatasan yang tetap ada
Saya tulis jujur, karena ini juga yang biasanya ditanyakan:
Keterbatasan | Kenapa diterima | Mitigasi |
|---|---|---|
Key Dokploy tetap statis 90 hari dan berlaku untuk seluruh akun Dokploy | Dokploy belum punya token per-service atau sementara | Hanya ada di Vault, hanya bisa diambil job deploy, dirotasi tiap 90 hari ( |
Selama job berjalan, key ada di memori runner | Key memang harus dikirim ke Dokploy | Runner GitHub-hosted dibuang setelah job. Di-mask, |
Orang dengan hak tulis bisa mengubah | Yang boleh mengubah pipeline memang dipercaya pipeline | PR wajib lolos gate dan review. Role Vault juga lewat PR dan diterapkan admin Vault |
Vault jadi titik gagal deploy | Disengaja, fail closed | Aplikasi yang sudah jalan nggak terdampak. Hanya deploy baru yang tertahan |
| Rename file = deploy ditolak | Ubah role di |
Satu hal lagi: nggak ada auto refresh untuk key Dokploy. Yang dibuat ulang tiap run cuma OIDC token dan token Vault. Key Dokploy itu secret statis di KV, Vault hanya menyimpannya, karena nggak ada secrets engine untuk Dokploy. Kalau kedaluwarsa, Vault tetap memberi key-nya, Dokploy menjawab 401, dan step deploy merah.
Bedanya sekarang, rotasi cukup satu kali vault kv put dengan key baru, lalu perbarui rotate_before. Nggak perlu mengubah GitHub, workflow, atau membuat PR. KV v2 menyimpan versi lama, jadi kalau key baru ternyata salah, vault kv rollback -version=<versi lama> mengembalikannya.
Menutup seri: apa yang sudah dibangun
Ini bagian terakhir. Kalau dilihat dari awal, inilah yang sekarang berdiri:
Bagian | Hasilnya |
|---|---|
1 | Tujuan, anti-goal, daftar security gate, tools, DNS |
2 | Secret di Vault, policy dan AppRole terpisah per environment, bukti role dev nggak bisa baca prod |
3 | Aplikasi Go kecil yang membaca secret dari file, bukan dari env atau repo |
4 | Vault Agent sidecar yang me-render |
5 | Repo publik dan workflow PR: test, Gitleaks, Semgrep, govulncheck, dependency review, Trivy config dan image |
6 | Ruleset yang memblokir merge, tanpa bypass, plus demo PR yang terbukti diblokir |
7 | Dokploy dev dan prod, auto-deploy mati, hanya CI yang memicu deploy |
8 | Build sekali, scan, SBOM, GHCR, deploy dev, promosi image yang sama ke prod setelah approval, smoke test |
9 | Proses exception dengan pemilik, alasan, dan tanggal kedaluwarsa yang ditegakkan CI |
10 | CI tanpa kredensial Dokploy tersimpan: OIDC → Vault → token 5 menit untuk satu path |
Kalau dirangkum dalam satu kalimat: deploy job nggak menyimpan kredensial Dokploy apa pun, mereka menukar OIDC token GitHub yang berumur pendek dengan token Vault 5 menit yang cuma bisa membaca satu path, terikat ke repo, environment, dan file workflow ini.
Setelah ini masih ada satu langkah yang nggak saya jadikan artikel tersendiri: merapikan README, diagram, runbook, dan screenshot bukti supaya orang lain bisa menilai proyek ini dalam lima menit.
Risiko yang sengaja diterima
Nggak ada sistem yang sempurna. Yang penting risikonya ditulis, ada alasannya, dan ada mitigasinya. Ini versi akhirnya:
Item | Kenapa diterima | Mitigasi |
|---|---|---|
| Kesederhanaan | Policy hanya |
Key API Dokploy statis 90 hari, berlaku untuk seluruh akun | Dokploy belum punya token per-service | Sejak Bagian 10 hanya di Vault, diambil lewat OIDC, token Vault 5 menit, rotasi manual dengan |
Image dev pertama (bootstrap) dibuat dari laptop | CI deploy belum ada sebelum Bagian 8 | Dari |
Deploy pakai tag, bukan digest | Digest + signature itu proyek tersendiri | Smoke test |
Image vault-agent dibangun di server Dokploy | Template di-bake ke image | Versi base dipatok, Dockerfile-nya ikut dipindai Trivy config |
Hardening docker-compose nggak dicek otomatis | Trivy nggak mendukung compose | Review PR |
App dan vault-agent memakai UID yang sama | Supaya | Volume auth vault-agent nggak pernah di-mount ke app |
Rotasi secret aplikasi nggak otomatis sampai ke app | App membaca | Agent me-render ulang, app memakai nilai baru setelah restart. Ditulis di runbook |
Mencabut | Secret sudah di memori app | Untuk memutus akses segera: cabut |
Healthcheck vault-agent = "config pernah ter-render", bukan "Vault bisa dihubungi" | Render lama tetap ada di tmpfs | Pantau log agent ( |
Langkah berikutnya
Beberapa hal sengaja saya tinggalkan supaya scope-nya nggak melebar. Kalau mau melanjutkan, ini urutan yang menurut saya masuk akal:
Signing image dengan Cosign dan deploy pakai digest. Sekarang deploy masih pakai tag. Dengan signature dan digest, Dokploy hanya menjalankan image yang benar-benar dibangun dan ditandatangani CI ini. Ini topik container supply-chain security yang layak jadi proyek sendiri.
DAST dengan OWASP ZAP. Semua scanner di seri ini statis. ZAP baseline scan ke environment dev setelah deploy akan menangkap hal yang hanya muncul saat aplikasi jalan, seperti header yang hilang atau endpoint yang terbuka.
Satu dashboard temuan: DefectDojo. Sekarang hasil Gitleaks, Semgrep, dan Trivy sudah masuk tab Security GitHub lewat SARIF, tapi govulncheck, dependency review, dan SBOM belum. DefectDojo (OWASP, open source) bisa mengumpulkan semuanya, deduplikasi, dan melacak SLA. API key-nya bisa diambil dari Vault lewat OIDC, pola yang sama persis dengan bagian ini. Dependency-Track juga menarik untuk memantau SBOM terus-menerus, supaya CVE yang terbit setelah deploy tetap ketahuan.
Rotasi key Dokploy otomatis. Dokploy punya prosedur untuk membuat dan menghapus API key, jadi job terjadwal secara teori bisa merotasinya. Harganya: job itu butuh hak tulis ke Vault dan hak membuat key Dokploy, jauh lebih besar daripada job deploy yang cuma membaca satu path. Untuk sekarang, rotasi manual yang terdokumentasi sudah cukup.
Policy-as-code untuk Compose (misalnya Conftest/OPA), supaya hardening docker-compose ikut jadi gate, bukan cuma review.
Rangkuman
Di bagian ini:
Policy
secure-cicd-go-ci-<env>yang hanya bisareadsatu path, disimpan divault/policies/.Role JWT
secure-cicd-go-ci-<env>divault/roles/, terikataud,bound_subject(repo + environment),repository_id, danjob_workflow_ref(deploy.ymldi branch yang benar), dengan token Vault 5 menit.Auth method
jwt-githubdi Vault yang memverifikasi tanda tangan GitHub lewatoidc_discovery_url.Key Dokploy baru, hanya di Vault, dengan metadata
rotate_before.deploy.ymldenganid-token: writehanya di dua job deploy, vault-action dipin SHA,exportEnv: false.DOKPLOY_API_KEYsudah dihapus dari kedua GitHub Environment.Bukti bahwa job dari PR ditolak kedua role dengan
invalid subject (sub) claim.
Dan itu menutup seri ini. Dari "security tergantung ingatan orang" di Bagian 1, sekarang setiap perubahan harus lewat gate yang memblokir merge, image yang jalan di production adalah image yang sudah diuji di dev, secret aplikasi datang dari Vault, dan CI nggak menyimpan kredensial deploy sama sekali. Repo lengkapnya ada di github.com/dimss113/secure-cicd-go. Terima kasih sudah mengikuti sampai sini. Kalau kamu membangun versi kamu sendiri, saya penasaran gate mana yang paling sering memblokir PR kamu.