Every day, engineers paste AWS keys and API tokens into GitHub secrets. They rotate them when they remember, audit them when something breaks, and quietly hope no one with push access ever decides to print them in a workflow log. This is the state of CI security in 2026, and it’s not good enough.
GitHub’s OIDC support for AWS helped. Instead of a static key, your workflow can exchange a short-lived token for an IAM role. But it only covers AWS, and it tells you nothing about what your job actually did once it had those credentials. The deeper problem remains: CI jobs are stateless, ephemeral workloads with no persistent identity. They borrow credentials rather than earning them. And because they’re treated as second-class citizens in your security model, nobody really knows what they’re talking to.
Every entity. Not just the services.
Modern non-human identity practice is straightforward in principle: every non-human actor in your system (every pod, every VM, every service) gets a cryptographic identity. You don’t hand a service an API key and hope for the best; you issue it a short-lived certificate, bind it to what it actually is, and enforce policy from there. This is what SPIFFE was designed for, and it’s how Riptides works across your production fleet.
CI jobs are the conspicuous exception. A GitHub Actions job has write access to your source code, your artifact stores, your signing keys, and your deployment targets. It runs arbitrary code from your dependency graph on every pull request. In terms of blast radius, a compromised job is at least as dangerous as a compromised production service, often more so. Yet most teams treat it as outside the identity model entirely: give it some secrets, hope the workflow file doesn’t leak them, move on.
The gap isn’t technical. It’s that no one built the bridge.
This isn’t a GitHub Actions problem. It’s a CI problem. Jenkins jobs, GitLab pipelines, CircleCI workflows: they all run code, call services, and handle secrets, and almost none of them have a cryptographic identity. GitHub Actions happens to have the best building block available today (the OIDC token), which is why we started there. But the principle is the same everywhere: if a process touches your infrastructure, it should have an identity.
One thing the AI era hasn’t changed: CI is still there. Whether your engineers write every line by hand or vibe-code entire features in an afternoon, the pipeline that builds, tests, and ships that code runs the same way it always did. The surface area of CI isn’t shrinking. If anything, faster iteration means more runs, more secrets in play, more opportunities for a compromised dependency to slip through unnoticed.
Workload identity for CI
Riptides gives every workload (whether it’s a Kubernetes pod, a bare-metal server, or a GitHub Actions job) a SPIFFE x509 identity issued from your control plane. That identity is cryptographically bound to what the job actually is: which repository it came from, which workflow triggered it, which branch, which actor.
When a GitHub Actions job starts, it presents a GitHub OIDC token to the Riptides control plane. The control plane verifies it against GitHub’s public JWKS, checks the claims against your Verifier policy (repository owner, environment, ref, whatever you care about), and issues a short-lived x509 SVID. From that point on, the job has a real identity, not a borrowed credential.
Two things that change
Secretless credential injection. Once the job has a workload identity, Riptides enforces your policy at the network layer. You define which workloads are allowed to call which services, and what credentials to inject when they do. The job runs aws s3 cp s3://my-bucket/config.json .: no access key in the environment, no secret in the workflow, no IAM role assumption in the code. The kernel module intercepts the outbound TLS connection, verifies the workload identity, injects the right credentials on the wire, and wraps everything in mTLS. The application never knew anything happened.
This is the same credential injection Riptides uses for production workloads. Your CI jobs now work exactly like your services: identity-first, secretless, policy-enforced.
Full connection visibility. Every TCP connection a job makes (to S3, to your internal API, to your deployment target, to anything) is tracked with full workload identity context. Which workflow. Which repository. Which actor. Which ref. Whether it was allowed or denied by policy. You get the same network observability in CI that you have across the rest of your fleet.
This matters more than it sounds. A supply chain compromise doesn’t announce itself. A malicious dependency that phones home during a build looks like normal outbound traffic, unless you’re watching. With Riptides, you are. And because credentials are injected at the kernel layer on the wire, they never exist in the job’s environment. There’s nothing for a malicious package to read, and unexpected outbound connections can be blocked by policy before they leave the host.
Setup
Riptides is open to everyone now (anyone can spin up a workspace), so you don’t have to take our word for any of this. You can wire it into one of your own workflows in a few minutes. Three things are needed.
A Verifier on the control plane that trusts GitHub Actions tokens from your organisation:
apiVersion: auth.riptides.io/v1alpha1
kind: Verifier
metadata:
name: github-actions
namespace: riptides-system
spec:
GitHubActions:
repositoryOwner: your-org
audience: riptides
A workflow step that joins the job to the control plane:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: riptideslabs/setup-riptides@v1
with:
controlplane-url: https://your-env.console.riptides.io
And nothing else. The setup-riptides action installs the kernel module, fetches an OIDC token, exchanges it for an x509 identity, and starts the daemon. Every subsequent step in the job runs with that identity.
A real workflow
Take a deploy job that pushes a build to S3, registers the release with an internal API, and posts to Slack. Today it looks like this:
- env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
RELEASE_API_TOKEN: ${{ secrets.RELEASE_API_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: |
aws s3 cp ./dist "s3://artifacts/$GITHUB_SHA" --recursive
curl -H "Authorization: Bearer $RELEASE_API_TOKEN" \
https://releases.internal/deploys -d "{\"sha\":\"$GITHUB_SHA\"}"
curl -X POST "$SLACK_WEBHOOK_URL" -d '{"text":"deployed"}'
Four secrets, sitting in the environment for the whole job. With Riptides the same job is:
- run: |
aws s3 cp ./dist "s3://artifacts/$GITHUB_SHA" --recursive
curl https://releases.internal/deploys -d "{\"sha\":\"$GITHUB_SHA\"}"
curl -X POST https://hooks.slack.com/services/... -d '{"text":"deployed"}'
No env: block, no secrets:. The AWS credentials, the bearer token, the webhook authentication — all injected on the wire by the kernel, based on the job’s identity, after your policy confirms this workflow is allowed to reach those destinations.
Why this can’t be stolen
The difference isn’t cosmetic. Picture the realistic attack: a compromised build dependency, one transitive package updated overnight, wakes up inside your deploy job and looks for credentials to ship somewhere it controls. In the first version it finds four of them sitting in the environment, reads them, and POSTs them to https://evil.example.com. The job succeeds, the logs look normal, and your AWS keys are gone. Because they’re long-lived secrets in GitHub’s store, they keep working long after the run finishes.
In the second version that attack has nothing to work with. The credentials are never written to the environment, a file, or any process memory the dependency can reach. They exist only inside the kernel module, on the connection, for the instant each request is made. And the injection is destination-bound: a credential is only attached to traffic headed for a service your policy actually defines. The attacker’s endpoint isn’t one of them, so nothing gets injected. And because it isn’t an allowed destination either, the kernel drops the connection before a single byte leaves the runner.
You can’t steal a secret that was never there, and you can’t smuggle it out to a destination the kernel won’t let you reach.
The broader point
CI is part of your production trust boundary. It builds your software, signs your artifacts, deploys to your infrastructure, and calls your internal APIs. It deserves the same security posture you apply to everything else.
Treating CI jobs as identity-less credential consumers is a design choice, not a constraint. Riptides makes the alternative straightforward.
Spin up a workspace at riptides.io/get-started. Setup takes a few minutes, and the setup-riptides action, available on the GitHub Actions Marketplace, handles the rest.
How exposed are your workloads?
Run the NHI Security Audit Checklist, 8 questions to map your credential exposure, attribution gaps, and lateral movement surface across your own environment. Takes about 15 minutes.
Run the ChecklistFollow us on LinkedIn and X for more updates.