secrets
GitLab Ultimate Secrets Management
This guide covers secrets management in GitLab Ultimate including CI/CD variables, OIDC authentication, external secrets integration (HashiCorp Vault, AWS Secrets Manager), and secret detection. Proper secrets management prevents credential exposure and security breaches.
Overview
GitLab Ultimate provides comprehensive secrets management:
- CI/CD Variables - Masked and protected variables for pipelines
- OIDC Authentication - Short-lived tokens instead of long-lived credentials
- External Secrets Integration - HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager
- Secret Detection - Prevent committed secrets (push protection and pipeline scanning)
- Secret Rotation - Automated secret rotation workflows
- Audit Trail - Complete secrets access logging
Secrets management follows the principle: Never commit secrets to Git, never use long-lived credentials.
CI/CD Variables
Overview
CI/CD variables store sensitive configuration values like API tokens, passwords, and certificates. They're available to pipelines without being committed to Git.
Variable Types
Regular Variables:
- Visible in job logs
- Accessible to all pipelines
- Use for non-sensitive configuration
Masked Variables:
- Hidden in job logs (shown as
[masked]) - Cannot be less than 8 characters
- Must not contain special characters that break masking
Protected Variables:
- Only available to pipelines on protected branches/tags
- Use for production credentials
- Higher security for sensitive secrets
File Variables:
- Stored as temporary files instead of environment variables
- Use for certificates, keys, large secrets
- File path available in
$VARIABLE_NAME
Configuration
Creating Variables:
- Navigate to: Settings > CI/CD > Variables
- Click "Add Variable"
- Configure:
- Key: Variable name (e.g.,
API_TOKEN) - Value: Secret value
- Type: Variable or File
- Protected: Yes/No
- Masked: Yes/No
- Environment scope:
*or specific environment
- Key: Variable name (e.g.,
Variable Scopes:
Variable: DATABASE_URL
Value: postgres://prod.example.com
Environment Scope: production
Protected: Yes
Masked: Yes
Variable: DATABASE_URL
Value: postgres://staging.example.com
Environment Scope: staging
Protected: No
Masked: Yes
Variable: DATABASE_URL
Value: postgres://localhost
Environment Scope: *
Protected: No
Masked: No
Best Practices
Security:
Mask all secrets
Protect production credentials
Scope to specific environments
Use file type for certificates/keys
Rotate secrets regularly
Limit variable access with roles
Store secrets in .gitlab-ci.yml
Echo secrets in scripts
Use unmasked variables for credentials
Use wildcard scope for production secrets
Share secrets across projects unnecessarily
Naming Conventions:
Good:
- API_TOKEN
- DATABASE_PASSWORD
- AWS_ACCESS_KEY_ID
- PRIVATE_KEY_FILE
- STAGING_DB_URL
Avoid:
- apiToken (inconsistent casing)
- secret123 (unclear purpose)
- PASSWORD (too generic)
- prod_db (inconsistent naming)
Using Variables in Pipelines
Environment Variables:
deploy: stage: deploy script: - echo "Deploying to production..." - ./deploy.sh environment: name: production variables: DEPLOY_ENV: "production" # Variables automatically available: # - $API_TOKEN (from CI/CD settings) # - $DATABASE_URL (scoped to production) # - $DEPLOY_ENV (from job variables)
File Variables:
deploy: stage: deploy script: # File variable creates temporary file - cat $CERTIFICATE_FILE > /tmp/cert.pem - kubectl create secret tls my-cert --cert=/tmp/cert.pem --key=$PRIVATE_KEY_FILE - rm /tmp/cert.pem # Clean up variables: # These are defined as File type in CI/CD settings # $CERTIFICATE_FILE contains path to temporary file # $PRIVATE_KEY_FILE contains path to private key file
Passing to Docker:
build: stage: build script: # Securely pass secrets to Docker build - echo "$NPM_TOKEN" | docker login -u $NPM_USER --password-stdin - docker build --build-arg NPM_TOKEN=$NPM_TOKEN --build-arg API_KEY=$API_KEY -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Variable Precedence
Order (highest to lowest priority):
- Trigger variables
- Scheduled pipeline variables
- Manual pipeline run variables
- Job variables (defined in
.gitlab-ci.yml) - Project variables
- Group variables
- Instance variables
- Deployment variables
- Predefined variables
OIDC Authentication
Overview
OIDC (OpenID Connect) authentication provides short-lived tokens for accessing external services without long-lived credentials. GitLab automatically generates JWT tokens that can be exchanged for service credentials.
Benefits:
- No long-lived credentials stored in GitLab
- Automatic token rotation
- Scoped access per job
- Audit trail of token usage
- Reduced security risk
Supported Services
Official Support:
- HashiCorp Vault
- AWS (via STS AssumeRoleWithWebIdentity)
- Google Cloud (via Workload Identity Federation)
- Azure (via Workload Identity Federation)
Custom Integration:
- Any service supporting OIDC/JWT authentication
GitLab OIDC Token
Token Structure:
{ "jti": "c82eeb0c-5c6f-4a33-abf5-4c474b92b558", "iss": "https://gitlab.com", "aud": "https://gitlab.com", "sub": "project_path:mygroup/myproject:ref_type:branch:ref:main", "namespace_id": "72", "namespace_path": "mygroup", "project_id": "20", "project_path": "mygroup/myproject", "user_id": "42", "user_login": "alice", "user_email": "alice@example.com", "pipeline_id": "1234", "pipeline_source": "push", "job_id": "5678", "ref": "main", "ref_type": "branch", "ref_protected": "true", "environment": "production", "environment_protected": "true", "iat": 1516239022, "nbf": 1516239022, "exp": 1516239922 }
Key Fields:
sub: Subject identifying the job contextaud: Audience (customizable per ID token)iss: Issuer (GitLab instance URL)exp: Expiration (typically 1 hour)
Configuration
Defining ID Tokens (.gitlab-ci.yml):
job_with_oidc: id_tokens: GITLAB_OIDC_TOKEN: aud: https://gitlab.com script: - echo $GITLAB_OIDC_TOKEN # Token automatically available in job
Multiple Audiences:
multi_cloud_deploy: id_tokens: AWS_TOKEN: aud: https://aws.amazon.com GCP_TOKEN: aud: https://gcp.example.com VAULT_TOKEN: aud: https://vault.example.com script: - echo "AWS token: $AWS_TOKEN" - echo "GCP token: $GCP_TOKEN" - echo "Vault token: $VAULT_TOKEN"
OIDC with HashiCorp Vault
Complete Integration:
1. Configure Vault JWT Auth:
# Enable JWT auth method vault auth enable jwt # Configure GitLab as OIDC provider vault write auth/jwt/config \ oidc_discovery_url="https://gitlab.com" \ bound_issuer="https://gitlab.com"
2. Create Vault Role:
# Create role for GitLab project vault write auth/jwt/role/myproject-role \ role_type="jwt" \ bound_audiences="https://gitlab.com" \ bound_claims='{"project_id":"20","ref":"main","ref_type":"branch"}' \ user_claim="user_email" \ policies="myproject-policy" \ ttl="1h"
3. Create Vault Policy:
# Create policy with required permissions vault policy write myproject-policy - <<EOF path "secret/data/myproject/*" { capabilities = ["read"] } EOF
4. Store Secrets in Vault:
# Store secrets vault kv put secret/myproject/production \ DATABASE_URL="postgres://prod.example.com" \ API_TOKEN="secret-token-value"
5. Use in GitLab CI/CD:
deploy_production: stage: deploy id_tokens: VAULT_ID_TOKEN: aud: https://gitlab.com before_script: # Authenticate with Vault using OIDC token - export VAULT_ADDR="https://vault.example.com" - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-role jwt=$VAULT_ID_TOKEN)" script: # Read secrets from Vault - export DATABASE_URL="$(vault kv get -field=DATABASE_URL secret/myproject/production)" - export API_TOKEN="$(vault kv get -field=API_TOKEN secret/myproject/production)" # Use secrets in deployment - ./deploy.sh environment: name: production only: - main
GitLab Secrets Keyword (Simplified):
deploy_production: stage: deploy id_tokens: VAULT_ID_TOKEN: aud: https://gitlab.com secrets: DATABASE_URL: vault: secret/myproject/production/DATABASE_URL@myproject-role token: $VAULT_ID_TOKEN API_TOKEN: vault: secret/myproject/production/API_TOKEN@myproject-role token: $VAULT_ID_TOKEN script: # Secrets automatically available as environment variables - echo "Database: $DATABASE_URL" - ./deploy.sh environment: name: production
OIDC with AWS
AWS STS AssumeRoleWithWebIdentity:
1. Create IAM OIDC Provider:
aws iam create-open-id-connect-provider \ --url https://gitlab.com \ --client-id-list https://gitlab.com \ --thumbprint-list <thumbprint>
2. Create IAM Role:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/gitlab.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "gitlab.com:aud": "https://gitlab.com", "gitlab.com:sub": "project_path:mygroup/myproject:ref_type:branch:ref:main" } } } ] }
3. Use in GitLab CI/CD:
deploy_to_aws: stage: deploy id_tokens: GITLAB_OIDC_TOKEN: aud: https://gitlab.com before_script: # Assume AWS role using OIDC token - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn arn:aws:iam::123456789012:role/gitlab-deploy-role --role-session-name "gitlab-${CI_PROJECT_ID}-${CI_PIPELINE_ID}" --web-identity-token $GITLAB_OIDC_TOKEN --duration-seconds 3600 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) script: # AWS credentials automatically available - aws s3 cp ./build s3://my-bucket/ --recursive - aws ecs update-service --cluster prod --service my-app --force-new-deployment
OIDC with Google Cloud
Workload Identity Federation:
1. Create Workload Identity Pool:
gcloud iam workload-identity-pools create "gitlab-pool" \ --project="my-project" \ --location="global" \ --display-name="GitLab CI/CD"
2. Create Workload Identity Provider:
gcloud iam workload-identity-pools providers create-oidc "gitlab-provider" \ --project="my-project" \ --location="global" \ --workload-identity-pool="gitlab-pool" \ --display-name="GitLab" \ --attribute-mapping="google.subject=assertion.sub,attribute.project_path=assertion.project_path" \ --issuer-uri="https://gitlab.com"
3. Grant Service Account Access:
gcloud iam service-accounts add-iam-policy-binding "my-service-account@my-project.iam.gserviceaccount.com" \ --project="my-project" \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/attribute.project_path/mygroup/myproject"
4. Use in GitLab CI/CD:
deploy_to_gcp: stage: deploy id_tokens: GITLAB_OIDC_TOKEN: aud: https://gcp.example.com before_script: # Authenticate with GCP using OIDC - echo $GITLAB_OIDC_TOKEN > /tmp/token.txt - gcloud iam workload-identity-pools create-cred-config \ projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/providers/gitlab-provider \ --service-account="my-service-account@my-project.iam.gserviceaccount.com" \ --credential-source-file=/tmp/token.txt \ --output-file=/tmp/gcp-credentials.json - export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-credentials.json script: # GCP credentials automatically available - gcloud storage cp ./build gs://my-bucket/ - gcloud run deploy my-service --image gcr.io/my-project/my-image:latest
External Secrets Integration
HashiCorp Vault
Advanced Configuration:
Dynamic Secrets:
deploy: id_tokens: VAULT_ID_TOKEN: aud: https://gitlab.com secrets: # Static secret API_TOKEN: vault: secret/data/myproject/api_token@myproject-role token: $VAULT_ID_TOKEN # Dynamic AWS credentials (generated on-demand) AWS_ACCESS_KEY_ID: vault: aws/creds/deploy-role/access_key@myproject-role token: $VAULT_ID_TOKEN AWS_SECRET_ACCESS_KEY: vault: aws/creds/deploy-role/secret_key@myproject-role token: $VAULT_ID_TOKEN script: - aws s3 cp ./build s3://my-bucket/ # AWS credentials automatically revoked after job completes
Vault Namespaces:
secrets: DATABASE_PASSWORD: vault: engine: name: kv-v2 path: secret path: myproject/database field: password vault_server: url: https://vault.example.com namespace: engineering/backend auth_path: jwt token: $VAULT_ID_TOKEN
AWS Secrets Manager
Using AWS CLI:
deploy: id_tokens: GITLAB_OIDC_TOKEN: aud: https://gitlab.com before_script: # Assume AWS role - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn $AWS_ROLE_ARN --role-session-name "gitlab-ci" --web-identity-token $GITLAB_OIDC_TOKEN --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text)) # Retrieve secrets - export DATABASE_URL=$(aws secretsmanager get-secret-value --secret-id myproject/production/database_url --query SecretString --output text) - export API_TOKEN=$(aws secretsmanager get-secret-value --secret-id myproject/production/api_token --query SecretString --output text) script: - ./deploy.sh
Google Cloud Secret Manager
Using gcloud:
deploy: id_tokens: GITLAB_OIDC_TOKEN: aud: https://gcp.example.com before_script: # Authenticate with GCP - gcloud auth login --cred-file=$GOOGLE_APPLICATION_CREDENTIALS # Retrieve secrets - export DATABASE_URL=$(gcloud secrets versions access latest --secret="database-url") - export API_TOKEN=$(gcloud secrets versions access latest --secret="api-token") script: - ./deploy.sh
Secret Rotation
Automated Rotation
Rotation Strategy:
1. Generate new secret
2. Deploy new secret to services (both old and new active)
3. Update secret in GitLab/Vault
4. Wait for rollout completion
5. Revoke old secret
6. Verify all services using new secret
Example: Database Password Rotation:
rotate_database_password: stage: maintenance id_tokens: VAULT_ID_TOKEN: aud: https://gitlab.com script: # 1. Generate new password - NEW_PASSWORD=$(openssl rand -base64 32) # 2. Update database user password - psql -h $DB_HOST -U $DB_ADMIN -c "ALTER USER appuser PASSWORD '$NEW_PASSWORD';" # 3. Update secret in Vault - vault kv put secret/myproject/production database_password=$NEW_PASSWORD # 4. Trigger application redeployment - curl -X POST $DEPLOYMENT_WEBHOOK # 5. Wait for deployment - sleep 300 # 6. Verify connectivity - psql -h $DB_HOST -U appuser -c "SELECT 1;" when: manual only: - schedules
Scheduled Rotation:
# .gitlab-ci.yml include: - local: .gitlab/ci/secret-rotation.yml # Scheduled pipeline: Weekly on Sundays at 2 AM
Rotation Best Practices
- Automate Rotation - Use scheduled pipelines
- Grace Periods - Keep old secrets active during transition
- Verification - Test new secrets before revoking old ones
- Rollback Plan - Be prepared to revert to old secrets
- Audit Rotation - Log all secret rotation events
- Notify Teams - Inform teams of rotation schedules
- Emergency Rotation - Have process for immediate rotation
Secret Detection
Overview
Secret Detection identifies committed credentials and prevents exposure. GitLab provides two complementary approaches:
- Secret Push Protection - Blocks secrets at push time
- Pipeline Secret Detection - Scans in CI/CD pipelines
Secret Push Protection
How It Works:
- Runs in pre-receive hook
- Scans diff for secrets (not entire files)
- Blocks push immediately if secrets detected
- Prevents secrets from entering repository
Enabling:
- Navigate to: Settings > Security & Compliance
- Enable "Secret push protection"
- Configure detection rules (optional)
Detected Secret Types:
- AWS Access Keys
- GCP Service Account Keys
- Azure Storage Keys
- GitHub Personal Access Tokens
- GitLab Personal Access Tokens
- Private SSH Keys
- Database Connection Strings
- JWT Tokens
- API Keys (various services)
- OAuth Client Secrets
- Stripe API Keys
- PayPal credentials
Bypass (Emergency):
# Only for false positives or emergency situations git commit --trailer "skip-secret-detection=true" -m "Your commit message"
Pipeline Secret Detection
Configuration:
include: - template: Security/Secret-Detection.gitlab-ci.yml secret_detection: variables: SECRET_DETECTION_EXCLUDED_PATHS: "test/,spec/,docs/" SECRET_DETECTION_HISTORIC_SCAN: "false" # Set true for full history scan
Custom Patterns:
# .gitlab/secret-detection-ruleset.yml patterns: - name: "Company API Key" pattern: "company_api_[a-zA-Z0-9]{32}" severity: "critical" - name: "Internal Token" pattern: "internal_token_[a-zA-Z0-9]{40}" severity: "high"
Integration:
include: - template: Security/Secret-Detection.gitlab-ci.yml secret_detection: variables: SECRET_DETECTION_CUSTOM_PATTERNS: ".gitlab/secret-detection-ruleset.yml"
Handling Detected Secrets
If Secret Detected:
-
Immediate Actions:
- Stop deployment immediately
- Rotate exposed secret
- Audit access logs
- Determine exposure scope
-
Remove from History:
# Using git filter-branch git filter-branch --force --index-filter \ "git rm --cached --ignore-unmatch config/secrets.yml" \ --prune-empty --tag-name-filter cat -- --all # Using BFG Repo-Cleaner (faster, recommended) bfg --delete-files secrets.yml bfg --replace-text passwords.txt # Force push (WARNING: Rewrites history) git push origin --force --all
-
Notify Team:
- Inform all team members of history rewrite
- Have them rebase/re-clone
- Document incident for audit
-
Prevent Recurrence:
- Enable Secret Push Protection
- Add to custom detection patterns
- Implement pre-commit hooks
- Train developers
Best Practices
General Principles
Never:
- Commit secrets to Git
- Store secrets in
.gitlab-ci.yml - Echo secrets in CI/CD logs
- Share secrets via chat/email
- Use long-lived credentials
- Store production secrets unencrypted
Always:
- Use CI/CD variables (masked, protected)
- Prefer OIDC over long-lived credentials
- Store secrets in external systems (Vault, AWS Secrets Manager)
- Rotate secrets regularly
- Audit secret access
- Enable Secret Push Protection
Security Hierarchy
Secret Storage (Best to Worst):
-
OIDC with External Secrets (Best)
- Short-lived tokens
- No stored credentials
- Automatic rotation
- Full audit trail
-
External Secrets Manager (Good)
- Centralized management
- Rotation capabilities
- Access controls
- Audit logging
-
GitLab CI/CD Variables (Acceptable)
- Masked and protected
- Scoped to environments
- GitLab-managed
-
Encrypted Secrets in Git (Poor)
- Still in version control
- Key management challenges
- Rotation difficulties
-
Plain Text in Git (Never)
- Complete exposure
- Permanent history
- Unacceptable risk
Development Workflow
Local Development:
# Use .env files (never commit) echo ".env" >> .gitignore # .env.example (commit this) DATABASE_URL=postgres://localhost/myapp API_TOKEN=your-api-token-here # .env (never commit) DATABASE_URL=postgres://localhost/myapp API_TOKEN=actual-secret-token
CI/CD Pipeline:
# Reference secrets from CI/CD variables deploy: script: - export DATABASE_URL=$DATABASE_URL # From CI/CD variable - export API_TOKEN=$API_TOKEN # From CI/CD variable - ./deploy.sh
Production:
# Use OIDC + External Secrets deploy_production: id_tokens: VAULT_ID_TOKEN: aud: https://gitlab.com secrets: DATABASE_URL: vault: secret/myproject/production/database_url@prod-role API_TOKEN: vault: secret/myproject/production/api_token@prod-role script: - ./deploy.sh
Troubleshooting
Common Issues
Variable not available in job:
Checklist:
Variable defined in CI/CD settings
Variable not protected (or branch is protected)
Environment scope matches job environment
Variable key matches reference ($VARIABLE_NAME)
Job has permission to access variable
OIDC authentication failing:
Checklist:
ID token defined in job
Audience matches external service configuration
External service OIDC provider configured correctly
Token not expired
Claims match external service requirements
Secret detected despite masking:
Issue: Secret visible in logs despite masked variable
Causes:
- Secret less than 8 characters
- Secret contains special characters
- Secret echoed by command output
- Secret in error messages
Resolution:
- Ensure secrets meet masking requirements
- Avoid echoing secrets
- Redirect secret-containing output
- Use file variables for complex secrets
Vault authentication failing:
Error: Permission denied
Checklist:
Vault auth method enabled
Vault role created with correct claims
Vault policy grants required permissions
GitLab OIDC token claims match role bounds
Vault URL accessible from GitLab Runner
References
- CI/CD Variables Documentation
- OIDC Authentication Documentation
- HashiCorp Vault Integration
- Secret Detection Documentation
- AWS Integration
Next Steps
- Security Scanning - Configure secret detection scanners
- Security Policies - Enforce secret detection policies
- Supply Chain Security - Secure dependencies and builds