2026-07-18 18:16:06 +05:30

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 false and nologin are rejected.
  • Xauthority cookies are generated in a 0600 file owned by the account, supplied to xauth through 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 --preview mode.

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.

Description
No description provided
Readme Apache-2.0 380 KiB
Languages
Rust 72.9%
Go 20.1%
Shell 3.5%
Makefile 3.4%