← Kursa Dön
📄 Text · 30 min

Container Güvenlik Temelleri — Root Sorunu, Namespace, cgroup

Şu ana kadar Docker'ın nasıl çalıştığını, image'ları nasıl oluşturduğumuzu, container'ları nasıl yönettiğimizi ve Docker Compose ile çoklu servisleri nasıl ayağa kaldırdığımızı öğrendik. Harika — artık Docker ile çalışan uygulamalar yaratabiliyorsun. Ama şimdi çok kritik bir konuya geliyoruz: güvenlik.

Şöyle düşün: bir ev inşa etmeyi öğrendin. Duvarları, çatıyı, kapıları biliyorsun. Ama kapıya kilit takmayı, pencereye parmaklık koymayı, alarm sistemi kurmayı öğrenmediysen, o ev ne kadar sağlam olursa olsun güvende değil. Docker güvenliği de tam böyle — uygulamanı çalıştırmayı bilmek yetmez, onu güvenli çalıştırmayı da bilmen gerekiyor.

Neden Güvenlik Bu Kadar Önemli?

Belki şu anda düşünüyorsun: "Ben küçük bir proje yapıyorum, kim benim container'ıma saldıracak ki?" Haklı olabilirsin, ama güvenlik alışkanlıkları baştan edinilmeli. Çünkü bir gün production'a çıktığında, yanlış bir konfigürasyon yüzünden tüm sunucunun ele geçirilmesini istemezsin.

Gerçek dünyadan bir örnek vereyim: 2022'de Uber hacklendi. Saldırgan, şirket içi sistemlere hardcoded (kodun içine gömülü) bir credential ile erişti. Sonuç? Tüm iç ağa erişim. Docker dünyasında da benzer tehlikeler var — container'ın root olarak çalışması, image içinde unutulan bir API key, açık bırakılan bir Docker socket... Bunların her biri bir güvenlik açığı.

Bu bölümde dört ders boyunca Docker güvenliğinin her katmanını inceleyeceğiz. Bu ilk derste genel tabloyu göreceğiz ve en temel güvenlik risklerini tanıyacağız. Hazırsan başlayalım.

Docker Güvenliğini Bir AVM Gibi Düşün

Bir AVM'yi hayal et. Her mağazanın kendi kapısı var, kendi kasası var, kendi güvenlik kamerası var. Ama bunların yanı sıra AVM'nin de genel güvenliği var — giriş kontrolü, yangın çıkışları, alarm sistemi. Bir mağazada hırsızlık olursa ideal senaryoda sadece o mağaza etkilenir, AVM'nin tamamı değil. Ama güvenlik sistemi düzgün kurulmamışsa, bir mağazadaki sorun tüm AVM'ye yayılabilir.

Docker güvenliği de tıpkı bu AVM gibi çok katmanlı bir yapı. Her katman ayrı bir sorumluluk taşıyor:

┌─────────────────────────────────────────────────────────────┐
│                    1. Image Güvenliği                        │
│  Hangi base image kullanıyorsun? İçinde zafiyet var mı?     │
├─────────────────────────────────────────────────────────────┤
│                    2. Build Güvenliği                        │
│  Dockerfile'ına yanlışlıkla secret gömüyor musun?           │
├─────────────────────────────────────────────────────────────┤
│                    3. Runtime Güvenliği                      │
│  Container çalışırken ne kadar yetkisi var?                  │
├─────────────────────────────────────────────────────────────┤
│                    4. Network Güvenliği                      │
│  Container'lar birbirleriyle nasıl konuşuyor?               │
├─────────────────────────────────────────────────────────────┤
│                    5. Secret Management                      │
│  Şifreler, API key'ler nerede saklanıyor?                   │
├─────────────────────────────────────────────────────────────┤
│                    6. Host Güvenliği                         │
│  Docker daemon'ın kendisi güvende mi?                        │
└─────────────────────────────────────────────────────────────┘

Bu derste bu katmanların her birine genel bir bakış atacağız. Sonraki üç derste ise image güvenliği, runtime güvenliği ve secret management konularını derinlemesine inceleyeceğiz.

En Büyük Risk: Root Olarak Çalışan Container'lar

Docker güvenliğinde en sık yapılan ve en tehlikeli hata, container'ları root kullanıcısı olarak çalıştırmaktır. Peki bu neden bu kadar tehlikeli?

Hadi bir deney yapalım. Bir container başlatalım ve içinde hangi kullanıcı olduğumuza bakalım:

docker run --rm alpine whoami

Bu komutun çıktısı root olacak. Şimdi bir de kullanıcı ID'sine bakalım:

docker run --rm alpine id

Çıktı şöyle bir şey olacak: uid=0(root) gid=0(root). Buradaki uid=0 çok önemli — bu, Linux'ta en yetkili kullanıcı anlamına geliyor.

Peki bu neden sorun? Normalde container'lar izole edilmiş bir ortamda çalışır, değil mi? Doğru. Ama bu izolasyon kusursuz değil. Eğer bir saldırgan container'daki bir güvenlik açığını kullanarak "container'dan kaçmayı" (container escape) başarırsa, container içinde root olduğu için host sistemde de root yetkilerine sahip olur. Bu da sunucunun tamamen ele geçirilmesi demek.

Bunu şöyle düşün: bir misafir odasındasın ve kapı kilitli. Ama eğer kapıyı kırmayı başarırsan ve elinde evin ana anahtarı varsa, tüm eve erişirsin. Container'da root çalışmak, misafire evin ana anahtarını vermek gibi.

Çözüm: Non-Root Kullanıcı ile Çalışmak

Çözüm basit — Dockerfile'ında bir kullanıcı oluştur ve container'ı o kullanıcıyla çalıştır:

FROM node:20-alpine

# Önce bir kullanıcı grubu ve kullanıcı oluşturuyoruz
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# Dosyaları kopyalarken sahipliği yeni kullanıcıya veriyoruz
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .

# Artık bu kullanıcıyla çalışacağız
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

Bu Dockerfile'da neler yaptık, adım adım bakalım:

  1. addgroup -S appgroup — "appgroup" adında bir sistem grubu oluşturduk

  2. adduser -S appuser -G appgroup — "appuser" adında bir kullanıcı oluşturduk ve bu gruba ekledik

  3. COPY --chown=appuser:appgroup — dosyaları kopyalarken sahipliği bu kullanıcıya verdik

  4. USER appuser — bu noktadan sonra her şey "appuser" olarak çalışacak

Şimdi doğrulayalım:

docker build -t myapp .
docker run --rm myapp whoami

Çıktı artık appuser olacak. Container'dan kaçış olsa bile, saldırgan artık root değil — yapabilecekleri çok sınırlı.

💡 İpucu: USER komutu, Dockerfile'daki en önemli güvenlik satırıdır. Her Dockerfile'da olmalı.

User Namespace Remapping ile Ekstra Güvenlik

Docker daemon seviyesinde bir adım daha ileri gidebilirsin. User namespace remapping özelliğiyle, container içindeki root (UID 0) kullanıcısı, host üzerinde çok yüksek ve yetkisiz bir UID'ye (mesela 100000) eşlenir:

{
  "userns-remap": "default"
}

Bu ayarı /etc/docker/daemon.json dosyasına ekleyip Docker'ı yeniden başlattığında, artık container'daki root bile host üzerinde sıradan bir kullanıcı olur. Container'dan kaçış olsa bile host'ta root yetkisi yok.

Image Güvenliği: Nereden Çektiğine Dikkat Et

Düşün ki bir yemek yapacaksın ve malzemeyi tanımadığın birinden, sokak kenarından alıyorsun. Etiket yok, son kullanma tarihi yok, nereden geldiği belli değil. Riskli, değil mi? Docker image'ları da öyle.

Docker Hub'da yüz binlerce image var. Bazıları Docker'ın resmi ekibi tarafından bakımı yapılan, güvenilir image'lar. Bazıları ise... kim tarafından yapıldığı, içinde ne olduğu belirsiz image'lar.

# ✅ Güvenilir: Docker Official Image
FROM node:20-alpine
FROM postgres:16
FROM nginx:1.25-alpine

# ✅ Güvenilir: Verified Publisher (doğrulanmış yayıncı)
FROM bitnami/redis:7.2

# ❌ Riskli: Tanımadığın birinin image'ı
FROM randomuser/mystery-image:latest
# Bunun içinde ne var bilmiyorsun — backdoor bile olabilir!

Resmi image'ları nasıl tanırsın? Docker Hub'da "Docker Official Image" veya "Verified Publisher" rozeti olan image'lara güvenebilirsin. Tanımadığın kullanıcıların image'larını kullanmaktan kaçın — ya da en azından Dockerfile'larını incele.

Bir de image'ların boyutu meselesi var. Küçük image = daha az yazılım = daha az güvenlik açığı potansiyeli:

# ❌ Full Ubuntu image — 1.1GB, yüzlerce paket, geniş saldırı yüzeyi
FROM ubuntu:22.04

# ✅ Alpine — 5MB, sadece gerekli minimum
FROM alpine:3.19

# ✅✅ Distroless — shell bile yok, sadece runtime
FROM gcr.io/distroless/nodejs20-debian12

# ✅✅✅ Scratch — tamamen boş, sadece senin binary'n (Go, Rust için)
FROM scratch

Bir image'da ne kadar az yazılım varsa, o kadar az güvenlik açığı olur. Bu mantığı aklında tut — sonraki derste bunu çok daha derinlemesine işleyeceğiz.

Build Güvenliği: Secret'ları Image'a Gömme!

Bu çok yaygın yapılan bir hata ve sonuçları çok kötü olabiliyor. Şunu düşün: API anahtarını veya veritabanı şifresini Dockerfile'a yazıyorsun. Image build ediliyor, registry'ye push ediliyor. Artık o secret, image'ın katmanlarında sonsuza kadar kalıyor.

# ❌ ASLA YAPMA: Secret image'a gömülür
ENV API_KEY=super-secret-key-123

"Ama sonra sildim" diyebilirsin. Ama Docker image'ları katmanlardan oluşur, hatırla. Her RUN, COPY, ENV komutu yeni bir katman oluşturur. Ve docker history komutuyla herkes bu katmanları görebilir:

docker history myapp:latest
# ... ENV API_KEY=super-secret-key-123  ← Herkes görebilir!

Hatta ARG (build argument) bile güvenli değil — image history'de görünebilir:

# ❌ Bu da güvenli değil
ARG API_KEY
RUN curl -H "Authorization: Bearer ${API_KEY}" https://api.example.com/download

Peki doğru yöntem ne? BuildKit'in secret mount özelliğini kullanmak:

# ✅ Doğru yöntem: BuildKit secret mount
RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) && \
    curl -H "Authorization: Bearer ${API_KEY}" https://api.example.com/download

Ve build sırasında secret'ı şöyle geçiriyorsun:

docker build --secret id=api_key,src=./api_key.txt -t myapp .

Bu yöntemle secret build sırasında kullanılır ama image katmanlarına yazılmaz. docker history ile baktığında hiçbir iz yok.

Ayrıca .dockerignore dosyasını kullanmayı da unutma. .env dosyaları, SSH key'leri, .git dizini gibi hassas dosyalar image'a kopyalanmamalı:

# .dockerignore
.env
.env.*
*.key
*.pem
.git
.ssh
node_modules

Runtime Güvenliği: Çalışan Container'ı Korumak

Container ayağa kalktıktan sonra da güvenlik bitmez. Çalışan container'ın ne yapabildiğini sınırlandırman gerekiyor. Bunu birkaç yöntemle yaparsın.

Read-Only Dosya Sistemi

Container'ın dosya sistemini salt okunur (read-only) yapmak, saldırganın zararlı dosya yazmasını engeller:

docker run -d \
    --read-only \
    --tmpfs /tmp \
    --tmpfs /var/run \
    nginx:alpine

Şimdi birisi container'a sızsa bile, dosya sistemi kilitli olduğu için zararlı script yazamaz, binary indiremez. --tmpfs ile geçici dizinleri yazılabilir yapıyoruz çünkü bazı uygulamalar geçici dosyalara ihtiyaç duyuyor — ama bunlar RAM'de tutuluyor ve container restart olunca kayboluyor.

Linux Capabilities: İnce Ayar Yetkiler

Linux'ta root yetkisi aslında tek bir büyük yetki değil — 37'den fazla küçük parçaya (capability) bölünmüş. Docker bize bu parçaları tek tek verme veya alma imkanı sunuyor:

# Tüm yetkileri al, sadece gerekeni ver
docker run -d \
    --cap-drop ALL \
    --cap-add NET_BIND_SERVICE \
    nginx:alpine

Bu örnekte container'ın tek yapabildiği şey 80 ve 443 gibi düşük portlara bağlanmak (NET_BIND_SERVICE). Başka hiçbir privileged işlem yapamaz — dosya sahipliği değiştiremez, kernel modülü yükleyemez, sistem saatini değiştiremez.

--privileged Flagini ASLA Kullanma

# ❌ ASLA production'da kullanma
docker run --privileged myapp

--privileged flag'i, container'a host'un tüm yetkilerini verir. Bu, güvenlik izolasyonunu tamamen devre dışı bırakır. Container'da root olmak zaten riskliydi — --privileged ile çalışan root ise host'ta root olmakla neredeyse aynı şey.

Network Güvenliği: Kimin Kiminle Konuştuğunu Kontrol Et

Varsayılan olarak aynı Docker network'ündeki tüm container'lar birbirleriyle konuşabilir. Bu, güvenlik açısından her zaman istenen bir durum değil. Örneğin, veritabanı container'ının internete çıkmasına gerek yok:

# Internal network oluştur — dış erişim yok
docker network create --internal backend-net

# Veritabanını sadece internal network'e bağla
docker run -d --network backend-net --name db postgres:16

Bu container internete çıkamaz ve dışarıdan erişilemez. Sadece aynı backend-net ağındaki container'lar (örneğin API servisin) ile konuşabilir.

Production'da genellikle iki ağ oluşturursun:

docker network create frontend-net              # Dış dünyaya açık
docker network create backend-net --internal     # Sadece iç iletişim

Frontend servislerin (Nginx gibi) her iki ağa da bağlanır — hem dışarıyla hem backend ile konuşabilir. Ama veritabanın sadece backend-net'te kalır — dış dünyadan tamamen izole.

Secret Management: Şifreleri Nerede Saklıyorsun?

Docker güvenliğinin en çok ihmal edilen kısmı secret management. Veritabanı şifreleri, API key'leri, JWT secret'ları... Bunlar nerede duruyor?

En sık yapılan hata, environment variable olarak geçmek:

# ❌ docker inspect ile herkes görebilir!
docker run -e DB_PASSWORD=supersecret myapp

docker inspect myapp --format '{{.Config.Env}}'
# [DB_PASSWORD=supersecret NODE_ENV=production ...]

Gördüğün gibi, docker inspect komutuyla secret açık açık okunabiliyor. Sunucuya SSH erişimi olan herkes bu komutu çalıştırabilir.

Daha güvenli yöntem, Docker secrets kullanmak:

# docker-compose.yml
services:
  api:
    image: myapp
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Bu yöntemde secret, container içinde /run/secrets/db_password dosyası olarak görünür ve docker inspect'te görünmez. Bu konuyu dördüncü derste çok detaylı işleyeceğiz.

Docker Daemon Güvenliği: Arka Plandaki Tehlike

Docker daemon, tüm container'ları yöneten arka plan servisi. Bu daemon'a erişimi olan biri, sunucudaki her şeyi yapabilir — container oluşturabilir, silebilir, host dosya sistemini okuyabilir.

İki kritik kural:

1. Docker socket'ını container'a mount etme (mümkünse):

# ❌ Bu container, host'taki TÜM container'ları yönetebilir
docker run -v /var/run/docker.sock:/var/run/docker.sock myapp

Docker socket'ına erişim = root erişimi. Bunu bir container'a vermek, o container'a sunucunun anahtarını vermek demek.

2. Docker daemon'ı şifresiz TCP'ye açma:

{
  "hosts": ["tcp://0.0.0.0:2375"]
}

Bu konfigürasyonla Docker daemon'ına dünyadan herkes erişebilir. İnternete açık sunucularda bu yapılmışsa, dakikalar içinde cryptocurrency miner kurulur.

Eğer TCP erişimi gerçekten gerekiyorsa, mutlaka TLS ile koru:

{
  "hosts": ["tcp://0.0.0.0:2376"],
  "tls": true,
  "tlscacert": "/etc/docker/ca.pem",
  "tlscert": "/etc/docker/server-cert.pem",
  "tlskey": "/etc/docker/server-key.pem"
}

Güvenlik Denetimi: Docker Bench Security

Tüm bu kuralları tek tek kontrol etmek zor olabilir. Neyse ki Docker'ın otomatik bir güvenlik denetim aracı var — Docker Bench for Security. Bu araç, CIS (Center for Internet Security) Docker Benchmark'ına göre sisteminizi denetler:

docker run --rm --net host --pid host \
    --userns host --cap-add audit_control \
    -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
    -v /etc:/etc:ro \
    -v /usr/bin/containerd:/usr/bin/containerd:ro \
    -v /usr/bin/runc:/usr/bin/runc:ro \
    -v /usr/lib/systemd:/usr/lib/systemd:ro \
    -v /var/lib:/var/lib:ro \
    -v /var/run/docker.sock:/var/run/docker.sock:ro \
    docker/docker-bench-security

Bu araç çalıştığında, sisteminizi yüzlerce güvenlik kontrolünden geçirir ve size bir rapor sunar: nelerin iyi olduğunu, nelerin düzeltilmesi gerektiğini söyler. Production'a çıkmadan önce mutlaka çalıştır.

Production Güvenlik Kontrol Listesi

Hadi öğrendiklerimizi bir kontrol listesi haline getirelim. Production'a deploy etmeden önce bu listeyi gözden geçir:

Container Güvenliği:

  • Non-root user kullanılıyor mu?

  • Read-only root filesystem aktif mi?

  • cap_drop ALL yapılmış, sadece gerekliler eklenmiş mi?

  • no-new-privileges aktif mi?

  • Memory ve CPU limitleri ayarlanmış mı?

  • Healthcheck tanımlanmış mı?

Image Güvenliği:

  • Official veya verified base image mi?

  • Image düzenli taranıyor mu (vulnerability scan)?

  • Minimal base image mi (alpine/distroless)?

  • Belirli tag veya digest kullanılıyor mu (latest değil)?

  • Multi-stage build ile build araçları temizlenmiş mi?

  • .dockerignore ile gereksiz dosyalar hariç tutulmuş mu?

Network ve Secret:

  • Internal network ile backend izole mi?

  • Gereksiz portlar kapalı mı?

  • Secret'lar environment variable'da değil, düzgün yönetiliyor mu?

  • Docker socket container'a mount edilmemiş, değil mi?

Bu listeyi bir yere kaydet. Her deployment öncesi gözden geçirmeni öneriyorum.

Bu Derste Ne Öğrendik?

Bu derse Docker güvenliğinin büyük resmini gördük. Şimdi bir özet yapalım:

  • Docker güvenliği çok katmanlı bir yapıdır — image, build, runtime, network, secret ve host güvenliği

  • Container'ları root olarak çalıştırmak en büyük güvenlik riskidir — her Dockerfile'da USER komutu olmalı

  • Image içine secret gömülmemeli — BuildKit secret mount kullanılmalı

  • Container'ın dosya sistemi read-only olmalı, yetkileri minimum tutulmalı

  • Network segmentation ile backend servisler izole edilmeli

  • Docker daemon güvenliği ihmal edilmemeli — socket ve TCP erişimi kontrol altında olmalı

  • Docker Bench Security ile düzenli güvenlik denetimi yapılmalı

Sonraki derste image güvenliğini derinlemesine inceleyeceğiz — vulnerability scanning, image signing ve güvenilir base image seçimi konularına bakacağız.

⚠️ Dikkat: Güvenlik bir "bir kere yap, unut" konusu değil. Sürekli güncel kalman, yeni açıkları takip etmen ve güvenlik pratiklerini alışkanlık haline getirmen gerekiyor.