Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

Signing-Identity Rotation

Release signing for @automagik/genie is cosign keyless only. There is no long-lived private key, no hardware-backed offline key, no public-key fingerprint to pin. "Rotation" here means rotating the certificate-identity pin (workflow-path@ref, OIDC issuer, and provenance source-uri) that operators cross-check against SECURITY.md, the repository's .well-known/security.txt and the in-repo witnesses.

When to rotate

Rotate the pinned certificate identity when, and ONLY when, one of these happens. Routine releases do NOT require rotation.
  1. Workflow path moves. .github/workflows/sign-attest.yml, the workflow the certificate names, is renamed or split. The Fulcio certificate SAN embeds the workflow path, so a move changes the certificate identity verifiers must accept.
  2. Delivery-evidence workflow moves. genie update also verifies a second identity, which every binary embeds (src/lib/delivery-evidence-verify.ts): the release's delivery evidence must be signed by .github/workflows/release-publish.yml@refs/heads/main. Renaming, splitting or moving that workflow, or signing it from another ref, breaks genie update on every host already running a release, because those binaries carry the old identity. scripts/reconcile-release-assets.sh carries it too.
  3. Repository is renamed. automagik-dev/genie becomes something else. Both the signer identity regexp and the SLSA provenance source-uri must move atomically.
  4. OIDC issuer changes. GitHub Actions' OIDC token issuer URL changes (rare; announced upstream). The pinned certificate-oidc-issuer must change with it.
  5. Keyless trust-root incident. Sigstore / Fulcio announces a trust-root rotation that requires consumers to re-verify against a new root. Operators follow the cosign upstream rotation advisory in addition to this runbook.
There is no routine calendar rotation. Rotation is driven by events, not time.

Rotation contract (summary)

ConstraintDetail
Minimum approversTwo Namastex security officers (independent GitHub accounts)
Pinning channelsSECURITY.md, the repository's .well-known/security.txt, the in-repo witnesses
Grace periodMinimum 72 hours in which the OLD and NEW identities both verify
Retirement testscripts/verify-release.sh <tag> MUST verify a post-rotation release
Audit trailRotation PR signed by both officers; the Phase 1 tracking issue records the Filed by (GPG fingerprint).
If any of the five constraints above cannot be met, the rotation does not ship. Do NOT reduce the grace period to "fix" a broken window.

Step-by-step rotation procedure

Phase 1: pre-rotation (before touching anything)

  1. Open a tracking issue in automagik-dev/genie titled SIGNING_CERT_IDENTITY_<YYYYMMDD> using the .github/ISSUE_TEMPLATE/signing-key-fingerprint.md template.
  2. Draft the new values:
    • certificate-identity-regexp
    • certificate-oidc-issuer
    • provenance source-uri
  3. Confirm the CURRENT values agree across SECURITY.md, the repository's .well-known/security.txt and the in-repo witnesses (scripts/check-fingerprint-pinning.sh checks them). If they already drift, stop and open an incident: rotation cannot overlay a broken baseline.

Phase 2: two-officer ceremony

  1. Two Namastex security officers (distinct GitHub identities, distinct hardware keys for commit signing) co-author the rotation PR. The PR MUST:
    • Update .github/workflows/sign-attest.yml (if workflow path changes).
    • Update every other copy of the identity in the repository: the witnesses scripts/check-fingerprint-pinning.sh checks (.github/ISSUE_TEMPLATE/signing-key-fingerprint.md, .github/cosign.pub, scripts/verify-release.sh, install.sh) and the copies the release and update paths use, such as src/genie-commands/update.ts, scripts/reconcile-release-assets.sh and .github/workflows/release-publish.yml. git grep -n 'sign-attest.*@refs/heads/main' lists every copy, tests included.
    • Update SECURITY.md so its pinned values match the new identity byte-for-byte.
    • Update .well-known/security.txt in the repository, which is served from its raw.githubusercontent.com URL.
    • Update (or open) the tracking issue created in Phase 1 with the final values.
  2. Both officers sign the PR via git commit -S with a GPG key that appears on their GitHub profile. No CI job counts the co-authors, so reviewers check for both verified signatures and refuse a single-officer PR.
  3. After merge, the next release runs sign-attest.yml under the new certificate identity. scripts/verify-release.sh <tag> MUST verify that release; if it fails, the rotation is aborted and rolled back.

Phase 3: grace-period dual verification

  1. For at least 72 hours after the rotation lands, verification tooling MUST accept BOTH the old and new certificate identities. In practice that means keeping the old entry in SECURITY.md under a ## Previous pinning heading so operators running the previous release can still verify it with scripts/verify-release.sh.
  2. The tracking issue opened in Phase 1 records the old value under its ## Previous pinning section and is never deleted.
  3. Operators who verified a release before the rotation re-run scripts/verify-release.sh against the first post-rotation release, and any operator still running a release signed before the rotation is instructed to pull the new release.

Phase 4: retirement of the old identity

  1. 72 hours after Phase 3 begins, and AFTER the post-rotation release has been verified end-to-end at least once against every pinning channel, the old identity is retired:
    • SECURITY.md drops the ## Previous pinning section.
    • The tracking issue is marked RESOLVED (but never deleted).
    • Verification tooling stops accepting the old identity.
  2. Retirement is a separate PR. It is NOT bundled with the rotation PR, because a bundled retirement eliminates the grace window.

Test-key dry-run recipe

The rotation procedure MUST be practiced at least once per quarter using throwaway test identities. A successful dry-run shows the signature check of the verification script accepting the rehearsed identity, then refusing the tarball once it has been tampered with.

Goal

Simulate the whole procedure end to end in under one hour, against a disposable automagik-dev/genie-signing-drill repo (or a local fork pointing at a fixture workflow), without touching the production signing identity.

Steps

# 1. Stand up a disposable fixture directory under /tmp. No production repos. # Run this from a clone of automagik-dev/genie: the drill rehearses the # rotation on a copy of its verification script. export DRILL=$(mktemp -d -t genie-signing-drill-XXXXXX) cp scripts/verify-release.sh "${DRILL}/verify-release.sh" cd "${DRILL}" # 2. Produce a fake signed release bundle using cosign against a throwaway # Fulcio identity. `cosign sign-blob --yes` will mint an ephemeral cert # from the drill operator's OIDC login (GitHub OAuth, short-lived). echo "drill tarball" > drill.tar.gz cosign sign-blob \ --yes \ --bundle drill.tar.gz.bundle \ drill.tar.gz # 3. Rehearse the rotation edit on the copy: set WORKFLOW_IDENTITY_REGEXP and # OIDC_ISSUER in ./verify-release.sh to the identity and the issuer of the # certificate that step 2 minted. # 4. Fabricate a minimal SLSA provenance file. Real provenance is generated # by slsa-github-generator; the dry-run uses a hand-rolled fixture to # exercise the local verify path, NOT to validate provenance contract. cat > drill.tar.gz.intoto.jsonl <<'EOF' {"drill": true} EOF # 5. Exercise the copy. Expected: the cosign step passes and the script stops # at the SLSA provenance step, because the drill provenance is # intentionally invalid. That proves the rehearsed identity is accepted. ./verify-release.sh --local drill.tar.gz || echo "exit=$?" # 6. Flip one byte in the tarball and re-run. Expected: the cosign step # fails. Tamper detection works end to end. printf '\x01' | dd of=drill.tar.gz bs=1 count=1 conv=notrunc ./verify-release.sh --local drill.tar.gz || echo "exit=$?" # 7. Clean up. rm -rf "${DRILL}"

Acceptance

A dry-run is successful when every expected outcome in the recipe above appears. If any step deviates, file an issue tagged signing-rotation-drill and do not ship the rotation PR until the deviation is understood.

Anti-patterns

The following have bitten prior rotations and MUST NOT be repeated:
  • Rotating without updating every pinning channel. SECURITY.md, the repository's .well-known/security.txt and the in-repo witnesses change in the same PR. scripts/check-fingerprint-pinning.sh fails a PR that leaves one of them behind.
  • Single-officer rotation. A rotation touched by one human account means a single compromise can move the pin. Refuse single-officer PRs.
  • Grace-period shortcuts. Reducing the 72-hour window to "speed up" a rotation makes operators on older releases fail verification. If a rotation is urgent enough to skip the grace window, it is an incident: file it as such, don't rotate.
  • Bundling retirement with rotation. Retirement collapses the grace window. Always two PRs.
  • "Just amend" fixups. Rotation history is load-bearing. Fix via a new PR that explicitly references the broken rotation; do not rewrite history.

References

  • .github/cosign.pub: the documented NO-PINNED-KEY sentinel (keyless only).
  • .github/workflows/sign-attest.yml: the signing workflow the certificate identity names; .github/workflows/release.yml calls it.
  • .github/ISSUE_TEMPLATE/signing-key-fingerprint.md: the pinned-issue template.
  • scripts/check-fingerprint-pinning.sh: checks that every witness carries the same pin; .github/workflows/signing-identity-pin.yml runs it on pull requests that touch the pin.
  • scripts/verify-release.sh: the local verification script operators run. There is no supported flag that bypasses a failed verification.