# Deploying a Locked-Down Host
This page is a practical recipe for shipping a Deft appliance: a
pinned host, a curated launcher (or a single full-bleed app via the
`:chrome :app` variant in step 1), confined apps, and the roadmap for
the remaining hardening.
## 1. A Vendor Prelude
# /opt/vendor/.deft/prelude.dft %Prelude{ :mounts %{ "stdext" "/opt/vendor/packages/stdext/src" "deft/stdext" "/opt/vendor/packages/stdext/src" :launcher "/opt/vendor/packages/acme-launcher/src" } :pkgs @{ "/opt/vendor/packages" } :lockdown %{ :chrome :none :layout false :launcher "launcher/launcher.dft" :apps @{ "pos" "inventory" "reports" } :images-only true :app "/opt/vendor/packages/acme/pos/pos.dftc" } :sandbox %{ :readonly @{ "/etc" "/usr" "/opt/vendor" } :writable @{ "/var/lib/acme" "/tmp" } } }
### Single-app variant (no launcher)
For a single-app appliance — one full-bleed app, no launcher — use
the `:chrome :app` presentation and name the app in the policy:
# /opt/vendor/.deft/prelude.dft %Prelude{ :pkgs @{ "/opt/vendor/packages" } :lockdown %{ :chrome :app :app "/opt/vendor/packages/acme/pos/pos.dftc" :images-only true } :sandbox %{ :readonly @{ "/etc" "/usr" "/opt/vendor" } :writable @{ "/var/lib/acme" "/tmp" } } }
The host ignores workspace specs and starts the app in a single panel
below its own title/menu strip (which still carries the app's
`[tui/set-menu]` menus). Closing the app exits the process, so run it
under a service manager with restart-on-exit (`Restart=always` in
systemd, `respawn` in OpenRC); the `distro/` image supervises its
session the same way. The launcher and workspace sections below apply
to the multi-app variant.
## 2. A Vendor Launcher
The launcher is an ordinary Deft app that lists only your apps and
calls `[tui/launch]`. The host enforces the same allowlist, so keep
the two in sync — or drive both from one list:
def apps @{ %{ :name "pos" :title "Point of Sale" :path "acme/pos/pos.dft" } %{ :name "inventory" :title "Inventory" :path "acme/inventory/inventory.dft" } %{ :name "reports" :title "Reports" :path "acme/reports/reports.dft" } } def launch {| app | [tui/launch [tui/self] $app~name $app~path] }
Make it the prelude `:launcher` source (or the `:lockdown :launcher`
path) and the stock launcher never appears.
## 3. A Pinned Workspace
Optionally define the exact panel layout the vendor ships. Lockdown
applies the spec at startup and refuses all later changes:
# .deft/workspaces/pos.dft %{ :title "POS" :split :horizontal :ratio 0.7 :children @{ %{ :app "pos" :source "acme/pos/pos.dft" } %{ :app "inventory" :source "acme/inventory/inventory.dft" } } }
With `:images-only true`, point each `:source` at the installed image
path (e.g. `/opt/vendor/packages/acme/pos/pos.dftc`) — a source `.dft`
path would be refused at load. (The pinned workspace does not apply
under `:chrome :app`, which ignores workspace specs.)
## 4. Confinement
Lock the process down at the OS level as well:
```sh
# Landlock filesystem sandbox (read-only/writable paths from the prelude)
DEFT_SANDBOX=1 retra pos
# Or in an autostart unit:
Environment=DEFT_LOCKDOWN=1
Environment=DEFT_SANDBOX=1
ExecStart=/usr/bin/retra pos
````
For the single-app variant the workspace name is unnecessary — the
policy `:app` names the app, and restart-on-exit turns the unit into
the appliance supervisor:
```sh
# systemd unit for the single-app variant
Environment=DEFT_SANDBOX=1
ExecStart=/usr/bin/retra
Restart=always
````
Recommended additionally:
- A dedicated unprivileged user with no shell, autologin only into
the kiosk session.
- A read-only root filesystem (image + overlay for `/var`), with
write paths listed in `:sandbox :writable`.
- On the Linux console, keep `TERM=linux` so `retra` uses the GPM
pointer path (the appliance image already bundles gpm).
- No package manager, compiler, or terminal app in the deployed
package set.
Landlock is filesystem-only today; it does not restrict network
access.
## 5. AOT Images
Compile each app to a source-less image so the device carries no
editable code:
```sh
deft compile --app acme/pos -o /opt/vendor/packages/acme/pos
deft compile --app acme/inventory -o /opt/vendor/packages/acme/inventory
````
The bundle directory (image + assets + manifest) drops into the
`:pkgs` root and the launcher discovers it. With `:images-only true`,
any leftover source `.dft` is refused at load. For the single-app
variant, point the policy `:app` at the image file itself (e.g.
`:app "/opt/vendor/packages/acme/pos/pos.dftc"`, or a mount-relative
path that resolves to it) so the initial launch satisfies
`:images-only`.
## 6. The Appliance Image
The workspace's `distro/` build assembles an Alpine + Deft image with
the POS prelude, Launcher, and gpm already wired. Use it as the
starting point for a custom vendor image or ship the host binaries and
packages on your own base.
## Roadmap
Lockdown today is UI-level. The planned layers complete the stack:
1. Runtime lockdown — a per-runtime restriction propagated to app
threads and child runtimes: disable `eval`/`reload` and source
loading, refuse package installs, config writes, and shell
execution, and optionally compile the host without the compiler.
2. Signed images — Ed25519 signatures over `.dftc` payloads,
verified before load, with a vendor trust store and key rotation.
3. Licensing — vendor-signed license claims (customer, expiry,
features) embedded in or shipped beside the image, optionally
machine-bound or leased online, enforced by the locked host.
Licensing is a commercial control, not DRM: pair it with signed
images and a read-only system.
Until those land, treat lockdown as a strong UX guardrail and put the
real boundary at the OS: dedicated user, read-only filesystem,
Landlock, no shell, and no developer tools on the device.
## See Also
- Configuration — every policy key.
- Behavior — the exact gating and the current security
posture.