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 mainTrigger 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?
| Value | Secret or Variable? | Why |
|---|---|---|
| AWS access key | Secret | Grants real access if exposed |
AWS region name (us-east-1) | Variable | Not sensitive, just configuration |
| Database password | Secret | Grants real access if exposed |
| Database hostname | Usually Variable | Rarely sensitive on its own |
| API token | Secret | Grants real access if exposed |
| Application name / log level | Variable | Purely 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-identityTry 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 pushAdd 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-demoDelete the AWS_REGION variable and DEMO_TOKEN secret from the demo repository’s Settings once finished.