Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

On this page

How to choose a signer

CI/lock supports ten signer providers. This guide is the decision tree, pick the path that matches your environment, then jump to the relevant config detail.

TL;DR

EnvironmentPick thisShips in default binary?
Hosted CI with OIDC (GitHub Actions, GitLab.com, Buildkite, CircleCI; see per CI platform)fulcio keyless through the TestifySec Platform: no signer flags (Fulcio specifics)✅ default
Developer workstationfulcio keyless through the TestifySec Platform, as an enrolled agent (cilock enroll agent) or as you (cilock login)✅ default
Public Sigstore or your own Fulcio instead of the platformfulcio with the complete alternative flag set✅ default
Offline scratch work / no identity availablefile, with --offline (alternative; not for production releases)✅ default
Internal CI/lock developmentdebug-signerbuilder opt-in
Workloads inside a SPIFFE/SPIRE service meshspiffebuilder opt-in
AWS-native, want an HSM-backed identitykms/awsbuilder opt-in
GCP-nativekms/gcpbuilder opt-in
Azure-nativekms/azurebuilder opt-in
HashiCorp Vault user with the Transit enginevault-transitbuilder opt-in
HashiCorp Vault user, simple key-stored modevaultbuilder opt-in

The default cilock binary ships file, fulcio and piv, the three signers that cover most teams' needs while keeping the binary small. The remaining seven (including debug-signer) are opt-in via rookery-builder:

rookery-builder --preset cicd \
  --with github.com/aflock-ai/rookery/plugins/signers/fulcio \
  --with github.com/aflock-ai/rookery/plugins/signers/kms/aws \
  --output ./cilock

The cicd preset carries only the CI-provenance attestors and the file signer, so the command adds fulcio back to keep keyless signing. If your steps use build-tool attestors such as maven or docker, add them with --with or use --preset all (see Presets).

Same source, same flag names, same wire format — just compiled in only when you ask for them. The slim default dropped the prebuilt binary's transitive dependency tree by roughly 600 Go packages.

If cilock --help doesn't show the --signer-<name>-* flags you need, you're on the default binary and need to build a variant that includes the signer.

The decision tree

Is the workload running in CI with an OIDC token available?
├── Yes → Is it inside a SPIFFE/SPIRE mesh?
│        ├── Yes → spiffe
│        └── No  → fulcio via the TestifySec Platform (the default: no signer flags)
└── No  → Is it a developer workstation?
         ├── Yes → fulcio via the TestifySec Platform, as an enrolled agent
         │         (`cilock enroll agent`) or as you (`cilock login`)
         └── No → Is it inside a cloud you already have KMS in?
                  ├── AWS  → kms/aws
                  ├── GCP  → kms/gcp
                  ├── Azure → kms/azure
                  └── No → Are you using HashiCorp Vault?
                           ├── Yes, with Transit engine → vault-transit
                           ├── Yes, key-stored          → vault
                           └── No → file (and consider whether you should be in CI at all)

Why prefer keyless?

Both fulcio and spiffe issue short-lived certificates tied to runtime identity. There's no long-lived private key to leak, no rotation schedule to forget, no credential to exfiltrate that's useful from an attacker's laptop.

The two recent supply-chain attacks CI/lock catches at scale (Trivy March 2026, LiteLLM March 2026) both worked by exfiltrating exactly the kind of long-lived credentials that file/KMS signing requires you to store. Keyless signing eliminates that attack surface entirely for the signing keys themselves.

If you can do keyless, do keyless.

Fulcio specifics

Keyless signing through the TestifySec Platform is the default. With no signer flags, cilock run signs against the platform's Fulcio CA (https://platform.testifysec.com/fulcio) and timestamps against its TSA (https://platform.testifysec.com/api/v1/timestamp):

cilock run --step build -- go build -o myapp ./

Which identity signs depends on what the machine has, in this order:

The machine hasIt signs asPrerequisite
An enrolled agentthe agent principala person approves cilock enroll agent in the browser; the agent cannot complete it
A cilock login sessionthe signed-in person's sessioncilock login on that machine
A CI job identitythe CI workloadGitHub Actions: permissions: { id-token: write }. GitLab.com, Buildkite, CircleCI: see per CI platform

With none of these, the run stops before the wrapped command starts: not signed in to https://platform.testifysec.com; run 'cilock login' first, or pass -k/--signer-file-key-path for a local key (or --offline to skip platform signing). Keep the three identities apart: an agent that will run cilock run should be enrolled, not left signing under a person's cilock login.

Signing is not storage. Whether the evidence is also uploaded to the platform depends on the identity; see Store attestations. For a self-hosted TestifySec Platform, add --platform-url with your platform's URL; the CA, TSA and evidence store are derived from it.

For the GitHub Action, see the CI quickstart.

Alternative: public Sigstore or your own Fulcio

To sign in another trust domain, leave the platform completely and name the CA and TSA yourself. --offline turns off the platform's Fulcio, TSA, evidence upload and platform attestor; naming only a Fulcio would keep the platform TSA and upload.

cilock run --step build \
  --offline \
  --signer-fulcio-url https://fulcio.sigstore.dev \
  --signer-fulcio-use-http=false \
  --timestamp-servers https://timestamp.sigstore.dev/api/v1/timestamp \
  -o build.bundle.json -- go build -o myapp ./

On GitHub Actions the Fulcio signer fetches the job's token itself; elsewhere pass it with --signer-fulcio-token or --signer-fulcio-token-path. Verify this evidence against the public Sigstore roots, never the platform's. For your own Fulcio, set --signer-fulcio-url to your instance. The client defaults to HTTP/REST (--signer-fulcio-use-http=true); set --signer-fulcio-use-http=false to switch to gRPC.

Keyless signing per CI platform

A Fulcio CA issues a certificate only for an OIDC issuer it maps to an identity. The TestifySec platform Fulcio (the one --platform-url selects) maps the CI issuers below. For these issuers it stamps the same certificate extensions as public Sigstore (fulcio.sigstore.dev), so a policy constraint means the same thing with either CA.

CIToken issuerPlatform FulcioPublic SigstoreCI/lock gets the token
GitHub Actions (github.com)https://token.actions.githubusercontent.comyesyesautomatically (needs id-token: write)
GitLab.comhttps://gitlab.comyesyesautomatically, from the job's id_tokens variable
Buildkitehttps://agent.buildkite.comyesyesautomatically (buildkite-agent oidc request-token)
CircleCIhttps://oidc.circleci.com/org/<org-id>yesyesautomatically (circleci run oidc get)
Self-managed GitLabyour GitLab hostnononot applicable
GitHub Enterprise Serveryour GHES hostnononot applicable
Jenkins, Azure DevOpsnone mappednononot applicable
AWS CodeBuildno workload OIDC tokennononot applicable

CI/lock gets the job's OIDC token by itself on all four, with the audience sigstore (a token with another audience is refused). With no signer flag, it signs keyless against the platform Fulcio:

  • GitLab.com: the job must declare the token. CI/lock reads $SIGSTORE_ID_TOKEN, the name GitLab's Sigstore example uses; name another variable with --signer-fulcio-token-env. If the variable is empty, CI/lock stops and says how to declare it.
  • Buildkite: CI/lock runs buildkite-agent oidc request-token --audience sigstore.
  • CircleCI: CI/lock runs circleci run oidc get --claims '{"aud":"sigstore"}'. It does not use $CIRCLE_OIDC_TOKEN or $CIRCLE_OIDC_TOKEN_V2: their audience is your organization ID, which Fulcio refuses.

CI/lock reads the token in its own process when it signs, after the wrapped command ends. --signer-fulcio-token and --signer-fulcio-token-path still take precedence. These runs sign; they do not upload. On GitLab.com, a job that also declares an ID token with audience https://platform.testifysec.com/archivista uploads under the job's identity (see Store attestations). On Buildkite and CircleCI, uploading to the platform still needs cilock login.

# GitLab.com (.gitlab-ci.yml)
build:
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  script:
    - cilock run --step build -- make build
# Buildkite (command step) or CircleCI (job step)
cilock run --step build -- make build

To use public Sigstore instead, use the complete flag set in the alternative above. Replacing only the Fulcio URL would leave the platform TSA and upload in place.

What the certificate identifies differs per CI. Pin the fields that exist. A constraint on a field that the certificate does not carry fails.

CISubject (URI SAN) and Build Signer URIRunner environmentSource Repository URI
GitHub Actionsthe workflow file that runs the job (job_workflow_ref), so a reusable workflow is its own signergithub-hosted / self-hostedyes
GitLab.comthe top-level pipeline file (ci_config_ref_uri). Every job in the pipeline has this identity, so a separate signer job is not a separate signergitlab-hosted / self-hostedyes
BuildkiteSAN: the pipeline (https://buildkite.com/<org>/<pipeline>). No Build Signer URIbuildkite-hosted / self-hostedno (the token names no repository; pin the Run Invocation URI and Source Repository Digest)
CircleCIthe pipeline definition. Every job in the pipeline has this identitynot recorded: the value is "" for cloud and self-hosted runners alikeyes (vcs-origin, without a scheme, for example github.com/acme/widget)

Always pin the issuer (certConstraint.extensions.issuer) together with the identity. The same repository path under a different issuer is a different identity.

The slsa attestor names a CI builder (builder.id) only for GitHub Actions and GitLab.com jobs whose token came from the issuer in this table. Everywhere else it records the default builder, which claims nothing.

SPIFFE/SPIRE specifics

For workloads that already have SVID identities from a SPIRE agent. The signer fetches an SVID from the agent's workload API socket and uses the cert as the signing identity.

cilock run --step build \
  --signer-spiffe-socket-path "unix:///run/spire/sockets/agent.sock" \
  ...

The socket path must have a unix:// or tcp:// scheme; the bare filesystem path is not accepted.

Pair with policy certConstraint.uris containing your SPIFFE ID:

{
  "type": "root",
  "certConstraint": {
    "commonname": "*",
    "dnsnames": ["*"],
    "emails": ["*"],
    "organizations": ["*"],
    "uris": ["spiffe://example.com/build"],
    "roots": ["*"]
  }
}

KMS specifics

KMS signing needs a rookery-builder binary that links the backend for your key's scheme (presets/all, or --with the plugins/signers/kms/{aws,gcp,azure} or plugins/signers/vault-transit module); see build a custom CI/lock. The default cilock links none, so it has no --signer-kms-* flags and rejects --signer-kms-ref as an unknown flag.

KMS signers use a URI-based reference scheme borrowed from Sigstore Cosign. The same URI works as the --signer-kms-ref flag and as the publickeyid in a policy:

ProviderURI form
AWSawskms:///arn:aws:kms:<region>:<account>:key/<key-id> (note the triple slash; the alternative awskms://[endpoint]/[ID/ALIAS/ARN] lets you target a non-default endpoint such as LocalStack)
GCPgcpkms://projects/<proj>/locations/<loc>/keyRings/<ring>/cryptoKeys/<key>/cryptoKeyVersions/<v> (/versions/<v> shorthand also accepted; the whole version suffix is optional and resolves to the key's primary version)
Azureazurekms://<vault-name>.vault.azure.net/<key>[/<key-version>]
cilock run --step build \
  --signer-kms-ref "awskms:///arn:aws:kms:us-east-2:111122223333:key/1234abcd-..." \
  ...

Authentication uses each cloud provider's standard credential resolution, no extra cilock config needed. For full flag detail (custom endpoints, profile selection, etc.) see signing & identity.

For air-gapped verification where the KMS service isn't reachable from the verifier, embed the base64-encoded PEM public key directly in the policy alongside the publickeyid URI.

Vault specifics

Two distinct Vault signers, two different flag namespaces, two different Vault engines. Pick by engine:

vault-transit uses Vault's Transit secrets engine for signing-as-a-service. The key stays in Vault; CI/lock asks Vault to sign the payload digest. Configure via the kms-hashivault provider:

cilock run --step build \
  --signer-kms-ref "hashivault://my-signing-key" \
  --signer-kms-hashivault-addr "https://vault.internal:8200" \
  --signer-kms-hashivault-token-file /run/secrets/vault-token \
  ...

This calls PUT /v1/transit/sign/<key>/<hash> under the hood (verified by tracing a failed run against a nonexistent host).

vault uses Vault's PKI secrets engine. Each invocation asks Vault to issue a short-lived X.509 certificate for the role, similar in spirit to Fulcio:

cilock run --step build \
  --signer-vault-url "https://vault.internal:8200" \
  --signer-vault-token "$VAULT_TOKEN" \
  --signer-vault-role build-signer \
  ...

This calls POST /v1/pki/issue/<role> under the hood. Pick this when you want Vault-managed identity but cert-based rather than long-lived-key-based signing.

Most teams pick vault-transit. Pick vault when you already have a Vault PKI engine standing up build identities.

File specifics

A local PEM-format key file. Useful for local dev and CI environments without OIDC support, but not recommended for production releases: there's a long-lived private key to manage, rotate, and protect.

cilock run --step build \
  --signer-file-key-path ./signing.key \
  --signer-file-cert-path ./signing.crt \
  ...

If you find yourself using file in production CI, the question to ask is: why don't I have OIDC available here? GitHub Actions, GitLab CI, AWS, GCP, and Azure all expose OIDC tokens.

Mixing signers across pipelines

Different steps can use different signers, the verifier resolves each step's collection against that step's functionaries independently. A common pattern:

StepSignerWhy
buildfulcio (keyless)Runs in CI, has OIDC
policy-signkms/aws (long-lived)Policy is signed once at policy-publish time, away from CI
release-promotionkms/aws (long-lived)Promotion gate runs as a service account, not a CI workflow

By default one signer signs each envelope. Pass --countersign to sign the same envelope with every configured signer: with a platform session or CI identity, the platform keyless certificate signs and each --signer-* key adds its own signature, each with its own timestamps (cilock/internal/options/countersign.go). See countersign an attestation.

Parity with Witness

The CI/lock signer surface is a superset of the upstream Witness signer surface; everything Witness supports, CI/lock supports with the same flag namespaces. CI/lock adds two:

ProviderWitnessCI/lockNotes
file✓✓CI/lock adds --signer-file-key-passphrase / --signer-file-key-passphrase-path for encrypted PEMs.
fulcio✓✓CI/lock adds --signer-fulcio-use-http (HTTP/REST vs. gRPC; defaults to HTTP).
spiffe✓✓Identical.
vault (PKI engine)✓✓Identical.
kms/aws✓✓Identical.
kms/gcp✓✓Identical.
kms/azure✓✓Identical.
vault-transit (kms/hashivault)✓✓Uses Vault's Transit engine instead of PKI.
piv(none)✓Cilock-only; signs with a key on a PIV token such as a YubiKey. In the default binary.
debug-signer(none)✓Cilock-only; ephemeral keypair for development.

For the eight shared providers, the Witness docs are an authoritative reference; CI/lock's wire format and flag names are identical.

See also

Reference generated from the product documentation. Match commands and support details to your installed release.