← Kursa Dön
📄 Text · 35 min

Secret Management — Secrets, Env Vars, Vault Entegrasyonu

Bu bölümün son dersine geldik. Şu ana kadar image güvenliğini, runtime güvenliğini öğrendik. Şimdi belki de en çok ihmal edilen ama en kritik konuya dalıyoruz: secret management — yani şifreler, API anahtarları, token'lar gibi hassas bilgilerin güvenli yönetimi.

Anahtarı Paspasın Altına Koymak

Evin anahtarını düşün. Anahtarı kapının altındaki paspasın altına koyarsan, "sakladım" diyebilirsin — ama güvende değil. Herkes bilir oraya bakacağını. Anahtarı cüzdanında taşırsan daha güvenli, ama cüzdanını kaybedebilirsin. Bir akıllı kilit kullanırsan — parmak izi veya PIN ile — fiziksel anahtar bile gerekmez ve güvenlik seviyesi çok daha yüksek.

Docker'da secret yönetimi de aynı mantıkta çalışıyor. Environment variable'a şifre koymak "paspasın altına anahtar" gibi — docker inspect ile herkes görebilir. Docker secrets ile dosya bazlı çalışmak cüzdanda taşımak gibi — daha güvenli ama hâlâ sınırlı. HashiCorp Vault gibi bir secret manager kullanmak ise akıllı kilit — en güvenli ve en profesyonel yöntem.

Secret Nedir?

Önce tanımlayalım. Secret, uygulamanın çalışması için gereken ama hiç kimsenin görmemesi gereken bilgilerdir:

  • Veritabanı şifreleri (POSTGRES_PASSWORD, MYSQL_ROOT_PASSWORD)

  • API anahtarları (STRIPE_SECRET_KEY, AWS_ACCESS_KEY)

  • JWT secret'ları (JWT_SECRET, SESSION_SECRET)

  • SSH private key'leri

  • TLS sertifikaları ve private key'leri

  • OAuth client secret'ları

  • Encryption key'leri

Bu bilgilerin herhangi birinin sızması, ciddi sonuçlara yol açabilir. 2019'da Capital One'da bir AWS credential sızıntısı 100 milyon kullanıcının verilerinin ele geçirilmesine neden oldu. GitHub'da her gün binlerce secret yanlışlıkla commit'leniyor — ve bot'lar bunları saniyeler içinde tespit edip kötüye kullanıyor. Bir AWS secret key'i GitHub'a push'ladığında, dakikalar içinde hesabında kripto miner'lar çalışmaya başlayabilir. Faturalar 10.000$'dan 100.000$'a kadar çıkabiliyor.

Yanlış Yöntemler: Bunları YAPMA!

Önce yanlış yöntemleri görelim ki neden yanlış olduklarını anlayalım.

Yanlış 1: Dockerfile'a Secret Gömmek

# ❌ EN KÖTÜ YOL: Secret image katmanlarına yazılır
ENV DATABASE_PASSWORD=supersecret123
ENV API_KEY=sk-1234567890abcdef

Bu neden tehlikeli? Çünkü docker history komutuyla herkes bu secret'ları görebilir:

docker history myapp:latest
# ... ENV DATABASE_PASSWORD=supersecret123

Image'ı registry'ye push ettiğinde, registry'ye erişimi olan herkes bu secret'ları okuyabilir. Image'ı birine gönderdiğinde, alan kişi docker history yapar ve tüm secret'lar orada.

Yanlış 2: Build Argument Kullanmak

# ❌ ARG da image history'de görünebilir
ARG DB_PASSWORD
ENV DATABASE_URL=postgres://user:${DB_PASSWORD}@db:5432/myapp

"ARG build sırasında kullanılır, image'da kalmaz" diye düşünebilirsin. Ama ENV ile kullandığın anda veya RUN komutunda kullandığın anda, o bilgi image katmanlarına yazılır.

Yanlış 3: docker run -e ile Açık Metin

# ❌ docker inspect ile görünür!
docker run -e DB_PASSWORD=supersecret myapp

# Herkes görebilir:
docker inspect myapp --format '{{.Config.Env}}'
# [DB_PASSWORD=supersecret NODE_ENV=production ...]

Bu yöntem en yaygın kullanılanı ve en riskli olanı. Sunucuya SSH erişimi olan herkes docker inspect yapabilir. Hatta process listesinde bile görünebilir:

ps aux | grep docker
# ... -e DB_PASSWORD=supersecret ...

Yanlış 4: .env Dosyasını Image'a COPY Etmek

# ❌ .env dosyası image'a gömülür
COPY .env /app/.env

Image katmanlarında .env dosyası kalır — docker cp ile çıkarılabilir.

Doğru Yöntem 1: .env + Docker Compose (Basit Projeler)

En basit güvenli yöntem. Secret'ları .env dosyasında tut ve .gitignore ile koru:

# .env dosyası (Git'e COMMIT'LENMEZ!)
DB_USER=appuser
DB_PASS=Kj8$mN2pQr5vX9
JWT_SECRET=a1b2c3d4e5f6g7h8i9j0
# .env.example (Git'e commit'lenir — template)
DB_USER=appuser
DB_PASS=CHANGE_ME
JWT_SECRET=CHANGE_ME
# .gitignore — çok önemli!
.env
.env.production
.env.staging
!.env.example

Docker Compose'da kullanımı:

services:
  api:
    build: ./api
    environment:
      DATABASE_URL: postgres://${DB_USER}:${DB_PASS}@db:5432/mydb
      JWT_SECRET: ${JWT_SECRET}

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}

Docker Compose otomatik olarak aynı dizindeki .env dosyasını okur ve variable'ları yerleştirir.

Bu yöntemin güvenlik seviyesi: ⭐⭐ (temel). Git'e commit'lenmez ama docker inspect ile hâlâ görünür. Kişisel projeler ve geliştirme ortamı için uygun.

Doğru Yöntem 2: Docker Secrets (Dosya Bazlı)

Docker secrets, hassas bilgileri dosya olarak container'a mount eder. Environment variable'dan daha güvenli çünkü docker inspect'te görünmez.

Önce secret dosyalarını oluşturalım:

mkdir -p secrets
echo -n "Kj8\$mN2pQr5vX9" > secrets/db_password.txt
echo -n "a1b2c3d4e5f6g7h8" > secrets/jwt_secret.txt

# Dosya izinlerini kısıtla — sadece sahip okuyabilsin
chmod 600 secrets/*

# .gitignore'a ekle
echo "secrets/" >> .gitignore

Docker Compose'da kullanımı:

services:
  api:
    build: ./api
    environment:
      DB_HOST: db
      DB_PORT: "5432"
      DB_NAME: myapp
    secrets:
      - db_password
      - jwt_secret

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

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

Dikkat ettin mi? PostgreSQL image'ında POSTGRES_PASSWORD yerine POSTGRES_PASSWORD_FILE kullanıyoruz. Bu, resmi PostgreSQL image'ının desteklediği bir özellik — secret'ı dosyadan okuyor. MySQL, Redis gibi birçok resmi image de bu _FILE suffix'ini destekliyor.

Peki uygulama tarafında secret'ı nasıl okuyacaksın? Dosyadan:

// Node.js — secret'ı dosyadan oku
const fs = require('fs');

function readSecret(name) {
    const secretPath = `/run/secrets/${name}`;
    try {
        return fs.readFileSync(secretPath, 'utf8').trim();
    } catch (err) {
        // Fallback: Swarm dışında çalışıyorsa ENV'den oku
        const envName = name.toUpperCase().replace(/-/g, '_');
        return process.env[envName];
    }
}

const dbPassword = readSecret('db_password');
const jwtSecret = readSecret('jwt_secret');
# Python — secret'ı dosyadan oku
import os

def read_secret(name: str) -> str:
    secret_path = f"/run/secrets/{name}"
    try:
        with open(secret_path, 'r') as f:
            return f.read().strip()
    except FileNotFoundError:
        env_name = name.upper().replace('-', '_')
        return os.environ.get(env_name, '')

db_password = read_secret('db_password')

Bu fonksiyonların güzel tarafı: önce /run/secrets/ altına bakar (production). Dosya yoksa environment variable'dan okur (development). Böylece aynı kod her iki ortamda da çalışır.

Güvenlik seviyesi: ⭐⭐⭐ (iyi). docker inspect'te görünmez, dosya olarak mount edilir. Ama secret dosyaları hâlâ host'ta açık metin.

Doğru Yöntem 3: BuildKit Secrets (Build Zamanı)

Build sırasında private registry'ye erişim, private package çekme gibi durumlarda secret gerekebilir. BuildKit secrets ile bunları güvenle kullanırsın:

# syntax=docker/dockerfile:1

FROM node:20-alpine
WORKDIR /app

# Private npm registry'den paket çekme
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci
# .npmrc BUILD SIRASINDA kullanılır ama image'a GÖMÜLMEZ!

COPY . .
RUN npm run build

CMD ["node", "dist/server.js"]

Build komutunda secret'ı geçiriyorsun:

docker build \
    --secret id=npmrc,src=$HOME/.npmrc \
    -t myapp:latest .

docker history ile baktığında hiçbir iz yok — secret image katmanlarında kalmaz.

Doğru Yöntem 4: HashiCorp Vault (Enterprise Grade)

Büyük projeler ve production ortamları için en iyi çözüm, merkezi bir secret manager kullanmak. HashiCorp Vault en popüler seçenek:

services:
  vault:
    image: hashicorp/vault:1.15
    ports:
      - "8200:8200"
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: dev-token
    cap_add:
      - IPC_LOCK
    volumes:
      - vault-data:/vault/data

  api:
    build: ./api
    environment:
      VAULT_ADDR: http://vault:8200
      VAULT_TOKEN: ${VAULT_TOKEN}

Vault'a secret yazmak:

vault kv put secret/myapp \
    db_password="Kj8\$mN2pQr5vX9" \
    jwt_secret="a1b2c3d4e5f6g7h8"

Uygulamadan okumak:

const vault = require('node-vault')({
    apiVersion: 'v1',
    endpoint: process.env.VAULT_ADDR,
    token: process.env.VAULT_TOKEN,
});

async function getSecrets() {
    const result = await vault.read('secret/data/myapp');
    return result.data.data;
}

Vault'un süper güçleri:

  • Merkezi yönetim: Tüm secret'lar tek yerde, şifreli

  • Erişim kontrolü: Kim hangi secret'a erişebilir, policy ile belirlenir

  • Audit log: Kim, ne zaman, hangi secret'a erişti — tam iz kaydı

  • Secret rotation: Şifreler otomatik olarak periyodik değiştirilebilir

  • Dynamic secrets: Geçici veritabanı credential'ları oluşturulabilir — 1 saat sonra otomatik expire olur

Güvenlik seviyesi: ⭐⭐⭐⭐⭐ (en iyi). Ama kurulum ve yönetimi karmaşık.

Secret Sızıntısını Önlemek: Pre-commit Hook'ları

En iyi güvenlik önlemi, secret'ın hiç sızmamasını sağlamak. Pre-commit hook'ları ile secret commit'lenmeden önce yakala:

# git-secrets kurulumu
git secrets --install
git secrets --register-aws

# Commit'te secret varsa ENGELLE
git commit -m "update config"
# [ERROR] secrets/db.env:3: DB_PASSWORD=supersecret123
# Commit BLOCKED ✗

TruffleHog ile tüm Git geçmişini tarayabilirsin:

docker run --rm -v $(pwd):/repo trufflesecurity/trufflehog:latest \
    git file:///repo --since-commit HEAD~50

Bu araç, geçmişte commit'lenmiş tüm secret'ları bulur — sonradan silinmiş olsalar bile.

Secret Git Geçmişine Sızmışsa Ne Yaparsın?

Diyelim ki bir secret yanlışlıkla commit'lendi. Sonra sildin ve yeni commit attın. Ama o secret hâlâ Git geçmişinde duruyor! Birisi git log ile geçmişe bakabilir ve secret'ı görebilir.

Çözüm iki adımlı:

Adım 1: Git geçmişinden temizle

# BFG Repo-Cleaner ile (hızlı ve güvenli)
bfg --delete-files .env
bfg --replace-text passwords.txt
git push --force

Adım 2: Secret'ı ROTATE et

Bu çok önemli — geçmişten silmek yetmez! O secret zaten sızmış olabilir. Eski şifreyi değiştir, yeni şifre belirle. Tüm servisleri yeni şifreyle güncelle.

Hangi Yöntemi Ne Zaman Kullanmalısın?

Kafan karışmış olabilir — bu kadar yöntem var, hangisini seçeceğim? İşte basit bir karar ağacı:

Kişisel proje / geliştirme ortamı?
  → .env + Docker Compose (Yöntem 1)

Küçük takım / staging?
  → Docker Secrets - dosya bazlı (Yöntem 2)

Production / enterprise?
  → HashiCorp Vault veya Cloud secret manager (Yöntem 4)

Build sırasında private erişim?
  → BuildKit secrets (Yöntem 3)

CI/CD'de Secret Yönetimi

Production'a deploy ederken CI/CD pipeline'ın da secret'lara erişmesi gerekiyor — registry şifresi, deploy server SSH key'i, veritabanı şifreleri... Bunları nasıl güvenle yöneteceksin?

GitHub Actions

GitHub Actions'ta secret'lar Settings → Secrets and variables → Actions altında saklanır. Pipeline içinden ${{ secrets.XXX }} ile erişilir:

# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Build and Push
        env:
          REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
        run: |
          echo "$REGISTRY_PASSWORD" | docker login -u user --password-stdin registry.example.com
          docker build -t registry.example.com/myapp:${{ github.sha }} .
          docker push registry.example.com/myapp:${{ github.sha }}

      - name: Deploy to Server
        env:
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
        run: |
          # Secret'ları geçici dosyalara yaz
          echo "$DB_PASSWORD" > /tmp/db_password
          echo "$JWT_SECRET" > /tmp/jwt_secret
          
          # Sunucuya gönder
          scp /tmp/db_password /tmp/jwt_secret deploy@server:/app/secrets/
          ssh deploy@server 'cd /app && docker compose up -d'
          
          # Geçici dosyaları temizle
          rm -f /tmp/db_password /tmp/jwt_secret

GitHub Actions loglarında secret'lar otomatik olarak *** ile maskelenir — yani echo $DB_PASSWORD yapsan bile logda görünmez. Bu güzel bir güvenlik önlemi.

GitLab CI

GitLab'da secret'lar Settings → CI/CD → Variables altında saklanır. "Masked" ve "Protected" seçenekleriyle ekstra güvenlik sağlanır:

deploy:
  stage: deploy
  script:
    - echo "$DB_PASSWORD" > secrets/db_password.txt
    - docker compose up -d
  after_script:
    - rm -rf secrets/    # Temizlik — pipeline bitince sil
  variables:
    DB_PASSWORD: $DB_PASSWORD

Cloud Provider Secret Manager'ları

Büyük ölçekli projelerde Vault yerine cloud provider'ın kendi secret manager'ını da kullanabilirsin:

AWS Secrets Manager:

# Secret oluştur
aws secretsmanager create-secret \
    --name myapp/production \
    --secret-string '{"db_password":"Kj8$mN2p","jwt_secret":"a1b2c3d4"}'

Uygulamanın başlangıcında AWS SDK ile secret'ı çekersin. Container'a sadece AWS credential'ları verirsin (veya daha iyisi, IAM role kullanırsın) — secret'ların kendisi environment'a gömülmez, runtime'da çekilir.

AWS ECS kullanıyorsan, secret injection native olarak desteklenir:

{
  "containerDefinitions": [{
    "secrets": [{
      "name": "DB_PASSWORD",
      "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456:secret:myapp/production:db_password::"
    }]
  }]
}

ECS otomatik olarak Secrets Manager'dan çeker ve environment variable olarak inject eder. Secret hiçbir zaman açık metin olarak bir yerde durmaz.

Azure Key Vault ve Google Cloud Secret Manager da benzer şekilde çalışır. Hangi cloud provider'daysan, onun secret manager'ını tercih et — integration'lar daha doğal ve kolay olur.

Secret Rotation: Şifreleri Düzenli Değiştirmek

Bir secret'ın ne kadar süre geçerli olması gerektiğini hiç düşündün mü? Çoğu projede veritabanı şifresi bir kere belirlenir ve yıllarca aynı kalır. Bu büyük bir risk — çünkü o şifre zamanla daha fazla kişinin eline geçer, daha fazla yerde kullanılır ve sızma ihtimali artar.

Secret rotation, şifreleri periyodik olarak otomatik değiştirmek demek. Vault ile bunu yapmak mümkün:

# Vault, PostgreSQL'e bağlanır ve geçici kullanıcı oluşturur
vault write database/roles/app-role \
    db_name=mydb \
    creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
    default_ttl="1h" \
    max_ttl="24h"

# Uygulama her seferinde geçici credential alır
vault read database/creds/app-role
# username: v-app-role-abc123
# password: A1b2C3d4E5f6G7h8
# lease_duration: 1h
# → 1 saat sonra otomatik expire olur!

Bu yaklaşımda kalıcı şifre yok — her seferinde geçici bir credential oluşturuluyor ve belirli süre sonra otomatik olarak geçersiz hale geliyor. Sızma olsa bile, sızan credential kısa sürede işe yaramaz hale gelir.

Yöntemlerin Karşılaştırması

Hangi yöntemin hangi durumda uygun olduğunu toparlayalım:

┌──────────────────┬─────────┬──────────────┬───────────┐
│  Yöntem          │ Güvenlik│ docker inspect│ Rotation  │
├──────────────────┼─────────┼──────────────┼───────────┤
│ Dockerfile ENV   │ ⭐      │ GÖRÜNÜR      │ Yok       │
│ docker run -e    │ ⭐⭐    │ GÖRÜNÜR      │ Manuel    │
│ .env + compose   │ ⭐⭐    │ GÖRÜNÜR      │ Manuel    │
│ Docker secrets   │ ⭐⭐⭐  │ Görünmez     │ Manuel    │
│ BuildKit secret  │ ⭐⭐⭐  │ N/A (build)  │ N/A       │
│ Vault/Cloud SM   │ ⭐⭐⭐⭐⭐│ Görünmez    │ Otomatik  │
└──────────────────┴─────────┴──────────────┴───────────┘

Her ortam için farklı secret kullanmayı da unutma. Development, staging ve production ortamlarında aynı şifreyi kullanmak, tek bir kırılma noktası yaratır. Dev ortamındaki bir sızıntı, production'ı da tehlikeye atar.

Secret'ları Asla Loglama!

Son bir uyarı — çok sık yapılan bir hata:

// ❌ ASLA yapma!
console.log(`Connecting with password: ${dbPassword}`);
console.log('Config:', JSON.stringify(config));
// config içinde secret olabilir!

// ✅ Doğru yaklaşım
console.log(`Connecting to ${dbHost}:${dbPort}/${dbName}`);
console.log('Config:', JSON.stringify(config, (key, value) => {
    if (['password', 'secret', 'key', 'token'].some(s => 
        key.toLowerCase().includes(s))) {
        return '***REDACTED***';
    }
    return value;
}));

Log'lar genellikle merkezi bir sisteme gönderilir, dosyalara yazılır, bazen üçüncü parti servislere iletilir. Secret'ı logladığın anda, kontrolün dışına çıkar.

Bu Derste Ne Öğrendik?

  • Environment variable ile secret geçmek en yaygın ama en az güvenli yöntem — docker inspect ile görünür

  • Docker secrets (dosya bazlı) daha güvenli — /run/secrets/ altında, inspect'te görünmez

  • BuildKit secrets ile build-time secret'lar güvenle kullanılır — image katmanlarına yazılmaz

  • HashiCorp Vault enterprise-grade çözüm — merkezi, şifreli, audit'li, rotation destekli

  • Pre-commit hook ile secret sızıntılarını commit öncesi yakala

  • Secret Git geçmişine sızmışsa, sadece silmek yetmez — mutlaka rotate et

  • Secret'ları asla loglama — bu en sık yapılan hatalardan biri

  • Her ortam (dev/staging/prod) farklı secret'lar kullanmalı

Bu dersle birlikte Docker güvenliği bölümünü tamamladık! Image güvenliği, runtime güvenliği ve secret management — bu üç katman Docker'ını production'da güvenle çalıştırmanın temelini oluşturuyor. Sonraki bölümde container orchestration dünyasına gireceğiz.