Signed-Ticket Production Promotion
This guide is the operator walkthrough for the OpenSSH signed-ticket design that gates PRODUCTION_PROMOTION. It is derived from scripts/goal-runner/signed_ticket_schema.py and signed_ticket_verifier.py.
Never put a real private key in the ASF50 repository, runtime, evidence, chat, or this documentation. The walkthrough below uses placeholders.
Design summary
A signed ticket is an OpenSSH-signed authorization payload that the orchestrator verifies with ssh-keygen -Y verify. The payload binds:
ticket_id— a UUID identifying the ticket.gate_id— the gate this ticket authorizes.goal_id— the goal this ticket authorizes.subject_sha— the exact commit SHA being authorized.production_target— the environment identifier (e.g.,staging,canary,live).issuer— the human principal that signs.namespace— the OpenSSH signer namespace.payload_digest— a hex digest of the operation payload.created_at— UTC ISO-8601 timestamp.
The signature is verified against agent-harness/governance/allowed_signers.
Operator signing walkthrough
Step 1 — generate the ticket payload
On your workstation, with the private key available only locally:
GOAL_ID="ASF50_DOCS_PORTAL_001"
GATE_ID="00000000-0000-0000-0000-000000000000"
SUBJECT_SHA="$(git -C /path/to/repo rev-parse HEAD)"
PRODUCTION_TARGET="staging"
cat > payload.json <<JSON
{
"ticket_id": "$(uuidgen)",
"gate_id": "$GATE_ID",
"goal_id": "$GOAL_ID",
"subject_sha": "$SUBJECT_SHA",
"production_target": "$PRODUCTION_TARGET",
"issuer": "asf50-owner",
"namespace": "asf50-production",
"payload_digest": "$(sha256sum payload.json | awk '{print $1}')",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
JSON
Step 2 — sign the payload with ssh-keygen -Y sign
ssh-keygen -Y sign \
-f ~/.ssh/asf50_owner_ed25519 \
-n asf50-production \
-s payload.json.sig \
< payload.json
This produces payload.json.sig next to the payload. The signature file is the artifact you submit to the orchestrator.
Step 3 — submit the signature to ASF50
Submit the signature via the orchestrator's signed-ticket ingestion CLI (referenced from scripts/goal-runner/signed_ticket_verifier.py):
.\scripts\goal-runner\goal-gate.ps1 `
-Action approve `
-GateId $gateId `
-Approver "<GitHub login or owner display name>" `
-Source signed-ticket `
-Note "<bounded note>" `
-EvidencePath "<path-to-payload.json.sig>"
The orchestrator verifies with:
ssh-keygen -Y verify \
-f agent-harness/governance/allowed_signers \
-I asf50-owner \
-n asf50-production \
-s <signature-file> \
< payload.json
Step 4 — orchestrator consumes the gate
If verification succeeds, the orchestrator consumes the gate, transitions the goal toward FROZEN_PASS, and emits the deployment step.
Verification-before-approval
The orchestrator verifies the signature before marking the gate as approved. A failed verification leaves the gate OPEN.
Approval-before-consumption
The orchestrator marks the gate approved only after verification. Consumption happens only when the orchestrator actually performs the production action.
Replay protection
After consumption, the ticket_id is recorded as consumed. A second submission of the same ticket_id is rejected.
Subject binding
If payload.subject_sha does not match the runtime HEAD, the ticket is rejected. Re-base the work and issue a fresh ticket at the new head.
What the private key is NOT
- Not in the repo.
- Not in the runtime DB.
- Not in
evidence/. - Not in chat history.
- Not in this documentation.