Kurniadi Ahmad Wijaya
Forward Deployed AI Engineer
Membangun AI dan software kelas production untuk perbankan, fintech, dan enterprise.
Technical Consultant, Software & AI · Metrodata (PT Mitra Integrasi Informatika)
Langsung ke konten

Tulisan

Kode Asing di Samping Secret Production

EN, ID

  • application-security
  • supply-chain-security
  • azure-container-apps
  • keda
  • case-study

Platform keamanan seharusnya tidak mengeksekusi kode yang sedang diperiksa. Namun, dari audit yang saya minta, saya mengetahui bahwa hal itu dapat terjadi pada platform yang saya bangun. Saat repository diproses, kode di dalamnya dapat berjalan di proses API yang terbuka ke internet. Environment proses tersebut menyimpan secret production, termasuk key GitHub App yang terpasang di seluruh organisasi.

Di sini saya menjelaskan langkah penanganannya: stopgap pada hari yang sama, perpindahan ke worker terisolasi dengan izin cloud terbatas, cut-over dengan outage tercatat sekitar 9 menit, serta bug scaling yang ditemukan saat rollout. Saya juga menjelaskan pembaruan tool yang kemudian membuat stopgap tersebut tidak lagi bekerja sesuai harapan.

Masalah tersebut ditemukan dan berhasil direproduksi dalam audit sebelum platform digunakan lebih luas.

Scanner yang Berjalan di dalam API

Pris.AI adalah platform keamanan kode berbasis AI yang saya bangun dan operasikan di Metrodata (MII). Sebuah GitHub App menghubungkan repository di organisasi. Setiap pull request menjalankan delapan security engine open-source, model AI melakukan triage temuan, dan fix agent bisa membuka pull request berisi usulan perbaikan. Per 24 Oktober, platform ini sudah mengirim hasil scan ke 628 pull request di 24 repository.

Pada versi pertama di bulan Juni, scan berjalan secara inline dalam thread backend API. Saya memilih cara itu karena hak deployment saya hanya Contributor. Untuk menjalankan job terpisah, saya membutuhkan principal dengan role assignment yang tidak dapat saya buat. Dengan executor inline, produk tetap dapat dirilis.

Akibatnya, isi repository diproses dalam proses dan environment yang sama dengan API yang menyimpan secret platform.

Reproduksi Masalah dalam Audit

Pada akhir September, sebelum platform dipakai lebih luas, saya meminta audit kesiapan komersial: tiga belas review spesialis, ditambah review red-team dan sintesis. Temuan teratasnya adalah eksekusi kode di proses API.

Eksekusi kode itu dapat terjadi melalui alur upgrade dependency yang biasa digunakan fix agent. Saat meng-upgrade dependency Python, agent menjalankan uv lock. Tanpa --no-build, uv dapat mem-build project lokal untuk membaca metadata, termasuk menjalankan setup.py milik project tersebut. Kode apa pun yang disisipkan di sana akan berjalan dengan seluruh environment backend. Jalur ini berhasil direproduksi dalam audit.

Risiko terkait juga muncul karena beberapa scanner memuat konfigurasi dari repository yang sedang di-scan. Konfigurasi Checkov di repository dapat menunjuk ke direktori external checks. Sementara itu, gitleaks otomatis memuat file ignore dari source yang di-scan, sehingga temuan tertentu dapat disembunyikan.

Audit yang sama menemukan masalah lain, dan semuanya diperbaiki dalam rangkaian kerja yang sama: setiap pengguna Entra bisa menjadi admin, chat agent bisa men-suppress temuan, dan token GitHub level organisasi muncul di argumen proses.

Stopgap pada Pagi yang Sama

Perbaikan pertama diterapkan pada pagi 25 September untuk segera mengurangi risiko. Sambil menyiapkan isolasi sebagai batas keamanan, saya menerapkan langkah sementara berikut:

  • uv lock dan uv export berjalan dengan --no-build. Project yang butuh build dilaporkan sebagai "could not upgrade safely", dan perubahan parsial dikembalikan. bun tetap memakai --ignore-scripts.
  • Setiap subprocess scanner dan fix mendapat environment berbasis allowlist. Variabel yang namanya seperti key, token, dan secret, endpoint managed identity, serta setting Azure dan GitHub dibuang.
  • Scanner memakai konfigurasi milik platform. File konfigurasi Checkov dan gitleaks dari repository dihapus dari hasil clone, karena saya sudah memverifikasi bahwa flag command line saja tidak mencegah konfigurasi repository dimuat.
  • Kill switch bisa mematikan fix pull request sebelum ada yang dibaca atau dijalankan.

Acceptance test memakai fixture repository yang setup.py-nya menulis file penanda. Setelah perubahan, tidak ada penanda yang tertulis, dan dump environment dari child process sungguhan tidak berisi variabel secret.

Efektivitas flag bergantung pada perilaku tool yang menjalankannya. Karena itu, batas keamanan tetap perlu diterapkan melalui arsitektur.

Isolasi Tanpa Izin Baru

Pada sore hari yang sama, implementasi arsitektur baru sudah tersedia di branch:

Pris.AI saat ini, dari studi kasusnya. Kode repository hanya di-clone dan diproses di Container Apps Jobs; di production, control plane menolak menjalankan scan secara inline.
  1. API menaruh pesan di queue Service Bus. Pesan itu hanya membawa claim token sekali pakai.
  2. KEDA melihat queue tersebut dan menjalankan Container Apps Job yang terisolasi.
  3. Job mengklaim pekerjaannya dari API dengan compare-and-swap dan lease. Klaim itu mengembalikan bearer token per job dan token GitHub baru yang dibatasi untuk satu repository dan satu tujuan.
  4. Di dalam job, tool repository berjalan di sandbox: user ID diturunkan, resource dibatasi, process group bisa di-kill, dan pembacaan file tidak mengikuti symlink. Clone dibatasi, dan ukuran repository diperiksa lebih dulu.
  5. Hasilnya dikirim kembali ke API lewat bearer milik job itu sendiri. Pesan yang rusak atau tidak sah masuk ke dead-letter queue.

Di production, control plane menolak scan inline dengan 503, dengan emergency override yang harus diset secara sengaja.

Desain ini disesuaikan dengan batas izin Contributor. Karena tidak dapat memberikan role kepada worker, saya melakukan deploy tanpa managed identity sama sekali. Validation script memeriksa agar definisi worker tidak menyertakan kredensial. Setiap job hanya menerima token dari proses klaim untuk satu repository. Role assignment sudah ditulis di Bicep dengan flag pengaktifan, tetapi pemberian role-nya masih tertunda saat terakhir saya periksa.

Pekerjaan hardening lainnya dilakukan secara paralel: identity assertion bertanda tangan dengan masa berlaku 30 detik yang terikat ke body request dari web tier, kontrol akses dengan role tetap, sign-in khusus Entra, dan session server-side yang dapat dicabut. Seluruh 14 item prioritas selesai dalam sekitar satu setengah hari melalui stacked branch. Saya menulis spec dan mengarahkan AI coding agent untuk mengerjakan sebagian besar implementasi, lalu meninjau, menguji di Azure, dan menggabungkan setiap perubahan.

Cut-Over dan Worker yang Tidak Berjalan

Rollout berlangsung pada 26 dan 27 September. Saat beralih dari mode inline ke mode queue, backend tidak tersedia selama sekitar 9 menit. Platform ini merupakan tool internal, tetapi outage seperti itu tidak dapat diterima untuk produk pelanggan.

Setelah perpindahan, scan tetap tidak berjalan karena belum ada satu pun eksekusi worker yang dimulai di production. Scaler Service Bus milik KEDA memerlukan klaim Manage untuk membaca jumlah pesan di queue melalui management endpoint. Scale rule yang saya gunakan justru memakai key Listen-only milik worker, sehingga setiap polling mendapat 401.

Memberikan key Manage kepada worker memang lebih mudah, tetapi akan memperluas izinnya secara keliru. Saya memilih rule terpisah untuk setiap queue, dengan hak Manage yang hanya digunakan untuk autentikasi scaler. Container tetap memakai key Listen-only agar kode repository tidak mendapat akses ke key yang dapat mengelola queue. Identity khusus scaler juga sudah disiapkan untuk digunakan setelah role assignment memungkinkan.

Setelah perbaikan, saya menjalankan latihan dead-letter untuk memastikan pesan bermasalah masuk ke dead-letter queue dan email alert diterima.

Perubahan Perilaku Tool Setelah Pembaruan

Pada 1 Oktober, CI mengambil versi uv yang lebih baru. Versi 0.12.20 dan 0.12.21 menjalankan setup.py milik project lokal saat uv lock --no-build jika project mendeklarasikan metadata dinamis. Jaminan stopgap tidak lagi berlaku untuk upgrade dependency oleh fix agent.

Perbaikannya tidak lagi bergantung pada uv. Sebuah pre-check menolak project dengan metadata dinamis, project lama berbasis setup.py atau setup.cfg, serta anggota workspace seperti itu sebelum uv dijalankan, dan uv di-pin ke 0.12.19 di image maupun CI.

Perubahan pada rilis minor tersebut memperlihatkan mengapa saya tidak dapat mengandalkan flag sebagai batas keamanan. Isolasi tetap diperlukan ketika perilaku tool pihak ketiga berubah.

Batasan dan Perbaikan Proses

  • Buat threat model sejak deploy pertama. Executor inline merupakan kompromi yang masuk akal untuk batas izin pada Juni. Namun, threat model tertulis seharusnya sudah tersedia saat itu agar risikonya dapat ditinjau sejak awal, tanpa menunggu audit tiga bulan kemudian.
  • Rencanakan cut-over tanpa downtime. Menjalankan mode lama dan baru berdampingan, lalu memindahkan traffic setelah scan pertama lewat queue berhasil, kemungkinan besar bisa menghindari 9 menit itu.
  • Uji seluruh jalur sebelum berpindah. Satu pemeriksaan end to end, "scan yang masuk queue menjalankan job dan mengembalikan temuan", akan mengungkap bug scaler sebelum perubahan diterapkan ke production.
  • Pin tool yang menegakkan keamanan. Jaminan --no-build bergantung pada versi tool yang tidak di-pin. Sekarang uv di-pin dan pre-check berjalan sebelumnya, jadi rilis baru tidak mengubah apa pun sampai ada yang memindahkan pin dengan sengaja.
  • Auditnya internal. Cakupannya luas, tetapi itu bukan penetration test independen.

Dengan flag, saya dapat mengurangi risiko dalam hitungan jam sambil menyiapkan isolasi. Risiko yang tersisa kemudian dibatasi melalui job terpisah: kode repository hanya mendapat token untuk satu repository dan tidak lagi berjalan di proses API yang menyimpan key.