Debugging backend untuk job runner yang berjalan di sandbox sering terasa membingungkan ketika hasil eksekusi tidak konsisten: command terlihat berjalan di VM, tetapi file yang terbaca ternyata berasal dari host developer, atau proses tertentu diam-diam menggunakan socket dan cache host. Ini bukan bug kecil. Begitu boundary antara sandbox dan host bocor, hasil debugging menjadi tidak bisa dipercaya dan risiko keamanan meningkat.

Studi kasus ini membahas pola bug nyata pada service/job runner untuk agen coding yang seharusnya dieksekusi di VM Linux disposable, tetapi sebagian proses, file, atau environment justru berasal dari host. Pola isolasi ini bisa dianggap sejalan dengan pendekatan sistem kerja terisolasi seperti konsep sandbox disposable ala Clawk, tetapi artikel ini fokus pada debugging dan hardening, bukan ulasan produk.

Gejala awal: output terlihat benar, tetapi sumber eksekusinya salah

Kebocoran sandbox ke host jarang muncul sebagai error yang eksplisit. Yang lebih sering terjadi adalah gejala yang tampak sepele, misalnya path yang aneh, dependency tersedia padahal image sandbox tidak memasangnya, atau file sementara tetap ada setelah VM dihancurkan.

Indikator yang sering muncul

  • PATH atau binary tidak sesuai image sandbox. Contoh: command node, python, atau git tersedia dengan versi yang hanya ada di laptop developer.
  • Workspace berisi file yang tidak pernah disalin ke VM. Misalnya file konfigurasi lokal host ikut terbaca.
  • Cache terlalu “ajaib”. Build kedua sangat cepat karena ternyata memakai cache host atau shared temp directory.
  • Job bisa mengakses daemon host, seperti Docker socket atau SSH agent socket, meskipun seharusnya tidak ada akses keluar dari VM.
  • Hostname, UID, atau mount table tidak sesuai ekspektasi. Log menunjukkan identitas mesin host, bukan guest VM.
  • Bug tidak bisa direproduksi di CI. Di laptop jalan, di CI gagal, karena CI tidak memiliki kebocoran mount atau env yang sama.

Contoh log yang patut dicurigai

[runner] workspace=/work/task-1842
[runner] executing: /bin/sh -lc "which python3 && python3 --version"
[runner] output: /usr/local/bin/python3
[runner] output: Python 3.x
[runner] note: image sandbox seharusnya hanya memiliki /usr/bin/python3
[runner] env HOME=/home/dev
[runner] env SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.xxx/Listeners
[runner] warning: host-specific socket detected
[runner] reading /tmp/agent-cache/index.json
[runner] file owner uid=501 gid=20
[runner] warning: uid/gid cocok dengan host developer, bukan guest VM

Log seperti ini tidak otomatis membuktikan kebocoran, tetapi cukup kuat untuk memulai audit boundary secara sistematis.

Langkah reproduksi: buktikan boundary, jangan menebak

Kesalahan umum saat debugging backend adalah langsung memperbaiki gejala. Pendekatan yang lebih aman adalah membuktikan dulu apakah job benar-benar berjalan di guest VM dan seluruh resource yang dipakai memang milik sandbox.

1. Verifikasi identitas runtime

Jalankan pemeriksaan minimal sebelum job utama:

uname -a
hostname
id
pwd
printenv | sort
mount
cat /proc/1/cgroup 2>/dev/null || true
ls -ld /tmp /var/tmp .

Yang dicari bukan output spesifik tertentu, tetapi konsistensi. Jika hostname kadang nama VM dan kadang nama host laptop, atau HOME mengarah ke direktori pengguna host, boundary sudah meragukan.

2. Tanam file sentinel di host dan di guest

Buat penanda yang hanya ada di satu sisi.

# di host
mkdir -p /tmp/host-only
echo host > /tmp/host-only/sentinel.txt

# di image guest atau saat bootstrap VM
mkdir -p /sandbox-only
echo guest > /sandbox-only/sentinel.txt

Lalu dari dalam job:

test -f /tmp/host-only/sentinel.txt && echo "host file visible"
test -f /sandbox-only/sentinel.txt && echo "guest file visible"

Jika file host terlihat dari job tanpa alasan desain yang jelas, berarti ada mount atau shared directory yang perlu diaudit.

3. Uji socket dan service yang seharusnya tidak tersedia

env | grep -E 'SSH_AUTH_SOCK|DOCKER_HOST|XDG_RUNTIME_DIR'
ls -l /var/run/docker.sock 2>/dev/null || true
ss -ltnp 2>/dev/null | head

Kebocoran umum bukan hanya file, tetapi juga socket. Begitu socket host bisa diakses, job runner dapat memperoleh kemampuan yang jauh lebih besar daripada yang dimaksudkan.

4. Uji perilaku setelah VM dihancurkan

Jika sandbox benar-benar disposable, file output dan cache lokal tertentu seharusnya hilang setelah teardown, kecuali memang ada volume yang sengaja dipersistenkan. Bila state tetap muncul tanpa volume yang sah, besar kemungkinan ada shared directory yang tidak disadari.

Akar masalah umum yang membuat job runner bocor ke host

Kasus seperti ini biasanya bukan berasal dari satu bug tunggal, melainkan kombinasi asumsi yang salah pada mount, environment, permission, dan fallback runtime.

1. Mount workspace salah atau terlalu luas

Penyebab paling umum adalah direktori host di-bind ke guest dengan cakupan terlalu besar. Contohnya, seluruh home directory host ikut termount demi “memudahkan akses source code”, lalu job tanpa sadar membaca ~/.gitconfig, credential helper, cache package manager, atau file SSH.

Masalah lain adalah path canonicalization yang longgar. Misalnya service menganggap semua path berada di bawah /work, tetapi symlink atau relative path seperti ../ memungkinkan akses ke mount lain.

Catatan: Bind mount bukan selalu salah. Ia berguna untuk sinkronisasi workspace. Masalah muncul ketika mount tidak dibatasi, tidak di-audit, atau dipakai sebagai pengganti desain artifact/copy yang lebih aman.

2. Fallback PATH atau shell init dari host

Runner sering mengeksekusi command lewat shell seperti sh -lc atau bash -lc. Jika environment tidak dibersihkan, shell dapat memuat profil atau membawa nilai PATH, HOME, dan variabel lain dari host. Akibatnya binary dari host lebih diprioritaskan dibanding binary di guest.

Gejala khasnya adalah command berjalan sukses, tetapi versi tool tidak cocok dengan image sandbox. Ini berbahaya karena memberi ilusi bahwa sandbox sehat padahal job sebenarnya tergantung pada host.

3. Socket atau daemon host ikut terpasang

Kasus yang sering luput adalah bind ke socket seperti /var/run/docker.sock, SSH_AUTH_SOCK, atau socket build cache. Dari sudut pandang debugging, ini menyebabkan job bisa berinteraksi dengan service host. Dari sudut pandang keamanan, ini bisa menjadi eskalasi hak akses yang serius.

Jika agen coding di dalam VM bisa memakai Docker daemon host, maka batas isolasi praktis runtuh: job bisa membuat container baru di host, mengakses filesystem melalui mount, atau membaca image dan layer cache yang bukan bagian dari sandbox.

4. Permission dan ownership yang memaksa fallback aneh

Permission yang tidak konsisten antara host dan guest sering mendorong developer menambahkan workaround seperti menjalankan proses sebagai user host, mewariskan HOME, atau memakai shared temp directory supaya build tidak gagal. Workaround ini sering menyelesaikan gejala jangka pendek, tetapi justru memperlebar kebocoran boundary.

UID/GID mismatch juga dapat menyebabkan file cache ditulis ke lokasi alternatif yang ternyata berada di host.

5. Cache, temp dir, atau artifact bersama

Cache package manager, build cache, atau directory sementara yang dibagi lintas job adalah sumber kebocoran state yang sangat umum. Walaupun ini tidak selalu berarti “eksekusi di host”, efeknya tetap sama: hasil job tidak lagi murni berasal dari lingkungan disposable.

Contoh yang sering terjadi:

  • /tmp atau /var/tmp di-mount dari host.
  • Directory cache global dibagi untuk semua sandbox.
  • Workspace cleanup hanya menghapus source, tetapi tidak membersihkan hidden state seperti lockfile cache atau credential cache.

Dampak keamanan dan konsistensi debugging

Kebocoran ini bukan sekadar masalah kerapihan infrastruktur.

Dampak pada keamanan

  • Kredensial host bisa terbaca, langsung atau tidak langsung, melalui file config, token cache, atau socket agent.
  • Privilege escalation lewat daemon host, terutama jika Docker socket atau service setara dapat diakses.
  • Boundary audit menjadi tidak valid, karena job yang dianggap terisolasi sebenarnya masih membawa trust dari mesin developer.

Dampak pada debugging dan kualitas sistem

  • Bug sulit direproduksi karena hanya muncul di laptop tertentu.
  • Build menjadi nondeterministic; job sukses karena dependency “kebetulan” tersedia di host.
  • Hasil test menipu; integration test tampak hijau tetapi sebenarnya memakai service lokal host.
  • Pembersihan state gagal; sandbox disposable tidak lagi benar-benar disposable.

Jika tujuan Anda adalah agen coding, job runner, atau executor yang dapat dipercaya, maka konsistensi boundary sama pentingnya dengan keberhasilan eksekusi.

Perbaikan bertahap: dari verifikasi hingga hardening

Memperbaiki kebocoran boundary sebaiknya dilakukan bertahap. Jangan mulai dari optimasi. Mulailah dari memastikan batas eksekusi benar, lalu baru mengembalikan fitur seperti cache atau sinkronisasi workspace dengan kontrol yang jelas.

1. Verifikasi boundary secara eksplisit di runner

Tambahkan preflight check sebelum job utama berjalan. Tujuannya adalah memvalidasi bahwa runtime memang berada di lingkungan yang diharapkan.

# contoh pemeriksaan generik
EXPECTED_ROOT=/work
EXPECTED_HOSTNAME_PREFIX=sandbox-

pwd
hostname
id
printenv | sort
mount | grep -E '/work|/tmp|/var/tmp'

# validasi sederhana
case "$(pwd)" in
  ${EXPECTED_ROOT}/*) : ;;
  *) echo "workspace di luar boundary"; exit 1 ;;
esac

Di level service, simpan hasil preflight ini ke log terstruktur. Saat insiden terjadi, Anda punya bukti awal tentang identitas runtime, mount, dan env efektif.

2. Bersihkan environment, jangan mewariskan env host mentah

Jalankan job dengan allowlist variabel environment, bukan denylist. Secara umum hanya teruskan variabel yang memang diperlukan oleh job.

# ilustrasi konsep, bukan perintah framework tertentu
env -i \
  PATH=/usr/bin:/bin \
  HOME=/home/runner \
  WORKDIR=/work/task-1842 \
  /bin/sh -lc 'cd "$WORKDIR" && ./run-job.sh'

Poin pentingnya:

  • Set PATH eksplisit.
  • Set HOME ke direktori guest yang bersih.
  • Jangan teruskan SSH_AUTH_SOCK, DOCKER_HOST, token cloud, atau env desktop host kecuali benar-benar diperlukan dan dibatasi.

3. Minimalkan mount dan buat daftar mount yang sah

Lebih aman menyalin source code ke workspace guest daripada memount seluruh direktori kerja host. Jika bind mount diperlukan demi performa atau DX, batasi hanya ke path yang dibutuhkan dan tandai read-only bila memungkinkan.

Praktik yang lebih aman:

  • Mount hanya workspace per-job, bukan home directory.
  • Gunakan path absolut yang tervalidasi.
  • Tolak symlink yang keluar dari root workspace.
  • Pisahkan volume cache yang memang disetujui dari filesystem host umum.

4. Putus akses ke socket host

Audit seluruh bind mount dan environment yang berhubungan dengan daemon atau agent host. Jika tidak ada kebutuhan yang jelas, jangan expose socket sama sekali.

# contoh audit generik di host/launcher
mount | grep sock
printenv | grep -E 'SSH_AUTH_SOCK|DOCKER_HOST'

Bila workflow memang membutuhkan Docker atau service lain, lebih aman menyediakan service tersebut di dalam boundary sandbox sendiri, atau melalui API perantara yang membatasi operasi.

5. Isolasi cache dan temp dir

Cache tidak harus dihapus total, tetapi harus scoped. Pisahkan cache per image, per proyek, atau per trust boundary. Hindari memakai /tmp host sebagai tempat kerja default untuk semua sandbox.

Jika ingin mempertahankan performa, buat volume cache yang eksplisit dan terdokumentasi, lalu pastikan:

  • ownership konsisten dengan user di guest,
  • isi cache tidak mengandung credential,
  • cache tidak dipakai sebagai jalur fallback untuk source of truth job.

6. Validasi runtime di dalam job, bukan hanya di launcher

Launcher bisa saja benar, tetapi shell script internal job dapat mengubah perilaku lewat source profile atau binary fallback. Karena itu, tambahkan pemeriksaan di level job:

echo "runtime check"
command -v python3 || true
command -v git || true
printf 'HOME=%s\n' "$HOME"
printf 'PATH=%s\n' "$PATH"
stat -c '%u:%g %n' . /tmp 2>/dev/null || true

Ini membantu membedakan bug pada orchestration dari bug pada script eksekusi.

Checklist pencegahan untuk job runner sandboxed

Gunakan checklist berikut sebelum menganggap sistem Anda benar-benar terisolasi:

  1. Workspace root jelas dan semua path tervalidasi terhadap root tersebut.
  2. Environment berbasis allowlist, bukan warisan env host penuh.
  3. PATH dan HOME diset eksplisit ke lokasi dalam guest.
  4. Tidak ada bind mount ke home host kecuali sangat diperlukan dan sudah diaudit.
  5. Tidak ada akses ke socket host seperti Docker atau SSH agent tanpa kontrol ketat.
  6. Cache dan temp dir terisolasi, tidak berbagi state tak terdokumentasi.
  7. UID/GID konsisten agar tidak memicu workaround permission yang berbahaya.
  8. Preflight check dicatat pada setiap job untuk membantu forensik.
  9. Teardown diverifikasi; state disposable benar-benar hilang setelah job selesai.
  10. Uji reproduksi lintas mesin; hasil di laptop developer dan CI harus konsisten.

Kesalahan umum saat memperbaiki bug ini

  • Hanya memperbaiki satu symptom, misalnya mengubah PATH, padahal mount host masih bocor.
  • Menambah permission untuk “melancarkan build”, yang justru membuka akses lebih luas ke host.
  • Mengandalkan log aplikasi saja tanpa menginspeksi mount, env, user, dan socket.
  • Menyamakan container dengan isolasi penuh. Tanpa konfigurasi yang ketat, container tetap bisa berbagi banyak state dengan host.
  • Tidak memisahkan kebutuhan performa dan keamanan. Cache cepat berguna, tetapi harus tetap berada dalam boundary yang jelas.

Penutup

Kasus job runner bocor dari sandbox ke host hampir selalu berawal dari kompromi kecil: mount yang terlalu luas, env yang diwariskan tanpa filter, socket yang ikut terbuka, atau cache bersama yang dianggap aman. Dalam jangka pendek, semua itu membuat sistem terasa praktis. Dalam jangka panjang, hasil debugging menjadi tidak dapat dipercaya dan risiko keamanan meningkat.

Untuk debugging backend pada arsitektur executor terisolasi, prinsip utamanya sederhana: jangan percaya asumsi boundary; buktikan boundary lewat log, preflight check, dan audit mount/env secara eksplisit. Setelah itu, lakukan hardening bertahap sampai job benar-benar hanya melihat apa yang seharusnya tersedia di sandbox disposable.