Belajar Seccomp - Seccomp untuk Aplikasi & Daemon
Episode 11 of 23

Belajar Seccomp - Seccomp untuk Aplikasi & Daemon

Memasang filter seccomp langsung di dalam program memakai libseccomp, python-seccomp, dan Go, dengan pemangkasan syscall per fitur. Termasuk pembatasan keluarga socket untuk daemon jaringan serta blokir syscall berbahaya seperti unshare, mount, dan ptrace.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Pendahuluan

Di episode 10 kalian memasang seccomp dari luar: pod, service unit, container. Semua pendekatan itu punya satu kelemahan: mereka mengandalkan orang yang menjalankan aplikasi. Bagaimana jika aplikasi bisa mengunci dirinya sendiri — membatasi syscall yang boleh dipanggilnya tanpa menunggu admin memasang profil?

Episode 11 ini membahas self-confinement: aplikasi memasang filter seccomp dari dalam kodenya sendiri. Kalian akan melihat libseccomp sebagai fondasinya, python-seccomp dan Go sebagai cara praktis, serta pola-pola nyata: memangkas syscall per fitur, membatasi keluarga socket untuk daemon jaringan, dan memblokir syscall berbahaya.

Mengapa Aplikasi Mengunci Dirinya Sendiri

Bayangkan pilot yang mengunci pintu kokpit setelah lepas landas. Sebelum lepas landas, kru butuh akses bebas — membuka file konfigurasi, menghubungkan database, membuka port. Setelah itu, kebutuhan itu hilang, dan setiap pintu yang tersisa hanya memperbesar risiko. Prinsip yang sama berlaku untuk aplikasi: lakukan inisialisasi dulu, lalu kunci.

Pola yang benar selalu berurutan:

  1. Jalankan inisialisasi: baca konfigurasi, buka koneksi, bind port.
  2. Turunkan privilege (setuid/setgid) bila diperlukan.
  3. Pasang filter seccomp sebagai langkah terakhir.

Setelah langkah 3, syscall yang tidak terdaftar ditolak — bahkan jika aplikasi berhasil dieksploitasi, setiap langkah berikutnya (spawn shell, baca file, buka socket) sudah dipangkas. Ini adalah defense in depth di garis terdepan: dekat sekali dengan kode yang rentan.

Note

Self-confinement bukan pengganti profil eksternal dari episode 8–10, tapi pelengkapnya. Profil container memangkas dari luar, filter di dalam aplikasi memangkas dari dalam. Dua lapisan yang memeriksa dimensi yang sama dari arah berbeda — mempersulit penyerang dua kali lipat.

libseccomp: Fondasi Semua Bahasa

libseccomp adalah library C yang membungkus pembuatan filter — seccomp_init membuat konteks, seccomp_rule_add menambahkan aturan, seccomp_load memuat ke kernel. Semua binding bahasa lain hanyalah pembungkus dari tiga fungsi ini:

libseccomp.c — init, rule, load
#include <seccomp.h>
#include <errno.h>
 
int main(void) {
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
    if (ctx == NULL) return 1;
 
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
 
    seccomp_load(ctx);
    seccomp_release(ctx);
    return 0;
}

Bacalah alurnya: init menetapkan action default (ERRNO(EPERM) untuk semua syscall), rule_add mengizinkan yang spesifik, dan load mengirim filter ke kernel. Setelah load, aturan tidak bisa diubah lagi — kunci sudah terkunci.

Python: python-seccomp

Di Python, binding resminya adalah python-seccomp, diinstall dengan pip install python-seccomp. Konfigurasi yang sama jadi jauh lebih ringkas:

sandbox.py — filter dasar python-seccomp
import seccomp
 
f = seccomp.SyscallFilter(defaction=seccomp.ERRNO(1))
for name in [
    "read", "write", "openat", "close", "mmap",
    "mprotect", "futex", "exit_group", "rt_sigaction",
]:
    f.add_rule(seccomp.ALLOW, name)
 
f.load()

Perhatikan detail yang sering menjebak: daftar syscall tidak boleh melupakan yang dipakai interpreter itu sendiri. Python runtime memanggil mmap, mprotect, futex, dan rt_sigaction untuk alokasi memori dan sinyal — filter yang terlalu kurus membuat interpreter mati di detik pertama. Selalu jalankan test suite setelah load().

Go: libseccomp-golang

Di Go, dua paket yang paling relevan: github.com/seccomp/libseccomp-golang untuk membangun filter langsung, dan github.com/containers/common/pkg/seccomp yang bisa memuat profil JSON gaya Docker ke dalam proses Go. Contoh yang pertama:

sandbox.go — filter dasar Go
package main
 
import (
	"log"
 
	seccomp "github.com/seccomp/libseccomp-golang"
)
 
func main() {
	filter, err := seccomp.NewFilter(seccomp.ActErrno.SetReturnCode(1))
	if err != nil {
		log.Fatal(err)
	}
	defer filter.Release()
 
	names := []string{"read", "write", "openat", "close",
		"mmap", "mprotect", "futex", "exit_group"}
	for _, name := range names {
		id, err := seccomp.GetSyscallFromName(name)
		if err != nil {
			log.Fatal(err)
		}
		if err := filter.AddRule(id, seccomp.ActAllow); err != nil {
			log.Fatal(err)
		}
	}
	if err := filter.Load(); err != nil {
		log.Fatal(err)
	}
}

Keunggulan GetSyscallFromName adalah portabilitas nama: kalian menulis nama syscall, bukan nomor, dan library menerjemahkannya sesuai arsitektur. Kode yang sama berjalan di x86_64 dan ARM64 tanpa diubah.

Membatasi Keluarga Socket di Daemon Jaringan

Daemon jaringan membutuhkan socket, bind, dan connect — tetapi tidak semua keluarga socket. Sebuah web server seharusnya tidak perlu AF_PACKET (raw packet) atau AF_NETLINK (komunikasi dengan kernel). Dengan argfilter, kalian bisa mengizinkan socket hanya untuk keluarga tertentu. Di Python:

daemon.py — socket hanya AF_UNIX dan AF_INET
import seccomp
 
f = seccomp.SyscallFilter(defaction=seccomp.ERRNO(1))
f.add_rule(seccomp.ALLOW, "socket", seccomp.Arg(0, seccomp.EQ, 1))  # AF_UNIX
f.add_rule(seccomp.ALLOW, "socket", seccomp.Arg(0, seccomp.EQ, 2))  # AF_INET
f.add_rule(seccomp.ALLOW, "bind")
f.add_rule(seccomp.ALLOW, "connect")
f.add_rule(seccomp.ALLOW, "accept4")
f.add_rule(seccomp.ALLOW, "getsockname")
f.load()

seccomp.Arg(0, seccomp.EQ, 1) berarti: argumen ke-0 dari socket harus sama dengan 1 (AF_UNIX). Kombinasi ini mengunci daemon agar hanya bisa berkomunikasi lewat socket Unix dan IPv4 — setiap upaya membuka AF_INET6, AF_PACKET, atau AF_NETLINK langsung ditolak, bahkan jika aplikasi sudah terkompromi. Ini bentuk kunci dari episode 5 yang diaplikasikan ke kehidupan nyata daemon.

Tip

Prinsipnya sama untuk semua syscall yang punya argumen pembatas: openat bisa dibatasi per path, clone bisa dibatasi per flag namespace, connect bisa dibatasi per port. Pangkas syscall per fitur, bukan per nama saja — semakin spesifik, semakin sempit celahnya.

Blokir Syscall Berbahaya

Ada kelas syscall yang hampir tidak pernah dibutuhkan daemon biasa, tapi sangat berharga bagi penyerang. Blokir secara eksplisit, bahkan ketika daemon berjalan tanpa privilege:

LinuxSyscall yang layak diblokir di daemon
unshare            # membuat namespace baru — jalur pelarian sandbox
mount              # mengubah filesystem host
ptrace             # men-debug atau menyuntik proses lain
bpf                # memanipulasi program BPF kernel
perf_event_open    # mengintip aktivitas sistem
keyctl             # mengelola keyring kernel
process_vm_readv   # membaca memori proses lain
process_vm_writev  # menulis ke memori proses lain

Mengapa memblokir syscall yang "butuh privilege" jika daemon tidak punya privilege? Karena defense in depth. Penyerang yang menemukan cara menaikkan privilege masih harus menghadapi lapisan berikutnya — dan filter ini memotongnya di level syscall, jauh sebelum privilege itu berguna. unshare khususnya penting: ia menjadi bahan baku eksploitasi container escape, dan memblokirnya dari dalam aplikasi memutus jalur itu sejak awal.

Warning

Memuat filter bukan alasan untuk berhenti menurunkan privilege. Seccomp membatasi aksi apa yang bisa dipanggil; capabilities dan drop UID membatasi dengan hak apa. Kombinasi keduanya — bukan pengganti satu sama lain — itulah yang membuat sandbox benar-benar solid.

Menyusun Filter per Fitur

Rahasia filter yang bertahan lama bukan di daftar panjang, tapi di pemetaan fitur ke syscall. Tanyakan per fitur: apa yang dibutuhkan untuk menjalankannya?

  • Worker batch tanpa jaringan — izinkan read/write file, mmap, exit_group; blokir socket, connect, execve.
  • Daemon yang hanya melayani TCP — izinkan socket (dibatasi AF_INET/AF_INET6), bind, accept, sendto, recvfrom; blokir ptrace, keyctl, unshare.
  • Aplikasi yang sudah selesai boot — setelah inisialisasi, blokir clone, execve, openat yang tidak dibutuhkan lagi.

Setiap fitur menghasilkan satu baris pemikiran, bukan satu baris kode. Hasilnya adalah filter yang bisa dijelaskan — dan filter yang bisa dijelaskan adalah filter yang bisa dirawat.

Penutup

Di episode 11 ini kalian telah memahami self-confinement: aplikasi memasang filter dari dalam kodenya dengan libseccomp sebagai fondasi, python-seccomp dan libseccomp-golang sebagai pembungkus yang lebih nyaman, serta pola pangkas-per-fitur yang membatasi syscall sesuai kebutuhan aktual. Kalian juga bisa membatasi keluarga socket daemon dengan argfilter dan memblokir syscall berbahaya seperti unshare, mount, ptrace, bpf, dan perf_event_open.

Kunci yang harus dibawa pulang:

  • Inisialisasi dulu, turunkan privilege, baru kunci filter.
  • Pangkas per fitur, bukan per nama syscall — spesifik lebih aman.
  • Seccomp dan privilege drop adalah pasangan, bukan pesaing.

Filter di dalam aplikasi menuntut kedisiplinan yang tinggi: aplikasi harus tahu persis apa yang dilakukannya. Bagaimana mekanisme seccomp berinteraksi dengan lapisan keamanan kernel lainnya — capabilities, namespaces, dan LSM? Di episode 12, kita masuk ke Seccomp & Capabilities Kombinasi: menyusun profil yang memangkas syscall sekaligus menajamkan capability set, agar pertahanan berlapis bekerja satu sama lain, bukan saling mengalahkan.

Belajar Seccomp - Seccomp untuk Aplikasi & Daemon | Belajar Seccomp