Runtime Güvenliği — seccomp, AppArmor, Read-Only FS
Bir önceki derste image güvenliğini, yani container'ın "kapıya gelmeden önceki" güvenliğini konuştuk — güvenilir base image, vulnerability scanning, image signing. Şimdi bir adım ileri gidiyoruz: container çalışırken güvenliği nasıl sağlarız?
Banka Kasası Analojisi
Bir banka kasasını düşün. Kasanın üç güvenlik katmanı var:
Kapı kilidi — sadece yetkili kişiler girer. Bu, Docker'daki "capabilities" — container'a hangi yetkileri verdiğin.
Güvenlik kameraları — kasada ne yapıldığını izler, şüpheli hareket olursa alarm çalar. Bu, seccomp ve AppArmor — hangi işlemlere izin verildiğini kontrol eden mekanizmalar.
Kilitli çekmeceler — kasaya giren kişi bile her çekmeceye erişemez, sadece kendisine atanmış olanlara. Bu, read-only filesystem ve namespace izolasyonu.
Image güvenliği "kasaya kimin girdiğini" kontrol ediyordu. Runtime güvenliği ise içeri girdikten sonra ne yapabildiğini kontrol ediyor. Bir saldırgan container'a sızsa bile, doğru runtime güvenliği ile hasarı minimize edebilirsin.
Linux Capabilities: Root Yetkilerini Parçalara Böl
Çoğu kişi Linux'ta root yetkisini "ya hep ya hiç" olarak düşünür — ya root'sun ya değilsin. Ama aslında Linux, root yetkilerini 37'den fazla küçük parçaya (capability) bölmüş durumda. Ve Docker bize bu parçaları tek tek verme veya alma imkanı sunuyor.
Bunu şöyle düşün: bir oteldeki genel anahtar kartı. Bu kart her kapıyı açar — odaları, kasayı, mutfağı, çatıyı, bodrum katını... Ama aslında temizlik personelinin sadece odalara erişmesi yeterli. Mutfak personelinin sadece mutfağa. Her çalışana sadece ihtiyacı olan erişimi vermen, güvenliği artırır.
Docker varsayılan olarak container'lara bazı capabilities veriyor — bunlar çoğu uygulama için yeterli:
Varsayılan olarak VERİLEN yetkiler:
CHOWN — Dosya sahipliği değiştirme
NET_BIND_SERVICE — 1024 altı portlara bağlanma (web sunucu için gerekli)
SETUID / SETGID — Kullanıcı değiştirme
KILL — Process'lere sinyal gönderme
... ve birkaç tane daha
Varsayılan olarak VERİLMEYEN yetkiler (çok tehlikeli):
SYS_ADMIN — Mount, namespace işlemleri (çok tehlikeli!)
SYS_MODULE — Kernel modülleri yükleme
NET_ADMIN — Network konfigürasyonu
SYS_PTRACE — Process trace (debug)
SYS_TIME — Sistem saatini değiştirmeEn güvenli yaklaşım şu: önce tüm yetkileri kaldır, sonra sadece gerçekten ihtiyaç duyulanları geri ekle:
docker run -d \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
nginx:alpineBu örnekte Nginx container'ına sadece tek bir yetki verdik: port 80 ve 443'e bağlanabilme (NET_BIND_SERVICE). Başka hiçbir şey yapamaz — dosya sahipliği değiştiremez, kernel modülü yükleyemez, network konfigürasyonuna dokunamaz.
Docker Compose'da aynı şeyi şöyle yaparsın:
services:
web:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
- CHOWN # nginx worker process'ler için gerekebilir
- SETGID
- SETUIDPeki hangi capability'ye ihtiyacın olduğunu nasıl bulacaksın? Deneyerek! --cap-drop ALL ile başla ve uygulamayı çalıştır. Hata alırsan, hangi capability'nin eksik olduğunu araştır ve sadece onu ekle.
# Test et: tüm yetkileri kaldır
docker run --cap-drop ALL myapp
# Çalışmazsa, bir bir ekle:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp
# Hâlâ çalışmıyor? Bir tane daha ekle:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE --cap-add CHOWN myapp⚠️ Dikkat: SYS_ADMIN capability'si asla verme. Bu capability mount işlemi, namespace yönetimi gibi çok güçlü yetkiler içeriyor ve container'dan kaçış için kullanılabilir.
Read-Only Filesystem: Dosya Sistemini Kilitle
Şimdi çok etkili ama basit bir güvenlik önlemine bakalım. Container'ın dosya sistemini salt okunur (read-only) yapmak, saldırganın container'a sızdığında yapabileceklerini dramatik şekilde sınırlar.
Neden bu kadar etkili? Bir saldırı senaryosu düşünelim:
Read-only OLMADAN:
Saldırgan bir RCE (Remote Code Execution) açığı buluyor
Container'a shell alıyor
/tmp/malware.shdosyasını yazıp çalıştırıyorReverse shell açıyor, cryptocurrency miner kuruyor
Container'ın network erişimini kullanarak iç ağa yayılıyor
Read-only İLE:
Saldırgan bir RCE açığı buluyor
Container'a shell alıyor
Dosya yazmaya çalışıyor → "Read-only file system" hatası!
Binary indiremez (wget/curl çıktısı yazılamaz)
Saldırı çok daha zor ve sınırlı
Kullanımı çok basit:
docker run -d \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
nginx:alpine--read-only tüm dosya sistemini kilitliyor. Ama bazı uygulamalar geçici dosyalara yazmak ister — bunun için --tmpfs ile RAM-based geçici dizinler tanımlıyoruz. Bu dizinlere yazılabilir ama veriler container restart olduğunda kaybolur.
Hadi doğrulayalım:
# Dosya yazmayı dene
docker exec myapp touch /test.txt
# touch: /test.txt: Read-only file system ✓
# tmpfs'e yazmayı dene
docker exec myapp touch /tmp/test.txt
# Başarılı ✓ (ama restart'ta kaybolur)Her uygulama farklı dizinlere yazmak isteyebilir. Nginx'in cache dizini, Node.js'in temp dizini, PostgreSQL'in data dizini... Bunları bulmak için uygulamayı read-only modda çalıştır ve hata mesajlarına bak:
# Docker Compose'da her servis için uygun tmpfs ayarları
services:
api:
build: ./api
read_only: true
tmpfs:
- /tmp:size=100M # 100MB limitle
- /app/tmp:size=50M
nginx:
image: nginx:alpine
read_only: true
tmpfs:
- /tmp
- /var/run
- /var/cache/nginx
- /var/log/nginx
db:
image: postgres:16-alpine
read_only: true
volumes:
- pgdata:/var/lib/postgresql/data # Kalıcı veri volume'da
tmpfs:
- /tmp
- /var/run/postgresqltmpfs'e noexec flag'i de ekleyebilirsin — böylece tmpfs'e yazılsa bile çalıştırılamaz:
tmpfs:
- /tmp:size=100M,noexec,nosuidBu ek güvenlik katmanı, saldırganın tmpfs'e script yazıp çalıştırmasını da engeller.
Seccomp: Sistem Çağrılarını Filtrele
Seccomp (Secure Computing Mode), Linux kernel'ın sağladığı bir güvenlik mekanizması. Container'ın hangi sistem çağrılarını (syscall) yapabileceğini kontrol ediyor.
Sistem çağrısı ne demek? Uygulamanın kernel'a "şunu yap" dediği her istek bir syscall. Dosya okuma (read), dosya yazma (write), ağ bağlantısı (connect), process oluşturma (fork) — hepsi syscall. Linux'ta 300'den fazla syscall var, ama çoğu uygulama bunların küçük bir kısmını kullanır.
Docker varsayılan olarak yaklaşık 44 tehlikeli syscall'ı engeller. Mesela:
reboot— host'u yeniden başlatma (!)mount/umount— filesystem mount etmekexec_load— yeni kernel yüklemeinit_module— kernel modülü yüklemeclock_settime— sistem saatini değiştirme
Bu varsayılan profil çoğu uygulama için yeterli. Ama daha kısıtlayıcı bir profil de oluşturabilirsin:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": [
"read", "write", "open", "close", "stat",
"mmap", "munmap", "brk", "exit", "exit_group",
"socket", "connect", "accept", "bind", "listen",
"clone", "execve", "getpid", "getuid",
"epoll_create1", "epoll_ctl", "epoll_wait",
"futex", "nanosleep", "clock_gettime",
"getrandom", "fcntl", "getcwd"
],
"action": "SCMP_ACT_ALLOW"
}
]
}Bu profilde defaultAction olarak SCMP_ACT_ERRNO ayarlanmış — yani listedekiler dışında tüm syscall'lar engellenir. Sadece uygulamanın gerçekten ihtiyaç duyduğu syscall'lara izin veriyorsun.
Custom profil ile çalıştırmak için:
docker run -d \
--security-opt seccomp=./custom-seccomp.json \
myapp:latest⚠️ Önemli: Seccomp'ı asla unconfined olarak çalıştırma:
# ❌ TÜM güvenlik filtrelerini devre dışı bırakır!
docker run --security-opt seccomp=unconfined myappHangi syscall'ların gerektiğini nasıl bulursun? strace kullanarak uygulamanın hangi syscall'ları yaptığını izleyebilirsin:
docker run --rm --cap-add SYS_PTRACE myapp \
strace -c -f node server.jsBu komut, uygulamanın kullandığı tüm syscall'ları ve kaç kez çağrıldığını gösterir. Bu bilgiyle custom seccomp profili oluşturabilirsin.
AppArmor: Dosya ve Network Erişim Kontrolü
AppArmor, Ubuntu ve Debian tabanlı sistemlerde varsayılan olarak gelen bir güvenlik modülü (Mandatory Access Control). Seccomp syscall seviyesinde çalışırken, AppArmor dosya sistemi ve network erişimini profil bazında kontrol eder.
Docker varsayılan olarak docker-default AppArmor profilini kullanır. Bu profil:
/procve/sysdizinlerine yazımı engellerMount işlemlerini engeller
Kernel modülleri yüklemeyi engeller
Process trace'i sınırlar
Daha kısıtlayıcı bir custom profil oluşturabilirsin:
profile docker-custom flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# Dosya erişimi
/app/** r, # /app altı okunabilir
/app/tmp/** rw, # /app/tmp yazılabilir
/tmp/** rw, # /tmp yazılabilir
# Engellenen erişimler
deny /etc/shadow r, # Şifre dosyası okunamaz
deny /proc/*/mem r, # Process memory okunamaz
# Shell çalıştırma engeli
deny /bin/sh x, # sh çalıştırılamaz!
deny /bin/bash x, # bash çalıştırılamaz!
# İzin verilen çalıştırılabilirler
/usr/bin/node ix, # Sadece Node.js çalışabilir
}Bu profil çok ilginç şeyler yapıyor: /bin/sh ve /bin/bash çalıştırılmasını engelliyor! Yani saldırgan container'a sızsa bile shell alamaz. Sadece Node.js çalıştırılabilir. Bu, saldırı yüzeyini inanılmaz daraltır.
Profili yükleyip kullanmak için:
# Profili yükle
sudo apparmor_parser -r /etc/apparmor.d/docker-custom
# Container'ı bu profille çalıştır
docker run -d \
--security-opt apparmor=docker-custom \
myapp:latest💡 İpucu: Yeni bir AppArmor profili oluştururken önce "complain" modunda çalıştır — bu mod engellemez ama loglar. Logları inceleyerek neye izin vermen gerektiğini anlarsın, sonra "enforce" moduna geçersin.
No New Privileges: Yetki Yükseltmeyi Engelle
Bu küçük ama çok güçlü bir güvenlik ayarı. Container içindeki process'lerin yetki yükseltme (privilege escalation) yapmasını engeller:
docker run -d \
--security-opt no-new-privileges:true \
myapp:latestBu ne yapar? Normalde Linux'ta bazı binary'ler "setuid" bit'ine sahiptir — yani çalıştırıldıklarında root yetkisiyle çalışırlar (su, sudo, ping gibi). no-new-privileges aktif olduğunda, bu binary'ler çalışsa bile yetki yükseltemez. Bu, privilege escalation saldırılarının büyük bir kısmını engeller.
Docker Compose'da:
services:
api:
image: myapp:latest
security_opt:
- no-new-privileges:trueBir adım daha ileri gidebilirsin — Dockerfile'da setuid/setgid bit'lerini tamamen kaldır:
FROM node:20-alpine
# ... uygulama kurulumu ...
# setuid/setgid bitlerini temizle
RUN find / -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
USER node
CMD ["node", "server.js"]Resource Limits: Fork Bomb ve Memory Abuse'u Önle
Güvenlik sadece saldırılardan korunmak değil, kaynakların kötüye kullanılmasını da önlemek demek. Bir container sınırsız kaynak kullanabiliyorsa, diğer container'ları ve host'u etkileyebilir.
# PID limiti — fork bomb koruması
docker run -d --pids-limit 100 myapp:latest
# Container en fazla 100 process oluşturabilir
# Fork bomb yapılmaya çalışılsa bile 100'de durur
# Memory limiti
docker run -d --memory 512m --memory-swap 512m myapp:latest
# 512MB'den fazla kullanamasın, swap da kullanmasın
# CPU limiti
docker run -d --cpus 1.5 myapp:latest
# En fazla 1.5 CPU core kullanabilirDocker Compose'da hepsini bir arada:
services:
api:
build: ./api
pids_limit: 100
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"Hepsini Bir Araya Koyalım: Hardened Container
Şimdi öğrendiğimiz tüm güvenlik katmanlarını tek bir servis tanımında birleştirelim:
services:
api:
build:
context: ./api
target: production
read_only: true # 1. Read-only filesystem
tmpfs:
- /tmp:size=100M,noexec,nosuid # 2. tmpfs — binary çalıştırılamaz
- /var/run:size=10M
security_opt:
- no-new-privileges:true # 3. Privilege escalation engeli
cap_drop:
- ALL # 4. Tüm capabilities kaldır
cap_add:
- NET_BIND_SERVICE # 5. Sadece port binding
pids_limit: 50 # 6. Fork bomb koruması
user: "1000:1000" # 7. Non-root user
deploy:
resources:
limits:
memory: 256M # 8. Memory limiti
cpus: "0.5" # 9. CPU limiti
logging:
driver: json-file
options:
max-size: "10m" # 10. Log boyut limiti
max-file: "3"
networks:
- backend # 11. Network segmentation
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3Bu tanım 11 farklı güvenlik katmanını içeriyor. Saldırganın işi inanılmaz zor — container'a sızsa bile dosya yazamaz, yetki yükseltemez, shell alamaz, kaynak abuse edemez ve sadece kendi network'ündeki servislerle konuşabilir.
Yaygın Hatalar ve Çözümleri
"Read-only filesystem yüzünden uygulamam çalışmıyor!"
Bu en sık karşılaşılan sorun. Uygulaman bir yere yazmaya çalışıyor ama dosya sistemi kilitli. Çözüm: hangi dizinlere yazıldığını bul ve tmpfs ekle:
# Hangi dizinlere yazılmaya çalışıldığını bul
docker run --rm myapp strace -e trace=open,openat -f node server.js 2>&1 | grep "O_WRONLY\|O_RDWR"Yaygın yazma dizinleri: /tmp, /var/run, /var/cache, /var/log, /app/uploads. Bunları tmpfs veya volume olarak ekle.
"cap-drop ALL sonrası uygulamam bozuldu!"
Bazı uygulamalar beklenmedik capability'lere ihtiyaç duyar. Çözüm: bir bir ekleyerek test et. Hangi capability'nin eksik olduğunu hata mesajından anlayabilirsin.
SELinux: Red Hat Ekosistemi
Ubuntu ve Debian sistemlerde AppArmor kullanılırken, Red Hat, Fedora ve CentOS sistemlerde SELinux varsayılan güvenlik modülüdür. İkisi de aynı amaca hizmet eder — zorunlu erişim kontrolü (Mandatory Access Control) — ama farklı mekanizmalar kullanır.
Docker ile SELinux kullanırken özellikle bind mount'larda dikkat etmen gereken bir nokta var. SELinux, dosyalara "label" (etiket) atar ve sadece uygun labele sahip process'ler o dosyalara erişebilir. Container'dan host dosyasına erişmek istiyorsan, uygun label'ı belirtmen gerekir:
# z flag'i: shared label — birden fazla container erişebilir
docker run -v /data:/app/data:z myapp
# Z flag'i: private label — sadece bu container erişebilir
docker run -v /data:/app/data:Z myappz ve Z arasındaki fark önemli: Z kullandığında sadece o container erişebilir — daha güvenli ama birden fazla container'ın aynı dizine erişmesi gerekiyorsa z kullanmalısın.
SELinux durumunu kontrol etmek için:
getenforce
# Enforcing / Permissive / DisabledEnforcing modda kurallar uygulanır, Permissive modda ihlaller loglanır ama engellenmez (test için kullanışlı), Disabled modda SELinux kapalıdır.
Container'ın Gerçek İzolasyonu: Namespace'ler
Runtime güvenliğini tam anlamak için Docker'ın izolasyon mekanizmasını da bilmen gerekiyor. Docker, Linux kernel'ın namespace özelliğini kullanarak container'ları izole eder:
PID namespace: Container kendi process'lerini görür, host'un process'lerini görmez
NET namespace: Container kendi network stack'ine sahip
MNT namespace: Container kendi dosya sistemi görünümüne sahip
USER namespace: Container kendi kullanıcı tablosuna sahip
UTS namespace: Container kendi hostname'ine sahip
IPC namespace: Container kendi inter-process communication'ına sahip
Bu namespace'ler sayesinde bir container, kendini ayrı bir makine gibi "görür." Ama dikkat: kernel paylaşımlıdır! Tüm container'lar aynı Linux kernel'ı kullanır. Bu yüzden kernel seviyesindeki güvenlik (seccomp, capabilities) bu kadar önemli — kernel üzerinden yapılacak bir saldırı tüm container'ları etkileyebilir.
Bazı Docker flag'leri bu izolasyonu kırar ve güvenlik riskini artırır:
# ❌ Host PID namespace — container host'un TÜM process'lerini görür
docker run --pid=host myapp
# ❌ Host network — container host'un network stack'ini kullanır
docker run --net=host myapp
# ❌ Privileged — TÜM güvenlik katmanlarını devre dışı bırakır
docker run --privileged myappBu flag'leri production'da kullanma — sadece çok özel debug senaryolarında, geçici olarak kullan.
Pratik Senaryo: Bir Web Uygulamasını Güvenli Hale Getirmek
Teorik bilgiyi pratiğe dönüştürelim. Diyelim ki basit bir Node.js API'n var ve onu production'a deploy edeceksin. İlk hali güvensiz:
# ❌ Güvensiz versiyon
services:
api:
build: ./api
ports:
- "3000:3000"Şimdi adım adım güvenli hale getirelim:
# ✅ Güvenli versiyon
services:
api:
build:
context: ./api
target: production
read_only: true
tmpfs:
- /tmp:size=50M,noexec,nosuid
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
pids_limit: 50
user: "1000:1000"
deploy:
resources:
limits:
memory: 256M
cpus: "0.5"
networks:
- backend
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3Her satır bilinçli bir güvenlik kararı. "Ama bu kadar ayar gerekli mi?" diye sorabilirsin. Cevap: evet. Çünkü güvenlik, "işler ters gitmediği sürece gereksiz görünen" şeylerden biri. Ters gittiğinde ise hayat kurtarır.
Bu Derste Ne Öğrendik?
Linux Capabilities ile root yetkilerini parçalara böl —
--cap-drop ALLile başla, sadece gerekeni ekleRead-only filesystem ile container'ın dosya sistemini kilitle — saldırganın dosya yazmasını engelle
Seccomp ile tehlikeli sistem çağrılarını filtrele — Docker varsayılanı 44 syscall'ı engeller
AppArmor ile dosya ve network erişimini profil bazında kısıtla — shell bile engellenebilir
No new privileges ile privilege escalation saldırılarını engelle
Resource limits (memory, CPU, PID) ile kaynak abuse'u önle
Tüm bu katmanları birlikte kullan — tek bir katman yeterli değil, defense in depth (derinlemesine savunma) prensibi
Sonraki derste secret management konusuna geçeceğiz — şifreler, API key'ler ve hassas bilgileri Docker ortamında güvenli şekilde yönetmeyi öğreneceğiz.
AI Asistan
Sorularını yanıtlamaya hazır