Docker Compose Secrets ile Hassas Veri Yönetimi: Şifre ve Token Güvenliği

Production ortamında çalışan bir uygulamanın veritabanı şifresi, API anahtarı ya da JWT token’ı yanlış yönetildiğinde sonuçları felaket olabilir. Docker Compose kullanarak uygulama deploy eden birçok ekip, hassas verileri environment variable olarak düz metin halinde docker-compose.yml dosyasına yazıyor ve bu dosyayı Git reposuna push ediyor. Bu yazıda Docker Compose Secrets mekanizmasını kullanarak bu güvenlik açığını nasıl kapatabileceğinizi, gerçek dünya senaryolarıyla birlikte adım adım anlatacağım.

Neden Environment Variable Yeterli Değil?

Klasik yöntemde şifreler genellikle şöyle tanımlanır:

# docker-compose.yml - YANLIS YONTEM
version: '3.8'
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: super_gizli_sifre_123
      POSTGRES_USER: myapp

Bu yaklaşımın birkaç kritik sorunu var:

  • Git geçmişine gömülür: Şifreyi sonradan değiştirseniz bile eski commit’lerde kalır
  • docker inspect ile görünür: Container’a erişimi olan herkes docker inspect komutuyla tüm environment variable’ları düz metin olarak görebilir
  • Log’lara sızabilir: Bazı uygulamalar hata durumunda ortam değişkenlerini log’a yazar
  • .env dosyası ihmalkar ellerde: Ekip üyeleri .env dosyasını yanlışlıkla commit’leyebilir

Docker Secrets tam olarak bu problemleri çözmek için tasarlanmış bir mekanizmadır.

Docker Compose Secrets Nasıl Çalışır?

Docker Secrets, hassas verileri şifreli bir şekilde saklar ve container’a /run/secrets/ yolu üzerinden bir dosya olarak mount eder. Container içindeki process bu dosyayı okuyarak şifreye erişir. Bu yöntemde şifre hiçbir zaman environment variable olarak process listesinde görünmez.

Secrets iki farklı şekilde tanımlanabilir:

  • file tabanlı: Şifreyi bir dosyadan okur
  • external: Docker Swarm’daki mevcut bir secret’ı kullanır (production için)

Compose geliştirme ortamında genellikle file tabanlı yaklaşımı kullanırız.

Temel Kurulum: İlk Secrets Yapılandırması

Önce basit bir örnekle başlayalım. PostgreSQL veritabanı şifresini secrets ile yönetelim:

# Önce secret dosyalarını oluştur
mkdir -p secrets
echo "guclu_veritabani_sifresi_2024!" > secrets/db_password.txt
echo "myapp_user" > secrets/db_user.txt

# Bu dosyaları .gitignore'a ekle
echo "secrets/" >> .gitignore

Şimdi docker-compose.yml dosyasını oluşturalım:

# docker-compose.yml - DOGRU YONTEM
version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_USER_FILE: /run/secrets/db_user
      POSTGRES_DB: myapp_db
    secrets:
      - db_password
      - db_user
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend

  app:
    image: myapp:latest
    depends_on:
      - db
    secrets:
      - db_password
    environment:
      DB_HOST: db
      DB_NAME: myapp_db
    networks:
      - backend

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

volumes:
  postgres_data:

networks:
  backend:

Burada dikkat edilmesi gereken önemli nokta: PostgreSQL resmi imajı _FILE suffix’li environment variable’ları destekler. Bu sayede şifreyi /run/secrets/db_password dosyasından okur.

Uygulama Tarafında Secret Okuma

Uygulamanızın secrets dosyasını okuması gerekiyor. İşte birkaç farklı dilde nasıl yapılacağı:

# Python örneği - secret_helper.py
def read_secret(secret_name):
    """Docker secret dosyasini oku"""
    secret_path = f"/run/secrets/{secret_name}"
    try:
        with open(secret_path, 'r') as f:
            return f.read().strip()
    except FileNotFoundError:
        # Geliştirme ortamında environment variable'dan oku
        import os
        return os.environ.get(secret_name.upper())

# Kullanim
db_password = read_secret("db_password")
api_token = read_secret("api_token")

Bu yaklaşım hem production’da secrets dosyasından hem de lokal geliştirme ortamında environment variable’dan okuma yapabildiği için esnektir.

Gerçek Dünya Senaryosu: Mikroservis Mimarisi

Diyelim ki bir e-ticaret uygulaması deploy ediyorsunuz. Bu uygulama şu bileşenlerden oluşuyor:

  • PostgreSQL veritabanı
  • Redis cache
  • Backend API servisi
  • Stripe ödeme entegrasyonu
  • SendGrid e-posta servisi
# secrets/ dizin yapısı
secrets/
  db_password.txt
  db_user.txt
  redis_password.txt
  stripe_secret_key.txt
  sendgrid_api_key.txt
  jwt_secret.txt
# docker-compose.yml - Tam mikroservis ornegi
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_USER_FILE: /run/secrets/db_user
      POSTGRES_DB: ecommerce
    secrets:
      - db_password
      - db_user
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - db_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$(cat /run/secrets/db_user)"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: >
      sh -c 'redis-server --requirepass "$$(cat /run/secrets/redis_password)"'
    secrets:
      - redis_password
    networks:
      - cache_network

  api:
    image: ecommerce-api:latest
    depends_on:
      postgres:
        condition: service_healthy
    secrets:
      - db_password
      - db_user
      - redis_password
      - stripe_secret_key
      - sendgrid_api_key
      - jwt_secret
    environment:
      DB_HOST: postgres
      DB_NAME: ecommerce
      REDIS_HOST: redis
      NODE_ENV: production
    networks:
      - db_network
      - cache_network
      - frontend_network
    ports:
      - "3000:3000"

secrets:
  db_password:
    file: ./secrets/db_password.txt
  db_user:
    file: ./secrets/db_user.txt
  redis_password:
    file: ./secrets/redis_password.txt
  stripe_secret_key:
    file: ./secrets/stripe_secret_key.txt
  sendgrid_api_key:
    file: ./secrets/sendgrid_api_key.txt
  jwt_secret:
    file: ./secrets/jwt_secret.txt

volumes:
  pgdata:

networks:
  db_network:
  cache_network:
  frontend_network:

Secret Dosyalarını Güvenli Şekilde Oluşturma

Secret dosyalarını elle oluşturmak yerine otomatik ve güvenli bir yöntem kullanmak çok daha iyidir:

#!/bin/bash
# create_secrets.sh - Secret dosyalarini guvenli olustur

SECRETS_DIR="./secrets"
mkdir -p "$SECRETS_DIR"

# Güçlü rastgele şifre üret (32 karakter)
generate_password() {
    openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
}

# Eğer dosya yoksa oluştur, varsa dokunma
create_secret_if_not_exists() {
    local secret_file="$SECRETS_DIR/$1"
    local secret_value="$2"
    
    if [ ! -f "$secret_file" ]; then
        echo "$secret_value" > "$secret_file"
        chmod 600 "$secret_file"
        echo "Olusturuldu: $secret_file"
    else
        echo "Mevcut, atlandı: $secret_file"
    fi
}

# Secrets olustur
create_secret_if_not_exists "db_password.txt" "$(generate_password)"
create_secret_if_not_exists "db_user.txt" "appuser"
create_secret_if_not_exists "redis_password.txt" "$(generate_password)"
create_secret_if_not_exists "jwt_secret.txt" "$(openssl rand -hex 64)"

echo ""
echo "Secrets olusturuldu. Bu dosyalari asla Git'e commit'lemeyin!"
echo "secrets/ dizininin .gitignore'da oldugunu dogrulayin."
# Scripti çalıştırılabilir yap ve çalıştır
chmod +x create_secrets.sh
./create_secrets.sh

# .gitignore kontrolü
cat .gitignore | grep secrets

Secret Güvenliğini Doğrulama

Secrets’ın gerçekten güvenli çalıştığını doğrulamak için şu komutları kullanabilirsiniz:

# Container'ı başlat
docker-compose up -d

# docker inspect ile environment variable'larda şifre görünmemeli
docker inspect ecommerce_api_1 | grep -A 20 '"Env"'
# Çıktıda şifre görünmemeli, sadece DB_HOST, DB_NAME gibi değerler olmalı

# Secret'ın container içinde mount edildiğini doğrula
docker exec ecommerce_api_1 ls -la /run/secrets/
# Çıktı:
# -r--r--r-- 1 root root 33 Jan 15 10:23 db_password
# -r--r--r-- 1 root root 65 Jan 15 10:23 jwt_secret

# Secret içeriğini container içinden oku
docker exec ecommerce_api_1 cat /run/secrets/db_password
# Şifre burada görünür, ama bu kasıtlı - sadece yetkili container erişebilir

# Secret dosyasının izinlerini kontrol et
docker exec ecommerce_api_1 stat /run/secrets/db_password

Docker Swarm ile Production’da External Secrets

Compose dosyanızı production’da Docker Swarm üzerinde çalıştırıyorsanız, external secrets kullanmak çok daha güvenlidir. Bu yöntemde secret değerleri disk üzerinde şifreli saklanır:

# Swarm modunu etkinleştir
docker swarm init

# Secret'ı doğrudan oluştur (dosya kullanmadan)
echo "super_gizli_db_sifresi" | docker secret create db_password -

# Veya dosyadan oluştur ve sonra dosyayı sil
openssl rand -base64 32 > /tmp/db_pass.txt
docker secret create db_password /tmp/db_pass.txt
shred -u /tmp/db_pass.txt  # Dosyayı güvenli sil

# Mevcut secret'ları listele
docker secret ls

# Secret detayını gör (değeri göremezsiniz, sadece metadata)
docker secret inspect db_password
# docker-compose.yml - Swarm için external secrets
version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager

secrets:
  db_password:
    external: true  # Swarm'da onceden olusturulmus secret'i kullan

Secrets Rotasyonu: Şifre Değiştirme Süreci

Production’da periyodik olarak şifre değiştirmeniz gerektiğinde süreci şöyle yönetebilirsiniz:

#!/bin/bash
# rotate_secret.sh - Secret rotasyonu

SECRET_NAME="db_password"
NEW_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)

echo "Secret rotasyonu basliyor: $SECRET_NAME"

# 1. Adım: Yeni şifreyi veritabanında güncelle
echo "Veritabani sifresi guncelleniyor..."
docker exec postgres_db psql -U postgres -c 
    "ALTER USER appuser PASSWORD '$NEW_PASSWORD';"

# 2. Adım: Secret dosyasını güncelle
echo "$NEW_PASSWORD" > ./secrets/${SECRET_NAME}.txt

# 3. Adım: Servisleri yeniden başlat
echo "Servisler yeniden baslatiliyor..."
docker-compose up -d --force-recreate api

echo "Secret rotasyonu tamamlandi!"
echo "Yeni şifre secrets/$SECRET_NAME.txt dosyasinda"

CI/CD Pipeline’da Secrets Yönetimi

GitLab CI veya GitHub Actions kullanıyorsanız, secret dosyalarını pipeline içinde güvenli bir şekilde oluşturabilirsiniz:

# .gitlab-ci.yml örneği
deploy_production:
  stage: deploy
  script:
    # CI/CD değişkenlerinden secret dosyaları oluştur
    # Bu değerler GitLab CI'da masked variable olarak tanımlanır
    - mkdir -p secrets
    - echo "$PROD_DB_PASSWORD" > secrets/db_password.txt
    - echo "$PROD_STRIPE_KEY" > secrets/stripe_secret_key.txt
    - echo "$PROD_JWT_SECRET" > secrets/jwt_secret.txt
    - chmod 600 secrets/*.txt
    
    # Deploy et
    - docker-compose -f docker-compose.prod.yml up -d
    
    # Güvenlik için secret dosyalarını temizle
    - shred -u secrets/*.txt
    - rmdir secrets
  environment:
    name: production
  only:
    - main

Buradaki kritik nokta: $PROD_DB_PASSWORD gibi değerler GitLab ya da GitHub’da masked ve protected olarak işaretlenmiş CI/CD değişkenlerinden geliyor. Bu sayede log’larda hiç görünmüyor.

Sık Yapılan Hatalar ve Çözümleri

Pratikte karşılaşılan en yaygın problemlerin listesi:

  • Secrets dizinini .dockerignore’a eklememek: Docker image build sırasında COPY . . komutuyla şifre dosyaları image’a dahil olabilir. secrets/ dizinini hem .gitignore hem .dockerignore dosyasına ekleyin.
  • Secret dosyasında sondaki newline karakteri: Bazı uygulamalar dosyayı okurken n karakterini şifrenin parçası olarak algılar. echo -n "sifre" kullanarak veya uygulama tarafında .strip() ile bu sorunu çözebilirsiniz.
  • Container içinde gereksiz secret mount’ları: Her servise sadece ihtiyaç duyduğu secret’ları verin. API servisi Redis şifresine ihtiyaç duyuyorsa, veritabanı servisi Stripe API key’ine erişmemelidir.
  • Development ve production için aynı yapı: Geliştirme ortamında zayıf test şifresi kullanıyorsanız, bunu açıkça belirtmek için ayrı docker-compose.dev.yml ve docker-compose.prod.yml dosyaları kullanın.
  • Log’larda secret değerlerini yazdırmak: Uygulamanızın başlangıç loglarında bağlantı string’lerini tam olarak yazdırmayın. postgresql://user:*@host/db formatını tercih edin.

Secrets Yönetimi için Güvenlik Kontrol Listesi

Kurulumunuzu tamamladıktan sonra şu maddeleri kontrol edin:

  • secrets/ dizini .gitignore‘da bulunuyor mu?
  • Secret dosya izinleri 600 olarak ayarlı mı? (chmod 600 secrets/*.txt)
  • docker inspect çıktısında şifreler görünmüyor mu?
  • docker-compose.yml dosyasında düz metin şifre var mı?
  • CI/CD değişkenleri masked ve protected olarak işaretli mi?
  • Secret dosyaları image build context’inden hariç tutuluyor mu?
  • Periyodik rotasyon planı var mı?

Sonuç

Docker Compose Secrets, hassas veri yönetimi için minimum eforla maksimum güvenlik sağlayan bir mekanizma. Environment variable’ların yetersizliğinden kaynaklanan güvenlik açıklarını kapatıyor, secrets’ı container filesystem’ine salt okunur dosya olarak mount ediyor ve docker inspect gibi araçlardan gizliyor.

Özetlemek gerekirse: secret dosyalarınızı oluşturun, .gitignore ve .dockerignore‘a ekleyin, docker-compose.yml içinde secrets: bloğunu tanımlayın ve uygulamanızı /run/secrets/ yolundan okuyacak şekilde yapılandırın. Production’da Docker Swarm kullanıyorsanız external: true ile Swarm’ın yerleşik şifreli secret store’undan yararlanın.

Bu yöntemi benimsemek başta biraz zahmetli görünse de bir ekip büyüdükçe, repo’ya erişim sayısı arttıkça ve audit gereksinimleri çıktıkça bu küçük yatırımın ne kadar değerli olduğu anlaşılıyor. Güvenliği sonradan yamamamak için baştan doğru alışkanlıkları edinmek, uzun vadede hem teknik borcu hem de potansiyel güvenlik olaylarını önlüyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir