@lamalibre/create-lamaste 4.4.1 → 4.5.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.
- package/package.json +2 -2
- package/scripts/lamaste-priv +200 -16
- package/src/index.js +1 -0
- package/src/lib/backend-cookie.js +65 -0
- package/src/lib/ip-panel-vhost.js +208 -0
- package/src/lib/service-config.js +5 -3
- package/src/tasks/nginx.js +18 -135
- package/src/tasks/redeploy.js +39 -1
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +1 -1
- package/vendor/docs/01-concepts/authorization.md +66 -48
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +70 -33
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +113 -25
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +3 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +165 -18
- package/vendor/docs/03-architecture/overview.md +13 -17
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +3 -2
- package/vendor/docs/03-architecture/state-management.md +4 -8
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +291 -22
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +60 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
- package/vendor/docs/04-api-reference/tunnels.md +67 -29
- package/vendor/docs/04-api-reference/users.md +241 -45
- package/vendor/docs/05-operations/backup-and-restore.md +35 -26
- package/vendor/docs/05-operations/monitoring.md +39 -0
- package/vendor/docs/05-operations/uninstalling.md +2 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +82 -188
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +25 -26
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +10 -8
- package/vendor/docs/identity-directory-design.md +48 -14
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +6 -1
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.js +5 -1
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
- package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/principals.js +67 -0
- package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +13 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +68 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +19 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +22 -48
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +250 -103
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +15 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-identity.js +335 -0
- package/vendor/server/src/commands/plugins.js +99 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/tunnels.js +102 -2
- package/vendor/server/src/commands/uninstall.js +3 -1
- package/vendor/server/src/commands/users.js +358 -0
- package/vendor/server/src/plugin-approval.js +107 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +1 -0
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +25 -1
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/config.js +68 -43
- package/vendor/serverd/src/lib/directory.js +1113 -0
- package/vendor/serverd/src/lib/entitlements.js +517 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +215 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +13 -0
- package/vendor/serverd/src/lib/identity-sync.js +905 -0
- package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
- package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +279 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +98 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +100 -9
- package/vendor/serverd/src/lib/plugins.js +59 -4
- package/vendor/serverd/src/lib/registry-credential.js +9 -6
- package/vendor/serverd/src/lib/request-logging.js +19 -0
- package/vendor/serverd/src/lib/retained-data.js +50 -4
- package/vendor/serverd/src/lib/state.js +18 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
- package/vendor/serverd/src/lib/user-access-session.js +21 -11
- package/vendor/serverd/src/lib/user-caller.js +67 -0
- package/vendor/serverd/src/lib/user-surface.js +733 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
- package/vendor/serverd/src/middleware/user-access-session.js +9 -0
- package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
- package/vendor/serverd/src/routes/invite.js +53 -12
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
- package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
- package/vendor/serverd/src/routes/management/invitations.js +78 -68
- package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
- package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/tunnels.js +98 -2
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
- package/vendor/serverd/src/routes/user-access.js +17 -4
- package/vendor/serverd/src/routes/user-surface.js +69 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
- package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
|
@@ -15,12 +15,12 @@ The helper does only what the relay **signed**. When it is enabled it remembers
|
|
|
15
15
|
Once per agent machine (Linux with systemd; macOS agents cannot host an agent half Lamaste installs):
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
sudo npm install -g @lamalibre/lamaste-agent@4.
|
|
18
|
+
sudo npm install -g @lamalibre/lamaste-agent@4.5.0 # a root-owned install
|
|
19
19
|
sudo lamaste-agent apphost enable # the agent runs as the user who ran sudo
|
|
20
20
|
sudo lamaste-agent apphost enable --user root # the agent runs as root: no sudoers rule
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`enable` refuses to act from an install another user could have changed: the package, the helper it ships and the core library it loads — and every directory above them — must be root-owned and writable by no group or other user. A user-level install (`nvm`, a home-directory prefix) is not; install it root-owned with `sudo npm install -g @lamalibre/lamaste-agent@4.
|
|
23
|
+
`enable` refuses to act from an install another user could have changed: the package, the helper it ships and the core library it loads — and every directory above them — must be root-owned and writable by no group or other user. A user-level install (`nvm`, a home-directory prefix) is not; install it root-owned with `sudo npm install -g @lamalibre/lamaste-agent@4.5.0` and run `enable` from that install. It also needs `/usr/bin/node` (the helper's interpreter and the Node.js every agent half runs on — from the distribution or NodeSource), systemd as the system manager (`/run/systemd/system`), and `sudo` and `visudo` unless the agent runs as root.
|
|
24
24
|
|
|
25
25
|
What `enable` does:
|
|
26
26
|
|
|
@@ -357,7 +357,7 @@ The contract:
|
|
|
357
357
|
|
|
358
358
|
```js
|
|
359
359
|
import Fastify from 'fastify';
|
|
360
|
-
import { agentHalfListenTarget, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.
|
|
360
|
+
import { agentHalfListenTarget, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.5.0/agent'; // bundled
|
|
361
361
|
|
|
362
362
|
const verifier = createCallerAssertionVerifier({
|
|
363
363
|
configPath: process.env.LAMASTE_APP_CONFIG,
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## Authentication
|
|
6
6
|
|
|
7
|
-
All Gatekeeper endpoints proxied through the panel require admin mTLS authentication. The internal `authz/tunnel/<fqdn>`
|
|
7
|
+
All Gatekeeper endpoints proxied through the panel require admin mTLS authentication. The internal `authz/tunnel/<fqdn>` and `authz/user-surface/<fqdn>` endpoints on `127.0.0.1:9294` are called by nginx only and are not exposed externally; Gatekeeper's own management API (`127.0.0.1:9294/api/*`, the shared secret in `gatekeeper-secret.json`) is called by the panel only.
|
|
8
|
+
|
|
9
|
+
**Grants and groups decide who uses agent-hosted plugins.** A plugin grant whose `context` names `roles` is an **entitlement** ([Agent-Hosted Plugins](agent-hosted-plugins.md#entitlements-and-the-audience)): every grant or group change that goes through the proxy is followed by a recompute of the plugins' audiences before the answer goes back, and anything given to a username — a user grant, a group membership — first clears what a removed user who held that username left in Gatekeeper ([Users API](users.md#usernames-and-gatekeeper)); when that cannot complete, the proxy answers `503` with `code: "gatekeeper_unavailable"`. Gatekeeper's own CLI writes the same database without telling the panel; the panel's minute-long recompute picks such edits up.
|
|
8
10
|
|
|
9
11
|
If the Gatekeeper service is unavailable, proxied endpoints return `503 Service Unavailable`.
|
|
10
12
|
|
|
@@ -195,7 +197,7 @@ Add one or more members to a group.
|
|
|
195
197
|
}
|
|
196
198
|
```
|
|
197
199
|
|
|
198
|
-
**Errors:** 400 (invalid or empty usernames array), 404 (group not found)
|
|
200
|
+
**Errors:** 400 (invalid or empty usernames array), 404 (group not found), 503 (a removed user's principal under one of the usernames cannot be cleared — `code: "gatekeeper_unavailable"`)
|
|
199
201
|
|
|
200
202
|
---
|
|
201
203
|
|
|
@@ -268,7 +270,9 @@ Create a new authorization grant linking a principal (user or group) to a resour
|
|
|
268
270
|
}
|
|
269
271
|
```
|
|
270
272
|
|
|
271
|
-
**
|
|
273
|
+
**Plugin role grants.** For `resourceType: "plugin"` with `context.roles`, the panel checks the grant before Gatekeeper sees it: `resourceId` must be the route of an installed plugin that declares `identity` (`400`, `code: "no_identity"`) and `roles` a non-empty list of its role ids (`400`, `code: "invalid_roles"`, or `"unknown_roles"` with the `unknown` ones); they go on sorted and without repeats. The plugin's **People** panel and `lamaste-server plugins entitlements` manage these grants one principal at a time (`PUT /api/plugins/:name/entitlements` replaces a principal's role grants) — prefer them to creating one here. A plugin grant without `roles` (the desktop's local-install grants) never makes anyone a plugin user.
|
|
274
|
+
|
|
275
|
+
**Errors:** 400 (invalid body; a role grant as above), 409 (duplicate grant), 503 (Gatekeeper unavailable, or a removed user's principal under `principalId` cannot be cleared — `code: "gatekeeper_unavailable"`)
|
|
272
276
|
|
|
273
277
|
---
|
|
274
278
|
|
|
@@ -555,16 +559,31 @@ DELETE /api/gatekeeper/access-log
|
|
|
555
559
|
|
|
556
560
|
**Auth:** Admin only
|
|
557
561
|
|
|
558
|
-
Delete all access log entries.
|
|
562
|
+
Delete all access log entries. It takes no query: Gatekeeper refuses one (`400`), so a narrowing query from an older caller never clears the whole log instead.
|
|
559
563
|
|
|
560
564
|
**Response (200):**
|
|
561
565
|
|
|
562
566
|
```json
|
|
563
567
|
{
|
|
564
|
-
"ok": true
|
|
568
|
+
"ok": true,
|
|
569
|
+
"removed": 142
|
|
565
570
|
}
|
|
566
571
|
```
|
|
567
572
|
|
|
573
|
+
### Purge One User's Access-Log Entries
|
|
574
|
+
|
|
575
|
+
```
|
|
576
|
+
POST /api/gatekeeper/access-log/purge
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**Auth:** Admin only
|
|
580
|
+
|
|
581
|
+
```json
|
|
582
|
+
{ "username": "alice", "before": "2026-10-22T08:00:00.000Z" }
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Deletes only that user's entries — logged before `before` (ISO 8601 UTC, optional) when given, since a later user may have the same username — and the entries still queued for the log, so none of them is written afterwards. `{ "ok": true, "removed": 3 }`; `400` for a body that is not `{ username, before? }`. The username travels in the body, never in a URL that request logs keep. The panel calls Gatekeeper's `POST /api/access-log/purge` itself when it [erases](users.md#erasing-a-removed-user) a removed user.
|
|
586
|
+
|
|
568
587
|
---
|
|
569
588
|
|
|
570
589
|
## Internal Endpoints
|
|
@@ -581,6 +600,12 @@ GET /authz/tunnel/:fqdn
|
|
|
581
600
|
|
|
582
601
|
This is the nginx `auth_request` target. nginx calls this endpoint on every request to a Gatekeeper-protected resource. The tunnel vhost names its tunnel in the path (`proxy_pass http://127.0.0.1:9294/authz/tunnel/<fqdn>;`) and sends the same hostname in `X-Original-URL` (with the request's path and query) and `Host`; a request whose `X-Original-URL` or `Host` names another hostname, or has no `Host`, is refused with 401. Gatekeeper validates the user's Authelia session cookie for that URL and checks the corresponding grant.
|
|
583
602
|
|
|
603
|
+
**The session cookie.** Of the browser's `Cookie` header Gatekeeper reads the `authelia_session` pair alone (core `autheliaSessionCookies`: a pair whose name, spaces and tabs trimmed, is exactly `authelia_session`) and sends Authelia `Cookie: authelia_session=<value>` — never the browser's other cookies. A request carrying **more than one** `authelia_session` is refused with `401` before Authelia is asked (logged `duplicate_authelia_cookie`): another host under the domain can plant a second one under another path, and which one Authelia would read is not Gatekeeper's to guess. A validated session is cached for `sessionCacheTtlMs` (30 s) under an HMAC of the cookie value **and the tunnel host** — Authelia decides per host, so a session accepted on one host is asked about again on another.
|
|
604
|
+
|
|
605
|
+
**Rate limit.** Every authorization route (`/authz/tunnel/:fqdn`, `/authz/user-surface/:fqdn`, `/authz/user-surface-unauthenticated`, `/authz/check`) allows 6000 requests a minute per client address and tunnel host (`AUTHZ_RATE_LIMIT_PER_CLIENT`), `429` past it. The client address is `X-Forwarded-For`, which nginx sets to `$remote_addr` on every subrequest and Gatekeeper trusts from `127.0.0.1` only (`trustProxy`) — one client exhausting its bucket leaves the others theirs. The management API keeps one shared bucket (1000 a minute): only the panel and local tooling reach it.
|
|
606
|
+
|
|
607
|
+
**Logs.** A refusal is logged with the tunnel, the reason and the request id — never the username (`no_matching_grant` included). The access-request log (`accessLoggingEnabled`) is the one place Gatekeeper records who asked.
|
|
608
|
+
|
|
584
609
|
`GET /authz/check` — what tunnel vhosts written before 4.3 call, with `X-Original-URL` and `Host` taken from the client's `Host` header — answers `403` (logged as `vhost_predates_host_binding`): such a vhost closes its tunnel until the panel rewrites it, which it does at startup. See [nginx Configuration](../03-architecture/nginx-configuration.md#authorization-subrequests-name-their-own-hostname).
|
|
585
610
|
|
|
586
611
|
**Response codes:**
|
|
@@ -592,3 +617,33 @@ This is the nginx `auth_request` target. nginx calls this endpoint on every requ
|
|
|
592
617
|
| 403 | Denied | User is authenticated but lacks a grant; returns an HTML body with denial details |
|
|
593
618
|
|
|
594
619
|
This endpoint is bound to `127.0.0.1` only. External traffic never reaches it directly -- nginx forwards the subrequest internally.
|
|
620
|
+
|
|
621
|
+
### User-Surface Authorization (nginx auth_request)
|
|
622
|
+
|
|
623
|
+
```
|
|
624
|
+
GET /authz/user-surface/:fqdn
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Host:** `127.0.0.1:9294`
|
|
628
|
+
|
|
629
|
+
The `auth_request` target of a plugin's user surface — `/api/<route>/app` and everything under it on the host of a tunnel bound to the plugin ([Tunnels API](tunnels.md#user-surface-binding)). The vhost names its tunnel in the path, `X-Original-URL` and `Host`, as for `/authz/tunnel/:fqdn`, and sends `X-Original-Method`. Gatekeeper checks, in order: the hostnames agree; the tunnel is known, enabled, not public and has a user surface; the raw request target — what follows `https://<fqdn>` in `X-Original-URL`, never re-encoded by a URL parser — lies on the user surface of a route bound to the tunnel, with no `.` or `..` segment, backslash, percent-encoded `.`, `/`, `\` or `%` (`%25`), or escape that does not decode; the tunnel's agent hosts the plugin — it carries the plugin's internal tunnel, the one `tunnels.json` records for the route (core `tunnelServedUserSurfaceRoutes`; refused `not_hosting_agent` otherwise: the tunnel moved or its agent revoked, the plugin hosted elsewhere or not at all); the Authelia session, read as for `/authz/tunnel/:fqdn` (one `authelia_session` only); on a `restricted` tunnel, the user's grant for the tunnel. Then it signs a **relay assertion for the panel**: Ed25519 with the relay signing key, audience `@panel`, port 3100, caller `{ type: "user", username }` — the username alone; the panel takes everything else about the user from its directory — bound to the method and the raw target (`{ route, path }`).
|
|
630
|
+
|
|
631
|
+
| Status | Meaning |
|
|
632
|
+
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
633
|
+
| 200 | `X-Lamalibre-Relay-Assertion` for this one request; `Cache-Control: no-store`; no `Remote-*` headers |
|
|
634
|
+
| 401 | No valid session (or more than one session cookie) — nginx answers with `GET /authz/user-surface-unauthenticated`: a JSON `401 { "error": "unauthenticated" }`, never a redirect (the caller is an application's request) |
|
|
635
|
+
| 403 | Anything else refused, with an empty body — never the access-request page; the reason is logged (`user-surface authz denied`) |
|
|
636
|
+
| 503 | The relay signing key is unusable |
|
|
637
|
+
|
|
638
|
+
### Principals (Gatekeeper's own API)
|
|
639
|
+
|
|
640
|
+
```
|
|
641
|
+
POST /api/principals/user/remove
|
|
642
|
+
{ "username": "alice" }
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Host:** `127.0.0.1:9294`, with the panel's secret. Not proxied.
|
|
646
|
+
|
|
647
|
+
Removes a user principal everywhere — every grant naming the username, of any resource type, and its membership of every group — in one transaction, holding the groups lock and then the grants lock. Idempotent. `{ "ok": true, "grantsRemoved": 2, "groupsLeft": 1 }`; `400` for a body that is not `{ username }` (1–100 characters), never echoing it. The username travels in the body, never in a URL that request logs keep (the `DELETE /api/principals/user/:username` of earlier 4.5 builds is gone). The panel calls it when a user is removed, retries it every minute while it is pending, and calls it before it creates a user under a username a removed user held — always then — and before it gives a grant or a membership to a username whose cleanup is pending ([Usernames and Gatekeeper](users.md#usernames-and-gatekeeper)).
|
|
648
|
+
|
|
649
|
+
**Request logging.** Gatekeeper logs one metadata line per request — method, route pattern (`/authz/tunnel/:fqdn`, never the URL), status, duration — and no client address; Fastify's own request lines are off.
|
|
@@ -60,18 +60,19 @@ Only a plugin with `"hosting": "agent"` and an `agent` block may declare hooks.
|
|
|
60
60
|
|
|
61
61
|
### Route patterns
|
|
62
62
|
|
|
63
|
-
`"<METHOD> <path>"` or `"<path>"` (any method). The method is `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` or `*`; `GET` also matches `HEAD`. The path starts with `/admin` (the forwarded admin API) or `/app` (the user surface —
|
|
63
|
+
`"<METHOD> <path>"` or `"<path>"` (any method). The method is `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` or `*`; `GET` also matches `HEAD`. The path starts with `/admin` (the forwarded admin API) or `/app` (the user surface — requests of the plugin's users, which only a plugin that declares `identity` receives; listed as `userSurfaceRoutes` in the preview). A segment is a literal or `*`: one segment, except as the last, where it matches one or more.
|
|
64
64
|
|
|
65
65
|
Matching errs towards running the hook: literal segments match case-insensitively, segments are compared percent-decoded (`/admin/%69mport` is `/admin/import`), and empty segments are ignored. The forwarder refuses a path with an encoded `/` or `\`.
|
|
66
66
|
|
|
67
67
|
## Approval
|
|
68
68
|
|
|
69
|
-
Relay hooks are code Lamaste runs on the relay, and an [agent half Lamaste installs](app-host.md) is code it has an agent's root helper install, so an administrator approves both first. What is approved is one **digest**: SHA-256 over the canonical JSON of the normalised relay declarations (defaults applied, shared-hook settings included) with the SHA-256 of the entry module, and of the agent half pinned for the version — its package, version, tarball integrity, service (`entry`, `memoryMax`) and system packages (`{ v: 2, relay, agentHalf }`, either part `null` when the version brings none). A version whose declarations, module
|
|
69
|
+
Relay hooks are code Lamaste runs on the relay, and an [agent half Lamaste installs](app-host.md) is code it has an agent's root helper install, so an administrator approves both first. What is approved is one **digest**: SHA-256 over the canonical JSON of the normalised relay declarations (defaults applied, shared-hook settings included) with the SHA-256 of the entry module, and of the agent half pinned for the version — its package, version, tarball integrity, service (`entry`, `memoryMax`) and system packages (`{ v: 2, relay, agentHalf }`, either part `null` when the version brings none) — and, only when the plugin declares one, its normalized [identity block](agent-hosted-plugins.md#the-identity-block) (`identity`: the roles its users hold and the attributes it receives about them), so a plugin without one keeps the digest it had before 4.5. A version whose declarations, module, agent half or identity block differ by one byte has another digest. The runner records the relay part's own digest, so a new agent half with an unchanged relay module leaves the runner current.
|
|
70
70
|
|
|
71
|
-
- Installing a plugin with relay hooks
|
|
71
|
+
- Installing a plugin with relay hooks, an agent half Lamaste installs or an identity block leaves them **pending**: the plugin can be enabled, but not hosted, its forwarded API answers `503 { code: "hooks_not_approved" }`, and its users are neither sent to its agent half nor served on its user surface until the approval.
|
|
72
72
|
- **Approving** names the digest the administrator saw; if what the version brings changed meanwhile the panel answers `409` with the current digest.
|
|
73
73
|
- **Updating** a plugin whose new version brings another hook set or agent half waits for approval; the installed version stays live. **Auto-approve**, off by default and per plugin, approves such changes automatically — each use is in the audit log — except an agent half change that runs more as root (an agent half where there was none, another agent package, added system packages, another service entry or memory limit; [withheld](agent-hosted-plugins.md#approval)).
|
|
74
74
|
- A version that turns a secret setting into a plain one — whose next value the relay would keep in clear — **always waits for an administrator**: settings are outside the digest, so this is a gate of its own, held even when the digest is already approved, when the plugin brings neither relay hooks nor an agent half, and whatever auto-approve says. The pending update says why it waits (`approvalRequired`), and its approval covers both.
|
|
75
|
+
- A version that **adds or removes a role or widens the attributes** of its identity block waits for an administrator too (`identity.changed`) — a new role is a new power over the plugin's users, a new attribute more about them — and auto-approve never covers it. Narrowing the attributes and editing labels only change the digest.
|
|
75
76
|
- Agent hooks are the plugin's own code on its own agent and need no approval. Shared hooks are part of the approved set by name and settings; their rules ride Lamaste releases (see below).
|
|
76
77
|
|
|
77
78
|
Every approval, auto-approve change and auto-approval is recorded in the [audit log](audit-and-notifications.md) with the administrator certificate's serial.
|
|
@@ -98,7 +99,7 @@ Every approval, auto-approve change and auto-approval is recorded in the [audit
|
|
|
98
99
|
"use": null,
|
|
99
100
|
"settings": null,
|
|
100
101
|
"failOpen": [],
|
|
101
|
-
"
|
|
102
|
+
"userSurfaceRoutes": []
|
|
102
103
|
}
|
|
103
104
|
],
|
|
104
105
|
"effects": ["audit", "notify-admins"],
|
|
@@ -117,6 +118,7 @@ Every approval, auto-approve change and auto-approval is recorded in the [audit
|
|
|
117
118
|
"unitName": "lamalibre-app-fx.service",
|
|
118
119
|
"dataDir": "/var/lib/lamalibre/apps/fx"
|
|
119
120
|
},
|
|
121
|
+
"identity": { "declared": false, "approved": false, "block": null },
|
|
120
122
|
"approval": {
|
|
121
123
|
"required": true,
|
|
122
124
|
"state": "pending",
|
|
@@ -133,7 +135,7 @@ Every approval, auto-approve change and auto-approval is recorded in the [audit
|
|
|
133
135
|
}
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
`failOpen` lists a hook's fail-open settings (`onError`, `onOversize`, `onUnscannable`); the panel highlights them. `relay.digest` is the relay hook set's own digest (what the runner holds); `approval.digest` is the one to approve, covering the relay hooks
|
|
138
|
+
`failOpen` lists a hook's fail-open settings (`onError`, `onOversize`, `onUnscannable`); the panel highlights them. `relay.digest` is the relay hook set's own digest (what the runner holds); `approval.digest` is the one to approve, covering the relay hooks, `agentHalf` — the agent half Lamaste installs, `null` when there is none or it is installed by hand ([App Host](app-host.md)) — and the identity block (`identity.block`, normalized; `identity.approved` once the installed version is). `pendingUpdate` is `{ version, digest, stagedAt, relay, agentHalf, settings, identity, approvalRequired, autoApproveWithheld }` while an update waits — `digest` is the one to approve it with (`null` when it brings no relay hooks, no agent half and no identity block: it is approved by its version), `identity` how it changes the identity block (`{ before, after, rolesAdded, rolesRemoved, attributesAdded, attributesRemoved, labelsChanged }`, `null` when neither version declares one), `settings` lists how the version changes the plugin's [settings schema](agent-hosted-plugins.md#in-the-plugin-list-and-the-update-preview) (`{ added: [{ key, type }], removed: [{ key, type }], retyped: [{ key, from, to }], newRequired: [key], secretToPlain: [key] }`, `null` when neither version declares settings; not part of the digest), `approvalRequired` why it waits for an administrator, and `autoApproveWithheld` why auto-approve, when on, did not cover it, in sentences (`null` otherwise):
|
|
137
139
|
|
|
138
140
|
```json
|
|
139
141
|
"approvalRequired": [
|
|
@@ -145,7 +147,7 @@ Every approval, auto-approve change and auto-approval is recorded in the [audit
|
|
|
145
147
|
]
|
|
146
148
|
```
|
|
147
149
|
|
|
148
|
-
`digest.unapproved`: what it brings — relay hooks, an agent half Lamaste installs — is not what was approved (auto-approve may stand in for an administrator on it). `settings.secret-to-plain`: it turns secret settings into plain ones — always an administrator's call, its digest approved or not.
|
|
150
|
+
`digest.unapproved`: what it brings — relay hooks, an agent half Lamaste installs, an identity block — is not what was approved (auto-approve may stand in for an administrator on it). `settings.secret-to-plain`: it turns secret settings into plain ones — always an administrator's call, its digest approved or not. `identity.changed`: it adds or removes roles, or receives more about its users (`it adds roles (editor) and receives more about its users (email)`) — always an administrator's call.
|
|
149
151
|
|
|
150
152
|
### `POST /api/plugins/:name/hooks/approve`
|
|
151
153
|
|
|
@@ -166,7 +168,7 @@ Approving a pending update approves everything it waits for (`approvalRequired`)
|
|
|
166
168
|
`{ "version"?: "1.2.3" | "latest" }` (default `latest`). For agent-hosted plugins. The panel fetches the version with `npm pack` (from the registry its [registry credential](registry-and-oidc.md) names, authenticated as the panel, when one is configured) and reads its manifest and relay module **in memory** — nothing of it is installed yet. Then:
|
|
167
169
|
|
|
168
170
|
- `200 { upToDate: true }` — nothing to do;
|
|
169
|
-
- `202 { pendingUpdate }` — what it brings (relay hook set, agent half) differs from what was approved and auto-approve is off or withheld, or it turns a secret setting into a plain one (always); `approvalRequired` says which, and the administrators are notified. Approve it with the digest shown, or by its version;
|
|
171
|
+
- `202 { pendingUpdate }` — what it brings (relay hook set, agent half, identity block) differs from what was approved and auto-approve is off or withheld, or it turns a secret setting into a plain one or adds or removes a role or widens the attributes (always); `approvalRequired` says which, and the administrators are notified. Approve it with the digest shown, or by its version;
|
|
170
172
|
- `202 { operation }` — the update runs: the new hook module goes into the runner, the install pipeline runs with `phase: "upgrade"` around installing the package (agent stages through the tunnel), and a failure restores the previous package, row and module.
|
|
171
173
|
|
|
172
174
|
The agent half of a hosted plugin whose agent half Lamaste installs is pinned when the version is staged and installed on the agent **first**, after the agent's pre stage; a failure after that activates the version that ran before ([Hosting with an agent half](agent-hosted-plugins.md#the-pipelines)). That is a push: the hosting agent must be eligible for the new version and push install must allow `update` — checked when the update starts, not while it waits for approval.
|
|
@@ -236,7 +238,7 @@ What a request hook sees: method, path (on the agent half), query, the caller (`
|
|
|
236
238
|
|
|
237
239
|
### Tunnel state
|
|
238
240
|
|
|
239
|
-
When the [health monitor](agent-hosted-plugins.md#health) sees a hosting go `down` (reason `tunnel` or `app`), Lamaste runs the relay's `tunnel:down`; its `fallback` (`{ status, retryAfter?, contentType?, body? }`) answers
|
|
241
|
+
When the [health monitor](agent-hosted-plugins.md#health) sees a hosting go `down` (reason `tunnel` or `app`), Lamaste runs the relay's `tunnel:down`; its `fallback` (`{ status, retryAfter?, contentType?, body? }`) answers the plugin's admin API while the plugin is down — no request is forwarded to the port then (without a fallback: `502` with the reason). `tunnel:up` runs when it is back after a `down` (not on the first `up`), and clears the fallback.
|
|
240
242
|
|
|
241
243
|
The agent cannot see its own chisel session; `agent-config` carries the relay's view (`appPlugins[].health`), and the agent's sync calls the agent half's `tunnel:down` / `tunnel:up` once per change. The panel unreachable for 90 s counts as `tunnel` down on the agent.
|
|
242
244
|
|
|
@@ -250,13 +252,13 @@ Relay hooks run in `lamalibre-lamaste-hooks@<route>.service`, started by its soc
|
|
|
250
252
|
|
|
251
253
|
`lamaste-priv hooks install <route> <sha256|none>` (module on stdin, checked against the approved digest) writes the module root-owned to `/opt/lamalibre/lamaste/hooks/<route>/<sha256>.mjs` and enables the socket; `hooks activate` points back at the previous module (an update's revert); `hooks restart` stops a stuck runner; `hooks remove` disables it and deletes its modules. The panel reinstalls an approved runner at start-up if it is missing.
|
|
252
254
|
|
|
253
|
-
The protocol between the panel and the runner is one length-prefixed JSON frame each way per connection (`@lamalibre/lamaste@4.
|
|
255
|
+
The protocol between the panel and the runner is one length-prefixed JSON frame each way per connection (`@lamalibre/lamaste@4.5.0/hooks`, `protocol.ts`).
|
|
254
256
|
|
|
255
257
|
## Writing hooks
|
|
256
258
|
|
|
257
259
|
```js
|
|
258
260
|
// hooks/relay.mjs — bundled into one file (esbuild), Node built-ins only
|
|
259
|
-
import { defineRelayHooks } from '@lamalibre/lamaste@4.
|
|
261
|
+
import { defineRelayHooks } from '@lamalibre/lamaste@4.5.0/hooks';
|
|
260
262
|
|
|
261
263
|
export default defineRelayHooks({
|
|
262
264
|
'install:pre-tunnel': async (input) =>
|
|
@@ -285,10 +287,10 @@ Every decision may carry `effects` (at most 10): `{ type: 'audit', event, data?
|
|
|
285
287
|
|
|
286
288
|
### Agent hooks
|
|
287
289
|
|
|
288
|
-
The agent half answers `POST /admin/hooks/<stage>` (body `{ stage, input }`, answer `{ ok: true, output? }` or `{ ok: false, reason }`). `createAgentHookHandler` from `@lamalibre/lamaste@4.
|
|
290
|
+
The agent half answers `POST /admin/hooks/<stage>` (body `{ stage, input }`, answer `{ ok: true, output? }` or `{ ok: false, reason }`). `createAgentHookHandler` from `@lamalibre/lamaste@4.5.0/agent` does the checking — the caller assertion must name Lamaste itself (`{ type: 'lamaste' }`), which no forwarded caller can, and bind the SHA-256 of the exact body (`wrong_body` otherwise), so it needs the body **as received** (a `Buffer` or a string; a parsed object is refused `400` "The body must be passed as received"); the forwarder never reaches `/admin/hooks*`. Send the `status`, `headers` and `bodyText` it returns, `bodyText` byte for byte: its `X-Lamalibre-Responder` header signs those bytes, and the relay counts a hook answer without a valid one as a failure (`unauthenticated`) — see [Authentication between the relay and the agent half](agent-hosted-plugins.md#authentication-between-the-relay-and-the-agent-half):
|
|
289
291
|
|
|
290
292
|
```js
|
|
291
|
-
import { createAgentHookHandler, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.
|
|
293
|
+
import { createAgentHookHandler, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.5.0/agent';
|
|
292
294
|
|
|
293
295
|
const hooks = createAgentHookHandler({
|
|
294
296
|
verifier: createCallerAssertionVerifier({ configPath, plugin: 'fx' }),
|
|
@@ -255,7 +255,7 @@ The wildcard check probes `test-lamaste-check.<domain>`. Wildcard DNS is optiona
|
|
|
255
255
|
|
|
256
256
|
### `POST /api/onboarding/provision`
|
|
257
257
|
|
|
258
|
-
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.
|
|
258
|
+
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.5.0` on the server to repair the installation." (likewise for Authelia). Task IDs are unchanged from earlier versions; only the titles changed.
|
|
259
259
|
|
|
260
260
|
Provisioning runs asynchronously. This endpoint returns immediately with a 202 status. Use the WebSocket stream endpoint to follow progress in real time.
|
|
261
261
|
|
|
@@ -165,10 +165,10 @@ Admin only, 10 per minute. Verifies the whole chain against the registry, then s
|
|
|
165
165
|
{ "registryUrl": "https://feria.example.com", "auth": { "kind": "token", "token": "feria_…" }, "scope": "@lamalibre" }
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
- `auth.kind: "token"` — a long-lived Feria token. The registry's `/-/whoami` must report the `admin` scope (the broker route requires it).
|
|
169
|
-
- `auth.kind: "oidc"` — the panel's own service principal. The panel first makes sure Authelia carries the confidential client `lamaste-server` (grant `client_credentials`, `token_endpoint_auth_method: client_secret_basic`, scope `lamaste.registry` — defined under `identity_providers.oidc.scopes` —, audience = the registry URL without a trailing slash, RS256 JWT access tokens; secret in `oidc-clients.json`). It then obtains a JWT from Authelia's token endpoint (`grant_type=client_credentials&scope=lamaste.registry&audience=<registryUrl>`), posts it to `<registryUrl>/-/feria/login`, and checks that `/-/whoami` reports `admin`. If any step fails the client is removed again and the error says what is missing — typically that Feria's `auth.json` needs, on its `groups` issuer for `https://auth.<domain>` (audience `<registryUrl>`), a `workloads` rule granting the `admin`
|
|
168
|
+
- `auth.kind: "token"` — a long-lived Feria token. The registry's `/-/whoami` must report the `read` scope (the panel's own installs, update staging and agent-half pinning) and the `admin` scope (the broker route requires it).
|
|
169
|
+
- `auth.kind: "oidc"` — the panel's own service principal. The panel first makes sure Authelia carries the confidential client `lamaste-server` (grant `client_credentials`, `token_endpoint_auth_method: client_secret_basic`, scope `lamaste.registry` — defined under `identity_providers.oidc.scopes` —, audience = the registry URL without a trailing slash, RS256 JWT access tokens; secret in `oidc-clients.json`). It then obtains a JWT from Authelia's token endpoint (`grant_type=client_credentials&scope=lamaste.registry&audience=<registryUrl>`), posts it to `<registryUrl>/-/feria/login`, and checks that `/-/whoami` reports `read` and `admin`. If any step fails the client is removed again and the error says what is missing — typically that Feria's `auth.json` needs, on its `groups` issuer for `https://auth.<domain>` (audience `<registryUrl>`), a `workloads` rule granting the scopes `read` and `admin` when the `client_id` claim (or `sub`, which carries the same value) is `lamaste-server`. Feria keys issuers by `iss` and the relay's users log in through the same Authelia, so the rule cannot be a second `rules` issuer; `workloads` needs Feria 0.3.2 or later.
|
|
170
170
|
|
|
171
|
-
Response: as `GET`. Errors: `400` (
|
|
171
|
+
Response: as `GET`. Errors: `400` (the `read` or `admin` scope is missing; the message names which), `502` (registry or Authelia refused or unreachable), `503` (no domain yet).
|
|
172
172
|
|
|
173
173
|
The Authelia call goes to `127.0.0.1:9091` with `X-Forwarded-Proto: https` and `X-Forwarded-Host: auth.<domain>`, which is how Authelia derives the issuer — the JWT says `iss: https://auth.<domain>`, exactly as it would through nginx. In the JWT, Authelia sets both `sub` and `client_id` to the client id for the client-credentials flow (there is no end user).
|
|
174
174
|
|
|
@@ -53,20 +53,22 @@ curl -s --cert client.p12:password \
|
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
| Field
|
|
57
|
-
|
|
|
58
|
-
| `id`
|
|
59
|
-
| `subdomain`
|
|
60
|
-
| `fqdn`
|
|
61
|
-
| `port`
|
|
62
|
-
| `description`
|
|
63
|
-
| `type`
|
|
64
|
-
| `accessMode`
|
|
65
|
-
| `agentLabel`
|
|
66
|
-
| `exposure`
|
|
67
|
-
| `maxBodySizeMb`
|
|
68
|
-
| `enabled`
|
|
69
|
-
| `
|
|
56
|
+
| Field | Type | Description |
|
|
57
|
+
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `id` | `string` | UUID v4 identifier |
|
|
59
|
+
| `subdomain` | `string` | The subdomain portion (e.g., `app`) |
|
|
60
|
+
| `fqdn` | `string` | Fully qualified domain name (e.g., `app.example.com`) |
|
|
61
|
+
| `port` | `number` | Port the carrying agent forwards; bound on `127.0.0.1` on both the server and the agent |
|
|
62
|
+
| `description` | `string \| null` | Optional human-readable description |
|
|
63
|
+
| `type` | `string` | `'app'` (default), `'panel'` (agent web panel), or `'plugin'` |
|
|
64
|
+
| `accessMode` | `string` | `'restricted'` (default), `'authenticated'`, or `'public'` — absent on panel tunnels |
|
|
65
|
+
| `agentLabel` | `string` | The agent that carries the tunnel. Absent only on an unassigned legacy tunnel (see below) |
|
|
66
|
+
| `exposure` | `string` | `'internal'` on the internal tunnel of a hosted agent-hosted plugin: no `subdomain`, `fqdn` or `accessMode`, only the chisel grant on a relay-loopback port. Absent (public) on every other tunnel. See [Agent-Hosted Plugins](agent-hosted-plugins.md) |
|
|
67
|
+
| `maxBodySizeMb` | `number` | Largest request body nginx accepts, in MiB. Absent on panel tunnels and on tunnels created before the setting existed, which keep nginx's built-in 1 MiB until a body limit is set on them |
|
|
68
|
+
| `enabled` | `boolean` | Whether the tunnel is active (defaults to `true`) |
|
|
69
|
+
| `userSurface` | `object` | `{ routes: string[] }` — the agent-hosted plugins whose user surface this host serves ([User Surface Binding](#user-surface-binding)). Absent when none |
|
|
70
|
+
| `userSurfaceStatus` | `object[]` | With `userSurface`: how each bound route is served — `{ route, served, reason }`, `reason` `null` when served, else `not_installed`, `no_identity` or `not_approved` |
|
|
71
|
+
| `createdAt` | `string` | ISO 8601 timestamp |
|
|
70
72
|
|
|
71
73
|
---
|
|
72
74
|
|
|
@@ -205,8 +207,8 @@ Return 201 with tunnel object
|
|
|
205
207
|
Changes any of: carrying agent, access mode, request body limit, enabled state. All of them are applied in one workflow under the tunnel lock, in that order — ownership first, so a later failure still leaves the tunnel with the agent the caller asked for; access mode and body limit next; the enabled state last, so a tunnel being disabled is never re-enabled by the vhost rewrite before it. Authorization is checked against the tunnel as it is when the lock is taken.
|
|
206
208
|
|
|
207
209
|
- **`enabled`** — when disabled, the tunnel's nginx vhost symlink is removed (the config file is kept) and the owner's Chisel grant for the port is withdrawn (Chisel restarts, which ends sessions still holding it). Re-enabling restores both. Only enabled tunnels appear in an agent's config, and the owner's sync timer drops or adds the remote within about 30 seconds.
|
|
208
|
-
- **`agentLabel`** (admin only) — moves the tunnel to another active agent. The previous owner loses the port grant in the same Chisel sync that gives it to the new owner. Both agents' sync timers apply the move within about 30 seconds; `lamaste-agent update` applies it at once. Agent panel tunnels (`type: 'panel'`) belong to the agent they expose and cannot be moved.
|
|
209
|
-
- **`accessMode`** and **`maxBodySizeMb`** — both live in the tunnel's nginx vhost, which is re-rendered and tested (nginx keeps the previous vhost if the test fails). Gatekeeper watches the tunnel state and picks up an access-mode change on its own. A disabled tunnel stays disabled. `public` and `authenticated` are admin-only, as on creation. Agent panel tunnels have a fixed mTLS vhost and cannot be reconfigured.
|
|
210
|
+
- **`agentLabel`** (admin only) — moves the tunnel to another active agent. The previous owner loses the port grant in the same Chisel sync that gives it to the new owner. Both agents' sync timers apply the move within about 30 seconds; `lamaste-agent update` applies it at once. Agent panel tunnels (`type: 'panel'`) belong to the agent they expose and cannot be moved. A tunnel bound as a plugin's [user surface](#user-surface-binding) keeps its binding, but serves a route only while the new agent hosts the plugin: when that changes what it serves, its vhost is re-rendered first (and restored if the state cannot be saved).
|
|
211
|
+
- **`accessMode`** and **`maxBodySizeMb`** — both live in the tunnel's nginx vhost, which is re-rendered and tested (nginx keeps the previous vhost if the test fails). Gatekeeper watches the tunnel state and picks up an access-mode change on its own. A disabled tunnel stays disabled. `public` and `authenticated` are admin-only, as on creation. A tunnel bound as a plugin's [user surface](#user-surface-binding) cannot be made public (`409`), and its re-rendered vhost keeps the user-surface locations it serves. Agent panel tunnels have a fixed mTLS vhost and cannot be reconfigured.
|
|
210
212
|
|
|
211
213
|
**Request:**
|
|
212
214
|
|
|
@@ -252,7 +254,7 @@ curl -s --cert client.p12:password \
|
|
|
252
254
|
|
|
253
255
|
### `DELETE /api/tunnels/:id`
|
|
254
256
|
|
|
255
|
-
Deletes a tunnel by its UUID: removes the nginx vhost, removes the tunnel from the state file, and syncs Chisel so the owner's grant for the port is withdrawn.
|
|
257
|
+
Deletes a tunnel by its UUID: removes the nginx vhost, removes the tunnel from the state file, and syncs Chisel so the owner's grant for the port is withdrawn. Its [user-surface bindings](#user-surface-binding) go with it.
|
|
256
258
|
|
|
257
259
|
The TLS certificate is not deleted (it is harmless to keep and may be reused if the subdomain is recreated).
|
|
258
260
|
|
|
@@ -316,7 +318,7 @@ curl -s --cert client.p12:password \
|
|
|
316
318
|
| `chiselArgs` | `string[]` | Chisel client arguments: `client`, the server URL, and one `R:127.0.0.1:<port>:127.0.0.1:<port>` remote per tunnel. No `--tls-skip-verify` (the agent verifies the relay's certificate) and no credential — the agent supplies its own through the service environment, never argv |
|
|
317
319
|
| `chiselCredentialIssuedAt` | `string \| null` | When the agent's current Chisel credential was issued — a timestamp, never the credential. An agent holding a credential with a different issue time (an administrator rotated it) fetches the current one from `GET /api/agents/me/chisel-credential` |
|
|
318
320
|
| `tunnels` | `array` | The agent's enabled tunnels with port and subdomain |
|
|
319
|
-
| `hostFactsHash` | `string \| null` | Agent certificates only: the SHA-256 of the [host facts](agent-hosted-plugins.md#host-facts) the relay stores for the agent, computed as the agent computes it over its own (`hostFactsHash` in `@lamalibre/lamaste@4.
|
|
321
|
+
| `hostFactsHash` | `string \| null` | Agent certificates only: the SHA-256 of the [host facts](agent-hosted-plugins.md#host-facts) the relay stores for the agent, computed as the agent computes it over its own (`hostFactsHash` in `@lamalibre/lamaste@4.5.0`); `null` when it stores none. The agent's sync reports its facts again when this is not the hash of its own, so a record replaced since its last report — the agent enrolled again on its label, the relay restored from a backup — has them again after one sync, not a day. An agent behind a relay without the field reports as before |
|
|
320
322
|
|
|
321
323
|
**Errors:**
|
|
322
324
|
|
|
@@ -325,6 +327,40 @@ curl -s --cert client.p12:password \
|
|
|
325
327
|
| 400 | `{"error":"Domain not configured"}` | Domain has not been set |
|
|
326
328
|
| 500 | `{"error":"Failed to generate agent config"}` | Config generation failed |
|
|
327
329
|
|
|
330
|
+
## User Surface Binding
|
|
331
|
+
|
|
332
|
+
An app tunnel can be **bound** as the user surface of an agent-hosted plugin: its host then serves the plugin's users at `/api/<route>/app` and everything under it — Gatekeeper authenticates the Authelia session and vouches for the user to the panel, and the panel forwards the request to the plugin's agent half with the directory's `userId`, username and the user's roles ([Agent-Hosted Plugins](agent-hosted-plugins.md#the-user-surface)). Every other path of the host goes down the tunnel as before. Admin only, 30 a minute.
|
|
333
|
+
|
|
334
|
+
**What can be bound.** An enabled app tunnel (`type` `app`, not internal), with access mode `authenticated` or `restricted` — a public host has no signed-in user to vouch for — **carried by the agent that hosts the plugin** (`400` otherwise: "Plugin '…' is hosted on agent '…', but this tunnel is carried by '…'", or "… is not hosted on an agent" before it is hosted). The host whose sessions reach the plugin's users is never another agent's: whatever answers on its other paths — another agent's application — would share the user surface's origin. On a `restricted` tunnel the user also needs a grant for the tunnel. The route must be held by an installed agent-hosted plugin; a plugin's user surface is on one host at most, and one host serves at most 16 plugins' user surfaces. A bound tunnel cannot be made public (`PATCH` answers `409` "… cannot be made public — unbind it first").
|
|
335
|
+
|
|
336
|
+
**When it is served.** A plugin may be bound before it declares `identity`: its host serves the route — locations that ask Gatekeeper and pass the request to the panel ([nginx Configuration](../03-architecture/nginx-configuration.md#user-surface-locations)) — only while the plugin's installed, approved version declares it **and the tunnel's agent is the plugin's hosting agent**. Until then its requests go down the tunnel as before, so a plugin moves its users' API to the relay with a plain update: bind first, then update and approve. A binding stays when the tunnel moves to another agent, its agent is revoked, or the plugin is unhosted or hosted elsewhere — but the host serves none of it then (`userSurfaceStatus` reason `not_hosting_agent`): its vhost loses the locations, Gatekeeper refuses to vouch for the route, and the panel refuses a request for it even with an assertion. The vhosts of the bound tunnels are re-rendered whenever that can change — the plugin's approval, update or its revert, hosting and unhosting, a move of the tunnel, its agent's revocation — and at the panel's start. Uninstalling the plugin releases its binding everywhere — and any binding left for a route no installed plugin holds, should a bind have raced the uninstall (a bind that finds its plugin gone once the binding is written undoes it and answers `409`); deleting a tunnel releases its bindings. Every change is audited: `tunnel.user-surface.bound`, `tunnel.user-surface.unbound` (with `reason`: `unbound`, `tunnel-deleted`, `plugin-uninstalled`).
|
|
337
|
+
|
|
338
|
+
`userSurfaceStatus` lists each bound route as `{ route, served, reason }`; `reason` is `not_installed`, `no_identity`, `not_approved` or `not_hosting_agent`, and null when served.
|
|
339
|
+
|
|
340
|
+
### `PUT /api/tunnels/:id/user-surface`
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{ "route": "market-indicators" }
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Binds the plugin route to the tunnel's host. Idempotent. **Response (200):** `{ "ok": true, "changed": true, "tunnel": { ... } }` — the tunnel with its `userSurface` and `userSurfaceStatus`; `changed: false` when the route was bound there already.
|
|
347
|
+
|
|
348
|
+
### `DELETE /api/tunnels/:id/user-surface/:route`
|
|
349
|
+
|
|
350
|
+
Unbinds it: requests for the route go down the tunnel again. **Response (200):** `{ "ok": true, "tunnel": { ... } }`.
|
|
351
|
+
|
|
352
|
+
**Errors** (both):
|
|
353
|
+
|
|
354
|
+
| Status | Body | When |
|
|
355
|
+
| ------ | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
356
|
+
| 400 | `{"error":"…"}` | Not an app tunnel's host, disabled, unassigned, public; no installed agent-hosted plugin holds the route; the plugin not hosted, or hosted by another agent than the tunnel's; 16 routes bound already; domain or email not configured |
|
|
357
|
+
| 404 | `{"error":"Tunnel not found"}` / `{"error":"This tunnel does not serve the user surface of '…'"}` | No such tunnel; unbinding a route that is not bound there |
|
|
358
|
+
| 409 | `{"error":"The user surface of '…' is already served on …","code":"user_surface_conflict"}` | The route is bound to another tunnel |
|
|
359
|
+
| 409 | `{"error":"The plugin holding the route '…' was uninstalled while it was being bound — nothing is bound"}` | The plugin was uninstalled while the binding was written (released again) |
|
|
360
|
+
| 500 | `{"error":"Failed to bind the user surface","details":"…"}` | nginx or state operation failed (the previous vhost is restored) |
|
|
361
|
+
|
|
362
|
+
From the CLI: `lamaste-server tunnels bind-user-surface <tunnel> <plugin>` and `tunnels unbind-user-surface <tunnel> <plugin>` (the tunnel by id, subdomain or FQDN; the plugin by name or route). In the panel: the Tunnels page's **User surface** action on an eligible tunnel, which shows each bound route's status; the desktop app through `AdminClient` `bindTunnelUserSurface(id, route)` and `unbindTunnelUserSurface(id, route)`.
|
|
363
|
+
|
|
328
364
|
## Chisel Grants
|
|
329
365
|
|
|
330
366
|
The Chisel server authenticates every client against `/etc/lamalibre/lamaste/chisel-users`. That file is rendered by the panel from two persisted inputs — the per-agent credential store and the tunnel state — and re-rendered whenever either changes:
|
|
@@ -355,7 +391,7 @@ Tunnels on a reserved port, or two tunnels on the same port, are granted to no a
|
|
|
355
391
|
|
|
356
392
|
### When an agent certificate is revoked
|
|
357
393
|
|
|
358
|
-
Revoking an agent's certificate releases its tunnels: its app and plugin tunnels become unassigned (their hostnames stay reserved and dark until an administrator assigns them), and its `agent-<label>` panel tunnel is removed with its vhost. A later enrollment that reuses the label therefore inherits nothing.
|
|
394
|
+
Revoking an agent's certificate releases its tunnels: its app and plugin tunnels become unassigned (their hostnames stay reserved and dark until an administrator assigns them), and its `agent-<label>` panel tunnel is removed with its vhost. A later enrollment that reuses the label therefore inherits nothing. A released tunnel bound as a plugin's user surface keeps its binding but serves none of it, and its vhost is re-rendered without those locations.
|
|
359
395
|
|
|
360
396
|
## Validation Rules
|
|
361
397
|
|
|
@@ -386,16 +422,18 @@ A tunnel's vhost proxies to `127.0.0.1:<port>` on the relay, so a tunnel on one
|
|
|
386
422
|
|
|
387
423
|
## Quick Reference
|
|
388
424
|
|
|
389
|
-
| Method | Path
|
|
390
|
-
| ------ |
|
|
391
|
-
| GET | `/api/tunnels`
|
|
392
|
-
| POST | `/api/tunnels`
|
|
393
|
-
| PATCH | `/api/tunnels/:id`
|
|
394
|
-
| DELETE | `/api/tunnels/:id`
|
|
395
|
-
|
|
|
396
|
-
|
|
|
397
|
-
|
|
|
398
|
-
|
|
|
425
|
+
| Method | Path | Description |
|
|
426
|
+
| ------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
427
|
+
| GET | `/api/tunnels` | List tunnels (an agent sees only its own) |
|
|
428
|
+
| POST | `/api/tunnels` | Create a tunnel carried by an agent |
|
|
429
|
+
| PATCH | `/api/tunnels/:id` | Move (admin), access mode, body limit, enable/disable |
|
|
430
|
+
| DELETE | `/api/tunnels/:id` | Delete a tunnel by UUID |
|
|
431
|
+
| PUT | `/api/tunnels/:id/user-surface` | Bind a plugin route as the tunnel host's user surface (admin) |
|
|
432
|
+
| DELETE | `/api/tunnels/:id/user-surface/:route` | Unbind it (admin) |
|
|
433
|
+
| GET | `/api/tunnels/agent-config` | One agent's Chisel client config (macOS & Linux) |
|
|
434
|
+
| GET | `/api/tunnels/agent-panel-status` | Check if agent has an exposed panel tunnel |
|
|
435
|
+
| POST | `/api/tunnels/expose-panel` | Create mTLS panel tunnel for requesting agent (refused when a static site or alias serves `agent-<label>.<domain>`) |
|
|
436
|
+
| DELETE | `/api/tunnels/retract-panel` | Remove the panel tunnel for requesting agent |
|
|
399
437
|
|
|
400
438
|
### curl Cheat Sheet
|
|
401
439
|
|