latchd
latchd is a Linux/PAM terminal login manager written in Rust. It presents a keyboard-first Ratatui login screen on a virtual terminal, authenticates through PAM, and starts an isolated TTY, X11, or Wayland session with an explicit per-user environment.
The Rust binary is the supported implementation. The original Go/cgo tree under legacy/go/ is retained as a quarantined comparison artifact and is not built by the default targets.
Security model
- The service requires root for real sessions and refuses to start from an existing desktop session.
- PAM work is serialized on one dedicated worker thread. PAM handles never cross that thread boundary; authentication tickets are opaque RAII-owned transactions.
- Password buffers are zeroized and are never placed in argv, logs, or the rendered UI.
- Session environments are rebuilt from an allowlist for every login. Shell commands are argv vectors; no configured command is passed through a shell.
- Desktop files and session scripts must be root-owned, regular, non-group-writable files. Account shells such as
falseandnologinare rejected. - Xauthority cookies are generated in a
0600file owned by the account, supplied toxauththrough stdin, and removed by an RAII cleanup guard. - Main, graphical-session, and Xorg logs are private (
0600) and opened without following symlinks. - Power actions are an explicit absolute-path allowlist and require confirmation. They are disabled in
--previewmode.
The implementation intentionally does not provide an administrator unlock shortcut: account lockout and authorization remain the responsibility of the configured PAM policy.
Build and test
Requirements: Rust 1.88 or newer, a Linux PAM development package, and a C toolchain. Debian/Ubuntu packages are libpam0g-dev, libpam-modules, and build-essential.
make deps
make build
make test # unit tests and rustdoc warnings
make lint # Clippy with warnings denied
make audit # RustSec dependency advisory scan
make docker-test # the same checks in a pinned Debian container
make package # deterministic install archive plus SHA-256
make release # all local gates, Docker, and package output
make build installs the release binary at build/latchd. The reproducible test image is defined by Dockerfile.test and does not require --privileged.
Releases
The supported release pipeline is GitLab CI in .gitlab-ci.yml.
Every branch and merge request runs formatting, tests, Clippy, rustdoc, a
RustSec dependency audit, and the Docker suite. Docker-in-Docker requires a trusted privileged GitLab Runner with
the runner client-certificate volume configured; untrusted fork pipelines
should not be assigned to that runner. A SemVer tag
such as v0.3.0 additionally builds a deterministic
Linux archive containing the binary, systemd unit, PAM policy, configuration,
license, changelog, build metadata, and a SHA-256 checksum, then publishes it
as a GitLab Release. The tag version must match the workspace version. CI
creates the GitLab Release only after both Rust and Docker verification pass.
Release builds use SOURCE_DATE_EPOCH, commit metadata, stripped symbols,
opt-level = 3, thin LTO, one codegen unit, no incremental state, and
panic = "abort". Set TARGET or DESTDIR when invoking make package for a
controlled package build; VERSION must match the workspace version. The
legacy Go tree is physically isolated under legacy/go/ and is never included
in release artifacts.
Installation
sudo make install
sudo systemctl enable latchd.service
Installation places the binary at /usr/bin/latchd, the configuration at /etc/latchd/config.toml, the PAM policy at /etc/pam.d/latchd, and the systemd unit at /usr/lib/systemd/system/latchd.service. The unit owns /dev/tty2; adjust TTYPath, After, and the tty configuration together if another VT is required.
Keep the previous package/binary available while validating a deployment. --preview is a safe UI exercise that does not authenticate, create a user session, or execute power actions. --no-tui (also --preflight) validates configuration and exits without entering the login screen.
Command line
latchd [OPTIONS]
-c, --config <PATH> Configuration TOML (default /etc/latchd/config.toml)
-v, --variables <PATH> Optional TOML variable file
--tty <N> Override the virtual terminal number
--xsessions <DIR> Override the X11 desktop-entry directory
--wlsessions <DIR> Override the Wayland desktop-entry directory
--initial-path <PATH> Override the session PATH
--preview Draw the UI without authentication or session startup
--no-tui, --preflight Validate configuration and exit
--no-log Disable the main log
--show-config Print the key validated settings and exit
--build-info Print version, commit, and build epoch and exit
-V, --version Print the version
An explicit configuration or variables file used by a real (non-preview) service must be root-owned, regular, and not group/other writable. The default configuration is optional: when /etc/latchd/config.toml is absent, built-in defaults are used.
Configuration
See extra/config.toml for the complete schema. Values may be substituted from a TOML variables file with $NAME syntax. Power entries contain an executable path and an argv array, never a shell command string.
Session discovery reads root-owned .desktop files from the configured X11 and Wayland directories and executable root-owned scripts from /etc/latchd/wms and /etc/latchd/wayland. Unsupported desktop-entry field codes are rejected rather than interpreted by a shell. A TTY shell is offered when no graphical sessions are available, or when include_tty_shell = true.
When enabled, the account chooser discovers eligible NSS users (by default,
UID 1000 and above with a real home directory and login shell). Ctrl-N and
Ctrl-P cycle accounts, clear the password, and start a bounded advisory
faillock query. The displayed lockout state never overrides PAM: a PAM
conversation remains the only authorization decision. Set
accounts.enumerate = false when listing local accounts is not appropriate.
The [visual] section is TUI-only. It can load a root-owned PNG/JPEG as a
downsampled terminal-cell wallpaper, run a low-rate cellular-automata ambient
layer, and show short-lived notifications. reduced_motion = true disables
motion while preserving the static layout; NO_COLOR=1 disables the image
layer and all color effects.
TUI controls
The screen is designed for 80×24 and remains usable down to 60 columns. Below the supported minimum it reports the required size without drawing a partial frame.
| Key | Action |
|---|---|
| Tab / Shift-Tab | Move focus |
| Up / Down | Move focus |
| Left / Right | Change session when the chooser is focused |
| Ctrl-N / Ctrl-P | Select the next / previous discovered account |
| Ctrl-B | Toggle the TUI-only wallpaper/automata background |
| Ctrl-M | Toggle reduced motion |
| Enter | Authenticate from the password field |
| Esc | Clear the password / cancel power confirmation |
? |
Show keyboard help |
| Ctrl-C | Restore the terminal and quit |
| F1–F12 | Confirm and run a configured power action |
NO_COLOR=1 requests a monochrome semantic style, and LATCHD_ASCII=1 selects ASCII session markers for terminals without reliable Unicode support.
Project layout
crates/
├── latchd-core/ validated config, accounts, sessions, and UTMPX accounting
├── latchd-auth/ PAM FFI, conversation callback, and transaction worker
└── latchd-tui/ Ratatui state machine, wallpaper, automata, and effects
rust/main.rs CLI, privilege checks, logging, and orchestration
extra/ example config, PAM policy, systemd unit, session directories
scripts/ Docker test entry point
legacy/
└── go/ non-production Go reference (explicit `make legacy-build` only)
Platform support
The first release targets Linux with PAM and systemd. BSD and non-PAM platforms are not claimed or tested. Real PAM/VT/X11 integration should be exercised on an isolated test host; the ordinary Docker suite covers compilation, unit tests, rendering tests, formatting, Clippy, and rustdoc without privileged container access.
License
Apache-2.0; see LICENSE.