Secrets and Configuration Variables
Workflows rarely run in isolation. They connect to cloud providers, package registries, notification services, and databases — all of which require credentials and configuration that changes between environments. GitHub provides two dedicated mechanisms for this: secrets for sensitive values and configuration variables (repository variables) for non-sensitive settings.
Why Not Just Use Environment Variables?
Environment variables defined with the env: key live inside the workflow file itself, which is committed to the repository. That makes them visible to anyone with read access to the repository. For an API key, a deploy token, or a webhook URL, this is unacceptable. Even for a non-sensitive value such as a Slack channel name or a deploy target region, hardcoding it in the workflow file means a team must open a pull request just to change a setting.
Secrets and configuration variables solve both problems:
Secrets
Encrypted at rest. Never printed in log output — GitHub automatically masks them. Accessed via the secrets context: ${{ secrets.SECRET_NAME }}.
Configuration variables (vars)
Stored in plain text, visible in the GitHub UI, suitable for non-sensitive settings. Accessed via the vars context: ${{ vars.VARIABLE_NAME }}. They can be changed in the UI without touching the workflow file.
Secrets vs Configuration Variables at a Glance
| Property | Secret | Configuration Variable |
|---|---|---|
| Stored as | Encrypted | Plain text |
| Visible in GitHub UI after saving | No (write-only) | Yes |
| Masked in run logs | Yes (automatically) | No |
| Context expression | ${{ secrets.NAME }} | ${{ vars.NAME }} |
| Available scopes | Repository, Environment, Organization | Repository, Environment, Organization |
| Typical use cases | API keys, tokens, passwords, webhook URLs | Target region, feature flags, channel names, log levels |
Creating a Secret or Configuration Variable
Both types are created through the same part of the GitHub UI. The path is: Repository → Settings → Security → Secrets and variables → Actions. From there, choose either the Secrets tab or the Variables tab.
- Navigate to your repository on GitHub.
- Click Settings in the top navigation bar.
- In the left sidebar, under Security, click Secrets and variables.
- Click Actions.
- Select the Secrets or Variables tab.
- Click New repository secret or New repository variable.
- Enter a name (uppercase with underscores is the convention) and a value.
- Click Add secret or Add variable to save.
Organization-level secrets and variables follow the same steps but are reached via the Organization settings page and include an extra Repository access option that lets you scope them to all repositories, public repositories only, or a selected list.
After saving a secret its value cannot be read back — not even by a repository admin. It can only be overwritten. Configuration variable values remain readable in the UI.
Accessing Secrets and Variables in a Workflow
Consider a web application release pipeline that deploys to a cloud platform and posts a Slack notification when complete. Three categories of data are needed:
- DEPLOY_API_KEY — credential to authenticate with the cloud provider (secret)
- SLACK_WEBHOOK_URL — URL that grants permission to post messages (secret)
- DEPLOY_TARGET — staging or production (configuration variable)
- SLACK_CHANNEL — which channel to post to (configuration variable)
Referencing a configuration variable
The vars context makes the value available anywhere a context expression is supported: job names, if: conditions, run: scripts, and with: inputs to actions.
Referencing a secret
The secret is mapped into the step's environment using env:. Assigning it at step scope — rather than job or workflow scope — limits its exposure to only the step that actually needs it. The value is masked in all log output regardless of scope.
Configuration Variables as Feature Flags
One practical pattern is using a configuration variable as a runtime toggle. Because variables can be changed in the GitHub UI without opening a pull request, they are well-suited to enabling or disabling optional workflow jobs on demand.
Set AUDIT_ON_RELEASE to false in the Variables UI and the entire audit job is skipped on the next push — no code change required. Notice that AUDIT_LEVEL is a variable (the log level is not sensitive) while AUDIT_TOKEN is a secret (it authenticates with an external service).
Combining Secrets, Variables, and Environment Variables
All three mechanisms can coexist in the same workflow. A common pattern is to bridge a configuration variable into a local environment variable so that shell scripts can reference it with the simpler $NAME syntax instead of the full expression syntax.
The key distinction between the three types when they appear together:
| Type | Where it lives | Survives workflow edits? | Masked in logs? |
|---|---|---|---|
Inline env var (env:) | Workflow file (git) | Yes, until edited | No |
Configuration variable (vars) | GitHub UI | Yes, independently | No |
Secret (secrets) | GitHub (encrypted) | Yes, independently | Yes |
Scope: Repository, Environment, and Organization
Both secrets and configuration variables can be defined at three scopes. The narrowest scope always takes precedence when names collide.
Repository
Available to all workflows in one repository. The most common scope for project-specific credentials such as a container registry password.
Environment
Tied to a named deployment environment (for example, staging or production). A job must declare environment: staging to gain access. This allows different secrets per environment — for instance, different API keys for staging and production — without duplicating job definitions.
Organization
Shared across multiple repositories in an organization. Ideal for company-wide bot tokens or a central Slack webhook that all teams use for release notifications. Repository access can be limited to all, public-only, or selected repositories.
Connecting a job to an environment
Secret Masking in Practice
GitHub scans every line of log output for values that match a registered secret and replaces them with ***. This means an accidental echo of the secret value is safe — but it also means a secret that has been split across multiple concatenations may not be fully masked.
Masking protects against accidental exposure, but it is not a substitute for good secret hygiene. Never construct a secret by concatenating smaller pieces in a run: script — the fragments may appear unmasked.Key Takeaways
- Use secrets for any value that authenticates or grants access: API keys, tokens, passwords, webhook URLs.
- Use configuration variables for non-sensitive settings that need to change per environment or be adjusted without a code change.
- Access secrets with
${{ secrets.NAME }}and configuration variables with${{ vars.NAME }}. - Prefer step-level
env:injection for secrets to limit their exposure to the job steps that actually need them. - Configuration variables work as feature flags: toggle a job on or off from the GitHub UI with no pull request required.
- Environment-scoped secrets allow different credentials per deployment target (staging vs. production) within the same workflow.
- GitHub automatically masks secret values in all log output — but never intentionally split or echo secrets in ways that could defeat masking.
Complete Working Example
The following is the full workflow.yaml for this topic. It demonstrates a three-job release pipeline (deploy, notify, audit) that uses secrets, configuration variables, and environment variables together.