
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.
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.
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.
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.
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:
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 logs | What 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. |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 requirement | GitHub native | External 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.
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.
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.
Store secrets by adding them to your repository, organization, or environment GitHub Actions secrets settings.
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.
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.



Protect secrets across your entire stack with centralized management, automated workflows, and the flexibility to deploy in the cloud or on-prem.
