How do GitHub Actions secrets work, and what is the difference between repository secrets, environment secrets, and organization secrets?
Quick Answer
GitHub Actions secrets are encrypted values stored in GitHub and exposed as environment variables during workflow runs. Repository secrets are scoped to a single repo, environment secrets to a specific deployment environment with approval gates, and organization secrets to multiple repos across an org. They are masked in logs and never exposed in plain text through the API.
Detailed Answer
Think of GitHub secrets like the different tiers of keys in a hotel. Organization secrets are the master key that opens every door on designated floors (repositories). Repository secrets are the room key that only works for one specific room (repository). Environment secrets are the minibar key that requires an extra confirmation (approval gate) before you can access premium items (production credentials). Each tier adds specificity and control, and the most specific key always takes precedence when there is a naming conflict.
GitHub encrypts secrets using libsodium sealed box encryption before they are stored. When you create a secret through the Settings UI or the API, the client fetches the repository's public key, encrypts the secret value locally, and sends only the ciphertext to GitHub. The plaintext value never traverses the network unencrypted and cannot be retrieved after storage; you can only overwrite or delete it. During a workflow run, GitHub decrypts the requested secrets and injects them as environment variables. The runner process has access to the decrypted values, but GitHub's log masking automatically redacts any output that matches a secret's value, replacing it with three asterisks. This masking is best-effort: if a secret is transformed (base64-encoded, split across lines, or included in a structured format like JSON), the masking may fail to catch it.
The three scopes serve different governance needs. Organization secrets are managed by org admins and can be scoped to all repositories, private repositories only, or a selected list of repositories. This is ideal for shared credentials like an artifact registry token or a Slack webhook URL that every repository needs. Repository secrets are managed by repository admins and are accessible only within that repository's workflows. They suit repo-specific credentials like a database password for a particular service. Environment secrets are tied to a deployment environment (such as staging or production) configured in the repository settings. Environments can require manual approvals, restrict which branches can deploy, and have wait timers, making environment secrets the most controlled tier. When you reference ${{ secrets.API_KEY }} in a workflow, GitHub resolves it by checking environment secrets first (if the job has an environment: declaration), then repository secrets, then organization secrets, using the first match found.
In production, a typical setup at a company like Acme Corp might have organization secrets for DOCKER_REGISTRY_TOKEN (shared across all service repos), SONAR_TOKEN (for code quality scans), and SLACK_WEBHOOK (for deployment notifications). The checkout-service repository would have repository secrets for CHECKOUT_DB_PASSWORD and STRIPE_API_KEY. The production environment would have environment secrets for PROD_AWS_ACCESS_KEY and PROD_AWS_SECRET_KEY, protected by a manual approval gate requiring sign-off from two members of the sre-team. This layered approach means developers can run CI tests using org-level tokens, but production deployment credentials are locked behind approval workflows and only accessible from the main branch.
A critical gotcha is that secrets are not available to workflows triggered by pull requests from forks. This is a deliberate security measure to prevent malicious fork PRs from exfiltrating secrets. If your CI workflow needs secrets for tests (like an API key for integration tests), you must either use the pull_request_target trigger (which runs in the context of the base repo, not the fork) or use a separate workflow that runs after the PR is approved. Another pitfall is secret sprawl: without regular audits, teams accumulate orphaned secrets for decommissioned services, and there is no built-in way to see when a secret was last used. Implement a quarterly secret rotation policy and use the GitHub API to audit the last-updated timestamp of each secret.