Goodbye SSH Keys: Keyless Git and Artifact Signing with OIDC, Sigstore, and Workload Identity in 2025
Long‑lived credentials remain one of the most common and costly weak points in modern software delivery. SSH keys, static API tokens, and personal access tokens are easy to leak, hard to rotate, and often wildly over‑privileged. In 2025 there is a credible, production‑ready alternative: short‑lived, verifiable identities issued on demand via OIDC and used to sign everything — source code commits, container images, attestations — with transparent auditability.
This article lays out a complete, opinionated plan to go keyless:
- Use gitsign to replace developer GPG/SSH signing keys for Git commits and tags.
- Use cosign to keylessly sign container images and generate SLSA provenance attestations.
- Use CI OIDC federation to replace cloud secrets in pipelines with short‑lived roles.
- Enforce verification in admission, deployment, and release policies.
- Roll out safely with dual‑signing, gradual enforcement, and measurable guardrails.
If you can adopt only one security initiative this year, make it this one. It reduces blast radius, improves transparency, and is developer‑friendly when done thoughtfully.
Why long‑lived credentials are a liability
Long‑lived credentials fail in three predictable ways:
- They leak. CI logs, misconfigured caches, browser autofill, dotfiles in public repos, screenshots, support tickets, and laptop theft all routinely expose tokens. The 2022 CircleCI incident is a canonical example: environment secrets were exfiltrated and rotated under emergency conditions.
- They over‑authorize. A multi‑purpose PAT commonly grants read/write across many repos. Stolen once, it becomes a skeleton key for weeks or months until rotated.
- They lack provenance. A signed Git commit using a borrowed private key doesn’t communicate who or what actually produced it. Was it a developer’s workstation, a CI job in a specific repo and ref, or something else entirely? Auditors need more context.
Short‑lived, verifiable identities change the calculus:
- Identity proof is delegated to an IdP (e.g., GitHub Actions’ OIDC, GitLab OIDC, cloud IdPs, or your enterprise SSO).
- Keys are ephemeral. You mint them when you sign, discard them immediately, and pin verification to the OIDC issuer and claims.
- Transparency logs (Rekor) let you prove who signed what, when, and from which identity.
The net result is a dramatic reduction in value‑at‑risk: even if a token is captured, it’s typically only valid for minutes and with tight audience restrictions.
The building blocks in 2025
- OIDC: An identity layer on top of OAuth 2.0 that conveys claims about a workload or user in a signed JWT. In CI, OIDC tokens include claims like repo, ref, workflow, and run ID.
- Workload identity federation: Cloud vendors accept third‑party OIDC tokens and exchange them for short‑lived cloud credentials (AWS STS, GCP STS/WIF, Azure Federated Credentials). No long‑lived cloud secrets in your CI.
- Sigstore: An ecosystem for keyless signing.
- Fulcio: Issues short‑lived X.509 certificates tied to an OIDC identity.
- Cosign: Signs OCI images and creates/validates attestations.
- Gitsign: Signs Git commits/tags using Sigstore flow.
- Rekor: An append‑only transparency log that records signing events.
- SLSA: A framework and set of tools to attest build provenance and harden supply chains.
You can consume these as hosted community services (public Fulcio and Rekor) or run private instances for regulated/air‑gapped environments.
Keyless Git with gitsign
GPG signing for Git commits was a step forward, but managing keys and distributing public keys is operationally fragile. Gitsign replaces that with short‑lived X.509 certs tied to your OIDC identity and logged in Rekor.
How it works
- When you commit, gitsign requests an OIDC token for your user session or configured provider.
- It generates an ephemeral keypair locally and requests a Fulcio certificate binding the key to your OIDC subject and issuer.
- It signs the commit with the ephemeral key and records an entry in Rekor.
- GitHub/GitLab/Gerrit can verify and display the signature as "Verified" with Sigstore/X.509.
Developer setup (GitHub example)
Prerequisites: Install gitsign and the Sigstore root.
- macOS (Homebrew):
bashbrew install sigstore/tap/gitsign cosign
- Linux:
bashcurl -sSfL https://github.com/sigstore/gitsign/releases/latest/download/gitsign_$(uname -s)_$(uname -m).tar.gz \ | sudo tar -xz -C /usr/local/bin gitsign
Configure Git to use X.509 signing via gitsign:
bashgit config --global commit.gpgsign true git config --global gpg.x509.program gitsign git config --global gpg.format x509 # Optional: ensure correct email matches your IdP git config --global user.email 'dev@example.com'
Make a signed commit:
bashecho 'hello' > demo.txt git add demo.txt git commit -S -m 'Keyless signed commit with gitsign'
The first time, gitsign may open a browser to complete OIDC login (e.g., GitHub, Google Workspace, Okta). After that, tokens refresh automatically.
Enterprise IdP and on‑prem scenarios
- If you use Okta, Azure AD, or Google Workspace, configure an OIDC app that allows the gitsign client to request tokens. Gitsign supports the OAuth device code and browser flows.
- For isolated networks, deploy a private Fulcio and Rekor, and configure gitsign:
bashgit config --global gitsign.fulcio-url 'https://fulcio.internal' git config --global gitsign.rekor-url 'https://rekor.internal'
Enforcement in Git hosting
- GitHub: Enable "Require signed commits" on protected branches. GitHub recognizes Sigstore X.509 signatures and marks them Verified if the OIDC identity matches your org policy.
- GitLab: Similar commit signature verification is available; enforce per‑project.
- Gerrit: Integrate with commit verification hooks that validate gitsign signatures and Rekor inclusion.
Tip: Start in monitor mode. Use a pre‑receive hook or a GitHub Action to report unsigned or unverifiable commits before enforcing.
Keyless container image signing with cosign
Cosign provides a keyless signing flow that binds container images to specific identities and build contexts. Signatures are stored in the registry next to the image using the OCI signature spec. You can also generate and verify attestations (e.g., SLSA provenance, SBOMs).
Sign an image keylessly
From a developer workstation or CI job with OIDC:
bash# Build an image docker build -t ghcr.io/org/app:1.2.3 . docker push ghcr.io/org/app:1.2.3 # Keyless sign (OIDC flow) COSIGN_EXPERIMENTAL=1 cosign sign \ --keyless \ --rekor-url https://rekor.sigstore.dev \ ghcr.io/org/app:1.2.3
Cosign will retrieve an OIDC token, get a short‑lived cert from Fulcio, create a Rekor entry, and push a signature artifact to your registry.
Verify by identity and issuer
Verification should bind to both issuer and subject claims:
bashCOSIGN_EXPERIMENTAL=1 cosign verify \ --certificate-identity 'https://github.com/org/repo/.github/workflows/release.yml@refs/tags/v1.2.3' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ ghcr.io/org/app:1.2.3
You can broaden the identity match for branches:
bashCOSIGN_EXPERIMENTAL=1 cosign verify \ --certificate-identity-regexp 'https://github.com/org/repo/.*@refs/heads/main' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ ghcr.io/org/app:1.2.3
Enforce in Kubernetes admission
Use Sigstore’s policy-controller, Kyverno, or OPA/Gatekeeper to require valid cosign signatures by identity.
- Sigstore policy-controller example:
yamlapiVersion: policy.sigstore.dev/v1alpha1 kind: ClusterImagePolicy metadata: name: require-keyless-signatures spec: images: - glob: 'ghcr.io/org/*' authorities: - keyless: url: https://fulcio.sigstore.dev identities: - issuer: 'https://token.actions.githubusercontent.com' subject: 'https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main'
- Kyverno example policy snippet:
yamlapiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: verify-signature spec: validationFailureAction: Enforce rules: - name: verify-cosign match: any: - resources: kinds: [Pod] verifyImages: - image: 'ghcr.io/org/*' attestors: - entries: - keyless: issuer: 'https://token.actions.githubusercontent.com' subject: 'https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main'
Roll out in report mode first to observe impact, then flip to enforce.
CI OIDC federation: no more cloud secrets in pipelines
Modern CI systems can mint OIDC tokens representing the running job. Cloud providers will accept those to issue short‑lived credentials with fine‑grained conditions (repo, branch, workflow, environment).
GitHub Actions → AWS (role web identity)
- Create an OIDC identity provider in IAM that points to GitHub Actions:
bashaws iam create-open-id-connect-provider \ --url https://token.actions.githubusercontent.com \ --client-id-list sts.amazonaws.com \ --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
- Create a role with a trust policy that limits which repos/branches can assume it:
json{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { "token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main" } } } ] }
- Use it in a workflow without storing any AWS secrets:
yamlname: release permissions: id-token: write # Enable OIDC token minting contents: read on: push: branches: [main] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/gh-actions-publish aws-region: us-east-1 - run: aws s3 ls s3://your-bucket
GitHub Actions → GCP (Workload Identity Federation)
- Create a workload identity pool and provider referencing GitHub’s OIDC.
- Grant a service account "Workload Identity User" for conditions matching your repo/ref.
- Use gcloud in the workflow:
yamlpermissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - id: auth uses: google-github-actions/auth@v2 with: workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/gh-pool/providers/gh-provider service_account: ci-publisher@project.iam.gserviceaccount.com - run: gcloud auth list
GitHub Actions → Azure
Use an Entra ID application with a Federated Credential bound to your repo/ref. Then login without a secret:
yamlpermissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} allow-no-subscriptions: true enable-AzPSSession: false federated-token: ${{ steps.generate_token.outputs.token }}
Note: As of 2025, azure/login can request the OIDC token directly; you no longer need a client secret. Configure a federated credential in the app registration for your repo and branch.
GitLab CI, Buildkite, others
- GitLab: Built‑in OIDC tokens are exposed as CI_JOB_JWT. Use GCP WIF, AWS AssumeRoleWithWebIdentity, or Azure federated creds similarly.
- Buildkite: Supports OIDC for agents and pipelines; configure cloud roles to trust the Buildkite OIDC issuer.
The pattern is identical: a short‑lived token minted at job runtime replaces stored cloud secrets.
SLSA provenance and attestations with cosign
Signing containers is necessary but insufficient: you need provenance to answer "how was this built?" SLSA (Supply‑chain Levels for Software Artifacts) provides a standard predicate capturing builder identity, source, and steps. Cosign can attach this as an attestation.
Generate provenance in GitHub Actions
Use slsa-framework/slsa-github-generator or your own provenance emitter.
Example using the generator for container builds:
yamlname: build permissions: id-token: write contents: read on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build and push run: | docker build -t ghcr.io/org/app:${{ github.sha }} . docker push ghcr.io/org/app:${{ github.sha }} - name: Generate SLSA provenance uses: slsa-framework/slsa-github-generator@v2 with: artifact_type: container image: ghcr.io/org/app:${{ github.sha }} - name: Attach provenance attestation run: | COSIGN_EXPERIMENTAL=1 cosign attest \ --keyless \ --type slsaprovenance \ --predicate provenance.json \ ghcr.io/org/app:${{ github.sha }}
Verify provenance in deployment pipelines
bashCOSIGN_EXPERIMENTAL=1 cosign verify-attestation \ --type slsaprovenance \ --certificate-identity-regexp 'https://github.com/org/repo/.*' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ ghcr.io/org/app:${SHA}
Admission policies can require both a signature and a valid SLSA attestation with builder constraints.
A practical rollout plan
Moving an engineering org off SSH keys and static tokens is as much process as technology. A phased approach works best.
-
Inventory and baseline
- List all credential types in use: SSH deploy keys, PATs, CI secrets, cloud access keys.
- Identify pipelines that push to registries or clouds.
- Capture current rates of unsigned commits and unsigned images.
-
Pilot keyless signing
- Pick one repo and one image to sign with gitsign and cosign.
- Use public Fulcio/Rekor for speed. If you must run private infra, mirror later.
- Teach developers how to complete the OIDC flow locally.
-
Add verification in monitor mode
- Add a CI step that verifies commit signatures and image signatures by issuer and identity.
- Add a Kubernetes admission policy in "dry run" or "audit" mode.
-
Dual‑sign and dual‑publish
- Keep existing GPG signatures and/or KMS keys for a short period.
- Keylessly sign in parallel and compare verification outcomes.
-
Flip enforcement per perimeter
- Protected branches: require signed commits (Sigstore/X.509 accepted).
- Registry: require signatures for promotion (policy in your promotion job).
- Cluster: enforce signature policy on production namespaces.
-
Replace CI secrets with OIDC federation
- For each pipeline, remove cloud keys and configure OIDC roles.
- Restrict roles by repo/ref to reduce abuse if a repo is compromised.
-
Decommission long‑lived credentials
- Revoke PATs and SSH deploy keys; create break‑glass emergency tokens with 1–8 hour TTL guarded by approvals and logging.
-
Institutionalize
- Document identity mappings, claims used in policies, and incident response playbooks.
- Add dashboards: percentage of verified commits/images, policy denials, Rekor lookups.
This plan avoids flag days and gives developers time to adapt without breaking builds.
Policy design: describe identities, not keys
Keys are ephemeral in this model. Your policies should describe identities and contexts:
- Issuer: The OIDC provider, e.g., "https://token.actions.githubusercontent.com" for GitHub Actions.
- Subject: The workload identity. For GitHub, it’s a structured string like "repo:org/repo:ref:refs/heads/main" or a workflow path.
- Constraints: Branch/tag, environment (e.g., prod), repository, team, or actor claims.
Examples:
- Allow only images signed by the release workflow in the org/repo on the main branch to deploy to prod.
- Accept developer gitsign signatures from your enterprise IdP for commits on feature branches, but require CI signatures for tags.
Avoid policy that relies solely on email addresses; tie to repo/ref or workload.
Threat model and trade‑offs
Pros:
- Steeply reduced credential half‑life and blast radius.
- Transparent, auditable records via Rekor.
- Better provenance — you can prove a CI job for repo/ref built and signed an artifact.
- Developer experience improves: no manual key generation or distribution.
Considerations:
- IdP trust: Your IdP and CI OIDC configuration become critical trust roots. Harden them: MFA, conditional access, and strong change control.
- Time and clock skew: Short‑lived tokens require accurate time. Ensure NTP is dependable across builders.
- Availability: Hosted Fulcio/Rekor are highly available, but introduce an external dependency. For high‑assurance environments, run private instances with HA.
- Privacy: Rekor entries are public by default on the community log. If that’s not acceptable, run a private log or use minimal disclosure modes.
Air‑gapped and regulated environments
You can still go keyless:
- Run private Fulcio and Rekor inside your network, anchored to your enterprise CA or a dedicated root.
- Use Dex or another OIDC provider to bridge to your SSO.
- For workloads, use SPIFFE/SPIRE to issue short‑lived identities in‑cluster and integrate with Sigstore (experimental integrations exist) or use KMS‑backed keys with tight rotation as a transitional step.
- Mirror signatures and attestations within your private registries.
Developer experience tips
- Cache OIDC device code logins on developer machines using the system keychain. Gitsign handles this; communicate how long the session persists and how to log out.
- Prefer Git over HTTPS with OAuth device flow and gitsign for signatures. If you must keep SSH for transport, consider SSH certificates with a short lifetime from an internal CA as a transitional measure.
- Document common errors: browser pop‑ups blocked, corporate proxy breaks OIDC callback, WSL time drift, incorrect system time.
Performance and cost
- Token minting and Fulcio issuance add a few hundred milliseconds to signing, usually negligible compared to build time. In CI, fetch tokens once per job.
- Verification cost is minimal; cosign verification queries are fast, and Rekor inclusion proofs are cached.
- Operating private Fulcio/Rekor has infra cost but consolidates trust and may simplify audits.
Frequently asked questions
- Isn’t OIDC "just another token"? Yes, but it’s minted on demand, scoped by audience, expires in minutes, and encodes verified claims. It’s dramatically safer than a 90‑day PAT in a CI secret store.
- What if the OIDC issuer is compromised? That’s your IdP supply chain risk. Harden governance, reduce issuer permissions, require hardware‑backed MFA for admins, and monitor anomalies. Rekor provides forensics to detect misuse after the fact.
- Do I need to rotate anything? You rotate IdP credentials and Fulcio roots like any PKI, but not per‑developer keys, because they’re ephemeral.
- Can I keep GPG? Yes, dual‑sign during migration. But long‑term, X.509 via Sigstore is easier to operate and integrates better with workload context.
- How do I handle monorepos and multiple workflows? Encode identity constraints precisely: reference specific workflow files and refs in policies. Use regular expressions carefully.
Concrete end‑to‑end example
A release pipeline that builds, signs, attests, and deploys only if everything verifies.
- Build and push image in GitHub Actions with OIDC and cosign keyless.
yamlname: release on: push: tags: ['v*'] permissions: id-token: write contents: read jobs: build-sign: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: | IMAGE=ghcr.io/org/app:${{ github.ref_name }} docker build -t $IMAGE . echo IMAGE=$IMAGE >> $GITHUB_ENV - name: Push run: docker push $IMAGE - name: Sign image run: | COSIGN_EXPERIMENTAL=1 cosign sign --keyless $IMAGE - name: Generate provenance run: | ./scripts/gen-provenance.sh > provenance.json COSIGN_EXPERIMENTAL=1 cosign attest --keyless \ --type slsaprovenance --predicate provenance.json $IMAGE - name: Verify (self-check) run: | COSIGN_EXPERIMENTAL=1 cosign verify \ --certificate-identity "https://github.com/org/repo/.github/workflows/release.yml@${{ github.ref }}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ $IMAGE COSIGN_EXPERIMENTAL=1 cosign verify-attestation \ --type slsaprovenance \ --certificate-identity-regexp "https://github.com/org/repo/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ $IMAGE
- Admission in the cluster requires cosign signature and SLSA attestation by the release workflow on tag refs.
yamlapiVersion: policy.sigstore.dev/v1alpha1 kind: ClusterImagePolicy metadata: name: prod-release spec: images: - glob: 'ghcr.io/org/app:*' authorities: - keyless: url: https://fulcio.sigstore.dev identities: - issuer: 'https://token.actions.githubusercontent.com' subjectRegExp: 'https://github.com/org/repo/.github/workflows/release.yml@refs/tags/v.*' attestations: - name: slsa predicateType: https://slsa.dev/provenance/v1 policy: type: cue data: | predicateType: "https://slsa.dev/provenance/v1" predicate: { builder: { id: =~"https://github.com/org/repo/.+" } }
- Deployment job verifies again before applying manifests.
bashCOSIGN_EXPERIMENTAL=1 cosign verify \ --certificate-identity-regexp 'https://github.com/org/repo/.github/workflows/release.yml@refs/tags/v.*' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ ghcr.io/org/app:${TAG}
No static secrets are present in the repo or CI settings. Every step is bound to identity and context.
Alternatives and complements
- SSH certificates: If you must keep Git over SSH, replace static keys with SSH certs from an internal CA (e.g., Smallstep step‑ca). Certificates expire in minutes to hours and can encode user/group constraints.
- KMS‑backed keys: For organizations not ready for Fulcio/Rekor, use short‑lived KMS signing with strong IAM. You still lose the transparency log and workload identity richness, but it’s a step forward.
- Notary v2/OCI artifacts: Cosign interoperates with OCI registries and complements emerging standardization of artifact signing in registries.
What "good" looks like by the end of 2025
- 100% of commits on protected branches are signed with gitsign and verifiable to your IdP.
- 100% of images in production are cosign‑signed and accompanied by SLSA provenance.
- Admission policies enforce identity‑based verification in production clusters.
- CI pipelines hold zero long‑lived cloud secrets; all cloud access uses OIDC federation.
- Clear dashboards and logs exist: Rekor queries, verification rates, policy denials.
References and further reading
- Sigstore: https://sigstore.dev/
- Cosign docs: https://docs.sigstore.dev/cosign/overview/
- Gitsign: https://github.com/sigstore/gitsign
- Rekor: https://docs.sigstore.dev/logging/rekor/overview/
- Fulcio: https://docs.sigstore.dev/certificate_authority/fulcio/overview/
- SLSA: https://slsa.dev/
- GitHub Actions OIDC: https://docs.github.com/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
- AWS OIDC federation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html
- GCP Workload Identity Federation: https://cloud.google.com/iam/docs/workload-identity-federation
- Azure Federated Credentials: https://learn.microsoft.com/entra/workload-id/workload-identity-federation
- CircleCI 2022 incident postmortem: https://circleci.com/blog/jan-4-2023-incident-report/
Conclusion
Long‑lived SSH keys and tokens were an expedient solution for a different era. Today, short‑lived, verifiable identities via OIDC, Sigstore, and workload identity are mature enough to run at scale. They reduce risk, improve accountability, and fit naturally into modern CI/CD. Start with gitsign and cosign, adopt CI OIDC federation, require verification in your admission and promotion gates, and phase out static secrets. You will spend fewer weekends rotating credentials and more time shipping software you can trust.