Secrets, Environments & Security Best Practices
Why Secret Management Matters
A secret is any piece of sensitive information your application or CI/CD pipeline needs to function: API keys, database passwords, TLS private keys, OAuth client secrets, deployment credentials, and similar values. Handling secrets incorrectly is one of the most common and consequential security mistakes in software development.
The risks are real: credentials committed to a public GitHub repository are indexed by search engines within minutes and by specialized credential-hunting services within seconds. GitHub's own security team has documented cases where compromised API keys have led to data breaches, cryptocurrency mining on cloud accounts generating five-figure bills in hours, and complete infrastructure takeovers. These are not theoretical risks.
The fundamental rule is simple: secrets never live in your code, your configuration files, your documentation, or your repository in any form. They are injected at runtime by a secrets management system. GitHub's built-in secrets system is the secrets management layer for your GitHub Actions workflows.
What GitHub Secrets Are
GitHub Secrets are encrypted key-value pairs stored at the repository, environment, or organization level. Their key security properties:
- Encrypted at rest using libsodium public-key cryptography. Even GitHub employees cannot read the stored values.
- Masked in logs: if a secret's value appears in a workflow log, it is replaced with
***. This is not foolproof (if you base64-encode a secret it may not be masked) but provides a safety net. - Scoped access: secrets are only accessible to workflows running on the repository or organization they are stored in.
- Not accessible to forked repositories by default: workflows triggered by pull requests from forks cannot read secrets (a critical security boundary for open source projects).
Repository Secrets
Repository secrets are available to all workflows in a specific repository.
Creating Repository Secrets
Go to Settings → Secrets and variables → Actions → New repository secret.
Enter:
- Name: conventionally uppercase with underscores —
GROQ_API_KEY,DATABASE_URL,STRIPE_SECRET_KEY - Value: the actual secret value
Click Add secret.
Using Repository Secrets in Workflows
Note: the ${{ secrets.NAME }} syntax is only valid inside workflow YAML. Never include it in scripts committed to your repository as a template — the ${} will error or be interpreted as a shell variable substitution.
Updating and Deleting Secrets
Secrets can be updated (but the current value is never shown — updating always replaces). Navigate to the secret and click Update.
To delete: navigate to the secret and click Remove.
The GITHUB_TOKEN Secret
GitHub automatically creates a special secret called GITHUB_TOKEN for every workflow run. It is a short-lived token with permissions scoped to the repository and valid only for the duration of the workflow run.
GITHUB_TOKEN is the right tool for:
- Creating releases and uploading release assets
- Commenting on PRs from a workflow
- Creating commits (e.g., updating a changelog)
- Interacting with the GitHub API from within a workflow
The token's permissions are set in the workflow with a permissions block:
Use the principle of least privilege: only grant the permissions the workflow actually needs.
Organization Secrets
Organization secrets are available to multiple repositories within a GitHub organization, eliminating the need to duplicate the same secret in every repository.
Creating Organization Secrets
Go to the organization's Settings → Secrets and variables → Actions → New organization secret.
In addition to name and value, configure Repository access:
- All repositories — every repository in the organization can use this secret
- Private repositories — only private repositories (useful for keeping secrets away from public forks)
- Selected repositories — explicitly choose which repositories can access the secret
Example: Shared Deployment Credentials
A deployment key used across all services in an organization:
Any workflow in those repositories can use ${{ secrets.AWS_DEPLOY_KEY_ID }} without each repository maintaining its own copy.
Environments
GitHub Environments add a governance layer on top of secrets. An environment represents a deployment target — typically production, staging, and preview — and can have:
- Environment-specific secrets — secrets that differ per environment (e.g., different database URLs for staging vs production)
- Environment variables — non-secret configuration that differs per environment
- Protection rules — require human approval, impose wait timers, or restrict which branches can deploy
Creating Environments
Go to Settings → Environments → New environment. Name it (e.g., production, staging).
Environment Secrets
After creating an environment, add secrets and variables to it:
Settings → Environments → production → Add secret
Environment secrets override repository secrets of the same name for workflows deploying to that environment. This allows you to use the same secret name (e.g., DATABASE_URL) in workflows but have different values per environment.
Using Environments in Workflows
Environment Protection Rules
Protection rules add human gates and timing controls to deployments.
Required reviewers: Before a job targeting this environment can run, the listed people or teams must approve it. Up to 6 reviewers/teams can be required.
When a workflow reaches a job targeting production, it pauses and sends approval requests. Reviewers see the pending deployment and can approve or reject it.
Wait timer: Adds a mandatory delay (1-43,200 minutes) after a deployment is triggered before it actually runs. Useful for canary deployment patterns where you want to observe metrics before proceeding.
Deployment branches: Restrict which branches can deploy to this environment.
This prevents a feature/experiment branch from accidentally deploying to production even if someone triggers the workflow manually.
Dependabot Secrets
Dependabot is GitHub's automated dependency update service (covered more in the security section). When Dependabot opens a PR to update a dependency, any CI workflow triggered by that PR cannot access regular repository secrets (for security reasons — Dependabot is effectively an external contributor).
Dependabot secrets are a separate set of secrets specifically available to Dependabot-triggered workflows:
Settings → Secrets and variables → Dependabot → New repository secret
Never Committing Secrets
The best way to handle secret leakage is prevention. Use these tools to catch secrets before they are committed.
git-secrets (AWS)
git-secrets is a pre-commit hook that scans staged changes for patterns matching known secret formats:
gitleaks
gitleaks is a more comprehensive secret scanning tool with detection rules for hundreds of secret types:
trufflehog
trufflehog specializes in finding high-entropy strings (random-looking tokens) and known secret formats:
Pre-commit Framework
The pre-commit framework manages pre-commit hooks declaratively:
What To Do If You Commit a Secret
If you accidentally commit a secret, act immediately. The clock is ticking.
Step 1: Revoke the Secret First
Before doing anything with the repository, go to whatever service issued the credential and revoke or rotate it. Change the password, revoke the API key, delete and recreate the credentials. This is the most urgent step — removing the secret from the repository history does not help if it was already scraped.
Step 2: Remove from Repository History
Use git filter-repo (the modern replacement for git filter-branch):
Step 3: Force Push and Notify
After rewriting history, force push all branches and delete all cached views:
Contact GitHub support to purge cached data if the repository is public.
Step 4: Rotate Again
Rotate the credential one more time after cleanup. Assume the original value was compromised and treat any access logs as potentially tainted.
GitHub Secret Scanning
GitHub automatically scans every commit pushed to repositories for patterns matching known secret formats from over 200 service providers (AWS, Google Cloud, Stripe, Twilio, etc.).
For Public Repositories
Secret scanning is enabled by default and free for all public repositories. When a secret is detected:
- GitHub alerts the repository owner.
- GitHub notifies the service provider directly (for participating providers), who can immediately revoke the exposed credential.
- A security alert appears in Security → Secret scanning.
For Private Repositories
Secret scanning for private repositories requires GitHub Advanced Security (GHAS), which is included in GitHub Enterprise Cloud and available as an add-on for organizations.
Push Protection
Push protection takes secret scanning one step further: it blocks pushes that contain secrets before they reach GitHub.
Enable for a repository: Settings → Code security and analysis → Secret scanning → Push protection: Enable.
When push protection is active and a secret is detected in a push:
The push is rejected. The developer must remove the secret from the commit before pushing.
GitHub Advanced Security Overview
GitHub Advanced Security (GHAS) is a suite of security features for organizations:
- Secret scanning (with push protection) — covered above
- Code scanning — static analysis using CodeQL or third-party tools to find security vulnerabilities in code
- Dependency review — shows the security impact of dependency changes in PRs (which new vulnerabilities are being introduced)
- Dependabot alerts — notifications when your dependencies have known CVEs
- Dependabot security updates — automatically opens PRs to fix vulnerable dependencies
- Dependabot version updates — automatically opens PRs to keep dependencies up to date
Enabling Dependabot Alerts and Updates
These are free for all repositories:
Settings → Code security and analysis:
- Enable Dependabot alerts
- Enable Dependabot security updates
- Enable Dependabot version updates (requires a
.github/dependabot.ymlconfiguration)
Dependabot Configuration
This configuration tells Dependabot to open PRs every Monday morning for outdated npm dependencies, and once a month for Docker base image updates.
Practical Exercises
Exercise 1 — Create and Use Repository Secrets
- In a repository, create two secrets:
TEST_API_KEYwith valuetest-key-12345andTEST_DB_URLwith valuepostgres://localhost/testdb. - Create a workflow that uses both secrets in environment variables and prints a masked version (e.g.,
echo "Key length: ${#TEST_API_KEY}"). - Run the workflow and check the logs — verify the actual values are masked.
Exercise 2 — Set Up Environments
- Create two environments:
stagingandproduction. - Add a secret
DATABASE_URLto each with different values. - Add a required reviewer to the
productionenvironment. - Create a workflow with two jobs targeting each environment.
- Run the workflow — observe that the
productionjob waits for approval.
Exercise 3 — Install Gitleaks
- Install
gitleakslocally. - Run
gitleaks detecton a repository. - Deliberately add a fake secret (e.g., an AWS-format access key) to a file and run
gitleaks detect --stagedbefore committing. - Verify gitleaks catches it.
- Remove the fake secret.
Exercise 4 — Configure Dependabot
- In a repository with a
package.json(orrequirements.txt), create.github/dependabot.yml. - Configure Dependabot to check for updates weekly.
- Verify that Dependabot alerts are enabled in the security settings.
- If you have outdated dependencies, check the Security tab for any existing alerts.