Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Fulcio: the Sigstore certificate authority

By updated

Improve this page

In the Sigstore ecosystem, Fulcio is the certificate authority (CA) that makes keyless signing possible: you prove your identity to an OIDC provider (OpenID Connect, the protocol by which GitHub, Google or GitLab attest who you are), and Fulcio issues you a ten-minute short-lived certificate to sign your artifacts. This lesson is for a DevOps engineer who has read the Sigstore overview and wants to understand what this certificate contains, how to verify an identity precisely, and when to deploy their own instance.

What you will learn

  • Understand why a 10-minute certificate replaces a signing key you would have to keep for years
  • Read the actual content of a Fulcio certificate and its Sigstore-specific OID extensions
  • Sign and verify with the public instance fulcio.sigstore.dev, from GitHub Actions or from your workstation
  • Deploy a private instance with the official Helm chart for a disconnected environment, then connect Cosign to it with a signing config and a trusted root
  • Measure the limits of Fulcio: trust rests on your OIDC provider, and the certificate cannot be revoked
  • Link Fulcio to the SOCLE requirement SOCLE-REL-KEY-3 (in French), signing without a long-lived key, and to the Release & provenance family (in French)

What exactly is Fulcio?

Fulcio is a certificate authority (CA) specialized in code signing. Its role: convert an OIDC proof of identity into a short-lived X.509 certificate (X.509 is the standard certificate format, the one used by HTTPS). It never signs your artifacts itself: it only attests that a given public key belongs, for a few minutes, to a given identity.

Why short-lived certificates?

The great innovation of Fulcio is the very short lifetime of its certificates (10 minutes). Compared with traditional certificates:

AspectTraditional certificateFulcio certificate
Validity period1 to 3 years10 minutes
Private key managementYou must protect it for yearsDestroyed right after signing
If the key is stolenAll signatures are compromisedAttack window of 10 minutes at most
RevocationComplex process (CRL, OCSP)Unnecessary for the certificate, which expires
Long-term verificationThe certificate must stay validThe Rekor proof is enough

The short lifetime is not a problem because every signature is recorded in Rekor, the public transparency log (an append-only register in which entries cannot be altered without being detected), with a cryptographic timestamp. You can verify indefinitely that the signature was made during the validity of the certificate.

How Fulcio works

Here is the sequence when you sign with Cosign in keyless mode, from the certificate request to the entry in the Rekor log:

  1. OIDC authentication

    Cosign redirects you to an OIDC provider (GitHub, Google, etc.). You authenticate and receive a JWT token (JSON Web Token: a token signed by the provider, whose fields, the claims, describe your identity).

    Claims of a GitHub Actions token (excerpt):
    - iss: "https://token.actions.githubusercontent.com" (who issued the token)
    - sub: "repo:my-org/my-app:ref:refs/heads/main" (subject)
    - job_workflow_ref: "my-org/my-app/.github/workflows/sign.yml@refs/heads/main"
    (workflow, copied into the certificate)

    For an interactive login with Google, the identity is the email claim.

  2. Generating an ephemeral key pair

    On the client side, Cosign generates a key pair (public + private) that will be used only once.

  3. Certificate request to Fulcio

    Cosign sends Fulcio:

    • The OIDC token (proof of identity)
    • The public key (to be included in the certificate)
    • A proof of possession of the private key (the client signs an identity field of the token with that key)
  4. Verification and issuance by Fulcio

    Fulcio checks that:

    • The OIDC token is valid (signature, expiry)
    • The OIDC issuer is in its allowed list
    • The proof of possession is correct

    It then generates an X.509 certificate containing the identity extracted from the OIDC token.

  5. Signing the artifact

    Cosign uses the private key to sign the artifact, then destroys the key immediately. The certificate is attached to the signature.

  6. Recording in Rekor

    The signature and the certificate are recorded in Rekor, creating an immutable, timestamped proof.

Flow diagram

The diagram below condenses the six previous steps. One point deserves your attention: the private key never leaves the workstation or the runner, and is sent neither to Fulcio nor to Rekor. Fulcio only receives the public key and the OIDC token. This is the property that makes the model acceptable: a compromise of the certificate authority does not hand over any existing signing key.

Keyless signing with Fulcio: Cosign authenticates with the OIDC provider and obtains a JWT token, sends its public key and the token to Fulcio, receives a certificate valid for 10 minutes, signs then destroys the key, and records signature and certificate in Rekor.Rekor(TransparencyLog)Fulcio (CA)Client(Cosign)OIDC provider(GitHub, etc.)signs, thendestroys thekey1. AuthenticationJWT token2. Public key + token3. Certificate (10 min)4. Signature + certificate
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Two ways to use Fulcio

Before going further, clarify your context of use:

ModeWhen to use itWhat you need
Public instanceOpen source projects, companies connected to the InternetNothing to deploy, just use Cosign
Private instanceAir-gapped environments, confidentiality required, full controlDeploy Fulcio + Rekor + (optionally) Dex, a ready-to-use OIDC provider

Sigstore operates a free public instance open to everyone at fulcio.sigstore.dev. It is the default mode when you use Cosign.

Advantages:

  • Nothing to install or maintain
  • Operated by the Sigstore community
  • Compatible with GitHub Actions, GitLab CI, Google Cloud Build...

Drawbacks:

  • Requires an Internet connection
  • Your signatures are publicly visible in Rekor
  • You depend on the Sigstore infrastructure

Who it is for: most users, especially for open source projects.

What does a Fulcio certificate contain?

A Fulcio certificate is a standard X.509 certificate enriched with specific extensions that hold the metadata of the OIDC identity.

Example certificate

Two details are surprising on first reading. First, the Subject field is empty: unlike a TLS certificate, the identity is not there, it is carried by the Subject Alternative Name and by the OID extensions. Second, for a GitHub Actions signer, this SAN is of type URI and not email, and it designates the workflow file and the exact Git reference. The excerpt below comes from a real certificate, the one of the image ghcr.io/sigstore/policy-controller/policy-controller:v0.13.0, decoded with openssl x509 -text -noout.

Certificate:
Issuer: O = sigstore.dev, CN = sigstore-intermediate
Validity
Not Before: Jul 2 08:25:55 2025 GMT
Not After : Jul 2 08:35:55 2025 GMT <- exactly 10 minutes
Subject:
X509v3 extensions:
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Alternative Name: critical
URI:https://github.com/sigstore/policy-controller/.github/workflows/release.yaml@refs/tags/v0.13.0
1.3.6.1.4.1.57264.1.1: <- OIDC Issuer
https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.5: <- GitHub Repository
sigstore/policy-controller
1.3.6.1.4.1.57264.1.6: <- GitHub Workflow Ref
refs/tags/v0.13.0
1.3.6.1.4.1.57264.1.11: <- Runner Environment
github-hosted

The Code Signing extended usage is what prevents this certificate from being recycled to serve TLS, and the criticality of the SAN forces any compliant verifier to take it into account.

Sigstore OID extensions

Fulcio uses custom OIDs (Object Identifiers) to store the metadata of the identity. The table below covers the historical block 1.1 to 1.6, the one you will meet in most examples online:

OIDNameContent
1.3.6.1.4.1.57264.1.1OIDC IssuerURL of the provider (GitHub, Google...)
1.3.6.1.4.1.57264.1.2Workflow TriggerTriggering event (push, PR...)
1.3.6.1.4.1.57264.1.3Workflow SHACommit SHA of the executed workflow
1.3.6.1.4.1.57264.1.4Workflow NameName of the GitHub Actions workflow
1.3.6.1.4.1.57264.1.5RepositorySource repository (org/repo)
1.3.6.1.4.1.57264.1.6Workflow RefBranch or tag (refs/heads/main)

These extensions allow a very precise verification: you can require that an image be signed by a specific workflow, from a specific branch, of a specific repository.

A point of attention for your audit scripts: the upstream documentation marks these six OIDs as deprecated. They are still issued by Fulcio, as the certificate above shows, but the extensions to prefer are now the generic OIDs 1.3.6.1.4.1.57264.1.8 to 1.24, which are no longer GitHub-specific. The most useful day to day are 1.8 (Issuer V2), 1.11 (Runner Environment, with the value github-hosted or self-hosted), 1.12 (Source Repository URI) and 1.14 (Source Repository Ref). The complete, authoritative list is published in oid-info.md.

Fulcio also records every certificate it issues in a certificate transparency log, and embeds in the certificate a signed certificate timestamp (SCT): a receipt from that log proving the certificate was logged.

Using the public instance

The public instance is the default mode. No configuration is needed.

Supported OIDC providers

The issuer URL in this table is the value you will find in the 1.3.6.1.4.1.57264.1.1 extension of the certificate and that you will pass to --certificate-oidc-issuer when verifying. Remember the practical distinction: the GitHub Actions, GitLab CI and Kubernetes providers supply a token automatically in a pipeline, without interaction; Google and Microsoft mostly serve interactive signing from a workstation, with a browser opening.

ProviderIssuer URLUsage
GitHub Actionshttps://token.actions.githubusercontent.comGitHub CI/CD
GitLab CIhttps://gitlab.comGitLab CI/CD
Googlehttps://accounts.google.comDevelopers, GCP
Microsofthttps://login.microsoftonline.com/{tenant}Azure DevOps
KubernetesService Account tokensK8s pods

Example: signing in GitHub Actions

The workflow below signs an image that a previous step has already pushed. It is the minimal version, focused on what Fulcio needs: the id-token: write permission, which lets the job request its OIDC token.

.github/workflows/sign.yml
name: Build and Sign
on:
push:
branches: [main]
permissions: {}
jobs:
sign:
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write # To push the signature to GHCR
id-token: write # ← Required to obtain the OIDC token
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Sign image
run: |
cosign sign --yes "ghcr.io/my-org/my-app@sha256:<image-digest>"

The Log in to GHCR step authenticates the job to the registry with the GITHUB_TOKEN: without it, Cosign cannot push the signature next to the image (packages: write permission). The OIDC token is automatically available thanks to the id-token: write permission. Cosign contacts fulcio.sigstore.dev, obtains a certificate, signs the image, and records the result in Rekor, all in one command. Replace <image-digest> with the digest of the image you have just pushed: you sign a digest, never a mutable tag. The complete workflow, with build, signing and immediate verification, is in the Sigstore lesson.

Example: signing locally (interactive)

From a workstation, there is no CI to provide a token: Cosign opens the browser so that you authenticate with your own provider.

Terminal window
# Cosign opens the browser for authentication
cosign sign --yes my-registry/my-image@sha256:<image-digest>

You authenticate through Google or GitHub, and receive a certificate carrying your email.

Verifying a signature

Verification is the step that gives the whole chain its value: a signature nobody checks protects against nothing. The two mandatory flags are --certificate-identity and --certificate-oidc-issuer. Without them, you only confirm that a Fulcio signature exists, not that it comes from the expected author; an attacker able to sign with their own GitHub account would pass the test. The classic trap is to loosen the identity with an expression that is too broad.

Terminal window
# Strict verification: specify the expected identity
cosign verify ghcr.io/my-org/my-app@sha256:<image-digest> \
--certificate-identity="https://github.com/my-org/my-app/.github/workflows/sign.yml@refs/heads/main" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"

This command checks that:

  1. The signature is cryptographically valid
  2. The certificate was issued by Fulcio (chain of trust)
  3. The identity in the certificate matches exactly
  4. The OIDC issuer is the expected one
  5. The signature is recorded in Rekor

Examining the certificate of a signature

This manipulation is for investigation, not for control: you want to know who signed, when and from which workflow, before writing the strict verification rule. The .* expressions below therefore only make sense in this exploratory context. Never copy them into an admission policy, they accept any signer.

The JSON output of cosign verify carries the certificate encoded in the publicKey.content field of the Rekor body. By decoding it, you get the PEM that you can feed to openssl to read the extensions shown above.

Terminal window
# Extract the certificate from the signature, then decode it
cosign verify ghcr.io/sigstore/policy-controller/policy-controller:v0.13.0@sha256:62d474d1be06a9dbf5051f14c82d995711fc02cd0bc1ca94ef6baacec3d2f3b1 \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer-regexp=".*" 2>/dev/null \
| jq -r '.[0].optional.Bundle.Payload.body' \
| base64 -d \
| jq -r '.spec.signature.publicKey.content' \
| base64 -d > cert.pem
openssl x509 -in cert.pem -text -noout

The Sigstore extensions appear in clear in the openssl output, under their OID number. If openssl rejects the file with unable to load certificate, it is almost always because the double base64 decoding was interrupted by a silent error from cosign verify: rerun the command without the 2>/dev/null redirection to read the message.

Deploying a private instance

For disconnected environments or those needing confidentiality, deploy your own Fulcio.

Architecture of a private instance

Three components form the minimal base, and the dependency order matters. The OIDC provider (Dex or an existing Keycloak) must answer before Fulcio, since Fulcio validates each token against it. Rekor then records the signatures. The diagram below shows these flows; remember that Fulcio only makes sense together with Rekor, a Fulcio instance alone provides no verifiable trace over time.

Private Sigstore in your infrastructure: the clients (Cosign, gitsign, policy-controller) rely on Dex as the OIDC provider, Fulcio as the certificate authority and Rekor as the transparency log.Your infrastructureDex(OIDC IdP)Fulcio(CA)Rekor(CT Log)Clients(Cosign, gitsign, policy-controller)
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Installing with Helm

Sigstore publishes an official Helm chart per component. The repository and the install command are stable; pin the chart version with --version so that your deployment is reproducible, otherwise Helm pulls the latest published revision and your configuration may break at the next version bump.

Terminal window
# Add the Sigstore chart repository
helm repo add sigstore https://sigstore.github.io/helm-charts
helm repo update
# Install Fulcio in its namespace, chart version pinned
helm install fulcio sigstore/fulcio \
--version 2.11.0 \
--namespace fulcio-system --create-namespace \
--values fulcio-values.yaml

Chart fulcio version 2.11.0 deploys Fulcio 1.8.8. The command creates the namespace and applies your values file. Then check startup with kubectl get pods -n fulcio-system; the fulcio-server pod must reach the Running state.

Basic configuration (fulcio-values.yaml)

The point most often miscopied from old tutorials is the location of the OIDC providers. In this chart, they are not declared under server, but in the config.contents object, which is serialized as is into Fulcio's config.json file. The OIDCIssuers key maps each issuer URL to its IssuerURL, its ClientID and its Type. The values below are deliberately minimal and to be adapted to your domain.

fulcio-values.yaml
config:
contents:
OIDCIssuers:
# Internal Dex provider
"https://dex.your-domain.internal":
IssuerURL: "https://dex.your-domain.internal"
ClientID: "sigstore"
Type: "email"
server:
# Certificate authority: fileca reads its key and certificate
# from a Secret. See the createcerts block below.
args:
certificateAuthority: fileca
# HTTP exposure through an Ingress
ingress:
http:
enabled: true
className: nginx
hosts:
- host: fulcio.your-domain.internal
path: /
tls:
- secretName: fulcio-tls
hosts:
- fulcio.your-domain.internal
# Certificate transparency log (Certificate Transparency)
ctlog:
enabled: true
# Dev / test only: this Job generates a self-signed fileca CA.
# In production, disable it and provide your own
# fulcio-server-secret Secret (CA private key + certificate).
createcerts:
enabled: true

Configuring the clients for the private instance

The clients must point at your instance instead of the public one. With Cosign v3, this goes through two documented files: a signing config (where to sign: URL of Fulcio, of Rekor, of the OIDC provider) and a trusted root (what to trust: your CA's certificate chain, your Rekor's public key). They are created with cosign signing-config create and cosign trusted-root create, then passed to commands with --signing-config and --trusted-root.

Terminal window
# 1. Where to sign: your private services (dates and operator are mandatory)
cosign signing-config create \
--no-default-fulcio --no-default-rekor --no-default-oidc --no-default-tsa \
--fulcio="url=https://fulcio.your-domain.internal,api-version=1,start-time=2026-01-01T00:00:00Z,operator=your-domain.internal" \
--rekor="url=https://rekor.your-domain.internal,api-version=1,start-time=2026-01-01T00:00:00Z,operator=your-domain.internal" \
--rekor-config="ANY" \
--oidc-provider="url=https://dex.your-domain.internal,api-version=1,start-time=2026-01-01T00:00:00Z,operator=your-domain.internal" \
--out signing-config.json
# 2. What to trust: your CA chain (PEM) and the Rekor public key
cosign trusted-root create \
--no-default-fulcio --no-default-rekor --no-default-ctfe --no-default-tsa \
--fulcio="url=https://fulcio.your-domain.internal,certificate-chain=/path/to/ca-chain.pem" \
--rekor="url=https://rekor.your-domain.internal,public-key=/path/to/rekor.pub,start-time=2026-01-01T00:00:00Z" \
--out trusted-root.json
# 3. Sign with your instance (the signature is re-verified right away)
cosign sign --yes \
--signing-config signing-config.json \
--trusted-root trusted-root.json \
my-registry/my-image@sha256:<image-digest>

On cosign sign, --trusted-root is used to verify the signature right after producing it, whereas --signing-config decides where to sign. For cosign signing-config create, the keys url, api-version (integer), start-time and operator are mandatory, and --rekor-config is mandatory as soon as --rekor is provided.

These three commands follow the Cosign v3 help but were not replayed here: the setup depends on your private instance. The same root of trust is used for verification: cosign verify --trusted-root trusted-root.json ..., with the identity options seen in the Cosign lesson. The trusted-root.json file is distributed to every workstation and runner that verifies your signatures, which the next section details.

Chain of trust and trust root

A Fulcio certificate is only worth what the chain above it is worth. This section shows that chain, then how a verifier learns which root to trust.

Sigstore PKI architecture

Fulcio fits into a PKI hierarchy (public key infrastructure: the chain of certificates that leads from a root to the certificate you check). The diagram shows it: the offline root signs an intermediate CA, which signs the short-lived certificates.

Sigstore PKI chain: the root CA, an offline key in an HSM, signs the intermediate CA used by Fulcio, which signs the short-lived end-entity certificate, valid for 10 minutes and carrying your OIDC identity.signssigns (ephemeral)Sigstore Root CA(offline key, HSM)Sigstore Intermediate CA(used by Fulcio)End-entity certificate(valid 10 minutes), holds your OIDC identity
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Distribution through TUF

A verifier needs a root of trust: the set of public keys and root certificates it has decided to trust, against which every chain is checked. Sigstore distributes this trust metadata through TUF (The Update Framework), a secure update protocol that protects against rollback attacks (serving an old, vulnerable version) and partial compromise (stealing only some of the signing keys).

Terminal window
# Initialize the Sigstore trust root
cosign initialize
# The metadata is downloaded under ~/.sigstore/root/
ls ~/.sigstore/root/tuf-repo-cdn.sigstore.dev/
# root.json snapshot.json targets targets.json timestamp.json

For a private instance, you no longer depend on this public TUF repository: the root of trust is the one of your CA, already gathered in the trusted-root.json file created above. It is up to you to distribute it to every workstation and every runner that verifies your signatures, and to renew it when your CA changes.

Fulcio security

Fulcio's security has two sides: the checks it runs before issuing a certificate, and the limits inherited from the identity providers it relies on.

What Fulcio verifies

Before issuing a certificate, Fulcio performs the checks in the table. Reading the right column tells you which attack each check closes.

CheckDescriptionAttack blocked
JWT token signatureThe token is signed by the OIDC providerForged token
Token expiryThe exp claim has not passedReplay with an old token
Allowed issuerThe issuer is in the allowlistMalicious OIDC provider
Correct audienceThe token is meant for FulcioToken stolen from another app
Proof of possessionThe requester owns the private keyInterception of the request

Limits and considerations

Fulcio moves the trust problem rather than removing it, and you must be aware of this before adopting it. The most important row of the table is the first: the security of your signing chain never exceeds that of your OIDC provider. If a maintainer's GitHub account is hacked, the attacker obtains perfectly valid Fulcio certificates. The absence of certificate revocation is not a flaw but a deliberate Fulcio design choice (upstream docs speak of avoiding revocation), compensated by the ten-minute lifetime and by the Rekor timestamp.

LimitImplicationMitigation
Trust in the OIDC providerIf GitHub is compromised, so are the certificatesDiversify providers, audit
No certificate revocationAn issued certificate cannot be cancelled; the authority's own key falls under root-of-trust management10-minute lifetime limits the impact
Identity = OIDC identityNo anonymity possibleUse service identities
Network dependency (public)Requires InternetPrivate instance for air-gapped

Best practices

These four habits cover the limits above. Apply them in the order given: the first two are about verification, the last two about the accounts and the log that Fulcio's security depends on.

  1. Always check the OIDC issuer

    Only trust the expected providers. Use the exact --certificate-oidc-issuer, no overly broad regexp.

  2. Use strict identity constraints

    Specify --certificate-identity with the exact path of the workflow, not just the repository.

  3. Enable 2FA on your OIDC accounts

    The security of Fulcio rests on that of your OIDC provider.

  4. Monitor signatures in Rekor

    Set up alerts on unexpected signatures from your repositories.

Key points

The table sums up the lesson concept by concept; the last row states the evidence to produce for the SOCLE requirement.

ConceptSummary
Role of FulcioConverts an OIDC identity into an X.509 certificate
Short-lived certificates10 minutes of validity, key destroyed after signing
Public instanceFree, ready to use, for most usages
Private instanceFor air-gapped, confidentiality, full control
OID extensionsThe certificate carries the workflow/repo metadata
VerificationAlways specify --certificate-identity and --certificate-oidc-issuer
LimitThe signature's security never exceeds that of the OIDC provider; no certificate revocation
Expected evidenceFor SOCLE-REL-KEY-3, the preuve field is the "keyless signing configuration (ephemeral identity)". Here: a workflow that signs through Fulcio with no stored key, and a cosign verify that fixes the exact workflow identity and the OIDC issuer

Knowledge check

Check that the essentials of this lesson are in place. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn