CI/CD pipeline adalah jalur otomatis dari kode yang kamu push sampai aplikasi jalan di production: install, test, build, lalu deploy. Di GitHub, jalurnya ditulis sebagai workflow GitHub Actions dan dijalankan di server sewaan bernama runner. Bayangin ban berjalan di pabrik otomatis. Bahan masuk di satu ujung, produk dikirim ke semua pelanggan di ujung lain, dan mesinnya memegang kunci gudang. Siapa pun yang bisa menyelipkan sesuatu di ban itu, atau menukar salah satu mesinnya, ikut terkirim ke semua pelanggan.
🧪 Tebakan cepat: GitHub menyamarkan secret di log jadi
***. Jadi kalau workflow menjalankancurl "https://api.example.com/deploy?token=${{ secrets.DEPLOY_TOKEN }}", token-nya aman? Tebak dulu, jawabannya ada di bagian "Jauhkan secret dari log dan URL".
Workflow yang diperiksa
Workflow ada di folder .github/workflows/. on menyebut kapan dia jalan (trigger), jobs berisi langkah-langkahnya. uses menjalankan action, yaitu kode siap pakai buatan orang lain, dan run menjalankan perintah shell. Ini workflow deploy tokokopi, sama dengan yang ada di Lab:
name: Deploy
on:
push:
branches: [main]
pull_request_target:
permissions: write-all
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npm ci && npm test
- run: echo "Token: ${{ secrets.DEPLOY_TOKEN }}"
- run: curl "https://api.example.com/deploy?token=${{ secrets.DEPLOY_TOKEN }}"
Ada lima temuan: secret dicetak ke log (echo), secret di URL (curl), action dipanggil lewat tag (@v7), token bawaan bisa menulis apa saja (write-all), dan job yang memegang secret ikut jalan untuk pull request dari fork (pull_request_target).
Secret disimpan di GitHub Secrets
Token deploy gak boleh ditulis langsung di file workflow, karena file itu ikut ke git. Simpan di GitHub Secrets: Settings, lalu Secrets and variables, lalu Actions. Nilainya terenkripsi, dan workflow memanggilnya lewat context secrets:
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Setiap kali nilai itu muncul persis di log, GitHub menggantinya dengan ***.
Jauhkan secret dari log dan URL
Penyamaran *** cuma menyaring teks di log GitHub. Ini yang tercatat waktu workflow di atas jalan:
# log GitHub Actions
Run echo "Token: ***"
Token: ***
# access log api.example.com (server tujuan)
203.0.113.7 - - [08/Oct/2026:09:12:44 +0000] "GET /deploy?token=dpl_9f8e7d6c5b4a HTTP/1.1" 200 2 "-" "curl/8.5.0"
URL lengkap, termasuk query string-nya, tercatat di access log server tujuan, di luar jangkauan GitHub. Siapa pun yang bisa membaca log itu memegang token-mu. Itu jawaban tebakannya: gak aman. Dan echo secret tetap kebiasaan buruk: penyamaran cuma mengenali teks yang persis sama.
Perbaikannya: jangan cetak secret, masukkan lewat env, dan kirim di header:
- run: curl -X POST -H "Authorization: Bearer $DEPLOY_TOKEN" https://api.example.com/deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
env:menjadikan secret environment variable khusus langkah ini, dan shell membacanya sebagai$DEPLOY_TOKEN.- Header
Authorizationgak dicatat di format access log standar, jadi log server cuma berisi"POST /deploy HTTP/1.1".
Pin action ke commit SHA
actions/checkout@v7 menunjuk sebuah tag, dan pemilik repo action bisa memindahkan tag ke commit lain kapan saja. Run berikutnya menjalankan kode yang beda, lengkap dengan secret-mu. Ini pernah terjadi: Maret 2025, tag action tj-actions/changed-files dipindah ke commit jahat yang mencetak secret CI ke log, di lebih dari 23.000 repo yang memakainya.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
SHA commit 40 karakter menunjuk tepat satu commit dan gak bisa dipindah. Komentar # v7.0.1 mencatat versinya untuk manusia, dan Dependabot bisa memperbarui pin itu lewat pull request.
Permissions secukupnya
Setiap run mendapat GITHUB_TOKEN, token bawaan untuk mengakses repo-mu sendiri. permissions: write-all membuat setiap langkah bisa push kode, membuat release, dan mengubah issue. Batasi di level atas, lalu tambah per job kalau perlu:
permissions:
contents: read
Secret cuma untuk trigger tepercaya
pull_request: untuk PR dari fork, workflow jalan tanpa secret (kecualiGITHUB_TOKENyang cuma bisa membaca). Aman untuk menjalankan test.pull_request_target: jalan dengan secret dan token tulis milik repo-mu, bahkan untuk PR dari fork. Jangan taruh job deploy di situ, dan jangan jalankan kode dari PR di situ.- Deploy jalan cuma saat
pushkemain, setelah PR di-review, di job terpisah denganenvironment: productionyang bisa diatur supaya minta persetujuan dulu.
Workflow setelah semua dibenahi:
name: Deploy
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: npm ci && npm test
deploy:
needs: test
if: github.event_name == 'push'
environment: production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: curl -X POST -H "Authorization: Bearer $DEPLOY_TOKEN" https://api.example.com/deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
⚠️ Jebakan yang sering ketemu
- Terlalu percaya
***di log. Penyamaran gak menjangkau access log server, file yang di-upload sebagai artifact, atau bentuk lain dari secret itu. Jangan cetak secret sama sekali. - Action pihak ketiga lewat tag atau
@main. Keduanya bisa berubah. Pin ke SHA lengkap. permissionsdibiarkanwrite-allatau gak ditulis. Tulis eksplisit, mulai daricontents: read.- Job ber-secret di
pull_request_target. PR dari siapa pun memicunya dengan secret-mu.
TL;DR: Simpan secret di GitHub Secrets dan panggil dengan ${{ secrets.NAMA }}, jangan ditulis di workflow. Penyamaran *** cuma berlaku di log GitHub, jadi jangan echo secret dan jangan taruh di URL: masukkan lewat env dan kirim di header Authorization. Pin setiap action ke commit SHA lengkap, batasi permissions mulai dari contents: read, jalankan test PR dengan pull_request (tanpa secret), dan deploy cuma saat push ke main di job dengan environment: production.