Variablen und Secrets sind das Nervensystem deiner CI/CD-Pipeline. Sie liefern Configuration, API-Keys, Database-Credentials, und Feature-Flags – alles, was Code von Hard-coded-Values befreit und flexible, sichere Deployments ermöglicht.
Dieses Kapitel erklärt GitLab-Variablen systematisch: Typen, Scopes, Precedence, Security-Features (Protected, Masked), und Integration mit externen Secrets-Management-Tools (Vault, SOPS, AWS Secrets Manager). Am Ende kannst du Secrets sicher verwalten, Compliance-Anforderungen erfüllen, und häufige Security-Pitfalls vermeiden.
GitLab stellt automatisch hunderte Variablen bereit, die Pipeline-Context beschreiben.
Kategorien:
Git-bezogen:
$CI_COMMIT_SHA # Full commit hash
$CI_COMMIT_SHORT_SHA # Short commit hash (8 chars)
$CI_COMMIT_REF_NAME # Branch or tag name
$CI_COMMIT_BRANCH # Branch name (wenn Branch)
$CI_COMMIT_TAG # Tag name (wenn Tag)
$CI_COMMIT_MESSAGE # Commit message
$CI_COMMIT_AUTHOR # Commit author namePipeline-bezogen:
$CI_PIPELINE_ID # Unique pipeline ID
$CI_PIPELINE_IID # Project-scoped pipeline ID
$CI_PIPELINE_SOURCE # push, merge_request_event, schedule, etc.
$CI_PIPELINE_URL # URL to pipelineJob-bezogen:
$CI_JOB_ID # Unique job ID
$CI_JOB_NAME # Job name from .gitlab-ci.yml
$CI_JOB_STAGE # Stage name
$CI_JOB_TOKEN # Temporary token for API/Registry access
$CI_JOB_STARTED_AT # Job start timestampProject-bezogen:
$CI_PROJECT_ID # Numeric project ID
$CI_PROJECT_NAME # Project name
$CI_PROJECT_PATH # Full project path (namespace/project)
$CI_PROJECT_DIR # Working directory
$CI_PROJECT_URL # HTTP URL to projectRunner-bezogen:
$CI_RUNNER_ID # Runner ID
$CI_RUNNER_DESCRIPTION # Runner description
$CI_RUNNER_TAGS # Comma-separated runner tagsRegistry-bezogen:
$CI_REGISTRY # registry.gitlab.com
$CI_REGISTRY_IMAGE # Full image path
$CI_REGISTRY_USER # Username for registry
$CI_REGISTRY_PASSWORD # Password for registry (CI_JOB_TOKEN)Merge Request-bezogen:
$CI_MERGE_REQUEST_IID # MR number
$CI_MERGE_REQUEST_TITLE # MR title
$CI_MERGE_REQUEST_SOURCE_BRANCH # Source branch
$CI_MERGE_REQUEST_TARGET_BRANCH # Target branchPraktisches Beispiel:
build:
script:
- echo "Building commit $CI_COMMIT_SHORT_SHA"
- echo "Author: $CI_COMMIT_AUTHOR"
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHAVollständige Liste: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
In .gitlab-ci.yml:
variables:
DEPLOY_ENV: staging
DATABASE_NAME: myapp_db
NODE_VERSION: "18"
job:
script:
- echo "Deploying to $DEPLOY_ENV"
- echo "Using Node.js $NODE_VERSION"Variable Expansion:
variables:
BASE_URL: https://api.example.com
API_ENDPOINT: ${BASE_URL}/v1/users # Expansion
job:
script:
- curl $API_ENDPOINTJob-spezifische Variables (überschreiben globale):
variables:
ENVIRONMENT: staging
deploy:staging:
variables:
ENVIRONMENT: staging # Explizit staging
deploy:production:
variables:
ENVIRONMENT: production # Override zu production
script:
- echo "Deploying to $ENVIRONMENT"UI-basierte Variables: Settings → CI/CD → Variables
Vorteile: - Nicht im Git-Repository (kein Leak-Risiko) - Können Protected/Masked sein - Einfach zu rotieren ohne Code-Change
Erstellen:
Key: DATABASE_PASSWORD
Value: super_secret_password
Type: Variable (oder File)
Environments: All (oder specific)
Protected: ☑
Masked: ☑
In Pipeline verfügbar:
deploy:
script:
- psql -h db.example.com -U user -d mydb
# PGPASSWORD=$DATABASE_PASSWORD wird automatisch gesetztUse Case: Multi-Line-Secrets (SSH Keys, SSL Certificates, kubeconfig)
UI-Setup:
Key: SSH_PRIVATE_KEY
Value: [paste entire SSH key, including -----BEGIN/END-----]
Type: File
Variable wird zu temporärem File-Path:
deploy:
before_script:
- eval "$(ssh-agent -s)"
- ssh-add $SSH_PRIVATE_KEY # $SSH_PRIVATE_KEY ist Path zu temp file
- chmod 700 ~/.ssh
script:
- scp -r dist/ server:/var/www/Kubeconfig-Beispiel:
deploy:k8s:
before_script:
- mkdir -p ~/.kube
- cp $KUBECONFIG ~/.kube/config # KUBECONFIG ist File variable
script:
- kubectl apply -f deployment.yamlDynamische Variables zwischen Jobs weitergeben:
build:
script:
- npm run build
- echo "BUILD_VERSION=$(cat package.json | jq -r .version)" >> build.env
- echo "BUILD_TIME=$(date -Iseconds)" >> build.env
artifacts:
reports:
dotenv: build.env # Variables aus build.env werden exportiert
deploy:
needs: [build]
script:
- echo "Deploying version $BUILD_VERSION" # Variable verfügbar!
- echo "Built at $BUILD_TIME"build.env-Format:
BUILD_VERSION=1.2.3
BUILD_TIME=2024-01-15T14:30:00+00:00
1. Instance-Level (Admin-only): - Settings → CI/CD → Variables (Instance-Admin) - Verfügbar in allen Projects
2. Group-Level: - Group → Settings → CI/CD → Variables - Verfügbar in allen Projects der Group (und Subgroups)
3. Project-Level: - Project → Settings → CI/CD → Variables - Verfügbar nur in diesem Project
4. Pipeline-Level: - Definiert in
.gitlab-ci.yml - Nur in dieser Pipeline
5. Job-Level: - Definiert in Job-Definition in
.gitlab-ci.yml - Nur in diesem Job
Highest Precedence (überschreibt alle): 1. Job-Level
(in .gitlab-ci.yml) 2. Pipeline-Level (in
.gitlab-ci.yml) 3. Project-Level (UI) 4. Group-Level (UI)
5. Instance-Level (UI) Lowest Precedence
Beispiel:
# Group-Level (UI): DATABASE_URL = postgres://group-db
# Project-Level (UI): DATABASE_URL = postgres://project-db
variables:
DATABASE_URL: postgres://pipeline-db # Überschreibt Project
job1:
script:
- echo $DATABASE_URL # postgres://pipeline-db
job2:
variables:
DATABASE_URL: postgres://job-db # Überschreibt Pipeline
script:
- echo $DATABASE_URL # postgres://job-dbUI-Setup mit Environment-Scope:
Key: API_URL
Value: https://staging-api.example.com
Environment: staging
Key: API_URL
Value: https://api.example.com
Environment: production
deploy:
script:
- echo $API_URL # Wert hängt von Environment ab
environment:
name: $ENVIRONMENTEnvironment-Matching: GitLab matched Environment-Name mit Variable-Scope.
Definition: Nur verfügbar auf protected branches und protected tags.
Use Case: Production-Secrets sollen nur auf
main-Branch verfügbar sein.
Setup:
Settings → Repository → Protected Branches
- Branch: main
- Allowed to merge: Maintainers
- Allowed to push: No one
Settings → CI/CD → Variables
- Key: PROD_API_KEY
- Value: sk_live_abc123
- Protected: ☑
In Pipeline:
deploy:production:
script:
- curl -H "Authorization: $PROD_API_KEY" api.example.com
only:
- main # Protected branch → PROD_API_KEY verfügbarAuf Feature-Branch:
deploy:production:
script:
- echo $PROD_API_KEY # EMPTY! Variable nicht verfügbarBest Practice: Alle Production-Secrets als Protected markieren.
Definition: Wert wird in Logs durch
[masked] ersetzt.
Setup:
Key: SECRET_TOKEN
Value: abc123xyz789
Masked: ☑
Job-Log ohne Masking:
$ curl -H "Authorization: Bearer abc123xyz789" api.example.com
HTTP/1.1 200 OK
Job-Log mit Masking:
$ curl -H "Authorization: Bearer [masked]" api.example.com
HTTP/1.1 200 OK
Masked-Anforderungen: - Mindestens 8 Zeichen - Nur Base64-kompatible Zeichen (A-Z, a-z, 0-9, +, /, =) - Keine Leerzeichen
Nicht maskierbar:
Value: "my secret" # Leerzeichen → nicht maskierbar
Value: "abc123" # Nur 6 chars → nicht maskierbar
Value: "abc@#$xyz" # Sonderzeichen → nicht maskierbar
Production-Secrets sollten BEIDE sein:
Key: PROD_DATABASE_PASSWORD
Value: aB3dF8hJ2kM9pQ5rT7vW0xY4zC6eG1i
Protected: ☑ # Nur auf protected branches
Masked: ☑ # Nicht in Logs sichtbar
.gitlab-ci.yml committen✗ FALSCH:
# .gitlab-ci.yml
variables:
API_KEY: sk_live_abc123 # SECRET EXPOSED IN GIT HISTORY!
deploy:
script:
- deploy --api-key $API_KEY✓ RICHTIG:
# .gitlab-ci.yml (kein Secret)
deploy:
script:
- deploy --api-key $API_KEY
# Settings → CI/CD → Variables
# API_KEY = sk_live_abc123 (Protected, Masked)Principle: Secret sollte minimale Permissions haben.
✗ Schlecht: AWS Root Account Credentials in Pipeline ✓ Gut: IAM Role mit minimalen Permissions für spezifische Task
Beispiel (AWS):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}In Pipeline: IAM User mit dieser Policy, nicht Root.
Regel: Secrets sollten regelmäßig rotiert werden (z.B. 90 Tage).
Workflow: 1. Neues Secret generieren (z.B. neuer API Key) 2. In GitLab Variables hinzufügen (parallel zum alten) 3. Code updaten (alte → neue Variable) 4. Alten Key deaktivieren/löschen 5. Alte Variable aus GitLab entfernen
Automated Rotation (mit External Tools wie Vault): Vault rotiert automatisch.
GitLab Audit Events (Premium/Ultimate): - Settings → Security & Compliance → Audit Events - Zeigt Variable-Changes: Created, Updated, Deleted
Log-Einträge:
User @andreas added variable DATABASE_PASSWORD
User @bob updated variable API_KEY
User @alice deleted variable OLD_TOKEN
✗ Ein Secret für alle Environments:
DATABASE_PASSWORD = same_for_staging_and_prod # SCHLECHT
✓ Separate Secrets per Environment:
Key: DATABASE_PASSWORD
Value: staging_password_abc123
Environment: staging
Key: DATABASE_PASSWORD
Value: production_password_xyz789
Environment: production
Vorteil: Staging-Compromise betrifft nicht Production.
test:
script:
- npm test # Kein Secret nötig
deploy:production:
script:
- deploy --api-key $PROD_API_KEY # Secret nur hier
only:
- mainNicht:
variables:
PROD_API_KEY: $PROD_API_KEY # Jetzt in ALLEN Jobs verfügbar!GitLab Secret Detection (Ultimate): - Automatisch aktiviert - Scannt Commits für Secrets - Blockt Pipeline bei Leak
Manual Tools:
scan-secrets:
image: trufflesecurity/trufflehog:latest
script:
- trufflehog git file://. --only-verified
allow_failure: false # Blockt bei Secret-FundWhy Vault: Zentrales Secret-Management, Dynamic Secrets, Audit-Trail, Rotation.
Setup:
Vault-Server (external):
vault kv put secret/myapp/db \
username=dbuser \
password=dbpass123GitLab Integration (GitLab 13.4+):
Settings → CI/CD → Variables:
Key: VAULT_ADDR
Value: https://vault.example.com
Key: VAULT_TOKEN # Service Token für CI/CD
Value: hvs.abc123xyz
Protected: ☑
Masked: ☑
In Pipeline:
deploy:
before_script:
- apk add --no-cache curl jq
- export DB_PASSWORD=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/secret/data/myapp/db | jq -r .data.data.password)
script:
- psql -h db.example.com -U dbuser -d myapp
# PGPASSWORD=$DB_PASSWORD ist gesetztVault JWT Auth (bessere Security, kein Long-Lived Token):
deploy:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
before_script:
- export VAULT_TOKEN=$(curl -s --request POST \
--data '{"jwt":"'"$VAULT_ID_TOKEN"'","role":"gitlab-ci"}' \
$VAULT_ADDR/v1/auth/jwt/login | jq -r .auth.client_token)
- export DB_PASSWORD=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/secret/data/myapp/db | jq -r .data.data.password)
script:
- deployVorteile: - Secrets nicht in GitLab gespeichert - Audit-Trail in Vault - Dynamic Secrets (DB-Credentials für 1 Stunde) - Automatic Rotation
Setup:
AWS Secrets Manager (external):
aws secretsmanager create-secret \
--name myapp/db-password \
--secret-string "super_secret_password"GitLab Variables:
Key: AWS_ACCESS_KEY_ID
Value: AKIAIOSFODNN7EXAMPLE
Key: AWS_SECRET_ACCESS_KEY
Value: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Protected: ☑
Masked: ☑
Key: AWS_REGION
Value: us-east-1
In Pipeline:
deploy:
image: amazon/aws-cli:latest
before_script:
- export DB_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id myapp/db-password \
--query SecretString \
--output text)
script:
- psql -h db.example.com -U user -d mydbMit IAM Role (besser als Access Keys):
deploy:
before_script:
- export AWS_ROLE_ARN="arn:aws:iam::123456789012:role/gitlab-ci-role"
- aws sts assume-role --role-arn $AWS_ROLE_ARN --role-session-name gitlab-ci
- export DB_PASSWORD=$(aws secretsmanager get-secret-value ...)
script:
- deploySetup:
GCP Secret Manager:
echo "super_secret_password" | gcloud secrets create db-password --data-file=-GitLab Variables:
Key: GCP_SERVICE_ACCOUNT_KEY
Value: [JSON Key Content]
Type: File
Protected: ☑
In Pipeline:
deploy:
image: google/cloud-sdk:alpine
before_script:
- gcloud auth activate-service-account --key-file=$GCP_SERVICE_ACCOUNT_KEY
- export DB_PASSWORD=$(gcloud secrets versions access latest --secret=db-password)
script:
- deployWhy SOPS: Encrypt secrets in Git, decrypt in Pipeline.
Encrypt Secret:
# Create secrets.yaml
cat > secrets.yaml <<EOF
database:
password: super_secret_password
api:
key: sk_live_abc123
EOF
# Encrypt with age
sops --encrypt --age age1abc123... secrets.yaml > secrets.enc.yaml
# Commit encrypted file
git add secrets.enc.yaml
git commit -m "Add encrypted secrets"Decrypt in Pipeline:
deploy:
before_script:
- apk add --no-cache age sops
- echo "$SOPS_AGE_KEY" > /tmp/age-key.txt
- export SOPS_AGE_KEY_FILE=/tmp/age-key.txt
- sops --decrypt secrets.enc.yaml > secrets.yaml
- export DB_PASSWORD=$(yq .database.password secrets.yaml)
script:
- deploy
# GitLab Variable: SOPS_AGE_KEY (Private Key, File Type)Vorteile: - Secrets versioniert in Git (encrypted) - Code-Review für Secret-Changes - Kein External Service nötig
Symptom:
$ echo $DATABASE_PASSWORD
$ # Variable ist leerDiagnose:
1. Check Variable existiert: - Settings → CI/CD → Variables → DATABASE_PASSWORD vorhanden?
2. Check Protected-Flag: - Variable Protected: ☑ - Job läuft auf non-protected branch → Variable nicht verfügbar
3. Check Environment-Scope: - Variable Environment: production - Job Environment: staging → Variable nicht verfügbar
4. Check Spelling: - Variable:
DATABASE_PASSWORD - Code: $DATABSE_PASSWORD
(Typo!)
Fix: Korrekte Scope/Environment setzen, Typo fixen.
Symptom:
$ curl -H "Authorization: Bearer abc123xyz" api.example.comSecret abc123xyz ist sichtbar!
Diagnose:
1. Check Masked-Flag: - Settings → CI/CD → Variables → SECRET_TOKEN → Masked: ☐
Fix: Masked: ☑
2. Secret erfüllt Masked-Requirements nicht: - Zu kurz (< 8 chars) - Sonderzeichen
Fix: Secret regenerieren mit Base64-kompatiblen Zeichen.
Symptom:
variables:
BASE_URL: https://api.example.com
API_URL: ${BASE_URL}/v1
job:
script:
- echo $API_URL # Output: ${BASE_URL}/v1 (nicht expanded)Diagnose: Variable-Expansion in GitLab funktioniert, aber Shell-Syntax ist wichtig.
Fix:
variables:
BASE_URL: https://api.example.com
API_URL: $BASE_URL/v1 # Kein ${...} nötig
# Oder in script:
job:
script:
- export API_URL="${BASE_URL}/v1" # Shell-Expansion
- echo $API_URLSymptom:
$ ssh-add $SSH_PRIVATE_KEY
Error loading key "(stdin)": invalid formatDiagnose: SSH_PRIVATE_KEY ist normale
Variable, nicht File.
Fix: Settings → CI/CD → Variables → SSH_PRIVATE_KEY → Type: File
variables:
# Defaults (Development)
DATABASE_HOST: localhost
API_URL: http://localhost:3000
deploy:staging:
variables:
DATABASE_HOST: staging-db.example.com
API_URL: https://staging-api.example.com
environment:
name: staging
script:
- deploy
deploy:production:
variables:
DATABASE_HOST: prod-db.example.com
API_URL: https://api.example.com
environment:
name: production
script:
- deploy
only:
- mainvariables:
FEATURE_NEW_UI: "false"
FEATURE_ANALYTICS: "false"
deploy:canary:
variables:
FEATURE_NEW_UI: "true" # Canary aktiviert Feature
script:
- deploy --feature-flags FEATURE_NEW_UI=$FEATURE_NEW_UI
environment:
name: production-canary
deploy:production:
script:
- deploy --feature-flags FEATURE_NEW_UI=$FEATURE_NEW_UI
environment:
name: productiondeploy:
before_script:
- apk add --no-cache curl jq
- export DEPLOY_TOKEN=$(curl -s -X POST \
-H "Authorization: Bearer $SERVICE_KEY" \
https://auth.example.com/token | jq -r .token)
script:
- deploy --token $DEPLOY_TOKENjob:
script:
- |
if [ "$CI_COMMIT_BRANCH" == "main" ]; then
export ENVIRONMENT="production"
else
export ENVIRONMENT="staging"
fi
- echo "Deploying to $ENVIRONMENT"Variablen und Secrets sind kritisch für sichere CI/CD:
Variable-Typen: - Predefined:
Automatisch von GitLab bereitgestellt - Custom: In
.gitlab-ci.yml oder UI definiert - File:
Multi-Line-Secrets als temporäre Files
Scope: - Instance → Group → Project → Pipeline → Job - Precedence: Job überschreibt alles
Security-Features: - Protected: Nur auf protected branches - Masked: Nicht in Logs sichtbar - Environment-Scoped: Per Environment unterschiedlich
External Tools: - Vault, AWS Secrets Manager, GCP Secret Manager, SOPS - Für zentrales Secret-Management, Rotation, Audit
Best Practices: - NIEMALS Secrets in Git - Protected + Masked für Production - Separate Secrets per Environment - Minimal Privilege - Regular Rotation - Secret Scanning
Master Variablen und Secrets, und CI/CD wird von unsicher zu Production-Grade Security.