27 Variablen und Secrets: Sichere Configuration-Management

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.

27.1 Variable-Typen: Das GitLab-Variablen-System

27.1.1 1. Predefined Variables

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 name

Pipeline-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 pipeline

Job-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 timestamp

Project-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 project

Runner-bezogen:

$CI_RUNNER_ID           # Runner ID
$CI_RUNNER_DESCRIPTION  # Runner description
$CI_RUNNER_TAGS         # Comma-separated runner tags

Registry-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 branch

Praktisches 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_SHA

Vollständige Liste: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html

27.1.2 2. Custom Variables

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_ENDPOINT

Job-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"

27.1.3 3. CI/CD Settings Variables

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 gesetzt

27.1.4 4. File Variables

Use 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.yaml

27.1.5 5. Environment Variables (dotenv artifacts)

Dynamische 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

27.2 Variable Scope und Precedence

27.2.1 Scope-Levels (wo definiert)

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

27.2.2 Precedence-Regel

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-db

27.2.3 Environment-Specific Variables

UI-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: $ENVIRONMENT

Environment-Matching: GitLab matched Environment-Name mit Variable-Scope.

27.3 Protected und Masked Variables

27.3.1 Protected Variables

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ügbar

Auf Feature-Branch:

deploy:production:
  script:
    - echo $PROD_API_KEY  # EMPTY! Variable nicht verfügbar

Best Practice: Alle Production-Secrets als Protected markieren.

27.3.2 Masked Variables

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

27.3.3 Protected + Masked: Best Practice

Production-Secrets sollten BEIDE sein:

Key: PROD_DATABASE_PASSWORD
Value: aB3dF8hJ2kM9pQ5rT7vW0xY4zC6eG1i
Protected: ☑  # Nur auf protected branches
Masked: ☑     # Nicht in Logs sichtbar

27.4 Security Best Practices

27.4.1 1. NIEMALS Secrets in .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)

27.4.2 2. Minimal Privilege für Secrets

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.

27.4.3 3. Secret Rotation

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.

27.4.4 4. Audit-Logging

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

27.4.5 5. Separate Secrets für Environments

✗ 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.

27.4.6 6. Least Exposure: Nur nötige Jobs bekommen Secrets

test:
  script:
    - npm test  # Kein Secret nötig

deploy:production:
  script:
    - deploy --api-key $PROD_API_KEY  # Secret nur hier
  only:
    - main

Nicht:

variables:
  PROD_API_KEY: $PROD_API_KEY  # Jetzt in ALLEN Jobs verfügbar!

27.4.7 7. Scan für Secrets im Code

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-Fund

27.5 Externe Secrets-Management-Tools

27.5.1 1. HashiCorp Vault

Why Vault: Zentrales Secret-Management, Dynamic Secrets, Audit-Trail, Rotation.

Setup:

Vault-Server (external):

vault kv put secret/myapp/db \
  username=dbuser \
  password=dbpass123

GitLab 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 gesetzt

Vault 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:
    - deploy

Vorteile: - Secrets nicht in GitLab gespeichert - Audit-Trail in Vault - Dynamic Secrets (DB-Credentials für 1 Stunde) - Automatic Rotation

27.5.2 2. AWS Secrets Manager

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 mydb

Mit 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:
    - deploy

27.5.3 3. Google Cloud Secret Manager

Setup:

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:
    - deploy

27.5.4 4. SOPS (Secrets OPerationS)

Why 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

27.6 Troubleshooting

27.6.1 Problem 1: Variable nicht verfügbar

Symptom:

$ echo $DATABASE_PASSWORD

$ # Variable ist leer

Diagnose:

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.

27.6.2 Problem 2: Secret erscheint in Logs

Symptom:

$ curl -H "Authorization: Bearer abc123xyz" api.example.com

Secret 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.

27.6.3 Problem 3: Variable-Expansion funktioniert nicht

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_URL

27.6.4 Problem 4: File Variable ist nicht File

Symptom:

$ ssh-add $SSH_PRIVATE_KEY
Error loading key "(stdin)": invalid format

Diagnose: SSH_PRIVATE_KEY ist normale Variable, nicht File.

Fix: Settings → CI/CD → Variables → SSH_PRIVATE_KEY → Type: File

27.7 Praktische Patterns

27.7.1 Pattern 1: Multi-Environment-Config

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:
    - main

27.7.2 Pattern 2: Feature Flags

variables:
  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: production

27.7.3 Pattern 3: Dynamic Variables from External API

deploy:
  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_TOKEN

27.7.4 Pattern 4: Conditional Variables

job:
  script:
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        export ENVIRONMENT="production"
      else
        export ENVIRONMENT="staging"
      fi
    - echo "Deploying to $ENVIRONMENT"

27.8 Zusammenfassung

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.