The /sign API
The server exposes a single endpoint. The request and response format is identical across every deployment (standalone HTTP and Lambda Function URL), so clients never care where the CA runs.
Request
POST /sign
Authorization: Bearer <OIDC JWT>
Content-Type: application/json
{"public_key": "ssh-ed25519 AAAA... comment"}
The body carries only the public key (OpenSSH
authorized_keysformat,ssh-ed25519only; certificate keys are rejected). Principals, TTL, extensions, and the key ID are decided by the policy — there is no way to request them.The bearer token is the caller’s identity. For GitHub Actions this is the workflow’s OIDC token, requested with the audience your policy expects.
The body is limited to 16 KiB.
Success response
200 OK with Content-Type: text/plain; the body is the OpenSSH
certificate, ready to be written to <key>-cert.pub:
ssh-ed25519-cert-v01@openssh.com AAAA...
Error responses
Errors are deliberately generic — a fixed message plus a request ID, as
JSON. The real denial reason goes only to the server’s audit log, so the
policy cannot be probed by varying claims; an operator correlates the
caller’s request_id with the audit event.
{"error": "certificate request denied; contact your administrator with the request_id",
"request_id": "a1b2c3d4e5f60708"}
The request ID is also returned in the X-Request-Id response header.
Status |
Meaning |
|---|---|
400 |
malformed body, body too large, or unacceptable public key |
401 |
missing or invalid bearer token |
403 |
policy denied (no rule, multiple rules, or key ID expansion failed) |
405 |
method other than POST |
500 |
signing error |
503 |
the policy is disabled (emergency stop) |
The status code distinguishes only the broad class (the caller needs to know whether to fix the request, fetch a new token, or call an operator); everything finer-grained is audit-log-only.