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
mainbranch’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.
