CI/CD Pipeline Security — Security

Keamanan CI/CD Pipeline CI/CD pipeline adalah jalur otomatis dari kode ke production. Jika pipeline di-compromise, penyerang bisa menyisipkan kode berbahaya…

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 menjalankan curl "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 }}

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

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

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.

Yang akan kamu pelajari