# LockFlare Lens LockFlare Lens is a desktop application (macOS and Windows) for managing Linux servers over SSH. It is a configuration console, not an agent or a monitoring service: it connects with the operator's own SSH logins, reads and changes each server through ordinary commands, installs nothing on the server, and runs no service of its own. The operator's identity is an encrypted key file on a USB drive plus Touch ID; there is no login and no cloud account in the loop. Pricing: the terminal is free with no limit — any box you can SSH into is a "console" in Lens, with unlimited servers and unlimited seats, no licence and no count. What is licensed is what Lens does to a box as root (security setup, system setup, backups, packages, databases, clusters), and every account gets 10 of those free forever; past them a server licence is $99 per address per year. What Lens does on a server: a full terminal (splits, broadcast, history, recordings), Security Setup (firewall, Fail2Ban, SSH hardening proven by a fresh login, password policy, WAF with ModSecurity and CrowdSec, integrity and rootkit checks, AppArmor/SELinux, kernel hardening, scheduled tasks, time), System Setup (users, groups, certificates with a vault, automatic updates, audit, services, storage, network), tunnels, an HTTP tunnel through the server, file management with an editor, backups to named repositories, and an AI sysadmin (Themis) that reads through curated probes and proposes changes the operator confirms. What Lens installs and administers: nginx or Apache with sites as things, vsftpd or ProFTPD, Postfix with DKIM and DMARC and Dovecot mailboxes, Docker with compose stacks and stack templates, KVM/libvirt virtualization, MariaDB, PostgreSQL and MongoDB with replication, MongoDB sharding, cluster backups and TLS between members, Redis or Valkey with Sentinel, a WireGuard mesh between servers, and an HAProxy load balancer that follows a database pair's primary. Across servers: Actions (run a command, ask a question, install, set up the same thing on every server on a board), Snippets, Server Templates (a hardening playbook drawn as a flow and run on one box or a fleet), and NOC diagrams where every wire is proved from the box the traffic starts at. Licensing: every account comes with 10 Lens-managed servers free forever, unlimited terminals (consoles need no license) and unlimited seats — servers are licensed, never people. Past the free ones, a server license is $99 per address per year. Licenses are signed keys opened on the computer; Lens never connects to LockFlare. Zero-knowledge architecture: the only thing LockFlare keeps about a server is the IP address on its licence, and only so that licence can be reissued — not the hostname, the operating system, the provider, the role, or what runs on it. Card payments go directly to Stripe and no card details are held. Nothing operational (fleet map, credentials, keys, certificates, snippets, telemetry) ever leaves the operator's computer. Company: LockFlare Corp, Delaware, USA. Contact: engineering@lockflare.com. Website: https://lockflare.com. # LockFlare Cipher documentation # LockFlare Cipher documentation Cipher is LockFlare's flagship: the product the company was founded on, and the engineering Lens grew out of. These pages are its technical documentation — how a build is packed, encrypted, delivered and executed in memory; what the three pieces are and how a server behaves; the Node and .NET engines; the known limits of each runtime; and what to do when something does not come up. Cipher is not a public download: it makes source code unreadable on the server that runs it, so it is licensed on request, to companies we have spoken with. If you are evaluating it, start with [How it works](/docs/cipher/how-it-works/) and [Understanding a LockFlare server](/docs/cipher/server-model/), then [write to us](/contact/). # What is LockFlare # What is LockFlare

LockFlare keeps your source code encrypted everywhere it rests — on disk, in transit, and in storage — and decrypts it only into the volatile memory of a server you control. Nothing readable is ever written to that machine’s filesystem.

It is software-only. There is no special hardware, no secure enclave chip, no cloud provider to trust. You install a single binary on an ordinary Linux server — your own, or your client’s — and it serves your application exactly as it would have run natively.

The problem it solves

When you deploy software to infrastructure you do not own, you hand over your source. A client with root on their own server can read every file you put there. So can anyone who compromises it, anyone who takes a snapshot of the disk, and anyone who walks out with a backup.

The usual answers are all partial. Obfuscation slows a reader down for an afternoon. Compiling to bytecode moves the problem rather than solving it. Containers isolate processes from each other, not from the host. And a hosted platform means the code sits on someone else’s machine under someone else’s terms.

LockFlare’s position is narrow and specific: the plaintext of your application exists only inside a running process. Stop the process and there is nothing on that machine to read.

What that looks like in practice

What LockFlare is not

It is not a build system. LockFlare serves the build you give it — it does not compile, transpile or bundle your code. If your project needs a build step, you run it as you always have, and LockFlare deploys the output.

It is not a hosting provider. The servers are yours. LockFlare never runs your application on infrastructure it controls, which is the whole point — your deployment target can be a client’s datacentre, an air-gapped network, or a rack in your own office.

And it is not a DRM wrapper. There is no phone-home licence check that breaks your app when a network blips. The Engine holds a licence bound to its server, but your application’s availability does not depend on reaching us.

Who it is for

Software vendors

You ship an application that runs on a customer’s infrastructure and you would rather not ship the source with it. On-premise delivery without handing over the product.

Regulated deployments

Data residency or sovereignty rules put the servers somewhere specific. LockFlare lets the code run there without being readable there.

Agencies and contractors

You build and host for clients who own their infrastructure. The client gets a running system; you keep the source you wrote.

Multi-tenant operators

One fleet, many customers, per-domain isolation — and every tenant’s code encrypted separately from every other.

Not sure whether it fits? The free tier is genuinely free — one server, eight cores, no expiry and no card. It is the same runtime and the same encryption as every paid plan. See Editions.

Read next

The rest of this section is what to know before you install anything. How it works covers the mechanism, and The three pieces covers what runs where.

Do not skip Understanding a LockFlare server. A LockFlare host is an appliance, not a machine you keep — and once a server is sealed, that stops being a recommendation and becomes the only recovery path there is.
# How it works # How it works

Four stages: pack, encrypt, deliver, execute. The only one that produces readable code is the last, and it happens in memory on a machine you own.

1 · Pack

LockFlare Cipher reads the project folder on your machine — the same folder you develop in — and packs it in memory. Nothing is written to a temp directory. The pack is deterministic, so an unchanged project produces an identical archive and an identical version hash.

What gets packed is your build output as it stands. LockFlare does not run your bundler, compiler or package manager. If your project needs npm run build or dotnet publish, you run it first, exactly as you do today.

2 · Encrypt

The archive is encrypted with AES-256-GCM before it leaves your machine. GCM is authenticated encryption: it protects confidentiality and integrity together, so a modified ciphertext fails to decrypt rather than executing something you did not write.

Encryption happens on your machine, not on receipt. At no point does an unencrypted copy of your project exist anywhere outside the folder you develop in and the RAM of your own server.

3 · Deliver

The ciphertext is uploaded to the Cipher Encrypted CDN and recorded against a version. Promoting a build from one environment to another moves that exact artifact — the bytes do not change between Development and Live, so what you tested is byte-for-byte what ships.

Each of your servers holds a licence bound to its own IP address. When the Engine fetches a build it authenticates with that licence, and the payload is keyed so that a build for one server is not usable on another.

4 · Execute

The Engine decrypts the build into volatile memory and executes it there. On the Node runtime that means loading modules through Node’s native vm sandbox with per-domain isolation; on .NET it means Roslyn in-memory compilation into a collectible AssemblyLoadContext.

Either way, the filesystem never receives plaintext. There is no extraction step, no staging directory, and nothing left behind when the process stops.

What an attacker with root sees

They do thisThey get
Read the filesystemThe Engine binary. No application source.
Snapshot the diskThe same. Ciphertext at most, and only if the build is stored locally.
Restart the serverThe Engine re-fetches and re-decrypts into RAM. Still nothing on disk.
Attach a debuggerNothing — the Engine refuses to run with strace, gdb or ltrace present.
Read process memoryThe plaintext, if they have root and the process is running. This is the honest limit.
That last row matters, so we state it plainly rather than burying it. A sufficiently determined party with root on a running machine can inspect that machine’s memory. Software-only confidential computing raises the cost of extraction from “copy a folder” to “attach to a hardened process and reconstruct it” — it does not make it impossible. Server Enclave exists to close the remaining gap by removing interactive access to the machine entirely.
# The three pieces # The three pieces

Three pieces, with a deliberate split of knowledge between them. Each one holds what it needs and nothing more.

LockFlare Cipher — the desktop app

A native application for macOS and Windows. It is where you import projects, manage environments and servers, push builds, watch fleet health and administer your team. It is also the only place your source is ever read in plaintext by LockFlare software.

Cipher runs on your machine, next to your code. It packs and encrypts locally, then talks to the control plane over an authenticated channel. It never uploads anything readable.

The control plane

The coordination layer. It holds accounts, permissions, environments, server registrations, licences, deployment manifests and the activity log — and it holds your builds as ciphertext.

What it does not hold is a way to read them. Build artifacts arrive encrypted and are stored exactly as received. The control plane orchestrates which version belongs on which server; it is not a party to the contents.

This is the split worth understanding: the control plane knows your topology, and the Engine knows your code — briefly, in memory. Neither one holds both.

LockFlare Engine — the runtime

A single binary installed as a system service on each of your servers. It registers with its licence, fetches the builds assigned to it, decrypts them into memory and serves traffic.

It is deliberately the only thing on the machine. A LockFlare server is an appliance, not a general-purpose host — no database alongside it, no second application, no files that matter. That constraint is what makes it disposable: if the box is lost, corrupted or sealed shut, you install the Engine on a fresh one and push every project back. Minutes, not a recovery project.

Node runtime

Hosts JavaScript projects. Per-domain isolation through Node’s native vm module, so one tenant’s code cannot reach another’s. Requires Node.js 24.

.NET runtime

Hosts ASP.NET Core projects. Roslyn compiles in memory into a collectible AssemblyLoadContext, so a reload frees the old assembly cleanly. Requires .NET 10.

How they fit together

CipherControl planeEngine
Runs onYour laptopLockFlareYour servers
Sees plaintext sourceYes — it is your machineNeverIn RAM only
Holds encryption keysYesYes, per accountFor its own builds
Knows your topologyYesYesOnly itself
Writes plaintext to diskNoNoNo

One more consequence worth naming: because the Engine only knows about itself, compromising one server tells an attacker nothing about the rest of your fleet. There is no shared secret sitting on every box.

# Understanding a LockFlare server # Understanding a LockFlare server

A LockFlare server is an appliance, not a computer you keep. Treat it as disposable from the day you register it — because the moment you seal one, disposability stops being a convenience and becomes the only recovery path that exists.

Read this before you seal anything. A sealed server whose engine stops responding cannot be recovered. Not by SSH, not by console, not by the hosting provider, not by us. The machine is finished and the only answer is to build another one.

Why the constraint exists

It is not a limitation of the implementation. It follows directly from what the product is for.

LockFlare’s central claim is that your source is unreadable on the server. RAM-only execution gets most of the way there: nothing readable is written to disk. What it cannot stop is someone with root on a running machine attaching to the process and reading its memory. That is the honest ceiling of every software-only approach.

Enclave closes that gap by removing every interactive login path — SSH, console, KVM, IPMI, an attached keyboard. There is no shell to get root in, so there is no session from which to dump memory. That is the whole point, and it works.

But a machine with no way in has no way in for you either. A recovery channel that survived sealing would be a recovery channel an attacker could use, so it does not exist. The security property and the lockout are the same property, seen from two sides.

What happens when a sealed server fails

SituationCan you get back in?
Sealed, engine healthy Yes. Unseal from Cipher — it is authorized through the engine, which is running.
Sealed, engine crashed but the service manager restarts it Yes. The engine comes back and the unseal path comes back with it. This is why a managed service is mandatory.
Sealed, engine dead and nothing restarts it No. No SSH, no console, no rescue boot. Rebuild the server.
Sealed, a service the engine depends on has failed No. Same outcome — unsealing needs the engine responding.
Not sealed, anything at all Yes. SSH still works. This is the state to be in while you are still setting things up.

Why this is survivable

Losing a server sounds severe until you look at what is actually on it. A LockFlare host holds the engine binary, a licence file, and encrypted artifacts it can fetch again. There is no application source, no build output, no database, no writable web root, no state that exists only there.

So the recovery is: provision a machine, install the engine, apply a licence, push. Every project is back and serving within minutes, and nothing was restored because nothing was lost.

That is the trade. You accept that a server can become unrecoverable, and in exchange an unrecoverable server costs you fifteen minutes instead of a weekend.

The contract, as Cipher states it

Registering a server asks you to acknowledge five things. They are not boilerplate — each one is a precondition for the paragraph above being true.

You confirmBecause
This will be a dedicated LockFlare server. It runs the engine and nothing else — no second application, no web server of your own, no background workloads. Anything else on the box is something you would lose, and something you would need a shell to fix.
This server does not contain any database engine. MongoDB, PostgreSQL, MySQL, Redis and anything comparable live on separate machines. A database is state that exists only there. Rebuilding the host would destroy it.
Nothing irreplaceable is stored on its disk. Uploads, backups and generated files belong elsewhere. This disk is never the source of truth for anything. The disk is not backed up, because there is supposed to be nothing on it worth backing up.
I can wipe and rebuild this server at any time. A fresh machine, the engine binary, and a push — every project is back and serving within minutes. This is the recovery plan. If it is not true for this machine, there is no recovery plan.
I understand a sealed server can lock me out permanently. Because it can, and the consequence lands on whoever sealed it.

If you are not going to seal

The appliance discipline is still the right way to run a LockFlare host — it is what makes a lost server cheap — but the stakes are lower. An unsealed server still has SSH, so a mistake is recoverable in the ordinary way.

Sealing is what converts every one of those acknowledgements from good practice into a hard precondition. A server that breaks the contract and is never sealed is untidy. The same server sealed is a machine you will one day be unable to open.

Before sealing, ask one question: if this machine stopped responding in ten minutes and I could never log into it again, would I lose anything I cannot replace with a push? If the answer is yes, fix that first. If the answer is no, seal it — that is exactly the state Enclave is for.

The one thing that must be running

The engine has to be under a service manager — systemd or PM2, as root. Not for convenience: it is the difference between a crash that self-heals and a crash that ends the machine.

Cipher asks you to confirm this again in the seal dialog, separately from the registration acknowledgements, because it is the single condition most likely to be true at registration and false by the time someone seals.

See Node Engine installation or .NET Engine installation for the service setup.

# Editions # Editions

One runtime, one encryption model, one set of features. The difference between the editions is where the encrypted build is stored — not how strongly it is protected.

Your code is AES-256-GCM encrypted on both editions, everywhere it rests. Free tier does not mean plaintext on your server. It means the ciphertext lives on your machine instead of ours. The encryption is identical, the key handling is identical, and nothing readable is written to disk in either case.

The one architectural difference

Free — stored on your server

Once a server confirms it holds a verified copy of a build, the central copy is dropped. The AES-256-GCM encrypted artifact then lives on your own machine and nowhere else.

LockFlare holds no copy of your build. That is a stronger privacy position, not a weaker one.

Enterprise — stored on the Cipher Encrypted CDN

The AES-256-GCM encrypted artifact is retained centrally and versioned, in addition to being delivered to your servers.

A server can be rebuilt from that copy at any time, and a build can be promoted between environments without being pushed again.

What that changes in practice

FreeEnterprise
Encryption of the build AES-256-GCM AES-256-GCM
Plaintext written to disk Never Never
Runtime, isolation, Enclave Identical Identical
Where the ciphertext rests Your server only Cipher Encrypted CDN + your servers
Promote a build between environments Push again Repoint — no bytes move
Rebuild a lost server from a stored copy Push again from your source Pulls its artifacts and resumes

The two consequences at the bottom of that table both follow from the same fact: on the free tier there is no central artifact to promote or to restore from, because it was deliberately dropped once your server confirmed it had one.

On the free tier, your server holds the only copy of that build. If it is lost and you have not backed it up, push again from the machine that still has the source. That is normally a minor inconvenience — you keep your source and your build outputs either way — but it is worth knowing rather than discovering.

Capacity

Separate from storage, and metered on four axes.

Free forever

1 server · 8 cores, split any way you want. No clock, no card, no trial period, no feature restrictions.

Enterprise

Servers, cores, seats and environments to whatever scale you need. Add capacity from your LockFlare account; changes appear in Cipher the next time it loads.

Cipher shows all four as live gauges on the Licensing screen, so you can see how close to a ceiling you are before you hit it.

Moving between editions does not change your projects, servers or environments. Adding capacity raises the ceiling; nothing is migrated and nothing is redeployed.
# Licensing # Licensing

Four axes, each licensed independently and billed only when it changes. Start on the free plan and stay there for as long as it fits — there is no trial clock.

The four axes

AxisFromWhat it is
Per server $180 An engine instance on your infrastructure — cloud, bare metal, or air-gapped.
Per core $55 CPU cores the runtime may use. Your first 8 are free, forever.
Per developer seat $30 A named account in Cipher with its own permissions, login rules and audit trail.
Per environment $22 Dev, staging, production — each isolated, each with its own servers and push rules.

Server and core rates step down with scale, applied automatically as you grow. Current rates are on the licensing page; this page is about how the model behaves.

Cores float across the fleet

This is the part worth understanding, because it is not how most per-core licensing works.

Licensed cores are not bound to any single machine. They are a pool. The runtime assigns them across active workloads, and you decide the split per server when you register or edit one.

Add a server today, move cores to it tomorrow, spin up a new environment next week. Only the axes that actually changed are billed — there is no plan to renegotiate and no re-provisioning.

Practically: capacity added for a launch can be removed afterwards. A fleet that needs 24 cores in December and 8 in February pays for what it holds, not for its peak.

The free 8 cores carry over

Going paid does not forfeit them. The eight free cores stay yours and join the pool, to split across as many servers as you like. A paid account with 16 licensed cores has 24 in total.

The free plan

1 server · 8 cores, free forever. No card, no expiry, no feature restrictions — same encryption, same runtime, same deployment pipeline as every paid plan. Create the account inside Cipher and start deploying.

The only architectural difference is where your encrypted build is stored. See Editions.

Where your encrypted code lives

On the free plan the AES-256-GCM encrypted build lives on each LockFlare Engine server — the machines you install the engine on. LockFlare retains no copy.

Enterprise adds Cipher repositories, so a build is also held centrally and can be promoted to any environment without being pushed again. It is encrypted identically in both cases; only the location differs.

Watching your usage

Cipher’s Licensing screen shows all four axes as live gauges — servers, cores, endpoints and seats — against what your plan allows. A gauge turns red at the ceiling, so you can see you are about to run out before an action fails.

Capacity is added from your LockFlare account on the web, not inside the app. Manage licences on that screen hands off to it, and changes appear the next time Cipher loads.

At the seat ceiling, Add user in Team management offers the same hand-off — and points out that removing a member frees a seat immediately, which is often the faster answer.

Enterprise arrangements

Beyond the published axes, enterprise agreements cover unlimited servers and cores, dedicated infrastructure, custom service levels, air-gapped and government deployments, and priority support.

Self-hosted artifact storage — which removes LockFlare from artifact custody entirely — is available on request, and is the recommended configuration for regulated workloads.

# Server requirements # Server requirements

A LockFlare server is an appliance. It should run the Engine and nothing else — no database, no second application, no files that matter.

Operating system

Linux x86-64 with systemd. The Engine installs as a system service and runs as root. Any mainstream distribution works — Ubuntu, Debian, Rocky, Alma.

Both runtimes need their platform present: Node.js 24 for the Node Engine, the ASP.NET Core 10 runtime for the .NET Engine.

Network

PortRequiredWhy
80 Always The control plane reaches the server by IP over HTTP for reloads, health checks and Enclave operations. Certificate renewal depends on it too.
443 Conditional Only if the server terminates TLS itself. Not needed behind a CDN or reverse proxy.
Port 80 stays open even when you serve HTTPS. This catches people out. The control plane addresses your server by IP address, not by domain name, so it cannot use a certificate issued for your domain. Closing port 80 does not break your site — it breaks reloads, health reporting and sealing, and it will look like the server has gone offline.

Debug tools must not be present

The Engine refuses to seal on a machine with strace, gdb or ltrace installed. Those tools attach to a running process and read its memory, which is precisely the attack that RAM-only execution exists to make expensive.

check before installing
which strace gdb ltrace
# no output means you are clear

# if any are present:
apt remove --purge strace gdb ltrace     # Debian / Ubuntu
dnf remove strace gdb ltrace             # Rocky / Alma

Sizing

Cores are what you licence, so allocate them per server according to the traffic that server takes. A single core is enough for a low-traffic site; a busy API benefits from more. You can change the allocation later without reinstalling.

Memory is the resource to watch, because your application is decrypted into it. Budget for your normal working set plus the decrypted build — in practice this is close to what the same application would use running natively, since it is the same code doing the same work.

Dedicated, and disposable

The appliance constraint is not arbitrary. It is what makes a server replaceable: if the box is lost, corrupted or sealed shut, you install the Engine on a fresh one and push every project back encrypted. Minutes, not a recovery project.

That only holds while the server stays dedicated. Put a database on it and you have created something you have to back up, restore and care about — and the whole recovery story goes with it.

# Installation # Installation

The whole path, once: install Cipher, create an environment, register a server, install the Engine on it, import a project, push. Fifteen minutes if the server is already up.

1 · Install LockFlare Cipher

Download the desktop app for macOS or Windows and open it. Create an account, or sign in if you have one. Every account starts free and stays free — one server, eight cores, no expiry.

Two-factor sign-in is required. Email codes work immediately with no setup; an authenticator app or Touch ID is faster once configured. See Signing in.

2 · Create an environment

An environment is a deployment target — Development, Staging, Live, or whatever your process calls them. Open Environments and add one. It is just a name and a colour at this point; the servers come next.

3 · Register a server

Add a server to that environment: give it a label, its public IP address, and the number of cores to allocate. LockFlare issues a licence key bound to that IP.

Copy the key — you need it in the next step, and it is the only thing that ties the Engine on that machine to your account.

Nothing is installed on the server yet, so it will show as unreachable until you finish step 4. That is expected — the licence is issued first, then the Engine uses it to register.

4 · Install the Engine

Download the interpreter for your runtime from the Downloads screen in Cipher, copy it to the server, and follow the guide for that runtime. The two differ in more than the binary name — Node runs behind a web server on port 3000, .NET binds port 80 directly — so use the right one.

Node Engine

Node.js 24, nginx in front, then PM2 or systemd to keep it running.

Installation guide →

.NET Engine

ASP.NET Core 10, no reverse proxy, systemd required. Optional TLS on 443.

Installation guide →

Both end the same way: you run the engine once by hand, it prompts for the licence key you copied in step 3, validates it and writes it to .env. After that you put it under a service manager so it survives a reboot.

Within a few seconds the server should appear as online in Monitoring. If it does not, the cause is almost always port 80 — see Server requirements.

5 · Import a project

Back in Cipher, open Projects and add one. Point it at the folder on your disk, name it, choose the runtime, and set the starting file — the file the interpreter loads and runs, typically server.js for Node.

Attach the environment you created in step 2, and add the domain the project should answer on.

6 · Push

Open the project and press Push. Cipher packs the folder in memory, encrypts it, uploads the ciphertext, and signals the Engine to load it. The chain on screen shows each stage as it completes.

Point your DNS at the server and the project is live. Subsequent deploys are the same single action.

What to do next

Read the Enclave documentation before you seal anything. Sealing disables SSH and console access permanently, and the only way back in is unsealing from Cipher. It is the right end state for a production appliance and a bad surprise if you did not mean it.
# Migrating an existing app # Migrating an existing app

Most applications deploy unchanged. The ones that need work usually need one adjustment, and it is nearly always the same one — something writing to the local filesystem.

Before you start: a five-minute audit

Run these against your source. Each hit is something to look at, not necessarily something to change.

what to look for
# writes to the local filesystem — the most common blocker
grep -rn "writeFile\|createWriteStream\|mkdir" --include=*.js src/

# a local file-backed database
grep -rn "sqlite\|better-sqlite3\|\.db'" --include=*.js src/

# schedulers — check the worker model before you push
grep -rn "setInterval\|node-cron\|cron.schedule" --include=*.js src/

# in-process state that assumes a single process
grep -rn "MemoryStore\|new Map()" --include=*.js src/

The four things to change

PatternChange to
Uploads written to a local folder Object storage — S3, R2, Azure Blob — or a database blob column.
SQLite or any file-backed store A network database. There is nowhere for the file to live, and it would not survive a rebuild.
Logs written to a file Write to stdout. The engine captures it and it reaches journalctl or PM2.
Sessions or caches in process memory Redis, or the database — or pick Single instance as the worker model if you cannot change it yet.

All four are things you would have had to fix to run more than one instance anyway. LockFlare surfaces them earlier, not additionally.

Runtime-specific checks

Node

Dependencies must be in the fleet package set or bundled into the artifact. Pick a worker model — Standard unless you have an unguarded scheduler.

.NET

Enable outbound network for the environment if you talk to a database. Precompile Razor views, and check nothing reads Assembly.Location.

Both are covered in detail under Known Limitations.

A sensible order

  1. Register a server and install the engine. Nothing about your application yet.
  2. Create a Development environment pointing at it.
  3. Import the project and push. Read the engine log — most problems announce themselves there.
  4. Fix what the log names. Missing packages and startup errors are both reported explicitly.
  5. Point a test domain at it and exercise the application properly.
  6. Add a production environment and promote, rather than pushing a second time.
  7. Once it is settled, consider sealing the server.
Migrate one service at a time. Each is independent — there is no fleet-wide cutover, and nothing requires you to run old and new side by side beyond the service you are working on.

What does not change

Source control, CI, build tooling, test suites, code review, release approvals. LockFlare consumes the output of that process rather than replacing any part of it, which is why adoption does not require re-certifying a pipeline.

# LockFlare Cipher # LockFlare Cipher

Cipher is the desktop app you deploy from. It is also the only piece of LockFlare software that ever reads your source in plaintext — and it runs on your machine, not ours.

Everything you do to a deployment happens here: importing projects, defining environments, registering servers, pushing builds, watching fleet health, sealing machines and administering your team.

What it does on your machine

The order matters. Encryption happens locally, not on receipt. There is no window in which a readable copy of your project exists anywhere outside your development folder and the RAM of your own server.

Platforms

macOS (Apple Silicon and Intel) and Windows. Download either from lockflare.com/download, or from inside the app when an update is available.

How this section is organised

The pages that follow track the order you will actually do things in: sign in, look around the home screen, create an environment, register a server, import a project, push it, point a domain at it. Monitoring, Enclave, team management and the activity log come after, because they matter once something is running.

If you want the short version first, Installation walks the whole path end to end in one page.
# Signing in # Signing in

Two-factor is required on every account. Which second factor you use is your choice, and you can register more than one.

Signing in

Enter your username and password. Cipher then asks for a second factor before it will open the workspace.

The second factor

Email codes

A six-digit code to your inbox. Always available with no setup — this is the floor, and the fallback if another factor fails.

Touch ID

Your fingerprint on this Mac. Nothing to carry or type, and the fastest option when the hardware is there.

Authenticator app

Google Authenticator, Authy, 1Password and similar. Works offline and moves with you between machines.

USB security key

Turns a USB drive into a hardware key. The drive must stay plugged in — pulling it locks the app immediately.

Register as many as you like under My account → Two-factor authentication. One is marked in use; the rest stay registered as backups you can switch to.

Touch ID is not offered when confirming a seal or unseal. Those operations need a code the server can verify, not a check that happened locally on your laptop.

If you lose access

Forgot your password? on the sign-in screen sends a verification code to your registered email and lets you set a new one. Changing your password signs out every other session immediately.

Login restrictions

Administrators can restrict a member to specific IP addresses or time windows — useful for contractors, or for accounts that should only be used from an office network. See Team management.

# The home screen # The home screen

The home screen answers one question before you read a word: is anything wrong right now?

The LockFlare Cipher home screen

The header states the fleet’s condition and changes colour with it. Below: recent projects, then fleet, security posture and licence.

The header

The status chip and the headline are driven by live state, not by a greeting. Everything encrypted with a green wash means every server is serving. If servers are unreachable it says so, turns amber, and the primary action changes to take you to Monitoring.

The mark on the right follows the same state, so the screen reads as healthy or unhealthy from across the room.

Recent projects

The projects you touched most recently, with their runtime, environment count and last change. Click through to open one directly.

Fleet

Two gauges: servers online and average latency across the fleet. The online gauge runs backwards from the others — full is good, and it walks left into amber and red as servers drop.

Security posture

A checklist of the account’s security state: two-factor method, whether a USB key is linked, and how many servers are sealed. Anything unresolved shows an amber triangle and a link straight to the screen that fixes it.

Enclave · 0 of 10 sealed is the line worth watching. Unsealed servers still allow SSH and console logins — that is fine while you are setting up, and worth closing before production.

Licence

Servers and cores as gauges against your plan. These run the usual direction: full is bad, because full means you cannot add another server without more capacity. At the ceiling the gauge turns red.

Where your builds live

Two cards at the bottom state which storage options this account has — the Cipher Encrypted CDN, and your own Engine servers. On the free tier the first is marked Enterprise and dimmed, which means the encrypted build lives on your server rather than centrally. It is AES-256-GCM encrypted in both cases. See Editions.

# Projects # Projects

A project is a folder on your disk plus the settings that describe how to run it. LockFlare serves the build you give it — it does not compile, transpile or bundle your code.

The projects list in LockFlare Cipher

Every project on the account, with its runtime, type, environment count and status.

Creating a project

Press New project. The first screen asks what you are deploying and where it lives on disk.

Creating a project — name, runtime and source folder

Name, runtime and source folder. The folder is the one you develop in — there is no export or copy step.

FieldWhat it is
NameWhat you call it. Shown everywhere in the app.
SlugDerived from the name. Used internally to identify the project.
TypeBackend, frontend, or a backend with a frontend attached.
RuntimeNode or .NET. Must match the Engine installed on the target servers.
Source folderThe directory on your machine. Read at push time, never copied.
Starting fileThe file the interpreter loads and runs — typically server.js.
Creating a project — environments and worker model

The second screen attaches environments and sets the worker model.

The starting file

This is the one people get wrong. It is the entry point the interpreter executes — the file that creates your server and starts listening. For an Express app that is usually server.js or app.js. If your framework has a different entry point, name that instead.

It is not your build output directory and not your package entry — it is the file that, if you ran node <file> on your own machine, would start the server.

Build first, then push

If your project needs a build step, run it as you always have. LockFlare deploys what is in the folder at the moment you push. It will not run npm run build for you, and a stale dist/ will deploy exactly as stale as it is.

Backend with a frontend

A backend project can carry a frontend build alongside it — a Vue, React, Angular, Svelte or plain static bundle. Point the frontend folder at your build output and choose the path it serves from. Both are packed and encrypted together, and a push deploys them as one artifact.

Editing later

Everything above is changeable from the project’s General pane — source folder, starting file, worker model, attached frontend. Changes take effect on the next push.

Members without project-create permission can open a project and read it, but the controls that change it are not shown. See Team management.
# Worker models # Worker models

Every project picks a worker model, and the choice has real consequences. Get it wrong and a scheduled job can run once per core instead of once.

If your application runs crons, schedulers or anything that must happen exactly once, read this before your first push. The default is correct for most applications and wrong for that one.

Standard recommended

One real worker process per licensed core, all serving in parallel. This is real multi-core — four licensed cores means four workers handling requests simultaneously.

It works for ordinary applications and for fork-and-serve cluster code alike. If your application calls cluster.fork(), that call is skipped, because the engine has already forked real workers for you.

WORKER_NUMBER and cluster.worker.id are the real worker number under Standard. A guard like if (cluster.worker.id === 1) runs your cron exactly once, on one worker — which is the pattern you want.

Self-coordinating cluster single core

Your entire application — its master and the workers it forks — runs emulated inside one process on one core. There is no parallelism.

Choose it only when your master does coordination the engine cannot replace: aggregating messages from workers over IPC, distributing jobs, holding state the workers ask it for.

If your master only forks and respawns, use Standard instead. That is the common case, and Standard gives you the cores you are paying for.

Single instance single core

One copy, one core. For applications that must never run twice and cannot be adapted — in-memory sessions or caches with no shared store, or a scheduler with no worker guard.

It is the safe answer when you are unsure and cannot change the code. It is also the slowest, so treat it as a stopgap rather than a destination.

Choosing

Your applicationModel
An ordinary web app or APIStandard
Uses cluster.fork() just to spread loadStandard — your fork is skipped, the engine already did it
Runs a cron guarded by a worker checkStandard — the guard works as written
Runs a cron with no guardSingle instance, or add a guard and use Standard
Keeps sessions or a cache in process memorySingle instance, or move state to Redis and use Standard
Master aggregates worker messages over IPCSelf-coordinating cluster

The failure to avoid

An unguarded setInterval or cron under Standard runs in every worker. Four cores means four invoices emailed, four cleanup jobs, four webhook retries. Nothing errors — it just happens four times, which is worse.

The fix is one line, and it is the same line you would want on any clustered Node deployment:

run the scheduler on one worker only
const cluster = require('cluster')

if (cluster.worker.id === 1) {
  startScheduler()
}

Changing it later

The worker model is on the project’s General pane and can be changed at any time. It takes effect on the next push or reload.

# Frontend projects # Frontend projects

A static build from any toolchain that emits a folder with an entry document can be deployed on its own — no backend required, served from memory like everything else.

Supported toolchains

FrameworkBuild output
Vuedist/
Reactbuild/ or dist/
Angulardist/<project>/
Svelte / SvelteKit staticbuild/
Astrodist/
SolidJSdist/
Preactbuild/
Next.js static exportout/
Nuxt generate.output/public/
Plain HTMLthe folder itself
Static export, not server rendering. Next.js and Nuxt must be configured to emit a static folder — next export or nuxt generate. An SSR build expects a Node server at runtime; deploy that as a backend project instead.

Creating one

In New project, choose Frontend as the type and point the source folder at your build output — the folder containing index.html, not the project root.

There is no starting file to set: a static project has no entry point to execute, only an entry document to serve.

Build first, then push. LockFlare deploys the folder as it stands — it does not run your bundler, so an out-of-date dist/ deploys exactly as out-of-date as it is.

Client-side routing

Single-page applications route in the browser, so a deep link like /settings/profile has to resolve to your entry document rather than 404. The runtime serves the entry document for unmatched paths, which is what a static host has to do for SPA routing to work.

Hash routing works without any of this, since the path never leaves the browser.

Full-stack: a frontend attached to a backend

The other shape. A backend project can carry a frontend build alongside it, grafted in at a path you choose and served by your own server code.

Point the frontend folder at your build output and set the destination path. Both are packed and encrypted together, and a push deploys them as one artifact — so the API and the UI can never be out of step with each other.

Frontend projectBackend with frontend
Runs codeNoYes — your server
Starting fileNot usedRequired
Serves the UIThe runtimeYour own server code
Deployed together with an APINo — separate projectYes — one artifact

Same guarantees

A static project is encrypted, delivered and served exactly like a backend one: AES-256-GCM at rest, decrypted into memory, never written to disk as readable files. There is no reduced mode for frontends.

# Environments # Environments

An environment is a deployment target with servers behind it. Development, Staging, Live — or whatever your process calls them.

Adding an environment in LockFlare Cipher

An environment is a name, a colour and a set of servers. The colour is how you tell them apart at a glance everywhere else in the app.

Creating one

Open Environments and add one. At this point it is just a label — servers come next, and projects attach to it after that.

Environments are metered against your plan, so each one consumes an environment licence. Cipher tells you before you run out.

Servers behind an environment

An environment can hold one server or many. A push fans out to every server in the environment, so adding a second machine is how you get redundancy — there is nothing else to configure.

See Servers and licences for registering machines.

Resetting an environment

A reset tells every server in the environment to reload — drop what it is running and fetch its assigned build again. It is the first thing to try when a server is serving something unexpected.

Resetting an environment with three servers

The reset is sent to every server in the environment at once.

Each server confirms independently, so you see the result per machine rather than a single success or failure for the whole operation.

All three servers confirmed the reset

All three confirmed — every server reloaded and is serving again.

Two servers confirmed, one failed

Two confirmed, one failed. The environment is now in a mixed state — two machines reloaded, one did not.

A partial reset is worth acting on. The failed server is still serving whatever it had before, so the environment is no longer uniform. Check it in Monitoring — an unreachable machine is usually port 80.

Attaching projects

A project attaches to one or more environments from its Environments pane. Only attached environments appear as push targets, and detaching one does not remove what is already deployed — it stops future pushes reaching it.

# Servers and licences # Servers and licences

Registering a server issues a licence key bound to that machine’s IP address. The Engine uses that key to prove which account it belongs to.

1 · Add the server

Adding a server to an environment

Label, public IP address and core allocation. The label is for you; the IP is what the licence binds to.

FieldWhat it is
LabelA name you will recognise in Monitoring. Not used by the Engine.
IP addressThe server’s public IP. The licence is bound to it — a typo produces a machine that never comes online.
CoresHow many CPU cores to allocate here, counted against your plan.
The server shows as unreachable until you finish installing the Engine on it. That is expected — the licence is issued first, and the Engine uses it to register.

2 · Copy the licence key

Viewing server licence keys

Every server’s key, on the Licensing screen. Click to copy.

You need this in a moment. It is the only thing tying the Engine on that machine to your account, and it will not work on a different IP.

3 · Download the Engine

Downloading the Node and .NET interpreters

Two runtimes. Pick the one matching your project — a Node project needs the Node Engine.

Each card carries an Installation guide with the exact commands, including the Node.js or .NET prerequisites. The same content lives here: Node Engine and .NET Engine.

4 · Install it on the server

Extracting and starting the Engine over SSH

Extract the interpreter and start it once by hand.

Entering the licence key on first run

On first run it prompts for the licence key, validates it and writes it to .env. Subsequent starts do not prompt.

5 · Make it a service

Installing and checking the systemd service

Install it under a service manager and confirm it is running.

Do not skip this. A server without a managed service does not come back after a reboot — and Enclave will refuse to seal it, because sealing removes every other way of restarting the Engine.

Cores

Cores are what you licence, and you decide the split. A low-traffic site is fine on one; a busy API benefits from more. The allocation is changeable later without reinstalling anything.

The Licensing screen shows cores used against cores available as a live gauge, so you can see the headroom before you add a machine.

Replacing a server

Because a LockFlare server holds nothing but the Engine, replacing one is quick: register the new machine, install the Engine with its new licence, push. There is no state to migrate.

That is the whole point of keeping the box dedicated — and it stops being true the moment you put a database on it.

# Pushing code # Pushing code

One action: pack, encrypt, upload, serve. The chain on screen shows each stage as it completes.

Pushing code to an environment

DISK → CIPHER → REPOSITORY → RUNTIME. Each link lights as that stage finishes.

What happens

StageWhereWhat
DiskYour machineThe source folder is read as it stands right now.
CipherYour machinePacked in memory, then AES-256-GCM encrypted.
RepositoryEncrypted CDNCiphertext uploaded and recorded against a version hash.
RuntimeYour serversEvery server in the environment fetches, decrypts into RAM, and serves.

Before you push

Cipher checks four things and will not let the push start until all of them hold:

Each is reported specifically, so a blocked push tells you which one rather than failing generically.

Run your build first. LockFlare packs the folder as it is at that moment — it does not run your bundler, and a stale dist/ deploys exactly as stale as it is.

Versions

Every push produces a version hash derived from the content. An unchanged project produces the same hash, so you can tell at a glance whether two environments are running identical code.

The push result reports file count, total size and that hash.

Promoting instead of rebuilding

On Enterprise you can promote an existing build from one environment to another. It moves that exact artifact — the bytes do not change between Development and Live, so what you tested is byte-for-byte what ships.

On the free tier there is nothing central to promote, because the encrypted build retires to your own server once it confirms it holds a copy. Push again instead — the artifact is AES-256-GCM encrypted either way, only its location differs. See Editions.

Comparing environments

The Compare pane shows which version each environment is running, when it was pushed and by whom. It is the fastest way to answer “is Live actually running what we tested?”

Many servers, one push

A push fans out to every server in the environment. There is no per-server step and no staggering to configure — each machine fetches the same artifact and reloads.

# Comparing environments # Comparing environments

Which build is each environment actually running, and is Live what you tested? The Compare pane answers both, and promotes between them without rebuilding.

What it shows

One card per environment: the version hash it points at, when that build was pushed and by whom. A version hash is derived from content, so identical hashes mean byte-identical builds — which is the only way to answer the question with certainty.

Same hash in Staging and Live means the same bytes. Not “built from the same commit” — the same artifact, which is a stronger statement and the one that matters when something behaves differently in one place.

Promoting

Promotion repoints an environment at a build that already exists. No bytes move, nothing is re-encrypted, and no rebuild happens — which is why what you tested in Staging is exactly what ships.

Pick the source environment, pick the target, promote. It is also how you roll back: promote the previous build and the environment returns to it.

A rollback is a pointer change. There is no re-upload and no rebuild, so it takes as long as a reload — which is the reason a bad deploy is a minor event rather than an incident.

Free tier

Promotion needs a centrally-held artifact to repoint at, and on the free tier there is not one — the encrypted build retires to your own server once it confirms it holds a copy. Push again to the target environment instead.

See Editions for why that trade exists.

Push and reload are separate

Worth knowing, because it is not what most pipelines do. A push uploads a build and moves the environment’s pointer; the fleet keeps serving the previous build until it is reloaded.

So deploying and rolling out are two deliberate acts. You can push at any time and choose when it goes live.

# Domains # Domains

A domain tells the interpreter which requests belong to which project. Without one, a push has nowhere to land.

Several domains serving the same project

Several domains pointing at one project in one environment. All of them serve the same build.

Adding a domain

Open the project, go to Domains, pick the environment and add the hostname. Domains are per environment — Development and Live hold different lists, which is what lets the same project serve staging.example.com and example.com from different machines.

Several domains, one project

Add as many as you need. Every domain in the list resolves to the same build, so this covers the usual cases: an apex plus www, a vanity domain, or a per-customer hostname on a multi-tenant deployment.

DNS

Point the domain’s A record at the server’s IP address — the same IP you registered the server with. If the environment has several servers, point at all of them, or put a load balancer in front.

Adding a domain in Cipher does not change DNS, and changing DNS does not tell Cipher. Both steps are needed: the interpreter has to know the hostname is yours, and the internet has to know where to send it.

Per-domain isolation

On the Node runtime each domain runs in its own vm sandbox. One tenant’s code cannot reach another’s, even on the same server — which is what makes multi-tenant hosting on a single machine reasonable rather than reckless.

HTTPS

Two options. Terminate TLS in front — a CDN or reverse proxy — and the server only needs port 80. Or let the server terminate it itself, which needs 443 open and a certificate present. The .NET Engine has this built in; see .NET Engine installation.

Port 80 stays open either way. The control plane reaches your server by IP address, so it cannot use a certificate issued for your domain. Closing 80 does not break your site — it breaks reloads, health reporting and sealing, and the server will look offline.
# Environment variables # Environment variables

Configuration for your application, held encrypted and injected into the process at start. Nothing is written to a file on the server.

Where they live

Variables are set per environment on the project’s Environment variables pane. Development and Live hold separate sets, which is the point — a staging database URL should not follow a build into production.

They are encrypted at rest alongside your build and delivered into the process environment in memory. There is no .env on the host to read, and no plaintext configuration anywhere on that machine.

Reading them in your application

Ordinarily — process.env.DATABASE_URL on Node, Environment.GetEnvironmentVariable or the standard configuration providers on .NET. Nothing LockFlare-specific.

On Node, dotenv still works. The runtime parses your uploaded .env and injects it before your code runs, then shims dotenv so config().parsed returns those values without touching the filesystem. You do not have to remove it.

Two sources, one environment

SourceBehaviour
The .env inside your pushed artifact Travels with the build. Same values in every environment you deploy it to.
Variables set in Cipher Per environment. What you want for anything that differs between staging and production.

Applications also receive a small OS baseline — PATH, HOME, TZ, LANG, TMPDIR and NODE_ENV, which defaults to production.

The engine’s own environment is never passed through to your application. It cannot read the server licence, and a read of the engine’s .env returns ENOENT — so a compromised application cannot lift the credential that identifies the server.

Applying a change

Variables are read when the application starts, so a change needs a reload of that environment to take effect. Save them, then reload — there is no need to push again if the code has not changed.

What belongs here

Changes are recorded in the activity log with the actor and the time — the fact of the change, not the value.

# Monitoring # Monitoring

Live interpreter health across every environment, polled continuously while the window is in front of you.

The board

One card per server: hostname, IP, uptime, and two gauges — latency and heap. Both run against fixed scales rather than each server’s own numbers, so you can compare machines at a glance instead of decoding four different axes.

GaugeGreenAmberRed
Latencyunder 100 ms100–200 msover 200 ms
Heapunder ~280 MB~280–420 MBover ~420 MB

An unreachable server shows a greyed dial and a dash rather than a needle at zero — a needle parked in the green would read as “healthy and fast”, which is the opposite of true.

Scoping

The control at the top filters to one environment. Everything follows it: the verdict chip, the online count, the averages and the grid. Cards name their environment only when you are looking at all of them.

Server detail

Click a card for the full health payload in a side panel — PID, Node or .NET version, platform and architecture, cores, heap used and total, RSS, external memory, uptime, and the domains that server is answering on.

It opens beside the grid rather than expanding in place, so the other servers do not move while you are comparing them.

Polling

Every ten seconds. Polling stops when the window is hidden or loses focus, and does one immediate refresh when you come back — so what you see on return is current rather than minutes stale.

A server that will not come online is almost always port 80. The control plane reaches it by IP over HTTP for health checks, reloads and Enclave — and it needs that port even when your site serves HTTPS.
# Enclave # Enclave

Sealing a server disables every login on it — SSH, console, direct access. After that only LockFlare reaches the machine, and the only way back in is unsealing from Cipher.

Read this page before sealing anything. It is the right end state for a production appliance and a genuinely bad surprise if you did not mean it.

What it is for

RAM-only execution stops your source being readable on disk. It does not stop someone with root on a running machine from inspecting that machine’s memory — we say so plainly in How it works.

Enclave closes that gap by removing interactive access entirely. There is no shell to get root in, because there is no login. The Engine keeps serving; nothing else can happen on the box.

Before you seal

The managed-service requirement is the one that matters. Sealing without one locks the server permanently — there is no login to restart the Engine by hand, and nothing to recover it. Cipher asks you to confirm this explicitly.

Sealing

The seal confirmation in LockFlare Cipher

The state change is shown before you commit: unsealed on the left, sealed on the right. Below it, the acknowledgement and the typed confirmation.

You confirm the managed service, then type the server’s name to continue. Both are deliberate friction on an action that cannot be undone from the machine itself.

Two-factor verification before sealing

A second factor is required. Email or authenticator only — Touch ID is not offered.

Touch ID is excluded on purpose. Sealing needs a code the server can verify, not a check that happened locally on your laptop.

What changes on the machine

BeforeAfter sealing
SSHAvailableDisabled
Console loginAvailableDisabled
Serving trafficYesYes — unchanged
Push and reloadYesYes — unchanged
Health reportingYesYes — unchanged

Deployment is unaffected. You still push, still reload, still see it in Monitoring. What you lose is the ability to log in and do anything else.

Unsealing

The unseal confirmation

Unsealing restores normal access. Same verification, opposite direction.

SSH and console logins are re-enabled and the machine behaves as it did before. Unsealing is done from Cipher — it is the only channel that still reaches a sealed box.

If you seal without a service manager

The server keeps serving until the Engine stops. When it does — a reboot, a crash — nothing restarts it, and there is no login to do it by hand. That machine is finished.

The recovery is the same as any lost server: install the Engine on a fresh box, register it, push. Minutes, because a LockFlare server holds nothing but the Engine. That is the appliance model doing its job.

# Team management # Team management

Everyone who can sign in to this account, and what each of them can reach. Seats are metered against your plan.

Adding a member

Creating a team member

Name, email and username. They can sign in immediately — you set what they can reach before that matters.

New members are prompted to set up two-factor on first sign-in, the same as any account. There is no way to opt a member out of it.

Permissions

Assigning permissions to a member

Permissions are per area. Grant only what the person needs.

Permissions gate both the interface and the API. A member without project-create permission does not see Push, Delete or the source-configuration controls — and the server rejects those calls independently, so hiding a button is never the only thing standing between someone and an action.

AreaCovers
ProjectsListing, creating, editing and deleting projects; pushing and promoting builds.
EnvironmentsCreating environments, registering servers, resetting.
UsersAdding members, changing permissions, removing accounts.
SettingsAccount configuration and licensing.
Full adminEverything, including Enclave and account termination.
Sealing and unsealing require full admin. So does terminating the account. Those are the two operations that cannot be undone from anywhere else.

Login restrictions

Setting login restrictions for a member

Restrict a member to specific addresses or times, on top of their permissions.

Useful for contractors who should only reach the account during an engagement, or for accounts that should only be used from an office network. Restrictions are evaluated at sign-in and are independent of what the member is allowed to do once inside.

Seats

Each member consumes a seat. At the ceiling, Add user hands off to your LockFlare account to add more — and points out that removing a member frees a seat immediately, which is often the faster fix.

Disabling instead of removing

A disabled member cannot sign in, and existing sessions are rejected mid-flight rather than surviving until they expire. Their history stays in the activity log. Removing them frees the seat; disabling them does not.

# Activity log # Activity log

Every action against the account, attributed and timestamped. Nothing that changes state happens without a record.

The project activity log

Per-project history: who pushed, when, from where, and which version.

What is recorded

Each entry carries the actor, the time, and the originating IP address.

Where to find it

Per project, in the project’s Activity pane — scoped to that project, which is usually what you want when working out what changed. Per member, from their row in Team management.

What it is good for

Mostly the ordinary question: something changed, when and by whom. A push you did not expect, an environment that is running an older version than you thought, a setting that is not what it was last week.

It is also the record you hand to an auditor. Deployments to regulated infrastructure usually need to show who changed what and when, and that is exactly what the log holds.

The log records that an action happened, not its contents. A push is recorded with its version hash; the code in it is not readable from the log, because it is not readable anywhere outside your own server’s memory.
# Node Engine — Installation # Node Engine — Installation

The Node Engine runs behind a web server on port 3000. Everything below assumes a fresh Linux x86-64 box with systemd and root access.

Register the server in LockFlare Cipher first — add it to an environment and copy the licence key it issues. You need that key in step 4, and it is bound to this machine’s IP address.

Prerequisites — Node.js 24

The interpreter requires Node.js 24. Install it with NVM before anything else.

install node 24
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
nvm install 24
node -v
npm -v

Verify the output shows v24.x.x for node and 11.x.x for npm before continuing.

1 · Install a web server

The Node interpreter listens on port 3000, so a web server sits in front of it on port 80. nginx below; Apache works equally well.

install nginx
sudo apt install nginx -y
sudo nano /etc/nginx/sites-available/default

Delete everything in that file and paste this:

/etc/nginx/sites-available/default
server {
    listen 80;
    server_name _;
    client_max_body_size 20M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;

        error_page 502 503 504 = @custom503;
    }

    location @custom503 {
        default_type application/json;
        return 503 '{"success":false,"message":"LockFlare Interpreter is restarting"}';
    }
}

The @custom503 block matters: during a reload the interpreter is briefly unavailable, and without it nginx returns its own HTML error page to API clients expecting JSON.

apply
sudo systemctl restart nginx

2 · Extract the interpreter

/var/www/interpreter
sudo mkdir -p /var/www/interpreter
sudo chown $USER:$USER /var/www/interpreter
cd /var/www/interpreter
wget https://cdn.lockflare.com/interpreter/lockflare-interpreter.tar.gz
tar -xzf lockflare-interpreter.tar.gz
rm lockflare-interpreter.tar.gz

3 · Install dependencies

npm
npm install

4 · Run and configure the licence

Start the engine once by hand. On first run it prompts for the licence key you copied from Cipher.

first run
./lf-engine
expected output
  ╔══════════════════════════════════╗
  ║        LOCKFLARE ENGINE          ║
  ╚══════════════════════════════════╝

  First-time setup
  ─────────────────────────────────

  License key: LF-xxxxx-xxxxx-xxxxx-xxxxx
  Validating license...
  ✓ License validated

  Configuration saved to /var/www/interpreter/.env

[ENGINE] License verified
[ENGINE] Runtime initialized
[ENGINE] Syncing...
[ENGINE] Payload ready
[ENGINE] Service started (PID 12345)

========================================
BOOTSTRAP COMPLETE
========================================

[MASTER] Allocated: 4 core(s) — Forking 4 worker(s)...
[MASTER] Workers ready (4)

[WORKER 12350] Listening on http://0.0.0.0:3000

The core allocation you set when registering the server is what the master forks against — four cores, four workers. The key is written to .env in the install directory, so subsequent starts do not prompt. Stop it with Ctrl+C once you see the engine sync — the next step makes it permanent.

5 · Keep it running

The engine must run as a managed service. Two options — PM2 is the recommended one for Node.

This step is not optional if you ever intend to seal this server. Enclave disables every login on the machine, and without a service manager to restart the engine there is no way back in. Cipher asks you to confirm a managed service is present before it will seal.

PM2 — recommended

pm2
sudo npm install -g pm2

cd /var/www/interpreter
sudo env "PATH=$PATH" pm2 start ./lf-engine --name lockflare
sudo env "PATH=$PATH" pm2 startup
sudo env "PATH=$PATH" pm2 save

Day-to-day commands:

operations
# View logs
sudo env "PATH=$PATH" pm2 logs lockflare

# Restart interpreter
sudo env "PATH=$PATH" pm2 restart lockflare

# Stop interpreter
pm2 stop lockflare

# Monitor all processes
sudo env "PATH=$PATH" pm2 monit

systemd — alternative

run from /var/www/interpreter
sudo tee /etc/systemd/system/lf-engine.service > /dev/null <<EOF
[Unit]
Description=LockFlare Node.js Interpreter Engine
After=network-online.target
Wants=network-online.target

[Service]
User=root
Group=root
WorkingDirectory=$(pwd)
ExecStart=$(pwd)/lf-engine
EnvironmentFile=$(pwd)/.env
Restart=always
RestartSec=5
Type=simple
TimeoutStartSec=30
TimeoutStopSec=15
KillMode=mixed
KillSignal=SIGTERM
StandardOutput=journal
StandardError=journal
SyslogIdentifier=lf-engine
Slice=system.slice

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable lf-engine
sudo systemctl start lf-engine
Run that block from inside the install directory — $(pwd) is expanded when the unit file is written, so running it from elsewhere bakes in the wrong path.
operations
sudo journalctl -u lf-engine -f

sudo systemctl restart lf-engine
sudo systemctl stop lf-engine
sudo systemctl status lf-engine

Confirm it worked

Open Monitoring in Cipher. The server should appear online within a few seconds, reporting latency, heap and uptime.

If it stays unreachable, the cause is almost always port 80 — the control plane reaches the server by IP over HTTP, and it needs that port even when your site serves HTTPS. See Server requirements.

# Node Engine — Configuration # Node Engine — Configuration

The engine reads its configuration from .env in its install directory. Only LICENSE is required; everything else has a working default.

Settings

KeyDefaultWhat it does
LICENSE required The server’s licence key, bound to its IP address. Written on first run.
LF_DIR working directory Where locally-stored encrypted builds are kept, as lfapp-<project>_<env>.lfb.
LF_FORWARD_MAX_BODY Ceiling on a forwarded request body. Exceeding it returns 413.
LF_RESPONSE_GUARD_MS How long a dispatched request may run before the guard fires.
Your applications cannot read this file. fs reads of the engine’s own .env always return ENOENT, so the licence is not reachable from application code.

Your application’s own .env

Separate, and easy to confuse with the above. Your application’s .env travels inside the encrypted artifact. The runtime parses it and injects the values before your code runs, so require('dotenv').config() and require('dotenv/config') both work and return what you expect — they just do not read from disk.

Applications get a deliberately small OS baseline — NODE_ENV, PATH, HOME, TZ, LANG, LC_ALL, TMPDIR — plus their own values on top. The engine’s environment is never passed through, so no licence or platform credential can leak into an application.

NODE_ENV defaults to production if you do not set it.

The shared package set

Bare require() calls resolve against node_modules in the engine’s directory, shared by every application on the host. Install there and restart to add a dependency fleet-wide — see Node runtime limitations for the trade-off against bundling.

Ports

The Node engine listens on 3000 and expects a web server in front of it on port 80. That is fixed — change the proxy target in nginx rather than the engine.

Applying changes

after editing .env
sudo systemctl restart lf-engine
# or, under PM2:
sudo env "PATH=$PATH" pm2 restart lockflare
# .NET Engine — Installation # .NET Engine — Installation

The .NET Engine binds port 80 directly — there is no reverse proxy in front of it. It can also terminate TLS itself on 443.

Register the server in LockFlare Cipher first — add it to an environment and copy the licence key it issues. You need that key in step 2, and it is bound to this machine’s IP address.
Different from the Node Engine. Node listens on 3000 behind nginx; .NET binds 80 and 443 itself. Do not install a web server on a .NET box — it will take the port the engine needs.

Prerequisites — .NET 10 ASP.NET Core runtime

ubuntu 24.04
sudo apt update
sudo apt install -y libssl-dev libcurl4-openssl-dev

# Add Microsoft package repository (Ubuntu 24.04)
wget https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb

# Install .NET 10 ASP.NET Core Runtime
sudo apt update
sudo apt install -y aspnetcore-runtime-10.0

dotnet --info

Adjust the repository URL if you are not on Ubuntu 24.04 — Microsoft publishes one per distribution and version.

Remove debug tooling

The engine refuses to seal on a machine where strace, gdb or ltrace is installed. Those attach to a running process and read its memory, which is exactly the attack RAM-only execution exists to make expensive.

harden
sudo apt remove -y strace gdb ltrace 2>/dev/null || true

1 · Extract the interpreter

/var/www/interpreter
sudo mkdir -p /var/www/interpreter
sudo chown $USER:$USER /var/www/interpreter
cd /var/www/interpreter
wget https://cdn.lockflare.com/interpreter/lockflare-interpreter-net.tar.gz
tar -xzf lockflare-interpreter-net.tar.gz
rm lockflare-interpreter-net.tar.gz

2 · Run and configure the licence

Start it once by hand. It needs sudo because it binds port 80. On first run it prompts for the licence key.

first run
sudo ./lf-engine-dotnet
expected output
  ╔══════════════════════════════════╗
  ║     LOCKFLARE ENGINE (.NET)      ║
  ╚══════════════════════════════════╝

  First-time setup
  ─────────────────────────────────

  License key: LF-xxxxx-xxxxx-xxxxx-xxxxx
  Validating license...
  ✓ License validated

  Configuration saved to /var/www/interpreter/.env

[ENGINE] License verified
[ENGINE] Fetching interpreter...
[ENGINE] Fetching dependencies (10)...
[ENGINE] All dependencies verified (SHA-256)
[ENGINE] Launching .NET runtime...

[INTERPRETER] Bootstrap complete
[INTERPRETER] 1 domain(s) ready
[INTERPRETER] Listening on http://0.0.0.0:80

Every dependency is verified by SHA-256 before it is loaded. Stop with Ctrl+C once you see the interpreter listening — the next step makes it permanent.

3 · Auto-start with systemd

Required, not optional. The engine must come back after a reboot, and Enclave will not seal a server without a managed service.

run from /var/www/interpreter
sudo tee /etc/systemd/system/lf-engine.service > /dev/null <<EOF
[Unit]
Description=LockFlare .NET Interpreter Engine
After=network-online.target
Wants=network-online.target

[Service]
User=root
Group=root
WorkingDirectory=$(pwd)
ExecStart=$(pwd)/lf-engine-dotnet
EnvironmentFile=$(pwd)/.env
Restart=always
RestartSec=5
Type=simple
TimeoutStartSec=30
TimeoutStopSec=15
KillMode=mixed
KillSignal=SIGTERM
StandardOutput=journal
StandardError=journal
SyslogIdentifier=lf-engine
Slice=system.slice

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable lf-engine
sudo systemctl start lf-engine
Run that block from inside the install directory — $(pwd) is expanded when the unit file is written, so running it from elsewhere bakes in the wrong path.
operations
# Follow logs
sudo journalctl -u lf-engine -f

# Restart interpreter
sudo systemctl restart lf-engine

# Stop interpreter
sudo systemctl stop lf-engine

# Check status
sudo systemctl status lf-engine

4 · HTTPS with your own certificates optional

Only needed if this server terminates TLS itself. Behind a CDN or a reverse proxy that already handles HTTPS, skip this entirely.

certbot
sudo apt install -y certbot
sudo mkdir -p /var/lib/lockflare/acme

sudo certbot certonly --webroot -w /var/lib/lockflare/acme -d yourdomain.com

Then turn TLS on and restart:

enable tls
echo 'LF_TLS=1' | sudo tee -a /var/www/interpreter/.env
sudo systemctl restart lf-engine
expected output
[TLS] enabled — listening on 443, certificates from /etc/lockflare/certs (falling back to /etc/letsencrypt/live)
[INTERPRETER] Listening on http://0.0.0.0:80 and https://0.0.0.0:443
[TLS] loaded certificate for 'yourdomain.com' (expires 2026-10-21)
verify
curl -vI https://yourdomain.com/
Port 80 stays open after you enable HTTPS. The control plane reaches the server by IP address, so it cannot use a certificate issued for your domain — and certificate renewal needs it too. Closing 80 does not break your site; it breaks reloads, health reporting and sealing.

Confirm it worked

Open Monitoring in Cipher. The server should appear online within a few seconds, reporting latency, heap and uptime.

# .NET Engine — Configuration # .NET Engine — Configuration

The engine reads its configuration from .env in its install directory. Only LICENSE is required.

Settings

KeyDefaultWhat it does
LICENSE required The server’s licence key, bound to its IP address. The engine exits immediately without it.
PORT 80 The HTTP port the engine binds directly. There is no reverse proxy.
LF_TLS off Set to 1 to terminate TLS on this server.
LF_TLS_PORT 443 The HTTPS port, when TLS is enabled.
LF_TLS_DIR /etc/lockflare/certs Where certificates are read from. Falls back to /etc/letsencrypt/live.
LF_ACME_DIR /var/lib/lockflare/acme Webroot for ACME challenges during certificate issuance and renewal.
LF_DIR working directory Where locally-stored encrypted builds are kept, as lfapp-<appKey>.lfb.
LF_READY_TIMEOUT_SEC How long the supervisor waits for a tenant process to report ready before treating the start as failed.

Isolation controls

KeyWhat it does
LF_SPAWN Path to the privileged helper that joins the cgroup, unshares namespaces and drops privileges before your code runs.
LF_ALLOW_UNCONFINED Permits an application to start when confinement could not be established.
Leave LF_ALLOW_UNCONFINED unset in production. With it unset, an application whose namespaces, cgroup or dedicated user could not be established refuses to start — which is the behaviour you want, because the alternative is a tenant running with the host’s view of processes and files. It exists for diagnosing a confinement failure, not for working around one.

What the engine does at startup

  1. Prints its build identifier and feature list — useful when confirming which binary is deployed.
  2. Hardens itself: marks the process non-dumpable and sets RLIMIT_CORE=0, so decrypted tenant assemblies cannot appear in a core dump.
  3. Reads LICENSE. Exits with status 1 if absent.
  4. Reports its seal state to the control plane.
  5. Fetches and builds every application assigned to it, then reports how many hosts are ready.
  6. Binds PORT and starts serving.

Per-application limits

Each application runs under a cgroup with a memory ceiling and a process cap, created fresh on every launch. An application that exceeds its memory cap is killed and restarted rather than being allowed to exhaust the host.

If a previous generation’s cgroup cannot be drained, the engine refuses to restart that application rather than starting a second copy against a stale limit. That appears in the log as refusing restart: previous generation could not be terminated.

Applying changes

after editing .env
sudo systemctl restart lf-engine
sudo journalctl -u lf-engine -f
# Known limitations # Known limitations

Running from memory with no writable application directory rules some things out. This section is the list — what each runtime cannot do, and what to do instead.

None of it is arbitrary. Every constraint here is the direct cost of the property you came for: an application that is never written to disk cannot install packages at runtime, cannot write files next to itself, and cannot compile on the host. Those follow from the architecture rather than from an incomplete implementation, and they are the same constraints any in-memory runtime would face.

Most applications hit none of these. The ones that do usually need a small, one-time change — and it is much cheaper to know before a migration than during one.

The white paper covers the execution model in full detail — isolation boundaries, the shared package set, the staging disclosure and the failure modes.
Technical White Paper (PDF)

The three pages

Quick check

If your application does any of the following, read the relevant page before you plan a migration.

If your app…See
Writes uploads, caches or logs to local filesBoth runtimes
Uses SQLite or any file-backed local storeBoth runtimes
Installs npm packages at deploy timeNode.js
Uses raw http.createServer or WebSocketsNode.js
Compiles Razor views at runtime.NET
Reads Assembly.Location or a physical assembly path.NET
Calls a database or external API from .NET.NET
# Both runtimes # Both runtimes

Three constraints apply whichever runtime you use, and all three come from the same place: the application exists in memory and has no writable directory of its own on the host.

No writable application directory

There is no web root, no package directory and no build output on disk — so there is nowhere for the application to write next to itself. This is the property that removes web shells, dropped executables and path traversal as attack classes, and it is not configurable, because making it configurable would remove the guarantee.

In practice this affects applications that treat the local filesystem as storage:

PatternWhat to do instead
File uploads written to ./uploads Object storage — S3, R2, Azure Blob — or a database blob column.
SQLite or any file-backed local store A network database. A local database file has nowhere to live and would not survive a restart anyway.
Disk caches written at runtime In-memory caching, or Redis / Memcached.
Logs written to a file Write to stdout or stderr. The engine captures them, and they reach journalctl or PM2 like any service.
Generated files served back to users Generate in memory and stream the response, or write to object storage and redirect.
Most applications that write locally are doing it for convenience rather than necessity. The ones where it is genuinely structural — a media pipeline working on multi-gigabyte temporary files, for instance — are a poor fit, and it is better to know that on day one.

Everything served is memory-resident

Application code and static assets stay in memory for the life of the process. That is what removes disk from the serving path — and it means RAM is the resource to plan against, not storage.

On small artifacts this is negligible. On large ones — a big static bundle, a large dependency graph, many applications on one host — it becomes the dominant cost, and capacity should be sized for every served application being resident simultaneously.

If memory is already the binding constraint on your hosts, this architecture works against you rather than for you. Worth modelling before you commit rather than discovering after.

Build before you push

LockFlare serves the build you give it. It does not run your bundler, compiler or package manager, and there is no build step on the server — deliberately, because a build step would need a writable directory and a toolchain on a host that has neither.

Run npm run build, dotnet publish or whatever your pipeline does, then push. A stale dist/ deploys exactly as stale as it is.

Nothing about your build changes: same toolchain, same CI, same test suite, same review process. LockFlare consumes the output rather than replacing any part of it.

One entry point

Each project has a single starting file that the interpreter loads and runs. Applications structured around several independently-launched processes need to be either split into separate projects or given one entry point that starts what it needs.

Static assets are staged, and ephemeral

Where an application serves its own static assets, those are staged into a RAM-backed filesystem inside that application’s private mount namespace, for the life of the process only. It is volatile memory: invisible to the host and to every other application, never flushed to durable storage, and destroyed when the process exits.

The practical consequence is that anything written there is gone on restart. It is a serving mechanism, not storage.

# Node.js runtime # Node.js runtime

Two constraints are specific to the Node runtime: where packages resolve from, and what happens to your listen() call.

The shared package set

Bare require() calls resolve against a package set installed on the host and shared by every application on the fleet. It is LockFlare’s, it is fixed, and an application process cannot write to it.

You cannot add a dependency by pushing it. A bare require() for a package outside the set will not resolve at runtime.

The engine detects this and tells you exactly what is missing rather than failing with a bare module error. On startup it walks the application’s declared dependencies and logs any that will not resolve:

journalctl -u lf-engine
!! app   example.com ("Kiero") — missing npm packages on the fleet
!! app   fix: npm install stripe ioredis  (on the fleet node, then restart lf-engine)

Node built-ins are skipped, and so are dev-only tools like nodemon — only real runtime dependencies are reported.

Two ways to fix it

Install on the fleet node

What the engine recommends. Install into the shared set on the server, then restart the engine.

The package is then available to every application on that host — convenient on a single-tenant box, and worth thinking about if it is not.

Bundle into the artifact

Bundle at build time and the dependency travels inside the encrypted artifact, loading from the in-memory file map like your own modules.

Nothing to install on the server, and nothing shared with other applications.

install on the fleet node
cd /var/www/interpreter
npm install stripe ioredis
sudo systemctl restart lf-engine
or bundle at build time
npx esbuild server.js \
  --bundle \
  --platform=node \
  --target=node24 \
  --outfile=dist/server.js

Why it works this way: an application running from memory has no package directory of its own, so the resolver has to be provided by the host rather than built per application. The .NET runtime has no equivalent constraint, because NuGet dependencies travel inside the encrypted artifact.

Your listen() call is intercepted

The application loads into an isolated vm context, and its listen() call is intercepted rather than honored — no socket is bound. The runtime captures your handler and dispatches requests into it directly, routing by Host header.

For a normal application this is invisible. You write the same code you always would:

server.js — unchanged
const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('hello'))

// Called as normal. The runtime captures `app` and
// dispatches into it; no socket is actually bound.
app.listen(3000)
ModuleWhat the runtime does
express, koa, fastifyShimmed. .listen() is captured per context; your handler is dispatched into directly.
http, httpsShimmed the same way, so createServer(handler).listen() works.
fsShimmed for reads — see below.
clusterShimmed. Your primary/worker code runs against an in-process bus, not real IPC.
osShimmed. os.cpus() reports your licensed worker count, not the host’s physical cores.
dotenv, dotenvxShimmed to a no-op. Your uploaded .env is already parsed and injected before your code runs, so config().parsed returns it.
Everything elseResolved normally against the fleet package set.
The rule of thumb: if your application is a request handler with a framework around it, it works untouched. If it reaches past the framework to the socket itself, that part needs rethinking — no socket is bound.

Reading files

fs reads are served from your bundled tree first, so route auto-loaders, template directories and asset reads work as written. Beyond that there are two behaviours worth knowing.

ReadResult
A path inside your bundleServed from the in-memory map.
A relative path not in your bundleENOENT. Relative paths never fall through to the engine’s own directory.
An absolute path on the hostRead normally — the real filesystem.
The engine’s own .envAlways ENOENT. Your application cannot read the fleet licence.
Writes are not intercepted, and that is a trap. fs.writeFileSync('./uploads/x.jpg') succeeds — it writes into the engine’s working directory — but reading it back by the same relative path returns ENOENT, because relative reads resolve against your bundle. Anything written there is also outside your deployment and gone when the server is rebuilt. Use object storage or a database; see Both runtimes.

Workers and cores

The runtime forks clustered workers against the core count licensed to that server — four cores, four workers. Anything your application holds in process memory is per worker, not shared across them.

os.cpus() reports your licensed worker count rather than the machine’s, so code that sizes a pool from it gets the right answer. But in-memory session stores, in-process caches and rate limiters that assume a single process will still behave inconsistently across workers. This is the ordinary clustered-Node caveat rather than a LockFlare one, and the ordinary answer applies: move shared state to Redis or the database.

Isolation is language-level

Applications run in separate vm contexts within one runtime process — not separate OS processes with separate users and namespaces, which is what the .NET runtime provides.

That is a property of the JavaScript runtime, not something LockFlare introduced. It is the right boundary when the code on a host is all yours. Where applications on one host belong to different customers or different trust domains, use the .NET runtime or give each trust domain its own server.

# .NET runtime # .NET runtime

The .NET runtime carries fewer application-level constraints than Node — dependencies travel inside the artifact — but two things differ from a conventional host: nothing compiles on the server, and outbound network is closed by default.

No compilation on the host

A published application is decrypted in RAM and its assemblies load through in-memory assembly loading inside a collectible load context. No compiler runs on the server — there is no compilation surface at all, which is deliberate: a compiler is a code-execution primitive, and removing it removes a class of attack.

The consequence is that anything expecting to be compiled at runtime must be precompiled at publish time instead.

PatternWhat to do
Razor views compiled at runtime Precompile them. Razor compilation at publish is the default for ASP.NET Core — confirm it is not disabled.
Runtime code generation via Roslyn Not available on the host. Move it to build time, or restructure around it.
Plugin assemblies loaded from a directory at runtime There is no directory to load from. Ship plugins inside the artifact.

Assemblies have no physical location

Assemblies load from memory streams, so they have no path on disk. Code that reads Assembly.Location, AppContext.BaseDirectory or builds a path relative to the executing assembly will not find what it expects — Location is empty for an assembly loaded this way.

This usually shows up in code locating a config file, a template, or an embedded resource by path. Embedded resources loaded through Assembly.GetManifestResourceStream work normally, because they travel inside the assembly.

Configuration is delivered to the process environment in memory rather than as a file on disk, so read settings from environment variables rather than locating an appsettings.json on the filesystem.

Outbound network is closed by default

An application runs in an empty network namespace and cannot open outbound connections — not to a database, not to a payment provider, not to any external API. That is what removes the exfiltration path for compromised application logic, and it is the recommended posture for anything that does not need egress.

Most applications need egress, and it must be enabled deliberately. If your app talks to a database, turn it on for that environment before you push — otherwise every outbound call fails and the cause is not obvious from the application’s own error messages.

Enabling it changes only the network namespace. Private mount, PID, IPC and UTS namespaces, the dedicated unprivileged user, no_new_privs and cgroup caps all still apply — the application can reach the network but still cannot observe host processes or host files.

It is currently a per-environment switch rather than a per-destination allow-list. Narrowing that is planned.

Resource caps are enforced

Each application runs under cgroup limits on memory and process count. An application that exceeds its memory cap is killed and restarted rather than being allowed to exhaust the host — which contains the failure to the application that caused it, but does mean a genuinely memory-hungry workload needs its cap sized deliberately rather than left at a default.

What works unchanged

Worth stating, because the list above is all constraints. Your application’s own entry point is invoked, so it runs exactly as written — Razor Pages, MVC, Blazor Server, minimal APIs, Kestrel. There is no LockFlare code in your application and no SDK to adopt.

NuGet dependencies travel inside the encrypted artifact and load from memory, resolved entirely by your build. Stable ASP.NET Data Protection is provided without application changes, so antiforgery tokens and authentication cookies survive restarts despite nothing being written to disk.

# A server will not come online # A server will not come online

A registered server that never appears online, or drops out later. In practice this is one of four things, and the first is by far the most common.

1 · Port 80 is closed

Port 80 must be reachable on the server’s IP, even when your site serves HTTPS. The control plane addresses the server by IP address, so it cannot present a certificate issued for your domain. Closing 80 does not break your site — it breaks reloads, health reporting and sealing, and the server simply looks offline.
from your own machine
curl -sS -m 5 -i http://YOUR.SERVER.IP/ping

A response means the path is open. A timeout means a firewall, a security group, or a cloud network ACL is in the way — check the provider’s firewall as well as the host’s, since both have to allow it.

2 · The IP does not match the licence

A licence is bound to the address you entered when registering the server. If that address was mistyped, or the machine sits behind NAT and its public address differs from what you registered, the engine will start but never be recognised.

Compare the address on the server against the one in Cipher:

on the server
curl -sS https://api.ipify.org; echo

If they differ, remove the server in Cipher and register it again with the correct address. The licence cannot be repointed.

3 · The engine is not running

systemd
sudo systemctl status lf-engine
sudo journalctl -u lf-engine -n 100 --no-pager
pm2
sudo env "PATH=$PATH" pm2 logs lockflare --lines 100

A licence problem appears in these logs explicitly. So does a missing runtime — Node 24 or the ASP.NET Core 10 runtime.

4 · Node only: the web server in front is down

The Node engine listens on port 3000 behind nginx. If nginx is not running, port 80 answers nothing even though the engine is healthy.

node hosts only
sudo systemctl status nginx
sudo nginx -t
curl -sS -i http://127.0.0.1:3000/ping

If the last command answers but the public IP does not, the problem is nginx or the firewall, not LockFlare. The .NET engine binds port 80 directly and has no reverse proxy, so this step does not apply.

If the server is sealed, steps 3 and 4 are not available to you — there is no login. A sealed server that has gone quiet and does not come back on its own has to be rebuilt. See Understanding a LockFlare server.
# Reading the engine log # Reading the engine log

The engine log is the first place to look for anything that is not a network problem. Messages are prefixed by scope, and a !! marks a warning or an error.

follow the log
# systemd
sudo journalctl -u lf-engine -f
sudo journalctl -u lf-engine -n 200 --no-pager

# pm2
sudo env "PATH=$PATH" pm2 logs lockflare --lines 200

Scopes

ScopeCovers
boot / [FETCH]Licence, reaching the control plane, fetching and decrypting builds.
app / [TENANT]Starting and running your application.
local / [LOCAL]Storing an encrypted build on this server, free-plan storage.
enclave / [ENCLAVE]Sealing and unsealing.
fwdNode only. Forwarding a request between workers.
[CGROUP].NET only. Per-application memory and process limits.

Startup and licensing

MessageWhat it means
LICENSE not set in .env
[INTERPRETER] LICENSE not set in environment
No licence key. Run the engine by hand once to be prompted, or add LICENSE= to .env. The .NET engine exits immediately.
[FETCH] bootstrap rejected (bad license/IP?) The licence does not match this server’s address. Almost always a mistyped IP at registration, or a machine behind NAT whose public address differs. Re-register with the correct address — a licence cannot be repointed.
bootstrap failed — no data from Unity. Set LICENSE in .env and restart. Every attempt to reach the control plane failed. Check outbound connectivity from the server first, then the licence.
connecting to Unity (attempt 2/…) · retrying in 3s Normal transient retry. Only a concern if it never succeeds.
Failed to decrypt bundle from Unity The configuration payload arrived but would not decrypt. Usually a licence that has been rotated or replaced — re-register the server.

Fetching builds

MessageWhat it means
[FETCH] R2 decrypt failed (key/AAD/tag)The artifact did not authenticate. The build is bound to its project and content hash, so this is a mismatch rather than a corrupt download. Push again.
[FETCH] no local copy and no download URLThe server was told to use a locally-stored build that is not present. Push again to restore it.
[FETCH] no DLLs in app zip.NET only. The published output contained no assemblies — usually dotnet publish pointed at the wrong folder.
[FETCH] R2 blob too largeThe artifact exceeded the size ceiling. Check for a build output that has picked up something it should not, such as a node_modules or a source-map bundle.
stored copy unusable, discarding and refetchingSelf-healing. The local copy failed verification and the engine is fetching a fresh one.

Your application

MessageWhat it means
missing npm packages on the fleet
fix: npm install …
Node only. A bare require() for something outside the shared package set. The engine names the packages — install them on the server and restart, or bundle them into the artifact. See Node runtime limitations.
start failed — will retry on request (backoff) Your application threw while starting. The message that follows is your own error. The engine keeps the previous healthy instance serving and retries.
could not initialize The application failed before it produced a handler. Check the starting file is the one that actually launches your server.
forwarded but handler not ready (async .listen pending) Node only. A request arrived before the application finished starting. Harmless in isolation; persistent means something in startup never resolves.
cluster.fork() beyond licensed N worker(s) ignored Your application asked for more workers than the server is licensed for. Not an error — the extra forks are ignored. Add cores if you want them.
forwarded body exceeded … — 413 An upload exceeded the forwarding ceiling. Raise LF_FORWARD_MAX_BODY if the request is legitimate.
[TENANT] refusing restart: previous generation could not be terminated .NET only. The prior instance’s cgroup would not drain, so the engine will not start a second copy against a stale limit. Restart the engine.

Enclave

MessageWhat it means
HMAC verification failed A seal or unseal command failed authentication. Replayed or stale control traffic is rejected by design — retry from Cipher.
PARTIAL UNSEAL — 2/3 restored; enclave file RETAINED for retry Act on this. Some accounts were restored and some were not. The enclave file is kept deliberately so the unseal can be retried — retry it before assuming the server is open.
Failed to report status to Unity after N attempts The seal state changed but Cipher was not told. The server is correct; the dashboard may show a stale seal status until the next report.
breaking stale enclave lock (previous holder likely crashed) Recovery from an interrupted seal operation. Informational.
accepted the LEGACY fleet-wide key Something authenticated with an older credential. Working, but worth migrating that caller to the per-server credential.
On a sealed server you cannot read these logs — there is no login. If a sealed server has gone quiet and does not recover on its own, rebuild it. See Understanding a LockFlare server.
# A push fails or does nothing # A push fails or does nothing

Cipher checks four conditions before it will start a push, and reports which one failed rather than failing generically.

Push is disabled

ReportedFix
Source folder not found The folder moved or was renamed. Update it in the project’s General pane.
Frontend folder not found Same, for the attached frontend build. Often a dist/ that was cleaned and not rebuilt.
No environment selected Attach an environment to the project first.
No serving domain Add a domain for that environment. The interpreter routes by Host header, so it needs to know which requests belong to this project.

Push succeeds but the site is unchanged

Almost always a build that was not rebuilt. LockFlare packs the folder as it stands at the moment you push — it does not run your bundler. Run your build, then push again and check the version hash changes.

If the hash is identical, the content is identical, and the problem is upstream in your build.

Push succeeds but the server serves an error

The build reached the server and failed to start. Check the engine logs — the application’s own startup error appears there.

Common causes on a first deploy, all covered in Known Limitations:

A build that fails to become ready does not replace a healthy one. The previous instance keeps serving, so a bad push is not an outage — you have time to read the logs.

Only some servers picked it up

A push fans out to every server in the environment and each confirms independently. If one did not, that machine is still serving the previous build and the environment is no longer uniform.

Check it in Monitoring — an unreachable server is the usual cause. See A server will not come online.

# A domain is not serving # A domain is not serving

A domain returning nothing, an error, or somebody else’s project. The interpreter routes by Host header, so nearly all of these are a mismatch between what it was told and what arrives.

The domain returns nothing at all

  1. Does DNS point at the server? dig +short yourdomain.com should return the IP you registered.
  2. Does the server answer? curl -sS -i http://YOUR.SERVER.IP/ping — if this fails, the problem is the server, not the domain. See A server will not come online.
  3. Is the domain added in Cipher? Adding it in DNS does not tell the interpreter it exists. Both steps are needed.

The domain is added but serves an error

CheckWhy
Is it on the right environment? Domains are per environment. A domain added to Development will not serve from the Live servers.
Has the project been pushed to that environment? A domain with no build behind it has nothing to serve.
Exact hostname match? example.com and www.example.com are two entries. Add both if you serve both.

The wrong project is served

The same hostname is registered on more than one project, or on the same project in two environments whose servers overlap. Check the Serving Domains pane of each project on that server.

This is most likely after moving a domain between projects — removing it from the old one is a separate action from adding it to the new one.

Node: a 503 with a JSON body

expected during a reload
{"success":false,"message":"LockFlare Interpreter is restarting"}

That is the @custom503 block in the nginx config doing its job — the interpreter is briefly unavailable and nginx is returning JSON rather than an HTML error page. Transient during a reload; persistent means the interpreter is not coming back, so check the engine log.

Behind a CDN or proxy

The Host header has to reach the server unchanged. A proxy rewriting it to its own hostname, or to the origin IP, means the interpreter cannot match the request to a project.

In nginx that is proxy_set_header Host $host;, which the supplied configuration already does. On a CDN, check that host-header forwarding or host-header override is set to preserve the original.

# Certificates and TLS # Certificates and TLS

Certificate problems fall into two groups: the server terminating TLS itself, or something in front of it doing so. The first thing to establish is which one you have.

Port 80 stays open regardless. The control plane reaches your server by IP address, so it cannot use a certificate issued for your domain — and ACME renewal needs it too. Closing 80 after enabling HTTPS is the most common cause of a server going quiet while the site still loads.

If a CDN or proxy terminates TLS

Nothing on the server needs TLS at all. Leave LF_TLS unset, leave 443 closed, and let the proxy handle certificates. A certificate error in this configuration is a proxy problem, not a LockFlare one.

If the server terminates TLS (.NET)

Enabled with LF_TLS=1. Confirm what the engine actually loaded:

healthy output
[TLS] enabled — listening on 443, certificates from /etc/lockflare/certs (falling back to /etc/letsencrypt/live)
[INTERPRETER] Listening on http://0.0.0.0:80 and https://0.0.0.0:443
[TLS] loaded certificate for 'yourdomain.com' (expires 2026-10-21)
SymptomCause
No [TLS] line at all LF_TLS is not set, or the engine was not restarted after setting it.
Listening on 443 but no certificate loaded Nothing found in LF_TLS_DIR or the Let’s Encrypt fallback. Check the certificate exists and is readable by root.
Certificate loaded for the wrong hostname The certificate’s name must match the domain being requested. A certificate for the apex will not serve www unless it covers both.
Expired The log prints the expiry date on load. Renewal is a certbot concern; the engine reads whatever is on disk.

ACME renewal failing

Certbot writes its challenge into the webroot at LF_ACME_DIR, and the engine serves it over port 80. Both halves have to line up.

Log lineMeaning
[ACME] challenge not found The engine looked in its ACME root and the file was not there. Confirm certbot’s -w matches LF_ACME_DIR.
[ACME] could not read The file exists but is not readable. A permissions problem on the challenge directory.
the two must match
# what the engine serves from
grep LF_ACME_DIR /var/www/interpreter/.env    # default: /var/lib/lockflare/acme

# what certbot writes into
sudo certbot certonly --webroot -w /var/lib/lockflare/acme -d yourdomain.com

Renewal also needs port 80 open from the internet. If issuance worked once and renewal fails later, the firewall is the first thing to check.

Node hosts

The Node engine sits behind nginx, so TLS is nginx’s job — configure certificates there as you would for any site. LF_TLS does not apply.

Keep the port 80 server block reachable rather than redirecting everything to HTTPS, or the control plane loses its path to the machine.

# An app keeps restarting # An app keeps restarting

An application that starts, dies and starts again. The engine retries with backoff, so this can run for a long time without anyone noticing — the log is where it shows.

First: is it your code or the platform?

journalctl -u lf-engine -n 200
!! app   example.com — start failed — will retry on request (backoff): <your error>

Whatever follows the colon is your application’s own error, thrown while starting. The engine is reporting it, not causing it — and the previous healthy instance keeps serving while it retries, so this is not an outage.

Killed for memory (.NET)

Each application runs under a cgroup with a memory ceiling. Exceeding it kills the process rather than letting it exhaust the host — the failure is contained to the application that caused it.

A restart loop with no application error in between usually means this. Everything served is memory-resident, so the working set is your application plus its decrypted build and static assets.

Log lineMeaning
[CGROUP] memory.max not applied The limit could not be set. The application may be running unconfined — investigate before treating it as fine.
[TENANT] refusing restart: previous generation could not be terminated The prior instance would not drain, so the engine will not start a second copy against a stale limit. Restart the engine.
[CGROUP] still populated after kill — not deleting Something in that application will not die. Usually a child process. Restarting the engine clears it.
Raise the application’s memory cap, or reduce what it holds. If the working set is genuinely larger than the host, that is the trade this architecture makes — see Both runtimes.

Never becomes ready

The supervisor waits a bounded time for an application to report ready before treating the start as failed. An application doing slow work before it listens — a migration, a large cache warm — can exceed it.

LF_READY_TIMEOUT_SEC raises the window. The better fix is usually to start listening first and warm afterwards.

Node: restarts after every request

Not a crash. start failed — will retry on request means the application never started successfully, so each incoming request triggers another attempt. Fix the underlying startup error and it stops.

Restarts only under load

Memory that grows with traffic — a leak, or an unbounded in-process cache. Under Standard the working set is per worker, so what looks survivable on one core can hit the cap on four.

See Worker models: in-process caches are one of the things that behave differently once there is more than one worker.

# LockFlare Lens documentation # LockFlare Lens documentation Every screen in Lens, written down at the feature level: what it does for you, what it writes on the server, and what it refuses to do. It is the same record the product is built against, so a sentence here is a promise the app keeps. If you are new, read the doctrine first — it is short, and every other page assumes it. Two things worth knowing before the list. The **terminal is free**: any box you can SSH into is a console in Lens, with unlimited servers, unlimited seats and no licence — what is licensed is only what Lens does to a box as root, and every account includes ten of those free forever. And **nothing is installed on a server** to make any of this work; the one exception is Themis, who creates a single folder that is deleted when you withdraw her. Every section below has a page on the site with the screens beside it — [Terminals](/lens/terminals/), [Actions](/lens/actions/), [Security setup](/lens/security/), [Server management](/lens/system/), [Databases](/lens/databases/), [MongoDB](/lens/mongodb/), [Redis](/lens/redis/), [Containers](/lens/containers/), [Virtualization](/lens/virtualization/), [Websites](/lens/websites/) and [Themis AI](/lens/themis/) — or [every published screenshot in one place](/lens/screenshots/). - [The doctrine every feature follows](/docs/lens/doctrine/) — Air-gapped, nothing installed, jobs on the box, proved before kept. - [Identity and the gate](/docs/lens/identity/) — The pen drive, Touch ID, licenses, backup, travel copy, recovery. - [The console frame](/docs/lens/console/) — The map, groups, bastions, tabs, split panes, shortcuts, jobs. - [The terminal](/docs/lens/terminal/) — Logins, sudo, resume, tmux, recording, history, broadcast. - [Per-server readings](/docs/lens/readings/) — Overview, Monitor, About this server, updates. - [Security Setup](/docs/lens/security-setup/) — Firewall, Fail2Ban, SSH, passwords, WAF, integrity, MAC, kernel, scheduled, time. - [System Setup](/docs/lens/system-setup/) — Users, groups, certificates, auto-updates, audit, services, storage, network, logs. - [Tools on every server](/docs/lens/tools/) — Tunnels, the HTTP tunnel, templates, files, the bucket, backups, Themis. - [Setup Packages](/docs/lens/packages/) — Web, FTP, mail, containers, virtualization, the engines, cache, VPN, load balancer. - [The Databases tool](/docs/lens/databases/) — Every tab, replication for both frames, sharding, cluster backups, Mirror, reports. - [The chairs](/docs/lens/chairs/) — Terminals, Actions, Snippets, Server Templates, NOC. - [Themis](/docs/lens/themis/) — Authorization, fleets, asking, the model. - [Files](/docs/lens/files/) — Six panes, repositories browsed. - [Team](/docs/lens/team/) — Roles first, members last. - [Setup](/docs/lens/setup/) — Backup, duplicate, licenses, repositories, notifications, the model. - [Platform](/docs/lens/platform/) — macOS and Windows, no server, every protocol direct. The whole documentation as one file, for people and for models: [llms-full.txt](/llms-full.txt). # The doctrine every feature follows # The doctrine every feature follows **Air-gapped console.** Lens is a desktop app for macOS (Intel and Apple Silicon) and Windows. It talks to servers over the operator's own SSH logins and to nothing else. LockFlare runs no service Lens depends on: licenses are signed keys opened on the computer, the key drive is the identity, and the only server-side infrastructure LockFlare keeps is the license issuer. Nothing Lens does phones home; the model catalog for Themis is refreshed only from a button. **Nothing installed on the box.** Every reading is a command over the session already open; every change is a script run as root through the same sudo path. There is no agent, no daemon, no port opened for Lens. What must run while Lens is closed is a file Lens writes on the box — a cron line, a systemd unit, a hook — never a Lens process. Lens is a configuration tool that acts when the operator opens it, not an alerting system. **Every job runs detached on the box.** Long work (installs, restores, upgrades, data moves, cluster builds) is started detached on the box, in its own folder under `/var/lib/lens/jobs`, and survives Lens closing. The screen shows one lightbox listing the job's own steps with Stop; blocking jobs keep it up, others offer "Keep working". Multi-box jobs read the whole board first — shells, root, tools, room — and show what is in the way with the fix beside each line; nothing writes until the list is empty. **Proved before kept.** Changes that can lock the operator out (sshd hardening, a network address, a bridge) arm a revert first and are kept only after a fresh login or reachability check succeeds; otherwise the previous file comes back and the screen says "reverted". Firewall rules never remove the operator's own SSH cover; the SSH port is allowed before the firewall is turned on and cannot be denied. **Being logged in is the authentication.** No Touch ID per action. Root is the saved login's password through sudo, sealed under the drive key and asked once if missing. Touch ID is used only to unlock Lens and for the ceremonies (uninstall, wipe, duplicating the key), where Go asks a second time so no screen can skip it. **Consistency of the frame.** Every screen wears the same head: a state band that says the verdict in one line, tabs, findings worst-first with the fix on the row, a row that opens into the thing's own page, quiet background re-reads that never disturb an open form, and a job console with live output wherever something runs for minutes. ![The Themis chair in LockFlare Lens showing the servers it is allowed on and the ones awaiting authorization, with the footprint stated on screen](10-1-themis-servers-awaiting-themis-authorization.jpg "The doctrine, stated on the screen where the operator decides: nothing installed, one folder, deleted on withdrawal.") # Identity and the gate # Identity and the gate **The pen drive is the identity.** There is no login and no account screen. An encrypted Ed25519 key file on a removable drive, whose private half only decrypts under this machine's device fingerprint, is what unlocks Lens. Drives over 128 GB or Time Machine destinations are refused as keys with no override: a key is a stick small enough to leave with its owner. Multiple keys can live on one machine; a session pins to its key and other keys' servers are never listed. **Two factors at the door, key first.** The gate order is KEY, then Touch ID. With no key plugged Lens shows the "Welcome to Lens" door with "I have a key" and "Start a new account" — it never assumes a new account. Touch ID falls back to the account password on a Mac with no sensor or a closed lid, and to key-only where nothing can be evaluated; the degraded state is always said on screen. The Windows build has no biometric step. **Product activation key (LFL1.).** A new account opens with the activation key from lockflare.com/create-account, opened on this computer with the envelope key built into Lens and LockFlare's Ed25519 signature checked locally — nothing is asked of anyone. The key is written onto the drive beside the identity and carries the account: name, email, and how many servers are free. **Server licenses (LFS1.).** A server license is issued at lockflare.com for one IP address and carries that address inside it, signed. Adding a Lens server means pasting the key; the address comes out of it, nothing is typed twice. Three checks: at the door (a key issued to another account is refused, so accounts cannot be pooled onto one map), on the box (`/var/lib/lens/license` is read on every connect and kept right), and at the root door (every root path in the app asks whether this is a licensed server). The shell itself is never refused for a license. **Lens server vs console.** Two kinds of server go on the map. A Lens server carries a license and gets everything Lens does to a box as root: Security Setup, System Setup, Backups, the tools, the clusters. A console carries no license and no count: a box Lens is a terminal to — shell, Files, Tunnels, Snippets, History, everything the login can do by hand. Locked features stay on the rail, marked, and say what they are when pressed — never greyed, never silent. A console becomes a Lens server on Edit the day a key for its address is pasted. **Remove permanently / Remove and add another.** A licensed server leaves two ways and the license goes with it: REMOVE PERMANENTLY hands its remaining days back to the account as credit; REMOVE AND ADD ANOTHER moves the license and its time to a new address. Both finish on lockflare.com; servers removed with a license still pending are held in the store and counted on the map and in Setup › Licenses so a closed browser loses nothing. Renewal opens 60 days before a key ends. **Recovery bundle.** For the worst day (key lost, no backup, computer gone): lockflare.com/recover takes the account's email, password and one of its server addresses, mails a 6-digit code, and hands back a bundle — the account license and every server key as one string. Pasted at the gate, Lens writes a new key with the account inside and every server goes back on the map with its license. Lens sends nothing; the visit is the browser's. **Encrypted backup on the key (`.lf-backup.enc`).** One file on the pen drive holding everything Lens has: the whole map document (servers, groups, boards, snippets, tunnels, grants, templates, diagrams, clusters), the audit ledger, the certificate vault and the saved credentials, decrypted from the credstore at write time and sealed inside. The data key is wrapped twice: under the operator's passphrase (PBKDF2-SHA512 + AES-256-GCM, the recovery path that works on a new machine) and under the drive's deterministic signature (so the automatic rewrite while the key is plugged needs no passphrase). A fresh Lens with a verified key offers the restore at the gate, before anything draws. **Travel copy (`.lf-travel-.key`).** A key is bound to the machine that made it, and a new account elsewhere would be a new key id, invalidating every server license. The travel copy is the same key pair, id and account license sealed under a passphrase, written beside the encrypted backup. On a new computer the gate offers "Your key, on a new computer": it enrols the same key id there and, with the backup on the drive, brings the whole map back under the same passphrase. Setup › Your key makes, reseals or removes it, and the trade-off (possession + a secret instead of possession + a machine) is said on screen. **Duplicate my USB drive.** A locked, full-screen ceremony that makes a twin key for the drawer or the other bag. The key is read into memory first under Touch ID while the source is plugged, so the screen can say UNPLUG YOUR CURRENT KEY and mean it; the new stick is watched for, written, read back, and only then called ready. Nothing else in Lens can be clicked mid-ceremony and the key-removed overlay stands down because the key being out is the plan. **Key-removed lock.** Pulling the drive after unlocking raises an opaque overlay ("Connect your pen drive"): the unlocked state, the shells and every job survive underneath, and reinserting the key puts the operator back exactly where they were. A lock, not a logout. The overlay is opaque on purpose: what is behind it is exactly what the absent key protects. **Session lock.** A button in the title bar blurs the console past reading and asks for the second factor to come back. The key stays in, shells stay open, jobs carry on; keystrokes are swallowed at the capture phase so a terminal behind the blur cannot be typed into. Nothing dismisses it but the factor. **Welcome.** One branded moment per unlock: what this is, that you are in, and with which physical key. Skippable by any key or click; nothing load-bearing, no "sign in as" language anywhere. ![A seven-member Redis Sentinel set in LockFlare Lens with every wire proved](23-showing-all-42-wires-talking.jpg "Everything above is what one key drive unlocks: this map, these servers, these saved logins — none of it on a LockFlare server.") # The console frame # The console frame **Activity bar and chairs.** A slim strip on the far left, Visual Studio style: Servers, Themis and Files at the top; Team and Setup at the foot. Inside Servers, six chairs at the head of the tree: Servers (the map), Terminals (just shells), Actions (work run on the servers you are connected to), Snippets, Server Templates and NOC. Clicking the active icon folds the tree away (⌘B); the bar keeps the one control to bring it back. Lens always opens on the map; the chair is session-only. **The map.** The landing is the map itself: every group as a card with its servers as rows — the dot, the name and state, the address, what it runs, what it is waiting on (updates, a restart). Named groups, orphans under a per-key "My Servers", and a search box that finds a server by name, address, note or group and opens it. A whole group has Connect all, Check for updates and Update all on its head. ![Fifty servers on the LockFlare Lens map, in their groups, each with its state and what it is waiting on](50servers.jpg "The map: every group a card, every server a row with its state, its address and what it is waiting on.") **The tree.** Groups and servers with drag to reorder within a group and to refile between groups; right-click menus with the verbs (connect, open, edit, disconnect, remove, colour); the whole map or a group draggable onto the Actions board, a NOC diagram or a fleet. Servers added under another drive's key are listed as such and cannot be opened without that drive. The foot of the tree carries "Connect Directly when I Click a Server" (a saved login goes straight in, no form) and "On unlock, open a shell to every server on this map". **Colour bands.** Five swatches and none, set on the row or on Edit: a band painted on everything that shows this server — the row, the tab, the head, the terminal's edge — so nobody types the wrong command into the wrong box. Red is what people give production. **Bastion / jump hosts.** A server can be reached THROUGH another server on the map. Lens opens the bastion with its own saved login and carries the target's connection over it; the target's host key and login stay its own. One exposed box lets the rest close port 22 to the world. **Quick Connect.** A session that leaves nothing behind: nothing saved, nothing looked up, nothing written. The server exists in a group that lives while the shell does; the store never sees it and a backup does not know it. **Import from ~/.ssh and PuTTY.** Nobody with eighty hosts types eighty addresses: the OpenSSH config and PuTTY's sessions are read, listed with a tick each, and land as consoles into one group or into the groups the source named. Addresses already on the map are said and left alone. A key path is noted for the shell to suggest; the key itself is never read. **Adding several Lens servers at once.** Paste a key and the server it names lands in a list above the box — address from the key, a name and a port typed on the row — and the box empties for the next key. One button adds them all into the group this was opened from. **Connect all / Connect on start.** One button opens a shell to every server on the map (or in a group), the shells dialling in parallel. Per server, "Connect automatically when I open Lens" dials it at unlock without asking; a server whose login is gone, or which has two logins, is simply not in the unattended list — an unattended open that stops to ask is not one. **Sessions, tabs and split panes.** A server's sessions are pages in a strip; a page is one shell or up to four split across the pane (`1 2 / 3 4`), dragged there from the strip or opened straight into a split with a key. Splitting, moving or resizing a pane never reconnects the shell underneath — the session is the same one, only its place on screen changes. Tabs scroll, close, and remember where the operator was per server for the session — the screen you left is the screen you return to. **Per-server state.** Every rail tool stays mounted, one instance per server: A→B→C→A lands exactly where it was left. Only Monitor is not kept. **Keyboard shortcuts.** One table shared by the terminal and the console, so a shortcut is never announced in one place and swallowed in another. ⌘ on the Mac, Ctrl+Shift elsewhere because plain Control letters are the shell's. New/close session, next/previous, session 1–9, add pane / split below, find, clear, read again, hide sidebar. A help card shows the list for the section on screen. **Jobs lightbox and the working card.** A job that must not be left halfway (restore, upgrade, engine install, data folder on the move) stands in front of the whole chair while it runs, with the job's own steps, its log following the last line, copy-to-clipboard, and Stop. A synchronous change over ~1.5 s shows an unstoppable "working" card only for a guarded list of operator-pressed verbs, never for anything Lens does by itself. Past eight boxes the job's member cards go compact. **The ledger.** Every verb on a box writes its line to the box's ledger (one signed file a day under `/var/lib/lens`), read back on System Setup › Audit. When Lens says "there is none" it says what it checked. **Notifications from boxes.** One SMTP server described once in Setup › Notifications, sealed like a login; any box gets `/usr/local/lib/lens/notify.sh` and a root-only `/etc/lens/notify.env` so a cron backup at 3 am or an integrity change can mail the operator with curl (TLS, a login) and fall back to the box's own sendmail. A test mail from the box says what happened. **Toasts, the working badge and traces.** A status strip says what Lens is doing ("Lens is …") and pulses only for work on a server, never at a login form. A background MongoDB metrics trace keeps sampling after the operator leaves the tab and blinks a pill at the foot of the window until they are back. **Window.** Native title bar hidden; the gate opens door-sized and centred, and the window becomes the console the moment the workspace draws. Minimise, maximise/restore, close and a resizable sidebar (double-click to reset). # The terminal # The terminal **A plain SSH terminal.** A real terminal in the window, the operator's own credentials, sshd decides and sshd logs — and the pane says so: nothing here consults a policy or writes an audit chain. Host keys are never auto-accepted; the confirmation is the only thing between a session and a machine in the middle. Remote output travels base64 as bytes so a multi-byte character split by a read is put back together. **Logins.** Password, a key file (keys found in `~/.ssh` are offered so nobody types a path; the OS picker for the rest; passphrase asked and sealed), or the SSH agent, which is always offered and dropped for one server only when a refusal shows the agent burned its attempts. Every non-ephemeral connect saves the login, encrypted under a key only this drive on this machine can produce: Lens is an automation console, and a server with nothing saved is a server nothing unattended can open. Forget removes it by name. A login that came in on the agent alone is flagged as unreliable for unattended runs at the moment it is saved. **The sudo path.** Root is normally the saved login's password behind sudo, direct, with no touch. The fallback dialog asks the account's password once when none is on record (a key, an agent) or the login password is not the sudo one; it is kept with the remembered login and the next session is direct from the first click. First login to a box per Lens run raises sshd MaxSessions to 250 through the proven SSH change, because every check is an SSH channel. **Resume.** A dropped link — lid closed, Wi-Fi change, VPN falling over — does not end the pane. Lens knows why a session closed ("lost", not "exit"); on lost, with a saved login, the pane keeps its screen and dials again by itself, soon then more slowly, up to twenty tries, with a Stop on the strip. On a server kept alive with tmux the reconnect lands in the same shell. A shell ended by `exit` is not resumed. **Keep alive (tmux), recording, no history.** Per server on Edit: KEEP ALIVE runs every shell inside tmux on the box, so the next connect lands in the same shell with its command still running; RECORD writes every session as a timestamped asciicast (output only — what is typed is never written, so a password at a prompt is not either); NO HISTORY leaves Lens's command-history hooks out of shells to that server. The terminal keepalive ticks every 30 s and waits 45 s before calling a box gone. **Command history.** Every command typed into a shell Lens opened is filed on this computer per server under the key — the whole line as the shell ran it, its exit status, the folder, when, how long — reported by the shell itself through a hook added to bash or zsh at login, invisible on screen. Nothing is written on the box: the hook lives in a temporary folder for the seconds the shell takes to start and the shell removes it. Searchable, across servers if asked. A `sudo -i` shell is a different shell and is not hooked. ![Command history in LockFlare Lens with exit codes, folders, timings and Run again](history-auto-recording.jpg "Every command with its exit status, its folder and how long it took — reported by the shell itself, kept on this computer.") **Recordings player.** Recordings play inside Lens in a terminal of their own at 1×, 2× or 4× with the resize events shaping the terminal as the session was shaped, or in asciinema from the file. **Snippets menu on the bar.** The saved snippets, typed into THIS shell from its own bar; a root snippet is wrapped in `sudo sh -c` so a `&&` chain stays root all the way, and it is the server's sudo that asks. **On connect line.** Per server, a line typed into every new shell the moment it is live — `cd /var/www/app`, `sudo -i`, `tmux a` — under the operator's login. **Find in scrollback, session log, paste.** Find across the scrollback; the scrollback as plain text saved wherever the operator says; paste goes straight through and relies on the shell's bracketed paste so a pasted block waits for Return. **Look and feel.** Font size and theme remembered globally, not per host: a preference about eyes, not infrastructure. A different cell size re-tells the remote its grid so full-screen programs redraw. **Broadcast commands.** One thing typed once, sent into many open shells at once — the live version of Actions › Execute. It writes through the terminals already on screen, so what is sent shows up in each pane as if typed there. Two kinds: a COMMAND (with or without its Enter) and a KEY (Ctrl-C, q, Esc, Ctrl-D, Ctrl-Z, Ctrl-L, Tab, Enter, Space, arrows) because watching htop on ten boxes is only useful if you can quit it on ten. Targets: a split, a group, all; the local shell left out unless ticked. A floating panel dragged anywhere on the screen. ![Six terminals in LockFlare Lens each showing a different hostname and the same file, after one broadcast command](f-executed-on-all-six-terminals.jpg "One command typed once: six machines, six hostnames, the same file written a moment earlier by the same keystrokes.") **A shell on this computer.** The operator's own shell in a pty, in the Terminals chair, on the same wire as an SSH session. # Per-server: the readings # Per-server: the readings **Overview.** The control panel read over the live login in one round trip and refreshed quietly while on screen: load, memory, disk, what the box is, what is listening, who is on it, what is eating the CPU, the disks, the templates the box remembers (read from `/var/lib/lens` on the server), and the pending updates with Upgrade (all, or a ticked subset by name) and a signed Restart. On a database member the engine's state and its role in its set are read beside the vitals and change what the dangerous buttons do. **Monitor.** The box right now: every number a rate between two samples, six minutes of history, every core at once, network mirrored on one axis, hover charts, what is eating it, and a spike log — a number crossing a line is a mark on the timeline with the processes that were running at that moment. Runs only while looked at; nothing polls thirty-six boxes in the background and nothing was installed. **About this server.** Everything Lens knows how to ask a Linux box, asked at once: the Find out catalog's 209 curated reads (the ones needing no input) run as one script per group over one channel, filling in a section at a time. The whole reading is copyable as text for a ticket or a handover note. **Update badge, Check for updates, Update all.** Every server's row on the map and in the tree carries what it is waiting on ("N updates · S security", "restart required") from a reading taken the moment a shell goes live, with Update and Restart as verbs on the row. A group's Check for updates refreshes package lists on every box (connecting the ones without a shell, after asking) and Update all sends one job per box. A member of a replica set, a config set or a router does not get Update/Restart on its row: its updates go through the Databases section, where the safe walk does secondaries first and the primary stepped down last. # Security Setup (eleven tabs) # Security Setup (eleven tabs) **Overview.** How exposed the box is, read in one pass as cards: firewall, Fail2Ban, SSH, passwords, WAF, integrity, MAC, kernel, scheduled, time — each carrying the live state of its area and opening to what the tool actually said, with the two or three worst findings named in one verdict line. Root reads happen only while the screen is on screen; coming back reads again. ![Security Setup Overview in LockFlare Lens with a card per area and its live state](security-overview.jpg "One pass over the box: a card per area, each carrying the live state of its own tool.") **Firewall.** ufw or firewalld (iptables/nftables read and judged): on/off with the defaults explained, the rules folded (a v4 rule and its v6 twin are one line), "What is reachable" (every listening port judged against the rules), "Open to the world" (the named services), who may reach SSH, "Who is knocking" (refused connections folded by address) and the log. One dialog for a preset ("Open MongoDB"), a new rule, or "Who may reach SSH" — "to whom" as allow/deny rows, denies before allows, a preview, and the invariant that the operator's own address stays allowed. The SSH port is allowed before the firewall is turned on, cannot be removed while it is the only cover, and a rule that would deny it is refused in Go. **Fail2Ban.** Which doors are watched (SSH, web logins, mail, FTP, databases) as jails switched on click, the rule (findtime/bantime/maxretry in words, per jail or global), who is banned right now and why, who is never banned, who is refused for good, the activity folded over a window, the log, and alerts by mail through the box's own Postfix. Lens owns only `/etc/fail2ban/jail.d/zz-lens.local`. **SSH.** Hardening as a draft proven as one batch: Lens writes its drop-in, sshd validates it, sshd reloads, then Lens opens a FRESH login from this computer on the port now in force; if that fails the previous file comes back and the screen says reverted. Root off is refused unless somebody else can get in and become root; password logins go off only once the key alone has proven a fresh login. "Who can log in" lists every account's door (keys, password, AllowUsers/AllowGroups); "Your login" switches Lens's own login to one of your keys already on the door; "Who logged in" reads the sshd log folded by address with key names by fingerprint. Lens makes no key and holds none. **Passwords.** Password policy: what a password must be, how long it lives, what happens after too many wrong ones — and which accounts break the rules right now. Installing the policy packages asks first and says what they do. **WAF.** Two layers: INSIDE THE WEB SERVER, ModSecurity with the OWASP Core Rule Set in nginx or Apache (one form per engine, paranoia level, anomaly thresholds); AT THE DOOR, CrowdSec, which reads the logs, bans at the firewall for every port and shares reputations. CrowdSec's three levers a person actually pulls: what it reads (acquisition sources — a log file, a container's output, a unit's journal, each a Lens-owned file in `acquis.d`), alerts by mail through the box's Postfix, and scenarios switched between watch-only and banning. Your own address is allowed in one and whitelisted in the other, and neither can be told to block it. "What it saw" is the activity. **Integrity & rootkits.** Three checks, each a job you watch with its report saved on the server: packages against what the distro shipped (debsums/rpm -V), files against a baseline (AIDE), rootkits (rkhunter and chkrootkit). A schedule of timers Lens owns and a mail when something changed; the two "accept the current state" actions carry a sentence before, because they are the only ones that can hide an intruder. Findings are explained in words. ![Integrity and rootkits in LockFlare Lens: the package check, the AIDE baseline and the rootkit scan with their reports and schedule](integrity.jpg "debsums or rpm -V, AIDE, rkhunter and chkrootkit — installed, run, scheduled, and mailed when something changed.") **AppArmor / SELinux.** The kernel's second opinion: the denials first (a refusal is far more often a wrong label than an attack, and the screen says which, with the fix when Lens knows it from the meaning), SELinux booleans and labelling, AppArmor profiles, and what is listening with nothing confining it. **Kernel.** The standard sysctl switches against spoofing, probing and escalation, grouped Network / Kernel / Filesystem, read as they are and set through one Lens file live and at boot, every changed value remembered so Remove puts the box back exactly as it was. **Scheduled.** Everything the box runs on its own — cron in all its places, systemd timers, at jobs — with who runs it and from where, and the shapes persistence takes called out (this is the list a responder asks for first). An entry of the operator's own goes in `/etc/cron.d/lens`, the one file Lens writes and deletes lines from. **Time & entropy.** Is the clock right and who keeps it so (NTP on, the source); does the box have a real source of randomness. Informative, never a verdict on the screen. # System Setup (eight tabs) and Logs # System Setup (eight tabs) and Logs **Users.** Who has an account, what each can do, and what is wrong: the table (names, uids, shells, keys) plus the deep reading (lifetime dates, the exact rule that grants root, the state of every home, who is stale) with findings judged from both — a second uid 0, an empty password, a locked account whose keys still work, a sudoers file visudo refuses. Tabs Overview / People / System accounts / Root / Changes / Backups; a row opens into the account's own page (lifetime, root and how far, groups, how they get in, right now, cron, activity over a window, what they reach and run). Verbs: add (onboarding as one card with the line to hand over), lock/unlock, shut out completely (locked + expired + keys backed up and emptied + sessions ended), sudo given or taken by a Lens-owned allowlist rule, keys added ("one of mine" or "someone else's" with a name that becomes the key's comment), delete with what the account owns counted first, off-boarding as a flow in order. Off limits here and in Go: yourself and root. **Groups.** Not who is in what but what each group MEANS and whether the people in it should be: docker is root by another name, adm reads every log, shadow reads the hashes, a `%group` line in sudoers makes a forgotten group into root; a member locked a year ago still in wheel; a name that belongs to nobody. Overview / Groups / Power / Shared folders / Changes, a group's own page, several members added at once, a folder shared to a group (setgid walk on ask), root scoped by a Lens `%group` rule, delete with what it owns counted first. **Certificates.** What the web servers present, when each dies, whether it renews itself and — the part that goes wrong quietly — whether the box actually serves what is on disk, whether anything reloads the web server after a renewal, whether the name still points here, whether the key matches, whether the chain verifies. Obtain a real one (HTTP, or DNS validation through certbot's provider plugins or a guided TXT record through Lens's own hook — both allow wildcards and boxes closed on 80), request one from a CA (key and CSR made on the box, the CA's answer installed beside the key later), import one you already have (PEM or PFX), a self-signed one for a box nobody outside reaches, renew as a job, auto-renew with the reload hook Lens sets. The VAULT keeps every certificate and key on this computer sealed under the drive's key and carried by the pen-drive backup, so a wiped box or a deleted lineage is a "put back", not a new request to Let's Encrypt. **Auto-updates.** Whether the box patches itself and whether it actually STAYS patched: which repositories the automatic run may take from (third-party repos — Docker, a database — left out by default and aging for years), what is held back and why, what is pending with its source, when the timer really fires, whether a reboot or service restart is owed, whether the lists refresh, whether dpkg is stuck, what the last runs did. Settings (schedule, holds, restarts), Pending, Repositories, History, Log; one-shot reboot, undo, changelog, snaps, leftovers. Lens owns its own `apt.conf.d` files and a timer drop-in; the package's own files are never edited. **Audit.** Who did what on this box, three records: BECAME ROOT (every sudo command from the journal, each with a verdict — a root shell, a sensitive file, a download-and-run, a permissions change, an account change, or routine — the short list on top), WHAT WAS TOUCHED (auditd watching the files that matter — passwd, shadow, sudoers, sshd, the firewall, nginx, the certificates — naming who changed them even when it was not Lens, plus every program run inside root shells, and auditd's own health with the verbs that fix it), and LENS' LEDGER (every signed change Lens made here). Logs is the catalogue reader. ![Audit in LockFlare Lens: every sudo command from the journal with a verdict, plus auditd's watches and Lens's own ledger](audit.jpg "Three records: who became root and what they ran, what auditd saw touched, and Lens's own signed ledger.") **Services.** What systemd runs: failed first, then running, then enabled-but-stopped, the rest folded. A row opens to the unit's status and its journal; a service page shows the unit file, drop-in, capabilities, hardening, exposure. Start/Stop/Restart through the sudo path, plus per unit: a drop-in written, restart policy and resource limits changed in place, hardening applied, mask, reset-failed; Timers and Boot tabs. sshd under any of its names is protected from every verb. "Run as a service" is a form that writes a systemd unit with the preview as the point: nobody signs a unit they have not read. **Storage.** How close each disk is, what is taking the room, and what can be given back without losing anything (Space: the reclaim actions, each saying afterwards what the disk looks like now). Filesystems, Disks as they physically are, LVM (a group made, a volume carved, grown live), partitions, swap, fstab, Health (SMART). Preparing a disk is the one destructive thing here and says so twice. **Network.** What the box is called, the addresses it answers on, routes, ports, traffic, kernel guards, `/etc/hosts` — and what its names actually resolve to, from the box and from here, because a site that will not load is nearly always the service, the firewall or the name. Changing an address arms a revert on the server before anything is touched: if Lens can still reach the box afterwards it disarms it; if not, the box puts the old settings back by itself. Nobody drives to a data centre. **Logs.** Every log on the server by service — running units with their journal, files under `/var/log` and where the configs point, an Unknown group for whatever Lens cannot place — and a reader for any of them. # Tools on every server # Tools on every server **Tunnels.** Local, remote and proxy (SOCKS5 + HTTP on one port here, traffic leaving from the server) forwards riding the SSH login Lens already has, saved per server and started when the shell is up; "on connect" opens them by themselves. A list of what a tunnel could reach on this server (what listens on loopback, judged), templates for the things people tunnel to with their ports and whether they open in a browser, start · stop · open · copy · forget, and in words what a bind host means for coworkers sharing it (only this computer, one interface, the VPN, every network). SHARED ON YOUR NETWORK: with Advertise on, a tunnel bound to the office Wi-Fi is announced by mDNS/Bonjour as `_lens-tunnel._tcp` (and as `_http._tcp` when the far end speaks HTTP, so Safari's and Chrome's Bonjour lists see it as a plain web service); any other Lens on the same network lists it with the address ready to copy or open. Never announced wider than its bind. **HTTP Tunnel (Browse through this server).** One switch and this computer's browsers leave the internet from the box — its address, its network, its DNS. Underneath: the proxy tunnel plus the computer's system proxy pointed at it while on; off with the shell, the setting restored the moment it goes (and on next launch if Lens died with it on). One switch per computer: on for server B turns off for A and says so. A second door that never depends on the system: a browser window Lens opens (Chrome, Edge or Brave, its own profile, `--proxy-server` at the tunnel). A lookup box asks a name on the box and here and shows both answers. **Run Template.** A Server Template walked on THIS box (see §10), the run owning the whole screen. **Browse files.** The server one pane full width, riding the live shell's login as ordinary commands — nothing installed on the far side. Downloads and uploads through the system's own open/save panels; reads fall back to root on their own ("read as root"), changes as root through the saved password. Right-click a text file → Edit opens a code editor beside the pane (syntax colours guessed from the name, correctable); saving writes the bytes back with the mode and owner the file had, and says when a save had to become root to keep the owner. A permissions dialog in the order a person asks: what can I do here, who else and what (classic bits and the ACL as one list, the acl tools installed from here when missing), who owns it and how far it reaches. Bulk delete in one call; compress a selection into an archive on the box, extract one, or download a folder as an archive. **The bucket.** References to files on ANY server (or this computer), collected in one pane and dropped onto another server's folder: Lens relays them box to box through this computer over the two sessions already open, nothing on disk. Buckets can be saved by name — the recipe (server ids, folders, names), never the files and never a credential. **Backups (plans on the box).** The box is the record: every plan is two files on it and a cron line, the runner lives there too, so a backup at 3 am needs nothing from this computer. The screen reads every plan on the box as a card — what · when · keep · where · last run — with findings worst first and the fix beside each, Run now, the last run's log, Pause/Resume, Edit, Forget, and Bring back (what every destination holds for this plan, newest first; picked, pulled, opened with the key on this pen drive and placed where its kind's Put back looks). What: a folder, a website, a database (made on the engine's tab), mailboxes. Where: the repositories under Setup, set up on this box as rclone remotes (root-only, scoped at the store). Encryption before the push with age: the operator's backup public key on the box, the secret half sealed on the pen-drive key, so what a store receives is unreadable without the key. Mail from the box on failure through Setup › Notifications. **Themis AI on one server.** The AI sysadmin, inside Lens, with nothing installed: she reads through curated probes and a leashed read-only shell over the session already open, and proposes, never changes. Enabling her creates one folder, `/var/lib/lens`, where her reports, the baseline and the run ledger live — no agent, no daemon, and removing the folder removes every trace. Tabs: Ask (a conversation), Security report, Health, Forensics, On this box (the box's memory: enable, ask, report, compare), Settings. Proposals come one at a time with Run / Show me / No — the row says in words what Run does and why, Show me prints the exact script — and Lens never answers as Themis after a Run. A report is confirmed before it is spent because it is the operator's own account at their own provider. Answers render as a document — headings, lists, commands — never as raw text from a model. A sheet beside any page lets a container's logs, a failed job or a NOC wire be asked about where it was seen. ![Themis's security audit in LockFlare Lens with an exposure table and findings carrying their evidence](5-themis-security-report.jpg "The audit: every port with its firewall rule and who can reach it, findings with their evidence, and what she dismissed and why.") # Setup Packages: the software on the box # Setup Packages: the software on the box The hub is one card per package; a card is built or not, and an unbuilt card is never shown. Each install is one root job the operator watches, asked first with what the package does and does not do. A tool appears in the rail as soon as the box runs its daemon — never on hope. **Web (nginx or Apache) → the Websites tool.** Install nginx or Apache; from then on the daemon lives on the Websites tool's Overview (version, at boot, reload/restart/stop, the firewall's word on 80 and 443). Sites as THINGS — a domain, a folder, whether it answers (asked through the box itself), the certificate it presents and for how long, its own logs — not as the web server's files. New site is one click and one job: static files, PHP (a pool), a container, a reverse proxy, or an app on a port; folder, owner, certificate, redirect. A site opens into Overview / Settings / Names & redirects / Protection / Traffic / Backups / Logs (live). Editing is inline: a Lens site has its Settings form and its File, any other site only its File (Lens never rewrites what it did not make); a save asks whether to reload and the bar says so until nginx has read the file. HTTPS from three places: a certificate already on the box, Let's Encrypt through the acme folder every Lens site serves on 80, or the operator's own. Site protection writes one transaction (protect file, zones file, the include line once, the server's own test, everything put back on failure). **FTP (vsftpd or ProFTPD) → the FTP client.** The screen says the honest thing first: SFTP already works over SSH with no extra service. For the cases where the other side insists — an old camera, a client's upload script, a device with no SSH — vsftpd is the small, careful one Lens configures from the screen and ProFTPD the bigger one, installed and controlled from the same place: who may log in and where each lands (own folder, shared default, home — the row says it), TLS, the passive range, the daemon's file edited in place and judged by proftpd before it reaches the service. The FTP client (kept on disk, not on the rail today) logs in from this computer through the firewall and the passive range to verify the account works from outside, with the two-pane file manager once in. **Mail (Postfix, DKIM, Dovecot, rspamd) → the Mail tool.** Mail is thirty percent software and seventy percent reputation, so the package reads the six facts a receiving server judges the box by — outbound 25, PTR, hostname, SPF, DKIM, DMARC — says each in words and hands over the exact DNS records to copy, re-checked from the box until green. Mode 1, SEND ONLY: Postfix on loopback, DKIM signing, direct delivery or a relay (SES, Mailgun, Postmark, your own), which is what an app server, a WordPress or an n8n needs; who may send through the box (its containers, other servers by address on 25, logins on 587 behind TLS). Mode 2, RECEIVING: Postfix takes mail for the domains on 25 and hands it to Dovecot, which keeps it and serves it on 993; mailboxes and aliases (the catch-all as `@domain`), the MX checked from the box, clients that set themselves up (autoconfig). Spam filter (rspamd), logins & defence (the jails), activity (today, and what happened to a message), queue & test, find what happened to a message, a sending rate limit, the client's own IP hidden from the headers it sends, the raw log (postfix, dovecot, rspamd, opendkim) with follow-live. Per domain: DKIM key rotation in three steps weeks apart, and DMARC REPORTS — a collector mailbox per domain nobody can log into, the record pointed at it, the daily XML reports from Google/Microsoft/Yahoo/Apple parsed and folded by sending address, each named in words, so a domain owner finally sees who else sends as them. A mailbox's mail goes out to a file or down to this computer and back, and can be pulled in from elsewhere (migration). Mailbox backups ride the Backups tool. **Containers (Docker) → the Containers tool.** The runtime: Docker from docker.com (the distro's docker.io lags a year and ships no compose plugin) with compose and buildx, enabled at boot, a `daemon.json` seeded with log rotation and live-restore before the first start so no box set up here fills a disk with logs. Then: the service and the docker group (offered, not assumed — it is root in all but name), the daemon's keys that decide whether the box is still standing in six months (data root moved as its own job, an outbound proxy for the daemon, a prune schedule), the FIREWALL (docker publishes past ufw unless told not to; the ufw-docker block in `after.rules` and `ufw route` rules added and removed here, "to whom" first), and the registries the box is logged in to (the login happens in the terminal, the token never passes through Lens). The tool: Containers (running first, then the trouble; a row opens to its logs, details, stats — the monitor's charts for one process tree — and top), Stacks (compose projects: pull-and-up, the update check, effective config, the editor), Images (pull, build), Volumes (data out or in), Networks (new, attach), Disk. The Run form carries the long tail of `docker run` — basics, network, storage, resources, security, logging & health — one control a row; any container reads back into it ("Edit & recreate") or into the equivalent command line, and Recreate steps the old one aside under a temporary name until the new one is up. A shell into a container is a real terminal tab with `docker exec` typed in. Secrets are masked on screen. Stack templates: ready compose files for a small developer — pick the app, answer the three things that matter, running — every choice a `${VAR}` written into a mode-600 `.env` so no password sits in the YAML; databases bound to 127.0.0.1, admin passwords generated, logs rotated, world-facing ports said in their note. ![A volume opened in LockFlare Lens with back up, restore and browse the files inside](506-container-volumes-and-volume-options.jpg "A container is disposable; its volume is not. Back it up to a repository, restore it, or browse the files inside without starting anything.") **Virtualization (KVM/libvirt, LXD/Incus).** Readiness first and honestly: most VPSs cannot run KVM at all, and saying so before 400 MB of packages is worth more than anything below. Then guests as whole machines through virsh on the system connection: create from a cloud image (the distribution's own disk configured on first boot by cloud-init — login, key, hostname; the image downloaded once and kept beside the pool) or from an ISO with a screen (VNC bound to the server's loopback and reached through a tunnel, nothing exposed), start/stop/snapshots, what it is given (CPU, memory, another disk, another card — written to the definition AND to the running machine), its disks and interfaces (a disk grown, a NIC moved to another network), clone, resize, the console over the serial port in a Lens terminal, a backup, and PUBLISHING a port (the guest's address pinned in libvirt's DHCP, a forward that survives reboots through libvirt's hook Lens writes, the host's firewall let in — three things at once, which is why it is a feature). Networks: a libvirt NAT network touches nothing on the host; a BRIDGE rewrites the server's networking and goes through the same armed revert an address change does. Rates on a guest's page are honest differences of libvirt's running counters. **MariaDB, PostgreSQL, MongoDB (the engines).** Installed from here like the rest, secured on the way in (root through the socket and the password you give, no anonymous, no test database, bound to 127.0.0.1; PostgreSQL's socket keeps peer auth; MongoDB 8.0 from mongodb.org with the admin made and authorization ON from the first minute). The install offer reads the versions the box can install — its own repository's and the vendor's — live, and lets the operator choose; a box joining a pair or a set is installed at the SAME version the pair runs. Once in, the engine's screen is on the rail under its own name (§9). SQL Server is read on a box that already has it (read-only tabs) but is not installed or built out. **Cache (Redis or Valkey) → the Cache tool.** Redis is fast and famous for being left open to the internet, so the install lands it bound to this box, protected mode on, the password you give set on the default user from the first minute, maxmemory a quarter of RAM with allkeys-lru, users kept past restarts (an aclfile under the data dir). Lens's own `lens.conf` is included LAST from the main file so it wins; the login in `/root/.lens-cache`. Tabs: Overview with findings, Keys (SCAN pages, open, TTL, delete, flush), Performance (slowlog, clients, commandstats, read every 10 s while shown), Access (ACL users with roles full/app/reader), Replication (the Sentinel set), Connect apps (URL and a snippet per driver, the password never shown), Firewall (the engines' firewall view on the cache's port), Settings (13 keys, live + file, restart-due), TLS beside the plain port on 6380 with the box's own Lens CA, replica-of on the Overview, Save now. **Cache → Sentinel set.** One primary, replicas that copy it, a sentinel beside each; when a majority agree the primary is gone one promotes a replica and apps that speak Sentinel follow by themselves. Filed on this computer like the databases' sets (at least three boxes), members added by dropping servers from the tree and read in parallel (one connect for the whole drop, one read across all), the boxes without a cache installed first, built by one job with the firewall step before any replica is told to follow (each wire proved from the replica first); Failover now (the sentinels choose, the tab shows RESYNCING while the demoted primary catches up), Apply the filing (shown only when a box and the filing disagree), Remove, Disband, sentinel-aware connect strings. Redis Cluster (sharding) is deliberately not built. **VPN (WireGuard mesh).** A private network between the boxes: every member gets a private address on one subnet (10.99.0.0/24 by default) and every other member as a peer, so replication, backups and admin traffic ride private addresses and nothing crosses the internet in the clear. Filed as a network on this computer; one job on every box at once (tools, keys — the private half never leaves its box, loaded by PostUp and never written in the conf — the interface `lens0` (never `wg0`, so a WireGuard from before Lens is shown and untouched), every other member a peer on 51820/udp, enabled at boot, the port opened between members, every tunnel pinged from both ends); a members table with the tunnels as this box sees them, a ping matrix, live sync when only peers change (the old members never restart), Remove, Take the network down (keys kept), and the rules it opened closed on the way out. The OPERATOR'S OWN MACHINE as a peer: a key pair made in Lens's Go (X25519), the public half given to every box as a peer that dials in, the private half written once into a `.conf` saved where the operator says and shown once — Lens keeps no copy. The private 10.99 addresses are offered by default on the Cache set's, the SQL pair's and the Mongo replication page's drop rows. The mesh does not touch a box's real network: one extra interface and address, only 10.99 traffic routed through it, one UDP port opened. Proved on a sixteen-box mesh. ![A WireGuard mesh of fourteen servers in LockFlare Lens, every tunnel alive](409-wireguard-tunnel-with-14-servers-in-8-minutes.jpg "Fourteen servers, one private network, built in 8 minutes: a key pair made on every box, the private half never leaving it.") **Load balancer (HAProxy).** One box runs HAProxy and its FRONTS are filed on this console: a front drawn from a filed pair or set is filled at Apply from that filing — a writes port that always reaches the primary, a reads port over the replicas — and is REPOINTED by the same job that moves a primary (Promote, Failover), so apps that dial the balancer never learn there was a switchover; a pool front is a list of web boxes behind one address. Each backend checked every three seconds; HTTPS on the front (443 when on, the plain-http redirect riding along; the boxes speak plain http behind it); a dropped box read for what listens on its port; Apply writes the backends' firewalls from the same job through the shells it can open. # The Databases tool (MariaDB, PostgreSQL, MongoDB) # The Databases tool (MariaDB, PostgreSQL, MongoDB) The engine is the thing you enter: its own band, the same menu on every engine (Overview alone; Data, Access, Cluster, Settings as folded menus), what is behind each differing by engine, and a screen that is not there yet saying so. "Writes go elsewhere" is said once at the top on every tab of a secondary, standby, arbiter or read replica, with the door to the primary; a config server or shard member takes no data writes (its banner opens a router). The database login Lens keeps on the box (`/root/.lens-mongo`, `.lens-mongos`, `/root/.my.cnf`, the SQL equivalents) is tested first, kept on the box, never here. **Overview and "where this box stands".** The service and the three numbers; the uninstall beside the restart, away from the tab strip. Under it, the topology drawn small: nothing when the box stands alone, the set with this box outlined and the primary marked, the whole cluster with this box's piece outlined, a router with every piece of the cluster as a tile — each box's dot from what its own box said (green answered, red systemd says not running, yellow runs but did not answer, grey no shell asked). From here: "The set, maintained" (rolling restart, rolling upgrade, FCV for Mongo; the pair one member at a time, replicas first, primary last, for the SQL engines) and the servers' Linux updates walked the same safe order — config set, shards rolling, routers one at a time from a router's page. **Databases.** Every database as a thing: what it holds, what it weighs, who reaches it. Opened: every collection or table with its numbers, every index with its shape and whether anything has used it, what runs against it this second, the profiler and the slow operations MongoDB kept, its backups. Verbs on the row, the engine's own: a collection made (ordinary, capped, time series with its retention), renamed, emptied, compacted, validated or dropped; an index made, hidden or dropped; a validator written; a user given the database or made for it. New database makes one with a user of its own that sees nothing else. Why a verb waits is said beside it — never a grey button and silence. **Explorer.** MongoDB the Compass way: a collection, a filter in mongosh's own syntax, documents as a table or as they print, one opened and edited as text, a pipeline run, the collection sampled for its schema, the planner asked what it does, bulk actions and moving documents, a collection made from here. The SQL engines the Studio way: a table, its columns, the statement typed and run — reads free, writes behind a tick — rows as a grid with a cell changed where it is and a row added on the last line, filters as chips or a WHERE by hand, the columns edited in place, indexes, foreign keys, a table made from typed columns or from a file (its columns read off the header and the values). Export as JSON lines, CSV or Excel (which fields, which order) and import, each a job in front of everything while it runs. **Users.** Every account opened: each database it may touch and at what level, locked or not, how its password is hashed, whether it must use TLS, how many connections it holds right now, and the verbs that change each. A new user gets a password made here and shown once, or a certificate minted on the box; an app gets its own database and nothing else. MongoDB by its own role names and a catalog of custom roles (collection-level access), where a user may connect from (the only thing between a leaked password and anyone using it), roles resolved into "can do"; MariaDB's grant privileges; PostgreSQL's; SQL Server's fixed roles, read. **Connect apps.** The one page an app developer needs: what the apps connect to (this box, every member of the set, every router of the cluster), the URI, whether the door speaks TLS and the CA to trust it by (Download ca.pem), how to reach it from this computer (a tunnel), and the job that connects the app servers: drop the backends' boxes from the tree and one job lets each in on every door from the address it actually leaves from (`ip route get`), writes the CA at `/etc/lens/mongo/-ca.pem` when the door speaks TLS, proves the wires and prints the URI per box. For the cache: URL and a snippet per driver. **Backups.** Every dump of this engine on the box — mongodump archive, mysqldump, pg_dump, a SQL Server .bak — under `/var/backups/lens/databases` with a note saying what each is; Back up now as a job with three doors (kept on the box, brought to this computer and taken off the box, both); scheduled backups as plans on the box (the same cards and sheet as the server's Backups: which databases, collections or tables to leave out, when, keep N, where among the repositories, encrypted to the operator's key); Put back into the database it came from or into another name; download with the Files transfer; a dump uploaded from another box shows in the list and goes back the same way. The databases no plan covers are a finding. A shard member or config server is never dumped here — the cluster is backed up through a router. **Performance and Metrics.** What is slow on this member from the log mongod keeps anyway (every operation over slowOpThresholdMs, on every database, profiler or no profiler), grouped by the shape of the query — same fields and operators, whatever the values — costliest first: how often, how long, what the planner did, which app sent it; a shape that scans gets the index that would stop it, checked against the indexes the collection has (exists, covered, extends, new), and one click makes it. Metrics: operations per second by kind, connections, the WiredTiger cache, what is queued, the network, and for a set the lag of every member, sampled every two seconds while on screen and drawn like Monitor; the sampler the box keeps (the last hour to the last week, from the box itself), a TRACE that keeps sampling after you leave, and a report as one HTML file with the charts drawn into it. **Security.** Everything after the firewall, as verdicts in words with the switch on each row: who may authenticate and how (pg_hba.conf as PostgreSQL parsed it, MongoDB authorization), whether the wire is encrypted (the authority on the box and the engine's own certificate; certificate logins), which dangerous features are on, whether failed logins get an address banned, whether the files the engine trusts are readable by the wrong people; rows whose fix lives on another tab say which; "Lens let in" fixes the login Lens keeps on the box. **Firewall (per engine).** The engine's port as THREE SEPARATE LISTS read from every member through the shells Lens opens with their saved logins (all at once): the cluster's own addresses (how the members reach each other), external addresses let in on every node (the app servers — the driver may send them to any member, so all must agree), and this node only (a read-only replica's own clients, a reporting box); a fourth, folded, for what is on other nodes and not here. Keyed by IP with an "On" column naming where each is let in; a verb on one node touches that node only, written through the same guarded change the Firewall screen uses. "Add my current IP"; on a router's page any address goes on every router. Auto-set firewall (add-only, never removes an address) on a set's band works from `rs.status()` and needs no filing. **Settings and Advanced settings.** The engine's configuration as rows, not a file: each setting read from the engine itself, said in words for someone who does not know what `innodb_buffer_pool_size` is, with what the box's RAM says it should be, changed on the spot (a draft per row, applied together, restart-due read from the files). Settings is how the engine is used — where it listens, how many may connect, what gets logged; Advanced is the tuning under it — memory, durability, autovacuum, timeouts — and the file itself by hand. Lens writes ONE file of its own on the box, never the distribution's, and a restart is always the checked kind: if the engine does not come back the previous file goes back and it is restarted again. A restart on a set's primary steps it down first; on a SQL primary it asks instead of refusing. **Replication (MongoDB).** The server is the door, the cluster is the thing: a replica set is a relationship filed once (members, addresses, roles) and every member's tab shows the same page with "you are here". Members are added by dragging servers onto the Members table (a box without MongoDB is offered the install at the set's version; a box on a Lens VPN is offered its private address), read in parallel. Apply makes the boxes match — keyfile, bind, port, set name, checked restarts, every wire proved from the box, then `rs.initiate()` from here or `rs.add()` from the primary; "File this set" adopts a running set Lens never saved; set identity is `replicaSetId`, never the name. On the page: the set as a canvas (every member a box, every pair a wire on the port in both directions, proved every half minute until Stop, red with the reason), Step down on the PRIMARY's row, Remove, Auto-set firewall, Re-apply the filing, Disband; "The set, maintained": versions per member, rolling restart, rolling upgrade, FCV, the oplog read on every member and resized on all of them, compact walked member by member. TLS between members is a MUST and is Lens's: Lens is the set's CA (private keys never leave Go, the record on the cluster), rolling allowTLS→preferTLS→requireTLS, renewal without restart, Turn off. Everything long goes through the blocking lightbox with names from the first second. ![The replication page in LockFlare Lens: every member as it reports itself, with the set drawn on the wire](118-replication-screen-operations.jpg "Every member as it reports itself — role, state, and the set's own verbs on the same page.") **Replication (MariaDB and PostgreSQL).** The same page on the same frame: one primary and its replicas (PostgreSQL says standbys) filed once with a role per member; lead (this box is the primary, pick the replicas) or follow (pick the primary, this box is a replica). Apply makes the primary ready, lets every member in on the port, and each new replica seeds itself from the primary over that port from its own box (mariadb-dump for MariaDB, pg_basebackup for PostgreSQL) — no SSH key or relay between boxes — and follows; a member gets the engine at the SAME version the pair runs. Promote as a switchover or a failover, Rejoin, Remove, Disband the pair, Auto-set firewall; TLS between the members Mongo's way (Lens the pair's CA in the vault, one job across all members, Turn on / Renew / Turn off, apps untouched); "The pair, maintained" (rolling restart, rolling upgrade within the series, replicas first, primary last). A pair built by other hands is filed from what the primary sees. A MariaDB replica restart that changes its drop-in goes STOP SLAVE + RESET SLAVE, the checked restart, CHANGE MASTER TO again, START SLAVE, polled until it follows. **Sharding (MongoDB).** A sharded cluster is replica sets stacked, so the builder is drop boxes: config servers (fresh boxes), shards (servers, or a set already filed), mongos (the app boxes), each with a plain title and one hint line. Name it, Build: one job on the card — every set given the cluster's one keyfile and its role, mongos on each app box, addShard from a mongos — with every box read at once before it starts. After that the tab is the cluster's picture through a mongos on every piece: the doors and the map on the left, the shards on the right, a card per database with what is sharded and where it sits; shard a collection on a key (a blocking job with a "spread it now" tick; sharded collections as cards with per-shard bars and the balancer's verdict), chunk size, the balancer, Drain a shard (the uninstall ceremony), Grow (drop more boxes, Apply puts them in), Upgrade the set when a newer MongoDB is in the repo, Re-apply the filing, Disband. Routers: a mongos box is a "MongoDB router" engine with its own page (Overview, Databases, Explorer, Users, Backups, Sharding, Firewall, Settings, Advanced); a router's row shows `address:port · up/paused` with Start it (its own job that waits until `config.mongos` has heard from it), Pause/Resume, Detach (stopped, unit and login removed, its allow closed on every piece, its `config.mongos` entry cleared through another router). Cluster-wide TLS: one CA per cluster, Finish TLS skips a paused router with a line, whole-cluster rolling Restart, the Linux updates + reboots walk. The FIREWALL PASS is every box at once, one script per box per phase (on firewalld all rules in one `--permanent` and one runtime call, never `--reload`), a box with no progress named after 10 s, a held box given up on with what holds it. The canvas: mongos on the top row, the config set in the middle, the shards below wrapping three to a row, no wires among routers (a mongos never talks to a mongos); Confirm connectivity proves EVERY wire again, failed wires get five passes, yellow from the second, red on the fifth, nothing keeps checking. The log in a side lightbox: one box, a set's members by chip, or the whole cluster stitched newest first, MongoDB's JSON lines shown as words, Follow live, Ask Themis with the lines in hand. ![A nineteen-server sharded MongoDB cluster in LockFlare Lens with every wire proved](135-nineteen-servers-shard-finished-2.jpg "Nineteen servers, four shards, five config servers, two routers — built from the drop boxes in 43 minutes.") **Sharding → Backups (the cluster as one thing).** A shard holds one slice of every sharded collection and the config set holds the map, so the cluster's backup is every piece taken at the same time: the balancer paused through a router and any move in flight waited out, then the config set and every shard dumped whole with `--oplog` on a member of its own (a secondary with room, the primary only when a set has no other), all at once, into one folder named for the cluster and the moment, a manifest beside every piece and on the router that ran it; the balancer put back the moment the dumps end, whatever happened. Check reads the board first and shows what is in the way with the fix; the pieces stay on the boxes, come to this computer into one folder, or both. A database goes back through a router — its collections sharded again on the keys the manifest kept, every shard's slice loaded from the box that holds it, all at once. The cluster on a schedule is a plan that lives on a router. A REHEARSAL runs a cluster backup from a repository into this cluster whole, for fresh boxes. **Lens Mirror (MongoDB).** One database of another MongoDB copied into a database on THIS box, faithfully and one way: a snapshot to start, then the source's own change stream, seconds behind, resumable. The source is a connection string with a read-only user (tested for what it may read and for the write it cannot do, from the target box — the one that will be talking to it); the target gets `lensMirror` with readWrite on that database alone; both strings sealed under the key. The worker runs on the target box under systemd (Lens never in the data path) or inside this Lens; its place and its lease live on the target, so this Lens can close and another with the same record can go on, and on a replica set the primary's worker runs it and an election hands it over. A five-step wizard (the provider's door, the database and its collections, how and where, the copy as it goes, mirrored) with the copy's progress drawn; pause · resume · resync · forget; the worker's tallies on the Overview. Nothing installed on either side. **Reports (MongoDB).** Every report is what Lens already reads for a tab, drawn on screen — big numbers, tables, bars, matrices — with the PDF as a button for the person who does not have Lens (the same paper the audits use). Scopes: a box (overview, databases and collections, users and roles, security, firewall, configuration, backups, slow operations, everything), a set (the set, versions, the set on the wire, everything), a cluster (the cluster, sharded collections, TLS on every piece, the cluster on the wire, versions on every piece, everything). A report reads and never writes; the most it does is prove a wire. **Uninstall ceremony and the Danger Zone.** Taking a service off a box: Touch ID, the warning that says what goes and what stays (the program only with its data kept, or everything), what the package manager says would ACTUALLY go, the phrase typed by hand, Enter, and Touch ID again asked by Go, then the job in front of everything. An uninstall is this box's and nobody else's — no "every member of the set", no "every server of the group". The Danger Zone lists every product Lens knows how to take apart properly plus everything else the box collected since its disk was made, ticked by hand; "Undo everything Lens did" removes the tools it installed and every file and service it wrote, in dependency order with the firewall and Fail2Ban last so the box is never left unfiltered mid-run — and nothing else: not the kernel, not `/etc`, not a package Lens did not install, never an account. # The chairs in the Servers section # The chairs in the Servers section **Terminals.** Just shells: click a server on the left to open one here, split up to four on a screen, and one on this computer, side by side on one tab. No head, no rail. **Actions › Included Servers.** The working set built by dragging from the tree — the tree is the palette, the pane the board. A server becomes a card, a group its servers' cards, the whole map at once; a card is coloured by one thing — can Lens reach the box: green a live shell, yellow a saved login being tried, red "you will have to log in yourself" (a door to the connect form). Boards are saved by name (40) with a line (100), loaded, cleared; with thirty of them the shelf folds and a filter appears. **Actions › Execute.** One line, every server on the board, the answers side by side. A rail of the questions a Linux admin asks a fleet most mornings (uptime & load, disk, memory, busiest processes, failed services, OS & kernel, listening ports, recent logins, who is on, restart needed, recent errors, refresh package lists) plus the operator's own snippets, each a preset that fills the box — picking never runs, what goes out is always in front of you. "As root" takes the same sudo path every root action takes; `sudo` typed into the box is stopped and explained. Rows land as they finish, failed ones open by default. **Actions › Find out.** A question asked of every server on the board, answered as one value per server through the question's own parser — "port 21 open" comes back as yes/no and a list, not ss output — with the hits marked and counted, grouped by answer biggest group first (six distinct CPUs is the finding, not thirty-six rows). A catalog of 209 curated questions folded into Network, Services, Packages & OS, Users & access, Storage & load, Files & time, As root, with a search box; what a question needs (a port, a package, a threshold) asked above the button; the raw output a click away. ![Find out in LockFlare Lens: the CPU model of eighteen servers answered as three groups](15-finding-out-fleet-cpu-model.jpg "Eighteen servers, three processors. The grouping is the answer, not the output.") **Actions › Install.** The plumbing a server should have, put on the ones that lack it: pick a thing — security (Fail2Ban, ufw, firewalld, CrowdSec, ModSecurity, unattended-upgrades, auditd, AIDE, debsums, the rootkit hunters, AppArmor, SELinux, pwquality), web (nginx, Apache, Caddy, PHP-FPM, certbot), FTP (ProFTPD, vsftpd), system (chrony, rngd, the acl tools, the disk tools) — never MongoDB, Node or app software — and every server on the board is asked whether it has it — two columns, has it and does not — an Install button per row and one on top for all; the same root job the server's own tab would start, watched here, the row asked again afterwards so it says "installed · running" from the box's own mouth. **Actions › Set up.** One change, the same on every server on the board, through the same one door the server's own tab uses: add a user (password, a pasted public key, or a key pair made here with the private half saved where the panel says and never kept), give or take sudo, add an SSH key, lock/unlock, delete, add a group, put a user in a group, grant a folder, open or close a firewall port (the box read first, ufw on ufw and firewall-cmd on firewalld, SSH never stripped), turn the firewall on, set up Fail2Ban, harden sshd, run as a service, automatic updates, set the timezone, turn NTP on. Twelve at a time, a row per server saying what happened, sudo per row when a box asks. **Snippets.** The commands worth a name: one line or a short `&&` chain, a name, a line about it, and whether it wants root. Made, edited and forgotten here; run from Execute and from every terminal's own bar. Kept under this key; another drive sees them and cannot run or edit them. **Server Templates — the builder.** The hardening playbook as a workflow on a canvas: a palette of curated actions (set values, ask for values, update & upgrade, install or remove packages, nginx or Apache, kernel hardening, keep my IP on SSH, harden SSH, add a user, firewall baseline / open / close, install the WAF, install FTP, the audit tools, the integrity tools, enable AppArmor/SELinux, automatic updates, timezone, control a service, swap, wait, restart, log out, log in, end, and a custom script) and conditions that branch Yes/No (a package present, a service running, a user exists, a port answers, SSH answering, a file exists, a custom check read by its exit code). Drag or click to place, click to configure in the panel, wire by dragging a port or click-port-then-node, one wire per logical port, pan and zoom, fly back to Start. A library of shipped templates (Baseline, Web, Data, Security, Ops) opened as a new template of the operator's; templates saved on the map and key-stamped, no secrets. **Server Templates — the runner.** A template walked on a real box top to bottom as the canvas watches, owning the whole screen with a veil, the flow lighting up node by node, the box's own words beside it, Cancel the one way out until Close. Actions run as root through the same sudo path; conditions are read by exit code; Wait sleeps; Restart reboots and the session is understood gone; "SSH answering?" is a TCP knock; Login opens a new session and the run continues on it. The run pauses for values (Ask for values is asked all at once before the start), for a host-key decision, and on a failed step (retry, skip, stop). `{{my_ip}}`, `{{hostname}}`, `{{os}}`, `{{fw}}` and the rest are known to the runner. Steps are Debian/Ubuntu shaped; a box that is not says so at the first step that fails. What the box remembers of past runs is read from `/var/lib/lens` on it. **Server Templates — run in fleet.** The same template on several boxes at once: tick servers from the tree (a dot for what each has — a shell, a saved login, nothing), the template's questions asked once for the whole fleet, then one run per box in parallel drawn as a board — one column per step in flow order, one row per box, each cell the step's state (done, failed, waiting on you, running, skipped, not reached); Watch on a row opens that box's own run overlay. **NOC.** The map drawn as boxes with the ports they talk on: a node is a server dropped from the tree (its role icon read from what it listens on), a wire is "A talks to B on 27017/tcp" with the port as a chip. The drawing is a CLAIM and Check proves it: every wire is tried FROM the box the traffic starts at — reach (the source opened a TCP connection), listen (something on the destination listens on that port on an address the source dials), allow (the destination's firewall lets the source in) — green, amber or red with the failing fact in words; nothing is checked from this computer. Diagrams are saved like templates (key-stamped, no secrets); wires between the same boxes fan out, same-side wires arc over; a box under the mouse lights its wires; port chips tag what the picture is about; floating terminals over the canvas; a box or a wire opens in a side lightbox with Ask Themis carrying the diagram as background. Only servers Lens can log into are on a diagram, which is what makes every wire provable. ![A NOC diagram in LockFlare Lens with every wire proved from the box the traffic starts at](114-NOC-animated-diagram-showing-connections-ok.jpg "A wire is a claim; Check proves it from the box the traffic starts at — reach, listen, allow.") # Themis (the activity) # Themis (the activity) **Authorization.** Two lists, Authorized and Awaiting authorization, one verb each. Authorizing a server creates one folder, `/var/lib/lens`, where her reports, the baseline and the run ledger live — no agent, no daemon, nothing listening, no AI call and no cost; removing the folder removes every trace. Authorize All runs in the background, 5–10 in parallel, over shells already open (Lens will not open a connection on its own to change a machine). **Fleets.** A named SELECTION of servers drawn from anywhere on the map — two boxes from one group, one from another, three from a third — with the words Themis is given before she reads them. The picker is the map with checkboxes; drag works too. A fleet is never a parallel list of groups. **Ask across servers.** One conversation over many sessions: the fleet on screen, or whatever is ticked for a one-off question with nothing saved. Servers without a shell are opened first in one batch behind a lightbox that counts them in; the question is not sent until every one is up or has given up. Proposals arrive one at a time with Run / Show me / No, each saying in words what is authorized; a transcript is kept per fleet and one for the chair. ![Themis in LockFlare Lens proposing a change with Run, Show me and No](4-2-talk-to-themis-as-to-a-person-asking-follow-up-themis-offering-solution.jpg "A proposal, not a command to paste: Run, Show me the exact script, or No.") **The model she thinks with.** Set per drive under Setup: the provider (Anthropic, OpenAI, xAI), the model, whose account pays (the API key sealed in the credstore, never in the map document), and how much she may write (the model's own ceiling). The model catalog is a document Go holds — the one the build shipped with, or a newer one downloaded only from a button; Lens never calls home on its own. # Files (the activity) # Files (the activity) **One screen, six panes.** This computer, any server on the map, and every repository described in Setup, each a pane with the same shape — selection, sorting, crumbs, renaming, filter, context menu, a drag handle at the leading edge. Any pane to any pane is one copy call (upload, download and repository-to-repository are the same feature); layouts of up to six panes offered as every shape that leaves no column empty; Send to… names the destination instead of pointing at it. A copy runs as a job on the head's line, never blocking the panes, with its output kept on failure and Stop asking first (what it costs is one part-written file on the far side). **Repositories, browsed.** A repository (S3-compatible and its houses, Azure Blob, GCS, SFTP, FTP/FTPS, WebDAV, SMB, NFS, a server on the map, a folder on this computer) is described once and named in Setup, then browsed here — Lens speaks each protocol itself; a kind Lens cannot open yet is left off the picker rather than refused at the click. ![The Files chair in LockFlare Lens with a Cloudflare R2 bucket, an Azure container, a server, this computer and the bucket side by side](50-bucket-showing-R2repo-an-Azure_repo-a-server-and_local_computer-plus-a-bucket.jpg "Five panes: two repositories, a server, this computer and the bucket. Any pane to any pane is one copy.") # Team # Team **Roles first.** Named sets of capabilities from a curated catalog (restart/reload/start/stop a service, service status and the journal, read the logs, update packages, docker, read the firewall and sockets, reboot…), shown as the exact sudoers policy they write — allowlist-first, `/etc/sudoers.d/lens-`, "only these" units named on the card. **Members last.** A person minted the way Add a user does it (identity, key-only login, NOPASSWD sudo scoped by the role), then her servers as board cards — pick one and a role dropdown appears on the card; the card with its dropdown IS the grant. Saving stores the desired state and each assignment carries where it stands; the writer that makes the servers match it (create, key, sudoers; remove what was taken away) is the next piece. Everything key-scoped like the map; there is no "My Account" because the operator's account is the biometric and the pen drive. # Setup — the things you do to Lens # Setup (the things you do to Lens, never to a server) **A page per row, the button at the bottom.** Every Setup row owns a page that is the documentation — what this does, what it writes, what it will not do, what happens if it goes wrong — because these are operations done rarely, under pressure, on a machine that is not the one you set up. **Encrypted backup on my key** (§1), **Duplicate my USB drive** (§1), **Your key** (the travel copy, §1). **Licenses.** The account's license and one per Lens server, each with what it is for, the free-server count against the account, the pending removals to finish on lockflare.com — and a text file of them all. **File Repositories.** Storage described once and given a name (≤40), so every screen that has to put a file somewhere — backup plans, database dumps, exports, the Files chair — offers the name. Kinds: S3-compatible (AWS, R2, Backblaze, Wasabi, MinIO, Spaces, Hetzner, Scaleway, Storj… as presets that fill an endpoint and a region — one kind, not nine), Azure Blob, Google Cloud Storage, SFTP, FTP/FTPS, WebDAV, SMB, NFS, a server on this map, a folder on this computer. Secrets sealed like the saved logins and carried in the key backup; there is no Test button on purpose — Browse is the test, offered where there is a driver. **Notifications.** One mail server described once and sealed; set up on any box from its Backups so the box can tell you when something goes wrong. **Themis — the model she thinks with** (§11). # Platform # Platform **One app, two platforms.** Lens runs on macOS (Intel and Apple Silicon, one universal app, signed and notarized by LockFlare Corp) and on Windows (a signed installer and a portable exe). Same map, same key drive, same features on both; the Windows build has no biometric step and keeps the console shortcuts on Ctrl+Shift so the shell keeps its Control keys. Downloads and checksums come from lockflare.com; a build identifies itself in About and on the command line. **No server, no account, no telemetry.** Nothing Lens does depends on a LockFlare service being up. Licenses are signed keys opened on the computer, the model catalog for Themis is refreshed only from a button, and Lens never calls home. Everything Lens keeps — the map, the saved logins, the certificate vault, the repository secrets, the API keys, the audit ledger — lives on the computer sealed under the key drive's own signature, and rides the encrypted backup on the drive. **Every protocol spoken directly.** SSH to the boxes; S3, Azure Blob, Google Cloud Storage, SFTP, FTP, WebDAV, SMB and NFS to the repositories; MongoDB, MariaDB, PostgreSQL, Redis and Valkey through their own clients on the box. No agents, no vendor SDKs, no cloud in between.