GitHub Actions Tutorial — Secrets
Secrets
Secrets store sensitive data — API keys, tokens, passwords — encrypted and accessible only during workflow execution. They are never printed in logs.
Creating Secrets
Repository secrets: Settings → Secrets and variables → Actions → New repository secret
Organization secrets: Organization Settings → Secrets → New organization secret (can be scoped to specific repos)
Environment secrets: Settings → Environments → Select environment → Add secret
Secret Hierarchy
| Level | Scope | Defined In |
|---|---|---|
| Environment | One environment only | Repo → Environments |
| Repository | All workflows in repo | Repo → Settings |
| Organization | All repos (or selected repos) | Org → Settings |
If the same secret name exists at multiple levels: Environment > Repository > Organization.
Using Secrets in Workflows
Access secrets with the secrets context:
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
echo "Deploying with API key..."
./deploy.sh
Important Rules
- Secrets are NOT available in
if:conditions directly — assign to env first - Secrets are automatically masked in logs — if the value appears in output, it shows
*** - Secrets can’t be used at workflow-level
env:— use job or step level - Fork PRs don’t have access to secrets (security measure)
Secret Masking
GitHub automatically masks secret values in logs:
steps:
- name: This won't expose the secret
env:
TOKEN: ${{ secrets.MY_TOKEN }}
run: echo "Token is $TOKEN"
# Output: Token is ***
Structured Secret Masking
If your secret is a JSON object, individual values may NOT be masked. Use environment variables for each field:
steps:
- name: Safe approach
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_PASS: ${{ secrets.DB_PASS }}
run: ./connect-db.sh
Secrets with Docker
Pass secrets to Docker build steps:
steps:
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myorg/myapp:latest
build-args: |
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
Warning: Build args are visible in the image history. For true build secrets, use Docker BuildKit secrets:
steps:
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myorg/myapp:latest
secrets: |
"npm_token=${{ secrets.NPM_TOKEN }}"
Limits
| Limit | Value |
|---|---|
| Secret name max length | 255 characters |
| Secret value max size | 48 KB |
| Max secrets per repo | 100 |
| Max secrets per org | 1,000 |
| Max secrets per environment | 100 |
For values larger than 48 KB, store the encrypted file in your repo and use a passphrase secret to decrypt it.
Common Mistakes
| Mistake | Fix |
|---|---|
Secret value shows *** but is empty |
Secret name is case-sensitive; check exact name in Settings |
| Secret not available in fork PR | Expected behavior — use pull_request_target if needed (carefully) |
Secret in workflow-level env: |
Move to job-level or step-level env: |
| Committing secrets to repo | Use .gitignore, git-secrets, or GitHub’s secret scanning |
Quick Reference
| Concept | Syntax |
|---|---|
| Access secret | ${{ secrets.NAME }} |
| In env var | env: KEY: ${{ secrets.NAME }} |
| Environment secret | ${{ secrets.NAME }} (with environment:) |
| Masked in logs | Automatic for all secrets |
| Max size | 48 KB per secret |
← Previous: Environment Variables
Next: GITHUB_TOKEN →