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.
withCredentialsbloğ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.
