Config Files
Complete reference for every configuration and state file Lamaste uses.
File Overview
| File | Format | Owner | Mode | Purpose |
|---|---|---|---|---|
/etc/lamalibre/lamaste/panel.json | JSON | lamaste:lamaste | 0600 | Panel server configuration |
/etc/lamalibre/lamaste/tunnels.json | JSON | lamaste:lamaste | 0600 | Tunnel definitions |
/etc/lamalibre/lamaste/sites.json | JSON | lamaste:lamaste | 0600 | Static site definitions |
/etc/lamalibre/lamaste/chisel-credentials.json | JSON | lamaste:lamaste | 0600 | Per-agent Chisel credentials (source of truth for the authfile) |
/etc/lamalibre/lamaste/chisel-users | JSON | lamaste:lamaste-chisel | 0640 | Chisel --authfile: per-agent credentials and port grants (rendered) |
/etc/lamalibre/lamaste/chisel-sentinel | text | lamaste:lamaste | 0600 | Password of the lamaste-no-grants sentinel user |
/etc/lamalibre/lamaste/chisel-server.key | PEM | lamaste:lamaste-chisel | 0640 | Chisel SSH host key (--keyfile), generated by the panel at onboarding |
/etc/authelia/configuration.yml | YAML | lamaste:lamaste-authelia | 0640 | Authelia server configuration |
/etc/authelia/users.yml | YAML | lamaste:lamaste-authelia | 0660 | User database (Authelia rewrites it on a password change) |
/etc/authelia/.secrets.json | JSON | lamaste:lamaste-authelia | 0600 | Authelia secrets (panel only) |
/etc/authelia/oidc-issuer.pem | PEM | lamaste-authelia | 0600 | RSA-4096 key Authelia signs OIDC tokens with, written by create-lamaste; the panel cannot read it |
/etc/lamalibre/lamaste/registry-credential.json | JSON | lamaste:lamaste | 0600 | The panel's registry (Feria) credential: a pasted token, or the lamaste-server OIDC client |
/etc/lamalibre/lamaste/oidc-clients.json | JSON | lamaste:lamaste | 0600 | Plaintext secrets (and pbkdf2 salts) of confidential OIDC clients |
/etc/lamalibre/lamaste/relay-signing-key.json | JSON | lamaste:lamaste | 0600 | Ed25519 key relay assertions (panel, Gatekeeper) and agents' app jobs (panel) are signed with |
/etc/lamalibre/lamaste/push-install-config.json | JSON | lamaste:lamaste | 0600 | Push install: the switch, the policies, the default policy |
/etc/lamalibre/lamaste/push-install-sessions.json | JSON | lamaste:lamaste | 0600 | Push install log (last 500 pushes) |
/etc/lamalibre/lamaste/ticket-scopes.json | JSON | lamaste:lamaste | 0600 | Ticket scope registry |
/etc/lamalibre/lamaste/tickets.json | JSON | lamaste:lamaste | 0600 | Ticket and session store |
/etc/lamalibre/lamaste/invitations.json | JSON | lamaste:lamaste | 0600 | Pending user invitations |
/etc/lamalibre/lamaste/plugins.json | JSON | lamaste:lamaste | 0600 | Plugin registry (before 4.2; the registry is now in state.db) |
/etc/lamalibre/lamaste/state.db | SQLite | lamaste:lamaste | 0600 | Plugins (with their hooks), agents, tickets, enrollment tokens, the audit log, notifications, relay hook approvals, plugin operations, agent jobs |
/etc/lamalibre/lamaste/plugin-hostings.json | JSON | lamaste:lamaste | 0600 | Hostings of agent-hosted plugins: agent, port, phase, installation secrets, health |
/opt/lamalibre/lamaste/hooks/<route>/ | directory | root:root | 0755 | A plugin's approved relay hook module (<sha256>.mjs, 0644) and runner.env, written by lamaste-priv |
/etc/lamalibre/lamaste/storage-config.json | JSON | lamaste:lamaste | 0600 | Storage server registry and plugin bindings (credentials AES-256-GCM encrypted) |
/etc/lamalibre/lamaste/storage-master.key | Binary | lamaste:lamaste | 0600 | 32-byte master key for storage credential encryption |
/etc/lamalibre/lamaste/groups.json | JSON | lamaste:lamaste | 0600 | Lamaste group definitions and membership |
/etc/lamalibre/lamaste/access-grants.json | JSON | lamaste:lamaste | 0600 | Generic access grants (principal → resource) |
/etc/lamalibre/lamaste/gatekeeper.json | JSON | lamaste:lamaste | 0600 | Gatekeeper settings (cache TTL, admin contact, logging) |
/etc/lamalibre/lamaste/access-request-log.json | JSON | lamaste:lamaste | 0600 | Optional denied access log |
/etc/lamalibre/lamaste/pki/enrollment-tokens.json | JSON | lamaste:lamaste | 0600 | One-time enrollment tokens for hardware-bound enrollment |
/etc/lamalibre/lamaste/pki/revoked.json | JSON | lamaste:lamaste | 0600 | Revoked certificate serial numbers |
/etc/lamalibre/lamaste/pki/agents/registry.json | JSON | lamaste:lamaste | 0600 | Agent certificate metadata |
/etc/nginx/sites-available/lamalibre-lamaste-* | nginx conf | root:root | 0644 | Vhost configurations (the panel's are installed by lamaste-priv) |
/etc/nginx/snippets/lamalibre-lamaste-mtls.conf | nginx conf | root:root | 0644 | mTLS snippet |
/etc/nginx/snippets/lamalibre-lamaste-authz-cache.conf | nginx conf | root:root | 0644 | Gatekeeper proxy_cache zone |
/etc/systemd/system/lamalibre-lamaste-gatekeeper.service | systemd | root:root | 0644 | Gatekeeper systemd unit |
/etc/systemd/system/chisel.service | systemd | root:root | 0644 | Chisel unit, written by create-lamaste |
/etc/systemd/system/authelia.service | systemd | root:root | 0644 | Authelia unit, written by create-lamaste |
/etc/systemd/system/lamalibre-lamaste-hooks@.socket, @.service | systemd | root:root | 0644 | Relay hook runner (one instance per plugin with relay hooks), written by create-lamaste |
~/.lamalibre/lamaste/servers.json | JSON | user | 0600 | Desktop app server registry |
~/.lamalibre/lamaste/agents.json | JSON | user | 0600 | Multi-agent registry |
~/.lamalibre/lamaste/agentd.token | JSON | user | 0600 | Bearer token of lamaste-agentd's owner (the desktop app, scripts run as the user) |
~/.lamalibre/lamaste/agents/<label>/config.json | JSON | user | 0600 | Per-agent configuration |
~/.lamalibre/lamaste/agents/<label>/client.p12 | PKCS#12 | user | 0600 | Per-agent mTLS certificate |
~/.lamalibre/lamaste/agents/<label>/ca.crt | PEM | user | 0644 | Per-agent CA certificate |
~/.lamalibre/lamaste/agents/<label>/chisel.json | JSON | user | 0600 | Per-agent Chisel credential (source of truth for the service) |
~/.lamalibre/lamaste/agents/<label>/chisel.env | env file | user | 0600 | Linux only: AUTH= for the systemd unit's EnvironmentFile= |
~/.lamalibre/lamaste/agents/<label>/ | directory | user | 0700 | Per-agent data directory |
~/.lamalibre/lamaste/agents/<label>/sync-state.json | JSON | user | 0600 | Outcome of the last lamaste-agent sync |
~/.lamalibre/lamaste/agents/<label>/app-plugins/<route>.json | JSON | user | 0600 | Port and installation secret of a plugin hosted on this agent |
~/.lamalibre/lamaste/agents/<label>/app-plugin-tunnel-state.json | JSON | user | 0600 | The tunnel state last delivered to each hosted plugin's agent half |
~/.lamalibre/lamaste/agents/<label>/apphost-state.json | JSON | user | 0600 | The agent halves the app host helper installed for this agent |
~/.lamalibre/lamaste/agents/<label>/host-facts.json | JSON | user | 0600 | What the agent last reported about its host, and the helper's last answer |
~/.lamalibre/lamaste/agents/<label>/app-job-results.json | JSON | user | 0600 | App job results the relay has not taken yet |
~/.lamalibre/lamaste/agents/<label>/logs/ | directory | user | 0700 | Per-agent Chisel and sync log files |
~/.lamalibre/lamaste/agent.json | JSON | user | 0600 | Legacy single-server config |
/usr/local/sbin/lamaste-apphost | Node.js | root:root | 0755 | Agent machine: the app host helper — see Agent machine: the app host |
/etc/sudoers.d/lamaste-apphost | sudoers | root:root | 0440 | Agent machine: lets the agent's user run the helper |
/etc/lamalibre/apphost/relays/<label>.json | JSON | root:root | 0644 | Agent machine: the relay key pinned for agent <label> |
/opt/lamalibre/apps/<route>/ | directory | root:root | 0755 | Agent machine: an agent half's installed versions |
/var/lib/lamalibre/apps/<route>/ | directory | lamaste-app-<route> | 0700 | Agent machine: an agent half's data |
/etc/lamalibre/apps/<route>/plugin-config.json | JSON | lamaste-app-<route> | 0600 | Agent machine: an agent half's port and installation secret |
/etc/systemd/system/lamalibre-app-<route>.service | systemd | root:root | 0644 | Agent machine: an agent half's unit |
/etc/lamalibre/lamaste/panel.json
The primary configuration file for the panel server. Created by the installer, updated during onboarding and tunnel management.
Schema:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
ip | string | Yes | — | Server public IP address |
domain | string | null | Yes | null | Base domain (set during onboarding) |
serverId | string | No | — | Auto-generated UUIDv4, used as bucket prefix for multi-server storage isolation |
email | string | null | Yes | null | Admin email for Let's Encrypt (set during onboarding) |
dataDir | string | Yes | /etc/lamalibre/lamaste | Path to data/state directory |
staticDir | string | No | — | Path to lamaste-server-ui dist directory |
maxSiteSize | number | No | 524288000 | Max static site upload size in bytes (500 MB) |
adminAuthMode | string | No | "p12" | "p12" or "hardware-bound". When "hardware-bound", P12 download and rotation are disabled; admin authenticates via Keychain-backed certificate. |
panel2fa | object | No | — | Built-in TOTP 2FA configuration (see sub-fields below) |
panel2fa.enabled | boolean | No | false | Whether 2FA is active for admin panel access |
panel2fa.secret | string | null | No | null | Base32-encoded TOTP secret |
panel2fa.setupComplete | boolean | No | false | Whether the 2FA setup flow has been confirmed |
sessionSecret | string | No | — | HMAC key for signing session cookies (auto-generated during 2FA setup) |
onboarding.status | enum | Yes | FRESH | Current onboarding state |
Onboarding status values:
| Value | Meaning |
|---|---|
FRESH | No onboarding started — shows onboarding wizard |
DOMAIN_SET | Domain and email entered, awaiting DNS verification |
DNS_READY | DNS verified, ready to provision |
PROVISIONING | Stack provisioning in progress |
COMPLETED | Onboarding complete — shows management UI |
Example (fresh install):
{
"ip": "203.0.113.42",
"domain": null,
"email": null,
"dataDir": "/etc/lamalibre/lamaste",
"staticDir": "/opt/lamalibre/lamaste/lamaste-server-ui/dist",
"onboarding": {
"status": "FRESH"
}
}Example (after onboarding):
{
"ip": "203.0.113.42",
"domain": "example.com",
"email": "admin@example.com",
"dataDir": "/etc/lamalibre/lamaste",
"staticDir": "/opt/lamalibre/lamaste/lamaste-server-ui/dist",
"maxSiteSize": 524288000,
"onboarding": {
"status": "COMPLETED"
}
}Example (with 2FA enabled):
{
"ip": "203.0.113.42",
"domain": "example.com",
"email": "admin@example.com",
"dataDir": "/etc/lamalibre/lamaste",
"staticDir": "/opt/lamalibre/lamaste/lamaste-server-ui/dist",
"maxSiteSize": 524288000,
"onboarding": {
"status": "COMPLETED"
},
"panel2fa": {
"enabled": true,
"secret": "JBSWY3DPEHPK3PXP...",
"setupComplete": true
},
"sessionSecret": "a1b2c3d4..."
}Config resolution order:
LAMALIBRE_LAMASTE_CONFIGenvironment variable (if set)- In development (
NODE_ENV=developmentorNODE_ENVunset):<package-root>/dev/panel.json - In production (
NODE_ENV=production):/etc/lamalibre/lamaste/panel.json
Note: The systemd service unit sets
CONFIG_FILEin the environment, but the panel server code readsLAMALIBRE_LAMASTE_CONFIG. The systemd variableCONFIG_FILEis not used by the application. In production deployments this has no effect because the code falls back to/etc/lamalibre/lamaste/panel.jsonwhenLAMALIBRE_LAMASTE_CONFIGis unset andNODE_ENV=production.
Write pattern: Atomic — writes to .tmp file then rename().
Validated with: Zod schema in packages/lamaste-serverd/src/lib/config.js.
/etc/lamalibre/lamaste/tunnels.json
Stores the array of configured tunnels. Created automatically when the first tunnel is added.
Schema: Array of tunnel objects.
| Field | Type | Description |
|---|---|---|
id | string | UUID |
subdomain | string | Subdomain name (e.g., myapp) |
fqdn | string | Full domain (e.g., myapp.example.com) |
port | number | Port forwarded between 127.0.0.1 on the server and 127.0.0.1 on the carrying agent |
description | string | null | Optional description (max 200 characters) |
type | string | "app" (default), "panel" (agent web panel), or "plugin" |
enabled | boolean | Whether the tunnel is active |
accessMode | string | undefined | "public", "authenticated", or "restricted". Controls whether nginx skips auth, requires Authelia login only, or requires Authelia login plus a Gatekeeper grant. Absent for panel tunnels. |
agentLabel | string | undefined | The agent that carries the tunnel. Only that agent receives it in agent-config, and only its Chisel user is granted the port. Absent only on a legacy tunnel that could not be bound at upgrade (carried by no agent until an administrator assigns it). |
maxBodySizeMb | number | undefined | Largest request body in MiB (1–10240, default 10), rendered as client_max_body_size. Absent on panel tunnels and on tunnels created before the setting existed, which keep nginx's 1 MiB until reconfigured. |
createdAt | string | ISO 8601 timestamp |
Tunnels of type plugin also carry pluginName and pluginRoute.
Example:
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"subdomain": "myapp",
"fqdn": "myapp.example.com",
"port": 3000,
"description": "My web app",
"type": "app",
"accessMode": "restricted",
"agentLabel": "laptop",
"maxBodySizeMb": 10,
"enabled": true,
"createdAt": "2026-03-13T10:30:45.000Z"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"subdomain": "api",
"fqdn": "api.example.com",
"port": 8080,
"description": null,
"type": "app",
"accessMode": "authenticated",
"agentLabel": "build-server",
"maxBodySizeMb": 100,
"enabled": true,
"createdAt": "2026-03-13T11:00:00.000Z"
}
]Ownership reconciliation: On startup the panel binds tunnels without an agentLabel (written by versions before tunnel ownership): an agent-<label> panel tunnel only to the agent it names, when that agent is active; any other tunnel to the sole active machine agent when exactly one exists (plugin-agents are not counted). The rest stay unassigned and are logged, as are tunnels on a reserved port or sharing a port (granted to no one and left out of agent-config — delete and recreate them on a free port). It then rewrites chisel-users and restarts Chisel when the grants were narrowed (see Tunneling).
Revocation: revoking an agent certificate removes agentLabel from its app and plugin tunnels (unassigned, dark) and deletes its agent-<label> panel tunnel.
Concurrency: every workflow that rewrites this file holds the panel's single tunnel lock, validation included.
Write pattern: Atomic — writes to .tmp, calls fsync(), then rename().
State directory: Configurable via LAMALIBRE_LAMASTE_STATE_DIR environment variable, defaults to /etc/lamalibre/lamaste.
/etc/lamalibre/lamaste/sites.json
Stores the array of static sites hosted through Lamaste.
Schema: Array of site objects.
| Field | Type | Description |
|---|---|---|
id | string | UUID |
name | string | Site name (the subdomain for managed sites) |
fqdn | string | Full domain (e.g., blog.example.com) |
type | string | "managed" (subdomain of the Lamaste domain) or "custom" (your own domain) |
spaMode | boolean | If true, try_files falls back to /index.html |
autheliaProtected | boolean | If true, requires Authelia authentication |
allowedUsers | string[] | Authelia users allowed when protected (empty: all authenticated users) |
dnsVerified | boolean | DNS verified (always true for managed sites) |
certIssued | boolean | TLS certificate issued and vhost live |
aliases | string[] | undefined | Custom sites only: hostnames answered with a 301 to https://<fqdn>. One certificate lineage named after fqdn covers all |
rootPath | string | Document root (e.g., /var/www/lamaste/<id>/) |
createdAt | string | ISO 8601 timestamp |
Example:
[
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"fqdn": "blog.example.com",
"spaMode": false,
"autheliaProtected": false,
"rootPath": "/var/www/lamaste/c3d4e5f6-a7b8-9012-cdef-123456789012/",
"createdAt": "2026-03-13T12:00:00.000Z"
}
]Write pattern: Same as tunnels.json — atomic with fsync().
/etc/lamalibre/lamaste/chisel-credentials.json
Per-agent Chisel credentials, keyed by agent label. Written when an agent is enrolled, rotated, or revoked; agents fetch their own entry over the panel API.
{
"laptop": { "password": "<48 hex chars>", "createdAt": "2026-03-13T10:00:00.000Z" }
}Write pattern: Atomic — temp file with mode 0600, fsync(), then rename().
/etc/lamalibre/lamaste/chisel-users
The Chisel server's --authfile. Never edit by hand — the panel renders it from chisel-credentials.json and tunnels.json whenever either changes, and again on every panel start. Each agent-<label> user is granted exactly the reverse remotes of the enabled tunnels it owns:
{
"lamaste-no-grants:<sentinel password>": [],
"agent-laptop:<password>": ["^R:127\\.0\\.0\\.1:3000$"],
"agent-build-server:<password>": []
}The patterns are anchored because Chisel matches them unanchored. An agent with an empty list can connect but bind nothing. Reserved ports (3100, 9090, 9091, 9292, 9294) and a port claimed by two tunnels are never granted.
Ownership and mode: 0640, owner lamaste, group lamaste-chisel. The file holds every agent's password; Chisel runs as User=nobody, Group=lamaste-chisel and is the only reader besides the panel. The installer creates the group and adds lamaste to it; the panel's unit carries SupplementaryGroups=lamaste-chisel. The Chisel SSH host key, chisel-server.key, has the same owner, group and mode; the panel generates it at onboarding (earlier versions made it nobody:nogroup 0400; a redeploy converts it).
Write pattern: Written by the panel itself, no sudo: a 0600 temp file in /etc/lamalibre/lamaste/, chgrp lamaste-chisel, chmod 0640, fsync(), rename() — Chisel's watcher never sees the file with the wrong permissions. Renders are serialized by a mutex.
Reload: Chisel 1.12.0 reloads the file on every replace for new sessions, so additions need no restart. A change that revokes something (removed or rotated password, withdrawn grant) restarts Chisel with systemctl try-restart.
/etc/lamalibre/lamaste/chisel-failed-closed
Present only while startup reconciliation holds Chisel down: when the panel cannot establish a correct authfile it stops and disables chisel.service and writes this file (mode 0600, containing the time). The next successful reconciliation re-enables and starts Chisel and removes it. Do not delete it by hand to bring Chisel back — fix what the panel log names and restart the panel.
/etc/lamalibre/lamaste/chisel-sentinel
The random password of the lamaste-no-grants sentinel user. Chisel disables authentication entirely when its authfile holds no users; the sentinel keeps the file non-empty on a fresh server or one whose agents are all revoked. It is granted nothing and never handed out. Created on first render (mode 0600).
/etc/lamalibre/lamaste/ticket-scopes.json
Stores the ticket scope registry: registered scopes, active instances, and agent-to-instance assignments. Created automatically on first use.
Schema:
| Field | Type | Description |
|---|---|---|
scopes | array | Registered scope definitions (name, version, transport) |
instances | array | Active instances (scope, instanceId, agentLabel, status) |
assignments | array | Agent-to-instance assignments |
Example:
{
"scopes": [
{
"name": "shell",
"version": "1.0.0",
"description": "Remote shell access",
"scopes": [
{ "name": "shell:connect", "description": "Connect to shell", "instanceScoped": true }
],
"transport": {
"strategies": ["tunnel"],
"preferred": "tunnel",
"port": 9000,
"protocol": "wss"
},
"hooks": {}, // Reserved for future hook configuration
"installedAt": "2026-03-26T10:00:00.000Z"
}
],
"instances": [
{
"scope": "shell:connect",
"instanceId": "a7f3b2c9d1e2f3a4b5c6d7e8f9a0b1c2",
"agentLabel": "macbook-pro",
"registeredAt": "2026-03-26T10:05:00.000Z",
"lastHeartbeat": "2026-03-26T10:15:30.000Z",
"status": "active",
"transport": { "strategies": ["tunnel"], "preferred": "tunnel" }
}
],
"assignments": [
{
"agentLabel": "linux-agent",
"instanceScope": "shell:connect:a7f3b2c9d1e2f3a4b5c6d7e8f9a0b1c2",
"assignedAt": "2026-03-26T10:10:00.000Z",
"assignedBy": "admin"
}
]
}Instance transport sub-schema:
The instance transport object may include a direct sub-object when the direct strategy is listed:
| Field | Type | Required | Description |
|---|---|---|---|
transport.strategies | string[] | Yes | Array of "tunnel", "relay", "direct" |
transport.preferred | string | No | Preferred strategy (must be in strategies) |
transport.direct | object | No | Direct connection details (required when using direct strategy) |
transport.direct.host | string | Yes* | Public hostname or IP (1-255 chars). Private/reserved IPs rejected (SSRF prevention) |
transport.direct.port | number | Yes* | Port number (1024-65535) |
* Required when transport.direct is provided.
Host validation: The transport.direct.host field rejects private and reserved addresses: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, loopback (localhost, 127.0.0.1, ::1), cloud metadata endpoints (169.254.169.254, metadata.google.internal), and the zero network (0.0.0.0/8).
Write pattern: Atomic — temp file, fsync(), rename(). Concurrency controlled by promise-chain mutex.
/etc/lamalibre/lamaste/tickets.json
Stores active tickets and sessions for agent-to-agent authorization. Created automatically on first use.
Schema:
| Field | Type | Description |
|---|---|---|
tickets | array | Issued tickets (id, scope, instanceId, source, target, expiry) |
sessions | array | Active sessions (server-generated sessionId, ticketId, status, heartbeat) |
Example:
{
"tickets": [
{
"id": "64-hex-char-ticket-id",
"scope": "shell:connect",
"instanceId": "a7f3b2c9d1e2f3a4b5c6d7e8f9a0b1c2",
"source": "macbook-pro",
"target": "linux-agent",
"createdAt": "2026-03-26T10:15:00.000Z",
"expiresAt": "2026-03-26T10:15:30.000Z",
"used": false,
"usedAt": null,
"sessionId": null,
"transport": {}
}
],
"sessions": [
{
"sessionId": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8",
"ticketId": "64-hex-char-ticket-id",
"scope": "shell:connect",
"instanceId": "a7f3b2c9d1e2f3a4b5c6d7e8f9a0b1c2",
"source": "macbook-pro",
"target": "linux-agent",
"createdAt": "2026-03-26T10:15:30.000Z",
"lastActivityAt": "2026-03-26T10:20:00.000Z",
"status": "active",
"reconnectGraceSeconds": 60
}
]
}Write pattern: Same as ticket-scopes.json — atomic with mutex.
Cleanup: Tickets older than 1 hour are removed. Dead sessions older than 24 hours are removed.
/etc/lamalibre/lamaste/groups.json
Stores Lamaste group definitions and membership. Separate from Authelia groups — Lamaste groups are used exclusively for Gatekeeper access control without modifying users.yml.
Schema:
| Field | Type | Description |
|---|---|---|
groups | array | Array of group objects |
groups[].name | string | Group name (unique, lowercase alphanumeric, 2-63 chars) |
groups[].description | string | Human-readable description |
groups[].members | string[] | Array of Authelia usernames |
groups[].createdAt | string | ISO 8601 timestamp |
groups[].createdBy | string | Admin who created the group |
Example:
{
"groups": [
{
"name": "developers",
"description": "Backend and frontend developers",
"members": ["alice", "bob"],
"createdAt": "2026-04-01T10:00:00.000Z",
"createdBy": "admin"
}
]
}Write pattern: Atomic — temp file, fsync(), rename(). Concurrency controlled by promise-chain mutex.
/etc/lamalibre/lamaste/access-grants.json
Stores generic access grants mapping principals (users or groups) to resources (tunnel subdomains or other protected endpoints).
Schema:
| Field | Type | Description |
|---|---|---|
grants | array | Array of grant objects |
grants[].grantId | string | Unique identifier (e.g., g_abc123) |
grants[].principalType | string | "user" or "group" |
grants[].principalId | string | Authelia username or Lamaste group name |
grants[].resourceType | string | "tunnel", "plugin", or custom resource type |
grants[].resourceId | string | Resource identifier (e.g., tunnel subdomain) |
grants[].context | object | Optional metadata (empty {} by default) |
grants[].used | boolean | Whether the grant has been consumed |
grants[].createdAt | string | ISO 8601 timestamp |
grants[].usedAt | string | null | ISO 8601 timestamp of consumption, or null |
Example:
{
"grants": [
{
"grantId": "g_b2c3d4e5",
"principalType": "user",
"principalId": "alice",
"resourceType": "tunnel",
"resourceId": "myapp",
"context": {},
"used": false,
"createdAt": "2026-04-01T10:05:00.000Z",
"usedAt": null
},
{
"grantId": "g_c3d4e5f6",
"principalType": "group",
"principalId": "developers",
"resourceType": "tunnel",
"resourceId": "staging",
"context": {},
"used": false,
"createdAt": "2026-04-01T10:10:00.000Z",
"usedAt": null
}
]
}Write pattern: Atomic — temp file, fsync(), rename(). Concurrency controlled by promise-chain mutex.
/etc/lamalibre/lamaste/gatekeeper.json
Gatekeeper service settings. Controls cache behavior, admin contact information (displayed on the access-denied page), and logging configuration.
Schema:
| Field | Type | Default | Description |
|---|---|---|---|
adminEmail | string | undefined | — | Admin email shown on access-denied page |
adminName | string | undefined | — | Admin display name shown on access-denied page |
slackChannel | string | undefined | — | Slack channel for access request templates |
teamsChannel | string | undefined | — | Teams channel for access request templates |
sessionCacheTtlMs | number | undefined | — | In-memory session cache TTL in milliseconds (default 30000) |
accessLoggingEnabled | boolean | undefined | — | Whether to log denied access attempts |
accessLogRetentionDays | number | undefined | — | How many days to retain access log entries |
Example:
{
"adminEmail": "admin@example.com",
"adminName": "Admin",
"slackChannel": "#access-requests",
"sessionCacheTtlMs": 30000,
"accessLoggingEnabled": true,
"accessLogRetentionDays": 90
}Write pattern: Atomic — temp file, fsync(), rename().
/etc/lamalibre/lamaste/access-request-log.json
Optional log of denied access attempts. Written by the Gatekeeper when accessLoggingEnabled is true. Read by the Panel Server for the admin UI access request review page.
Schema: Array of access request entries.
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO 8601 timestamp of the denied request |
username | string | Authelia username of the denied user |
resourceType | string | Resource type (e.g., "tunnel", "plugin") |
resourceId | string | Resource identifier (e.g., tunnel subdomain) |
resourceFqdn | string | Full domain the user attempted to access |
Example:
[
{
"timestamp": "2026-04-01T14:30:00.000Z",
"username": "charlie",
"resourceType": "tunnel",
"resourceId": "internal",
"resourceFqdn": "internal.example.com"
}
]Write pattern: Atomic — temp file, fsync(), rename().
/etc/authelia/configuration.yml
Authelia server configuration. Written during onboarding provisioning.
Key fields:
server:
host: 127.0.0.1
port: 9091
log:
level: info
file_path: /var/log/authelia/authelia.log
jwt_secret: <random-secret>
authentication_backend:
file:
path: /etc/authelia/users.yml
password:
algorithm: bcrypt
bcrypt:
cost: 12
access_control:
default_policy: one_factor
session:
name: lamaste_session
secret: <random-secret>
domain: example.com
expiration: 12h
inactivity: 2h
storage:
encryption_key: <random-secret>
local:
path: /etc/authelia/db.sqlite3
notifier:
filesystem:
filename: /etc/authelia/notifications.txt
totp:
issuer: Lamaste
period: 30
digits: 6Critical settings:
| Setting | Value | Why |
|---|---|---|
server.host | 127.0.0.1 | Never bind to 0.0.0.0 — nginx handles public access |
password.algorithm | bcrypt | Argon2id uses ~93 MB per hash, causes OOM on 512 MB droplets |
bcrypt.cost | 12 | Balance between security and performance |
session.domain | Your domain | Must match the domain in panel.json |
Ownership and mode: lamaste:lamaste-authelia, 0640. The panel writes it without sudo (atomic temp file + rename in /etc/authelia, which is lamaste:lamaste-authelia 2770, setgid); Authelia, running as lamaste-authelia, reads it through its group. The database (db.sqlite3) and notifications.txt belong to lamaste-authelia (0600).
Do not edit this file directly unless you understand Authelia configuration. Changes require a service restart: sudo systemctl restart authelia.
/etc/authelia/users.yml
The user database. Authelia reads this file live (no restart needed for user changes, but Lamaste restarts Authelia after writes for safety).
Format:
users:
admin:
displayname: admin
password: $2b$12$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ012
email: admin@lamaste.local
groups:
- admins
alice:
displayname: alice
password: $2b$12$xyzdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ012
email: alice@lamaste.local
groups:
- adminsPassword format: bcrypt hash (starts with $2b$12$). Never use argon2id on a 512 MB droplet.
Write pattern: Written by the panel without sudo — a temp file in /etc/authelia, fsync(), rename() — mode 0660, group lamaste-authelia (Authelia rewrites the file in place when a user changes a password). After writing, Authelia is restarted.
Warning: Never delete the last user. The panel server prevents this, but manual editing could bypass the guard.
/etc/authelia/.secrets.json
Stores the randomly generated secrets used in the Authelia configuration. Backed up here so they can be referenced if the configuration file is regenerated.
{
"jwtSecret": "<64-char-random-string>",
"sessionSecret": "<64-char-random-string>",
"storageEncryptionKey": "<64-char-random-string>",
"oidcHmacSecret": "<64-char-random-string>"
}Mode: 0600, owner lamaste (the panel only; Authelia reads the secrets from its configuration). If these values change, all active sessions are invalidated. oidcHmacSecret appears once an OpenID Connect client exists.
/etc/authelia/oidc-issuer.pem
The RSA-4096 private key (PKCS#8) Authelia signs ID and JWT access tokens with. Generated by create-lamaste — on a fresh install and on every redeploy that finds it missing — in a child process running as the lamaste-authelia account, so root never writes through /etc/authelia (a directory the panel owns). The panel references it from configuration.yml as key: {{ secret "/etc/authelia/oidc-issuer.pem" | mindent 10 "|" | msquote }}; Authelia expands the expression because its unit sets X_AUTHELIA_CONFIG_FILTERS=template.
Mode: 0600, owner lamaste-authelia. The panel cannot read it. If it is missing, the panel refuses to write an OIDC section and says to run the installer again.
/etc/lamalibre/lamaste/registry-credential.json
The panel's service credential on the Lamalibre npm registry (Feria), used to broker one-hour install tokens for agents (GET /api/agents/me/registry-credential) and to revoke a user's tokens when a feria-* group is lost.
{
"provider": "feria",
"registryUrl": "https://feria.example.com",
"scope": "@lamalibre",
"auth": { "kind": "oidc", "clientId": "lamaste-server" },
"owner": "ci:auth.example.com/lamaste-server",
"configuredAt": "2026-10-13T10:00:00.000Z"
}auth is { "kind": "token", "token": "feria_…" } for a pasted long-lived token, or { "kind": "oidc", "clientId": "lamaste-server" } when the panel authenticates as its own Authelia client (client-credentials grant, Feria login; nothing long-lived stored but the client secret in oidc-clients.json). Files written by 4.0 (token at the top level) are read as auth.kind: token. Set with lamaste-server registry-credential set --url <registry> (--oidc | --token-stdin); never shown back.
Mode: 0600, owner lamaste.
/etc/lamalibre/lamaste/oidc-clients.json
Secrets of confidential OIDC clients — those plugins declare with confidential: true, and the panel's own lamaste-server. Authelia's configuration carries only pbkdf2 digests; the salt is stored with the secret so the digest is the same on every render (the panel compares the rendered section to decide whether Authelia must restart).
{
"version": 1,
"clients": {
"lamaste-server": {
"plugin": "lamaste",
"secret": "<43-char-base64url>",
"salt": "<adapted-base64>",
"createdAt": "2026-10-13T10:00:00.000Z",
"rotatedAt": null
}
}
}A secret is minted when the client first appears (plugin install, or the registry credential set to oidc), shown once in that response, rotated with POST /api/plugins/:name/oidc-clients/:clientId/rotate-secret, and deleted when the client is gone (plugin uninstalled, credential cleared).
Mode: 0600, owner lamaste.
/etc/lamalibre/lamaste/relay-signing-key.json
The Ed25519 key the relay signs relay assertions with — the X-Lamalibre-Relay-Assertion header lamaste-agentd requires on every request from the relay (agent-panel vhost and plugin tunnels; see Security Model).
{
"v": 1,
"privateKey": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"createdAt": "2026-10-15T10:00:00.000Z"
}The panel creates it at startup when it is missing (exclusively: a 0600 temp file linked into place), logs its id (Relay assertion signing key ready, kid) and signs agent-panel requests with it — and every app job it creates for an agent's root helper (App Host); Gatekeeper reads it and signs for plugin tunnels. Both re-read it when it changes — content or permissions — and refuse to sign with a key other users can read. Agents receive only the public key (GET /api/agents/me/relay-keys); its id is derived from the key. While it is unusable no app job is created.
Rotation: replace the file (or remove it and restart lamalibre-lamaste-serverd, which creates a new one). Agents fetch the new public key when the first assertion signed with it arrives, at most every 30 seconds — requests through the relay may be refused for up to that long after a rotation. The app host helpers do not: they keep the key their operator pinned, refuse the relay's app jobs until sudo lamaste-agent apphost pin runs on each agent machine, and the panel shows those agents as not eligible to host an agent half meanwhile.
Mode: 0600, owner lamaste.
Client-Side Configuration
~/.lamalibre/lamaste/servers.json
Stores the server registry for the desktop app's multi-server support. Created by cloud provisioning or manual server addition. When this file exists and contains an active entry, load_effective_config() uses it instead of agent.json.
Schema: Array of server entry objects.
| Field | Type | Description |
|---|---|---|
id | string | UUID |
label | string | Display name (typically the domain) |
panelUrl | string | Panel URL (e.g., https://203.0.113.42:9292) |
ip | string | Server IP address |
provider | string | null | Cloud provider name (e.g., digitalocean) |
providerId | string | null | Provider-specific resource ID (e.g., droplet ID) |
region | string | null | Provider region slug |
createdAt | string | ISO 8601 timestamp |
active | boolean | Whether this is the currently active server |
authMethod | string | "p12" or "keychain" |
keychainIdentity | string | null | Keychain identity name (when authMethod is "keychain") |
p12Path | string | null | Path to P12 file (when authMethod is "p12") |
activeMode | string | "agent" or "admin" — which UI mode the desktop app shows for this server. Defaults to "agent" |
adminAuth | object | null | Admin certificate details for Server mode access (see sub-fields below) |
adminAuth.method | string | "p12" or "keychain" — how the admin cert is stored |
adminAuth.p12Path | string | null | Path to admin P12 file (when method is "p12") |
adminAuth.keychainIdentity | string | null | Keychain identity for admin cert (when method is "keychain") |
Example:
[
{
"id": "d4e5f6a7-b8c9-0123-defg-456789012345",
"label": "example.com",
"panelUrl": "https://203.0.113.42:9292",
"ip": "203.0.113.42",
"provider": "digitalocean",
"providerId": "123456789",
"region": "fra1",
"createdAt": "2026-03-27T10:00:00.000Z",
"active": true,
"authMethod": "p12",
"p12Path": "/Users/admin/.lamaste/servers/d4e5f6a7/client.p12",
"keychainIdentity": null,
"activeMode": "admin",
"adminAuth": {
"method": "p12",
"p12Path": "/Users/admin/.lamaste/servers/d4e5f6a7/admin.p12",
"keychainIdentity": null
}
}
]Notes on adminAuth: When adminAuth is present and valid, the desktop app shows the Agents/Servers mode toggle in the sidebar. Cloud-provisioned servers populate adminAuth automatically (the admin certificate is downloaded during provisioning). For manually added servers, the user must import an admin certificate to enable Server mode.
P12 password: Not stored in the JSON file. Retrieved from the OS credential store (com.lamalibre.lamaste.server service, keyed by server UUID).
Cloud API token: Not stored in this file. Retrieved from the OS credential store (com.lamalibre.cloud service).
Write pattern: Atomic — temp file with mode 0600, fsync(), then rename().
Config resolution: load_effective_config() checks agents.json first (multi-agent registry), then servers.json (active entry), then falls back to agent.json (legacy).
~/.lamalibre/lamaste/agents.json
Multi-agent registry. Created by lamaste-agent setup. Tracks all configured agents and the current default.
{
"version": 1,
"currentLabel": "my-server",
"agents": [
{
"label": "my-server",
"panelUrl": "https://1.2.3.4:9292",
"authMethod": "p12",
"p12Path": "~/.lamalibre/lamaste/agents/my-server/client.p12",
"keychainIdentity": null,
"agentLabel": "agent:my-machine",
"domain": "example.com",
"chiselVersion": "1.12.0",
"setupAt": "2026-03-28T10:00:00.000Z",
"updatedAt": null
}
]
}Write pattern: Atomic — temp file with mode 0600, fsync(), then rename().
Per-agent data is stored at ~/.lamalibre/lamaste/agents/<label>/:
The directory itself is mode 0700 (re-applied on every sync).
config.json— agent configuration (mode 0600):panelUrl,authMethod, credentials, the enrolleddomain(the agent refuses a panel that reports another one),chiselVersion, the panel TLS pin (panelServerPubkeySha256,panelServerCertSha256Hex,panelServerCertPinnedAt), and:chiselCredentialSealedAt— when the agent last received a Chisel credential that has only ever lived in 0600 files. Set at setup. Absent on agents set up by versions that passed the credential in process arguments and 0644 unit files; their next sync rotates the credential once (POST /api/agents/me/chisel-credential/rotate) and then sets ittunnelsStopped—truewhen the operator stopped the tunnels (agent panel or desktop app); sync keeps the Chisel client stopped. Start and restart remove it
client.p12— mTLS certificate (mode 0600)ca.crt— CA certificate (mode 0644)chisel.json— the agent's Chisel credential{ "user": "agent-<label>", "password": "...", "fetchedAt": "...", "issuedAt": "..." }(mode 0600).issuedAtis when the panel issued the password; when the panel'schiselCredentialIssuedAtdiffers, sync fetches the current credential. Source of truth: every rewrite of the service re-applies it.lamaste-agent chisel refresh-credentialrefreshes it immediatelychisel.env— Linux only:AUTH=agent-<label>:<password>, read by the systemd unit throughEnvironmentFile=(mode 0600)sync-state.json— outcome of the lastlamaste-agent sync(mode 0600):lastRunAt,lastOkAt,lastError(null after a success),lastState(running,idle— no tunnels, client stopped — orstoppedby the operator),tunnels,lastWarnings. Shown on the Sync line oflamaste-agent status(flagged whenlastRunAtis over 2 minutes old), and used to log only changes and new warningsagent.lock— present while an operation (setup, update, sync, the agent panel or daemon) changes this agent's service orconfig.json; holds the owner's PID, boot id and start time. A stale lock (dead process, earlier boot, older than 15 minutes) is taken overlogs/chisel.log— Chisel stdout loglogs/chisel.error.log— Chisel stderr loglogs/sync.log— sync timer log: only runs that changed something, and each error when it first appears and when it clearsplugins.json— agent plugin registryplugins/— per-plugin data directoriesapp-plugins/<route>.json— the port and installation secret of a plugin hosted on this agent (mode 0600):{ plugin, port, secret, issuedAt, previousSecret?, previousIssuedAt?, fetchedAt, acknowledged }apphost-state.json— the agent halves the app host helper installed for this agent (mode 0600):{ "apps": { "<route>": { "version": "1.0.0", "updatedAt": "…" } } }. Written by the sync's app jobs; read by converge, which hands a fetched plugin config to the service of a route listed here before acknowledging it. When it cannot be read the sync rebuilds it from the helper's owner records naming this labelhost-facts.json— what was last reported as host facts and the helper's last answer (mode 0600):{ "reported": { "hash", "at" } | null, "apphost": { "helper": "<dev:ino:size:mtime>", "facts": { "version", "node" } | null, "at" } | null }. Facts are reported when their hash changes or a day passed; the helper is asked again when its file changes or a day passedapp-job-results.json— results of app jobs the relay has not taken yet (mode 0600):{ "<jobId>": { "result", "deadlineAt" } }— a signed job runs once, so a report that failed is made again from here, until the job's deadline
An install job packs the agent package in ~/.lamalibre/lamaste/tmp/agent/apphost-*/ (0700, with a throw-away npm userconfig holding the brokered registry token) and deletes it afterwards.
Service files use per-agent names:
- macOS:
com.lamalibre.lamaste.chisel-<label>(plist label),~/Library/LaunchAgents/com.lamalibre.lamaste.chisel-<label>.plist— mode 0600, because it carriesAUTHinEnvironmentVariables - Linux:
lamalibre-lamaste-chisel-<label>(unit name),~/.config/systemd/user/lamalibre-lamaste-chisel-<label>.service— a systemd user unit withEnvironmentFile=pointing atchisel.env; enable lingering (sudo loginctl enable-linger <user>) for it to start at boot without a login
Both run chisel client --max-retry-interval 30s https://tunnel.<domain>:443 R:127.0.0.1:<port>:127.0.0.1:<port> ... with TLS verification and no credential in argv. They are generated by @lamalibre/lamaste/agent (chisel-service.ts) and rewritten by lamaste-agent setup, update and sync — do not edit them by hand. An agent carrying no tunnels has its service unloaded (Chisel cannot run without a remote).
The sync timer, which runs <node> <installed lamaste-agent> sync --label <label> --quiet (<node> is a stable path such as /opt/homebrew/bin/node when one on PATH resolves to the running binary, so a Node.js upgrade does not break it):
- macOS:
com.lamalibre.lamaste.sync-<label>(plist label),~/Library/LaunchAgents/com.lamalibre.lamaste.sync-<label>.plist—StartInterval30,RunAtLoad - Linux:
~/.config/systemd/user/lamalibre-lamaste-sync-<label>.service(Type=oneshot,TimeoutStartSec=20min— converge and the relay's jobs, the longest of which installs an agent half in up to 10 minutes) andlamalibre-lamaste-sync-<label>.timer(OnActiveSec=5s,OnUnitInactiveSec=30s) — also needs lingering to run without a login
Both contain no secret (mode 0644) and are removed by lamaste-agent uninstall and by the desktop app's uninstall.
The agent panel daemon (lamaste-agentd, started by lamaste-agent panel --enable):
- macOS:
com.lamalibre.lamaste.panel-<label>(plist label),~/Library/LaunchAgents/com.lamalibre.lamaste.panel-<label>.plist—RunAtLoad,KeepAlive - Linux:
~/.config/systemd/user/lamalibre-lamaste-panel-<label>.service, enabled intodefault.target— needs lingering to run without a login. Units written before 4.3 namedmulti-user.target, which a user manager does not have, so the panel never started at boot;lamaste-agent updateandlamaste-agent panel --enablerewrite that line and re-enable an enabled unit
~/.lamalibre/lamaste/agent.json
Legacy single-server configuration. Automatically migrated to the multi-agent registry on first use. After migration, renamed to agent.json.backup.
Agent machine: the app host
On a Linux agent where the operator ran sudo lamaste-agent apphost enable, the root helper lamaste-apphost installs the agent halves of the plugins the panel hosts there (App Host). Every path below is derived from a validated route or agent label; root writes them atomically (temp file, fsync, rename) and refuses to use one that is not what it expects — a directory another user can write, a link where a file belongs.
| Path | Owner | Mode | Contents |
|---|---|---|---|
/usr/local/sbin/lamaste-apphost | root:root | 0755 | The helper — copied by enable from a root-owned lamaste-agent install |
/etc/sudoers.d/lamaste-apphost | root:root | 0440 | <user> ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-apphost, with env_reset, !setenv and Node.js variables deleted; checked with visudo -c before it is renamed into place. None when the agent runs as root |
/etc/lamalibre/apphost/relays/<label>.json | root:root | 0644 (dirs 0755) | The relay key pinned for agent <label>: { v, label, agent, kid, publicKey, panelUrl, pinnedAt } — public material |
/opt/lamalibre/apps/<route>/<version>/ | root:root | dirs 0755, files 0644 | The package as published (package/) and apphost.json, the spec it was installed with |
/opt/lamalibre/apps/<route>/current, previous | root:root | symlinks | The running version and the one before it; other versions are deleted |
/opt/lamalibre/apps/.stage-<hex>-<route>/ | root:root | 0700 | Unpacking while an install runs |
/var/lib/lamalibre/apps/<route>/ | the agent half's account | 0700 | The agent half's data (LAMASTE_APP_DATA_DIR) — kept on removal, deleted by a purge |
/var/lib/lamalibre/apps/.owners/<route>.json | root:root | 0644 (dir 0755) | Who owns the route on this machine: { route, package, label, since } — kept on removal, deleted by a purge |
/var/lib/lamalibre/apps/.jobs/used.json | root:root | 0600 (dir 0700) | { v: 1, jobs: { <jobId>: <expiry, ms> } } — the signed jobs that ran, each until it expires |
/var/lib/lamalibre/apps/.trash/ | root:root | 0700 | Purged data, deleted there |
/etc/lamalibre/apps/<route>/ | root:<the account's group> | 0750 | The agent half's configuration directory |
/etc/lamalibre/apps/<route>/plugin-config.json | the agent half's account | 0600 | Port and installation secret (LAMASTE_APP_CONFIG), the shape of the agent's app-plugins/<route>.json |
/etc/systemd/system/lamalibre-app-<route>.service | root:root | 0644 | The agent half's unit, from a fixed template — paths, never secrets |
/run/lamaste-apphost.lock | root:root | 0600 | Held (with the owner's PID) while a mutating helper command runs |
The agent half's account and group are lamaste-app-<route> for routes of up to 18 characters, lamaste-app-<first 2 characters>-<16 hex digits of SHA-256(route)> otherwise; the account's comment is lamaste-app route=<route>. /opt/lamalibre, /opt/lamalibre/apps, /var/lib/lamalibre, /var/lib/lamalibre/apps, /etc/lamalibre and /etc/lamalibre/apps are root-owned 0755.
sudo lamaste-agent apphost disable removes the rule, the helper and the pins (and /etc/lamalibre/apphost when it is empty). Removing an agent half deletes its unit, code and configuration directory; a purge also deletes its data directory, account and owner record. System packages are never removed.
nginx Vhost Patterns
/etc/nginx/sites-available/lamalibre-lamaste-panel-ip
The IP-based panel vhost. Created by the installer. Active as a fallback unless panel 2FA is enabled, which disables it (domain-only access).
# Rate limit zone for public enrollment endpoint (5 requests/minute per IP)
limit_req_zone $binary_remote_addr zone=enroll:1m rate=5r/m;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 9292 ssl;
server_name _;
ssl_certificate /etc/lamalibre/lamaste/pki/self-signed.pem;
ssl_certificate_key /etc/lamalibre/lamaste/pki/self-signed-key.pem;
include /etc/nginx/snippets/lamalibre-lamaste-mtls.conf;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
error_page 495 496 /cert-help.html;
location = /cert-help.html {
root /opt/lamalibre/lamaste/lamaste-server-ui;
internal;
}
# Protected locations — reject if client cert missing or invalid
location / {
if ($ssl_client_verify != SUCCESS) {
return 496;
}
proxy_pass http://127.0.0.1:3100;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Public API paths — no mTLS check, cert headers cleared
location /api/enroll {
limit_req zone=enroll burst=5 nodelay;
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header X-SSL-Client-Verify "";
proxy_set_header X-SSL-Client-DN "";
proxy_set_header X-SSL-Client-Serial "";
# ... standard proxy headers
}
location /api/invite {
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header X-SSL-Client-Verify "";
proxy_set_header X-SSL-Client-DN "";
proxy_set_header X-SSL-Client-Serial "";
# ... standard proxy headers
}
# API paths with WebSocket upgrade support (mTLS required)
location /api {
if ($ssl_client_verify != SUCCESS) {
return 496;
}
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}/etc/nginx/sites-available/lamalibre-lamaste-panel-domain
The domain-based panel vhost. Created during onboarding provisioning. Uses Let's Encrypt certificates and mTLS.
server {
listen 443 ssl;
server_name panel.example.com;
ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
include /etc/nginx/snippets/lamalibre-lamaste-mtls.conf;
# ... (same proxy headers and locations as IP vhost)
}/etc/nginx/sites-available/lamalibre-lamaste-auth
The Authelia authentication portal. Proxies to 127.0.0.1:9091.
/etc/nginx/sites-available/lamalibre-lamaste-tunnel
The Chisel WebSocket tunnel endpoint. Proxies to 127.0.0.1:9090 with WebSocket upgrade and 24-hour timeout.
/etc/nginx/sites-available/lamalibre-lamaste-app-<subdomain>
Per-tunnel vhosts. restricted and authenticated tunnels authorize through Gatekeeper (auth_request to 127.0.0.1:9294), public tunnels proxy directly. Each proxies to the tunnel's port with WebSocket support and sets client_max_body_size from the tunnel's maxBodySizeMb (no directive — nginx's 1 MiB — on legacy tunnels without the field).
/etc/nginx/sites-available/lamalibre-lamaste-site-<uuid>
Per-static-site vhosts. Serve files from /var/www/lamaste/<uuid>/ with optional Authelia protection and SPA mode. A custom site with aliases gets an extra server block answering those hostnames with a 301 to https://<fqdn>, on the same certificate.
/etc/nginx/snippets/lamalibre-lamaste-mtls.conf
The mTLS configuration snippet included by all panel vhosts:
ssl_client_certificate /etc/lamalibre/lamaste/pki/ca.crt;
ssl_verify_client optional;This enables client certificate verification at the TLS level. The optional setting allows connections without a certificate (needed for public endpoints like /api/enroll and /api/invite). Protected locations enforce mTLS via if ($ssl_client_verify != SUCCESS) { return 496; } in each vhost's location blocks.
/etc/nginx/snippets/lamalibre-lamaste-authz-cache.conf
Defines the nginx proxy_cache zone installed by the Gatekeeper installer task:
proxy_cache_path /var/cache/nginx/authz levels=1:2 keys_zone=lamaste_authz:1m max_size=10m inactive=5m;This cache zone is available for nginx-level caching of Gatekeeper authorization responses. However, the generated vhosts (buildGatekeeperVhost()) do not currently include proxy_cache directives. Authorization caching relies on the Gatekeeper's in-memory session cache (30-second default TTL).
/etc/systemd/system/lamalibre-lamaste-gatekeeper.service
Systemd unit for the Gatekeeper tunnel authorization service. Runs as the lamaste user on 127.0.0.1:9294.
[Unit]
Description=Lamaste Gatekeeper
After=network.target
[Service]
Type=simple
User=lamaste
Group=lamaste
WorkingDirectory=/opt/lamalibre/lamaste/gatekeeper
ExecStart=/usr/bin/node src/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetFile Permissions Table
| Path | Owner | Mode | Notes |
|---|---|---|---|
/etc/lamalibre/lamaste/ | lamaste:lamaste | 0755 | State directory |
/etc/lamalibre/lamaste/panel.json | lamaste:lamaste | 0600 | Panel config |
/etc/lamalibre/lamaste/tunnels.json | lamaste:lamaste | 0600 | Tunnel state |
/etc/lamalibre/lamaste/sites.json | lamaste:lamaste | 0600 | Site state |
/etc/lamalibre/lamaste/chisel-credentials.json | lamaste:lamaste | 0600 | Chisel credentials |
/etc/lamalibre/lamaste/chisel-users | lamaste:lamaste-chisel | 0640 | Chisel authfile |
/etc/lamalibre/lamaste/chisel-sentinel | lamaste:lamaste | 0600 | Sentinel password |
/etc/lamalibre/lamaste/relay-signing-key.json | lamaste:lamaste | 0600 | Relay assertion signing key |
/etc/lamalibre/lamaste/pki/ | lamaste:lamaste | 0700 | PKI directory |
/etc/lamalibre/lamaste/pki/ca.key | lamaste:lamaste | 0600 | CA private key |
/etc/lamalibre/lamaste/pki/ca.crt | lamaste:lamaste | 0644 | CA certificate |
/etc/lamalibre/lamaste/pki/client.key | lamaste:lamaste | 0600 | Client private key |
/etc/lamalibre/lamaste/pki/client.crt | lamaste:lamaste | 0644 | Client certificate |
/etc/lamalibre/lamaste/pki/client.p12 | lamaste:lamaste | 0600 | PKCS12 bundle |
/etc/lamalibre/lamaste/pki/.p12-password | lamaste:lamaste | 0600 | PKCS12 password |
/etc/lamalibre/lamaste/pki/self-signed.pem | lamaste:lamaste | 0644 | Self-signed TLS cert |
/etc/lamalibre/lamaste/pki/self-signed-key.pem | lamaste:lamaste | 0600 | Self-signed TLS key |
/etc/lamalibre/lamaste/ticket-scopes.json | lamaste:lamaste | 0600 | Ticket scope registry |
/etc/lamalibre/lamaste/tickets.json | lamaste:lamaste | 0600 | Ticket/session store |
/etc/lamalibre/lamaste/invitations.json | lamaste:lamaste | 0600 | Pending invitations |
/etc/lamalibre/lamaste/plugins.json | lamaste:lamaste | 0600 | Plugin registry |
/etc/lamalibre/lamaste/storage-config.json | lamaste:lamaste | 0600 | Storage registry |
/etc/lamalibre/lamaste/storage-master.key | lamaste:lamaste | 0600 | Storage encryption key |
/etc/lamalibre/lamaste/groups.json | lamaste:lamaste | 0600 | Lamaste groups |
/etc/lamalibre/lamaste/access-grants.json | lamaste:lamaste | 0600 | Access grants |
/etc/lamalibre/lamaste/gatekeeper.json | lamaste:lamaste | 0600 | Gatekeeper settings |
/etc/lamalibre/lamaste/access-request-log.json | lamaste:lamaste | 0600 | Denied access log |
/etc/lamalibre/lamaste/pki/enrollment-tokens.json | lamaste:lamaste | 0600 | Enrollment tokens |
/etc/lamalibre/lamaste/pki/revoked.json | lamaste:lamaste | 0600 | Revocation list |
/etc/lamalibre/lamaste/pki/agents/registry.json | lamaste:lamaste | 0600 | Agent cert metadata |
/etc/lamalibre/lamaste/chisel-server.key | lamaste:lamaste-chisel | 0640 | Chisel SSH host key |
/etc/authelia/ | lamaste:lamaste-authelia | 2770 | Authelia config (setgid) |
/etc/authelia/configuration.yml | lamaste:lamaste-authelia | 0640 | Auth config |
/etc/authelia/users.yml | lamaste:lamaste-authelia | 0660 | User database |
/etc/authelia/.secrets.json | lamaste:lamaste-authelia | 0600 | Auth secrets (panel only) |
/etc/authelia/db.sqlite3, notifications.txt | lamaste-authelia:lamaste-authelia | 0600 | Authelia's own files |
/var/log/authelia/ | lamaste-authelia:lamaste-authelia | 0750 | Authelia log |
/opt/lamalibre/lamaste/ | root:root | 0755 | Install directory (read-only for lamaste) |
/opt/lamalibre/lamaste/hooks/<route>/<sha256>.mjs | root:root | 0644 | Approved relay hook module (read by the sandboxed runner) |
/run/lamalibre-lamaste-hooks/<route>.sock | root:lamaste | 0660 | Relay hook runner socket (systemd) |
/var/www/lamaste/ | lamaste:www-data | 2750 | Static site files (files 0640; nginx reads via the group) |
/etc/sudoers.d/lamaste | root:root | 0440 | Sudo rules (no wildcards) |
/usr/local/sbin/lamaste-priv, lamaste-certbot, lamaste-cert-info | root:root | 0755 | Validating sudo programs |
/usr/local/bin/chisel, /usr/local/bin/authelia | root:root | 0755 | Pinned releases (create-lamaste) |
~/.lamalibre/lamaste/servers.json | user | 0600 | Server registry |
~/.lamalibre/lamaste/agent.json | user | 0600 | Legacy agent config |
~/.lamalibre/lamaste/agentd.token | user | 0600 | Agent daemon owner token |
~/.lamalibre/lamaste/agents/<label>/ | user | 0700 | Agent data directory |
~/.lamalibre/lamaste/agents/<label>/chisel.json | user | 0600 | Agent Chisel credential |
~/.lamalibre/lamaste/agents/<label>/chisel.env | user | 0600 | Linux AUTH= env file |
~/Library/LaunchAgents/com.lamalibre.lamaste.chisel-<label>.plist | user | 0600 | macOS service (has AUTH) |
~/.lamalibre/lamaste/services.json | user | 0600 | Service registry |
~/.lamalibre/lamaste/agents/<label>/apphost-state.json, host-facts.json, app-job-results.json | user | 0600 | App host record, host facts, unreported app job results |
/usr/local/sbin/lamaste-apphost | root:root | 0755 | App host helper (agent machine) |
/etc/sudoers.d/lamaste-apphost | root:root | 0440 | App host rule (agent machine) |
/etc/lamalibre/apphost/relays/<label>.json | root:root | 0644 | Pinned relay key (agent machine) |
/opt/lamalibre/apps/<route>/ | root:root | 0755 | Agent half code, files 0644 (agent machine) |
/var/lib/lamalibre/apps/<route>/ | lamaste-app-<route> | 0700 | Agent half data (agent machine) |
/var/lib/lamalibre/apps/.owners/<route>.json | root:root | 0644 | Owner record (agent machine) |
/var/lib/lamalibre/apps/.jobs/, .trash/ | root:root | 0700 | Signed jobs run; purged data (agent machine) |
/etc/lamalibre/apps/<route>/ | root:lamaste-app-<route> | 0750 | Agent half config directory (agent machine) |
/etc/lamalibre/apps/<route>/plugin-config.json | lamaste-app-<route> | 0600 | Agent half port and secret (agent machine) |
/etc/systemd/system/lamalibre-app-<route>.service | root:root | 0644 | Agent half unit (agent machine) |
Quick Reference
| Config File | Read By | Modified By | Restart Needed? |
|---|---|---|---|
panel.json | lamalibre-lamaste-serverd | lamalibre-lamaste-serverd (atomic write) | No (hot reload) |
tunnels.json | lamalibre-lamaste-serverd | lamalibre-lamaste-serverd (atomic write + fsync) | No |
sites.json | lamalibre-lamaste-serverd | lamalibre-lamaste-serverd (atomic write + fsync) | No |
chisel-users | chisel | lamalibre-lamaste-serverd (rendered, atomic rename, no sudo) | No for additions (hot reload); the panel restarts chisel on revocations |
ticket-scopes.json | lamalibre-lamaste-serverd | lamalibre-lamaste-serverd (atomic write + mutex) | No |
tickets.json | lamalibre-lamaste-serverd | lamalibre-lamaste-serverd (atomic write + mutex) | No |
storage-config.json | lamalibre-lamaste-serverd | lamalibre-lamaste-serverd (atomic write + mutex) | No |
groups.json | gatekeeper | lamalibre-lamaste-serverd (atomic write + mutex) | No (gatekeeper watches file) |
access-grants.json | gatekeeper | lamalibre-lamaste-serverd (atomic write + mutex) | No (gatekeeper watches file) |
gatekeeper.json | gatekeeper | lamalibre-lamaste-serverd (atomic write) | Yes (systemctl restart lamalibre-lamaste-gatekeeper) |
access-request-log.json | lamalibre-lamaste-serverd | gatekeeper (atomic write) | No |
configuration.yml | authelia | onboarding provisioning | Yes (systemctl restart authelia) |
users.yml | authelia | lamalibre-lamaste-serverd (atomic rename, no sudo) | Yes (systemctl restart authelia) |
lamalibre-lamaste-* vhosts | nginx | lamalibre-lamaste-serverd (via sudo lamaste-priv nginx-site) | Yes (nginx -t && systemctl reload nginx) |