Skip to content

Certificates API ​

List, renew, rotate, and download the TLS and mTLS certificates that secure Lamaste.

In Plain English ​

Lamaste manages two kinds of certificates:

  1. Let's Encrypt certificates — these are the standard HTTPS certificates that browsers trust. Each domain and subdomain gets one, and they auto-renew every 60-90 days via certbot.

  2. mTLS client certificates — this is the special certificate in your browser that proves you are the admin. It is self-signed by Lamaste's own certificate authority and used to access the management panel.

The certificates API lets you see all certificates and their expiry dates, force an early renewal of any Let's Encrypt certificate, rotate the mTLS client certificate (generating a new one), download the client certificate file, and check whether the auto-renewal timer is running.

Authentication ​

All certificate endpoints require a valid mTLS client certificate and a completed onboarding, with one exception: POST /api/enroll is a public endpoint that does not require mTLS. It is used by agents to complete hardware-bound certificate enrollment using a token previously generated by an admin. See the API Overview for details.

If onboarding is not complete, all endpoints return 503 Service Unavailable.

Endpoints ​

GET /api/certs ​

Returns all certificates from both sources (Let's Encrypt and mTLS), sorted by days until expiry (soonest first).

Request:

No request body.

bash
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs | jq

Response (200):

json
{
  "certs": [
    {
      "type": "mtls-ca",
      "domain": null,
      "expiresAt": "2036-03-13T00:00:00.000Z",
      "daysUntilExpiry": 3650,
      "path": "/etc/lamalibre/lamaste/pki/ca.crt",
      "expiringSoon": false
    },
    {
      "type": "mtls-client",
      "domain": null,
      "expiresAt": "2028-03-13T00:00:00.000Z",
      "daysUntilExpiry": 730,
      "path": "/etc/lamalibre/lamaste/pki/client.crt",
      "expiringSoon": false
    },
    {
      "type": "letsencrypt",
      "domain": "panel.example.com",
      "expiresAt": "2026-06-11T00:00:00.000Z",
      "daysUntilExpiry": 90,
      "path": "/etc/letsencrypt/live/panel.example.com/fullchain.pem",
      "expiringSoon": false
    },
    {
      "type": "letsencrypt",
      "domain": "app.example.com",
      "expiresAt": "2026-04-12T00:00:00.000Z",
      "daysUntilExpiry": 30,
      "path": "/etc/letsencrypt/live/app.example.com/fullchain.pem",
      "expiringSoon": true
    }
  ]
}
FieldTypeDescription
typestring"letsencrypt", "mtls-ca", or "mtls-client"
domainstring | nullDomain name for Let's Encrypt certs, null for mTLS certs
expiresAtstringISO 8601 expiry date
daysUntilExpirynumberDays remaining until expiry
pathstringFilesystem path to the certificate
expiringSoonbooleantrue if fewer than 30 days remain

The endpoint never fails entirely — if one source (Let's Encrypt or mTLS) cannot be read, the other source's certificates are still returned. Errors are logged server-side.


GET /api/certs/auto-renew-status ​

Checks the status of the certbot auto-renewal systemd timer. Lamaste looks for both certbot.timer and certbot-renew.timer (different distributions use different names).

Request:

No request body.

bash
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs/auto-renew-status | jq

Response (200) — timer active:

json
{
  "active": true,
  "nextRun": "2026-03-14T03:00:00.000Z",
  "lastRun": "2026-03-13T03:00:00.000Z"
}

Response (200) — timer inactive:

json
{
  "active": false,
  "nextRun": null,
  "lastRun": null
}
FieldTypeDescription
activebooleanWhether the certbot renewal timer is running
nextRunstring | nullISO 8601 timestamp of next scheduled run, or null
lastRunstring | nullISO 8601 timestamp of last run, or null

This endpoint always returns 200. If the timer status cannot be determined, it returns active: false with null timestamps.


POST /api/certs/:domain/renew ​

Forces immediate renewal of a Let's Encrypt certificate: sudo lamaste-certbot renew <domain> force, which runs certbot renew --cert-name <domain> --force-renewal --no-random-sleep-on-renew --non-interactive. The random delay certbot otherwise adds before a non-interactive renewal (up to about 8 minutes, meant for certbot.timer) is skipped, because an operator is waiting. After renewal, the new expiry date is read from the certificate, and nginx is reloaded to pick up the new cert.

Request:

No request body. The domain is specified in the URL path.

URL parameter validation:

The :domain parameter is validated against ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ — a strict DNS hostname regex that enforces valid label lengths and boundary characters.

bash
curl -s --cert client.p12:password \
  -X POST \
  https://203.0.113.42:9292/api/certs/app.example.com/renew | jq

Response (200):

json
{
  "ok": true,
  "domain": "app.example.com",
  "newExpiry": "2026-06-11T00:00:00.000Z"
}

Response (200) — with warning:

json
{
  "ok": true,
  "domain": "app.example.com",
  "newExpiry": "2026-06-11T00:00:00.000Z",
  "warning": "Certificate renewed but nginx reload failed"
}
FieldTypeDescription
okbooleantrue if renewal succeeded
domainstringThe renewed domain
newExpirystring | nullISO 8601 expiry date of the new certificate, or null if it could not be read
warningstringPresent if nginx reload or config test failed after renewal

Errors:

StatusBodyWhen
400{"error":"Validation failed","details":{"issues":[...]}}Invalid domain format
404{"error":"Certificate not found"}certbot has no certificate with this name
500{"error":"Certificate renewal failed"}certbot command failed

POST /api/certs/mtls/rotate ​

Would rotate the admin mTLS client certificate. The panel does not issue admin certificates — it runs unprivileged and signs only agent certificates, so a compromised panel cannot mint an admin credential — and this endpoint answers 503 with the instruction to run sudo lamaste-server reset-admin on the server console, which performs the rotation (see Certificate Management). The response shape below is what the route returns if a rotation succeeds.

This is a disruptive operation. After rotation, you must download and import the new client certificate into your browser. The old certificate will no longer be accepted.

Request:

No request body.

bash
curl -s --cert client.p12:password \
  -X POST \
  https://203.0.113.42:9292/api/certs/mtls/rotate | jq

Response (200):

json
{
  "ok": true,
  "p12Password": "a1b2c3d4e5f6...",
  "expiresAt": "2028-03-16T00:00:00.000Z",
  "warning": "Your current browser certificate is now invalid. Download and import the new certificate before closing this page."
}
FieldTypeDescription
okbooleantrue if rotation succeeded
p12PasswordstringRandom hex password for the new .p12 bundle. Only shown once
expiresAtstringISO 8601 expiry date of the new certificate (2-year validity)
warningstringAlways present. Notes that the old certificate is now invalid. If nginx reload or config test also fails, the warning is appended with details

Errors:

StatusBodyWhen
410{"error":"P12 certificate rotation is disabled. Admin uses hardware-bound authentication. Use lamaste-reset-admin on the server to revert."}adminAuthMode is hardware-bound
503{"error":"Panel-initiated admin certificate rotation is disabled for security. Run sudo lamaste-server reset-admin ..."}Always (P12 mode): use lamaste-server reset-admin
500{"error":"mTLS rotation failed: ..."}Certificate generation or file operations failed
500{"error":"CA key not found — cannot sign new certificate"}CA key missing from PKI directory

GET /api/certs/mtls/download ​

Downloads the current mTLS client certificate as a PKCS#12 (.p12) file. This is the file you import into your browser or keychain.

Request:

No request body.

bash
curl -s --cert client.p12:password \
  -o client.p12 \
  https://203.0.113.42:9292/api/certs/mtls/download

Response (200):

Returns Content-Type: application/x-pkcs12 with Content-Disposition: attachment; filename="client.p12".

The response body is the raw binary PKCS#12 file.

Errors:

StatusBodyWhen
410{"error":"P12 certificate download is disabled. Admin uses hardware-bound authentication. Use lamaste-reset-admin on the server to revert."}adminAuthMode is hardware-bound
404{"error":"No client certificate found"}The .p12 file does not exist at the expected path
500{"error":"Failed to download certificate"}File read failed

Agent Certificate Endpoints ​

Agent certificates provide scoped access for tunnel clients. There are two types:

  • Regular agents (CN=agent:<label>) — created by the admin, default tunnels:read capability
  • Plugin-agents (CN=plugin-agent:<delegatingLabel>:<pluginAgentLabel>) — created via delegated enrollment, no default capabilities

Only admins can manage agent certificates directly. Agents can create delegated enrollment tokens for plugin-agents via POST /api/certs/agent/enroll-delegated.

POST /api/certs/agent ​

Generates a new agent-scoped certificate with the specified label, capabilities, and optional site access list. The certificate CN is set to agent:<label>.

Request:

json
{
  "label": "macbook-pro",
  "capabilities": ["tunnels:read", "tunnels:write", "sites:read", "sites:write"],
  "allowedSites": ["blog", "docs"]
}
FieldTypeRequiredDescription
labelstringYes1-50 characters, lowercase letters, numbers, and hyphens only
capabilitiesstring[]NoList of capabilities. Defaults to ["tunnels:read"]. Must always include tunnels:read
allowedSitesstring[]NoList of site names this agent can access for file operations. Defaults to [] (no site access). Only relevant when sites:read or sites:write capabilities are granted

Valid capabilities: tunnels:read, tunnels:write, services:read, services:write, system:read, sites:read, sites:write, panel:expose, identity:read, identity:query

bash
curl -s --cert client.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"label":"macbook-pro","capabilities":["tunnels:read","sites:read","sites:write"],"allowedSites":["blog"]}' \
  https://203.0.113.42:9292/api/certs/agent | jq

Response (200):

json
{
  "ok": true,
  "label": "macbook-pro",
  "p12Password": "a1b2c3d4e5f6...",
  "serial": "ABC123DEF456...",
  "expiresAt": "2028-03-16T00:00:00.000Z"
}
FieldTypeDescription
okbooleantrue if generation succeeded
labelstringThe agent label
p12PasswordstringRandom hex password for the .p12 bundle. Only shown once
serialstringCertificate serial number
expiresAtstringISO 8601 expiry date (2-year validity)

Important: The p12Password is only returned at generation time. It cannot be retrieved later.

Errors:

StatusBodyWhen
400{"error":"Validation failed","details":{...}}Invalid label format or missing tunnels:read
409{"error":"Agent certificate with label \"macbook-pro\" already exists"}Label already in use (non-revoked)
500{"error":"Agent certificate generation failed: ..."}OpenSSL or file operation failed

GET /api/certs/agent ​

Lists all agent certificates with their status, capabilities, and expiry information.

Request:

No request body.

bash
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs/agent | jq

Response (200):

json
{
  "agents": [
    {
      "label": "macbook-pro",
      "serial": "ABC123DEF456...",
      "capabilities": ["tunnels:read", "tunnels:write", "sites:read", "sites:write"],
      "allowedSites": ["blog", "docs"],
      "enrollmentMethod": "hardware-bound",
      "createdAt": "2026-03-16T10:00:00.000Z",
      "expiresAt": "2028-03-16T10:00:00.000Z",
      "revoked": false,
      "expiringSoon": false,
      "status": "active",
      "hostFacts": {
        "os": "darwin",
        "arch": "arm64",
        "distro": null,
        "node": "22.13.1",
        "systemd": false,
        "apphost": null
      },
      "hostFactsAt": "2026-10-16T09:00:00.000Z",
      "pushInstallEnabledUntil": "2026-10-16T10:30:00.000Z",
      "pushInstallPolicy": "default"
    },
    {
      "label": "office-imac",
      "serial": "789GHI012...",
      "capabilities": ["tunnels:read"],
      "allowedSites": [],
      "enrollmentMethod": "p12",
      "createdAt": "2026-03-10T08:00:00.000Z",
      "expiresAt": "2028-03-10T08:00:00.000Z",
      "revoked": true,
      "expiringSoon": false,
      "status": "revoked",
      "hostFacts": null,
      "hostFactsAt": null
    }
  ]
}
FieldTypeDescription
labelstringAgent identifier (matches certificate CN agent:<label>)
serialstringCertificate serial number
capabilitiesstring[]Granted capabilities
allowedSitesstring[]Site names this agent can access for file operations
enrollmentMethodstring"p12", "hardware-bound", or "delegated" — how the agent certificate was issued
delegatedBystring | undefinedFor delegated certs: the label of the delegating agent
certTypestring | undefined"plugin-agent" for delegated certs, absent for regular agents
createdAtstringISO 8601 creation timestamp
expiresAtstringISO 8601 expiry timestamp
revokedbooleanWhether the certificate has been revoked
expiringSoonbooleantrue if the certificate is not revoked and fewer than 30 days remain until expiry
statusstring"revoked", "expired", "expiring-soon" (within 30 days) or "active", in that order
hostFactsobject | nullWhat the agent last reported about its host (OS, distribution, architecture, Node.js, systemd, its app host helper) — see Host facts; null until it has
hostFactsAtstring | nullWhen it reported them
pushInstallEnabledUntilstring | undefinedEnd of the agent's last push-install window (open while in the future); absent once it is closed
pushInstallPolicystring | undefinedThe push-install policy of that window

GET /api/certs/agent/:label/download ​

Downloads an agent certificate as a PKCS#12 (.p12) file.

Request:

No request body. The label is specified in the URL path.

bash
curl -s --cert client.p12:password \
  -o macbook-pro.p12 \
  https://203.0.113.42:9292/api/certs/agent/macbook-pro/download

Response (200):

Returns Content-Type: application/x-pkcs12 with Content-Disposition: attachment; filename="macbook-pro.p12".

Errors:

StatusBodyWhen
400{"error":"Validation failed",...}Invalid label format
404{"error":"Agent certificate \"macbook-pro\" not found"}No P12 file on disk (may have been revoked)

PATCH /api/certs/agent/:label/capabilities ​

Updates the capabilities for an existing agent certificate. Capabilities are stored server-side in the agent registry, not in the certificate itself, so no certificate reissue is needed.

Request:

json
{
  "capabilities": ["tunnels:read", "tunnels:write", "system:read"]
}
FieldTypeRequiredDescription
capabilitiesstring[]YesNew capability list. Must include tunnels:read for regular agents. Plugin-agents have no mandatory capabilities
bash
curl -s --cert client.p12:password \
  -X PATCH -H 'Content-Type: application/json' \
  -d '{"capabilities":["tunnels:read","tunnels:write","system:read"]}' \
  https://203.0.113.42:9292/api/certs/agent/macbook-pro/capabilities | jq

Response (200):

json
{
  "ok": true,
  "label": "macbook-pro",
  "capabilities": ["tunnels:read", "tunnels:write", "system:read"]
}

Errors:

StatusBodyWhen
400{"error":"Validation failed",...}Invalid capability or missing tunnels:read
404{"error":"Agent certificate \"macbook-pro\" not found"}No active agent with this label

PATCH /api/certs/agent/:label/allowed-sites ​

Updates the list of sites an agent certificate is allowed to access. This controls which sites the agent can see (via GET /api/sites) and which sites the agent can perform file operations on (upload, list, delete files). Like capabilities, allowedSites is stored server-side in the agent registry, so no certificate reissue is needed.

Request:

json
{
  "allowedSites": ["blog", "docs", "landing-page"]
}
FieldTypeRequiredDescription
allowedSitesstring[]YesNew list of site names this agent can access. Use [] to remove all site access
bash
curl -s --cert client.p12:password \
  -X PATCH -H 'Content-Type: application/json' \
  -d '{"allowedSites":["blog","docs"]}' \
  https://203.0.113.42:9292/api/certs/agent/macbook-pro/allowed-sites | jq

Response (200):

json
{
  "ok": true,
  "label": "macbook-pro",
  "allowedSites": ["blog", "docs"]
}

Errors:

StatusBodyWhen
400{"error":"Validation failed",...}Invalid request body
404{"error":"Agent certificate \"macbook-pro\" not found"}No active agent with this label

DELETE /api/certs/agent/:label ​

Revokes an agent certificate. The certificate serial is added to the revocation list, the registry entry is marked as revoked, and the P12 files are deleted from disk. The agent loses access immediately.

Cascade revocation: When a regular agent is revoked, all plugin-agents delegated by that agent are automatically cascade-revoked in the same operation. Their serials are added to revoked.json and their cert files cleaned up.

Chisel credential and tunnels: Revoking a regular agent also removes its Chisel credential (Chisel restarts, ending the agent's tunnel session) and releases its tunnels: its app and plugin tunnels become unassigned — their hostnames stay reserved and dark until an administrator assigns them to another agent — and its agent-<label> panel tunnel is removed with its vhost. A later enrollment that reuses the label therefore inherits no tunnels.

Request:

No request body.

bash
curl -s --cert client.p12:password \
  -X DELETE \
  https://203.0.113.42:9292/api/certs/agent/macbook-pro | jq

Response (200):

json
{
  "ok": true,
  "label": "macbook-pro"
}

Errors:

StatusBodyWhen
400{"error":"Validation failed",...}Invalid label format
404{"error":"Agent certificate \"macbook-pro\" not found"}No agent with this label, or already revoked

Chisel Credential Endpoints ​

Every regular agent has its own Chisel credential — the user agent-<label> and a random password — which its tunnel client presents to the Chisel server. The credential is minted at enrollment; these endpoints fetch and rotate it. Plugin-agents have none.

GET /api/agents/me/chisel-credential (agent-only) ​

Returns the calling agent's credential. The agent is identified strictly by its certificate — there is no label parameter, so an agent can never read another agent's credential.

Required capability: tunnels:read

Response (200):

json
{
  "user": "agent-macbook-pro",
  "password": "<64 hex characters>",
  "createdAt": "2026-09-20T10:12:00.000Z"
}

createdAt is when this password was issued. It matches chiselCredentialIssuedAt in GET /api/tunnels/agent-config, which is how an agent's sync timer notices that an administrator rotated its credential and fetches the new one.

Errors:

StatusBodyWhen
400{"error":"Agent label could not be determined from client certificate"}Certificate has no agent label
404{"error":"No chisel credential found for this agent","hint":"..."}No credential; an admin must rotate one in

POST /api/agents/me/chisel-credential/rotate (agent-only) ​

Replaces the calling agent's credential and returns the new one (same shape as above). An agent upgraded from a version that exposed its credential in process arguments and 0644 unit files calls this once, automatically, on its first sync.

Every rotation restarts Chisel — the old password must stop working — which briefly drops every agent's tunnels. An agent may therefore rotate only a credential that is at least 10 minutes old.

Required capability: tunnels:read

Errors:

StatusBodyWhen
429{"error":"The chisel credential was issued less than 10 minutes ago","issuedAt":"..."}The current credential is under 10 min old

POST /api/agents/:label/chisel-credential/rotate (admin-only) ​

Mints a new credential for the named agent, rewrites the authfile and restarts Chisel. The response includes the new credential and restartOk (plus restartError when the restart failed). The agent does not need to be told: its sync timer sees the new chiselCredentialIssuedAt within about 30 seconds and fetches the credential through the /me endpoint (lamaste-agent chisel refresh-credential does it at once).

StatusWhen
400The label is a plugin-agent
404No active agent with this label

Agent Daemon Authorization Endpoints ​

An agent's lamaste-agentd accepts a request from the relay only with a relay assertion (X-Lamalibre-Relay-Assertion, Ed25519) — see Security Model.

GET /api/agents/me/relay-keys (admin and agent) ​

The public keys relay assertions are signed with, and the label this relay knows the calling agent by — the audience of the assertions made for it, which its local registry name need not equal. lamaste-agentd fetches this over its pinned mTLS channel on the first request from the relay and again when an assertion names a key it does not know (at most every 30 seconds). Public material; rate limited to 60 per minute.

Response (200):

json
{
  "keys": [{ "kid": "<32 hex>", "publicKey": "<base64 DER SubjectPublicKeyInfo, Ed25519>" }],
  "agentLabel": "macbook-pro"
}

kid is the first 16 bytes of the SHA-256 of the DER key, hex; the daemon derives it from the key and refuses a response where they disagree. agentLabel is null for an admin or plugin-agent certificate.

StatusWhen
503The relay's signing key file is unusable (permissions, content)

GET /api/agent-panel/authz/:label (nginx only) ​

The auth_request target of the agent-panel vhost agent-<label>.<domain>, called by nginx for every request on it — not by clients. nginx sends the client certificate's verification result (X-SSL-Client-*, so the mTLS middleware refuses a revoked or unknown certificate), and the request's X-Original-Method and X-Original-URI ($request_uri). Admitted: the admin certificate and the certificate of the agent the panel belongs to, when that agent has an enabled panel tunnel. The answer is 200 with the assertion for exactly that request in X-Lamalibre-Relay-Assertion (and Cache-Control: no-store), which nginx forwards to the daemon. Exempt from panel 2FA — the browser is on the agent's hostname and has no 2FA cookie; agent panels are guarded by the client certificate alone. Reachable through the panel's own vhosts as well, where it gains nothing: it only ever asserts the caller's own, verified certificate, for a panel that certificate may use.

StatusWhen
400Missing or malformed X-Original-Method / X-Original-URI
403Another agent's certificate, a plugin-agent, or no enabled panel tunnel
503The relay's signing key file is unusable

Hardware-Bound Certificate Endpoints ​

Hardware-bound certificates use a CSR-based enrollment flow where the private key never leaves the agent machine. Instead of generating a P12 bundle on the server, the agent generates a key pair locally and submits a Certificate Signing Request (CSR) to the panel. The panel signs the CSR and returns the certificate.

POST /api/certs/agent/enroll — Generate enrollment token (admin-only) ​

Creates a single-use enrollment token that an agent can use to enroll with a hardware-bound certificate. The token expires after 10 minutes. The request body uses the same schema as POST /api/certs/agent.

Request:

json
{
  "label": "macbook-pro",
  "capabilities": ["tunnels:read", "tunnels:write", "sites:read"],
  "allowedSites": ["blog"]
}
FieldTypeRequiredDescription
labelstringYes1-50 characters, lowercase letters, numbers, and hyphens only
capabilitiesstring[]NoList of capabilities. Defaults to ["tunnels:read"]. Must always include tunnels:read
allowedSitesstring[]NoList of site names this agent can access for file operations. Defaults to [] (no site access). Only relevant when sites:read or sites:write capabilities are granted
bash
curl -s --cert client.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"label":"macbook-pro","capabilities":["tunnels:read","tunnels:write"],"allowedSites":[]}' \
  https://203.0.113.42:9292/api/certs/agent/enroll | jq

Response (200):

json
{
  "ok": true,
  "token": "a1b2c3d4e5f6...",
  "label": "macbook-pro",
  "expiresAt": "2026-03-23T10:10:00.000Z"
}
FieldTypeDescription
okbooleantrue if the token was generated successfully
tokenstringSingle-use enrollment token. Only shown once
labelstringThe agent label the token is associated with
expiresAtstringISO 8601 timestamp when the token expires (10 min)

Errors:

StatusBodyWhen
400{"error":"Validation failed","details":{...}}Invalid label format or missing tunnels:read
409{"error":"Agent certificate with label \"macbook-pro\" already exists"}Label already in use (non-revoked)

Note: If a pending (unused, unexpired) enrollment token already exists for the same label, it is silently replaced by the new token. This allows retried installations without requiring explicit token revocation first.


POST /api/certs/agent/enroll-delegated — Delegated enrollment token (agent-only) ​

Creates a single-use enrollment token for a plugin-agent certificate. The calling agent must own an active ticket instance for the given scope. Only regular agents can delegate — plugin-agents cannot (prevents recursive delegation chains).

The returned token is passed out-of-band to the plugin agent, which submits it alongside a CSR to POST /api/enroll. The issued certificate has CN plugin-agent:<delegatingLabel>:<pluginAgentLabel> and starts with no base capabilities (only the delegated scope capability).

Authentication: Agent-only (mTLS with agent certificate). Plugin-agents are rejected with 403.

Request:

json
{
  "pluginAgentLabel": "rpi-sync",
  "scope": "plugin:sync:connect"
}
FieldTypeRequiredDescription
pluginAgentLabelstringYes1-50 characters, must start with a letter or number, lowercase letters, numbers, and hyphens only
scopestringYesTicket sub-scope in plugin:<name>:<action> format (e.g., plugin:sync:connect). Must be a registered ticket scope, not a base capability
bash
curl -s --cert agent.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"pluginAgentLabel":"rpi-sync","scope":"plugin:sync:connect"}' \
  https://203.0.113.42:9292/api/certs/agent/enroll-delegated | jq

Response (200):

json
{
  "ok": true,
  "enrollmentToken": "a1b2c3d4e5f6...",
  "expiresAt": "2026-04-04T10:10:00.000Z",
  "pluginAgentLabel": "rpi-sync"
}
FieldTypeDescription
okbooleantrue if the token was generated successfully
enrollmentTokenstringSingle-use delegated enrollment token. Only shown once
expiresAtstringISO 8601 timestamp when the token expires (10 min)
pluginAgentLabelstringThe plugin agent label

Errors:

StatusBodyWhen
400{"error":"Validation failed","details":{...}}Invalid label or scope format
403{"error":"Plugin agents cannot delegate enrollment"}Caller is a plugin-agent (recursive delegation blocked)
403{"error":"Agent label not found in certificate"}Calling certificate has no agent label
403{"error":"Insufficient delegation authority"}Agent does not own an active ticket instance for the scope
409{"error":"Agent certificate with label \"plugin-agent:macbook-pro:rpi-sync\" already exists"}Plugin-agent label already has an active (non-revoked) cert
500{"error":"Delegated enrollment token generation failed"}Token generation error

How it works:

  1. The panel verifies the calling agent is a regular agent (not a plugin-agent)
  2. Checks that the scope is a registered ticket scope (not a base capability like tunnels:read)
  3. Verifies the agent owns an active ticket instance for that scope
  4. Generates a delegated enrollment token with type: 'delegated', delegatedBy: <callingAgentLabel>, and scope
  5. The plugin agent uses this token with POST /api/enroll to receive a CN=plugin-agent:<delegatingLabel>:<pluginAgentLabel> certificate

Note: When an enrollment token already exists for the same plugin-agent label, it is silently replaced. When the delegating agent is later revoked, all plugin-agents it delegated are cascade-revoked automatically.


DELETE /api/certs/agent/enroll/:label — Revoke unused enrollment token (admin-only) ​

Revokes any active (unused, unexpired) enrollment token for the given agent label. This should be called when an agent installation fails before enrollment to prevent the token from being used on another machine.

Request:

No request body. The label is specified in the URL path.

bash
curl -s --cert client.p12:password \
  -X DELETE \
  https://203.0.113.42:9292/api/certs/agent/enroll/macbook-pro | jq

Response (200):

json
{
  "ok": true,
  "revoked": true
}
FieldTypeDescription
okbooleanAlways true
revokedbooleantrue if an active token was found and removed, false if no matching active token existed

Errors:

StatusBodyWhen
400{"error":"Validation failed",...}Invalid label format

POST /api/enroll — Enroll agent with token (PUBLIC, no mTLS) ​

Completes hardware-bound certificate enrollment. The agent submits the enrollment token along with a PEM-encoded Certificate Signing Request (CSR). The panel validates the token, signs the CSR with the CA key, and returns the signed certificate and CA certificate. This endpoint does not require mTLS — it is publicly accessible like /api/invite/*.

Request:

json
{
  "token": "a1b2c3d4e5f6...",
  "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
}
FieldTypeRequiredDescription
tokenstringYesThe enrollment token from POST /api/certs/agent/enroll
csrstringYesPEM-encoded PKCS#10 Certificate Signing Request
bash
curl -s -k \
  -X POST -H 'Content-Type: application/json' \
  -d '{"token":"a1b2c3d4e5f6...","csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
  https://203.0.113.42:9292/api/enroll | jq

Response (200):

json
{
  "ok": true,
  "cert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
  "caCert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
  "label": "macbook-pro",
  "serial": "ABC123DEF456...",
  "expiresAt": "2028-03-23T00:00:00.000Z"
}
FieldTypeDescription
okbooleantrue if enrollment succeeded
certstringPEM-encoded signed client certificate
caCertstringPEM-encoded CA certificate for TLS verification
labelstringThe agent label
serialstringCertificate serial number
expiresAtstringISO 8601 expiry date (2-year validity)

Important: The token is consumed upon successful enrollment and cannot be reused. The agent stores the returned certificate alongside its locally generated private key.

Errors:

StatusBodyWhen
400{"error":"Validation failed","details":{...}}Missing or malformed token or csr field
400{"error":"Validation failed","details":{...}}CSR field missing PEM header or other validation failure
400{"error":"CSR too large"}CSR exceeds 8192 bytes
400{"error":"Invalid CSR: structure or signature verification failed"}CSR fails OpenSSL validation
401{"error":"Invalid enrollment token"}Token not found
401{"error":"Enrollment token has already been used"}Token already consumed
401{"error":"Enrollment token has expired"}Token past expiry
500{"error":"Enrollment failed"}Server-side signing error (details hidden)
409{"error":"Agent certificate with label \"macbook-pro\" already exists"}Label already has an active (non-revoked) certificate

POST /api/certs/admin/upgrade-to-hardware-bound — Upgrade admin to hardware-bound (admin-only) ​

Upgrades the admin certificate from P12-based authentication to hardware-bound authentication. The admin submits a PEM-encoded CSR (generated from a key pair in the system keychain), and the panel would sign it with the CA key. The panel does not issue CN=admin certificates, so this endpoint currently answers 503 ("Panel-initiated admin cert issuance is disabled for security. Run sudo lamaste-server reset-admin ..."). The flow it describes: the old admin P12 certificate is revoked, and adminAuthMode is set to hardware-bound in the panel configuration. After this upgrade, P12 rotation and download endpoints return 410 Gone.

Request:

json
{
  "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
}
FieldTypeRequiredDescription
csrstringYesPEM-encoded PKCS#10 Certificate Signing Request
bash
curl -s --cert client.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
  https://203.0.113.42:9292/api/certs/admin/upgrade-to-hardware-bound | jq

Response (200):

json
{
  "ok": true,
  "cert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
  "caCert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
  "serial": "DEF789GHI012...",
  "expiresAt": "2028-03-23T00:00:00.000Z"
}
FieldTypeDescription
okbooleantrue if the upgrade succeeded
certstringPEM-encoded signed admin certificate
caCertstringPEM-encoded CA certificate for TLS verification
serialstringCertificate serial number of the new certificate
expiresAtstringISO 8601 expiry date (2-year validity)

Important: This operation is irreversible in normal use. After upgrade, the old P12 certificate is revoked and adminAuthMode is set to hardware-bound. The admin must use the hardware-bound certificate for all future panel access.

Errors:

StatusBodyWhen
400{"error":"Validation failed","details":{...}}Missing or malformed csr field
400{"error":"CSR too large"}CSR exceeds 8192 bytes
400{"error":"Invalid CSR: structure or signature verification failed"}CSR fails OpenSSL validation
409{"error":"Admin is already using hardware-bound authentication"}adminAuthMode is already hardware-bound
503{"error":"Panel-initiated admin cert issuance is disabled for security. ..."}Always: the panel does not sign admin CSRs
500{"error":"Admin upgrade failed"}OpenSSL signing or file operation failed

GET /api/certs/admin/auth-mode — Get admin auth mode (admin-only) ​

Returns the current admin authentication mode. This indicates whether the admin certificate was issued as a P12 bundle or upgraded to a hardware-bound certificate.

Request:

No request body.

bash
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs/admin/auth-mode | jq

Response (200):

json
{
  "adminAuthMode": "p12"
}
FieldTypeDescription
adminAuthModestring"p12" (default) or "hardware-bound" (after CSR upgrade)

POST /api/certs/agent/upgrade-cert — Upgrade agent cert to hardware-bound (agent-only) ​

Upgrades an existing agent certificate to hardware-bound authentication via CSR. The agent submits a PEM-encoded CSR generated from a key pair stored in the system keychain. The panel signs the CSR, revokes the old certificate, and updates the agent registry entry with the new serial and enrollmentMethod: 'hardware-bound'. Capabilities and allowed sites are preserved.

Authentication: Agent-only. Agents can only upgrade their own certificate (the label is extracted from the calling certificate's CN).

Request:

json
{
  "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
}
FieldTypeRequiredDescription
csrstringYesPEM-encoded PKCS#10 Certificate Signing Request
bash
curl -s --cert agent.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
  https://203.0.113.42:9292/api/certs/agent/upgrade-cert | jq

Response (200):

json
{
  "ok": true,
  "cert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
  "caCert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
  "serial": "ABC123DEF456...",
  "expiresAt": "2028-03-23T00:00:00.000Z"
}
FieldTypeDescription
okbooleantrue if the upgrade succeeded
certstringPEM-encoded signed agent certificate
caCertstringPEM-encoded CA certificate for TLS verification
serialstringCertificate serial number of the new certificate
expiresAtstringISO 8601 expiry date (2-year validity)

Errors:

StatusBodyWhen
400{"error":"Validation failed","details":{...}}Missing or malformed csr field
400{"error":"CSR too large"}CSR exceeds 8192 bytes
400{"error":"Invalid CSR: structure or signature verification failed"}CSR fails OpenSSL validation
403{"error":"Agent label not found in certificate"}Calling certificate has no agent label
404{"error":"Agent certificate \"macbook-pro\" not found"}No active (non-revoked) agent with this label
500{"error":"Agent certificate upgrade failed"}OpenSSL signing or file operation failed

Certificate Types ​

Let's Encrypt Certificates ​

  • Issued by: Let's Encrypt CA (publicly trusted)
  • Purpose: HTTPS for tunnel subdomains and panel domain
  • Validity: 90 days
  • Renewal: Automatic via certbot.timer (runs twice daily, renews when < 30 days remain)
  • Path pattern: /etc/letsencrypt/live/<domain>/fullchain.pem
  • Issued during: Onboarding provisioning (panel cert) and tunnel creation (per-subdomain certs)

mTLS Client Certificates ​

  • Issued by: Lamaste's self-signed CA
  • Purpose: Admin authentication to the management panel
  • Validity: 2 years
  • Renewal: Manual, sudo lamaste-server reset-admin on the server
  • Path: /etc/lamalibre/lamaste/pki/client.p12
  • Issued during: Initial installation by create-lamaste

Agent Certificates ​

  • Issued by: Lamaste's self-signed CA
  • Purpose: Scoped access for tunnel clients (CN=agent:<label>)
  • Validity: 2 years
  • Capabilities: Server-side, updatable without reissuing the certificate. Defaults to ["tunnels:read"]
  • Site access: Per-site scoping via allowedSites — agents can only see and modify files on sites explicitly assigned to them
  • Path: /etc/lamalibre/lamaste/pki/agents/<label>/client.p12
  • Issued via: POST /api/certs/agent (admin only) or POST /api/certs/agent/enroll (token-based)
  • Revocation: Immediate via /api/certs/agent/:label (serial added to revocation list). Cascade-revokes all plugin-agents delegated by this agent

Plugin-Agent Certificates ​

  • Issued by: Lamaste's self-signed CA
  • Purpose: Minimal identity for plugin agents participating in the ticket system (CN=plugin-agent:<delegatingLabel>:<pluginAgentLabel>)
  • Validity: 2 years
  • Capabilities: No default capabilities — starts empty. Admin can add capabilities via the Certificates page
  • Site access: None (plugin-agents never have allowed sites)
  • Path: /etc/lamalibre/lamaste/pki/agents/plugin-agent:<delegatingLabel>:<pluginAgentLabel>/client.p12 (generated during CSR enrollment)
  • Issued via: Delegated enrollment — agent calls POST /api/certs/agent/enroll-delegated, passes token to plugin agent, plugin agent calls POST /api/enroll with CSR
  • Revocation: Immediate via DELETE /api/certs/agent/<full-label>, or automatically cascade-revoked when the delegating agent is revoked

Quick Reference ​

MethodPathDescription
GET/api/certsList all certs (sorted by expiry)
GET/api/certs/auto-renew-statusCheck certbot timer status
POST/api/certs/:domain/renewForce-renew a Let's Encrypt cert
POST/api/certs/mtls/rotateRotate mTLS client certificate
GET/api/certs/mtls/downloadDownload client.p12 file
POST/api/certs/agentGenerate agent certificate (P12)
GET/api/certs/agentList agent certificates
GET/api/certs/agent/:label/downloadDownload agent .p12 file
PATCH/api/certs/agent/:label/capabilitiesUpdate agent capabilities
PATCH/api/certs/agent/:label/allowed-sitesUpdate agent site access
DELETE/api/certs/agent/:labelRevoke agent certificate (releases its tunnels)
GET/api/agents/me/chisel-credentialOwn Chisel credential (agent-only)
POST/api/agents/me/chisel-credential/rotateRotate own Chisel credential (agent-only, 10-min floor)
POST/api/agents/:label/chisel-credential/rotateRotate an agent's Chisel credential (admin-only)
GET/api/agents/me/relay-keysRelay assertion public keys and the caller's relay label
GET/api/agent-panel/authz/:labelAgent-panel vhost authorization (nginx auth_request)
POST/api/certs/agent/enrollGenerate enrollment token (admin-only)
POST/api/certs/agent/enroll-delegatedDelegated enrollment token (agent-only)
DELETE/api/certs/agent/enroll/:labelRevoke unused enrollment token
POST/api/certs/agent/upgrade-certUpgrade agent cert to hardware-bound
POST/api/enrollEnroll agent with token (public)
POST/api/certs/admin/upgrade-to-hardware-boundUpgrade admin to hardware-bound
GET/api/certs/admin/auth-modeGet admin auth mode

Certificate Object Shape ​

json
{
  "type": "letsencrypt",
  "domain": "app.example.com",
  "expiresAt": "2026-06-11T00:00:00.000Z",
  "daysUntilExpiry": 90,
  "path": "/etc/letsencrypt/live/app.example.com/fullchain.pem",
  "expiringSoon": false
}

curl Cheat Sheet ​

bash
# List all certificates
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs | jq

# Check auto-renewal timer
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs/auto-renew-status | jq

# Force-renew a certificate
curl -s --cert client.p12:password \
  -X POST \
  https://203.0.113.42:9292/api/certs/app.example.com/renew | jq

# Rotate mTLS certificate
curl -s --cert client.p12:password \
  -X POST \
  https://203.0.113.42:9292/api/certs/mtls/rotate | jq

# Download client certificate
curl -s --cert client.p12:password \
  -o client-new.p12 \
  https://203.0.113.42:9292/api/certs/mtls/download

# Generate an agent certificate with site access
curl -s --cert client.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"label":"macbook-pro","capabilities":["tunnels:read","sites:read","sites:write"],"allowedSites":["blog"]}' \
  https://203.0.113.42:9292/api/certs/agent | jq

# List agent certificates
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs/agent | jq

# Update agent capabilities
curl -s --cert client.p12:password \
  -X PATCH -H 'Content-Type: application/json' \
  -d '{"capabilities":["tunnels:read","services:read"]}' \
  https://203.0.113.42:9292/api/certs/agent/macbook-pro/capabilities | jq

# Update agent site access
curl -s --cert client.p12:password \
  -X PATCH -H 'Content-Type: application/json' \
  -d '{"allowedSites":["blog","docs"]}' \
  https://203.0.113.42:9292/api/certs/agent/macbook-pro/allowed-sites | jq

# Revoke an agent certificate
curl -s --cert client.p12:password \
  -X DELETE \
  https://203.0.113.42:9292/api/certs/agent/macbook-pro | jq

# Generate an enrollment token for hardware-bound agent enrollment
curl -s --cert client.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"label":"macbook-pro","capabilities":["tunnels:read","tunnels:write"]}' \
  https://203.0.113.42:9292/api/certs/agent/enroll | jq

# Generate a delegated enrollment token (agent-only)
curl -s --cert agent.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"pluginAgentLabel":"rpi-sync","scope":"sync:connect"}' \
  https://203.0.113.42:9292/api/certs/agent/enroll-delegated | jq

# Revoke an unused enrollment token
curl -s --cert client.p12:password \
  -X DELETE \
  https://203.0.113.42:9292/api/certs/agent/enroll/macbook-pro | jq

# Enroll agent with token and CSR (no mTLS required)
curl -s -k \
  -X POST -H 'Content-Type: application/json' \
  -d '{"token":"a1b2c3d4e5f6...","csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
  https://203.0.113.42:9292/api/enroll | jq

# Upgrade admin certificate to hardware-bound
curl -s --cert client.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
  https://203.0.113.42:9292/api/certs/admin/upgrade-to-hardware-bound | jq

# Upgrade agent certificate to hardware-bound (agent-only)
curl -s --cert agent.p12:password \
  -X POST -H 'Content-Type: application/json' \
  -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
  https://203.0.113.42:9292/api/certs/agent/upgrade-cert | jq

# Check admin auth mode
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/certs/admin/auth-mode | jq

Released under the PolyForm Noncommercial License 1.0.0