Create a PAM certificate request
const url = 'https://example.com/api/certs/pam';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/pam \ --header 'Content-Type: application/json' \ --data '{}'Unauthenticated. Username is the local account pam_ssoossh is authenticating,
reported by the caller and used for display and audit only. The certificate’s
principals are accounts the approver holds and selects at approval; the module
on the host matches them against that local account, directly or through its
principals-map. Set cert_options.pam.require to restrict who may approve one.
Request Bodyrequired
Section titled “Request Bodyrequired”The public key to sign, the local username being authenticated, and the options being asked for
object
The public key to sign, the local username being authenticated, and the options being asked for
object
CallerUID, CallerPID and CallerPPID identify the process on the host, for joining with the host’s own logs. Pointers so an absent value is distinguishable from uid 0 or pid 0. CallerGID is the process’s group, added with the Go client and accepted here so one column and one audit key mean the same thing whichever implementation sent the request; the C module does not report it yet.
Client names the module and its version (“pam_ssoossh-c/0.3.0”), which is what tells the two module implementations apart in a log.
ClientTime is the host’s own clock when it built the request, so skew against the server is visible before it fails a login.
The context an approver needs to tell a request they caused from one they did not: which machine, through which PAM service, at which terminal, and — since a real console has no remote host — whether PAM_RHOST says this did not come from a console at all.
Every one of these is self-reported by an unauthenticated caller and is displayed as a claim, never as a fact. Optional, so a client that does not read PAM items still works.
MachineID is a stable per-install identifier (/etc/machine-id or kern.hostuuid), so a host is still recognisable after a rename and two hosts claiming one name are distinguishable.
Mode is the module’s configured mode argument as written in pam.d (“auto”, “sudo”, “console”), not the route it resolved to; the endpoint already says that. Together they explain why a request arrived as a console one.
OS is the platform as the host describes itself: os-release PRETTY_NAME followed by uname -s and -r.
Process is the PAM host process’s command line (“sudo -i”, “sudo systemctl restart nginx”), read from /proc/self/cmdline where the platform has it. Empty where it does not.
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, same trust as the four above: every value is
what the module read off its own process and machine, and the approval
page renders each as a claim. They exist so an approver of a sudo can
see which command is asking, on which machine, invoked by whom, and so the
audit line joins against the host’s own auditd or journal. See
https://mnestor.github.io/ssoossh/internals/host-context/.
RequestingUser is PAM_RUSER: who invoked the service, as opposed to
Username, the account being authenticated. Under su or sudo’s
targetpw they differ.
TrustedCAFingerprints are the SHA256 fingerprints of the keys in the module’s trusted-ca-file, in OpenSSH form. The module will reject a certificate signed by any other key, so the server can warn the approver before that happens rather than after.
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"}