Create a user certificate request
const url = 'https://example.com/api/certs/user';const options = {method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/api/certs/user \ --header 'Content-Type: application/json' \ --data '{}'Unauthenticated. Returns two relative URLs: events_url to wait on, and
approval_url for a human to open. The request ID is the capability —
it is an unguessable UUID, and holding it is what authorizes waiting on
the outcome.
Request Bodyrequired
Section titled “Request Bodyrequired”The public key to sign, and the options being asked for
object
The public key to sign, and the options being asked for
object
CallerUID, CallerGID, CallerPID and CallerPPID identify the client process, for joining with the machine’s own logs. Pointers so an absent value is distinguishable from uid 0 or gid 0; the two ids are absent on Windows, which has neither.
Client names the implementation and version (“ssoossh/1.2.3”), which is what tells this client and pam_ssoossh apart in a log.
ClientPolicy is what the requesting machine’s administrative policy locks, so an approver can tell a user’s own opt-out from a rule imposed on them. Optional, and absent from an older client.
object
FIPS reports that the machine is held to FIPS-approved algorithms by policy. A pointer so “policy is silent” stays distinct from “policy says false”.
ForbiddenExtensions is forbidden_certificate_extensions as the requesting machine has it. The client has already subtracted these from what it asked for; sending them says why they are absent.
ClientTime is the machine’s own clock when it built the request, so skew against the server is visible before it breaks a login.
MachineID is a stable per-install identifier: /etc/machine-id on Linux, kern.uuid on macOS, kern.hostuuid on FreeBSD, MachineGuid on Windows.
OS is the platform as the machine describes itself.
Process is the client’s own argv (“ssoossh ssh login –force”), the analogue of the PAM host process’s command line. It is what distinguishes an interactive login from a ProxyCommand invocation.
RemoteHost is the peer address when the client is itself running inside an SSH session (SSH_CONNECTION), the analogue of PAM_RHOST. Empty at a local terminal.
object
Extensions are the SSH certificate extensions requested (e.g. “permit-pty”, “permit-agent-forwarding”).
ForceCommand is the SSH “force-command” critical option request.
NoTouchRequired requests the OpenSSH “no-touch-required” extension. Only meaningful for service enrollment of a hardware-backed sk- key.
SourceAddresses are the caller’s own interface addresses, unioned server-side with the address the request was observed from.
The rest of the host context, the same set and the same trust as PAMRequestBody’s – see that type for what each field means and https://mnestor.github.io/ssoossh/internals/host-context/ for how far each one travels. The client reads them off its own process and machine (internal/hostinfo); every value is self-reported by an unauthenticated caller and the approval page renders each as a claim.
A user request carried only the two identity fields above until this
existed, so an approver deciding a ssh login saw two strings where
an approver deciding a sudo saw a dozen. All optional: an older
client that sends none of them still works, and a field the platform
cannot answer is simply absent.
PAMService and Mode have no analogue here and are not sent. The endpoint already says this is a user request, and there is no pam.d line to have configured a mode in.
RequestingUser is SUDO_USER, the analogue of PAM_RUSER: who invoked the client, when that differs from the account it runs as.
TrustedCAFingerprints is the SHA256 fingerprint of the CA public key
this client has pinned (the capubkey setting), in OpenSSH form.
Sent only when it was pinned: a key the client fetched from this same
server says nothing the server does not already know, and reporting
one as trust would be circular.
TTY is the controlling terminal where the platform can name one.
Example generated
{}Responses
Section titled “Responses”Request created
object
object
ExpiresAt is when this request stops being approvable, from its own
type’s budget (cert_options.
A client bounds its wait by this rather than by a local guess, and a console displays the time remaining from it. Without it, the client’s timeout and the server’s budget are two numbers an operator keeps in agreement by hand, and disagreement surfaces as a client still waiting on a request the server already killed. RFC 8628 carries expires_in for the same reason.
UserCode is the short code a human types into the web UI, grouped for display (“K7M4-QP2X”). Console requests only; empty for every other type, so no existing consumer changes.
This is the only response it ever appears in. It is not in the SSE stream, not in the request detail, and not in any audit record.
VerificationURL is the page that accepts UserCode; VerificationURLComplete embeds the code so a device with a camera or a keyboard can skip the code box entirely. Relative, like the two URLs above, and console requests only.
Complete is kept short (/c/, not /approve/
Example generated
{ "data": { "approval_url": "example", "events_url": "example", "expires_at": "example", "request_id": "example", "user_code": "example", "verification_url": "example", "verification_url_complete": "example" }, "error": "example"}Malformed body, or a public key that will not parse
object
Data is always null on an error response.
Error is a human-readable message. Branch on the HTTP status rather than this string; it is for humans and logs.
ErrorCode is a machine-readable error classifier. One of the error_code constants from internal/apitypes; use this to decide whether to retry or branch on the failure mode.
Example
{ "error": "certificate request \"9f1c…\" not found", "error_code": "not_found"}