← Kursa Dön
📄 Text · 35 min

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:

  1. Kapı kilidi — sadece yetkili kişiler girer. Bu, Docker'daki "capabilities" — container'a hangi yetkileri verdiğin.

  2. 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.

  3. 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ştirme

En 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:alpine

Bu ö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
      - SETUID

Peki 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:

  1. Saldırgan bir RCE (Remote Code Execution) açığı buluyor

  2. Container'a shell alıyor

  3. /tmp/malware.sh dosyasını yazıp çalıştırıyor

  4. Reverse shell açıyor, cryptocurrency miner kuruyor

  5. Container'ın network erişimini kullanarak iç ağa yayılıyor

Read-only İLE:

  1. Saldırgan bir RCE açığı buluyor

  2. Container'a shell alıyor

  3. Dosya yazmaya çalışıyor → "Read-only file system" hatası!

  4. Binary indiremez (wget/curl çıktısı yazılamaz)

  5. 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/postgresql

tmpfs'e noexec flag'i de ekleyebilirsin — böylece tmpfs'e yazılsa bile çalıştırılamaz:

tmpfs:
  - /tmp:size=100M,noexec,nosuid

Bu 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 etme

  • kexec_load — yeni kernel yükleme

  • init_module — kernel modülü yükleme

  • clock_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 myapp

Hangi 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.js

Bu 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:

  • /proc ve /sys dizinlerine yazımı engeller

  • Mount 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:latest

Bu 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:true

Bir 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 kullanabilir

Docker 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: 3

Bu 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 myapp

z 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 / Disabled

Enforcing 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 myapp

Bu 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: 3

Her 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 ALL ile başla, sadece gerekeni ekle

  • Read-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.