DevToolBoxFREE
BlogAdvertise

GitHub Actions Secrets og Sikkerhet: Miljøer, OIDC og Best Practices

12 minby DevToolBox

GitHub Actions secrets allow you to store sensitive data — API keys, passwords, tokens, certificates — securely in your repository settings and use them in workflows without exposing them in your code. This guide covers everything from basic secret usage to advanced patterns like OIDC federation, environment-scoped secrets, and automated rotation.

Quick Answers: GitHub Actions Secrets, GITHUB_TOKEN, and gh secret set

Use this short decision guide when you need to configure a secret quickly and safely.

  • Repository secrets are available only to workflows in one repository. Create them in Settings > Secrets and variables > Actions, or with gh secret set API_TOKEN.
  • Environment secrets are tied to a named environment such as production. A job must select that environment before those secrets are available, and environment protection rules can require reviewers or branch restrictions before release jobs run.
  • Organization secrets can be shared across repositories, but organization owners should limit access to selected repositories whenever the credential is not truly global.
  • GITHUB_TOKEN is created automatically for each workflow run. You can reference it as secrets.GITHUB_TOKEN, but actions can also access github.token, so always set the minimum permissions block your workflow needs.
  • Secrets cannot be referenced directly in if: conditionals. If a secret is unset, the secrets context returns an empty string; move a boolean or the value into env before testing it.
  • Secrets other than GITHUB_TOKEN are not passed to workflows triggered from forked repositories. Treat pull_request_target workflows as privileged and never run untrusted fork code with secrets.
# Create or update a repository secret
gh secret set API_TOKEN --body "$API_TOKEN"

# Create an environment-scoped secret for production deploys
gh secret set DATABASE_URL --env production --body "$DATABASE_URL"

# Create an organization secret and limit it to selected repositories
gh secret set SENTRY_AUTH_TOKEN \
  --org ORG_NAME \
  --repos web-app,api-service \
  --body "$SENTRY_AUTH_TOKEN"

# List configured secret names without revealing values
gh secret list
gh secret list --env production
gh secret list --org ORG_NAME
# Use the automatic GITHUB_TOKEN with least privilege
permissions:
  contents: read
  pull-requests: write

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: 'CI completed.'
            })
# Secrets cannot be referenced directly in if:
# Move the value into an env var, then test the env var.
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      HAS_DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN != '' }}
    steps:
      - name: Deploy only when the secret exists
        if: env.HAS_DEPLOY_TOKEN == 'true'
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./deploy.sh

Basic Secret Usage

Secrets are defined in your repository settings (Settings > Secrets and variables > Actions) and referenced in workflows using the secrets context. They are automatically masked in workflow logs.

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to server
        env:
          # Reference a repository secret
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_TOKEN: ${{ secrets.API_TOKEN }}
        run: |
          echo "Deploying with secure credentials..."
          ./deploy.sh

Environment-Scoped Secrets

GitHub Environments let you define secrets that are specific to a deployment environment (staging, production) and add protection rules like required reviewers.

# Environment-specific secrets (staging vs production)
name: Deploy

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    # Pick environment based on branch
    environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}

    steps:
      - uses: actions/checkout@v4

      - name: Deploy
        env:
          # These come from the selected environment's secrets
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: ./deploy.sh --env ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}

OIDC Federation: No Long-Lived Secrets

The best practice in 2026 is to use OIDC (OpenID Connect) federation to authenticate with cloud providers instead of storing long-lived access keys. GitHub Actions can exchange a JWT token for temporary cloud credentials.

# OIDC: No long-lived credentials needed!
name: Deploy to AWS (OIDC)

on:
  push:
    branches: [main]

permissions:
  id-token: write   # Required for OIDC
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
          # No ACCESS_KEY or SECRET_KEY secrets needed!

      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-bucket --delete

Secret Masking and Safe Usage

GitHub automatically masks secret values in logs, but there are important caveats to understand to avoid accidental exposure.

# Secrets are automatically masked in logs
# But be careful with derived values!

jobs:
  careful:
    runs-on: ubuntu-latest
    steps:
      - name: Use secret safely
        env:
          MY_SECRET: ${{ secrets.MY_SECRET }}
        run: |
          # This output will be masked: ***
          echo "Secret value: $MY_SECRET"

          # DANGER: This may expose the secret!
          # If MY_SECRET="abc", base64 gives "YWJj" which is NOT masked
          echo $MY_SECRET | base64

          # Safe: always use secrets directly, not derived forms
          curl -H "Authorization: Bearer $MY_SECRET" https://api.example.com/endpoint

      - name: Add to mask (for dynamic secrets)
        run: |
          # Manually mask a dynamically generated value
          TOKEN=$(generate-token.sh)
          echo "::add-mask::${TOKEN}"
          echo "TOKEN=${TOKEN}" >> $GITHUB_ENV

Secrets in Reusable Workflows

When using reusable workflows (workflow_call), secrets must be passed explicitly with a secrets map, or intentionally forwarded with secrets: inherit. Do not assume a called workflow receives credentials unless the caller passes them.

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy Workflow

on:
  workflow_call:
    secrets:
      DEPLOY_KEY:
        required: true
      DATABASE_URL:
        required: true
      SLACK_WEBHOOK:
        required: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: ./deploy.sh

      - name: Notify Slack
        if: ${{ secrets.SLACK_WEBHOOK != '' }}
        run: |
          curl -X POST -H 'Content-type: application/json' \
            --data '{"text":"Deployment complete!"}' \
            ${{ secrets.SLACK_WEBHOOK }}

# .github/workflows/production.yml — caller
name: Production Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    secrets:
      DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
      DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
      SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

  # Alternative for trusted same-organization workflows:
  # deploy:
  #   uses: org/reusable/.github/workflows/deploy.yml@main
  #   secrets: inherit

Automated Secret Rotation

Long-lived secrets are a security risk. Set up automated rotation using scheduled workflows.

# Automated secret rotation pattern
name: Rotate API Token

on:
  schedule:
    - cron: '0 0 1 * *'  # Monthly rotation
  workflow_dispatch:       # Manual trigger

jobs:
  rotate:
    runs-on: ubuntu-latest
    steps:
      - name: Generate new token
        id: generate
        env:
          OLD_TOKEN: ${{ secrets.API_TOKEN }}
        run: |
          # Call API to generate new token using old token
          NEW_TOKEN=$(curl -s -X POST \
            -H "Authorization: Bearer $OLD_TOKEN" \
            https://api.example.com/tokens/rotate | jq -r '.token')
          echo "::add-mask::${NEW_TOKEN}"
          echo "new_token=${NEW_TOKEN}" >> $GITHUB_OUTPUT

      - name: Update GitHub secret
        uses: gliech/create-github-secret-action@v1
        with:
          name: API_TOKEN
          value: ${{ steps.generate.outputs.new_token }}
          github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

Secret Storage Options Comparison

OptionScopeVisibilityRotationCostBest For
GITHUB_TOKENCurrent workflow runAutomaticPer runFreeGitHub API automation
Repository SecretsSingle repoRepo adminsManualFreeSimple projects
Environment SecretsSingle repo + envRepo adminsManualFreeMulti-stage deploys
Organization SecretsMultiple reposOrg adminsManualFreeShared credentials
OIDC FederationSingle repoNo stored secretAutomaticFreeCloud deployments
HashiCorp VaultAnyConfigurableAutomaticPaidEnterprise

Security Best Practices

  1. Use OIDC federation for cloud providers (AWS, GCP, Azure) instead of static access keys whenever possible.
  2. Scope secrets to environments. Production secrets should require environment protection rules (required reviewers, deployment branches).
  3. Never print secrets in workflow steps, even for debugging. GitHub masks the exact string, but encoding/transformations bypass masking.
  4. Rotate secrets regularly. Set calendar reminders or use automated rotation workflows for API keys.
  5. Use the minimum required permissions. Create service accounts with narrow scopes specifically for GitHub Actions.
  6. Audit secret access via GitHub's audit log. Review which workflows are using which secrets.

Frequently Asked Questions

Can forked repositories access secrets?

No. By default, secrets are not passed to workflows triggered by pull requests from forks. This prevents forked repos from exfiltrating your secrets. For public repos, you can configure which events allow secrets for external contributors, but this requires explicit approval.

What is the difference between repository secrets and organization secrets?

Repository secrets are only available to workflows in that specific repository. Organization secrets can be shared across multiple repositories and have a policy setting that controls which repos can access them (all repos, selected repos, or only private repos).

How many secrets can I store?

GitHub allows up to 100 secrets per repository, 100 secrets per environment, and 1,000 secrets per organization. Each secret is limited to 48 KB. If a repository has access to more than 100 organization secrets, only the first 100 organization secrets sorted alphabetically are available to a workflow.

Can I read a secret value after it is stored?

No. Once a secret is created, its value cannot be read through the GitHub UI or API. You can only update or delete it. This is by design — if you lose track of a secret, create a new one and rotate the credentials.

How do I pass secrets between jobs in a workflow?

Secrets cannot be directly passed between jobs as job outputs (outputs are visible in logs). Instead, re-reference the secret in each job that needs it using secrets.MY_SECRET, or generate a temporary credential in one job and pass it through encrypted artifact storage using GPG.

Is secrets.GITHUB_TOKEN the same as GITHUB_TOKEN?

The automatic GITHUB_TOKEN is generated for each workflow run and can be referenced as secrets.GITHUB_TOKEN. Some actions can also access it through the github.token context even if you do not pass it explicitly, so restrict it with a permissions block.

Why is my GitHub Actions secret empty in an if condition?

GitHub Actions does not allow direct secrets references in if: conditionals. An unset secret resolves to an empty string when referenced in expressions. Put the check or value into an environment variable first, then test env.MY_FLAG or env.MY_SECRET in the conditional.

Var dette nyttig?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy