← blog · 10 Ekim 2026

systemd ile Servis Sertleştirme: ProtectSystem, DynamicUser, Sistem Çağrısı Filtresi ve Servisi Kıran Seçenekler

Konteynere geçmeden bir servisin saldırı yüzeyini birim dosyasından daraltmak: başlangıç şablonu, systemd-analyze security ile ölçmek ve 226/NAMESPACE, JIT, PrivateTmp, DynamicUser gibi servisi sessizce bozan tuzaklar.

Bir Linux sunucusunda çalışan servislerin büyük kısmı, systemd birim dosyasında yalnızca ExecStart, User ve Restart satırlarıyla yaşar. Bu durumda süreç, kendi kullanıcısının erişebildiği her dosyayı okuyabilir, ağda her adres ailesini kullanabilir, /tmp üzerinden başka süreçlerle dosya paylaşabilir ve çekirdeğin sunduğu sistem çağrılarının neredeyse tamamına erişir. Uygulamada bir uzaktan kod çalıştırma açığı çıktığında saldırganın eline geçen alan, servisin gerçekten ihtiyaç duyduğundan çok daha geniştir. systemd bu alanı daraltmak için onlarca seçenek sunar ve bunların çoğu tek satırdır. Sorun, hangisinin neyi kırdığını bilmeden eklenmeleridir.

Üç yaklaşım

Hiçbir şey yapmamak. Servisi ayrı bir kullanıcıyla çalıştırmak ve bununla yetinmek. Bu bir taban çizgisidir ama dosya sistemi izinleri tek savunma hattı olarak kalır. Dünya tarafından okunabilir yapılandırma dosyaları, başka servislerin soketleri ve ev dizinleri açıkta durur.

Konteynere taşımak. Ad alanları, cgroup sınırları ve seccomp profili zaten gelir. Ama yalnızca izolasyon için bir konteyner çalışma zamanı, imaj hattı ve kayıt defteri eklemek, tek bir ikili dosyayla gelen bir servis için orantısız bir maliyettir. Ayrıca varsayılan konteyner profilleri de çoğu zaman gereğinden geniştir.

systemd'nin kendi sertleştirme seçenekleri. Konteyner çalışma zamanının kullandığı çekirdek mekanizmalarının aynısı (ad alanları, yetenek kümeleri, seccomp) birim dosyasından açılır. Paket yöneticisiyle kurulan, tek ikili dosyalı ya da betik tabanlı servisler için en iyi oran budur. Bu yazının tavsiyesi de bu: konteyneri zaten kullanmıyorsanız, yalnızca izolasyon için konteynere geçmeyin; önce birim dosyasını sertleştirin.

Bir başlangıç şablonu

Aşağıdaki drop-in, ağ üzerinden dinleyen ve kendi veri dizinine yazan sıradan bir servis için makul bir başlangıçtır. systemctl edit uygulama.service ile açılan dosyaya yazılır, böylece paketin getirdiği birim dosyasına dokunulmaz.

[Service]
DynamicUser=yes
StateDirectory=uygulama
CacheDirectory=uygulama
LogsDirectory=uygulama

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes

ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes

CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
UMask=0077

Burada kararların gerekçesi önemli:

ProtectSystem=strict tüm dosya sistemini salt okunur bağlar. Servisin yazması gereken yerler StateDirectory, CacheDirectory ve LogsDirectory ile açılır; systemd bu dizinleri /var/lib, /var/cache ve /var/log altında oluşturur ve sahipliğini servisin kullanıcısına verir. Elle ReadWritePaths listesi tutmak yerine bu üçünü kullanmak, dizinin servisle birlikte doğup sahipliğinin her başlangıçta düzeltilmesi demektir.

DynamicUser=yes her başlangıçta geçici bir kullanıcı ayırır. Elle kullanıcı oluşturma adımı ortadan kalkar ve servisin dosyaları başka hiçbir hesapla ortak sahipliğe düşmez.

CapabilityBoundingSet= boş bırakıldığında süreç root olarak başlatılsa bile hiçbir yeteneği tutamaz. Servis 1024'ün altındaki bir portu dinleyecekse yalnızca o yeteneği verin:

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

SystemCallFilter=@system-service, tipik bir servisin kullandığı sistem çağrılarını kapsayan hazır bir gruptur. SystemCallErrorNumber=EPERM ise listede olmayan bir çağrıda sürecin öldürülmesi yerine çağrının hata döndürmesini sağlar. Varsayılan davranış süreci SIGSYS ile sonlandırmaktır ve bu, ilk denemede neyin bozulduğunu anlamayı çok zorlaştırır.

Ölçerek ilerlemek

systemd, bir birimin ne kadar açıkta olduğunu kendisi raporlar:

systemd-analyze security uygulama.service

Çıktı her seçeneği tek tek listeler ve sonda 0 ile 10 arasında bir açıklık puanı verir. Bu puanı bir hedef olarak değil, unutulan seçenekleri bulmak için bir kontrol listesi olarak kullanın. Bazı seçenekler servisiniz için anlamsızdır ve puanı düşürmek için onları açmak bir şey kazandırmaz.

Bir seçeneğin servisi kırıp kırmadığını görmek için birimi değiştirmeden önce geçici bir birimle deneyebilirsiniz:

sudo systemd-run --pty --wait \
  -p ProtectSystem=strict -p PrivateTmp=yes -p NoNewPrivileges=yes \
  /usr/bin/uygulama --config /etc/uygulama/config.toml

Böylece aynı kısıtlar altında sürecin ön planda nasıl davrandığını, hangi dosyaya yazmaya çalışıp reddedildiğini doğrudan görürsünüz.

Tuzaklar

Var olmayan yol servisi başlatmaz. ReadWritePaths, ReadOnlyPaths ya da InaccessiblePaths içinde var olmayan bir yol varsa servis 226/NAMESPACE çıkış koduyla düşer. Hata mesajı yolu söylemez, yalnızca ad alanı kurulamadı der. İsteğe bağlı yolların başına - koyun; o zaman yol yoksa yok sayılır.

Bilinmeyen seçenek sessizce yok sayılır. Daha eski bir systemd sürümü tanımadığı bir anahtarı görünce günlüğe bir uyarı yazar ve devam eder. Yeni bir dağıtımda hazırladığınız şablonu eski bir sunucuya kopyaladığınızda servis sorunsuz başlar, ama korumaların bir kısmı hiç uygulanmamıştır. Şablonu her hedef sürümde systemd-analyze verify ve systemd-analyze security ile kontrol edin.

MemoryDenyWriteExecute=yes JIT kullanan çalışma zamanlarını kırar. Hem yazılabilir hem çalıştırılabilir bellek ayırmayı yasaklar. Java, Node.js, .NET ve JIT açık PHP bu yüzden başlamaz ya da garip biçimde çöker. Derlenmiş Go ya da Rust ikilileri için güvenle açılabilir, yorumlanan ya da JIT derlenen her şey için önce deneyin.

PrivateTmp=yes paylaşılan /tmp varsayımını bozar. Servis bir soketi ya da geçici dosyayı /tmp altına koyup başka bir sürecin onu bulmasını bekliyorsa artık bulunamaz, çünkü servisin /tmp'si ona özeldir. Ayrıca servis yeniden başladığında bu dizin silinir. Paylaşılan soketler için RuntimeDirectory kullanıp yolu açıkça yapılandırın.

DynamicUser ile dışarıya yazılan dosyaların sahibi değişebilir. StateDirectory altındaki dosyaların sahipliğini systemd her başlangıçta düzeltir. Ama ReadWritePaths ile açtığınız başka bir dizine yazılan dosyalar geçici kullanıcı kimliğiyle kalır; bir sonraki başlangıçta servis başka bir kimlik alırsa kendi dosyalarını okuyamaz. Kalıcı veriyi yalnızca systemd'nin yönettiği dizinlerde tutun.

Drop-in'de ExecStart birikir. Bir drop-in dosyasında ExecStart yazarsanız, normal servislerde bu ikinci bir komut olarak eklenmeye çalışılır ve birim hata verir. Komutu değiştirmek için önce boş bir ExecStart= satırıyla listeyi temizlemek gerekir. Aynı şey liste alan diğer seçenekler için de geçerlidir.

RestrictAddressFamilies dar tutulunca isim çözümleme bozulabilir. AF_UNIX'i listeden çıkarırsanız, sistemdeki isim çözümleyiciyle yerel soket üzerinden konuşan kütüphane çağrıları başarısız olur. Belirti, servisin dış adrese bağlanamaması gibi görünür; asıl neden yerel sokete erişimin kapanmasıdır.

Ne zaman bu yolu seçmemeli

Servis zaten bir konteyner orkestratörü üzerinde çalışıyorsa, aynı kısıtları oradaki güvenlik bağlamında tanımlamak daha doğrudur; iki katmanda ayrı ayrı yönetilen kısıtlar, hangisinin bir hatanın sebebi olduğunu bulmayı zorlaştırır. Servis kendi alt süreçlerini başka kullanıcılara geçerek başlatıyorsa (bir giriş kabuğu, bir CI çalıştırıcısı, bir yedekleme aracı) NoNewPrivileges ve boş yetenek kümesi işini doğrudan engeller; bu tür servisler için genel şablon yerine tek tek seçenek açmak gerekir. Son olarak, servisi sık sık değiştirip yeniden konuşlandıracak bir ekip yoksa, her yeni sürümün yeni bir dosya yolu ya da sistem çağrısı istediğini hatırlayın: sertleştirme bir kez yazılıp unutulan bir yapılandırma değildir, sürüm yükseltmelerinin test listesine girmelidir.

Başlangıç için en çok getiriyi ProtectSystem=strict, ProtectHome, PrivateTmp, NoNewPrivileges ve boş CapabilityBoundingSet verir. Bu beşi neredeyse hiçbir sıradan servisi kırmaz ve saldırganın bir açıktan sonra hareket edebileceği alanı ciddi biçimde küçültür. Gerisini systemd-analyze security çıktısına bakarak, birer birer ekleyin.