Jenkins’te Credential Yönetimi: Güvenli Değişkenler ve Şifreler

CI/CD pipeline’larında en sık yapılan hatalardan biri, şifreleri ve API tokenlarını doğrudan Jenkinsfile içine yazmak. “Şimdilik böyle kalsın, sonra düzeltirim” diye başlayan şeyler production’da aylar boyunca öyle kalıyor. Git geçmişine gömülmüş bir AWS secret key ya da database şifresi, şirketi ciddi güvenlik açıklarına maruz bırakabilir. Bu yazıda Jenkins’in credential yönetim sistemini A’dan Z’ye ele alacağız; ne yapmamanız gerektiğini de, doğru yapmanın nasıl göründüğünü de.

Neden Credential Yönetimi Bu Kadar Önemli

Geçen yıl bir müşterinin Jenkins ortamını inceliyordum. Jenkinsfile’a bakınca şunu gördüm:

environment {
    DB_PASSWORD = "super_secret_123"
    AWS_KEY = "AKIAIOSFODNN7EXAMPLE"
    AWS_SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

Bu kod üç yıldır production’da çalışıyordu ve Git geçmişinde sonsuza dek kayıtlıydı. Dosyayı silseniz bile git log ile erişilebilir durumda. İşte bu yüzden Jenkins’in credential store’unu kullanmak bir tercih değil, zorunluluk.

Saldırganlar için Git repolarına sızmak, özellikle iç ağlarda, düşündüğünüzden çok daha kolay. Üstelik Jenkins build logları da risk taşıyor; yanlış yapılandırılmış bir log, şifreyi açıkça ekrana basabilir.

Jenkins Credential Store’un Temelleri

Jenkins, credential’ları hiyerarşik bir yapıda saklar. Bu yapıyı anlamak, doğru yerde doğru credential’ı oluşturmanıza yardımcı olur.

Scope Seviyeleri

Global Scope: Tüm job’lar ve pipeline’lar tarafından erişilebilir. Jenkins instance’ının geneline hizmet eden credential’lar için kullanılır.

System Scope: Sadece Jenkins’in kendisi ve plugin’ler tarafından kullanılabilir. Job’lar bu scope’taki credential’lara doğrudan erişemez. Slave agent bağlantıları için idealdir.

Folder Scope: Belirli bir klasör ve alt klasörlerindeki job’lar için geçerlidir. Ekipler arası izolasyon sağlamak istediğinizde çok işe yarar.

Desteklenen Credential Tipleri

  • Username with password: Klasik kullanıcı adı/şifre kombinasyonu. Git, SVN, registry erişimleri için.
  • SSH Username with private key: SSH key pair authentication için. Agent bağlantıları, sunucu erişimleri.
  • Secret text: Tek bir gizli string. API token, secret key gibi değerler.
  • Secret file: Bir dosyanın tamamını şifreli saklamak için. kubeconfig, p12 sertifikası gibi.
  • Certificate: X.509 sertifikası ve private key kombinasyonu.
  • GitHub App: GitHub App authentication için özel tip.

Credential Oluşturma: Arayüzden

Önce GUI üzerinden anlatayım, ardından script ile yapacağız.

Jenkins ana sayfasından Manage Jenkins > Credentials > System > Global credentials (unrestricted) yolunu izleyin. Add Credentials butonuna tıklayın.

Bir Docker Hub credential’ı oluşturduğumuzu düşünelim:

  • Kind: Username with password
  • Scope: Global
  • Username: mycompany-ci
  • Password: (Docker Hub access token)
  • ID: dockerhub-credentials (bu ID’yi Jenkinsfile’da kullanacaksınız)
  • Description: Docker Hub CI User – Production Registry

ID alanına dikkat edin. Otomatik UUID yerine anlamlı bir isim verin; dockerhub-credentials, aws-prod-keys, github-deploy-key gibi. Bu isimler Jenkinsfile’da referans olarak kullanılacak ve okunabilirliği doğrudan etkiliyor.

Credential Oluşturma: Groovy Script ile

Özellikle Jenkins’i kod olarak yönetiyorsanız (Jenkins as Code), credential’ları script ile oluşturmak çok daha pratik. Jenkins Script Console üzerinden ya da JCasC ile yapabilirsiniz.

# Jenkins Script Console'a erişmek için:
# Manage Jenkins > Script Console
import com.cloudbees.plugins.credentials.impl.*
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.domains.*
import hudson.util.Secret

def credentialsStore = Jenkins.instance.getExtensionList(
    'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()

def credentials = new UsernamePasswordCredentialsImpl(
    CredentialsScope.GLOBAL,
    'dockerhub-credentials',
    'Docker Hub CI User - Production Registry',
    'mycompany-ci',
    'docker_hub_token_here'
)

credentialsStore.addCredentials(Domain.global(), credentials)
println "Credential oluşturuldu."

Secret text tipi için:

import com.cloudbees.plugins.credentials.impl.*
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.domains.*
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl
import hudson.util.Secret

def credentialsStore = Jenkins.instance.getExtensionList(
    'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()

def secretCredential = new StringCredentialsImpl(
    CredentialsScope.GLOBAL,
    'sonarqube-token',
    'SonarQube Analysis Token - Production',
    Secret.fromString('sqa_abc123def456xyz789')
)

credentialsStore.addCredentials(Domain.global(), secretCredential)
println "Secret text credential oluşturuldu."

Jenkinsfile’da Credential Kullanımı

Şimdi asıl meseleye gelelim: credential’ları pipeline’da güvenli biçimde nasıl kullanırız?

withCredentials Bloğu

En esnek ve güvenli yöntem withCredentials bloğudur. Credential yalnızca bu blok içinde açık hale gelir ve blok bitince bellekten temizlenir.

pipeline {
    agent any
    
    stages {
        stage('Docker Build & Push') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'dockerhub-credentials',
                    usernameVariable: 'DOCKER_USER',
                    passwordVariable: 'DOCKER_PASS'
                )]) {
                    sh '''
                        echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin
                        docker build -t myapp:${BUILD_NUMBER} .
                        docker push mycompany/myapp:${BUILD_NUMBER}
                        docker logout
                    '''
                }
            }
        }
    }
}

Dikkat edin: echo "$DOCKER_PASS" yazdım ama bu sadece docker login --password-stdin için gerekli. Jenkins bu değişkenleri log’a basmaz; log’da görseydiniz ** olarak maskelenmiş görürdünüz.

Environment Blok ile Credential Bağlama

Credential’ı tüm pipeline boyunca environment variable olarak kullanmak istiyorsanız:

pipeline {
    agent any
    
    environment {
        SONAR_TOKEN = credentials('sonarqube-token')
        AWS_CREDS = credentials('aws-prod-credentials')
    }
    
    stages {
        stage('Code Analysis') {
            steps {
                sh '''
                    sonar-scanner 
                        -Dsonar.login=${SONAR_TOKEN} 
                        -Dsonar.projectKey=myapp 
                        -Dsonar.sources=src
                '''
            }
        }
        
        stage('Deploy to AWS') {
            steps {
                sh '''
                    aws s3 sync ./dist s3://myapp-bucket 
                        --region eu-west-1
                '''
            }
        }
    }
}

UsernamePassword tipinde bir credential’ı environment’a bağladığınızda Jenkins otomatik olarak iki değişken oluşturur: AWS_CREDS_USR (kullanıcı adı) ve AWS_CREDS_PSW (şifre).

SSH Key Kullanımı

Deployment server’lara SSH ile bağlanan pipeline’lar çok yaygın. Doğru yapısı şöyle:

pipeline {
    agent any
    
    stages {
        stage('Deploy to Production') {
            steps {
                withCredentials([sshUserPrivateKey(
                    credentialsId: 'prod-server-ssh-key',
                    keyFileVariable: 'SSH_KEY_FILE',
                    usernameVariable: 'SSH_USER'
                )]) {
                    sh '''
                        ssh -i "$SSH_KEY_FILE" 
                            -o StrictHostKeyChecking=no 
                            -o UserKnownHostsFile=/dev/null 
                            ${SSH_USER}@prod-server.internal 
                            "cd /var/www/myapp && git pull && systemctl restart myapp"
                    '''
                }
            }
        }
    }
}

SSH key’in geçici bir dosyaya yazıldığını ve $SSH_KEY_FILE ile erişilebildiğini görüyorsunuz. Blok bitince bu geçici dosya otomatik silinir.

Secret File Kullanımı

Kubernetes deployment için kubeconfig dosyasını credential olarak sakladığımız bir senaryo:

pipeline {
    agent any
    
    stages {
        stage('K8s Deploy') {
            steps {
                withCredentials([file(
                    credentialsId: 'k8s-prod-kubeconfig',
                    variable: 'KUBECONFIG_FILE'
                )]) {
                    sh '''
                        export KUBECONFIG="$KUBECONFIG_FILE"
                        kubectl set image deployment/myapp 
                            myapp=mycompany/myapp:${BUILD_NUMBER} 
                            -n production
                        kubectl rollout status deployment/myapp -n production
                    '''
                }
            }
        }
    }
}

Gerçek Dünya Senaryosu: Multi-Stage Pipeline

Bir e-ticaret projesinin tam deployment pipeline’ını düşünelim. Bu pipeline Docker build, test, staging deploy ve production deploy aşamalarını kapsıyor:

pipeline {
    agent any
    
    environment {
        APP_NAME = 'ecommerce-api'
        DOCKER_REGISTRY = 'registry.company.internal'
        REGISTRY_CREDS = credentials('internal-registry-creds')
    }
    
    stages {
        stage('Build') {
            steps {
                sh '''
                    docker build 
                        -t ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} 
                        -t ${DOCKER_REGISTRY}/${APP_NAME}:latest 
                        .
                '''
            }
        }
        
        stage('Test') {
            steps {
                withCredentials([
                    usernamePassword(
                        credentialsId: 'test-db-credentials',
                        usernameVariable: 'DB_USER',
                        passwordVariable: 'DB_PASS'
                    ),
                    string(
                        credentialsId: 'stripe-test-key',
                        variable: 'STRIPE_KEY'
                    )
                ]) {
                    sh '''
                        docker run --rm 
                            -e DB_HOST=test-db.internal 
                            -e DB_USER="${DB_USER}" 
                            -e DB_PASSWORD="${DB_PASS}" 
                            -e STRIPE_SECRET_KEY="${STRIPE_KEY}" 
                            ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} 
                            npm test
                    '''
                }
            }
        }
        
        stage('Push to Registry') {
            steps {
                sh '''
                    echo "${REGISTRY_CREDS_PSW}" | 
                        docker login ${DOCKER_REGISTRY} 
                        -u "${REGISTRY_CREDS_USR}" 
                        --password-stdin
                    docker push ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER}
                    docker push ${DOCKER_REGISTRY}/${APP_NAME}:latest
                    docker logout ${DOCKER_REGISTRY}
                '''
            }
        }
        
        stage('Deploy Staging') {
            steps {
                withCredentials([sshUserPrivateKey(
                    credentialsId: 'staging-deploy-key',
                    keyFileVariable: 'DEPLOY_KEY',
                    usernameVariable: 'DEPLOY_USER'
                )]) {
                    sh '''
                        ssh -i "$DEPLOY_KEY" 
                            -o StrictHostKeyChecking=no 
                            ${DEPLOY_USER}@staging.company.internal 
                            "docker pull ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} && 
                             docker-compose up -d --no-deps app"
                    '''
                }
            }
        }
        
        stage('Deploy Production') {
            when {
                branch 'main'
            }
            input {
                message "Production'a deploy edilsin mi?"
                ok "Deploy Et"
            }
            steps {
                withCredentials([sshUserPrivateKey(
                    credentialsId: 'prod-deploy-key',
                    keyFileVariable: 'DEPLOY_KEY',
                    usernameVariable: 'DEPLOY_USER'
                )]) {
                    sh '''
                        ssh -i "$DEPLOY_KEY" 
                            -o StrictHostKeyChecking=no 
                            ${DEPLOY_USER}@prod.company.internal 
                            "docker pull ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} && 
                             docker-compose up -d --no-deps app && 
                             docker system prune -f"
                    '''
                }
            }
        }
    }
    
    post {
        failure {
            withCredentials([string(
                credentialsId: 'slack-webhook-url',
                variable: 'SLACK_WEBHOOK'
            )]) {
                sh '''
                    curl -s -X POST ${SLACK_WEBHOOK} 
                        -H "Content-type: application/json" 
                        -d "{"text": "Build ${BUILD_NUMBER} FAILED: ${JOB_NAME}"}"
                '''
            }
        }
    }
}

Bu pipeline’da dikkat edin: her credential farklı bir aşamada, ihtiyaç duyulduğu anda açılıyor. Test DB credential’ı production stage’inde görünmüyor bile.

Credential Maskeleme ve Log Güvenliği

Jenkins varsayılan olarak credential değerlerini log’larda ** ile maskeler. Ama bunu atlatabilecek durumlar var:

Tehlikeli kullanım örnekleri:

# YAPMAYIN - credential'ı base64 encode edince maskeleme çalışmaz
echo -n "$SECRET" | base64

# YAPMAYIN - dosyaya yazip cat'lerseniz maskelenmez
echo "$SECRET" > /tmp/secret.txt
cat /tmp/secret.txt

# YAPMAYIN - hata mesajında credential sızabilir
curl -u "$USER:$PASS" https://api.example.com || echo "Hata: $PASS yanlış"

Güvenli alternatifler:

# Pipe ile doğrudan kullanın, dosyaya yazmayın
echo "$SECRET" | base64 | tr -d 'n'

# Hata loglarını dikkatli filtreleyin
curl -s -u "$USER:$PASS" https://api.example.com 
    -w "%{http_code}" -o /dev/null

Jenkins Configuration as Code ile Credential Yönetimi

Jenkins’i JCasC (Jenkins Configuration as Code) ile yönetiyorsanız, credential’ları YAML’da tanımlayabilirsiniz. Ancak burada kritik nokta: YAML dosyasını Git’e ATMAYACAKSINIZ. Bunun yerine environment variable referansı kullanın:

credentials:
  system:
    domainCredentials:
      - credentials:
          - usernamePassword:
              scope: GLOBAL
              id: "dockerhub-credentials"
              description: "Docker Hub CI User"
              username: "${DOCKERHUB_USER}"
              password: "${DOCKERHUB_PASSWORD}"
          - string:
              scope: GLOBAL
              id: "sonarqube-token"
              description: "SonarQube Analysis Token"
              secret: "${SONARQUBE_TOKEN}"
          - basicSSHUserPrivateKey:
              scope: GLOBAL
              id: "prod-deploy-key"
              description: "Production Server Deploy Key"
              username: "deploy"
              privateKeySource:
                directEntry:
                  privateKey: "${PROD_SSH_PRIVATE_KEY}"

Bu YAML’daki değerler Jenkins başlarken environment variable’lardan okunur. Jenkins’i Docker ile çalıştırıyorsanız:

docker run -d 
    --name jenkins 
    -p 8080:8080 
    -e CASC_JENKINS_CONFIG=/var/jenkins_home/casc.yaml 
    -e DOCKERHUB_USER="mycompany-ci" 
    -e DOCKERHUB_PASSWORD="$(cat /run/secrets/dockerhub_pass)" 
    -e SONARQUBE_TOKEN="$(cat /run/secrets/sonar_token)" 
    -e PROD_SSH_PRIVATE_KEY="$(cat /run/secrets/deploy_key)" 
    jenkins/jenkins:lts

Bu yaklaşımda secret değerler Docker secrets ya da HashiCorp Vault’tan okunuyor ve environment’a enjekte ediliyor. YAML dosyası Git’te kalabilir, çünkü sadece referansları içeriyor.

HashiCorp Vault Entegrasyonu

Kurumsal ortamlarda Vault entegrasyonu sık tercih edilen bir yaklaşım. Jenkins Vault Plugin ile credential’ları doğrudan Vault’tan çekebilirsiniz:

pipeline {
    agent any
    
    stages {
        stage('Vault ile Deploy') {
            steps {
                withVault(
                    configuration: [
                        vaultUrl: 'https://vault.company.internal',
                        vaultCredentialId: 'vault-app-role-credentials',
                        engineVersion: 2
                    ],
                    vaultSecrets: [
                        [
                            path: 'secret/production/database',
                            engineVersion: 2,
                            secretValues: [
                                [envVar: 'DB_HOST', vaultKey: 'host'],
                                [envVar: 'DB_USER', vaultKey: 'username'],
                                [envVar: 'DB_PASS', vaultKey: 'password']
                            ]
                        ],
                        [
                            path: 'secret/production/api-keys',
                            engineVersion: 2,
                            secretValues: [
                                [envVar: 'PAYMENT_API_KEY', vaultKey: 'stripe_live_key']
                            ]
                        ]
                    ]
                ) {
                    sh '''
                        ./deploy.sh 
                            --db-host "$DB_HOST" 
                            --db-user "$DB_USER"
                    '''
                }
            }
        }
    }
}

Bu yaklaşımın avantajı: credential’lar Jenkins’te hiç saklanmıyor, her build’de Vault’tan taze çekiliyor. Rotation da anlık etkili oluyor.

Credential Rotation ve Bakım

Credential’ları oluşturduktan sonra bakımını ihmal etmek de bir risk. Bazı pratik öneriler:

  • Periyodik rotation: Kritik credential’ları en fazla 90 günde bir rotate edin. Jenkins’te kolayca güncellenebilir; ID değişmediği sürece tüm pipeline’lar otomatik yeni değeri kullanır.
  • Kullanılmayan credential temizliği: Jenkins’in credential kullanım istatistiklerine bakın. Uzun süredir hiçbir job’ın kullanmadığı credential’ları silin.
  • Audit log takibi: Kimin hangi credential’a eriştiğini takip edin. Jenkins Audit Trail Plugin bu konuda yardımcı olur.
  • En az ayrıcalık ilkesi: Docker Hub için kullandığınız credential sadece belirli repository’lere push yapabilmeli. AWS key’leri sadece gereken S3 bucket’larına erişebilmeli. Jenkins credential’ı ne kadar güvenli saklarsa saklasın, o credential’ın arkasındaki hesabın yetkileri de sınırlı olmalı.
  • Folder scope kullanın: Farklı ekiplerin pipeline’ları varsa, her ekibin credential’larını ayrı folder’larda tutun. Backend ekibinin pipeline’ı frontend ekibinin deployment key’ine erişememeli.

Yaygın Hatalar ve Çözümleri

Hata 1: Credential ID’lerini hardcode etmek

Bazı ekipler credential ID’lerini her Jenkinsfile’a ayrı ayrı yazıyor. Bunun yerine shared library veya parametrik yaklaşım kullanın. Ortak bir vars/deployToServer.groovy yazıp credential ID’lerini oradan yönetin.

Hata 2: Build artifact içine credential gömülmesi

Docker image build ederken ARG ile credential verip sonra image’da kalmasına izin vermek tehlikeli. Multi-stage build kullanın ve credential’ların final image’a taşınmadığından emin olun.

Hata 3: Test ortamında production credential’ı kullanmak

Her environment için ayrı credential seti oluşturun. db-test-credentials, db-staging-credentials, db-prod-credentials şeklinde isimlendirin ve her stage doğru olanı kullansın.

Hata 4: Credential doğrulama eksikliği

Pipeline başında credential’ın erişilebilir olduğunu doğrulayan basit bir test adımı ekleyin. Deployment aşamasında hata almak yerine başta anlamak çok daha iyi.

Sonuç

Jenkins credential yönetimi, ilk bakışta basit bir konu gibi görünebilir ama production ortamlarında güvenlik açıklarının büyük kısmı bu konudaki ihmalkarlıktan kaynaklanıyor. Bugün uygulayabileceğiniz en önemli pratikler şunlar:

  • Jenkinsfile’da asla düz metin credential bulundurmayın, Git geçmişini kontrol edin.
  • Her credential için anlamlı ID ve açıklama yazın.
  • withCredentials bloğunu tercih edin; credential’ı yalnızca ihtiyaç duyulan yerde açık tutun.
  • Folder scope ile ekipler arası izolasyon sağlayın.
  • Credential rotation politikası oluşturun ve uygulayın.
  • Kurumsal ortamlarda Vault entegrasyonunu değerlendirin.

Güvenlik, pipeline hızını düşürmez. Aksine, doğru credential yönetimi sayesinde audit, rotation ve yetki yönetimi çok daha kolay hale gelir. Şimdi Jenkins’inizi açın ve var olan Jenkinsfile’larınızı gözden geçirin; düz metin credential var mı diye bakın. Varsa, bugün düzeltme vakti.

Bir yanıt yazın

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