Goodbye SSH Keys: Keyless Git and Artifact Signing with OIDC, Sigstore, and Workload Identity in 2025
Replace SSH keys and tokens with short‑lived OIDC identities: gitsign for commits, cosign for images, CI federation, SLSA provenance, and a safe rollout plan.

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:
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:
Short‑lived, verifiable identities change the calculus:
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
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
Developer setup (GitHub example)
Prerequisites: Install gitsign and the Sigstore root.
`bash
brew install sigstore/tap/gitsign cosign
`
`bash
curl -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:
`bash
git 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:
`bash
echo '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
`bash
git config --global gitsign.fulcio-url 'https://fulcio.internal'
git config --global gitsign.rekor-url 'https://rekor.internal'
`
Enforcement in Git hosting
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.3Keyless 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:
`bash
COSIGN_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:
`bash
COSIGN_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.
`yaml
apiVersion: 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'
`
`yaml
apiVersion: 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)
`bash
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
`
`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"
}
}
}
]
}
`
`yaml
name: 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)
`yaml
permissions:
id-token: write
contents: read
steps:
`
GitHub Actions → Azure
Use an Entra ID application with a Federated Credential bound to your repo/ref. Then login without a secret:
`yaml
permissions:
id-token: write
contents: read
steps:
`
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
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:
`yaml
name: 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
`bash
COSIGN_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.
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:
Examples:
Avoid policy that relies solely on email addresses; tie to repo/ref or workload.
Threat model and trade‑offs
Pros:
Considerations:
Air‑gapped and regulated environments
You can still go keyless:
Developer experience tips
Performance and cost
Frequently asked questions
Concrete end‑to‑end example
A release pipeline that builds, signs, attests, and deploys only if everything verifies.
`yaml
name: 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
`
`yaml
apiVersion: 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/.+" }
}
`
`bash
COSIGN_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
What "good" looks like by the end of 2025
References and further reading
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.