When using GitHub Actions to deploy your applications, you inevitably need to handle sensitive data: API keys, database connection strings, and cloud credentials. Storing these directly in your code is a critical security risk. GitHub Actions Secrets allow you to store sensitive information securely, making it available to your workflows while ensuring it remains encrypted and masked in your logs.
1. Understanding the Secrets Hierarchy
GitHub provides three layers for storing secrets. Choosing the right one helps you enforce the “Principle of Least Privilege”:
- Organization Secrets: Managed at the org level. These are ideal for credentials used across multiple projects (e.g., a shared SonarQube token or a corporate Docker registry).
- Repository Secrets: The most common level. These are specific to one repository and accessible by any workflow within it.
- Environment Secrets: The most secure option. Tied to specific environments (like
ProductionorStaging), these allow you to add “Protection Rules.” For example, you can require a senior engineer to approve a deployment before the production database password is even decrypted.
Precedence: If a secret name exists in multiple places, the most specific one wins: Environment > Repository > Organization.
2. How to Set Up Repository Secrets
Adding secrets to your GitHub repository is straightforward. Follow these steps:
- Navigate to your repository on GitHub.
- Click on the Settings tab.
- In the left sidebar, under the “Security” section, click Secrets and variables > Actions.
- Click the New repository secret button.
- Name: Enter a name (e.g.,
DB_CONNECTION_STRING). It should be uppercase with underscores. - Secret: Paste the sensitive value.
- Click Add secret.
Once added, the secret is encrypted. You can update it or delete it, but you (and other users) can never view the plain-text value again via the UI.
3. Practical Example: Deploying a Full-Stack App
Let’s look at a workflow for a standard stack: an ASP.NET Core API, a Vue.js UI, and an Entity Framework Database Migration. This example demonstrates how to inject secrets into different stages of the pipeline.
name: Deploy Full Stack App
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
# Referencing the 'production' environment to use Environment Secrets
environment: production
steps:
- uses: actions/checkout@v4
# 1. Run Database Migrations (ASP.NET / Entity Framework)
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Install EF Tool
run: dotnet tool install --global dotnet-ef
- name: Run DB Migrations
run: |
dotnet ef database update --project ./MyApiProject
env:
# Secret used to connect to the DB for migrations
ConnectionStrings__DefaultConnection: ${{ secrets.PROD_DB_CONNECTION_STRING }}
# 2. Build and Deploy ASP.NET API
- name: Build API
run: dotnet publish ./MyApiProject -c Release -o ./publish-api
- name: Deploy API to Azure
uses: azure/webapps-deploy@v2
with:
app-name: 'my-production-api'
# Secret used for cloud authentication
publish-profile: ${{ secrets.AZURE_API_PUBLISH_PROFILE }}
package: ./publish-api
# 3. Build and Deploy Vue.js UI
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install & Build Vue
run: |
npm install
npm run build
working-directory: ./my-vue-ui
env:
# Injecting an API Key into the frontend build process
VITE_APP_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
- name: Deploy UI to S3
run: aws s3 sync ./my-vue-ui/dist s3://my-prod-web-bucket
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'us-east-1'
4. Best Practices for Secure Workflows
- Never Log Secrets: While GitHub attempts to mask secrets in logs (replacing them with
***), avoid commands that print environment variables or configuration files to the console. - Use Environment Secrets for Production: Always use the
environmentkeyword in your YAML. This ensures that production-level keys are only available when deploying to that specific target. - Limit Secret Access: Don’t give “Organization” level access to a secret unless it is truly required by every repository.
- Prefer OIDC Over Long-Lived Keys: For AWS or Azure, use OpenID Connect (OIDC) to get temporary credentials instead of storing permanent access keys in GitHub.
- Rotate Regularly: Treat your secrets like passwords. Change them every 90 days or immediately if a team member with access leaves the project.
By following this hierarchy and using the built-in masking features, you can automate your deployments without compromising the security of your infrastructure.
