GitLab CI/CD Variables ile Güvenli Yapılandırma Yönetimi

Production ortamında bir GitLab pipeline’ı çalıştırırken veritabanı şifresi, API anahtarı veya deployment token’ı gibi hassas bilgileri .gitlab-ci.yml dosyasına gömmek, ne yazık ki hala yaygın görülen bir hata. Bu dosyalar genellikle repository’de açık şekilde duruyor ve ekipteki herkes, hatta bazen dışarıdan biri tarafından okunabiliyor. GitLab’ın CI/CD Variables özelliği tam da bu sorunu çözmek için var. Bu yazıda, GitLab CI/CD Variables’ı doğru şekilde nasıl kullanacağını, masked ve protected variable’ların farkını, scope yönetimini ve gerçek dünya senaryolarında nasıl entegre edeceğini ele alacağız.

CI/CD Variables Nedir ve Neden Önemlidir

GitLab CI/CD Variables, pipeline çalışırken erişilebilen key-value çiftleridir. Bunları kabaca iki kategoride düşünebilirsin: GitLab’ın kendi tanımladığı predefined variables (CI_COMMIT_BRANCH, CI_JOB_TOKEN gibi) ve senin tanımladığın custom variables. Bizi asıl ilgilendiren kısım custom variables.

Şifreler, API anahtarları, SSL sertifikaları, ortam bazlı konfigürasyonlar gibi hassas verileri kod içinde taşımak birkaç kritik risk yaratıyor:

  • Versiyon geçmişinde kalıcılık: Git history’den bir değeri silmek düşündüğünden çok daha zor. Birisi o commit’i checkout alırsa şifreye erişebilir.
  • Ekip içi aşırı erişim: Her geliştiricinin production şifresine ihtiyacı yok, ama .gitlab-ci.yml dosyasını herkes görebiliyor.
  • Rotation zorluğu: Şifreyi değiştirmen gerektiğinde tek bir yerden yapmak yerine kod içinde aramak zorunda kalırsın.
  • Audit trail eksikliği: Kim ne zaman hangi şifreye erişti? Variables kullanırsanız GitLab bunu logluyor.

Variable Türleri: Masked, Protected ve File

GitLab’da bir variable oluştururken karşına üç önemli seçenek çıkıyor.

Masked Variables, değerin job loglarında görünmesini engeller. Birisi echo $DB_PASSWORD yazsa bile log çıktısında [MASKED] görünür. Bu özelliği aktive etmek için variable değerinin en az 8 karakter olması ve Base64 veya alphanumeric karakterlerden oluşması gerekiyor. Bazı özel karakterler maskelemeyi bozuyor, buna dikkat et.

Protected Variables, sadece protected branch veya tag üzerinde çalışan job’lara görünür. Yani bir geliştirici feature branch’inde pipeline çalıştırıyorsa production credentials’larına erişemez. Bu, en temel güvenlik katmanlarından biri.

File Variables, değeri bir dosyaya yazar ve environment variable olarak o dosyanın path’ini verir. SSL sertifikası, kubeconfig veya benzeri dosya tabanlı konfigürasyonlar için idealdir.

Pratikte çoğu zaman hem Masked hem Protected seçeneklerini birlikte aktif etmek en mantıklı yaklaşım.

Variable Scope: Project, Group ve Instance Düzeyinde Yönetim

GitLab’da variable’ları üç farklı düzeyde tanımlayabilirsin.

Project-level variables, sadece o projeye ait pipeline’larda kullanılır. Settings > CI/CD > Variables menüsünden eklenir.

Group-level variables, gruba bağlı tüm projelere otomatik olarak aktarılır. Ortak kullanılan API anahtarları veya environment URL’leri için mükemmel. Group admin’inin yönettiği bu variables, her proje için ayrı ayrı tanımlama zahmetini ortadan kaldırır.

Instance-level variables, GitLab admin’i tarafından tanımlanır ve tüm instance’daki projelere uygulanır. Self-hosted GitLab kullanıyorsan ve şirket genelinde ortak bir konfigürasyon varsa bu seviyeyi kullanabilirsin.

Scope önceliği şu şekilde işler: Aynı isimde bir variable birden fazla düzeyde tanımlanmışsa, project-level olan group-level olanı ezer. Instance-level ise en düşük önceliğe sahip.

Ortam Bazlı Variable Yönetimi

GitLab, environment scope özelliğiyle aynı variable’ı farklı ortamlar için farklı değerlerle tanımlamana olanak tanıyor. Örneğin DATABASE_URL variable’ını hem staging hem production için ayrı ayrı tanımlayabilirsin.

Settings > CI/CD > Variables bölümünde bir variable eklerken “Environment scope” alanına staging veya production yazabilirsin. Wildcard olarak * kullanmak, o variable’ı tüm ortamlara uygulanır.

# .gitlab-ci.yml
deploy_staging:
  stage: deploy
  environment:
    name: staging
  script:
    - echo "Staging DB URL: $DATABASE_URL"
    - ./deploy.sh

deploy_production:
  stage: deploy
  environment:
    name: production
  script:
    - echo "Production deploy basliyor..."
    - ./deploy.sh
  only:
    - main

Bu yapıda deploy_staging job’u staging scope’undaki DATABASE_URL değerini alırken, deploy_production job’u production scope’undaki değeri alır. Kod tamamen aynı, sadece GitLab variables scope’u işi hallediyor.

Gerçek Dünya Senaryosu 1: Docker Registry Authentication

Diyelim ki private bir Docker registry kullanıyorsunuz. Credentials’ı pipeline içinde güvenli şekilde kullanmak için şu yaklaşımı kullanabilirsin.

Önce GitLab’da şu variable’ları tanımla (ikisi de Masked ve Protected):

  • REGISTRY_USER: Registry kullanıcı adı
  • REGISTRY_PASSWORD: Registry şifresi
  • REGISTRY_URL: Registry URL’i (bu masked olmak zorunda değil)
# .gitlab-ci.yml
stages:
  - build
  - push

variables:
  IMAGE_NAME: myapp
  IMAGE_TAG: $CI_COMMIT_SHORT_SHA

build_image:
  stage: build
  image: docker:24.0
  services:
    - docker:24.0-dind
  before_script:
    - echo "$REGISTRY_PASSWORD" | docker login $REGISTRY_URL -u $REGISTRY_USER --password-stdin
  script:
    - docker build -t $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG .
    - docker build -t $REGISTRY_URL/$IMAGE_NAME:latest .
  after_script:
    - docker logout $REGISTRY_URL

push_image:
  stage: push
  image: docker:24.0
  services:
    - docker:24.0-dind
  before_script:
    - echo "$REGISTRY_PASSWORD" | docker login $REGISTRY_URL -u $REGISTRY_USER --password-stdin
  script:
    - docker push $REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG
    - docker push $REGISTRY_URL/$IMAGE_NAME:latest
  only:
    - main
    - develop

--password-stdin kullanımına dikkat et. Şifreyi doğrudan komut satırında argüman olarak vermek (-p $REGISTRY_PASSWORD gibi) process listesinde görünür kılar. Stdin üzerinden geçirmek çok daha güvenli.

Gerçek Dünya Senaryosu 2: Kubernetes Deployment

Kubernetes’e deployment yapıyorsan, kubeconfig dosyasını File variable olarak saklamak en iyi pratik.

GitLab’da KUBECONFIG_CONTENT adında bir variable oluştur, türünü File olarak seç ve kubeconfig içeriğini yapıştır. GitLab bu içeriği otomatik olarak bir temp dosyaya yazacak ve $KUBECONFIG_CONTENT değişkeni o dosyanın path’ini tutacak.

# .gitlab-ci.yml
deploy_k8s:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: production
  script:
    - export KUBECONFIG=$KUBECONFIG_CONTENT
    - kubectl config get-contexts
    - kubectl set image deployment/myapp myapp=$REGISTRY_URL/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA -n production
    - kubectl rollout status deployment/myapp -n production --timeout=300s
  only:
    - main

Bu yaklaşımda kubeconfig içeriği asla log’a düşmüyor, geçici bir dosyada tutuluyor ve job bitince temizleniyor.

Gerçek Dünya Senaryosu 3: Veritabanı Migration

Database migration işlemleri sırasında connection string’i güvenli şekilde yönetmek kritik. Özellikle staging ve production için farklı değerler kullanıyorsan environment scope tam burada işe yarıyor.

# .gitlab-ci.yml
stages:
  - test
  - migrate
  - deploy

run_migrations_staging:
  stage: migrate
  image: python:3.11
  environment:
    name: staging
  before_script:
    - pip install -r requirements.txt
  script:
    - echo "Migration basliyor, ortam: staging"
    - python manage.py migrate --verbosity=1
  variables:
    DJANGO_SETTINGS_MODULE: config.settings.staging
  only:
    - develop

run_migrations_production:
  stage: migrate
  image: python:3.11
  environment:
    name: production
  before_script:
    - pip install -r requirements.txt
  script:
    - echo "Production migration basliyor..."
    - python manage.py migrate --verbosity=1
  variables:
    DJANGO_SETTINGS_MODULE: config.settings.production
  when: manual
  only:
    - main

DATABASE_URL variable’ı GitLab tarafından staging veya production scope’una göre otomatik inject ediliyor. Python uygulaması os.environ.get('DATABASE_URL') ile bu değere ulaşıyor. Kodda tek satır credentials yok.

Variable’ları .gitlab-ci.yml’de Doğru Kullanmak

Variable kullanımında birkaç pratik detay var.

Inline tanımlama ile global variable’ı geçici ezme:

deploy_hotfix:
  stage: deploy
  variables:
    DEPLOY_TIMEOUT: "600"
    LOG_LEVEL: "debug"
  script:
    - ./deploy.sh --timeout $DEPLOY_TIMEOUT --log-level $LOG_LEVEL

Bu job’daki DEPLOY_TIMEOUT ve LOG_LEVEL sadece bu job scope’unda geçerli. Sensitive olmayan, operasyonel değerler için bu yaklaşım tamam. Ama sensitive değerleri asla buraya yazma.

Variable expansion (değişken içinde değişken):

variables:
  BASE_URL: "https://api.example.com"
  API_ENDPOINT: "$BASE_URL/v2/users"

GitLab, $API_ENDPOINT değerini expand ederek https://api.example.com/v2/users şeklinde kullanır. Bu özelliği ortam bazlı URL’ler oluştururken verimli kullanabilirsin.

Shell variable ile GitLab variable karıştırma:

# Script içinde
export COMPUTED_VALUE="${CI_PROJECT_NAME}-${CI_COMMIT_SHORT_SHA}-${ENVIRONMENT_NAME}"
echo "Build tag: $COMPUTED_VALUE"

Predefined variables ile kendi tanımladığın variable’ları birleştirerek dinamik değerler üretebilirsin.

API ile Variable Yönetimi

Bazen variable’ları GitLab UI’dan değil, programatik olarak yönetmek gerekir. GitLab API bunu destekliyor. Özellikle CI/CD infrastructure’ını kod olarak yönetiyorsan (Infrastructure as Code yaklaşımı) bu çok işe yarıyor.

# Yeni variable ekle
curl --request POST 
  --header "PRIVATE-TOKEN: $GITLAB_PERSONAL_TOKEN" 
  --header "Content-Type: application/json" 
  --data '{
    "key": "DATABASE_URL",
    "value": "postgresql://user:pass@host:5432/dbname",
    "protected": true,
    "masked": true,
    "environment_scope": "production"
  }' 
  "https://gitlab.example.com/api/v4/projects/PROJECT_ID/variables"

# Mevcut variable'ı guncelle
curl --request PUT 
  --header "PRIVATE-TOKEN: $GITLAB_PERSONAL_TOKEN" 
  --header "Content-Type: application/json" 
  --data '{"value": "yeni-deger"}' 
  "https://gitlab.example.com/api/v4/projects/PROJECT_ID/variables/DATABASE_URL"

# Tum variable'lari listele
curl --header "PRIVATE-TOKEN: $GITLAB_PERSONAL_TOKEN" 
  "https://gitlab.example.com/api/v4/projects/PROJECT_ID/variables"

Bu API’yi bir shell script ile sarıp variable rotation işlemini otomatize edebilirsin. Örneğin 90 günde bir API anahtarlarını otomatik rotate eden bir cron job yazabilirsin.

Vault Entegrasyonu ile Gelişmiş Güvenlik

GitLab Premium ve Ultimate kullanıcıları için HashiCorp Vault entegrasyonu mevcut. Bu entegrasyonla variable’ları GitLab’da değil, Vault’ta saklayabilirsin. GitLab job’ları JWT authentication kullanarak Vault’tan secrets çekiyor.

# .gitlab-ci.yml - Vault entegrasyonu
job_with_vault:
  stage: deploy
  secrets:
    DATABASE_PASSWORD:
      vault: production/db/password@ops-vault
      file: false
    API_KEY:
      vault: production/services/external-api/key@ops-vault
      file: false
  script:
    - echo "Vault'tan alinan secret kullaniliyor..."
    - ./deploy.sh

Bu yaklaşımda secrets GitLab’da hiç saklanmıyor. Job çalışırken Vault’tan JWT ile doğrulanıp çekiliyor. Rotation tamamen Vault tarafında yönetiliyor. Enterprise ortamlarda önerilen yaklaşım bu.

Community edition kullanıyorsan alternatif olarak vault CLI’yı script içinde kullanabilirsin, sadece Vault token’ını veya AppRole credentials’ını GitLab variable olarak saklaman yeterli.

Güvenlik En İyi Pratikleri

Variable yönetiminde dikkat etmen gereken noktaları özetleyelim.

Debug modunu production’da kapatmak: GitLab CI’da CI_DEBUG_TRACE: "true" ayarı tüm değişkenleri loglara döker. Masked variable’lar bile bu modda görünür hale gelir. Bu ayarı sadece gerçekten debug yapman gerektiğinde ve non-production ortamlarda aç.

Variable isimlerini tutarlı tutmak: Bir naming convention belirle ve ona uyu. Örneğin PROD_DB_PASSWORD, STAGING_DB_PASSWORD gibi prefix kullanmak yerine environment scope mekanizmasını kullan ve her ikisine de DB_PASSWORD de. Bu hem kod okunabilirliğini artırır hem de yanlışlıkla staging değerini production’da kullanma riskini azaltır.

Minimum yetki prensibi: Her variable’ı ihtiyaç duyan job’lara sınırla. Group-level variable’ları gereğinden fazla kullanmaktan kaçın. Eğer bir secret sadece tek projeye aitse, group-level’da tanımlama.

Düzenli rotation: Özellikle otomatik deployment yapan service account token’larını ve API anahtarlarını düzenli olarak rotate et. GitLab API’sini kullanarak bu işlemi otomatize edebilirsin.

Audit loglarını takip et: GitLab, variable değişikliklerini audit log’a yazıyor. Settings > Audit Events bölümünden kim ne zaman hangi variable’ı değiştirdi görebilirsin. Bu özelliği düzenli gözden geçir.

# Bir job icinde mevcut environment variable'lari debug etmek icin
# (sadece gelistirme ortaminda, production'da ASLA kullanma)
debug_variables:
  stage: .pre
  script:
    - env | grep -v "PASSWORD|SECRET|TOKEN|KEY" | sort
  when: manual
  only:
    - develop

Bu script, hassas değerleri filtreleyen basit bir debug job’u. Sadece develop branch’inde ve manual tetiklendiğinde çalışıyor.

Sık Yapılan Hatalar

Hata 1: Variable değerini commit mesajına veya log’a yazmak. echo "Sunucuya $DB_PASSWORD ile baglaniliyor" yazmak log’da masked gösterir, ama echo "Sunucuya postgres://user:$DB_PASSWORD@host/db ile baglaniliyor" şeklinde connection string içine gömerseniz maskeleme devre dışı kalabilir. Variable değerini hiçbir zaman string içinde parçalara ayırma.

Hata 2: .env dosyasını repository’e commit etmek. .env dosyasını .gitignore‘a eklemek unutuluyor ve şifreler repository’e giriyor. .env.example dosyası oluştur, gerçek değerleri orada bırakma.

Hata 3: Shared runner’larda hassas variable’ları kullanmak. GitLab.com’un shared runner’ları kullanıyorsan ve çok hassas bir secret varsa, dedicated runner kullanmayı düşün. Shared runner’lar multiple tenant ortamında çalışıyor.

Hata 4: Variable scope’unu unutmak. Protected variable tanımladın ama branch protected değil. Job çalışıyor ama variable boş geliyor. Bu çok zaman kaybettiren bir hata. Variable oluştururken environment scope ve protected ayarlarını dikkatlice kontrol et.

Sonuç

GitLab CI/CD Variables, pipeline güvenliğinin temel taşlarından biri. Masked ve protected variable kombinasyonu, environment scope yönetimi ve File variable tipi bir arada kullanıldığında gerçekten sağlam bir güvenlik katmanı oluşturuyor. Ancak bu mekanizmaların işe yaraması için takımın bu pratiği benimsemesi gerekiyor. Kod review süreçlerine “variable dosyaya gömülmüş mü?” kontrolü eklemek, yeni ekip üyelerine bu yaklaşımı anlatmak ve otomatik secret scanning tool’larını (GitLab’ın built-in Secret Detection özelliği dahil) aktif tutmak şart.

Başlangıç noktası olarak şunu öneririm: Mevcut pipeline’larını tara, hardcoded olan tüm değerleri çıkar, GitLab Variables’a taşı ve branch protection kurallarını gözden geçir. Bu üç adım bile büyük çoğunluk için ciddi bir güvenlik iyileştirmesi sağlıyor. Geri kalanı zaman içinde olgunlaşır.

Bir yanıt yazın

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