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