Skip to content

Variables vs Secrets


By now you’ve used env: values, repository/environment/organization secrets, and GitHub also has a separate feature called Variables (vars) that looks suspiciously similar to secrets — same tiered scoping, same Settings UI, same ${{ }} reference syntax — but exists for a genuinely different purpose. Knowing which one to reach for isn’t just a style choice; using secrets for things that don’t need to be secret, or variables for things that do, causes real problems.

What vars Actually Is

Repository (and environment, and organization) Variables are configured through Settings → Secrets and variables → Actions → Variables tab — right next to where you set secrets, which is exactly why people conflate the two. Unlike secrets, variable values are visible in plain text in the Settings UI and are not masked in logs.

name: Vars Demo
on: workflow_dispatch

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - run: echo "AWS region configured as ${{ vars.AWS_REGION }}"

Set up a repository variable named AWS_REGION with value us-east-1 through the Settings UI, then push and run this:

mkdir gha-vars-demo && cd gha-vars-demo
git init
mkdir -p .github/workflows
cat > .github/workflows/vars-demo.yml << 'EOF'
name: Vars Demo
on: workflow_dispatch

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - run: echo "AWS region configured as ${{ vars.AWS_REGION }}"
EOF
git add .
git commit -m "Add vars demo workflow"
git branch -M main
git push -u origin main

Trigger it manually — the log prints AWS region configured as us-east-1 in full, plainly visible, exactly as expected. Compare that to a secret, where the same pattern would show ***.

The Actual Decision: Is It Sensitive?

The dividing line isn’t “is it configuration” versus “is it code” — plenty of configuration is genuinely sensitive. The real question is: would it cause harm if someone with read access to this repository (or its logs) saw the plain value?

ValueSecret or Variable?Why
AWS access keySecretGrants real access if exposed
AWS region name (us-east-1)VariableNot sensitive, just configuration
Database passwordSecretGrants real access if exposed
Database hostnameUsually VariableRarely sensitive on its own
API tokenSecretGrants real access if exposed
Application name / log levelVariablePurely descriptive configuration

us-east-1 sitting in plain text harms nobody. An AWS secret access key sitting in plain text is a genuine incident. That distinction is the entire rule.

Why It Matters To Get This Right

Putting a non-sensitive value in secrets isn’t catastrophic, but it does cost you: secret values are masked everywhere, including in situations where seeing the plain value would genuinely help debugging (like confirming which AWS region a job actually targeted, right there in the log, without needing to cross-reference Settings). It also implies a false sense of “this is sensitive” to anyone reading the workflow later, who then has to go check Settings just to find out it’s actually a plain region string.

Going the other direction is worse: putting a genuinely sensitive value in vars means it’s plainly visible in every log line that references it, and visible in plaintext to anyone with read access to repository settings — which is typically a much larger group of people than those with actual deploy or infrastructure access. This is a real, if easy to make, security mistake.

Using Both Together

A realistic AWS deployment step commonly combines both, side by side, each doing the job it’s suited for:

- name: Deploy to AWS
  env:
    AWS_REGION: ${{ vars.AWS_REGION }}
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  run: |
    echo "Deploying to region: $AWS_REGION"
    aws sts get-caller-identity

Try this shape (with placeholder values, since real AWS credentials aren’t needed just to see the pattern):

cat > .github/workflows/combined.yml << 'EOF'
name: Combined Vars and Secrets Demo
on: workflow_dispatch

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - name: Show configuration
        env:
          AWS_REGION: ${{ vars.AWS_REGION }}
          AWS_ACCESS_KEY_ID: ${{ secrets.DEMO_TOKEN }}
        run: |
          echo "Region (plainly visible): $AWS_REGION"
          echo "Access key (masked): $AWS_ACCESS_KEY_ID"
EOF
git add .
git commit -m "Add combined vars and secrets demo"
git push

Add a repository secret named DEMO_TOKEN with any placeholder value, then trigger this manually — the region prints in full, the placeholder “access key” prints as ***. Same syntax pattern, same env: block, completely different visibility behavior, purely because one came from vars and the other from secrets.

Scoping Works Identically

Variables support the same three tiers as secrets — repository, environment, and organization — with the same resolution order (environment beats repository beats organization). Everything covered about that scoping earlier applies to vars exactly as written, just swap the word “secret” for “variable.”

How Real DevOps Engineers Use This

A clean, well-organized workflow file tends to have a short, consistent block of vars for things like region, environment name, and feature flags, sitting clearly apart from a short block of secrets for credentials — making it easy for a reviewer to scan a pull request and immediately see what’s sensitive and what isn’t, without needing to check Settings. Teams that get sloppy about this distinction — throwing everything into secrets “to be safe,” or the reverse — tend to end up with either debugging pain (nothing useful visible in logs) or genuine security exposure (real credentials sitting in plain text). The rule is simple enough that there’s rarely a good excuse to get it wrong once you’ve internalized it: if exposing the plain value in a log would cause harm, it’s a secret; if it wouldn’t, it’s a variable.

Clean up:

cd .. && rm -rf gha-vars-demo

Delete the AWS_REGION variable and DEMO_TOKEN secret from the demo repository’s Settings once finished.

Last updated on