@lamalibre/create-lamaste 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/LICENSE.md +118 -0
  2. package/README.md +114 -0
  3. package/bin/create-lamaste.js +15 -0
  4. package/package.json +54 -0
  5. package/scripts/bundle-vendor.js +110 -0
  6. package/scripts/lamaste-cert-info +62 -0
  7. package/scripts/lamaste-certbot +96 -0
  8. package/scripts/lamaste-priv +693 -0
  9. package/src/index.js +646 -0
  10. package/src/lib/accounts.js +86 -0
  11. package/src/lib/binaries.js +137 -0
  12. package/src/lib/cert-help-page.js +160 -0
  13. package/src/lib/env.js +132 -0
  14. package/src/lib/http-redirect.js +38 -0
  15. package/src/lib/ownership.js +231 -0
  16. package/src/lib/relay-services.js +103 -0
  17. package/src/lib/secrets.js +19 -0
  18. package/src/lib/service-config.js +311 -0
  19. package/src/lib/summary.js +131 -0
  20. package/src/lib/wrappers.js +63 -0
  21. package/src/tasks/gatekeeper.js +233 -0
  22. package/src/tasks/harden.js +293 -0
  23. package/src/tasks/mtls.js +193 -0
  24. package/src/tasks/nginx.js +265 -0
  25. package/src/tasks/node.js +110 -0
  26. package/src/tasks/panel.js +408 -0
  27. package/src/tasks/redeploy.js +438 -0
  28. package/vendor/docs/00-introduction/how-it-works.md +447 -0
  29. package/vendor/docs/00-introduction/quickstart.md +472 -0
  30. package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
  31. package/vendor/docs/01-concepts/authentication.md +464 -0
  32. package/vendor/docs/01-concepts/authorization.md +242 -0
  33. package/vendor/docs/01-concepts/certificates.md +452 -0
  34. package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
  35. package/vendor/docs/01-concepts/mtls.md +490 -0
  36. package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
  37. package/vendor/docs/01-concepts/security-model.md +600 -0
  38. package/vendor/docs/01-concepts/tickets.md +414 -0
  39. package/vendor/docs/01-concepts/tunneling.md +367 -0
  40. package/vendor/docs/02-guides/agent-setup.md +194 -0
  41. package/vendor/docs/02-guides/certificate-management.md +341 -0
  42. package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
  43. package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
  44. package/vendor/docs/02-guides/disaster-recovery.md +464 -0
  45. package/vendor/docs/02-guides/first-tunnel.md +230 -0
  46. package/vendor/docs/02-guides/installation.md +393 -0
  47. package/vendor/docs/02-guides/managing-users.md +255 -0
  48. package/vendor/docs/02-guides/onboarding.md +244 -0
  49. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
  50. package/vendor/docs/02-guides/static-sites.md +364 -0
  51. package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
  52. package/vendor/docs/03-architecture/installer-flow.md +151 -0
  53. package/vendor/docs/03-architecture/installer.md +445 -0
  54. package/vendor/docs/03-architecture/management-flow.md +288 -0
  55. package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
  56. package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
  57. package/vendor/docs/03-architecture/overview.md +541 -0
  58. package/vendor/docs/03-architecture/panel-client.md +416 -0
  59. package/vendor/docs/03-architecture/panel-server.md +695 -0
  60. package/vendor/docs/03-architecture/state-management.md +529 -0
  61. package/vendor/docs/03-architecture/system-overview.md +167 -0
  62. package/vendor/docs/04-api-reference/certificates.md +1141 -0
  63. package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
  64. package/vendor/docs/04-api-reference/onboarding.md +473 -0
  65. package/vendor/docs/04-api-reference/overview.md +404 -0
  66. package/vendor/docs/04-api-reference/services.md +267 -0
  67. package/vendor/docs/04-api-reference/settings.md +343 -0
  68. package/vendor/docs/04-api-reference/sites.md +716 -0
  69. package/vendor/docs/04-api-reference/system.md +268 -0
  70. package/vendor/docs/04-api-reference/tickets.md +618 -0
  71. package/vendor/docs/04-api-reference/tunnels.md +417 -0
  72. package/vendor/docs/04-api-reference/users.md +333 -0
  73. package/vendor/docs/05-operations/backup-and-restore.md +342 -0
  74. package/vendor/docs/05-operations/monitoring.md +285 -0
  75. package/vendor/docs/05-operations/uninstalling.md +344 -0
  76. package/vendor/docs/05-operations/upgrades.md +270 -0
  77. package/vendor/docs/06-reference/config-files.md +1080 -0
  78. package/vendor/docs/06-reference/glossary.md +528 -0
  79. package/vendor/docs/06-reference/installer-flags.md +260 -0
  80. package/vendor/docs/06-reference/ports-and-services.md +244 -0
  81. package/vendor/docs/06-reference/troubleshooting.md +665 -0
  82. package/vendor/docs/_index.json +233 -0
  83. package/vendor/docs/gatekeeper-design.md +953 -0
  84. package/vendor/docs/package.json +15 -0
  85. package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
  86. package/vendor/gatekeeper/dist/boot.d.ts +1 -0
  87. package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
  88. package/vendor/gatekeeper/dist/boot.js +17 -0
  89. package/vendor/gatekeeper/dist/boot.js.map +1 -0
  90. package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
  91. package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
  92. package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
  93. package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
  94. package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
  95. package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
  96. package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
  97. package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
  98. package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
  99. package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
  100. package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
  101. package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
  102. package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
  103. package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
  104. package/vendor/gatekeeper/dist/cli/index.js +45 -0
  105. package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
  106. package/vendor/gatekeeper/dist/index.d.ts +9 -0
  107. package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
  108. package/vendor/gatekeeper/dist/index.js +13 -0
  109. package/vendor/gatekeeper/dist/index.js.map +1 -0
  110. package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
  111. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
  112. package/vendor/gatekeeper/dist/lib/authz.js +42 -0
  113. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
  114. package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
  115. package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
  116. package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
  117. package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
  118. package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
  119. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
  120. package/vendor/gatekeeper/dist/lib/constants.js +79 -0
  121. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
  122. package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
  123. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
  124. package/vendor/gatekeeper/dist/lib/grants.js +314 -0
  125. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
  126. package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
  127. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
  128. package/vendor/gatekeeper/dist/lib/groups.js +247 -0
  129. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
  130. package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
  131. package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
  132. package/vendor/gatekeeper/dist/lib/migration.js +79 -0
  133. package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
  134. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
  135. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
  136. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
  137. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
  138. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
  139. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
  140. package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
  141. package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
  142. package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
  143. package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
  144. package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
  145. package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
  146. package/vendor/gatekeeper/dist/lib/templates.js +151 -0
  147. package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
  148. package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
  149. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
  150. package/vendor/gatekeeper/dist/lib/types.js +2 -0
  151. package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
  152. package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
  153. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
  154. package/vendor/gatekeeper/dist/server/index.js +251 -0
  155. package/vendor/gatekeeper/dist/server/index.js.map +1 -0
  156. package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
  157. package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
  158. package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
  159. package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
  160. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
  161. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
  162. package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
  163. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
  164. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
  165. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
  166. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
  167. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
  168. package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
  169. package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
  170. package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
  171. package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
  172. package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
  173. package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
  174. package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
  175. package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
  176. package/vendor/gatekeeper/package.json +49 -0
  177. package/vendor/server/bin/lamaste-server.js +204 -0
  178. package/vendor/server/package.json +31 -0
  179. package/vendor/server/src/commands/certs.js +224 -0
  180. package/vendor/server/src/commands/chisel.js +132 -0
  181. package/vendor/server/src/commands/logs.js +49 -0
  182. package/vendor/server/src/commands/plugins.js +321 -0
  183. package/vendor/server/src/commands/reset-admin.js +478 -0
  184. package/vendor/server/src/commands/restart.js +94 -0
  185. package/vendor/server/src/commands/sites.js +257 -0
  186. package/vendor/server/src/commands/status.js +158 -0
  187. package/vendor/server/src/commands/tunnels.js +365 -0
  188. package/vendor/server/src/commands/uninstall.js +326 -0
  189. package/vendor/server/src/config.js +38 -0
  190. package/vendor/server/src/exec.js +26 -0
  191. package/vendor/server/src/logger.js +39 -0
  192. package/vendor/server/src/ndjson.js +56 -0
  193. package/vendor/server/src/panel-api.js +85 -0
  194. package/vendor/server/src/state.js +45 -0
  195. package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
  196. package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
  197. package/vendor/server-ui/dist/index.html +13 -0
  198. package/vendor/serverd/package.json +44 -0
  199. package/vendor/serverd/src/boot.js +13 -0
  200. package/vendor/serverd/src/index.js +191 -0
  201. package/vendor/serverd/src/lib/app-error.js +14 -0
  202. package/vendor/serverd/src/lib/authelia.js +85 -0
  203. package/vendor/serverd/src/lib/certbot.js +74 -0
  204. package/vendor/serverd/src/lib/chisel-args.js +6 -0
  205. package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
  206. package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
  207. package/vendor/serverd/src/lib/chisel-users.js +109 -0
  208. package/vendor/serverd/src/lib/chisel.js +81 -0
  209. package/vendor/serverd/src/lib/config.js +190 -0
  210. package/vendor/serverd/src/lib/constants.js +8 -0
  211. package/vendor/serverd/src/lib/csr-signing.js +331 -0
  212. package/vendor/serverd/src/lib/enrollment.js +302 -0
  213. package/vendor/serverd/src/lib/files.js +19 -0
  214. package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
  215. package/vendor/serverd/src/lib/invite-page.js +281 -0
  216. package/vendor/serverd/src/lib/ip-utils.js +79 -0
  217. package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
  218. package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
  219. package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
  220. package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
  221. package/vendor/serverd/src/lib/mtls.js +795 -0
  222. package/vendor/serverd/src/lib/nginx.js +1090 -0
  223. package/vendor/serverd/src/lib/pki-paths.js +41 -0
  224. package/vendor/serverd/src/lib/pki-sign.js +103 -0
  225. package/vendor/serverd/src/lib/plugins.js +538 -0
  226. package/vendor/serverd/src/lib/push-install.js +390 -0
  227. package/vendor/serverd/src/lib/revocation.js +112 -0
  228. package/vendor/serverd/src/lib/services.js +131 -0
  229. package/vendor/serverd/src/lib/session.js +151 -0
  230. package/vendor/serverd/src/lib/state-db.js +130 -0
  231. package/vendor/serverd/src/lib/state.js +140 -0
  232. package/vendor/serverd/src/lib/storage.js +63 -0
  233. package/vendor/serverd/src/lib/system-stats.js +58 -0
  234. package/vendor/serverd/src/lib/tickets.js +1556 -0
  235. package/vendor/serverd/src/lib/totp.js +76 -0
  236. package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
  237. package/vendor/serverd/src/lib/user-access-session.js +142 -0
  238. package/vendor/serverd/src/lib/user-access.js +52 -0
  239. package/vendor/serverd/src/middleware/errors.js +63 -0
  240. package/vendor/serverd/src/middleware/mtls.js +120 -0
  241. package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
  242. package/vendor/serverd/src/middleware/role-guard.js +58 -0
  243. package/vendor/serverd/src/middleware/twofa-session.js +65 -0
  244. package/vendor/serverd/src/middleware/user-access-session.js +52 -0
  245. package/vendor/serverd/src/routes/enrollment.js +120 -0
  246. package/vendor/serverd/src/routes/health.js +22 -0
  247. package/vendor/serverd/src/routes/invite.js +163 -0
  248. package/vendor/serverd/src/routes/management/agents.js +182 -0
  249. package/vendor/serverd/src/routes/management/certs.js +911 -0
  250. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
  251. package/vendor/serverd/src/routes/management/identity.js +107 -0
  252. package/vendor/serverd/src/routes/management/invitations.js +166 -0
  253. package/vendor/serverd/src/routes/management/logs.js +218 -0
  254. package/vendor/serverd/src/routes/management/plugins.js +560 -0
  255. package/vendor/serverd/src/routes/management/services.js +63 -0
  256. package/vendor/serverd/src/routes/management/settings.js +325 -0
  257. package/vendor/serverd/src/routes/management/sites.js +522 -0
  258. package/vendor/serverd/src/routes/management/storage.js +192 -0
  259. package/vendor/serverd/src/routes/management/system.js +201 -0
  260. package/vendor/serverd/src/routes/management/tickets.js +457 -0
  261. package/vendor/serverd/src/routes/management/tunnels.js +709 -0
  262. package/vendor/serverd/src/routes/management/users.js +377 -0
  263. package/vendor/serverd/src/routes/management.js +36 -0
  264. package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
  265. package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
  266. package/vendor/serverd/src/routes/onboarding/index.js +18 -0
  267. package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
  268. package/vendor/serverd/src/routes/onboarding/status.js +12 -0
  269. package/vendor/serverd/src/routes/plugin-router.js +79 -0
  270. package/vendor/serverd/src/routes/user-access.js +389 -0
@@ -0,0 +1,1141 @@
1
+ # Certificates API
2
+
3
+ > List, renew, rotate, and download the TLS and mTLS certificates that secure Lamaste.
4
+
5
+ ## In Plain English
6
+
7
+ Lamaste manages two kinds of certificates:
8
+
9
+ 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.
10
+
11
+ 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.
12
+
13
+ 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.
14
+
15
+ ## Authentication
16
+
17
+ 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](./overview.md) for details.
18
+
19
+ If onboarding is not complete, all endpoints return `503 Service Unavailable`.
20
+
21
+ ## Endpoints
22
+
23
+ ### `GET /api/certs`
24
+
25
+ Returns all certificates from both sources (Let's Encrypt and mTLS), sorted by days until expiry (soonest first).
26
+
27
+ **Request:**
28
+
29
+ No request body.
30
+
31
+ ```bash
32
+ curl -s --cert client.p12:password \
33
+ https://203.0.113.42:9292/api/certs | jq
34
+ ```
35
+
36
+ **Response (200):**
37
+
38
+ ```json
39
+ {
40
+ "certs": [
41
+ {
42
+ "type": "mtls-ca",
43
+ "domain": null,
44
+ "expiresAt": "2036-03-13T00:00:00.000Z",
45
+ "daysUntilExpiry": 3650,
46
+ "path": "/etc/lamalibre/lamaste/pki/ca.crt",
47
+ "expiringSoon": false
48
+ },
49
+ {
50
+ "type": "mtls-client",
51
+ "domain": null,
52
+ "expiresAt": "2028-03-13T00:00:00.000Z",
53
+ "daysUntilExpiry": 730,
54
+ "path": "/etc/lamalibre/lamaste/pki/client.crt",
55
+ "expiringSoon": false
56
+ },
57
+ {
58
+ "type": "letsencrypt",
59
+ "domain": "panel.example.com",
60
+ "expiresAt": "2026-06-11T00:00:00.000Z",
61
+ "daysUntilExpiry": 90,
62
+ "path": "/etc/letsencrypt/live/panel.example.com/fullchain.pem",
63
+ "expiringSoon": false
64
+ },
65
+ {
66
+ "type": "letsencrypt",
67
+ "domain": "app.example.com",
68
+ "expiresAt": "2026-04-12T00:00:00.000Z",
69
+ "daysUntilExpiry": 30,
70
+ "path": "/etc/letsencrypt/live/app.example.com/fullchain.pem",
71
+ "expiringSoon": true
72
+ }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ | Field | Type | Description |
78
+ | ----------------- | ---------------- | ---------------------------------------------------------- |
79
+ | `type` | `string` | `"letsencrypt"`, `"mtls-ca"`, or `"mtls-client"` |
80
+ | `domain` | `string \| null` | Domain name for Let's Encrypt certs, `null` for mTLS certs |
81
+ | `expiresAt` | `string` | ISO 8601 expiry date |
82
+ | `daysUntilExpiry` | `number` | Days remaining until expiry |
83
+ | `path` | `string` | Filesystem path to the certificate |
84
+ | `expiringSoon` | `boolean` | `true` if fewer than 30 days remain |
85
+
86
+ 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.
87
+
88
+ ---
89
+
90
+ ### `GET /api/certs/auto-renew-status`
91
+
92
+ 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).
93
+
94
+ **Request:**
95
+
96
+ No request body.
97
+
98
+ ```bash
99
+ curl -s --cert client.p12:password \
100
+ https://203.0.113.42:9292/api/certs/auto-renew-status | jq
101
+ ```
102
+
103
+ **Response (200) — timer active:**
104
+
105
+ ```json
106
+ {
107
+ "active": true,
108
+ "nextRun": "2026-03-14T03:00:00.000Z",
109
+ "lastRun": "2026-03-13T03:00:00.000Z"
110
+ }
111
+ ```
112
+
113
+ **Response (200) — timer inactive:**
114
+
115
+ ```json
116
+ {
117
+ "active": false,
118
+ "nextRun": null,
119
+ "lastRun": null
120
+ }
121
+ ```
122
+
123
+ | Field | Type | Description |
124
+ | --------- | ---------------- | --------------------------------------------------- |
125
+ | `active` | `boolean` | Whether the certbot renewal timer is running |
126
+ | `nextRun` | `string \| null` | ISO 8601 timestamp of next scheduled run, or `null` |
127
+ | `lastRun` | `string \| null` | ISO 8601 timestamp of last run, or `null` |
128
+
129
+ This endpoint always returns 200. If the timer status cannot be determined, it returns `active: false` with null timestamps.
130
+
131
+ ---
132
+
133
+ ### `POST /api/certs/:domain/renew`
134
+
135
+ 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.
136
+
137
+ **Request:**
138
+
139
+ No request body. The domain is specified in the URL path.
140
+
141
+ **URL parameter validation:**
142
+
143
+ 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.
144
+
145
+ ```bash
146
+ curl -s --cert client.p12:password \
147
+ -X POST \
148
+ https://203.0.113.42:9292/api/certs/app.example.com/renew | jq
149
+ ```
150
+
151
+ **Response (200):**
152
+
153
+ ```json
154
+ {
155
+ "ok": true,
156
+ "domain": "app.example.com",
157
+ "newExpiry": "2026-06-11T00:00:00.000Z"
158
+ }
159
+ ```
160
+
161
+ **Response (200) — with warning:**
162
+
163
+ ```json
164
+ {
165
+ "ok": true,
166
+ "domain": "app.example.com",
167
+ "newExpiry": "2026-06-11T00:00:00.000Z",
168
+ "warning": "Certificate renewed but nginx reload failed"
169
+ }
170
+ ```
171
+
172
+ | Field | Type | Description |
173
+ | ----------- | ---------------- | ------------------------------------------------------------------------------ |
174
+ | `ok` | `boolean` | `true` if renewal succeeded |
175
+ | `domain` | `string` | The renewed domain |
176
+ | `newExpiry` | `string \| null` | ISO 8601 expiry date of the new certificate, or `null` if it could not be read |
177
+ | `warning` | `string` | Present if nginx reload or config test failed after renewal |
178
+
179
+ **Errors:**
180
+
181
+ | Status | Body | When |
182
+ | ------ | ---------------------------------------------------------- | ----------------------------------------- |
183
+ | 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | Invalid domain format |
184
+ | 404 | `{"error":"Certificate not found"}` | certbot has no certificate with this name |
185
+ | 500 | `{"error":"Certificate renewal failed"}` | certbot command failed |
186
+
187
+ ---
188
+
189
+ ### `POST /api/certs/mtls/rotate`
190
+
191
+ 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](../02-guides/certificate-management.md#5-rotate-the-mtls-client-certificate)). The response shape below is what the route returns if a rotation succeeds.
192
+
193
+ **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.
194
+
195
+ **Request:**
196
+
197
+ No request body.
198
+
199
+ ```bash
200
+ curl -s --cert client.p12:password \
201
+ -X POST \
202
+ https://203.0.113.42:9292/api/certs/mtls/rotate | jq
203
+ ```
204
+
205
+ **Response (200):**
206
+
207
+ ```json
208
+ {
209
+ "ok": true,
210
+ "p12Password": "a1b2c3d4e5f6...",
211
+ "expiresAt": "2028-03-16T00:00:00.000Z",
212
+ "warning": "Your current browser certificate is now invalid. Download and import the new certificate before closing this page."
213
+ }
214
+ ```
215
+
216
+ | Field | Type | Description |
217
+ | ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
218
+ | `ok` | `boolean` | `true` if rotation succeeded |
219
+ | `p12Password` | `string` | Random hex password for the new `.p12` bundle. Only shown once |
220
+ | `expiresAt` | `string` | ISO 8601 expiry date of the new certificate (2-year validity) |
221
+ | `warning` | `string` | Always present. Notes that the old certificate is now invalid. If nginx reload or config test also fails, the warning is appended with details |
222
+
223
+ **Errors:**
224
+
225
+ | Status | Body | When |
226
+ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
227
+ | 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` |
228
+ | 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` |
229
+ | 500 | `{"error":"mTLS rotation failed: ..."}` | Certificate generation or file operations failed |
230
+ | 500 | `{"error":"CA key not found — cannot sign new certificate"}` | CA key missing from PKI directory |
231
+
232
+ ---
233
+
234
+ ### `GET /api/certs/mtls/download`
235
+
236
+ Downloads the current mTLS client certificate as a PKCS#12 (`.p12`) file. This is the file you import into your browser or keychain.
237
+
238
+ **Request:**
239
+
240
+ No request body.
241
+
242
+ ```bash
243
+ curl -s --cert client.p12:password \
244
+ -o client.p12 \
245
+ https://203.0.113.42:9292/api/certs/mtls/download
246
+ ```
247
+
248
+ **Response (200):**
249
+
250
+ Returns `Content-Type: application/x-pkcs12` with `Content-Disposition: attachment; filename="client.p12"`.
251
+
252
+ The response body is the raw binary PKCS#12 file.
253
+
254
+ **Errors:**
255
+
256
+ | Status | Body | When |
257
+ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
258
+ | 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` |
259
+ | 404 | `{"error":"No client certificate found"}` | The `.p12` file does not exist at the expected path |
260
+ | 500 | `{"error":"Failed to download certificate"}` | File read failed |
261
+
262
+ ---
263
+
264
+ ## Agent Certificate Endpoints
265
+
266
+ Agent certificates provide scoped access for tunnel clients. There are two types:
267
+
268
+ - **Regular agents** (`CN=agent:<label>`) — created by the admin, default `tunnels:read` capability
269
+ - **Plugin-agents** (`CN=plugin-agent:<delegatingLabel>:<pluginAgentLabel>`) — created via delegated enrollment, no default capabilities
270
+
271
+ Only admins can manage agent certificates directly. Agents can create delegated enrollment tokens for plugin-agents via `POST /api/certs/agent/enroll-delegated`.
272
+
273
+ ### `POST /api/certs/agent`
274
+
275
+ Generates a new agent-scoped certificate with the specified label, capabilities, and optional site access list. The certificate CN is set to `agent:<label>`.
276
+
277
+ **Request:**
278
+
279
+ ```json
280
+ {
281
+ "label": "macbook-pro",
282
+ "capabilities": ["tunnels:read", "tunnels:write", "sites:read", "sites:write"],
283
+ "allowedSites": ["blog", "docs"]
284
+ }
285
+ ```
286
+
287
+ | Field | Type | Required | Description |
288
+ | -------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
289
+ | `label` | `string` | Yes | 1-50 characters, lowercase letters, numbers, and hyphens only |
290
+ | `capabilities` | `string[]` | No | List of capabilities. Defaults to `["tunnels:read"]`. Must always include `tunnels:read` |
291
+ | `allowedSites` | `string[]` | No | List 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 |
292
+
293
+ **Valid capabilities:** `tunnels:read`, `tunnels:write`, `services:read`, `services:write`, `system:read`, `sites:read`, `sites:write`, `panel:expose`, `identity:read`, `identity:query`
294
+
295
+ ```bash
296
+ curl -s --cert client.p12:password \
297
+ -X POST -H 'Content-Type: application/json' \
298
+ -d '{"label":"macbook-pro","capabilities":["tunnels:read","sites:read","sites:write"],"allowedSites":["blog"]}' \
299
+ https://203.0.113.42:9292/api/certs/agent | jq
300
+ ```
301
+
302
+ **Response (200):**
303
+
304
+ ```json
305
+ {
306
+ "ok": true,
307
+ "label": "macbook-pro",
308
+ "p12Password": "a1b2c3d4e5f6...",
309
+ "serial": "ABC123DEF456...",
310
+ "expiresAt": "2028-03-16T00:00:00.000Z"
311
+ }
312
+ ```
313
+
314
+ | Field | Type | Description |
315
+ | ------------- | --------- | ---------------------------------------------------------- |
316
+ | `ok` | `boolean` | `true` if generation succeeded |
317
+ | `label` | `string` | The agent label |
318
+ | `p12Password` | `string` | Random hex password for the `.p12` bundle. Only shown once |
319
+ | `serial` | `string` | Certificate serial number |
320
+ | `expiresAt` | `string` | ISO 8601 expiry date (2-year validity) |
321
+
322
+ **Important:** The `p12Password` is only returned at generation time. It cannot be retrieved later.
323
+
324
+ **Errors:**
325
+
326
+ | Status | Body | When |
327
+ | ------ | ------------------------------------------------------------------------- | ---------------------------------------------- |
328
+ | 400 | `{"error":"Validation failed","details":{...}}` | Invalid label format or missing `tunnels:read` |
329
+ | 409 | `{"error":"Agent certificate with label \"macbook-pro\" already exists"}` | Label already in use (non-revoked) |
330
+ | 500 | `{"error":"Agent certificate generation failed: ..."}` | OpenSSL or file operation failed |
331
+
332
+ ---
333
+
334
+ ### `GET /api/certs/agent`
335
+
336
+ Lists all agent certificates with their status, capabilities, and expiry information.
337
+
338
+ **Request:**
339
+
340
+ No request body.
341
+
342
+ ```bash
343
+ curl -s --cert client.p12:password \
344
+ https://203.0.113.42:9292/api/certs/agent | jq
345
+ ```
346
+
347
+ **Response (200):**
348
+
349
+ ```json
350
+ {
351
+ "agents": [
352
+ {
353
+ "label": "macbook-pro",
354
+ "serial": "ABC123DEF456...",
355
+ "capabilities": ["tunnels:read", "tunnels:write", "sites:read", "sites:write"],
356
+ "allowedSites": ["blog", "docs"],
357
+ "enrollmentMethod": "hardware-bound",
358
+ "createdAt": "2026-03-16T10:00:00.000Z",
359
+ "expiresAt": "2028-03-16T10:00:00.000Z",
360
+ "revoked": false,
361
+ "expiringSoon": false
362
+ },
363
+ {
364
+ "label": "office-imac",
365
+ "serial": "789GHI012...",
366
+ "capabilities": ["tunnels:read"],
367
+ "allowedSites": [],
368
+ "enrollmentMethod": "p12",
369
+ "createdAt": "2026-03-10T08:00:00.000Z",
370
+ "expiresAt": "2028-03-10T08:00:00.000Z",
371
+ "revoked": true,
372
+ "expiringSoon": false
373
+ }
374
+ ]
375
+ }
376
+ ```
377
+
378
+ | Field | Type | Description |
379
+ | ------------------ | --------------------- | ------------------------------------------------------------------------------------ |
380
+ | `label` | `string` | Agent identifier (matches certificate CN `agent:<label>`) |
381
+ | `serial` | `string` | Certificate serial number |
382
+ | `capabilities` | `string[]` | Granted capabilities |
383
+ | `allowedSites` | `string[]` | Site names this agent can access for file operations |
384
+ | `enrollmentMethod` | `string` | `"p12"`, `"hardware-bound"`, or `"delegated"` — how the agent certificate was issued |
385
+ | `delegatedBy` | `string \| undefined` | For delegated certs: the label of the delegating agent |
386
+ | `certType` | `string \| undefined` | `"plugin-agent"` for delegated certs, absent for regular agents |
387
+ | `createdAt` | `string` | ISO 8601 creation timestamp |
388
+ | `expiresAt` | `string` | ISO 8601 expiry timestamp |
389
+ | `revoked` | `boolean` | Whether the certificate has been revoked |
390
+ | `expiringSoon` | `boolean` | `true` if the certificate is not revoked and fewer than 30 days remain until expiry |
391
+
392
+ ---
393
+
394
+ ### `GET /api/certs/agent/:label/download`
395
+
396
+ Downloads an agent certificate as a PKCS#12 (`.p12`) file.
397
+
398
+ **Request:**
399
+
400
+ No request body. The label is specified in the URL path.
401
+
402
+ ```bash
403
+ curl -s --cert client.p12:password \
404
+ -o macbook-pro.p12 \
405
+ https://203.0.113.42:9292/api/certs/agent/macbook-pro/download
406
+ ```
407
+
408
+ **Response (200):**
409
+
410
+ Returns `Content-Type: application/x-pkcs12` with `Content-Disposition: attachment; filename="macbook-pro.p12"`.
411
+
412
+ **Errors:**
413
+
414
+ | Status | Body | When |
415
+ | ------ | --------------------------------------------------------- | ------------------------------------------- |
416
+ | 400 | `{"error":"Validation failed",...}` | Invalid label format |
417
+ | 404 | `{"error":"Agent certificate \"macbook-pro\" not found"}` | No P12 file on disk (may have been revoked) |
418
+
419
+ ---
420
+
421
+ ### `PATCH /api/certs/agent/:label/capabilities`
422
+
423
+ 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.
424
+
425
+ **Request:**
426
+
427
+ ```json
428
+ {
429
+ "capabilities": ["tunnels:read", "tunnels:write", "system:read"]
430
+ }
431
+ ```
432
+
433
+ | Field | Type | Required | Description |
434
+ | -------------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
435
+ | `capabilities` | `string[]` | Yes | New capability list. Must include `tunnels:read` for regular agents. Plugin-agents have no mandatory capabilities |
436
+
437
+ ```bash
438
+ curl -s --cert client.p12:password \
439
+ -X PATCH -H 'Content-Type: application/json' \
440
+ -d '{"capabilities":["tunnels:read","tunnels:write","system:read"]}' \
441
+ https://203.0.113.42:9292/api/certs/agent/macbook-pro/capabilities | jq
442
+ ```
443
+
444
+ **Response (200):**
445
+
446
+ ```json
447
+ {
448
+ "ok": true,
449
+ "label": "macbook-pro",
450
+ "capabilities": ["tunnels:read", "tunnels:write", "system:read"]
451
+ }
452
+ ```
453
+
454
+ **Errors:**
455
+
456
+ | Status | Body | When |
457
+ | ------ | --------------------------------------------------------- | -------------------------------------------- |
458
+ | 400 | `{"error":"Validation failed",...}` | Invalid capability or missing `tunnels:read` |
459
+ | 404 | `{"error":"Agent certificate \"macbook-pro\" not found"}` | No active agent with this label |
460
+
461
+ ---
462
+
463
+ ### `PATCH /api/certs/agent/:label/allowed-sites`
464
+
465
+ 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.
466
+
467
+ **Request:**
468
+
469
+ ```json
470
+ {
471
+ "allowedSites": ["blog", "docs", "landing-page"]
472
+ }
473
+ ```
474
+
475
+ | Field | Type | Required | Description |
476
+ | -------------- | ---------- | -------- | -------------------------------------------------------------------------------- |
477
+ | `allowedSites` | `string[]` | Yes | New list of site names this agent can access. Use `[]` to remove all site access |
478
+
479
+ ```bash
480
+ curl -s --cert client.p12:password \
481
+ -X PATCH -H 'Content-Type: application/json' \
482
+ -d '{"allowedSites":["blog","docs"]}' \
483
+ https://203.0.113.42:9292/api/certs/agent/macbook-pro/allowed-sites | jq
484
+ ```
485
+
486
+ **Response (200):**
487
+
488
+ ```json
489
+ {
490
+ "ok": true,
491
+ "label": "macbook-pro",
492
+ "allowedSites": ["blog", "docs"]
493
+ }
494
+ ```
495
+
496
+ **Errors:**
497
+
498
+ | Status | Body | When |
499
+ | ------ | --------------------------------------------------------- | ------------------------------- |
500
+ | 400 | `{"error":"Validation failed",...}` | Invalid request body |
501
+ | 404 | `{"error":"Agent certificate \"macbook-pro\" not found"}` | No active agent with this label |
502
+
503
+ ---
504
+
505
+ ### `DELETE /api/certs/agent/:label`
506
+
507
+ 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.
508
+
509
+ **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.
510
+
511
+ **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.
512
+
513
+ **Request:**
514
+
515
+ No request body.
516
+
517
+ ```bash
518
+ curl -s --cert client.p12:password \
519
+ -X DELETE \
520
+ https://203.0.113.42:9292/api/certs/agent/macbook-pro | jq
521
+ ```
522
+
523
+ **Response (200):**
524
+
525
+ ```json
526
+ {
527
+ "ok": true,
528
+ "label": "macbook-pro"
529
+ }
530
+ ```
531
+
532
+ **Errors:**
533
+
534
+ | Status | Body | When |
535
+ | ------ | --------------------------------------------------------- | -------------------------------------------- |
536
+ | 400 | `{"error":"Validation failed",...}` | Invalid label format |
537
+ | 404 | `{"error":"Agent certificate \"macbook-pro\" not found"}` | No agent with this label, or already revoked |
538
+
539
+ ---
540
+
541
+ ## Chisel Credential Endpoints
542
+
543
+ 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.
544
+
545
+ ### `GET /api/agents/me/chisel-credential` (agent-only)
546
+
547
+ 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.
548
+
549
+ **Required capability:** `tunnels:read`
550
+
551
+ **Response (200):**
552
+
553
+ ```json
554
+ {
555
+ "user": "agent-macbook-pro",
556
+ "password": "<64 hex characters>",
557
+ "createdAt": "2026-09-20T10:12:00.000Z"
558
+ }
559
+ ```
560
+
561
+ `createdAt` is when this password was issued. It matches `chiselCredentialIssuedAt` in [`GET /api/tunnels/agent-config`](tunnels.md#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.
562
+
563
+ **Errors:**
564
+
565
+ | Status | Body | When |
566
+ | ------ | ------------------------------------------------------------------------- | ------------------------------------------ |
567
+ | 400 | `{"error":"Agent label could not be determined from client certificate"}` | Certificate has no agent label |
568
+ | 404 | `{"error":"No chisel credential found for this agent","hint":"..."}` | No credential; an admin must rotate one in |
569
+
570
+ ### `POST /api/agents/me/chisel-credential/rotate` (agent-only)
571
+
572
+ 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.
573
+
574
+ 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.
575
+
576
+ **Required capability:** `tunnels:read`
577
+
578
+ **Errors:**
579
+
580
+ | Status | Body | When |
581
+ | ------ | ---------------------------------------------------------------------------------------- | ------------------------------------------ |
582
+ | 429 | `{"error":"The chisel credential was issued less than 10 minutes ago","issuedAt":"..."}` | The current credential is under 10 min old |
583
+
584
+ ### `POST /api/agents/:label/chisel-credential/rotate` (admin-only)
585
+
586
+ 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).
587
+
588
+ | Status | When |
589
+ | ------ | ------------------------------- |
590
+ | 400 | The label is a plugin-agent |
591
+ | 404 | No active agent with this label |
592
+
593
+ ---
594
+
595
+ ## Hardware-Bound Certificate Endpoints
596
+
597
+ 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.
598
+
599
+ ### `POST /api/certs/agent/enroll` — Generate enrollment token (admin-only)
600
+
601
+ 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`.
602
+
603
+ **Request:**
604
+
605
+ ```json
606
+ {
607
+ "label": "macbook-pro",
608
+ "capabilities": ["tunnels:read", "tunnels:write", "sites:read"],
609
+ "allowedSites": ["blog"]
610
+ }
611
+ ```
612
+
613
+ | Field | Type | Required | Description |
614
+ | -------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
615
+ | `label` | `string` | Yes | 1-50 characters, lowercase letters, numbers, and hyphens only |
616
+ | `capabilities` | `string[]` | No | List of capabilities. Defaults to `["tunnels:read"]`. Must always include `tunnels:read` |
617
+ | `allowedSites` | `string[]` | No | List 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 |
618
+
619
+ ```bash
620
+ curl -s --cert client.p12:password \
621
+ -X POST -H 'Content-Type: application/json' \
622
+ -d '{"label":"macbook-pro","capabilities":["tunnels:read","tunnels:write"],"allowedSites":[]}' \
623
+ https://203.0.113.42:9292/api/certs/agent/enroll | jq
624
+ ```
625
+
626
+ **Response (200):**
627
+
628
+ ```json
629
+ {
630
+ "ok": true,
631
+ "token": "a1b2c3d4e5f6...",
632
+ "label": "macbook-pro",
633
+ "expiresAt": "2026-03-23T10:10:00.000Z"
634
+ }
635
+ ```
636
+
637
+ | Field | Type | Description |
638
+ | ----------- | --------- | -------------------------------------------------- |
639
+ | `ok` | `boolean` | `true` if the token was generated successfully |
640
+ | `token` | `string` | Single-use enrollment token. Only shown once |
641
+ | `label` | `string` | The agent label the token is associated with |
642
+ | `expiresAt` | `string` | ISO 8601 timestamp when the token expires (10 min) |
643
+
644
+ **Errors:**
645
+
646
+ | Status | Body | When |
647
+ | ------ | ------------------------------------------------------------------------- | ---------------------------------------------- |
648
+ | 400 | `{"error":"Validation failed","details":{...}}` | Invalid label format or missing `tunnels:read` |
649
+ | 409 | `{"error":"Agent certificate with label \"macbook-pro\" already exists"}` | Label already in use (non-revoked) |
650
+
651
+ **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.
652
+
653
+ ---
654
+
655
+ ### `POST /api/certs/agent/enroll-delegated` — Delegated enrollment token (agent-only)
656
+
657
+ 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).
658
+
659
+ 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).
660
+
661
+ **Authentication:** Agent-only (mTLS with agent certificate). Plugin-agents are rejected with 403.
662
+
663
+ **Request:**
664
+
665
+ ```json
666
+ {
667
+ "pluginAgentLabel": "rpi-sync",
668
+ "scope": "plugin:sync:connect"
669
+ }
670
+ ```
671
+
672
+ | Field | Type | Required | Description |
673
+ | ------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
674
+ | `pluginAgentLabel` | `string` | Yes | 1-50 characters, must start with a letter or number, lowercase letters, numbers, and hyphens only |
675
+ | `scope` | `string` | Yes | Ticket sub-scope in `plugin:<name>:<action>` format (e.g., `plugin:sync:connect`). Must be a registered ticket scope, not a base capability |
676
+
677
+ ```bash
678
+ curl -s --cert agent.p12:password \
679
+ -X POST -H 'Content-Type: application/json' \
680
+ -d '{"pluginAgentLabel":"rpi-sync","scope":"plugin:sync:connect"}' \
681
+ https://203.0.113.42:9292/api/certs/agent/enroll-delegated | jq
682
+ ```
683
+
684
+ **Response (200):**
685
+
686
+ ```json
687
+ {
688
+ "ok": true,
689
+ "enrollmentToken": "a1b2c3d4e5f6...",
690
+ "expiresAt": "2026-04-04T10:10:00.000Z",
691
+ "pluginAgentLabel": "rpi-sync"
692
+ }
693
+ ```
694
+
695
+ | Field | Type | Description |
696
+ | ------------------ | --------- | ------------------------------------------------------ |
697
+ | `ok` | `boolean` | `true` if the token was generated successfully |
698
+ | `enrollmentToken` | `string` | Single-use delegated enrollment token. Only shown once |
699
+ | `expiresAt` | `string` | ISO 8601 timestamp when the token expires (10 min) |
700
+ | `pluginAgentLabel` | `string` | The plugin agent label |
701
+
702
+ **Errors:**
703
+
704
+ | Status | Body | When |
705
+ | ------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
706
+ | 400 | `{"error":"Validation failed","details":{...}}` | Invalid label or scope format |
707
+ | 403 | `{"error":"Plugin agents cannot delegate enrollment"}` | Caller is a plugin-agent (recursive delegation blocked) |
708
+ | 403 | `{"error":"Agent label not found in certificate"}` | Calling certificate has no agent label |
709
+ | 403 | `{"error":"Insufficient delegation authority"}` | Agent does not own an active ticket instance for the scope |
710
+ | 409 | `{"error":"Agent certificate with label \"plugin-agent:macbook-pro:rpi-sync\" already exists"}` | Plugin-agent label already has an active (non-revoked) cert |
711
+ | 500 | `{"error":"Delegated enrollment token generation failed"}` | Token generation error |
712
+
713
+ **How it works:**
714
+
715
+ 1. The panel verifies the calling agent is a regular agent (not a plugin-agent)
716
+ 2. Checks that the scope is a registered ticket scope (not a base capability like `tunnels:read`)
717
+ 3. Verifies the agent owns an active ticket instance for that scope
718
+ 4. Generates a delegated enrollment token with `type: 'delegated'`, `delegatedBy: <callingAgentLabel>`, and `scope`
719
+ 5. The plugin agent uses this token with `POST /api/enroll` to receive a `CN=plugin-agent:<delegatingLabel>:<pluginAgentLabel>` certificate
720
+
721
+ **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.
722
+
723
+ ---
724
+
725
+ ### `DELETE /api/certs/agent/enroll/:label` — Revoke unused enrollment token (admin-only)
726
+
727
+ 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.
728
+
729
+ **Request:**
730
+
731
+ No request body. The label is specified in the URL path.
732
+
733
+ ```bash
734
+ curl -s --cert client.p12:password \
735
+ -X DELETE \
736
+ https://203.0.113.42:9292/api/certs/agent/enroll/macbook-pro | jq
737
+ ```
738
+
739
+ **Response (200):**
740
+
741
+ ```json
742
+ {
743
+ "ok": true,
744
+ "revoked": true
745
+ }
746
+ ```
747
+
748
+ | Field | Type | Description |
749
+ | --------- | --------- | -------------------------------------------------------------------------------------------- |
750
+ | `ok` | `boolean` | Always `true` |
751
+ | `revoked` | `boolean` | `true` if an active token was found and removed, `false` if no matching active token existed |
752
+
753
+ **Errors:**
754
+
755
+ | Status | Body | When |
756
+ | ------ | ----------------------------------- | -------------------- |
757
+ | 400 | `{"error":"Validation failed",...}` | Invalid label format |
758
+
759
+ ---
760
+
761
+ ### `POST /api/enroll` — Enroll agent with token (PUBLIC, no mTLS)
762
+
763
+ 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/*`.
764
+
765
+ **Request:**
766
+
767
+ ```json
768
+ {
769
+ "token": "a1b2c3d4e5f6...",
770
+ "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
771
+ }
772
+ ```
773
+
774
+ | Field | Type | Required | Description |
775
+ | ------- | -------- | -------- | -------------------------------------------------------- |
776
+ | `token` | `string` | Yes | The enrollment token from `POST /api/certs/agent/enroll` |
777
+ | `csr` | `string` | Yes | PEM-encoded PKCS#10 Certificate Signing Request |
778
+
779
+ ```bash
780
+ curl -s -k \
781
+ -X POST -H 'Content-Type: application/json' \
782
+ -d '{"token":"a1b2c3d4e5f6...","csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
783
+ https://203.0.113.42:9292/api/enroll | jq
784
+ ```
785
+
786
+ **Response (200):**
787
+
788
+ ```json
789
+ {
790
+ "ok": true,
791
+ "cert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
792
+ "caCert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
793
+ "label": "macbook-pro",
794
+ "serial": "ABC123DEF456...",
795
+ "expiresAt": "2028-03-23T00:00:00.000Z"
796
+ }
797
+ ```
798
+
799
+ | Field | Type | Description |
800
+ | ----------- | --------- | ----------------------------------------------- |
801
+ | `ok` | `boolean` | `true` if enrollment succeeded |
802
+ | `cert` | `string` | PEM-encoded signed client certificate |
803
+ | `caCert` | `string` | PEM-encoded CA certificate for TLS verification |
804
+ | `label` | `string` | The agent label |
805
+ | `serial` | `string` | Certificate serial number |
806
+ | `expiresAt` | `string` | ISO 8601 expiry date (2-year validity) |
807
+
808
+ **Important:** The token is consumed upon successful enrollment and cannot be reused. The agent stores the returned certificate alongside its locally generated private key.
809
+
810
+ **Errors:**
811
+
812
+ | Status | Body | When |
813
+ | ------ | ------------------------------------------------------------------------- | -------------------------------------------------------- |
814
+ | 400 | `{"error":"Validation failed","details":{...}}` | Missing or malformed `token` or `csr` field |
815
+ | 400 | `{"error":"Validation failed","details":{...}}` | CSR field missing PEM header or other validation failure |
816
+ | 400 | `{"error":"CSR too large"}` | CSR exceeds 8192 bytes |
817
+ | 400 | `{"error":"Invalid CSR: structure or signature verification failed"}` | CSR fails OpenSSL validation |
818
+ | 401 | `{"error":"Invalid enrollment token"}` | Token not found |
819
+ | 401 | `{"error":"Enrollment token has already been used"}` | Token already consumed |
820
+ | 401 | `{"error":"Enrollment token has expired"}` | Token past expiry |
821
+ | 500 | `{"error":"Enrollment failed"}` | Server-side signing error (details hidden) |
822
+ | 409 | `{"error":"Agent certificate with label \"macbook-pro\" already exists"}` | Label already has an active (non-revoked) certificate |
823
+
824
+ ---
825
+
826
+ ### `POST /api/certs/admin/upgrade-to-hardware-bound` — Upgrade admin to hardware-bound (admin-only)
827
+
828
+ 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`.
829
+
830
+ **Request:**
831
+
832
+ ```json
833
+ {
834
+ "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
835
+ }
836
+ ```
837
+
838
+ | Field | Type | Required | Description |
839
+ | ----- | -------- | -------- | ----------------------------------------------- |
840
+ | `csr` | `string` | Yes | PEM-encoded PKCS#10 Certificate Signing Request |
841
+
842
+ ```bash
843
+ curl -s --cert client.p12:password \
844
+ -X POST -H 'Content-Type: application/json' \
845
+ -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
846
+ https://203.0.113.42:9292/api/certs/admin/upgrade-to-hardware-bound | jq
847
+ ```
848
+
849
+ **Response (200):**
850
+
851
+ ```json
852
+ {
853
+ "ok": true,
854
+ "cert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
855
+ "caCert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
856
+ "serial": "DEF789GHI012...",
857
+ "expiresAt": "2028-03-23T00:00:00.000Z"
858
+ }
859
+ ```
860
+
861
+ | Field | Type | Description |
862
+ | ----------- | --------- | ------------------------------------------------ |
863
+ | `ok` | `boolean` | `true` if the upgrade succeeded |
864
+ | `cert` | `string` | PEM-encoded signed admin certificate |
865
+ | `caCert` | `string` | PEM-encoded CA certificate for TLS verification |
866
+ | `serial` | `string` | Certificate serial number of the new certificate |
867
+ | `expiresAt` | `string` | ISO 8601 expiry date (2-year validity) |
868
+
869
+ **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.
870
+
871
+ **Errors:**
872
+
873
+ | Status | Body | When |
874
+ | ------ | ------------------------------------------------------------------------------- | ------------------------------------------- |
875
+ | 400 | `{"error":"Validation failed","details":{...}}` | Missing or malformed `csr` field |
876
+ | 400 | `{"error":"CSR too large"}` | CSR exceeds 8192 bytes |
877
+ | 400 | `{"error":"Invalid CSR: structure or signature verification failed"}` | CSR fails OpenSSL validation |
878
+ | 409 | `{"error":"Admin is already using hardware-bound authentication"}` | `adminAuthMode` is already `hardware-bound` |
879
+ | 503 | `{"error":"Panel-initiated admin cert issuance is disabled for security. ..."}` | Always: the panel does not sign admin CSRs |
880
+ | 500 | `{"error":"Admin upgrade failed"}` | OpenSSL signing or file operation failed |
881
+
882
+ ---
883
+
884
+ ### `GET /api/certs/admin/auth-mode` — Get admin auth mode (admin-only)
885
+
886
+ 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.
887
+
888
+ **Request:**
889
+
890
+ No request body.
891
+
892
+ ```bash
893
+ curl -s --cert client.p12:password \
894
+ https://203.0.113.42:9292/api/certs/admin/auth-mode | jq
895
+ ```
896
+
897
+ **Response (200):**
898
+
899
+ ```json
900
+ {
901
+ "adminAuthMode": "p12"
902
+ }
903
+ ```
904
+
905
+ | Field | Type | Description |
906
+ | --------------- | -------- | ----------------------------------------------------------- |
907
+ | `adminAuthMode` | `string` | `"p12"` (default) or `"hardware-bound"` (after CSR upgrade) |
908
+
909
+ ---
910
+
911
+ ### `POST /api/certs/agent/upgrade-cert` — Upgrade agent cert to hardware-bound (agent-only)
912
+
913
+ 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.
914
+
915
+ **Authentication:** Agent-only. Agents can only upgrade their own certificate (the label is extracted from the calling certificate's CN).
916
+
917
+ **Request:**
918
+
919
+ ```json
920
+ {
921
+ "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
922
+ }
923
+ ```
924
+
925
+ | Field | Type | Required | Description |
926
+ | ----- | -------- | -------- | ----------------------------------------------- |
927
+ | `csr` | `string` | Yes | PEM-encoded PKCS#10 Certificate Signing Request |
928
+
929
+ ```bash
930
+ curl -s --cert agent.p12:password \
931
+ -X POST -H 'Content-Type: application/json' \
932
+ -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
933
+ https://203.0.113.42:9292/api/certs/agent/upgrade-cert | jq
934
+ ```
935
+
936
+ **Response (200):**
937
+
938
+ ```json
939
+ {
940
+ "ok": true,
941
+ "cert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
942
+ "caCert": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
943
+ "serial": "ABC123DEF456...",
944
+ "expiresAt": "2028-03-23T00:00:00.000Z"
945
+ }
946
+ ```
947
+
948
+ | Field | Type | Description |
949
+ | ----------- | --------- | ------------------------------------------------ |
950
+ | `ok` | `boolean` | `true` if the upgrade succeeded |
951
+ | `cert` | `string` | PEM-encoded signed agent certificate |
952
+ | `caCert` | `string` | PEM-encoded CA certificate for TLS verification |
953
+ | `serial` | `string` | Certificate serial number of the new certificate |
954
+ | `expiresAt` | `string` | ISO 8601 expiry date (2-year validity) |
955
+
956
+ **Errors:**
957
+
958
+ | Status | Body | When |
959
+ | ------ | --------------------------------------------------------------------- | --------------------------------------------- |
960
+ | 400 | `{"error":"Validation failed","details":{...}}` | Missing or malformed `csr` field |
961
+ | 400 | `{"error":"CSR too large"}` | CSR exceeds 8192 bytes |
962
+ | 400 | `{"error":"Invalid CSR: structure or signature verification failed"}` | CSR fails OpenSSL validation |
963
+ | 403 | `{"error":"Agent label not found in certificate"}` | Calling certificate has no agent label |
964
+ | 404 | `{"error":"Agent certificate \"macbook-pro\" not found"}` | No active (non-revoked) agent with this label |
965
+ | 500 | `{"error":"Agent certificate upgrade failed"}` | OpenSSL signing or file operation failed |
966
+
967
+ ---
968
+
969
+ ## Certificate Types
970
+
971
+ ### Let's Encrypt Certificates
972
+
973
+ - **Issued by:** Let's Encrypt CA (publicly trusted)
974
+ - **Purpose:** HTTPS for tunnel subdomains and panel domain
975
+ - **Validity:** 90 days
976
+ - **Renewal:** Automatic via `certbot.timer` (runs twice daily, renews when < 30 days remain)
977
+ - **Path pattern:** `/etc/letsencrypt/live/<domain>/fullchain.pem`
978
+ - **Issued during:** Onboarding provisioning (panel cert) and tunnel creation (per-subdomain certs)
979
+
980
+ ### mTLS Client Certificates
981
+
982
+ - **Issued by:** Lamaste's self-signed CA
983
+ - **Purpose:** Admin authentication to the management panel
984
+ - **Validity:** 2 years
985
+ - **Renewal:** Manual, `sudo lamaste-server reset-admin` on the server
986
+ - **Path:** `/etc/lamalibre/lamaste/pki/client.p12`
987
+ - **Issued during:** Initial installation by `create-lamaste`
988
+
989
+ ### Agent Certificates
990
+
991
+ - **Issued by:** Lamaste's self-signed CA
992
+ - **Purpose:** Scoped access for tunnel clients (`CN=agent:<label>`)
993
+ - **Validity:** 2 years
994
+ - **Capabilities:** Server-side, updatable without reissuing the certificate. Defaults to `["tunnels:read"]`
995
+ - **Site access:** Per-site scoping via `allowedSites` — agents can only see and modify files on sites explicitly assigned to them
996
+ - **Path:** `/etc/lamalibre/lamaste/pki/agents/<label>/client.p12`
997
+ - **Issued via:** `POST /api/certs/agent` (admin only) or `POST /api/certs/agent/enroll` (token-based)
998
+ - **Revocation:** Immediate via `/api/certs/agent/:label` (serial added to revocation list). Cascade-revokes all plugin-agents delegated by this agent
999
+
1000
+ ### Plugin-Agent Certificates
1001
+
1002
+ - **Issued by:** Lamaste's self-signed CA
1003
+ - **Purpose:** Minimal identity for plugin agents participating in the ticket system (`CN=plugin-agent:<delegatingLabel>:<pluginAgentLabel>`)
1004
+ - **Validity:** 2 years
1005
+ - **Capabilities:** No default capabilities — starts empty. Admin can add capabilities via the Certificates page
1006
+ - **Site access:** None (plugin-agents never have allowed sites)
1007
+ - **Path:** `/etc/lamalibre/lamaste/pki/agents/plugin-agent:<delegatingLabel>:<pluginAgentLabel>/client.p12` (generated during CSR enrollment)
1008
+ - **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
1009
+ - **Revocation:** Immediate via `DELETE /api/certs/agent/<full-label>`, or automatically cascade-revoked when the delegating agent is revoked
1010
+
1011
+ ## Quick Reference
1012
+
1013
+ | Method | Path | Description |
1014
+ | ------ | --------------------------------------------- | ------------------------------------------------------- |
1015
+ | GET | `/api/certs` | List all certs (sorted by expiry) |
1016
+ | GET | `/api/certs/auto-renew-status` | Check certbot timer status |
1017
+ | POST | `/api/certs/:domain/renew` | Force-renew a Let's Encrypt cert |
1018
+ | POST | `/api/certs/mtls/rotate` | Rotate mTLS client certificate |
1019
+ | GET | `/api/certs/mtls/download` | Download client.p12 file |
1020
+ | POST | `/api/certs/agent` | Generate agent certificate (P12) |
1021
+ | GET | `/api/certs/agent` | List agent certificates |
1022
+ | GET | `/api/certs/agent/:label/download` | Download agent .p12 file |
1023
+ | PATCH | `/api/certs/agent/:label/capabilities` | Update agent capabilities |
1024
+ | PATCH | `/api/certs/agent/:label/allowed-sites` | Update agent site access |
1025
+ | DELETE | `/api/certs/agent/:label` | Revoke agent certificate (releases its tunnels) |
1026
+ | GET | `/api/agents/me/chisel-credential` | Own Chisel credential (agent-only) |
1027
+ | POST | `/api/agents/me/chisel-credential/rotate` | Rotate own Chisel credential (agent-only, 10-min floor) |
1028
+ | POST | `/api/agents/:label/chisel-credential/rotate` | Rotate an agent's Chisel credential (admin-only) |
1029
+ | POST | `/api/certs/agent/enroll` | Generate enrollment token (admin-only) |
1030
+ | POST | `/api/certs/agent/enroll-delegated` | Delegated enrollment token (agent-only) |
1031
+ | DELETE | `/api/certs/agent/enroll/:label` | Revoke unused enrollment token |
1032
+ | POST | `/api/certs/agent/upgrade-cert` | Upgrade agent cert to hardware-bound |
1033
+ | POST | `/api/enroll` | Enroll agent with token (public) |
1034
+ | POST | `/api/certs/admin/upgrade-to-hardware-bound` | Upgrade admin to hardware-bound |
1035
+ | GET | `/api/certs/admin/auth-mode` | Get admin auth mode |
1036
+
1037
+ ### Certificate Object Shape
1038
+
1039
+ ```json
1040
+ {
1041
+ "type": "letsencrypt",
1042
+ "domain": "app.example.com",
1043
+ "expiresAt": "2026-06-11T00:00:00.000Z",
1044
+ "daysUntilExpiry": 90,
1045
+ "path": "/etc/letsencrypt/live/app.example.com/fullchain.pem",
1046
+ "expiringSoon": false
1047
+ }
1048
+ ```
1049
+
1050
+ ### curl Cheat Sheet
1051
+
1052
+ ```bash
1053
+ # List all certificates
1054
+ curl -s --cert client.p12:password \
1055
+ https://203.0.113.42:9292/api/certs | jq
1056
+
1057
+ # Check auto-renewal timer
1058
+ curl -s --cert client.p12:password \
1059
+ https://203.0.113.42:9292/api/certs/auto-renew-status | jq
1060
+
1061
+ # Force-renew a certificate
1062
+ curl -s --cert client.p12:password \
1063
+ -X POST \
1064
+ https://203.0.113.42:9292/api/certs/app.example.com/renew | jq
1065
+
1066
+ # Rotate mTLS certificate
1067
+ curl -s --cert client.p12:password \
1068
+ -X POST \
1069
+ https://203.0.113.42:9292/api/certs/mtls/rotate | jq
1070
+
1071
+ # Download client certificate
1072
+ curl -s --cert client.p12:password \
1073
+ -o client-new.p12 \
1074
+ https://203.0.113.42:9292/api/certs/mtls/download
1075
+
1076
+ # Generate an agent certificate with site access
1077
+ curl -s --cert client.p12:password \
1078
+ -X POST -H 'Content-Type: application/json' \
1079
+ -d '{"label":"macbook-pro","capabilities":["tunnels:read","sites:read","sites:write"],"allowedSites":["blog"]}' \
1080
+ https://203.0.113.42:9292/api/certs/agent | jq
1081
+
1082
+ # List agent certificates
1083
+ curl -s --cert client.p12:password \
1084
+ https://203.0.113.42:9292/api/certs/agent | jq
1085
+
1086
+ # Update agent capabilities
1087
+ curl -s --cert client.p12:password \
1088
+ -X PATCH -H 'Content-Type: application/json' \
1089
+ -d '{"capabilities":["tunnels:read","services:read"]}' \
1090
+ https://203.0.113.42:9292/api/certs/agent/macbook-pro/capabilities | jq
1091
+
1092
+ # Update agent site access
1093
+ curl -s --cert client.p12:password \
1094
+ -X PATCH -H 'Content-Type: application/json' \
1095
+ -d '{"allowedSites":["blog","docs"]}' \
1096
+ https://203.0.113.42:9292/api/certs/agent/macbook-pro/allowed-sites | jq
1097
+
1098
+ # Revoke an agent certificate
1099
+ curl -s --cert client.p12:password \
1100
+ -X DELETE \
1101
+ https://203.0.113.42:9292/api/certs/agent/macbook-pro | jq
1102
+
1103
+ # Generate an enrollment token for hardware-bound agent enrollment
1104
+ curl -s --cert client.p12:password \
1105
+ -X POST -H 'Content-Type: application/json' \
1106
+ -d '{"label":"macbook-pro","capabilities":["tunnels:read","tunnels:write"]}' \
1107
+ https://203.0.113.42:9292/api/certs/agent/enroll | jq
1108
+
1109
+ # Generate a delegated enrollment token (agent-only)
1110
+ curl -s --cert agent.p12:password \
1111
+ -X POST -H 'Content-Type: application/json' \
1112
+ -d '{"pluginAgentLabel":"rpi-sync","scope":"sync:connect"}' \
1113
+ https://203.0.113.42:9292/api/certs/agent/enroll-delegated | jq
1114
+
1115
+ # Revoke an unused enrollment token
1116
+ curl -s --cert client.p12:password \
1117
+ -X DELETE \
1118
+ https://203.0.113.42:9292/api/certs/agent/enroll/macbook-pro | jq
1119
+
1120
+ # Enroll agent with token and CSR (no mTLS required)
1121
+ curl -s -k \
1122
+ -X POST -H 'Content-Type: application/json' \
1123
+ -d '{"token":"a1b2c3d4e5f6...","csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
1124
+ https://203.0.113.42:9292/api/enroll | jq
1125
+
1126
+ # Upgrade admin certificate to hardware-bound
1127
+ curl -s --cert client.p12:password \
1128
+ -X POST -H 'Content-Type: application/json' \
1129
+ -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
1130
+ https://203.0.113.42:9292/api/certs/admin/upgrade-to-hardware-bound | jq
1131
+
1132
+ # Upgrade agent certificate to hardware-bound (agent-only)
1133
+ curl -s --cert agent.p12:password \
1134
+ -X POST -H 'Content-Type: application/json' \
1135
+ -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----"}' \
1136
+ https://203.0.113.42:9292/api/certs/agent/upgrade-cert | jq
1137
+
1138
+ # Check admin auth mode
1139
+ curl -s --cert client.p12:password \
1140
+ https://203.0.113.42:9292/api/certs/admin/auth-mode | jq
1141
+ ```