Skip to content

Config Files ​

Complete reference for every configuration and state file Lamaste uses.

File Overview ​

FileFormatOwnerModePurpose
/etc/lamalibre/lamaste/panel.jsonJSONlamaste:lamaste0600Panel server configuration
/etc/lamalibre/lamaste/tunnels.jsonJSONlamaste:lamaste0600Tunnel definitions
/etc/lamalibre/lamaste/sites.jsonJSONlamaste:lamaste0600Static site definitions
/etc/lamalibre/lamaste/chisel-credentials.jsonJSONlamaste:lamaste0600Per-agent Chisel credentials (source of truth for the authfile)
/etc/lamalibre/lamaste/chisel-usersJSONlamaste:lamaste-chisel0640Chisel --authfile: per-agent credentials and port grants (rendered)
/etc/lamalibre/lamaste/chisel-sentineltextlamaste:lamaste0600Password of the lamaste-no-grants sentinel user
/etc/lamalibre/lamaste/chisel-server.keyPEMlamaste:lamaste-chisel0640Chisel SSH host key (--keyfile), generated by the panel at onboarding
/etc/authelia/configuration.ymlYAMLlamaste:lamaste-authelia0640Authelia server configuration
/etc/authelia/users.ymlYAMLlamaste:lamaste-authelia0660User database (Authelia rewrites it on a password change)
/etc/authelia/.secrets.jsonJSONlamaste:lamaste-authelia0600Authelia secrets (panel only)
/etc/authelia/oidc-issuer.pemPEMlamaste-authelia0600RSA-4096 key Authelia signs OIDC tokens with, written by create-lamaste; the panel cannot read it
/etc/lamalibre/lamaste/registry-credential.jsonJSONlamaste:lamaste0600The panel's registry (Feria) credential: a pasted token, or the lamaste-server OIDC client
/etc/lamalibre/lamaste/oidc-clients.jsonJSONlamaste:lamaste0600Plaintext secrets (and pbkdf2 salts) of confidential OIDC clients
/etc/lamalibre/lamaste/relay-signing-key.jsonJSONlamaste:lamaste0600Ed25519 key relay assertions (panel, Gatekeeper) and agents' app jobs (panel) are signed with
/etc/lamalibre/lamaste/push-install-config.jsonJSONlamaste:lamaste0600Push install: the switch, the policies, the default policy
/etc/lamalibre/lamaste/push-install-sessions.jsonJSONlamaste:lamaste0600Push install log (last 500 pushes)
/etc/lamalibre/lamaste/ticket-scopes.jsonJSONlamaste:lamaste0600Ticket scope registry
/etc/lamalibre/lamaste/tickets.jsonJSONlamaste:lamaste0600Ticket and session store
/etc/lamalibre/lamaste/invitations.jsonJSONlamaste:lamaste0600Pending user invitations
/etc/lamalibre/lamaste/plugins.jsonJSONlamaste:lamaste0600Plugin registry (before 4.2; the registry is now in state.db)
/etc/lamalibre/lamaste/state.dbSQLitelamaste:lamaste0600Plugins (with their hooks), agents, tickets, enrollment tokens, the audit log, notifications, relay hook approvals, plugin operations, agent jobs
/etc/lamalibre/lamaste/plugin-hostings.jsonJSONlamaste:lamaste0600Hostings of agent-hosted plugins: agent, port, phase, installation secrets, health
/opt/lamalibre/lamaste/hooks/<route>/directoryroot:root0755A plugin's approved relay hook module (<sha256>.mjs, 0644) and runner.env, written by lamaste-priv
/etc/lamalibre/lamaste/storage-config.jsonJSONlamaste:lamaste0600Storage server registry and plugin bindings (credentials AES-256-GCM encrypted)
/etc/lamalibre/lamaste/storage-master.keyBinarylamaste:lamaste060032-byte master key for storage credential encryption
/etc/lamalibre/lamaste/groups.jsonJSONlamaste:lamaste0600Lamaste group definitions and membership
/etc/lamalibre/lamaste/access-grants.jsonJSONlamaste:lamaste0600Generic access grants (principal → resource)
/etc/lamalibre/lamaste/gatekeeper.jsonJSONlamaste:lamaste0600Gatekeeper settings (cache TTL, admin contact, logging)
/etc/lamalibre/lamaste/access-request-log.jsonJSONlamaste:lamaste0600Optional denied access log
/etc/lamalibre/lamaste/pki/enrollment-tokens.jsonJSONlamaste:lamaste0600One-time enrollment tokens for hardware-bound enrollment
/etc/lamalibre/lamaste/pki/revoked.jsonJSONlamaste:lamaste0600Revoked certificate serial numbers
/etc/lamalibre/lamaste/pki/agents/registry.jsonJSONlamaste:lamaste0600Agent certificate metadata
/etc/nginx/sites-available/lamalibre-lamaste-*nginx confroot:root0644Vhost configurations (the panel's are installed by lamaste-priv)
/etc/nginx/snippets/lamalibre-lamaste-mtls.confnginx confroot:root0644mTLS snippet
/etc/nginx/snippets/lamalibre-lamaste-authz-cache.confnginx confroot:root0644Gatekeeper proxy_cache zone
/etc/systemd/system/lamalibre-lamaste-gatekeeper.servicesystemdroot:root0644Gatekeeper systemd unit
/etc/systemd/system/chisel.servicesystemdroot:root0644Chisel unit, written by create-lamaste
/etc/systemd/system/authelia.servicesystemdroot:root0644Authelia unit, written by create-lamaste
/etc/systemd/system/lamalibre-lamaste-hooks@.socket, @.servicesystemdroot:root0644Relay hook runner (one instance per plugin with relay hooks), written by create-lamaste
~/.lamalibre/lamaste/servers.jsonJSONuser0600Desktop app server registry
~/.lamalibre/lamaste/agents.jsonJSONuser0600Multi-agent registry
~/.lamalibre/lamaste/agentd.tokenJSONuser0600Bearer token of lamaste-agentd's owner (the desktop app, scripts run as the user)
~/.lamalibre/lamaste/agents/<label>/config.jsonJSONuser0600Per-agent configuration
~/.lamalibre/lamaste/agents/<label>/client.p12PKCS#12user0600Per-agent mTLS certificate
~/.lamalibre/lamaste/agents/<label>/ca.crtPEMuser0644Per-agent CA certificate
~/.lamalibre/lamaste/agents/<label>/chisel.jsonJSONuser0600Per-agent Chisel credential (source of truth for the service)
~/.lamalibre/lamaste/agents/<label>/chisel.envenv fileuser0600Linux only: AUTH= for the systemd unit's EnvironmentFile=
~/.lamalibre/lamaste/agents/<label>/directoryuser0700Per-agent data directory
~/.lamalibre/lamaste/agents/<label>/sync-state.jsonJSONuser0600Outcome of the last lamaste-agent sync
~/.lamalibre/lamaste/agents/<label>/app-plugins/<route>.jsonJSONuser0600Port and installation secret of a plugin hosted on this agent
~/.lamalibre/lamaste/agents/<label>/app-plugin-tunnel-state.jsonJSONuser0600The tunnel state last delivered to each hosted plugin's agent half
~/.lamalibre/lamaste/agents/<label>/apphost-state.jsonJSONuser0600The agent halves the app host helper installed for this agent
~/.lamalibre/lamaste/agents/<label>/host-facts.jsonJSONuser0600What the agent last reported about its host, and the helper's last answer
~/.lamalibre/lamaste/agents/<label>/app-job-results.jsonJSONuser0600App job results the relay has not taken yet
~/.lamalibre/lamaste/agents/<label>/logs/directoryuser0700Per-agent Chisel and sync log files
~/.lamalibre/lamaste/agent.jsonJSONuser0600Legacy single-server config
/usr/local/sbin/lamaste-apphostNode.jsroot:root0755Agent machine: the app host helper — see Agent machine: the app host
/etc/sudoers.d/lamaste-apphostsudoersroot:root0440Agent machine: lets the agent's user run the helper
/etc/lamalibre/apphost/relays/<label>.jsonJSONroot:root0644Agent machine: the relay key pinned for agent <label>
/opt/lamalibre/apps/<route>/directoryroot:root0755Agent machine: an agent half's installed versions
/var/lib/lamalibre/apps/<route>/directorylamaste-app-<route>0700Agent machine: an agent half's data
/etc/lamalibre/apps/<route>/plugin-config.jsonJSONlamaste-app-<route>0600Agent machine: an agent half's port and installation secret
/etc/systemd/system/lamalibre-app-<route>.servicesystemdroot:root0644Agent 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:

FieldTypeRequiredDefaultDescription
ipstringYes—Server public IP address
domainstring | nullYesnullBase domain (set during onboarding)
serverIdstringNo—Auto-generated UUIDv4, used as bucket prefix for multi-server storage isolation
emailstring | nullYesnullAdmin email for Let's Encrypt (set during onboarding)
dataDirstringYes/etc/lamalibre/lamastePath to data/state directory
staticDirstringNo—Path to lamaste-server-ui dist directory
maxSiteSizenumberNo524288000Max static site upload size in bytes (500 MB)
adminAuthModestringNo"p12""p12" or "hardware-bound". When "hardware-bound", P12 download and rotation are disabled; admin authenticates via Keychain-backed certificate.
panel2faobjectNo—Built-in TOTP 2FA configuration (see sub-fields below)
panel2fa.enabledbooleanNofalseWhether 2FA is active for admin panel access
panel2fa.secretstring | nullNonullBase32-encoded TOTP secret
panel2fa.setupCompletebooleanNofalseWhether the 2FA setup flow has been confirmed
sessionSecretstringNo—HMAC key for signing session cookies (auto-generated during 2FA setup)
onboarding.statusenumYesFRESHCurrent onboarding state

Onboarding status values:

ValueMeaning
FRESHNo onboarding started — shows onboarding wizard
DOMAIN_SETDomain and email entered, awaiting DNS verification
DNS_READYDNS verified, ready to provision
PROVISIONINGStack provisioning in progress
COMPLETEDOnboarding complete — shows management UI

Example (fresh install):

json
{
  "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):

json
{
  "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):

json
{
  "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:

  1. LAMALIBRE_LAMASTE_CONFIG environment variable (if set)
  2. In development (NODE_ENV=development or NODE_ENV unset): <package-root>/dev/panel.json
  3. In production (NODE_ENV=production): /etc/lamalibre/lamaste/panel.json

Note: The systemd service unit sets CONFIG_FILE in the environment, but the panel server code reads LAMALIBRE_LAMASTE_CONFIG. The systemd variable CONFIG_FILE is not used by the application. In production deployments this has no effect because the code falls back to /etc/lamalibre/lamaste/panel.json when LAMALIBRE_LAMASTE_CONFIG is unset and NODE_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.

FieldTypeDescription
idstringUUID
subdomainstringSubdomain name (e.g., myapp)
fqdnstringFull domain (e.g., myapp.example.com)
portnumberPort forwarded between 127.0.0.1 on the server and 127.0.0.1 on the carrying agent
descriptionstring | nullOptional description (max 200 characters)
typestring"app" (default), "panel" (agent web panel), or "plugin"
enabledbooleanWhether the tunnel is active
accessModestring | 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.
agentLabelstring | undefinedThe 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).
maxBodySizeMbnumber | undefinedLargest 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.
createdAtstringISO 8601 timestamp

Tunnels of type plugin also carry pluginName and pluginRoute.

Example:

json
[
  {
    "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.

FieldTypeDescription
idstringUUID
namestringSite name (the subdomain for managed sites)
fqdnstringFull domain (e.g., blog.example.com)
typestring"managed" (subdomain of the Lamaste domain) or "custom" (your own domain)
spaModebooleanIf true, try_files falls back to /index.html
autheliaProtectedbooleanIf true, requires Authelia authentication
allowedUsersstring[]Authelia users allowed when protected (empty: all authenticated users)
dnsVerifiedbooleanDNS verified (always true for managed sites)
certIssuedbooleanTLS certificate issued and vhost live
aliasesstring[] | undefinedCustom sites only: hostnames answered with a 301 to https://<fqdn>. One certificate lineage named after fqdn covers all
rootPathstringDocument root (e.g., /var/www/lamaste/<id>/)
createdAtstringISO 8601 timestamp

Example:

json
[
  {
    "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.

json
{
  "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:

json
{
  "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:

FieldTypeDescription
scopesarrayRegistered scope definitions (name, version, transport)
instancesarrayActive instances (scope, instanceId, agentLabel, status)
assignmentsarrayAgent-to-instance assignments

Example:

json
{
  "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:

FieldTypeRequiredDescription
transport.strategiesstring[]YesArray of "tunnel", "relay", "direct"
transport.preferredstringNoPreferred strategy (must be in strategies)
transport.directobjectNoDirect connection details (required when using direct strategy)
transport.direct.hoststringYes*Public hostname or IP (1-255 chars). Private/reserved IPs rejected (SSRF prevention)
transport.direct.portnumberYes*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:

FieldTypeDescription
ticketsarrayIssued tickets (id, scope, instanceId, source, target, expiry)
sessionsarrayActive sessions (server-generated sessionId, ticketId, status, heartbeat)

Example:

json
{
  "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:

FieldTypeDescription
groupsarrayArray of group objects
groups[].namestringGroup name (unique, lowercase alphanumeric, 2-63 chars)
groups[].descriptionstringHuman-readable description
groups[].membersstring[]Array of Authelia usernames
groups[].createdAtstringISO 8601 timestamp
groups[].createdBystringAdmin who created the group

Example:

json
{
  "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:

FieldTypeDescription
grantsarrayArray of grant objects
grants[].grantIdstringUnique identifier (e.g., g_abc123)
grants[].principalTypestring"user" or "group"
grants[].principalIdstringAuthelia username or Lamaste group name
grants[].resourceTypestring"tunnel", "plugin", or custom resource type
grants[].resourceIdstringResource identifier (e.g., tunnel subdomain)
grants[].contextobjectOptional metadata (empty {} by default)
grants[].usedbooleanWhether the grant has been consumed
grants[].createdAtstringISO 8601 timestamp
grants[].usedAtstring | nullISO 8601 timestamp of consumption, or null

Example:

json
{
  "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:

FieldTypeDefaultDescription
adminEmailstring | undefined—Admin email shown on access-denied page
adminNamestring | undefined—Admin display name shown on access-denied page
slackChannelstring | undefined—Slack channel for access request templates
teamsChannelstring | undefined—Teams channel for access request templates
sessionCacheTtlMsnumber | undefined—In-memory session cache TTL in milliseconds (default 30000)
accessLoggingEnabledboolean | undefined—Whether to log denied access attempts
accessLogRetentionDaysnumber | undefined—How many days to retain access log entries

Example:

json
{
  "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.

FieldTypeDescription
timestampstringISO 8601 timestamp of the denied request
usernamestringAuthelia username of the denied user
resourceTypestringResource type (e.g., "tunnel", "plugin")
resourceIdstringResource identifier (e.g., tunnel subdomain)
resourceFqdnstringFull domain the user attempted to access

Example:

json
[
  {
    "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:

yaml
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: 6

Critical settings:

SettingValueWhy
server.host127.0.0.1Never bind to 0.0.0.0 — nginx handles public access
password.algorithmbcryptArgon2id uses ~93 MB per hash, causes OOM on 512 MB droplets
bcrypt.cost12Balance between security and performance
session.domainYour domainMust 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:

yaml
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:
      - admins

Password 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.

json
{
  "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.

json
{
  "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).

json
{
  "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).

json
{
  "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.

FieldTypeDescription
idstringUUID
labelstringDisplay name (typically the domain)
panelUrlstringPanel URL (e.g., https://203.0.113.42:9292)
ipstringServer IP address
providerstring | nullCloud provider name (e.g., digitalocean)
providerIdstring | nullProvider-specific resource ID (e.g., droplet ID)
regionstring | nullProvider region slug
createdAtstringISO 8601 timestamp
activebooleanWhether this is the currently active server
authMethodstring"p12" or "keychain"
keychainIdentitystring | nullKeychain identity name (when authMethod is "keychain")
p12Pathstring | nullPath to P12 file (when authMethod is "p12")
activeModestring"agent" or "admin" — which UI mode the desktop app shows for this server. Defaults to "agent"
adminAuthobject | nullAdmin certificate details for Server mode access (see sub-fields below)
adminAuth.methodstring"p12" or "keychain" — how the admin cert is stored
adminAuth.p12Pathstring | nullPath to admin P12 file (when method is "p12")
adminAuth.keychainIdentitystring | nullKeychain identity for admin cert (when method is "keychain")

Example:

json
[
  {
    "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.

json
{
  "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 enrolled domain (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 it
    • tunnelsStopped — true when 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). issuedAt is when the panel issued the password; when the panel's chiselCredentialIssuedAt differs, sync fetches the current credential. Source of truth: every rewrite of the service re-applies it. lamaste-agent chisel refresh-credential refreshes it immediately
  • chisel.env — Linux only: AUTH=agent-<label>:<password>, read by the systemd unit through EnvironmentFile= (mode 0600)
  • sync-state.json — outcome of the last lamaste-agent sync (mode 0600): lastRunAt, lastOkAt, lastError (null after a success), lastState (running, idle — no tunnels, client stopped — or stopped by the operator), tunnels, lastWarnings. Shown on the Sync line of lamaste-agent status (flagged when lastRunAt is over 2 minutes old), and used to log only changes and new warnings
  • agent.lock — present while an operation (setup, update, sync, the agent panel or daemon) changes this agent's service or config.json; holds the owner's PID, boot id and start time. A stale lock (dead process, earlier boot, older than 15 minutes) is taken over
  • logs/chisel.log — Chisel stdout log
  • logs/chisel.error.log — Chisel stderr log
  • logs/sync.log — sync timer log: only runs that changed something, and each error when it first appears and when it clears
  • plugins.json — agent plugin registry
  • plugins/ — per-plugin data directories
  • app-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 label
  • host-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 passed
  • app-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 carries AUTH in EnvironmentVariables
  • Linux: lamalibre-lamaste-chisel-<label> (unit name), ~/.config/systemd/user/lamalibre-lamaste-chisel-<label>.service — a systemd user unit with EnvironmentFile= pointing at chisel.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 — StartInterval 30, 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) and lamalibre-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 into default.target — needs lingering to run without a login. Units written before 4.3 named multi-user.target, which a user manager does not have, so the panel never started at boot; lamaste-agent update and lamaste-agent panel --enable rewrite 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.

PathOwnerModeContents
/usr/local/sbin/lamaste-apphostroot:root0755The helper — copied by enable from a root-owned lamaste-agent install
/etc/sudoers.d/lamaste-apphostroot:root0440<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>.jsonroot:root0644 (dirs 0755)The relay key pinned for agent <label>: { v, label, agent, kid, publicKey, panelUrl, pinnedAt } — public material
/opt/lamalibre/apps/<route>/<version>/root:rootdirs 0755, files 0644The package as published (package/) and apphost.json, the spec it was installed with
/opt/lamalibre/apps/<route>/current, previousroot:rootsymlinksThe running version and the one before it; other versions are deleted
/opt/lamalibre/apps/.stage-<hex>-<route>/root:root0700Unpacking while an install runs
/var/lib/lamalibre/apps/<route>/the agent half's account0700The agent half's data (LAMASTE_APP_DATA_DIR) — kept on removal, deleted by a purge
/var/lib/lamalibre/apps/.owners/<route>.jsonroot:root0644 (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.jsonroot:root0600 (dir 0700){ v: 1, jobs: { <jobId>: <expiry, ms> } } — the signed jobs that ran, each until it expires
/var/lib/lamalibre/apps/.trash/root:root0700Purged data, deleted there
/etc/lamalibre/apps/<route>/root:<the account's group>0750The agent half's configuration directory
/etc/lamalibre/apps/<route>/plugin-config.jsonthe agent half's account0600Port and installation secret (LAMASTE_APP_CONFIG), the shape of the agent's app-plugins/<route>.json
/etc/systemd/system/lamalibre-app-<route>.serviceroot:root0644The agent half's unit, from a fixed template — paths, never secrets
/run/lamaste-apphost.lockroot:root0600Held (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).

nginx
# 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.

nginx
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:

nginx
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:

nginx
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.

ini
[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.target

File Permissions Table ​

PathOwnerModeNotes
/etc/lamalibre/lamaste/lamaste:lamaste0755State directory
/etc/lamalibre/lamaste/panel.jsonlamaste:lamaste0600Panel config
/etc/lamalibre/lamaste/tunnels.jsonlamaste:lamaste0600Tunnel state
/etc/lamalibre/lamaste/sites.jsonlamaste:lamaste0600Site state
/etc/lamalibre/lamaste/chisel-credentials.jsonlamaste:lamaste0600Chisel credentials
/etc/lamalibre/lamaste/chisel-userslamaste:lamaste-chisel0640Chisel authfile
/etc/lamalibre/lamaste/chisel-sentinellamaste:lamaste0600Sentinel password
/etc/lamalibre/lamaste/relay-signing-key.jsonlamaste:lamaste0600Relay assertion signing key
/etc/lamalibre/lamaste/pki/lamaste:lamaste0700PKI directory
/etc/lamalibre/lamaste/pki/ca.keylamaste:lamaste0600CA private key
/etc/lamalibre/lamaste/pki/ca.crtlamaste:lamaste0644CA certificate
/etc/lamalibre/lamaste/pki/client.keylamaste:lamaste0600Client private key
/etc/lamalibre/lamaste/pki/client.crtlamaste:lamaste0644Client certificate
/etc/lamalibre/lamaste/pki/client.p12lamaste:lamaste0600PKCS12 bundle
/etc/lamalibre/lamaste/pki/.p12-passwordlamaste:lamaste0600PKCS12 password
/etc/lamalibre/lamaste/pki/self-signed.pemlamaste:lamaste0644Self-signed TLS cert
/etc/lamalibre/lamaste/pki/self-signed-key.pemlamaste:lamaste0600Self-signed TLS key
/etc/lamalibre/lamaste/ticket-scopes.jsonlamaste:lamaste0600Ticket scope registry
/etc/lamalibre/lamaste/tickets.jsonlamaste:lamaste0600Ticket/session store
/etc/lamalibre/lamaste/invitations.jsonlamaste:lamaste0600Pending invitations
/etc/lamalibre/lamaste/plugins.jsonlamaste:lamaste0600Plugin registry
/etc/lamalibre/lamaste/storage-config.jsonlamaste:lamaste0600Storage registry
/etc/lamalibre/lamaste/storage-master.keylamaste:lamaste0600Storage encryption key
/etc/lamalibre/lamaste/groups.jsonlamaste:lamaste0600Lamaste groups
/etc/lamalibre/lamaste/access-grants.jsonlamaste:lamaste0600Access grants
/etc/lamalibre/lamaste/gatekeeper.jsonlamaste:lamaste0600Gatekeeper settings
/etc/lamalibre/lamaste/access-request-log.jsonlamaste:lamaste0600Denied access log
/etc/lamalibre/lamaste/pki/enrollment-tokens.jsonlamaste:lamaste0600Enrollment tokens
/etc/lamalibre/lamaste/pki/revoked.jsonlamaste:lamaste0600Revocation list
/etc/lamalibre/lamaste/pki/agents/registry.jsonlamaste:lamaste0600Agent cert metadata
/etc/lamalibre/lamaste/chisel-server.keylamaste:lamaste-chisel0640Chisel SSH host key
/etc/authelia/lamaste:lamaste-authelia2770Authelia config (setgid)
/etc/authelia/configuration.ymllamaste:lamaste-authelia0640Auth config
/etc/authelia/users.ymllamaste:lamaste-authelia0660User database
/etc/authelia/.secrets.jsonlamaste:lamaste-authelia0600Auth secrets (panel only)
/etc/authelia/db.sqlite3, notifications.txtlamaste-authelia:lamaste-authelia0600Authelia's own files
/var/log/authelia/lamaste-authelia:lamaste-authelia0750Authelia log
/opt/lamalibre/lamaste/root:root0755Install directory (read-only for lamaste)
/opt/lamalibre/lamaste/hooks/<route>/<sha256>.mjsroot:root0644Approved relay hook module (read by the sandboxed runner)
/run/lamalibre-lamaste-hooks/<route>.sockroot:lamaste0660Relay hook runner socket (systemd)
/var/www/lamaste/lamaste:www-data2750Static site files (files 0640; nginx reads via the group)
/etc/sudoers.d/lamasteroot:root0440Sudo rules (no wildcards)
/usr/local/sbin/lamaste-priv, lamaste-certbot, lamaste-cert-inforoot:root0755Validating sudo programs
/usr/local/bin/chisel, /usr/local/bin/autheliaroot:root0755Pinned releases (create-lamaste)
~/.lamalibre/lamaste/servers.jsonuser0600Server registry
~/.lamalibre/lamaste/agent.jsonuser0600Legacy agent config
~/.lamalibre/lamaste/agentd.tokenuser0600Agent daemon owner token
~/.lamalibre/lamaste/agents/<label>/user0700Agent data directory
~/.lamalibre/lamaste/agents/<label>/chisel.jsonuser0600Agent Chisel credential
~/.lamalibre/lamaste/agents/<label>/chisel.envuser0600Linux AUTH= env file
~/Library/LaunchAgents/com.lamalibre.lamaste.chisel-<label>.plistuser0600macOS service (has AUTH)
~/.lamalibre/lamaste/services.jsonuser0600Service registry
~/.lamalibre/lamaste/agents/<label>/apphost-state.json, host-facts.json, app-job-results.jsonuser0600App host record, host facts, unreported app job results
/usr/local/sbin/lamaste-apphostroot:root0755App host helper (agent machine)
/etc/sudoers.d/lamaste-apphostroot:root0440App host rule (agent machine)
/etc/lamalibre/apphost/relays/<label>.jsonroot:root0644Pinned relay key (agent machine)
/opt/lamalibre/apps/<route>/root:root0755Agent half code, files 0644 (agent machine)
/var/lib/lamalibre/apps/<route>/lamaste-app-<route>0700Agent half data (agent machine)
/var/lib/lamalibre/apps/.owners/<route>.jsonroot:root0644Owner record (agent machine)
/var/lib/lamalibre/apps/.jobs/, .trash/root:root0700Signed jobs run; purged data (agent machine)
/etc/lamalibre/apps/<route>/root:lamaste-app-<route>0750Agent half config directory (agent machine)
/etc/lamalibre/apps/<route>/plugin-config.jsonlamaste-app-<route>0600Agent half port and secret (agent machine)
/etc/systemd/system/lamalibre-app-<route>.serviceroot:root0644Agent half unit (agent machine)

Quick Reference ​

Config FileRead ByModified ByRestart Needed?
panel.jsonlamalibre-lamaste-serverdlamalibre-lamaste-serverd (atomic write)No (hot reload)
tunnels.jsonlamalibre-lamaste-serverdlamalibre-lamaste-serverd (atomic write + fsync)No
sites.jsonlamalibre-lamaste-serverdlamalibre-lamaste-serverd (atomic write + fsync)No
chisel-userschisellamalibre-lamaste-serverd (rendered, atomic rename, no sudo)No for additions (hot reload); the panel restarts chisel on revocations
ticket-scopes.jsonlamalibre-lamaste-serverdlamalibre-lamaste-serverd (atomic write + mutex)No
tickets.jsonlamalibre-lamaste-serverdlamalibre-lamaste-serverd (atomic write + mutex)No
storage-config.jsonlamalibre-lamaste-serverdlamalibre-lamaste-serverd (atomic write + mutex)No
groups.jsongatekeeperlamalibre-lamaste-serverd (atomic write + mutex)No (gatekeeper watches file)
access-grants.jsongatekeeperlamalibre-lamaste-serverd (atomic write + mutex)No (gatekeeper watches file)
gatekeeper.jsongatekeeperlamalibre-lamaste-serverd (atomic write)Yes (systemctl restart lamalibre-lamaste-gatekeeper)
access-request-log.jsonlamalibre-lamaste-serverdgatekeeper (atomic write)No
configuration.ymlautheliaonboarding provisioningYes (systemctl restart authelia)
users.ymlauthelialamalibre-lamaste-serverd (atomic rename, no sudo)Yes (systemctl restart authelia)
lamalibre-lamaste-* vhostsnginxlamalibre-lamaste-serverd (via sudo lamaste-priv nginx-site)Yes (nginx -t && systemctl reload nginx)

Released under the PolyForm Noncommercial License 1.0.0