Skip to main content

How to choose a signer

CI/lock supports nine 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 CI)fulcio (Sigstore keyless)✅ default
Local dev / smoke tests / CI without OIDCfile✅ 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 only file and fulcio — the two 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/kms/aws \
--output ./cilock

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 (default for GitHub Actions, GitLab CI)
└── 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

The Sigstore CA. Most teams using GitHub Actions or GitLab CI should pick this.

cilock run --step build \
--signer-fulcio-url "$FULCIO_URL" \
--signer-fulcio-oidc-issuer "https://token.actions.githubusercontent.com" \
--signer-fulcio-oidc-client-id sigstore \
--signer-fulcio-token "$OIDC_TOKEN" \
...

In GitHub Actions, the cilock-action defaults all of this for you when enable-sigstore: true (the action's default). You only need permissions: { id-token: write } on the workflow.

For self-hosted 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.

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 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

Only one signer is supported per cilock run invocation (verified from cilock/internal/cmd/run.go). To use multiple signers, run CI/lock multiple times.

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 three:

ProviderWitnessCI/lockNotes
fileCI/lock adds --signer-file-key-passphrase / --signer-file-key-passphrase-path for encrypted PEMs.
fulcioCI/lock adds --signer-fulcio-use-http (HTTP/REST vs. gRPC; defaults to HTTP).
spiffeIdentical.
vault (PKI engine)Identical.
kms/awsIdentical.
kms/gcpIdentical.
kms/azure(none)Cilock-only.
vault-transit (kms/hashivault)(none)Cilock-only; uses Vault's Transit engine instead of PKI.
debug-signer(none)Cilock-only; ephemeral keypair for development.

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

See also