Skip to content

How It Works ​

Lamaste connects your local web apps to the internet through a cheap VPS relay, using encrypted WebSocket tunnels, with a browser-based admin panel you never need SSH to manage.

In Plain English ​

Picture a building with a locked front door and no doorbell. People outside cannot get in to visit you. That is your home network — your router blocks all incoming connections for security.

Now imagine you rent a tiny storefront on a busy street. You dig a private underground passage from your building to the storefront. When a visitor walks into the storefront, an attendant escorts them through the passage to your building and back again. The visitor never knows they went underground — they just see your building's interior.

That is Lamaste in a nutshell:

  • Your building is your machine — a Mac or Linux box — running web apps behind a firewall.
  • The storefront is a $4/month DigitalOcean droplet with a public IP address.
  • The underground passage is an encrypted WebSocket tunnel (powered by Chisel).
  • The attendant is nginx, routing visitors to the right app.
  • The locked front office is the admin panel, where only people with a special key card (client certificate) can enter and manage everything.

The most unusual part: after you set up the storefront (a single command over SSH), you throw away the SSH key. You never go back through that door. All future management — adding new passages, changing locks, checking security cameras — happens through the front office using your key card.

Why not just deploy to the cloud? ​

You could rent a bigger server and deploy your apps there. But that means:

  • Paying for compute (your machine is already sitting there, powered on)
  • Managing deployments, Docker, CI/CD pipelines
  • Migrating databases, file storage, environment configs
  • Losing access to local hardware (GPUs, attached drives, local network devices)

Lamaste keeps your apps at home and only relays the HTTP traffic. The VPS does almost no work — it just forwards bytes. That is why a $4 droplet with 512MB RAM is enough.

Why not use ngrok or Cloudflare Tunnel? ​

Those are great tools. Lamaste differs in a few ways:

  • Self-hosted: You own the relay. No third-party sees your traffic.
  • Your domain: Traffic goes through your own domain, not a vendor subdomain.
  • Your auth: You control user accounts and 2FA, not a vendor dashboard.
  • Fixed cost: $4/month for unlimited tunnels, not per-seat or per-tunnel pricing.
  • No vendor lock-in: It is open source. The droplet is yours.

For Users ​

The Big Picture ​

Lamaste has three phases of life: installation, onboarding, and daily operation.

Phase 1: Installation (5 minutes)

There are two ways to get a Lamaste server running:

Option A: SSH (manual)

SSH into a fresh Ubuntu droplet and run one command:

bash
apt install -y npm
npx @lamalibre/create-lamaste

This installs everything the relay needs: nginx, Node.js, firewall rules, certificates, and the management panel. At the end, it prints a client certificate file and password. You download the certificate, import it into your browser, and disconnect SSH. You never SSH in again.

Option B: Desktop app (cloud provisioning)

Install the desktop app (npx @lamalibre/create-lamaste-desktop) and use the Servers tab to create a DigitalOcean droplet. The app handles everything — droplet creation, Lamaste installation, certificate download, and connection — without you ever touching SSH. Cloud API tokens are stored in your OS credential store (macOS Keychain or Linux libsecret), never in plaintext. See the Cloud Provisioning guide for a step-by-step walkthrough.

Phase 2: Onboarding (10 minutes, browser)

You open https://<droplet-ip>:9292 in your browser. Because you imported the client certificate, the browser proves your identity at the TLS level — no username/password needed.

The onboarding wizard walks you through:

  1. Enter your domain name and email address
  2. Point your DNS records to the droplet's IP
  3. Click "Provision" — Lamaste installs Chisel (tunnel server), Authelia (2FA), and configures nginx with Let's Encrypt certificates

Phase 3: Daily operation (browser)

After onboarding, the same URL shows the management panel:

  • Dashboard: System stats, service health, resource usage
  • Tunnels: Create, edit, move, delete tunnels. Each tunnel is carried by one enrolled agent.
  • Users: Add people who can access your tunneled apps (each gets TOTP 2FA)
  • Certificates: Monitor Let's Encrypt expiry, rotate mTLS certificates
  • Services: Start, stop, restart Chisel/Authelia/nginx. View live logs.

Data Flow: How a Request Reaches Your App ​

When someone visits https://myapp.example.com:

1. Browser sends HTTPS request
   ↓
2. DNS resolves example.com → 203.0.113.42 (your droplet)
   ↓
3. nginx on the droplet receives the request
   ↓
4. nginx checks: does this visitor have a valid 2FA session?
   ├─ No  → Authelia login page (username + TOTP code)
   └─ Yes → continue
   ↓
5. nginx forwards the request to Chisel server (port 9090)
   ↓
6. Chisel server sends it through the WebSocket tunnel
   ↓
7. Chisel client on your machine receives the request
   ↓
8. Chisel client forwards it to localhost:8001 (your app)
   ↓
9. Your app responds
   ↓
10. Response travels back: app → Chisel client → tunnel → Chisel server → nginx → browser

The entire round trip is encrypted end-to-end. The tunnel itself runs inside a standard HTTPS connection (WebSocket upgrade), so it passes through corporate firewalls and ISP filters that block custom protocols.

The Admin Panel: A Different Security Model ​

The admin panel at https://<ip>:9292 uses a completely different authentication mechanism than your tunneled apps:

Admin PanelTunneled Apps
URLhttps://<ip>:9292https://app.example.com
Auth methodmTLS client certificateUsername + TOTP 2FA
Who accesses itYou (the admin)Your users
Login requiredNo — certificate is the loginYes — Authelia login page
Works without domainYes (IP access always works)No (needs DNS + domain)

This split is deliberate. The admin panel works by IP address so you can always reach it, even if your domain's DNS breaks or your Let's Encrypt certificate expires. It uses client certificates so there is no login form to brute-force.

Component Map ​

Here is every piece of software running on the droplet after onboarding:

┌──────────────────────────────────────────────────────────────────┐
│  Ubuntu 24.04 Droplet (512MB RAM)                                │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │  nginx (public-facing, ports 443 + 9292)                    │ │
│  │                                                             │ │
│  │  Port 9292 — Admin panel vhost (mTLS)                       │ │
│  │    └─ proxy_pass → Panel Server (127.0.0.1:3100)            │ │
│  │                                                             │ │
│  │  Port 443 — Tunnel vhosts (Let's Encrypt TLS)               │ │
│  │    ├─ forward auth → Authelia (127.0.0.1:9091)              │ │
│  │    └─ proxy_pass → Chisel Server (127.0.0.1:9090)           │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  ┌──────────────────┐  ┌──────────────┐  ┌───────────────────┐  │
│  │ Panel Server     │  │ Authelia      │  │ Chisel Server     │  │
│  │ (Fastify, :3100) │  │ (TOTP, :9091) │  │ (tunnels, :9090)  │  │
│  │ REST API +       │  │ bcrypt users  │  │ WebSocket-over-   │  │
│  │ static files     │  │ ~25MB RAM     │  │ HTTPS             │  │
│  └──────────────────┘  └──────────────┘  └───────────────────┘  │
│                                                                  │
│  ┌──────────────────┐  ┌──────────────┐  ┌───────────────────┐  │
│  │ UFW Firewall     │  │ fail2ban     │  │ certbot           │  │
│  │ ports 22/443/    │  │ SSH + nginx  │  │ auto-renewing     │  │
│  │ 9292 only        │  │ jails        │  │ Let's Encrypt     │  │
│  └──────────────────┘  └──────────────┘  └───────────────────┘  │
│                                                                  │
│  Total RAM: ~245MB    Swap: 1GB safety net                       │
└──────────────────────────────────────────────────────────────────┘
         ▲
         │ WebSocket tunnel (encrypted, inside HTTPS)
         │
┌────────┴─────────────────────────────────────────────────────────┐
│  Agent: your Mac or Linux machine (behind firewall)              │
│                                                                  │
│  ┌──────────────────┐       ┌──────────────────────────────────┐ │
│  │ Chisel Client    │       │ Your Web Apps                    │ │
│  │ (launchd/systemd │──────→│ localhost:8001 (blog)            │ │
│  │  auto-reconnect, │       │ localhost:3000 (dashboard)       │ │
│  │  30 s sync timer)│       │ localhost:5173 (dev server)      │ │
│  └──────────────────┘       └──────────────────────────────────┘ │
│  ┌──────────────────────────────────────────────────────────────┐ │
│  │ Lamaste Desktop (Tauri v2)                                  │ │
│  │  ├─ Service discovery + tunnel management                    │ │
│  │  ├─ Multi-server registry (~/.lamalibre/lamaste/servers.json)         │ │
│  │  ├─ Cloud provisioning (DigitalOcean)                        │ │
│  │  └─ Credential storage (macOS Keychain / Linux libsecret)   │ │
│  └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘

RAM Budget ​

Every component is chosen to fit within 512MB:

ComponentRAM UsagePurpose
Ubuntu OS baseline~120MBKernel, systemd, base services
nginx~15MBReverse proxy, TLS termination
Authelia~25MBTOTP 2FA authentication
Chisel Server~20MBWebSocket tunnel relay
Panel Server~30MBFastify REST API
fail2ban~35MBBrute-force protection
Total~245MBHeadroom: ~265MB

A 1GB swap file provides a safety net. Authelia uses bcrypt (not argon2id) specifically because argon2id uses ~93MB per password hash and would cause out-of-memory kills on a 512MB droplet.

For Developers ​

Architecture Philosophy ​

Lamaste follows three core principles:

  1. SSH is disposable. The installer runs once over SSH and provisions everything. After that, SSH is only a fallback for disaster recovery. All day-to-day management happens through the browser-based panel, authenticated by mTLS client certificates.

  2. nginx is the only public surface. Every backend service binds to 127.0.0.1. nginx is the sole process listening on public ports (443 and 9292). This minimizes the attack surface to a single, well-audited reverse proxy.

  3. No database. State is stored in JSON files (panel.json, tunnels.json) and YAML files (users.yml for Authelia). Writes use atomic rename (write .tmp then rename) to prevent corruption. At this scale, a database adds complexity without benefit.

Monorepo Structure ​

lamaste/
├── packages/
│   ├── create-lamaste/       ← npx installer (runs once on VPS)
│   │   └── src/
│   │       ├── index.js       ← Listr2 orchestrator
│   │       ├── tasks/         ← harden, node, mtls, nginx, panel
│   │       └── lib/           ← secrets, summary, cert-help-page
│   │
│   ├── lamaste-serverd/          ← Fastify REST API (runs as systemd service)
│   │   └── src/
│   │       ├── index.js       ← server entry
│   │       ├── middleware/    ← mTLS verification, onboarding guard
│   │       ├── routes/        ← onboarding/ + management/ endpoints
│   │       └── lib/           ← business logic (nginx, chisel, authelia, certbot)
│   │
│   └── lamaste-server-ui/          ← React SPA (served as static files)
│       └── src/
│           ├── App.jsx        ← mode detection (onboarding vs management)
│           ├── pages/         ← onboarding wizard + management pages
│           └── components/    ← shared UI components

Installer Pipeline ​

The installer (create-lamaste) runs as a Listr2 task pipeline:

Phase 1: Environment Detection
├── Verify root access
├── Detect OS (must be Ubuntu 24.04)
└── Detect public IP address

Phase 2: Installation
├── Harden OS
│   ├── Create 1GB swap file (swappiness=10)
│   ├── Configure UFW (allow 22, 443, 9292 only)
│   ├── Install and configure fail2ban
│   ├── Harden SSH (disable password auth)
│   └── Install system packages (nginx, certbot)
│
├── Install Node.js 20 LTS
│
├── Generate mTLS Certificates
│   ├── Create CA key + certificate (4096-bit RSA, 10-year validity)
│   ├── Create client key + CSR (4096-bit RSA)
│   ├── Sign client certificate with CA (2-year validity)
│   └── Create PKCS12 bundle (.p12) for browser import
│
├── Configure nginx
│   ├── Generate self-signed TLS certificate for IP access
│   ├── Write mTLS snippet (ssl_verify_client on)
│   ├── Write IP-based panel vhost (port 9292)
│   ├── Enable site, remove default
│   ├── Port 80 catch-all: redirect plain HTTP to HTTPS
│   └── Validate config and start nginx
│
├── Deploy Panel
│   ├── Create lamaste system user
│   ├── Create directory structure (/opt/lamalibre/lamaste root-owned, /etc/lamalibre/lamaste)
│   │   and deploy the certificate help page
│   ├── Install chisel + Authelia
│   │   ├── lamaste-chisel group (Chisel reads its authfile through it), lamaste-authelia account
│   │   ├── Pinned releases (Chisel 1.12.0, Authelia 4.39.28), SHA-256 verified
│   │   ├── Hardened systemd units (chisel.service, authelia.service)
│   │   └── Ownership: /var/www/lamaste (lamaste:www-data), /etc/authelia (lamaste:lamaste-authelia)
│   ├── Deploy lamalibre-lamaste-serverd, lamaste-server CLI, lamaste-server-ui, docs
│   ├── Write panel.json configuration
│   ├── Write systemd service unit (lamalibre-lamaste-serverd.service)
│   ├── Install root-owned sudo programs (lamaste-priv, lamaste-certbot, lamaste-cert-info)
│   ├── Write sudoers rules (fixed command lines, no wildcards; visudo-checked)
│   └── Start panel service + health check
└── Deploy Gatekeeper

Every task is idempotent — re-running the installer updates the installation without losing existing configuration. Skip guards check for existing state (swap already exists, certificates already generated, etc.).

Onboarding Provisioning ​

After the installer runs, the panel is in FRESH state. The onboarding wizard (accessed via browser) provisions the remaining stack:

Onboarding Wizard
├── Domain Step: user enters domain + email
├── DNS Step: panel shows required DNS records, user verifies
└── Provisioning Step: panel configures and starts the remaining components
    ├── Check Chisel is installed, create its key + authfile, start it
    ├── Check Authelia is installed, write its configuration + first admin user, start it
    ├── Issue Let's Encrypt certificate via certbot
    ├── Write domain-based nginx vhosts (through lamaste-priv)
    ├── Verify all services
    └── Update panel.json → onboarding status: COMPLETED

After provisioning, onboarding endpoints return 410 Gone and management endpoints become available.

Security Layers ​

Layer 1: UFW Firewall
  └─ Only ports 22 (SSH), 443 (HTTPS), 9292 (admin panel) are open

Layer 2: fail2ban
  └─ Bans IPs after 5 failed SSH or nginx auth attempts (1 hour ban)

Layer 3: nginx TLS
  └─ All traffic is HTTPS. Self-signed cert for IP:9292, Let's Encrypt for domain.

Layer 4a: mTLS (admin panel)
  └─ ssl_verify_client on — no certificate = TLS handshake rejected before HTTP

Layer 4b: Authelia (tunneled apps)
  └─ Forward auth — username + TOTP 2FA required for every tunneled app

Layer 5: Localhost binding
  └─ Panel server, Authelia, Chisel all bind 127.0.0.1 — unreachable from outside

Layer 6: Systemd hardening
  └─ NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp

Layer 7: Privilege boundary
  └─ lamaste user can only run fixed commands (nginx -t, systemctl for known services)
     and validating root-owned programs (lamaste-priv, lamaste-certbot, lamaste-cert-info);
     no sudoers wildcards. Authelia runs as its own account; code is root-owned

Key File Locations on the Droplet ​

PathPurpose
/etc/lamalibre/lamaste/panel.jsonMain configuration (IP, domain, onboarding status)
/etc/lamalibre/lamaste/tunnels.jsonTunnel definitions
/etc/lamalibre/lamaste/pki/CA cert/key, client cert/key, self-signed cert
/etc/lamalibre/lamaste/pki/client.p12PKCS12 bundle for browser import
/opt/lamalibre/lamaste/lamaste-serverd/Fastify backend (Node.js)
/opt/lamalibre/lamaste/lamaste-server-ui/dist/Built React SPA (static files)
/var/www/lamaste/Static site files served through tunnels
/etc/nginx/sites-available/lamaste-*nginx vhost configurations
/etc/nginx/snippets/lamalibre-lamaste-mtls.confmTLS client certificate requirement
/etc/systemd/system/lamalibre-lamaste-serverd.servicePanel server systemd unit
/etc/sudoers.d/lamasteFixed sudo rules (no wildcards)
/etc/fail2ban/jail.d/lamaste.conffail2ban jail configuration

State Machine ​

The panel operates in distinct states:

FRESH ──────→ DOMAIN_SET ──────→ DNS_READY ──────→ PROVISIONING ──────→ COMPLETED
  │               │                   │                    │                  │
  │  Onboarding   │  Onboarding       │  Onboarding        │  Onboarding      │ Management
  │  endpoints    │  endpoints        │  endpoints         │  endpoints       │ endpoints
  │  active       │  active           │  active            │  active          │ active
  │               │                   │                    │                  │
  │  Management   │  Management       │  Management        │  Management      │ Onboarding
  │  returns 503  │  returns 503      │  returns 503       │  returns 503     │ returns 410

Technology Choices Explained ​

ChoiceWhyAlternatives Considered
Chisel for tunnelsSingle Go binary, WebSocket-over-HTTPS (bypasses DPI), ~20MB RAMfrp (heavier), bore (less mature), SSH tunnels (fragile)
Authelia for 2FASingle Go binary, TOTP support, ~25MB RAM, file-based user storeKeycloak (too heavy), custom auth (security risk)
nginx for reverse proxyBattle-tested, mTLS support, forward auth, low RAMCaddy (no mTLS vhost control), Traefik (higher RAM)
Fastify for APIFast, schema-first validation, native WebSocket, ESMExpress (slower, less structured), Hono (less ecosystem)
JSON files for stateSimple, no database process, atomic writesSQLite (adds dependency), PostgreSQL (way too heavy)
bcrypt for passwordsLow memory (~4KB per hash)argon2id (~93MB per hash, causes OOM on 512MB)
Client certificates for adminZero-login, unforgeable, works at TLS layerSession cookies (brute-forceable), API keys (leakable)

Quick Reference ​

Ports ​

PortServiceAccess
22SSHPublic (key-only, fail2ban protected)
443nginx (tunnel vhosts)Public (Let's Encrypt TLS + Authelia 2FA)
9292nginx (admin panel)Public (self-signed TLS + mTLS client cert)
3100Panel Server (Fastify)Localhost only
9090Chisel ServerLocalhost only
9091AutheliaLocalhost only

Services ​

systemd UnitBinaryPurpose
lamalibre-lamaste-serverdnode src/index.jsREST API + static file server
nginx/usr/sbin/nginxReverse proxy, TLS, mTLS
chisel/usr/local/bin/chiselWebSocket tunnel server
authelia/usr/local/bin/autheliaTOTP 2FA authentication
fail2ban/usr/bin/fail2ban-serverBrute-force protection
certbot.timer/usr/bin/certbotAuto-renewing Let's Encrypt certs

Data Flow Summary ​

Browser → DNS → nginx:443 → Authelia check → Chisel server → tunnel → Chisel client → local app
Admin   →       nginx:9292 → mTLS check    → Panel server:3100

Key Commands (on the droplet) ​

bash
# Service management
systemctl status lamalibre-lamaste-serverd
systemctl restart chisel
journalctl -u lamalibre-lamaste-serverd -f    # live logs

# nginx
nginx -t                           # validate config
systemctl reload nginx             # apply vhost changes

# Certificates
certbot certificates               # list Let's Encrypt certs
certbot renew                      # force renewal check

# Firewall
ufw status                         # show open ports
fail2ban-client status             # show active jails

Released under the PolyForm Noncommercial License 1.0.0