How to Sign Build Artifacts With Cosign Keyless in GitHub Actions
Keyless cosign signs with a short-lived certificate from Fulcio bound to your workflow OIDC identity, so no signing key lives in the repo.
Install cosign, grant id-token: write, and run cosign sign-blob with -y. Sigstore issues an ephemeral cert and records the signature in the transparency log.
Steps
- Install cosign with
sigstore/cosign-installer. - Grant
id-token: writeso cosign can request the OIDC token. - Run
cosign sign-blob -yand save the signature and certificate.
Workflow
permissions:
id-token: write
contents: read
jobs:
sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- run: |
cosign sign-blob -y \
--output-signature app.sig \
--output-certificate app.pem \
./dist/app.tar.gzGotchas
- Keyless signing requires
id-token: write; without it cosign cannot get an OIDC token. - Verifiers must pass
--certificate-identityand--certificate-oidc-issuerto trust the right workflow.
Verify it actually works
A workflow that runs is not a workflow that works. Confirm the behaviour on a real event rather than on a manual dispatch, because trigger conditions, permissions, and context values all differ between the two.
# 1. validate the file before pushing
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
# 2. trigger the real event, not workflow_dispatch
git commit --allow-empty -m "ci: verify trigger" && git push
# 3. watch it and read the conclusion, not just the colour
gh run watch
gh run view --log-failedWhat usually goes wrong first
- The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
GITHUB_TOKENpermissions default to read-only in many organisations. Declare apermissions:block listing every scope the job needs.- Fork pull requests get a read-only token and no access to secrets, regardless of workflow configuration.
actions/checkoutgives you depth 1 on a detached HEAD, so anything needing history or a branch name needsfetch-depth: 0.