Doppler Logo
Nov 12, 2026
15 min read

GitHub Actions secrets aren't enough

GitHub Actions secrets aren't enough

The Megalodon campaign in May 2026 showed significant limitations of GitHub Actions secrets. It saw attackers automate 5,718 malicious commits across over 5,500 GitHub repositories. Using stolen Personal Access Tokens (PATs), they pushed malicious code containing base64-encoded Bash payloads to exfiltrate CI secrets, cloud credentials, SSH keys, and OIDC tokens. It also showed where GitHub Actions secrets stop. They're built to get credentials into a runner, not to rotate them, log every read, or limit who can see them.

GitHub Actions lacks security controls such as rotation, per-access audit logging, and per-secret access controls, all of which are essential for the safe use of secrets, even in CI. This article examines GitHub Actions secrets to help you understand its strengths, limitations, and when integrating an external secrets management solution becomes necessary.

What GitHub Actions secrets do (and what they don't)

GitHub Actions allows you to store secrets directly within GitHub and inject them into a workflow during execution. Unlike standard environment variables, GitHub secrets provide additional safeguards for sensitive values.

The structure was designed to allow pipelines to authenticate with external services without exposing raw keys in plaintext. However, it doesn't control who can read the secrets, how long they live, or whether anyone notices misuse. While those governance controls are missing, GitHub Actions still provides built-in protections through secret isolation and encryption.

Scope hierarchy

You can scope secrets within GitHub by organization, repository, and environment. Organization secrets allow cross-repository credential sharing. GitHub caps secret storage at this level at 1,000, and a single workflow run is capped at 100 organization secrets. Also, a free organization plan limits secret sharing to public repositories. A paid GitHub Team or Enterprise plan is required to share organization-level secrets with private repositories.

Repository secrets are stored at the individual repository level and are limited to 100 secrets per repository. Environment secrets are tied to explicit deployment targets such as production and staging, and are limited to 100 secrets per environment.

A limitation with the scope hierarchy is that it isolates secrets between repositories but not within a repository. So if attackers gain write access to a repository, they can modify workflows and potentially expose repository secrets, just like in the Megalodon campaign.

Masking and encryption mechanics

GitHub Actions secrets encrypt credentials at rest and mask them in logs. When a secret is saved in GitHub Actions, it is encrypted on the client side using Libsodium's sealed box encryption. GitHub stores the encrypted ciphertext and decrypts the secret only when it is needed during workflow execution. Within the runner, GitHub also intercepts the standard output (stdout) and standard error (stderr) streams during job execution to redact the decrypted secret value. But this protection mechanism can become ineffective when secrets are embedded in structured data, such as JSON, or transformed into encoded values such as Base64. The masking may not fire, leaving the secrets exposed in logs.

These features are meant to support safe storage and use of secrets, but there are critical security controls that GitHub Actions secrets cannot provide.

The four things GitHub secrets cannot do

GitHub Actions secrets were designed for developers to supply runners with secrets conveniently. The simplicity of the design isn't a flaw, but some risks remain difficult to address because the feature lacks several important security controls. Four of them are:

No per-access audit log

GitHub's native audit logging is management-focused. It creates a record when a configuration state changes, but generates no log when a workflow reads a secret at runtime. Without per-access logs, teams can't determine which workflow accessed a secret or when. The table below shows what GitHub Actions secrets audit logs capture compared with what auditors typically require.

What GitHub logsWhat auditors require

Secret creation: Timestamp and the user who created the secret.

Runtime access identity: The specific workflow run, job execution context, or calling entity that accessed the secret.

Secret update: Timestamp and the user who modified the secret.

Access timestamp: A record of when the secret was accessed.

Secret deletion: Timestamp and the user who deleted the secret.

Access history: Historical records showing how and when the secret was used.

Runtime access: Nothing. No log is generated when a workflow reads a secret.

Continuous access auditing: An audit trail that supports forensic investigations and compliance reviews.

No rotation or expiration enforcement

When you store secrets on GitHub, they remain permanent until manually overwritten. There is no provision for automated rotation or expiration. This directly contrasts with the OWASP Secrets Management Cheat Sheet, which emphasizes that security-conscious organizations should create secrets that expire after a defined period where possible. Tools with built-in controls that enforce this recommendation make incident response more efficient. In the CISA GitHub leak in May 2026, the exposed AWS keys remained valid for 48 hours after the account was taken down. That is a critical window during a breach, when attackers can continue to abuse compromised credentials.

No least-privilege access for secrets

GitHub uses a flat permission model within the repository boundary, so it cannot apply fine-grained Role-Based Access Control (RBAC) over secrets access. Even the GitHub Actions documentation states that, "Any user with write access to your repository has read access to all secrets configured in your repository." For example, a platform team might set up organization-wide secrets and expose them to a broad group of repositories. If attackers gain write permissions on a single low-impact repository, they could create a custom workflow on a feature branch to expose and steal those secrets. This means that if you do not apply least-privilege principles to the credentials stored in GitHub Actions, a single repository compromise can provide access to critical parts of your workflow, including databases and cloud resources.

No support for dynamic secrets

GitHub Actions secrets are strictly static values that cannot negotiate, mint, or provision just-in-time access credentials. Although GitHub Actions provides OpenID Connect (OIDC), which mitigates this limitation for cloud platforms such as AWS, Azure, and GCP by exchanging identity tokens for short-lived cloud credentials, it leaves everything else static. There are no dynamic secret configurations for database passwords, API credentials, or connection strings.

Lack of these controls reveals important security limitations in GitHub Actions secrets themselves. But even well-protected secrets can be exposed when GitHub workflows are configured incorrectly.

The fork and pull_request_target risk

Misusing pull_request_target can expose repository secrets to untrusted pull requests. When you use pull_request_target, the workflow runs in the security context of your base repository. This is one of the most common secret exposure vectors in Continuous Integration/Continuous Delivery (CI/CD). The CVE-2026-45132 case study identifies this vulnerability in CloudPirates Open Source Helm Charts. The flaw allowed a fork-controlled pull_request_target workflow to access PATs and SSH keys.

Why pull_request_target is dangerous

In a standard on: pull_request workflow triggered by an external repository fork, the runner executes in an unprivileged context and does not receive repository secrets.

With on: pull_request_target, the runner executes in the security context of the target repository and can access secrets made available to that workflow. If the pull request contains malicious code, an attacker can abuse that access to expose sensitive credentials.

However, you can avoid such unsafe configurations by adopting established GitHub patterns for handling external pull requests.

Safe patterns for external pull requests

There are two approaches you can use to ensure external pull requests interact safely with your repository.

The first is the use of a split workflow run. Here, code compilation and testing steps are strictly bound to a standard secretless on: pull_request workflow. Once this isolated run completes and saves the output artifacts, a follow-up workflow_run can execute in a privileged context. It can then download the untrusted artifact and perform tasks such as commenting or labeling without exposing code execution vectors to the fork contributor.

The second is implementing environment protection approvals. You can bind all workflows that handle high-value secrets to a GitHub environment that requires mandatory approval from a platform or security administrator before the job can run. Only after a human operator verifies the fork pull request are secrets injected into the runner.

The safe patterns above primarily protect against secrets exposure through external forks. They do not address what happens when a credential is eventually compromised through another route. In such situations, OIDC can help reduce the impact of the compromise.

When to use OIDC instead of stored secrets

OIDC is the recommended authentication model for GitHub Actions when accessing AWS, Azure, and GCP. It eliminates the need to store long-lived credentials and replaces them with short-lived, just-in-time cloud access tokens.

When a job requires cloud access, GitHub Actions requests an OIDC JSON Web Token (JWT) from GitHub. The token contains metadata about the execution context, including the repository owner, repository name, workflow file, and target environment. On the cloud side, a trust relationship is configured in the provider's identity and access management service to trust tokens signed by GitHub's issuer and map specific incoming claims to a scoped role.

When the runner transmits the JWT to the cloud provider's Security Token Service (STS), the provider validates it and returns temporary cloud credentials. For AWS, AssumeRoleWithWebIdentity sessions last from 15 minutes (900 seconds) to 12 hours (43,200 seconds), subject to the role’s configured maximum, with a default of one hour (3,600 seconds). Note that the GitHub-issued JWT and the resulting AWS credentials have separate expiration times.

The workflow below demonstrates how GitHub OIDC allows a runner to authenticate to AWS without storing long-lived cloud credentials in GitHub Actions secrets.

A primary limitation of GitHub's OIDC support is that it does not cover database credentials, internal APIs, Stripe or Twilio keys, or credentials outside the three major cloud providers. Some of these services can use OIDC, while others still depend on secrets that must be rotated. For safer handling of secrets outside AWS, Azure, and GCP within GitHub, you need an external secrets manager.

Integrating an external secrets manager

The aim of introducing an external secrets management tool into GitHub Actions is to address security gaps. An external secrets manager, such as Doppler, provides rotation, audit logging, and RBAC that GitHub Actions secrets do not. Depending on your workflow preference, you have to decide how best to integrate the manager with GitHub. Doppler offers two integration models.

The sync model: Doppler as the source of truth

In this model, Doppler acts as the administrative control panel where your team manages policies, team scoping, secret storage, and automated rotations. You configure the GitHub Actions sync in the Doppler dashboard, targeting a specific repository, organization, or environment. Doppler communicates directly with the configured scope and updates secret values in GitHub Actions secrets whenever a secret is rotated or added. This sync does not affect the workflow YAML configuration; you continue referencing ${{ secrets.STRIPE_API_KEY }} as usual.

The runtime-fetch model: OIDC with no stored token

This model eliminates the need to store static credentials inside GitHub. It combines the dopplerhq/secrets-fetch-action marketplace utility with GitHub OIDC identities to fetch keys from Doppler on demand at runtime. The secrets are made available to the runner for the duration of the job. Every credential fetched through this model is automatically registered with GitHub's log-redaction filter, allowing the runner to mask secrets when they appear in stdout or stderr. This model, however, requires modifying the workflow YAML to use dopplerhq/secrets-fetch-action@v2.0.0.

With either model, you gain better control over secrets governance while securely supplying secrets to GitHub workflows. GitHub Actions sync and runtime fetching with a service token are both available on Developer. OIDC-based runtime fetching is available on Team and Enterprise. Connect Doppler to GitHub Actions without changing how your workflows reference secrets.

Aside from secrets governance, external managers satisfy compliance requirements that GitHub secrets alone cannot satisfy.

Compliance requirements that GitHub secrets cannot satisfy alone

GitHub Actions secrets do not meet all the controls required by modern compliance frameworks. SOC 2 CC7.2 requires organizations to monitor system activity for anomalies and analyze potential security events. PCI DSS 7.2.5 requires least-privilege access for application and system accounts. These control requirements are only partially met, or not met at all, by using GitHub Actions secrets alone. Organizations whose workflows are subject to these audits would struggle to demonstrate compliance with this sole reliance. The table below highlights areas where GitHub Actions secrets fall short and where an external secrets manager can fill the gap.

Control requirementGitHub nativeExternal secrets manager

Access audit log

Absent. The platform generates no logs when a running workflow reads a secret.

Runtime secret access is logged and available for auditing.

Rotation evidence

Manual. Auditing relies on developer documentation because the platform does not generate rotation evidence.

Programmatic. Generates event logs that document automated secret rotation activities.

RBAC below repository level

Limited. Write access within a repository can expose all repository secrets available to that workflow context.

Granular. Allows security teams to restrict access to specific secrets, environments, or workloads.

TTL enforcement

Non-existent. Secret values remain valid until they are manually updated or replaced.

Dynamic. Supports short-lived credentials that automatically expire after a defined TTL.

Despite these limitations, when GitHub Actions controls are used correctly, they can help reduce the risk of secret exploitation.

GitHub Actions secrets best practices checklist

Below is a checklist of the controls GitHub offers natively, along with how to use them. They help reduce exposure, but remember that they do not close audit, rotation, or dynamic credential gaps.

  • Use environment secrets with required reviewers for production. This adds a manual approval step before secrets become available.
  • Restrict GITHUB_TOKEN permissions. Set permissions: contents: read and remove unused grants.
  • Pin third-party actions to a full commit SHA. This prevents unexpected upstream changes from affecting workflows.
  • Never reference secrets in if conditionals. GitHub does not support secrets in conditional expressions.
  • Avoid pull_request_target with untrusted code checkout in the same job. This prevents privileged workflows from executing attacker-controlled code.
  • Document a rotation cadence. Even without enforcement, a documented schedule reduces secret sprawl.
  • Restrict organization secrets to specific repositories. Only expose secrets to repositories that require them.

Are GitHub Actions secrets enough?

The Megalodon campaign demonstrated why protecting secrets requires more than just encryption and masking. You now have a map of what GitHub Actions secrets provide and what they do not. Use the four structural gaps, the OIDC option, and the two external secrets manager integration patterns discussed in this article to decide whether GitHub secrets are enough for your workflow. If your team needs rotation evidence, per-access audit logging, and fine-grained RBAC, an external secrets manager integrated into your existing workflow is the path to follow.

Frequently asked questions

How do you add secrets to GitHub Actions?

Store secrets by adding them to your repository, organization, or environment GitHub Actions secrets settings.

How do you use secrets in GitHub Actions?

Reference a stored secret in your workflow using the ${{ secrets.SECRET_NAME }} context. Alternatively, you can use a GitHub Action to fetch secrets from an external secrets manager such as Doppler at runtime instead of storing them in GitHub Actions secrets.

When should you use OIDC instead of GitHub Actions secrets?

Use OIDC when authenticating GitHub Actions to supported cloud providers such as AWS, Azure, or GCP. OIDC eliminates long-lived cloud credentials by exchanging a GitHub-issued identity token for temporary cloud credentials. For services that cannot use OIDC, an external secrets manager is typically the better option.

Enjoying this content? Stay up to date and get our latest blogs, guides, and tutorials.

Related Content

Explore More