systemd servis sandboxing, systemd unit dosyalarına eklenen sıkılaştırma direktifleri (ProtectSystem, NoNewPrivileges, CapabilityBoundingSet, SystemCallFilter gibi) aracılığıyla her servisin sadece gerçekten ihtiyaç duyduğu kernel arayüzlerine, dosyalara ve syscall'lara erişmesini sağlayan bir izolasyon katmanıdır. Bu direktifler; Linux namespace'leri, seccomp-bpf ve capability alt sistemlerini kullanarak, ayrı bir container runtime kurmadan servis başına compromise yüzeyini önemli ölçüde daraltır. Bu rehberde, geçen yıl bir müşteri sunucusunda nginx'i 9.6'dan 2.2'ye çekerken kullandığım adımları paylaşacağım: sandbox skorunu systemd-analyze security ile ölçmek, direktifleri doğru sırada uygulamak ve yaygın regresyonları teşhis etmek.
systemd-analyze security, her servise 0.0 (sıkı) ile 10.0 (açık) arasında bir "exposure level" verir; İnternet'e açık servislerde hedef 3.0 altıdır.
Dosya sistemi sıkılaştırması için ProtectSystem=strict, ProtectHome=yes ve PrivateTmp=yes en yüksek "kazanç/emek" oranına sahip direktiflerdir.
NoNewPrivileges=yes, setuid binary'lerden gelen ayrıcalık yükseltmesini kernel seviyesinde bloklar ve seccomp filter'larının etkinleşmesi için ön koşuldur.
CapabilityBoundingSet, tam root yerine yalnızca CAP_NET_BIND_SERVICE gibi hedeflenmiş yetenekleri servis işlemlerine bırakır.
DynamicUser=yes, servis her başladığında geçici UID/GID tahsis eder; kalıcı kullanıcı yönetimini ortadan kaldırır ve yatay hareket olasılığını düşürür.
systemd sandboxing nedir ve neden gerekli?
systemd sandboxing, PID 1 seviyesindeki servis yöneticisinin, unit dosyasında beyan edilen direktifleri kullanarak her servis işlemini Linux çekirdeğinin kendi izolasyon primitifleri (namespace'ler, capabilities, seccomp-bpf ve LSM'ler) ile sarmalamasıdır. Tarihsel olarak Linux servisleri ya root ya da adanmış bir kullanıcı olarak açık bir kernel API yüzeyi üzerinden çalışırdı; bir zafiyet exploit edildiğinde saldırgan hemen tüm sistem çağrılarına, yazılabilir dosya sistemine ve tam capability set'ine erişebilirdi.
2024'te systemd 256 ile birlikte ProtectHostname=, ProtectClock=, ProtectKernelLogs=, ProcSubset= gibi geniş bir direktif seti stabil hale geldi. Bunların hepsi, ayrı bir container runtime (Docker, Podman) kurmaya gerek kalmadan servis başına saldırı yüzeyini daraltır. Kernel açısından bakınca, aslında CLONE_NEWNS, CLONE_NEWUSER, CLONE_NEWNET flag'leri, prctl(PR_SET_NO_NEW_PRIVS) ve seccomp(SECCOMP_SET_MODE_FILTER) çağrıları soyutlanmıştır. Yani sandbox altyapısını yeniden yazmıyorsunuz, kernel'in sunduklarını beyan yoluyla kullanıyorsunuz.
Bir servisi sandbox içine almak; bir CVE'nin (örneğin CVE-2024-6387 "regreSSHion" tarzı bir pre-auth RCE) exploit edildiği anda saldırganın yatay hareketini, veri sızdırmasını ve ayrıcalık yükseltmesini fiziksel olarak sınırlar. Bu bir tespit değil, önleme katmanıdır; IDS ve sandboxing birbirinin yerine değil birlikte çalışır. systemd.exec(5) man sayfası her direktifin hangi systemd sürümünde eklendiğini belgeler; distronuzun taşıdığı sürümü systemctl --version ile doğrulayın.
systemd-analyze security ile temel çizgi ölçümü
Sıkılaştırmaya başlamadan önce ölçün. systemd-analyze security komutu; servis başına 0.0 (tam sandbox) ile 10.0 (sandbox yok) arası bir "exposure level" verir, her direktifin katkısını tablo halinde gösterir ve neyin eksik olduğunu tam olarak söyler.
# Tüm çalışan servisleri özet skorlarıyla listele
systemd-analyze security
# Belirli bir servisin detaylı raporu
systemd-analyze security sshd.service
# Sistem geneli JSON çıktısı (raporlama ve regresyon takibi için)
systemd-analyze security --json=pretty > /var/log/systemd-security-baseline.json
Çıktıda her direktifin bir "OK" veya "exposed" işareti ve skora katkısı vardır. Örneğin ProtectHome= ayarlanmamışsa +0.2, SystemCallFilter= tanımsızsa +0.3 katkı gelir; birkaç orta ağırlıklı direktif, bir servisi 9.6 gibi neredeyse tamamen açık bir skora çıkarabilir. Hedef, kritik servisleri (nginx, sshd, postgresql) 3.0 altına, veri-hassas olmayanları 5.0 altına indirmektir.
0.0'a çekmek çoğu zaman gerçekçi değildir çünkü servis fonksiyonel olarak belirli capability'lere ihtiyaç duyar. Skoru mutlak bir hedef değil, deployment'lar arası regresyonu yakalayan bir baseline olarak kullanın. Ben genelde CI'da 0.3 puanın üstünde bir sapmayı hata olarak işaretliyorum; bu, paket güncellemelerinin gizlice sıkılaştırmayı gevşetmesini erken yakalıyor.
Dosya sistemi izolasyonu: ProtectSystem ve ReadOnlyPaths
Bir servis exploit edildiğinde en kolay ilk hedefi dosya sistemidir: /etc/passwd'yi okumak, /root/.ssh'a yazmak, /usr/bin'e web shell koymak. Dosya sistemi direktifleri bunu bind-mount ve namespace tabanlı bir sanal görünüm ile bloklar; servis exploit edilse bile fiziksel yolları göremez.
ProtectSystem ve ProtectHome
ProtectSystem=strict, tüm dosya sistemini (birkaç istisna hariç) servis için salt-okunur bir bind-mount olarak sunar; ProtectHome=yes ise /home, /root ve /run/user'i tamamen erişilmez yapar (boş bir dizin olarak görünürler). Bu ikisi birlikte, çoğu servisi bir dosya sistemi karantinasına alır.
[Service]
# Kök dosya sistemi salt-okunur, /var da dahil
ProtectSystem=strict
# /home, /root ve /run/user erişilmez (boş)
ProtectHome=yes
# Servisin yazma ihtiyacı olan yolları açıkça beyan et
ReadWritePaths=/var/lib/myapp /var/log/myapp
StateDirectory=myapp
LogsDirectory=myapp
StateDirectory=, LogsDirectory= ve CacheDirectory= kullanmak, elle ReadWritePaths= listelemekten daha güvenlidir çünkü systemd; izinleri, mülkiyeti ve DynamicUser ile birlikte kullanıldığında UID'yi otomatik yönetir. Yolu elle beyan ederseniz mount noktası kurulur ama mülkiyet size aittir (yani chown ve permission bug'larını siz yakalayacaksınız).
PrivateTmp, NoExecPaths ve InaccessiblePaths
PrivateTmp=yes, servise özel bir /tmp ve /var/tmp mount namespace sunar. Servis exploit edilse bile başka servislerin geçici dosyalarını göremez veya sembolik link saldırısı (TOCTOU) yapamaz. InaccessiblePaths= ile açıkça hassas yolları maskeleyebilirsiniz:
NoExecPaths= ve ExecPaths= (systemd 254+), belirtilen yollarda mmap(PROT_EXEC)'i engelleyerek saldırganın /tmp'ye indirdiği bir dropper'ı çalıştırmasını önler. Bu, Linux çekirdek güvenlik sıkılaştırma rehberindeki W^X (write XOR execute) prensibinin servis seviyesindeki karşılığıdır.
Ayrıcalıklar, capabilities ve NoNewPrivileges
Root olarak çalışan bir servisin gerçekten root'un tüm yetkilerine ihtiyacı olduğu neredeyse hiç yoktur. Kernel, root ayrıcalıklarını 40'tan fazla ayrı capability'ye böler (ayrıntı için capabilities(7) man sayfası); servisinize sadece gerçekten kullandıklarını verin ve gerisini bounding set üzerinden düşürün.
CapabilityBoundingSet ve AmbientCapabilities
[Service]
# Servisin ihtiyacı olan tek capability: 80/443 portunu bind etmek
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
# Boş bırakırsanız (CapabilityBoundingSet=), root bile hiçbir yetkiye sahip olmaz
User=www-data
CapabilityBoundingSet= boş bırakılırsa, User=root olsa bile işlem CAP_CHOWN, CAP_SYS_ADMIN, CAP_DAC_OVERRIDE gibi hiçbir capability'yi kullanamaz. Bu, RCE elde eden bir saldırgan için root olmayı büyük ölçüde anlamsız hale getirir çünkü root'un tanımı zaten capability yığınıdır; yığın boşsa "root" sadece bir UID etiketidir.
NoNewPrivileges ve SUID etkisizleştirme
NoNewPrivileges=yes, kernel'in PR_SET_NO_NEW_PRIVS bit'ini set eder. Bu bit set edildikten sonra çocuk işlemler execve() üzerinden SUID/SGID binary'lerle ayrıcalık kazanamaz. Yani sudo, su, ping, mount hepsi normal kullanıcı olarak çalışır. Seccomp filter'larının etkin olabilmesi için de bu bit'in set olması gerekir (aksi halde kernel seccomp() çağrısı için CAP_SYS_ADMIN ister), dolayısıyla sandboxing zincirinin temelidir.
RestrictSUIDSGID=yes, servisin dosya sistemine SUID/SGID bit'iyle dosya yazmasını engeller. RemoveIPC=yes ise servis durduğunda System V ve POSIX IPC objelerini temizler, bir yeniden başlatma sonrası bellekte kalan artıklar üzerinden yapılacak bir saldırıyı önler.
Seccomp ile sistem çağrısı filtreleme
Kernel'in ~450 sistem çağrısının büyük çoğunluğuna sıradan bir servisin ihtiyacı yoktur. SystemCallFilter= direktifi seccomp-bpf kullanarak servise izin verilen syscall'ları beyaz listeye alır; gerisini kernel EPERM döner veya SIGSYS ile öldürür. Bu, saldırganın exploit sonrası "kernel'e derin dokunma" alanını dramatik biçimde daraltır.
[Service]
# systemd'nin hazır "system-service" seti çoğu daemon için yeterlidir
SystemCallFilter=@system-service
# Tehlikeli sınıfları açıkça reddet
SystemCallFilter=~@mount @debug @cpu-emulation @obsolete @privileged @reboot @swap @raw-io
# 64-bit ABI dışına çıkışı engelle (32-bit compat üzerinden bypass'ları önler)
SystemCallArchitectures=native
LockPersonality=yes
@system-service, systemd tarafından bakımı yapılan bir grup adıdır ve normal daemon operasyonu için gereken syscall'ları içerir. Grubun tam üyeliğini systemd-analyze syscall-filter @system-service ile inceleyebilirsiniz. Grup adının başına ~ koymak reddetme (deny) anlamına gelir; yukarıdaki örnek önce beyaz liste açar, sonra bazı sınıfları açıkça kara listeye alır. Grupların altında yatan syscall listesini kernel'in seccomp(2) man sayfası ve syscall(2) ile karşılaştırarak doğrulayabilirsiniz.
Ağ ve kullanıcı namespace izolasyonu
Ağ sandboxing'i iki eksene bölünür: hangi adres ailesi kullanılabilir (RestrictAddressFamilies=) ve ağ görünürlüğü var mı (PrivateNetwork=, IPAddressAllow=). Bu ikisini birleştirmek, uygulama katmanı zafiyetlerini ağ katmanında yalıtır.
# Sadece UNIX socket ve IPv4/IPv6; AF_PACKET, AF_NETLINK bloklu
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# Localhost hariç tüm ağ trafiğini reddet (systemd 235+, cgroup v2 gerektirir)
IPAddressDeny=any
IPAddressAllow=localhost
# Yerel-only bir batch script için tamamen izole ağ namespace
PrivateNetwork=yes
RestrictAddressFamilies=; AF_PACKET (raw ethernet) ve AF_NETLINK (kernel konfigürasyon kanalı) gibi tehlikeli aileleri devre dışı bırakır. Bir saldırganın tcpdump tarzı sniff etmesini ya da routing tablolarını düzenlemesini bu şekilde engellersiniz. nftables güvenlik duvarı katmanıyla birleştiğinde, ağ tarafında derinlemesine savunma elde edersiniz: nftables paket seviyesinde, systemd ise syscall seviyesinde filtreler.
PrivateUsers=yes, servise ayrı bir user namespace verir; içerideki root, hostte unprivileged UID'ye map edilir. Docker/Podman'ın rootless mode'unun sağladığı garantinin systemd servisi için karşılığıdır. Kernel'in user namespace exploit yüzeyi hakkında endişeniz varsa (CVE-2022-0185, CVE-2023-32233 gibi) ProtectKernelModules=yes ve ProtectKernelTunables=yes direktifleriyle katmanlayın.
DynamicUser ile efemer servis kullanıcıları
Kalıcı sistem kullanıcıları (nginx, postgres, redis) uzun ömürlü UID'lere sahiptir; bir servis compromise olduğunda saldırgan aynı UID'ye ait diğer dosyaları veya IPC objelerini görebilir. DynamicUser=yes, servis her başladığında 61184–65519 aralığında geçici bir UID/GID tahsis eder; servis durunca UID serbest bırakılır ve bir sonraki başlatmada farklı bir değer atanır.
[Service]
DynamicUser=yes
StateDirectory=myapp
CacheDirectory=myapp
LogsDirectory=myapp
# DynamicUser ile birlikte otomatik olarak set edilir:
# ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes,
# RemoveIPC=yes, NoNewPrivileges=yes, RestrictSUIDSGID=yes
DynamicUser=yes, aynı zamanda bir grup güvenlik varsayılanını implicit olarak aktifleştirir; tek bir direktifle güvenlik skorunu 2-3 puan aşağı çekebilir ve çoğu stateless daemon için ideal başlangıç noktasıdır. Ancak dosya sahipliğine dokunan servisler için uygun değildir (örneğin bir DB restore script'i veya dış disk üzerindeki dosyalarla çalışan job'lar). StateDirectory otomatik chown edilse de rastgele bir path'e önceden var olan izinleriyle dokunulamaz. Böyle servislerde sabit bir User= ve Group= tanımlayıp diğer direktifleri elle ekleyin.
Örnek: nginx için sıkılaştırılmış drop-in
Distro paketlerinden gelen unit dosyalarını doğrudan düzenlemek yerine systemctl edit nginx.service ile drop-in oluşturun. Bu, paket güncellemelerinin sıkılaştırmayı ezmesini önler ve /etc/systemd/system/nginx.service.d/ altında yaşar:
Tipik olarak bu konfigürasyon nginx'in exposure skorunu 9.6'dan 2.2 civarına indirir. sshd için benzer bir drop-in için SSH güvenlik sıkılaştırma rehberindeki servis izolasyon bölümüne bakın; oradaki drop-in aynı prensipleri sshd'nin özel capability ihtiyaçlarına (özellikle privilege separation için CAP_SYS_CHROOT ve CAP_SYS_RESOURCE) uyarlar.
Yaygın hatalar ve nasıl teşhis edilir
Sıkılaştırmayı test etmeden production'a itmek en yaygın hatadır. seccomp veya capability bir syscall bloklayınca servis çoğu zaman anlaşılmaz hata mesajlarıyla çöker. Sorunun sandbox'tan kaynaklandığını anlamak için birkaç aracı bilmek gerekir. (Ben ilk aylarımda buradaki her tuzağa en az bir kez düşmüştüm.)
SIGSYS ve syscall reddedişleri
# seccomp bir syscall reddettiyse journalctl'de SIGSYS görünür
journalctl -u nginx.service | grep -E "SIGSYS|seccomp"
# Hangi syscall'ın reddedildiğini görmek için audit'i aç
auditctl -a always,exit -F arch=b64 -S all -F key=seccomp
ausearch -k seccomp -ts recent | grep -i denied
Alternatif olarak strace -c -f -p <pid> ile hangi syscall'ın EPERM döndüğünü görebilirsiniz. Reddedilen syscall'ı SystemCallFilter= whitelist'ine ekleyin veya daha spesifik bir grup seçin; bir syscall için tüm @privileged grubunu açmayın.
ProtectSystem=strict altında yazma hatası
Servis "Read-only file system" hatasıyla çöker. Çözüm: yazma ihtiyacı olan yolları ReadWritePaths=, StateDirectory=, LogsDirectory= veya CacheDirectory= ile açıkça listeleyin. strace -e openat -f ile hangi path'e yazmaya çalıştığını görebilirsiniz. Çoğu zaman servis bir cache dizini veya PID dosyası açmaya çalışır ve yasal olarak buna hakkı vardır.
Namespace ve capability çakışmaları
PrivateNetwork=yes ile başlatılan bir servis dış API'ye ulaşamaz; metrikleri Prometheus'a push eden bir ajan sessizce susar. RestrictNamespaces=yes, servisin unshare() ile alt-namespace kurmasını engeller; container runtime script'leri veya CI runner'ları bozar. Fail2ban gibi servisler CAP_NET_ADMIN ister; onu bounding set'e eklemeden çalıştıramazsınız.
AppArmor/SELinux ile katman çakışmaları
systemd sandboxing LSM'lerle katmanlı çalışır, ama bazen çakışırlar. AppArmor profile'ı bir yolu okumaya izin verirken systemd'nin InaccessiblePaths= maskesi engelleyebilir; iki katman da bağımsız olarak reddeder. dmesg | grep -E "AVC|apparmor|selinux" ile LSM denied loglarını takip edin ve iki katmanı birlikte düşünün: LSM'ler kimlik/tip bazlı, systemd sandboxing ise namespace/mount bazlı çalışır.
Sıkça Sorulan Sorular
systemd sandboxing, Docker veya Podman container'larının yerini alır mı?
Hayır, tamamen değil. systemd sandboxing servis başına kernel-seviye izolasyon sağlar ve bare-metal servisler için container overhead'i olmadan sağlam bir sıkılaştırma katmanıdır. Ancak container imajları, bağımlılık paketleme ve orchestration sağlamaz. Kubernetes veya mikroservis mimarisi kullanıyorsanız container'lara ihtiyacınız devam eder; hostta çalışan systemd servisleri (nginx, ssh, postgres) için sandboxing en pratik savunmadır.
NoNewPrivileges=yes hangi durumlarda servisi bozar?
Setuid/setgid binary'lere veya file capabilities kullanan yürütülebilirlere bağlı servislerde. Örnek: PHP-FPM'in bazı eski konfigürasyonları setuid sudo'ya ihtiyaç duyar; ping, traceroute gibi araçlar file capability'siz çalışmaz. Çözüm: gerçekten gerekli capability'yi AmbientCapabilities= ile açıkça verin ve setuid bağımlılığını bırakın.
systemd-analyze security skorunu ne kadar düşürmeliyim?
İnternet'e açık servisler için (nginx, sshd) 3.0 altı gerçekçi bir hedeftir. Yerel batch job'lar için 1.0-2.0 arası mümkündür. 0.0'a çekmek çoğu servis için mümkün değildir çünkü User=root veya PrivateNetwork=no gibi zorunlu ayarlar skoru yukarı çeker. Skoru mutlak bir hedef olarak değil, deployment'lar arası regresyonu yakalayan bir baseline olarak kullanın.
SystemCallFilter=@system-service her servis için güvenli mi?
Çoğu daemon için evet, ama container runtime'lar, eBPF ajanları ve virtualization tool'ları (qemu, libvirt, containerd) bu grubun dışında syscall'lara ihtiyaç duyar. systemd-nspawn veya kubelet gibi servisleri asla @system-service ile kısıtlamayın; clone() flag'leri, mount(), pivot_root() gibi syscall'lar bloklanır ve servis başlamaz. Bu tür servisler için özelleştirilmiş, daha gevşek bir syscall set'i hazırlayın.
Sandboxing direktifleri hangi systemd sürümünden itibaren mevcut?
Temel direktifler (ProtectSystem, PrivateTmp, CapabilityBoundingSet) systemd 231+'da (2016) stabildir. IPAddressDeny systemd 235 (2017), ProtectProc/ProcSubset systemd 247 (2020), NoExecPaths/ExecPaths systemd 254 (2023) ile geldi. RHEL 9 systemd 252, Debian 12 systemd 252, Ubuntu 24.04 systemd 255 taşır. Üçü de bu rehberdeki direktiflerin büyük çoğunluğunu destekler. Kesin uyumluluk için systemd.exec(5) man sayfasındaki "Added in version" notlarına bakın.
Cosign ve Sigstore ile OCI container imajlarını keyless olarak imzalayın; Fulcio, Rekor, in-toto attestation ve Kubernetes'te Kyverno tabanlı imza zorunluluğunu 2026 sürümleriyle üretime çıkarın.
Falco ile Kubernetes'te konteyner içi tehditleri gerçek zamanlı yakalayın: modern eBPF sürücüsü, Helm kurulumu, özel kural yazımı, Falcosidekick ile Slack/PagerDuty uyarı yönlendirme ve MITRE ATT&CK for Containers kapsamı için 2026 rehberi.