DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    CI/CD PipelinesLesson 4 of 12
    Your progress · 0%
    On this page
    1. Secrets and variables
    2. Environments and approvals
    3. The GITHUB_TOKEN
    4. OIDC: no keys to steal
    ↖ Course roadmap
    Lesson 4 · Secrets & environments

    Secrets, environments and OIDC: deploy without leaking keys

    Give pipelines exactly what they need: secrets and variables, environments with approvals, a least-privilege GITHUB_TOKEN, and OIDC so the cloud trusts your workflow with no stored key.

    40 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    Secrets and variables

    A deploy needs things that don't belong in the repository: API keys, passwords, the region, the URL of the server. GitHub keeps them in two places:

    SecretsVariables
    Foranything that grants access: keys, tokens, passwordssettings: region, log level, feature flags
    Storedencrypted; nobody can read the value backplain text, visible to anyone with access
    In logsmasked as ***printed as written
    In a workflow${{ secrets.NAME }}${{ vars.NAME }}

    Both exist at three levels, and the most specific wins: organisation, repository, and environment (next section).

    $ gh secret set PAYMENTS_API_KEY                       # paste the value when asked: not in shell history
    $ gh secret set PAYMENTS_API_KEY --env production      # a different value for one environment
    $ gh variable set LOG_LEVEL --body info
    $ gh secret list
    

    Use them through env:, so the value reaches your script as an environment variable:

    - name: Deploy
      env:
        API_KEY: ${{ secrets.PAYMENTS_API_KEY }}
        LOG_LEVEL: ${{ vars.LOG_LEVEL }}
      run: ./scripts/deploy.sh staging
    
    Masking is a safety net, not a guarantee

    GitHub replaces a secret's exact value with *** in logs. It can't recognise a secret that was transformed: base64-encoded, split, URL-encoded, or one field of a JSON secret. Never print secrets, never pass them as command-line arguments (other processes can see those), and don't store structured data as one secret. Secrets are also not given to workflows triggered by pull requests from forks.

    Environments and approvals

    An environment is a named deployment target, such as staging or production, with its own secrets, variables and protection rules. A job that names an environment can't start until the rules pass:

    Staging deploys by itself. Production waits for a person, then gets one-hour cloud credentials through OIDC. No stored keys.© Diagram: DevOps Academy

    Staging deploys by itself. Production waits for a person, then gets one-hour cloud credentials through OIDC. No stored keys.© Diagram: DevOps Academy

    Create them in Settings → Environments. The rules that matter:

    RuleEffect
    Required reviewersthe job waits until one of them approves in the run page
    Deployment branches and tagsonly main (or v* tags) may deploy here
    Wait timera delay before the job starts, time to cancel
    Prevent self-reviewthe person who triggered the run can't approve it
    production:
      needs: staging
      runs-on: ubuntu-latest
      environment:
        name: production
        url: https://shop.example.com     # shown on the run and the repository's Deployments page
    

    Environment secrets are handed to the job only after the rules pass, so an unapproved run can't even read them. That's what makes production secrets safe even in a repository many people can push to.

    Concurrency: one deploy at a time

    Two deploys of the same environment at once can interleave and leave a half-updated server. A concurrency group allows one run at a time:

    concurrency:
      group: deploy-${{ github.ref }}
      cancel-in-progress: false      # deploys: let the running one finish, queue the next
    

    For pull request CI, cancel-in-progress: true is right instead: a new push makes the old run pointless.

    The GITHUB_TOKEN

    Every run gets a GITHUB_TOKEN automatically, a token for this repository that expires when the job ends. Its default permissions can be broad, so set them explicitly, at the top of every workflow, and widen only the jobs that need more:

    permissions:
      contents: read            # the whole workflow: read the code, nothing else
    
    jobs:
      release:
        permissions:
          contents: write       # this job creates a GitHub Release
      deploy:
        permissions:
          contents: read
          id-token: write       # this job asks for an OIDC token
    

    If a workflow step is ever compromised (a malicious dependency, a hijacked action), a read-only token limits what it can do. Set the repository default to read-only too: Settings → Actions → General → Workflow permissions.

    OIDC: no keys to steal

    The old way to deploy to AWS, Azure or GCP was to create a cloud access key and store it as a secret. That key works for anyone who gets it, from anywhere, until someone rotates it. OpenID Connect (OIDC) removes it:

    1. The job (with id-token: write) asks GitHub for a signed token that says which repository, branch, environment and workflow it is.
    2. It sends the token to the cloud. The cloud checks the signature and your trust policy.
    3. If they match, the cloud returns temporary credentials, valid for about an hour.

    The three steps, animated: ask GitHub for a signed token, AWS checks it against the trust policy, and one-hour credentials come back.© Animation: DevOps Academy

    The three steps, animated: ask GitHub for a signed token, AWS checks it against the trust policy, and one-hour credentials come back.© Animation: DevOps Academy

    On AWS, the trust policy of the deploy role decides exactly who may use it:

    {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {
          "StringEquals": {
            "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
            "token.actions.githubusercontent.com:sub": "repo:elonerajeev/shop-api:environment:production"
          }
        }
      }]
    }
    
    production:
      environment: production
      permissions:
        contents: read
        id-token: write
      steps:
        - uses: aws-actions/configure-aws-credentials@v6
          with:
            role-to-assume: arn:aws:iam::123456789012:role/shop-api-deploy
            aws-region: ap-south-1
        - run: aws sts get-caller-identity       # proves who the job is now
    

    The sub condition is the security: only jobs in this repository using the production environment (which itself requires approval and the main branch) can assume the role. A pull request, a fork or another repository gets nothing. Always pin sub; a trust policy that checks only aud lets every repository on GitHub use your role.

    In production

    Give the deploy role only what deploying needs (for example, update one ECS service or write to one S3 bucket), not administrator access. Use separate roles for staging and production, each trusting only its own environment. Delete any old access keys once OIDC works: they're the thing attackers look for.

    ⌘
    Mini mission

    Gate production behind an approval

    Add staging and production environments to shop-api. Staging deploys on every push to main; production needs staging to succeed, may only deploy from main, and waits for your approval. Store a secret per environment, keep the token read-only, and make sure only one deploy runs at a time.

    Input: The shop-api repository on GitHub, with ghOutput: A run where production waits for approval, then deploys
    Reveal one safe solution
    $ mkdir -p scripts && cat > scripts/deploy.sh <<'EOF'
    #!/usr/bin/env bash
    set -euo pipefail
    target=${1:?usage: deploy.sh <staging|production>}
    [[ -n ${API_KEY:-} ]] || { echo "API_KEY is not set for $target" >&2; exit 1; }
    echo "Deploying shop-api $GITHUB_SHA to $target (log level ${LOG_LEVEL:-info})"
    echo "API key is set (${#API_KEY} characters)"   # prove it arrived without printing any of it
    EOF
    $ chmod +x scripts/deploy.sh
    $ gh api -X PUT "repos/{owner}/{repo}/environments/staging" > /dev/null
    $ gh api -X PUT "repos/{owner}/{repo}/environments/production" > /dev/null
    $ gh secret set PAYMENTS_API_KEY --env staging --body "staging-demo-key"
    $ gh secret set PAYMENTS_API_KEY --env production --body "production-demo-key"
    $ gh variable set LOG_LEVEL --body info
    $ nano .github/workflows/deploy.yml     # this lesson's workflow, without the AWS steps
    $ actionlint && git add . && git commit -m "ci: deploy to staging, then production with approval" && git push
    
    mkdir -p scripts && cat > scripts/deploy.sh <<'EOF'
    • mkdirmake a directory
    • -pcreate missing parents, no error if it exists
    • &&run the next command only if this one worked
    • catprint a file
    • >write output to a file, replacing it
    chmod +x scripts/deploy.sh
    • chmodchange permissions
    actionlint && git add . && git commit -m "ci: deploy to staging, then production with approval" && git push
    • gitversion control
    • addstage changes
    • .this folder
    • commitsave staged changes
    • pushupload commits

    Then in Settings → Environments → production, add yourself under Required reviewers and limit Deployment branches to main. Push again: staging deploys, and production shows Waiting for review until you click Review deployments → Approve. The log line API key is set (19 characters) proves the secret arrived without printing any part of it: even a few characters of a key help an attacker. Add the AWS steps once you've created the role from the AWS course.

    Try it yourself

    0 of 6 steps done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    What is the difference between a secret and a variable in GitHub Actions?

    Go deeper

    Official reading for this lesson

    • DocsGitHub Docs: using secrets in GitHub ActionsGitHub Docs ↗
    • DocsGitHub Docs: managing environments for deploymentGitHub Docs ↗
    • DocsGitHub Docs: OpenID ConnectGitHub Docs ↗
    • DocsGitHub Docs: configuring OpenID Connect in Amazon Web ServicesGitHub Docs ↗
    • Toolaws-actions/configure-aws-credentialsAWS ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousWorkflow details: expressions, matrices, caches and artifactsNext lesson →Testing in pipelines: lint, unit, integration and the gate