GitHub Secrets ile Güvenli Ortam Değişkeni Yönetimi

Production ortamında bir uygulamanın kaynak koduna bakıyorsunuz ve tam ortasında bir API anahtarı görmek… Bu his tanıdık geliyorsa, yalnız değilsiniz. GitHub’da her gün binlerce credential commit ediliyor ve bir kısmı saatler içinde kötü niyetli botlar tarafından tespit edilerek istismar ediliyor. GitHub Secrets bu problemi çözmek için var, ama doğru kullanmak birkaç nüans gerektiriyor.

GitHub Secrets Nedir, Ne Değildir

Önce zihinlerdeki yanılgıyı giderelim. GitHub Secrets bir “şifre kasası” değil, CI/CD pipeline’larınızda kullanmak üzere tasarlanmış ortam değişkeni yönetim sistemidir. HashiCorp Vault veya AWS Secrets Manager ile doğrudan rekabet etmez, farklı bir problemi çözer.

Temel özellikleri şunlar:

  • Şifrelenmiş depolama: Secrets, libsodium ile sealed box şifreleme kullanılarak depolanır. GitHub bile düz metin olarak okuyamaz
  • Otomatik maskeleme: Log çıktılarında secret değerleri * ile maskelenir
  • Kapsam yönetimi: Repository, environment ve organization seviyelerinde ayrı ayrı tanımlanabilir
  • Fork güvenliği: Fork’lanmış repo’larda pull request’ler secrets’a erişemez (default olarak)

Şunu açıkça söyleyelim: GitHub Secrets mükemmel değil. Değerleri sonradan görüntüleyemezsiniz, rotation mekanizması manuel, ve audit log konusunda kurumsal araçlara kıyasla sınırlı kalıyor. Ama ücretsiz, entegre ve çoğu senaryo için yeterince güvenli.

Secret Türlerini Anlamak

GitHub üç farklı seviyede secret tanımlamanıza izin veriyor.

Repository Secrets: Sadece o repository’deki workflow’lar tarafından erişilebilir. En yaygın kullanılan tür.

Environment Secrets: Specific bir deployment environment’ına bağlı. Production, staging, development gibi ortamlar için ayrı credential setleri yönetmek istediğinizde kullanın. Environment protection rules ile birleşince güçlü bir kontrol mekanizması oluşturuyor.

Organization Secrets: Birden fazla repository’de kullanılacak ortak credentials için. AWS hesap erişimi, Docker Hub kimlik bilgileri gibi şeyler.

Bu hiyerarşiyi anlamak önemli çünkü bir secret’ın hangi scope’ta tanımlandığı hem güvenlik hem de bakım kolaylığını etkiliyor.

CLI ile Secret Yönetimi

Web arayüzünden tıklayıp durmak yerine GitHub CLI kullanmak çok daha verimli. Özellikle onlarca secret yönetiyorsanız bu fark ciddi zaman kazandırır.

# GitHub CLI kurulumu (Ubuntu/Debian)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update && sudo apt install gh -y

# Authentication
gh auth login

# Secret oluşturma
gh secret set DATABASE_URL --body "postgresql://user:password@host:5432/dbname"

# Dosyadan secret okuma (uzun değerler için)
gh secret set SSL_PRIVATE_KEY < /path/to/private.key

# Environment-specific secret
gh secret set API_KEY --env production --body "prod-api-key-here"

# Mevcut secrets'ları listeleme
gh secret list
gh secret list --env production

Bir şeyi vurgulayayım: --body parametresini kullanırken terminal history’nize dikkat edin. Bash history’de credential görünmemesi için şunu kullanabilirsiniz:

# Bash history'ye yazmadan secret set etme
read -s SECRET_VALUE
gh secret set MY_SECRET --body "$SECRET_VALUE"
unset SECRET_VALUE

Workflow’da Secrets Kullanımı

Secrets’ı tanımladıktan sonra workflow YAML dosyanızda ${{ secrets.SECRET_NAME }} syntax’ıyla erişirsiniz.

name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Configure AWS credentials
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: us-east-1
        run: |
          aws s3 sync ./dist s3://my-bucket --delete
          aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} --paths "/*"
      
      - name: Deploy with SSH
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no [email protected] "cd /app && git pull && systemctl restart app"

Burada dikkat edilmesi gereken birkaç nokak var. environment: production satırını görüyor musunuz? Bu satır o job’ın production environment’ına bağlı secrets ve protection rules kullanacağını belirtiyor. Bu satır olmadan environment secrets’a erişemezsiniz.

Environment Protection Rules ile Gerçek Güvenlik

Secrets tek başına yeterli değil. Environment protection rules olmadan, repo’ya push yetkisi olan herkes production deployment tetikleyebilir. İşte burada işler kritikleşiyor.

Repository Settings > Environments > New environment yolunu izleyerek production environment’ınızı oluşturun ve şu kuralları ekleyin:

  • Required reviewers: Production deploy’larının onay gerektirmesi için reviewer ekleyin. Minimum bir kişi bile büyük fark yaratır
  • Wait timer: Deploy öncesi bekleme süresi tanımlayabilirsiniz. “Aceleyle yapılan deploy” sendromunun önüne geçer
  • Deployment branches: Sadece main branch’inden deploy izni verin
name: Production Deploy

on:
  push:
    branches: [main]

jobs:
  # Önce test et
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          npm ci
          npm test

  # Test geçtikten sonra, onay bekleyerek deploy et
  deploy:
    needs: test
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.example.com
    steps:
      - name: Deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
        run: |
          curl -X POST https://deploy.example.com/deploy 
            -H "Authorization: Bearer $DEPLOY_TOKEN" 
            -H "Content-Type: application/json" 
            -d '{"version": "${{ github.sha }}"}'

Gerçek Dünya Senaryosu: Multi-Environment Django Uygulaması

Birkaç yıl önce üzerinde çalıştığım bir Django projesinde şöyle bir yapı oluşturmuştum. Üç ortam var: development, staging, production. Her ortamın farklı database, farklı API key’leri ve farklı deployment hedefleri var.

name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: pip install -r requirements.txt
      
      - name: Run tests
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
          SECRET_KEY: test-secret-key-not-real
          DEBUG: "True"
        run: |
          python manage.py migrate
          python manage.py test --verbosity=2

  deploy-staging:
    needs: test
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to staging
        env:
          DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
          SECRET_KEY: ${{ secrets.STAGING_SECRET_KEY }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          # Deployment script burada
          ./scripts/deploy.sh staging

  deploy-production:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to production
        env:
          DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
          SECRET_KEY: ${{ secrets.PROD_SECRET_KEY }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          ./scripts/deploy.sh production

Burada SENTRY_DSN ve AWS credentials’ları organization secret olarak tanımladım, ortama özgü olanları ise environment secret olarak. Bu ayrım hem bakımı kolaylaştırıyor hem de “bu secret hangi ortama ait” sorusunu ortadan kaldırıyor.

Secret Rotation Stratejisi

GitHub Secrets’ın en büyük zaafiyetlerinden biri otomatik rotation olmaması. Bu konuda disiplinli olmak zorundayız.

#!/bin/bash
# rotate-secrets.sh - AWS credentials rotation örneği

REPO="myorg/myrepo"
AWS_USER="github-actions-user"

# Yeni access key oluştur
NEW_KEY=$(aws iam create-access-key --user-name $AWS_USER)
NEW_ACCESS_KEY=$(echo $NEW_KEY | jq -r '.AccessKey.AccessKeyId')
NEW_SECRET_KEY=$(echo $NEW_KEY | jq -r '.AccessKey.SecretAccessKey')

# GitHub'a yeni key'leri yükle
gh secret set AWS_ACCESS_KEY_ID --repo $REPO --body "$NEW_ACCESS_KEY"
gh secret set AWS_SECRET_ACCESS_KEY --repo $REPO --body "$NEW_SECRET_KEY"

# Kısa bir bekleme - eski key'i kullanan pipeline'ların tamamlanması için
sleep 30

# Eski key'leri listele ve sil (en yeni key hariç)
OLD_KEYS=$(aws iam list-access-keys --user-name $AWS_USER | 
  jq -r '.AccessKeyMetadata | sort_by(.CreateDate) | .[:-1] | .[].AccessKeyId')

for KEY_ID in $OLD_KEYS; do
  echo "Deleting old key: $KEY_ID"
  aws iam delete-access-key --user-name $AWS_USER --access-key-id $KEY_ID
done

echo "Rotation tamamlandi. Yeni key: $NEW_ACCESS_KEY"

Bu scripti bir cron job olarak ya da GitHub Actions’ın schedule trigger’ı ile 30-90 günde bir çalıştırabilirsiniz. Ama şunu söyleyeyim: rotation scriptini de test etmeden production’da kullanmayın. Bir keresinde rotation sırasında eski key’i fazla erken silerek birkaç saatlik deployment kesintisine yol açmıştım.

GitHub Secrets API ile Programatik Yönetim

Büyük ekiplerde veya infrastructure-as-code yaklaşımınızda secrets’ı Terraform veya Ansible ile yönetmek isteyebilirsiniz.

#!/usr/bin/env python3
# github_secrets.py - GitHub API ile secret yönetimi

import base64
import json
import os
import requests
from nacl import encoding, public

def get_repo_public_key(owner: str, repo: str, token: str) -> dict:
    """Repository'nin public key'ini getir."""
    url = f"https://api.github.com/repos/{owner}/{repo}/actions/secrets/public-key"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28"
    }
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()

def encrypt_secret(public_key_value: str, secret_value: str) -> str:
    """Secret değerini repository public key ile şifrele."""
    public_key = public.PublicKey(
        public_key_value.encode("utf-8"),
        encoding.Base64Encoder()
    )
    sealed_box = public.SealedBox(public_key)
    encrypted = sealed_box.encrypt(secret_value.encode("utf-8"))
    return base64.b64encode(encrypted).decode("utf-8")

def set_secret(owner: str, repo: str, secret_name: str, 
               secret_value: str, token: str) -> None:
    """Secret oluştur veya güncelle."""
    # Public key al
    key_data = get_repo_public_key(owner, repo, token)
    
    # Şifrele
    encrypted_value = encrypt_secret(key_data["key"], secret_value)
    
    # API'ye gönder
    url = f"https://api.github.com/repos/{owner}/{repo}/actions/secrets/{secret_name}"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28"
    }
    payload = {
        "encrypted_value": encrypted_value,
        "key_id": key_data["key_id"]
    }
    
    response = requests.put(url, headers=headers, json=payload)
    response.raise_for_status()
    print(f"Secret '{secret_name}' basariyla guncellendi.")

# Kullanim
if __name__ == "__main__":
    token = os.environ.get("GITHUB_TOKEN")
    
    secrets_to_set = {
        "DATABASE_URL": os.environ.get("PROD_DATABASE_URL"),
        "API_KEY": os.environ.get("SERVICE_API_KEY"),
    }
    
    for name, value in secrets_to_set.items():
        if value:
            set_secret("myorg", "myrepo", name, value, token)

Bu yaklaşım, secrets’ı bir HashiCorp Vault veya AWS SSM Parameter Store’dan çekip GitHub’a senkronize etmek için kullanışlı. Böylece “tek kaynak of truth” prensibi korunuyor.

Yaygın Hatalar ve Kaçınma Yolları

Yıllar içinde gördüğüm ve bizzat yaptığım hatalar:

Secrets’ı log’a yazdırmak: GitHub otomatik maskeleme yapıyor ama bu her zaman çalışmıyor. Base64 encode edilmiş bir secret maskelenmez. Şunu asla yapmayın:

# YANLIS - secret'i debug icin yazdirmayin
echo "Connecting with key: ${{ secrets.API_KEY }}"

# DOGRU - debug gerekiyorsa uzunluk veya hash kontrolü yapın
echo "API key length: ${#API_KEY}"
echo "API key prefix: ${API_KEY:0:4}..."

Tek bir “god secret” kullanmak: Tüm credential’ları tek bir JSON blob’u olarak tek secret’ta tutmak cazip görünüyor ama kötü bir pratik. Her servis için ayrı secret kullanın. Rotation ve audit açısından çok daha yönetilebilir.

Environment secrets yerine repository secret kullanmak: Production credential’larını ortam ayırt etmeksizin repository secret’a koymak, staging ortamından production’a yanlışlıkla deploy yapılması durumunda ciddi sorun yaratır.

Pull request’lerde secrets: Fork’tan gelen PR’lar secrets’a erişemez, bu güvenlik özelliği. Ama kendi fork’unuzdaki PR bile production secrets kullanmamalı. pull_request_target event’ini dikkatli kullanın, ciddi bir güvenlik açığı oluşturabilir.

OIDC ile Secrets’a Gerek Kalmayan Mimari

Bu kısmı atlamamak önemli: Uzun ömürlü credential’lardan kurtulmanın en iyi yolu onları hiç kullanmamak.

AWS, Azure ve GCP; GitHub Actions için OIDC (OpenID Connect) desteği sunuyor. Bununla, access key ve secret key yerine GitHub Actions’ın kısa ömürlü token’lar almasını sağlayabilirsiniz.

name: Deploy with OIDC

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: eu-west-1
      
      - name: Deploy
        run: |
          aws s3 sync ./dist s3://my-production-bucket
          aws ecs update-service --cluster prod --service my-app --force-new-deployment

Bu yaklaşımda AWS_ACCESS_KEY_ID ve AWS_SECRET_ACCESS_KEY secrets’larına hiç ihtiyaç yok. GitHub Actions, IAM role’ü assume etmek için geçici credential alıyor. Credential rotation problemi de ortadan kalkıyor.

AWS IAM tarafında şu Trust Policy gerekiyor:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*"
        }
      }
    }
  ]
}

Sonuç

GitHub Secrets, karmaşıklık ile güvenlik arasında makul bir denge kuruyor. Her ekip için mükemmel çözüm olmayabilir ama doğru kurgulandığında ciddi bir güvenlik katmanı sağlıyor.

Pratik tavsiyem şu: Environment secrets’ı kullanın ve production environment’ınıza mutlaka required reviewer ekleyin. OIDC destekleyen cloud provider’lar için uzun ömürlü credential saklamaktan kaçının. Rotation için bir takvim belirleyin ve bunu otomatize edin. Ve en önemlisi, secrets’ı log’a yazdırmaktan kaçının, maskeleme mekanizmasına güvenmeyin.

Secrets yönetimi bir kez yapılıp bırakılan bir şey değil. Periyodik audit, düzenli rotation ve “neden bu credential hala burada?” sorusunu sormak, güvenli bir pipeline’ın temel gereksinimleri.

Bir yanıt yazın

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