Secrets & Environments
Safely use API keys, passwords, and deploy tokens in CI/CD without leaking them
Knowledge Debt detected
You can study this freely — but your score may plateau if these foundations have gaps. The Mastery badge requires them to be solid.
Explanation
CI/CD pipelines need credentials — database passwords, cloud API keys, deploy tokens — but these must never appear in source code or logs. GitHub Actions provides secrets to solve this.
GitHub Secrets — the safe way:
yaml # Add secrets: GitHub repo → Settings → Secrets and variables → Actions → New secret # Name: STRIPE_SECRET_KEY # Value: sk_live_abc123... # Reference in workflow with ${{ secrets.NAME }} jobs: deploy: runs-on: ubuntu-latest steps: - run: | curl -X POST https://api.stripe.com/v1/charges \ -u ${{ secrets.STRIPE_SECRET_KEY }}: GitHub automatically **masks** secrets in logs — they appear as ***` if accidentally printed.
Environment-level secrets (staging vs prod):
yaml # GitHub: Settings → Environments → Create "staging" and "production" # Add different secrets to each environment jobs: deploy-staging: environment: staging # uses staging secrets runs-on: ubuntu-latest steps: - run: deploy --url ${{ secrets.DEPLOY_URL }} deploy-prod: environment: production # uses production secrets, may require approval needs: deploy-staging runs-on: ubuntu-latest steps: - run: deploy --url ${{ secrets.DEPLOY_URL }}
Regular variables (non-secret config):
yaml env: NODE_ENV: production PORT: 3000 jobs: build: env: VITE_API_URL: https://api.myapp.com steps: - run: npm run build env: VITE_ANALYTICS_ID: GA-12345
OIDC — keyless cloud authentication (best practice):
yaml # Instead of storing long-lived AWS access keys as secrets, # configure GitHub's OIDC provider to generate short-lived tokens permissions: id-token: write contents: read steps: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/github-actions aws-region: us-east-1 # Now you have AWS access without any stored keys!
Security rules:
- Never hardcode credentials in workflow files
- Never echo a secret (even masked, avoid the habit)
- Use environment-level secrets to separate staging/prod access
- Rotate secrets regularly — treat them like passwords
- Use OIDC/short-lived tokens instead of long-lived keys when possible
Examples
Deploy to a server using SSH key secret
The SSH private key is stored as a GitHub Secret — it never appears in code or logs.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
- name: Deploy via rsync
run: |
rsync -avz --delete ./dist/ \
-e "ssh -i ~/.ssh/deploy_key" \
deploy@${{ secrets.SERVER_HOST }}:/var/www/app/
- name: Restart app
run: |
ssh -i ~/.ssh/deploy_key deploy@${{ secrets.SERVER_HOST }} \
"sudo systemctl restart myapp"Build and push a Docker image to a registry
Tagging with the git SHA means every deployed image is traceable back to the exact commit that built it.
# .github/workflows/docker.yml
name: Docker Build and Push
on:
push:
branches: [main]
jobs:
build-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: |
myusername/myapp:latest
myusername/myapp:${{ github.sha }}How well did you understand this?
Next in CI/CD
Building a CI Pipeline