Server Rumahan & Self-Hosting

Sandboxing systemd untuk Server Rumahan — Memberi Setiap Layanan Radius Ledakan yang Lebih Kecil

Server rumahan jarang tetap sederhana untuk lama. Awalnya hanya satu web server, lalu database ikut masuk, kemudian tunnel, lalu agen monitoring, dan tanpa terasa satu mesin menjalankan selusin layanan sepanjang waktu. Pada setup Linux pada umumnya, setiap layanan itu tinggal di dalam sebuah unit file systemd — dan unit yang polos tanpa modifikasi memberi prosesnya pemandangan host yang sangat murah hati: seluruh filesystem, hampir semua capabilities, akses ke informasi proses pengguna lain. Ini memunculkan pertanyaan yang layak diajukan dengan jujur: kalau hanya satu dari layanan-layanan ini ternyata punya celah, seberapa jauh penyerang bisa menjelajah dari dalam proses tersebut?

systemd sudah bertahun-tahun menyertakan sekumpulan opsi sandboxing bawaan, dan semuanya tidak butuh software tambahan. Artikel ini menelusuri cara kerjanya, cara mengukur posisi layanan kita sendiri, sampai batas-batas jujur dari pendekatan ini.

Apa itu sandboxing systemd — dan apa bukan

Opsi-opsinya hidup di dalam unit file dan didokumentasikan di systemd.exec(5): direktif seperti ProtectSystem=, NoNewPrivileges=, atau SystemCallFilter=. Di balik layar mereka menggabungkan primitif kernel — mount namespace, filter seccomp, capability set, opsi mount procfs — tapi dari sudut pandang administrator, semuanya hanyalah baris-baris di file konfigurasi. Banyak di antaranya sudah ada sejak lama; ProtectSystem= misalnya ditambahkan di versi 214, menurut dokumentasi.

Dua hal yang bukan dirinya: ini bukan container runtime dan bukan mesin virtual. Tujuannya lebih sempit. Container mengisolasi seluruh lingkungan sebuah aplikasi; direktif sandboxing memangkas segala hal yang tidak dibutuhkan oleh satu layanan spesifik, sementara layanan itu tetap berjalan seperti unit biasa. Dalam kerangka itu, pendekatan ini paling tepat dipahami sebagai defense in depth — satu lapisan tambahan antara proses yang disusupi dan sisanya mesin.

Mulai dari mengukur, bukan menebak

Cek realitas tercepat adalah systemd-analyze security. Jalankan tanpa argumen dan ia meninjau setiap service, memberi masing-masing "exposure level" antara 0.0 dan 10.0. Dokumentasinya terbuka soal arti angka itu: nilainya adalah estimasi, angka tinggi tidak berarti service benar-benar rentan, dan penilaian hanya mencakup fitur sandboxing yang diketahui systemd sendiri.

Aku menjalankannya di server rumahan yang meng-host situs ini, yang berjalan di Debian 13 dengan systemd 257 (versi yang didokumentasikan di man page Debian). Mayoritas service keluar dengan nilai 9,0 sampai 9,6 berlabel UNSAFE: tunnel Cloudflare, code-server, cron, dnsmasq. Unit nginx masih menjalankan proses utamanya sebagai root, dengan NoNewPrivileges= kosong dan nyaris semua capability masih utuh.

Ada satu pengecualian yang menarik: MariaDB yang terpasang dari repository Debian ternyata sudah membawa User=mysql dan ProtectSystem=full dari si pembuat paket — walau tanpa NoNewPrivileges=yes. Jadi pre-hardening sebagian memang ada di paket distro, tapi bervariasi antar paket, dan tidak ada yang melarang kita mengetatannya lagi lewat override sendiri.

Keluarga opsi yang layak dikenal

Puluhan direktif tersedia, tapi mereka mengelompok ke tiga keluarga yang intuitif.

Pemandangan filesystem

ProtectSystem=true me-mount /usr dan direktori boot menjadi read-only; full menambahkan /etc; strict membuat seluruh hierarki read-only kecuali API filesystem. Dokumentasi merekomendasikan mengaktifkannya "for all long-running services". ProtectHome=yes menyembunyikan /home, /root, dan /run/user dari layanan sepenuhnya. PrivateTmp=yes memberi proses /tmp dan /var/tmp miliknya sendiri.

Dunia read-only memunculkan pertanyaan jelas: di mana layanan boleh menulis? Ada dua jawaban rapi. ReadWritePaths=/path melubangi jalur tertentu di setup strict, dan opsi direktori — StateDirectory=notes, RuntimeDirectory=notes — membuat direktori writable khusus di bawah /var/lib atau /run, dikelola systemd dan dilindungi dari masalah daur ulang UID.

Privilege

User=/Group= menjalankan proses sebagai akun biasa; DynamicUser=yes melangkah lebih jauh dengan mengalokasikan UID sekali pakai selama umur layanan, sekaligus otomatis mengaktifkan ProtectSystem=strict. Di atasnya ada NoNewPrivileges=yes, yang oleh dokumentasi disebut "the simplest and most effective way" untuk memastikan sebuah proses beserta anak-anaknya tidak pernah bisa naik privilege lewat binary setuid lagi.

CapabilityBoundingSet= memangkas kernel capability yang dimiliki proses — isi dengan nilai kosong untuk mencabut semuanya. Kalau sebuah layanan memang butuh satu capability, Arch Wiki mendokumentasikan pola umumnya: berikan persis capability itu, misalnya AmbientCapabilities=CAP_NET_BIND_SERVICE agar proses non-root boleh bind port 80 atau 443, sementara CapabilityBoundingSet=CAP_NET_BIND_SERVICE menjamin tidak ada yang lain. RestrictSUIDSGID=yes melarang pembuatan file setuid baru, dan ProtectProc=invisible menyembunyikan proses pengguna lain dari /proc.

Permukaan kernel dan syscall

Keluarga ini memotong jalur dari proses yang disusupi menuju kernel itu sendiri: ProtectKernelModules=yes memblokir pemuatan module kernel, ProtectKernelLogs=yes menutup ring buffer log kernel, ProtectClock=yes melarang perubahan jam, PrivateDevices=yes menghapus perangkat fisik dari /dev, dan RestrictNamespaces=true merampas kemampuan membuat namespace. RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 melarang keluarga socket eksotis seperti packet socket, dan SystemCallArchitectures=native memblokir panggilan ABI asing.

Direktif paling presisi di sini adalah SystemCallFilter=, sebuah allow-list syscall atau grup syscall terdefinisi. @system-service adalah grup kurasi yang mencakup kebutuhan daemon jaringan pada umumnya. Satu direktif pantas diberi label peringatan: MemoryDenyWriteExecute=yes melarang memori yang writable sekaligus executable dan menutup satu kelas exploit secara utuh, tapi dokumentasi secara eksplisit mencatat bahwa ia inkompatibel dengan program yang membuat kode saat runtime — termasuk engine JIT. Service Node.js dan Java kemungkinan menolak jalan ketika ini aktif.

Contoh nyata, diukur secara offline

Untuk melihat efek konkretnya, aku menulis unit sengaja polos untuk sebuah service demo kecil, lalu varian hardened-nya:

[Service]
Type=simple
ExecStart=/usr/bin/python3 -m http.server 8080
Restart=on-failure
[Service]
Type=simple
DynamicUser=yes
StateDirectory=notes
RuntimeDirectory=notes
ExecStart=/usr/bin/python3 -m http.server 8080
Restart=on-failure

# Filesystem view
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

# Privileges
NoNewPrivileges=true
CapabilityBoundingSet=
PrivateDevices=true
ProtectProc=invisible
RestrictSUIDSGID=true

# Kernel surface
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectKernelLogs=true
ProtectClock=true
ProtectControlGroups=true
LockPersonality=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallFilter=@system-service
SystemCallArchitectures=native

systemd-analyze security punya mode offline yang menganalisis unit file langsung dari disk tanpa memasangnya, sehingga eksperimen seperti ini aman. Di mesin ini, menjalankan systemd-analyze security --offline=true memberi skor unit polos 9,4 (UNSAFE) dan versi hardened 1,6 (OK) — tanpa satu pun warning direktif tak dikenal, artinya setiap baris ter-parse bersih di systemd 257. Angka itu estimasi, bukan bukti keamanan, dan uji offline tentu tidak bisa memberi tahu apakah aplikasi sungguhan akan mentolerir batasan-batasan ini; yang ia tunjukkan adalah betapa besar permukaan serangan yang dibiarkan terbuka oleh default.

Apa yang sering rusak

Sandboxing gagal dengan pola yang bisa diprediksi — kabar baik, karena prediktabilitas membuat debugging jadi jauh lebih mudah dilacak.

  • Runtime JIT dan MemoryDenyWriteExecute= memang tidak akur, seperti dijelaskan di atas. Biarkan mati untuk service berbasis V8 atau JVM.
  • Berbagi file lewat /tmp: begitu PrivateTmp=yes aktif, dua service tidak bisa lagi bertukar file lewat /tmp — dokumentasinya menyatakannya apa adanya.
  • Port rendah: service yang pindah dari root ke dynamic user kehilangan izin bind port di bawah 1024 kecuali kita memberinya CAP_NET_BIND_SERVICE via AmbientCapabilities=.
  • Layanan jaringan dan namespace: Arch Wiki mengingatkan bahwa web server jangan memakai PrivateNetwork=yes, karena biasanya memang butuh akses jaringan sungguhan.
  • Error samar: saat service yang sudah di-hardening mati misterius, journalctl -u NAMA.service -e adalah pintu pertama, dan Arch Wiki menyarankan menaikkan verbositas manajer dengan systemctl log-level debug kalau pesannya tetap tidak jelas.

Ritme praktis yang mengikuti dari sini: kencangkan satu keluarga opsi, restart, amati journal, ulangi. Bukan empat puluh direktif sekaligus.

Batas-batas yang perlu diingat

Risiko terbesar dari alat berskor adalah memperlakukan skornya sebagai janji. Dokumentasi systemd memperingatkan bahwa setting individual "can be circumvented — unless combined with others", dan analisis mengabaikan perlindungan yang dibuat service itu sendiri maupun jalur samping berprivilege: proses yang bisa bicara lewat D-Bus dapat meminta service yang lebih terlindungi melakukan sesuatu untuknya. Arch Wiki mengatakannya lebih blak-blakan: skornya "slightly misleading", karena hanya program hello-world yang bisa mendekati rating sempurna — aplikasi nyata selalu butuh pengecualian.

Ada juga kontra-argumen yang adil dari dunia container: kalau semua service sudah berjalan di Docker atau podman dengan isolasinya masing-masing, sandboxing di level unit menambah sedikit saja. Alasan itu berlaku untuk sebagian setup — tapi banyak workload server rumahan, termasuk beberapa di mesin tempat situs ini berjalan, masih dikirim sebagai system unit biasa, dan itulah yang paling diuntungkan.

Dan sandboxing tidak menggantikan fundamental. Patching, backup, software minimalis, dan autentikasi yang waras tetaplah dinding penopang; sandboxing membatasi seberapa jauh sebuah breach menyebar setelah sesuatu yang lain sudah salah.

Posisiku Setelah Ini

Yang menarik dari pendekatan ini adalah rasio biaya-manfaatnya: mekanisme penegakannya sudah bertahun-tahun diam-diam ada di setiap distribusi modern, tinggal menunggu beberapa baris konfigurasi. Pengukuranku sendiri menunjukkan hampir semua yang ada di server ini masih berjalan terbuka lebar, jadi kesimpulan yang jujur: ini pekerjaan yang belum selesai di sini, bukan kisah sukses yang sudah tamat. Langkah lanjutan yang masuk akal adalah drop-in override — systemctl edit nginx.service membuat overlay alih-alih menyentuh file vendor — diterapkan service demi service, dimulai dari yang terekspos internet. Apakah sebuah layanan sanggup mentolerir sandbox-nya pada akhirnya adalah pertanyaan empiris, dan satu-satunya cara menjawabnya adalah mencoba, mengamati journal, lalu menyesuaikan.

References