# 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`. ```bash 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](Dockerfile.test) and does not require `--privileged`. ## Releases The supported release pipeline is GitLab CI in [`.gitlab-ci.yml`](.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 ```bash 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 ```text latchd [OPTIONS] -c, --config Configuration TOML (default /etc/latchd/config.toml) -v, --variables Optional TOML variable file --tty Override the virtual terminal number --xsessions Override the X11 desktop-entry directory --wlsessions Override the Wayland desktop-entry directory --initial-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](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 ```text 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](LICENSE).