Bagian 6: Ruleset yang Memblokir Merge, dan Lima Demo PR yang Harus Gagal

· 18 menit baca

English
//TUTORIAL · 6 DARI 10 — Secure CI/CD Pipeline dengan Go, GitHub Actions, Vault, dan Dokploy

Di Bagian 5, workflow pr-checks.yml dan source-branch.yml sudah jalan di setiap PR. Masalahnya, check yang merah itu belum memaksa apa-apa. Tombol Merge tetap hijau dan tetap bisa ditekan. Jadi gate-nya baru setengah jadi.

Di bagian ini saya menutup celah itu dengan ruleset GitHub di development dan main. Setelah ruleset aktif, PR dengan check merah nggak bisa di-merge, push langsung ditolak, dan saya sendiri sebagai admin juga nggak bisa melewatinya.

Lalu bagian yang paling saya suka: membuktikannya. Saya membuat lima PR yang sengaja jahat (secret bocor, command injection, dependency rentan, container root, dan PR yang melompati development). Kelimanya harus diblokir oleh gate yang berbeda.

Catatan

Analogi yang saya pakai: workflow di Bagian 5 itu detektor logam. Ruleset itu satpam yang benar-benar menahan orang kalau detektornya bunyi. Detektor tanpa satpam cuma bunyi, lalu orangnya tetap lewat.

Ruleset itu apa, dan kenapa bukan branch protection

Ruleset adalah kumpulan aturan yang GitHub tegakkan di server saat ada push atau merge ke branch tertentu. Karena dicek di server, aturannya nggak bisa diakali dari laptop.

GitHub punya dua fitur yang mirip. Saya pilih ruleset karena beberapa alasan:

Branch protection (lama)

Ruleset (baru)

Jumlah aturan per branch

Satu aturan per pola branch

Bisa berlapis

Siapa yang bisa melihat

Admin

Semua yang punya akses baca repo (transparan)

Riwayat perubahan

Nggak ada

Ada, plus Rule Insights (log push yang ditolak/diizinkan)

Ekspor/impor JSON

Nggak

Bisa, jadi bisa disimpan di git

Bypass admin

Satu centang

Diatur eksplisit lewat bypass_actors

Aturan yang saya pakai cuma lima jenis:

Aturan (type)

Artinya

Kenapa dipakai

deletion

Branch nggak boleh dihapus

development dan main nggak bisa hilang, sengaja atau nggak

non_fast_forward

Force push dilarang

Force push bisa menulis ulang history dan menghapus commit yang sudah di-review

pull_request

Semua perubahan wajib lewat PR

Nggak ada jalan masuk selain lewat check

required_status_checks

Check tertentu wajib hijau

Inilah yang menyambungkan ruleset ke workflow Bagian 5

bypass_actors: []

Nggak ada yang boleh melewati, termasuk admin

Anti-goal dari Bagian 1: "pipeline bisa dilewati admin"

Poin terakhir itu yang paling penting. Kalau admin boleh lewat, gate-nya nggak bermakna. Satu kali "ah, darurat, merge saja" dan semua jaminan hilang.

Langkah 0: workflow harus jalan sekali dulu

Ruleset mewajibkan check berdasarkan nama. Kalau nama di ruleset nggak sama persis dengan nama yang dilaporkan workflow, PR akan menunggu selamanya dengan status "Expected — Waiting for status to be reported".

Karena itu urutannya penting: workflow dari Bagian 5 harus sudah jalan minimal sekali di sebuah PR. Setelah itu nama check-nya sudah ada di GitHub, muncul di pilihan UI ruleset, dan bisa saya baca lewat API.

Saya ambil daftar check dari PR workflow di Bagian 5:

bash
R=dimss113/secure-cicd-goSHA=$(gh api repos/$R/pulls/2 --jq .head.sha)gh api repos/$R/commits/$SHA/check-runs \  --jq '.check_runs[] | .name + "  " + .conclusion + "  app=" + (.app.id|tostring) + " " + .app.slug'

SHA adalah commit terakhir PR itu. Endpoint check-runs mengembalikan semua check yang pernah dilaporkan untuk commit tersebut, lengkap dengan app yang mengirimnya.

Cek hasil:

text
Semgrep OSS  success  app=57789 github-advanced-securityTrivy  success  app=57789 github-advanced-securitygitleaks  success  app=57789 github-advanced-securitydependency-review  success  app=15368 github-actionstrivy-image  success  app=15368 github-actionsgovulncheck  success  app=15368 github-actionsgitleaks  success  app=15368 github-actionssemgrep  success  app=15368 github-actionstest  success  app=15368 github-actionstrivy-config  success  app=15368 github-actions

Tujuh check dengan app=15368 adalah job saya di pr-checks.yml (nama check = ID job). Tiga lainnya datang dari Code scanning, dibuat otomatis karena workflow meng-upload SARIF.

Yang sempat bikin saya bingung: gitleaks muncul dua kali, dari dua app berbeda.

Bahaya

Kunci check ke app GitHub Actions

Kalau ruleset cuma bilang "wajib ada check gitleaks yang hijau", check bernama gitleaks dari mana pun dianggap sah. Itu membuka dua masalah. Pertama, tabrakan nama: check dari Code scanning bisa ikut terhitung. Kedua, pemalsuan: siapa pun yang punya token dengan hak menulis status commit bisa mengirim test = success lewat API tanpa test benar-benar jalan. Dengan "integration_id": 15368, GitHub hanya menerima check yang dikirim oleh GitHub Actions. Angka 15368 itu ID app GitHub Actions di github.com, sama untuk semua repo.

Langkah 1: uji gate dulu, ternyata ada dua lubang

Sebelum memasang ruleset, saya coba dulu kelima demo di salinan repo. Gate yang belum pernah diuji dengan kasus buruk belum tentu bekerja. Dan memang ketemu dua masalah.

Lubang 1: salah ketik di trivy-config. Saya menulis limit-se-for-sarif, padahal input yang benar limit-severities-for-sarif. GitHub sebenarnya sudah memperingatkan lewat anotasi kuning di ringkasan run, tapi job-nya tetap hijau, jadi gampang terlewat. Akibatnya laporan SARIF memuat semua severity (LOW dan MEDIUM juga), jadi tab Security berisik. Gate-nya sendiri nggak terpengaruh. Pelajarannya: baca anotasi kuning di ringkasan run.

Lubang 2: Semgrep nggak menangkap command injection. Ini yang lebih serius. Kode demo exec.Command("sh", "-c", <input dari URL>) saya scan dengan beberapa ruleset:

Ruleset Semgrep

Hasil

p/golang (yang saya pakai waktu itu)

Nggak ada temuan

p/gosec

Nggak ada temuan

p/default

dangerous-exec-command (ERROR)

p/golang isinya lebih banyak aturan best practice. Aturan keamanan dangerous-exec-command ada di p/default. Saya cek juga p/default di kode yang sekarang: nol temuan, jadi nggak bikin PR lain merah tanpa alasan.

Perbaikannya dua baris di pr-checks.yml. Ini versi akhirnya di repo:

.github/workflows/pr-checks.yml yaml
  semgrep:    # ...          semgrep scan --config p/golang --config p/default --config p/secrets \            --severity ERROR --error --metrics=off \  # ...  trivy-config:    # ...          severity: HIGH,CRITICAL          limit-severities-for-sarif: true

(Dipotong, hanya baris yang berubah.) --config boleh lebih dari satu, Semgrep menggabungkan semua aturannya.

Cek hasil:

bash
grep -n "limit-severities-for-sarif" .github/workflows/pr-checks.yml   # 2 baris: trivy-config dan trivy-imagesemgrep scan --config p/golang --config p/default --config p/secrets \  --severity ERROR --error --metrics=off --quiet .echo "exit=$?"                                                         # exit=0

Perubahan ini belum saya commit. Nanti ikut di PR pertama yang jalan di bawah ruleset.

Langkah 2: ruleset sebagai kode

Ruleset bisa dibuat dengan klik-klik di Settings → Rules. Saya pilih file JSON + API, dengan prinsip yang sama seperti vault/policies/ di Bagian 2: bisa di-review lewat PR, bisa dipasang ulang persis sama, dan nggak ada centang yang terlewat.

Ini protect-development:

docs/rulesets/protect-development.json json
{  "name": "protect-development",  "target": "branch",  "enforcement": "active",  "bypass_actors": [],  "conditions": {    "ref_name": {      "include": ["refs/heads/development"],      "exclude": []    }  },  "rules": [    { "type": "deletion" },    { "type": "non_fast_forward" },    {      "type": "pull_request",      "parameters": {        "required_approving_review_count": 0,        "dismiss_stale_reviews_on_push": false,        "require_code_owner_review": false,        "require_last_push_approval": false,        "required_review_thread_resolution": false,        "allowed_merge_methods": ["merge"]      }    },    {      "type": "required_status_checks",      "parameters": {        "strict_required_status_checks_policy": true,        "do_not_enforce_on_create": false,        "required_status_checks": [          { "context": "test", "integration_id": 15368 },          { "context": "gitleaks", "integration_id": 15368 },          { "context": "semgrep", "integration_id": 15368 },          { "context": "govulncheck", "integration_id": 15368 },          { "context": "dependency-review", "integration_id": 15368 },          { "context": "trivy-config", "integration_id": 15368 },          { "context": "trivy-image", "integration_id": 15368 }        ]      }    }  ]}

protect-main isinya sama, dengan tiga perbedaan:

docs/rulesets/protect-main.json json
  "conditions": { "ref_name": { "include": ["refs/heads/main"], "exclude": [] } },  ...        "strict_required_status_checks_policy": false,        ...          { "context": "trivy-image", "integration_id": 15368 },          { "context": "source-branch", "integration_id": 15368 }

(Dipotong, sisanya identik dengan protect-development.)

Catatan

File di repo punya satu check lagi

Kalau kamu buka docs/rulesets/ di repo, kedua file juga mewajibkan check exceptions. Job itu baru dibuat di Bagian 9 (proses exception). Di titik ini job-nya belum ada, jadi jangan diwajibkan dulu, nanti PR menunggu selamanya.

Kunci yang perlu dijelaskan:

Kunci

Artinya

enforcement: "active"

Langsung ditegakkan. disabled berarti mati

bypass_actors: []

Kosong: nggak ada siapa pun yang boleh lewat, termasuk admin

ref_name.include

Branch yang dikenai aturan, dalam bentuk ref lengkap

required_approving_review_count: 0

Saya kerja sendiri, dan GitHub nggak mengizinkan approve PR milik sendiri. Di tim nyata minimal 1

allowed_merge_methods: ["merge"]

Hanya merge commit. Squash dan rebase dimatikan

strict_required_status_checks_policy

"Require branches to be up to date". true di development, false di main

do_not_enforce_on_create: false

Aturan tetap berlaku untuk branch yang baru dibuat

context

Nama check, sama dengan ID job

integration_id: 15368

Check harus dari GitHub Actions

Kenapa hanya merge commit

Ini bukan soal selera. Di Bagian 8, workflow deploy ke production nggak build ulang. Ia mencari commit head dari PR development → main lewat merge commit di main, lalu mencari image sha-<commit> yang sudah jalan di dev. Squash atau rebase membuat commit baru dengan SHA berbeda, dan jejak itu putus. Jadi metode merge saya kunci dari sekarang, di kedua branch supaya konsisten.

Kenapa strict beda di dua branch

Up to date artinya branch PR harus sudah berisi commit terbaru dari branch tujuan. Contoh: PR A dan PR B dibuat dari development yang sama. A di-merge duluan. B sekarang ketinggalan, dan check-nya diuji tanpa perubahan A. Bisa saja masing-masing hijau tapi gabungannya rusak. Dengan strict: true, B harus di-update dulu dan check jalan ulang. Itu yang saya mau di development.

Di main lain cerita. Setiap merge development → main membuat merge commit baru di main yang nggak ada di development. Kalau strict nyala, PR rilis berikutnya selalu dianggap ketinggalan, dan main harus digabung balik ke development setiap kali. Merepotkan dan mengotori history. Karena PR ke main selalu dari development yang sudah lolos semua check, strict di main saya matikan.

Kenapa source-branch hanya di main

source-branch.yml hanya jalan untuk PR ke main. Kalau saya wajibkan juga di development, check itu nggak pernah muncul di PR ke development, dan PR-nya menunggu selamanya.

Cek hasil:

bash
jq -e . docs/rulesets/protect-development.json > /dev/null && echo OK-devjq -e . docs/rulesets/protect-main.json > /dev/null && echo OK-main

jq -e . gagal dengan exit code bukan nol kalau ada koma atau kurung yang salah. Harus keluar OK-dev dan OK-main.

Langkah 3: pasang lewat API

bash
gh api -X POST repos/$R/rulesets --input docs/rulesets/protect-development.json --jq '.id, .name, .enforcement'gh api -X POST repos/$R/rulesets --input docs/rulesets/protect-main.json --jq '.id, .name, .enforcement'

POST repos/$R/rulesets membuat ruleset baru, isinya diambil dari file JSON. Output-nya ID (angka), nama, dan active. Catat kedua ID.

Perhatian

POST cuma sekali

Menjalankan POST lagi membuat ruleset kedua dengan nama sama. Untuk mengubah, edit JSON lalu gh api -X PUT repos/$R/rulesets/<ID> --input <file>. Kalau sudah terlanjur dobel, hapus satu dengan gh api -X DELETE repos/$R/rulesets/<ID>.

Cek hasil:

bash
gh api repos/$R/rulesets --jq '.[] | "\(.id)  \(.name)  \(.enforcement)"'gh api repos/$R/rules/branches/development --jq '.[].type'gh api repos/$R/rules/branches/main --jq '.[].type'

Endpoint rules/branches/<branch> menampilkan aturan yang benar-benar berlaku di branch itu, gabungan dari semua ruleset. Untuk kedua branch harus keluar deletion, non_fast_forward, pull_request, required_status_checks.

Di UI, buka Settings → Rules → Rulesets → protect-development. Yang harus kamu lihat: Enforcement status: Active, Bypass list kosong, dan di bagian Require status checks to pass ada tujuh check, masing-masing berlabel GitHub Actions.

Langkah 4: push langsung harus ditolak

Pastikan dulu kedua ruleset active. Kalau belum, push uji ini berhasil dan commit kosongnya masuk ke development.

bash
git stashgit switch developmentgit commit --allow-empty -m "test: direct push must be rejected"git push origin development

git stash menyimpan perubahan Langkah 1 dan 2 sementara. --allow-empty membuat commit tanpa perubahan file, cukup untuk menguji push.

Cek hasil (kurang lebih):

text
remote: error: GH013: Repository rule violations found for refs/heads/development.remote: - Changes must be made through a pull request.remote: - Required status checks "test", "gitleaks", ... are expected. ! [remote rejected] development -> development (push declined due to repository rule violations)

Ini ditolak walaupun saya admin repo. Bersihkan commit uji dan kembali ke branch kerja:

bash
git reset --hard origin/developmentgit switch ci/harden-gatesgit stash pop

reset --hard aman di sini karena satu-satunya isinya commit kosong tadi.

Langkah 5: PR pertama di bawah ruleset, dan coba --admin

bash
git add .github/workflows/pr-checks.yml docs/rulesets/git commit -m "ci: harden gates and record branch rulesets"git push -u origin ci/harden-gatesgh pr create --base development \  --title "ci: harden gates and record branch rulesets" \  --body "Fixes limit-severities-for-sarif typo, adds Semgrep p/default, records the rulesets applied via the API."

Selagi check masih jalan, saya langsung coba melewatinya dengan hak admin:

bash
gh pr merge --merge --admin

--admin artinya "merge dengan hak admin, abaikan aturan". Karena bypass_actors kosong, ini ditolak dengan pesan Repository rule violations found ... required status checks ... are expected.

Setelah check hijau, merge dengan cara normal:

bash
gh pr checks --watchgh pr merge --merge --delete-branchgit switch development && git pull

Tips

"This branch is out-of-date with the base branch"

Itu aturan strict sedang bekerja. Jalankan gh pr update-branch, check jalan ulang, lalu merge.

Semua demo di bawah harus dibuat setelah PR ini di-merge. Demo command injection bergantung pada p/default.

Lima demo PR yang harus gagal

Semua demo mengikuti pola yang sama:

text
development terbaru → branch demo/... → ubah satu hal "buruk" → commit → push → PR→ check merah → "Merging is blocked" → TUTUP PR (jangan merge) → hapus branch

Perintah yang dipakai berulang:

Perintah

Fungsinya

git switch development && git pull

Mulai dari development terbaru, supaya demo nggak saling tercampur

gh pr checks --watch

Tunggu semua check. Keluar dengan exit code bukan nol kalau ada yang gagal, dan itu memang yang diharapkan

gh pr view --json mergeStateStatus --jq .mergeStateStatus

Harus BLOCKED

gh pr close --delete-branch

Tutup tanpa merge, hapus branch

Di setiap PR demo, yang harus kamu lihat di bagian bawah halaman PR: daftar check dengan satu (atau dua) tanda silang merah, sisanya centang hijau, lalu kotak abu-abu bertuliskan "Merging is blocked" dengan keterangan bahwa required status check gagal. Tombol merge nonaktif. Klik Details di check yang merah untuk melihat log alasannya.

Demo 1: secret bocor, ditangkap gitleaks

bash
git switch development && git pullgit switch -c demo/leak-secretmkdir -p internal/demoKEY=$(openssl rand -hex 20)printf 'package demo\n\n// Demo only: fake credential that the secret scanner must catch.\nconst apiKey = "%s"\n\nfunc Key() string { return apiKey }\n' "$KEY" > internal/demo/leak.go

openssl rand -hex 20 menghasilkan 40 karakter hex acak. Bentuknya mirip API key, tapi nggak berlaku di mana pun. Nama variabel apiKey ditambah string acak panjang adalah pola yang dikenali aturan generic-api-key milik Gitleaks.

Dua hal yang saya pelajari waktu memilih secret palsu. Contoh AWS key yang berakhiran EXAMPLE justru diabaikan Gitleaks. Dan kalau pakai pola token GitHub asli, push protection GitHub sudah menolak di git push, jadi CI nggak pernah jalan (itu juga bukti, lapisan pertama, tapi bukan yang mau saya tunjukkan di sini).

Cek dulu di laptop:

bash
git add internal/demo/leak.gogit commit -m "demo: hardcode an api key"gitleaks git . --log-opts="HEAD~1..HEAD" --redact --no-bannerecho "exit=$?"
text
Finding:     const apiKey = "REDACTED"RuleID:      generic-api-keyFile:        internal/demo/leak.goWRN leaks found: 1exit=1

Push, buka PR ke development, tunggu check.

Cek hasil: gitleaks merah, enam check lain hijau, status BLOCKED. Di tab Security → Code scanning muncul alert generic-api-key untuk PR ini.

Perhatian

Commit-nya tetap tersimpan di GitHub

Setelah PR ditutup, commit-nya masih bisa diakses lewat refs/pull/<n>/head. Karena itu hanya pakai nilai acak. Kalau yang bocor secret asli, menutup PR nggak cukup: langsung rotasi secret-nya.

Demo 2: command injection, ditangkap semgrep

internal/httpapi/debug.go go
package httpapi import (	"net/http"	"os/exec") // Demo only: runs a shell command taken straight from the query string.func debugExec(w http.ResponseWriter, r *http.Request) {	out, _ := exec.Command("sh", "-c", r.URL.Query().Get("cmd")).CombinedOutput()	_, _ = w.Write(out)}

r.URL.Query().Get("cmd") sepenuhnya dikendalikan pengunjung, dan exec.Command("sh", "-c", ...) menjalankannya sebagai perintah shell. GET /debug?cmd=cat /run/config/config.json sudah cukup untuk membocorkan secret JWT dan AES dari Bagian 4. Itu Remote Code Execution. Fungsinya bahkan belum dipasang ke router, tapi Semgrep tetap menandainya karena ia membaca kode, bukan menjalankan aplikasi.

Cek di laptop:

bash
semgrep scan --config p/golang --config p/default --config p/secrets --severity ERROR --error --metrics=off .echo "exit=$?"

Harus muncul temuan go.lang.security.audit.dangerous-exec-command.dangerous-exec-command di internal/httpapi/debug.go, lalu exit=1.

Cek hasil: semgrep merah, status BLOCKED. Di tab Files changed, baris exec.Command diberi anotasi dari Code scanning.

Kalau demo ini ternyata hijau, hampir pasti branch-nya dibuat sebelum PR Langkah 5 di-merge. Sebelum perbaikan itu, demo ini lolos. Justru itu poinnya: saya menguji gate saya sendiri, bukan cuma memasang tool.

Demo 3: dependency rentan, ditangkap govulncheck dan dependency-review

bash
git switch development && git pullgit switch -c demo/vuln-depgo get github.com/golang-jwt/jwt/v5@v5.2.1grep jwt go.mod   # require github.com/golang-jwt/jwt/v5 v5.2.1

Repo memakai v5.3.1. Saya turunkan ke v5.2.1, yang punya CVE-2025-30204 (GO-2025-3553, GHSA-mh63-6h87-95cp, severity High): header token yang dibuat khusus membuat parser mengalokasikan memori berlebihan, jadi server bisa kehabisan memori. Diperbaiki di v5.2.2.

Cek di laptop:

bash
go build ./... && go test ./...go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./...

Build dan test tetap ok. Kodenya tetap jalan, dan justru itu bahayanya. Lalu govulncheck:

text
Vulnerability #1: GO-2025-3553    Found in: github.com/golang-jwt/jwt/v5@v5.2.1    Fixed in: github.com/golang-jwt/jwt/v5@v5.2.2      #1: internal/token/token.go:52:31: token.Issuer.Verify calls jwt.ParseWithClaims, which eventually calls jwt.Parser.ParseUnverified

Trace #1 membuktikan kode kita benar-benar memanggil bagian yang rentan: Verify dipakai endpoint GET /v1/me.

Cek hasil:

  • govulncheck merah (vuln yang reachable).

  • dependency-review merah, karena PR menambahkan versi dengan advisory High (fail-on-severity: high). Ringkasan job menampilkan GHSA-mh63-6h87-95cp.

  • trivy-image hijau, tapi laporannya mencatat CVE-2025-30204. Severity-nya HIGH, bukan CRITICAL, jadi nggak memblokir.

  • Status BLOCKED.

Dua alat berbeda menangkap masalah yang sama dari sudut berbeda. Itu yang saya maksud defense in depth. Kalau dependency-review ternyata hijau, cek apakah Dependency graph sudah aktif di Settings → Advanced Security.

Demo 4: container jalan sebagai root, ditangkap trivy-config

Di Dockerfile, saya ganti baris user:

Dockerfile dockerfile
# sebelumUSER 65532:65532# sesudah (demo)USER root

Proses di container jadi jalan sebagai UID 0. Kalau aplikasinya dibobol, misalnya lewat command injection seperti demo 2, penyerang langsung punya root di container, dan jalan ke host lewat celah container escape jadi lebih pendek.

Cek di laptop:

bash
trivy config --severity HIGH,CRITICAL --exit-code 1 .echo "exit=$?"
text
Dockerfile (dockerfile)Failures: 1 (HIGH: 1, CRITICAL: 0)DS-0002 (HIGH): Last USER command in Dockerfile should not be 'root'exit=1

Cek hasil: trivy-config merah dengan temuan DS-0002, status BLOCKED.

Demo 5: PR feature langsung ke main, ditangkap source-branch

Demo terakhir ini beda. Kodenya bersih. Yang salah adalah jalurnya.

bash
git switch development && git pullgit switch -c demo/main-from-featureecho "Demo: this branch tries to skip development." > docs/demo-skip-development.mdgit add docs/demo-skip-development.mdgit commit -m "demo: try to reach main without development"git push -u origin demo/main-from-featuregh pr create --base main --title "demo: feature straight to main" --body "Must be blocked by source-branch. Do not merge."

Yang diuji adalah --base main. Ini job yang menangkapnya, dari Bagian 5:

.github/workflows/source-branch.yml yaml
      - name: Only development may open a PR into main        env:          HEAD_REF: ${{ github.head_ref }}        run: |          if [ "$HEAD_REF" != "development" ]; then            echo "::error::PR into main must come from development, not '$HEAD_REF'"            exit 1          fi

Cek hasil: source-branch merah dengan pesan PR into main must come from development, not 'demo/main-from-feature'. Tujuh check pr-checks hijau. Status BLOCKED.

Artinya kode yang bersih pun nggak boleh masuk production tanpa lewat development, tempat ia di-deploy dan diuji dulu.

Ringkasan demo

PR demo

Yang diubah

Check yang merah

Kenapa

demo/leak-secret

const apiKey = "<40 hex acak>"

gitleaks

Pola generic-api-key

demo/cmd-injection

exec.Command("sh", "-c", query)

semgrep

dangerous-exec-command dari p/default

demo/vuln-dep

golang-jwt/jwt/v5 ke v5.2.1

govulncheck, dependency-review

GO-2025-3553, reachable lewat Verify, severity High

demo/root-container

USER root di Dockerfile

trivy-config

DS-0002 (HIGH)

demo/main-from-feature

PR feature → main

source-branch

Head branch bukan development

Untuk bukti, dua perintah ini berguna:

bash
gh pr list --state closed --search "demo in:title" --json number,title,state \  --jq '.[] | "#\(.number) \(.title) \(.state)"'gh api repos/$R/rulesets/rule-suites --jq '.[] | "\(.pushed_at)  \(.ref)  \(.result)  \(.actor_name)"'

Yang pertama mendaftar semua PR demo yang ditutup. Yang kedua membaca Rule Insights: setiap push yang dievaluasi ruleset beserta hasilnya. Push langsung di Langkah 4 harus tercatat sebagai fail. Di UI ada di Settings → Rules → Insights.

Batasan yang harus jujur ditulis

Ruleset menahan kesalahan dan jalan pintas. Ia nggak menahan semuanya.

Batasan

Penjelasan dan mitigasi

Admin bisa mengubah atau menghapus ruleset

Saya satu-satunya admin, jadi secara teknis saya bisa mematikan ruleset lalu push. Perubahannya tercatat di riwayat ruleset dan di JSON yang di-commit. Di perusahaan, ruleset dipasang di level organisasi oleh tim security, dan admin repo nggak bisa mengubahnya

0 approval

Kerja sendiri. Di tim: minimal 1 approval plus require_last_push_approval

PR bisa mengubah workflow gate-nya sendiri

Untuk event pull_request, GitHub menjalankan pr-checks.yml versi branch PR. Developer bisa mengganti isi job semgrep jadi run: echo ok, namanya tetap semgrep, statusnya hijau, dan ruleset puas karena hanya mengecek nama. Mitigasi di tim: CODEOWNERS untuk /.github/ plus code owner review, atau required workflows dari repo pusat milik tim security

Gate bergantung pada kualitas aturan scanner

Terbukti di Langkah 1: p/golang saja nggak cukup. Gate harus diuji ulang dengan kasus buruk setiap kali konfigurasinya diubah

Celah ketiga itu menurut saya pelajaran paling penting di bagian ini: gate harus dikelola oleh pihak yang berbeda dari yang dijaga.

Rangkuman

  • Dua ruleset aktif, protect-development dan protect-main, disimpan sebagai JSON di docs/rulesets/ dan dipasang lewat API.

  • Bypass list kosong. Push langsung ditolak GH013, dan gh pr merge --admin juga ditolak.

  • Tujuh check pr-checks wajib hijau, dikunci ke GitHub Actions lewat integration_id: 15368. main juga mewajibkan source-branch.

  • Hanya merge commit yang boleh, supaya Bagian 8 bisa mempromosikan image tanpa build ulang.

  • Dua lubang gate ditemukan dan ditambal sebelum ruleset dipasang: typo limit-severities-for-sarif dan Semgrep yang butuh p/default.

  • Lima demo PR, lima gate berbeda, semuanya BLOCKED.

Sekarang kode nggak bisa masuk development atau main tanpa lolos gate. Tapi kode yang lolos itu belum jalan di mana pun. Di Bagian 7, saya menyiapkan Dokploy dengan dua environment, dev dan production, lengkap dengan Vault Agent sidecar dan auto-deploy yang dimatikan, supaya nanti hanya CI yang boleh memicu deploy.

Artikel terkait