Skip to content
Hallway is out (npm i -g hallway): v0, v1 and v2 have shipped, apart from checking it all on a real Mac. See the roadmap.

Setup, teardown and doctor

Every step that needs root lives in one command, hallway setup, so the daemon never needs privileges. You only need it for fronted mode: direct mode, the default, needs no setup at all.

v1 Run it once, when you want fronted mode:

Terminal window
hallway setup --print # show the full plan; run nothing
hallway setup # fronted mode on *.localhost
hallway setup --domain myname.internal # under your own domain instead

The plan is a numbered list, and each step is tagged with who does it:

  • [Hallway]: Hallway’s own files and user-level jobs, as you.
  • [you]: on macOS, brew install caddy (and dnsmasq for a custom domain) if they’re missing, as you.
  • [sudo]: a privileged change. The exact command is shown; a file to be installed as root is first written to ~/.hallway/generated/setup/ as you, shown in full, then copied into place with sudo install.

For fronted mode on localhost, it:

  1. installs Caddy if it is missing (macOS, with Homebrew; on Linux it stops and says how to get a release binary);
  2. writes Caddy’s configuration (generated/caddy.json) and the fallback page, and checks them with caddy validate;
  3. starts Caddy as you, as the user-level job hallway.caddy, on 127.0.0.1:4443 and :4480, with its admin API on a private socket;
  4. [sudo] trusts Hallway’s local certificate authority;
  5. [sudo] redirects loopback ports 443 and 80 to Caddy’s 4443 and 4480;
  6. sets mode: fronted in config.yaml;
  7. checks that https://hallway.localhost answers through the redirect, with Hallway’s certificate.

With --domain, it also writes and starts Hallway’s own dnsmasq (as you) and adds one more [sudo] step, the resolver entry for that domain. See Custom domains.

The rules it follows:

  • Each privileged command is printed with a one-line reason, then run with sudo, so you see exactly what happens; sudo asks for your password as usual.
  • It is idempotent. Re-running it checks the real state (the root in the trust store, the installed files’ exact contents, a TLS handshake on port 443) and only does what is missing; already-done steps say so. If a command fails, the run stops there, and running setup again carries on.
  • It refuses before running anything when the domain is .local, a TLD on browsers’ HSTS preload list (.dev, .app, …), .example or .invalid; when anything else holds port 443 or 80, or Caddy’s or dnsmasq’s ports (it names the process, its PID and command line); when a helper it needs is missing and can’t be installed; and on Linux, when there is no systemd, no nft or no system CA store it recognises. It warns about a real or made-up TLD, a Caddy older than 2.7, and another Caddy’s admin API on port 2019.

Serving https://slides.localhost means something must answer on 127.0.0.1:443, which only root may bind. The obvious answer, a Caddy running as root, has a problem: anything that can reach Caddy’s admin API can reconfigure it, for instance to serve files only root can read. A root Caddy would turn any malicious install script on your machine into a privilege escalation. So Hallway doesn’t run one.

  • Caddy never runs as root. It runs as you, as a LaunchAgent or a systemd user unit, on loopback ports 4443 and 4480, which need no privileges. It can do nothing you couldn’t already do.

  • Its admin API is a Unix socket in your data home (state/caddy/admin.sock) with mode 0600, never a TCP port. Only your own processes can reach it, and since Caddy holds no privilege, reaching it gains them nothing.

  • The only privileged changes are fixed, and read nothing you could rewrite later:

    1. The redirect of loopback ports 443 → 4443 and 80 → 4480. On macOS, a pf anchor (com.apple/hallway, in /etc/pf.anchors/hallway), loaded at boot by the hallway.pf LaunchDaemon; macOS’s own /etc/pf.conf already evaluates com.apple/* anchors, so it is never edited. On Linux, an nftables table of Hallway’s own (inet hallway, in /etc/hallway/redirect.nft), applied at boot by hallway-redirect.service.
    2. Trusting Hallway’s local certificate authority: in the System keychain on macOS, in the system CA store on Linux (plus your own NSS database, for Chrome, without sudo).
    3. For a custom domain only, the resolver entry: /etc/resolver/<base> on macOS, a systemd-resolved drop-in on Linux, pointing that one domain at Hallway’s dnsmasq, which also runs as you.

    Each is printed and run through sudo. After that, adding, changing or removing an app never needs sudo again: the daemon reconfigures Caddy through its socket, as you.

Rejected along the way: a root Caddy with its admin API on TCP or on a user-only socket (your own processes could still escalate), a root Caddy with no admin API (sudo for every new app), Caddy as you on 0.0.0.0:443 (every app exposed to your network), and on Linux setcap on the Caddy binary (whoever reaches its API could bind any privileged port). See No root in the daemon.

If you set proxy.publicPort to the same value as proxy.httpsPort (4443) in config.yaml, setup installs no redirect: links then carry the port (https://slides.localhost:4443). Trusting the certificate still needs sudo once. See Fronted mode without the redirect.

macOSLinux
Caddy, as you~/Library/LaunchAgents/hallway.caddy.plist~/.config/systemd/user/hallway-caddy.service
dnsmasq, as you (custom domain)~/Library/LaunchAgents/hallway.dnsmasq.plist~/.config/systemd/user/hallway-dnsmasq.service
The redirect (sudo)/etc/pf.anchors/hallway, /Library/LaunchDaemons/hallway.pf.plist/etc/hallway/redirect.nft, /etc/systemd/system/hallway-redirect.service
The trusted root (sudo)the System keychainthe system CA store (e.g. /usr/local/share/ca-certificates/hallway-local-ca.crt); your NSS database, as you
The resolver (sudo, custom domain)/etc/resolver/<base>/etc/systemd/resolved.conf.d/hallway.conf

Caddy’s configuration, certificates and state, dnsmasq’s configuration, and the files staged for sudo all stay in the data home.

v1 Undo it any time:

Terminal window
hallway teardown --print # show what it would remove; run nothing
hallway teardown
hallway teardown --uninstall-packages # also uninstall Caddy and dnsmasq from Homebrew

It removes only what it finds: the resolver entry, the dnsmasq job, the redirect, the trusted root and the Caddy job, with sudo for the privileged ones, then sets mode: direct. The dashboard keeps working at http://localhost:4255. Homebrew packages are left alone unless you ask; on Linux, --uninstall-packages only reminds you to use your package manager. The certificate authority stays in the data home, unused, so a later setup can trust the same root again.

Terminal window
hallway doctor
hallway doctor --json

Read-only diagnostics. Each check prints pass, warn or fail with a suggested fix.

v0 The daemon, your data and your services:

  • the daemon is running, its port is free or its own, and its service (launchd job or systemd user unit) is loaded;
  • on Linux, whether lingering is on, so services keep running without a login session;
  • the data home is readable, and lists any YAML parse or validation errors;
  • two declared apps on one port;
  • apps listening on 0.0.0.0, which exposes them to your network;
  • managed-service jobs that are missing or orphaned, whose PATH contains per-shell version-manager directories or entries that no longer exist, or whose Node version has been uninstalled (see the PATH trap).

v1 Fronted mode, when it is on:

  • Caddy: its version, its job, its admin socket answering and private, and the configuration it is running matching generated/caddy.json;
  • who holds Caddy’s ports and ports 80 and 443 (process, PID and command line), if it isn’t Caddy;
  • a TLS handshake through the redirect that verifies against Hallway’s root, and the HTTP-to-HTTPS redirect;
  • the root trusted, with a note for Firefox;
  • *.localhost resolving (a warning only: browsers resolve it themselves);
  • for a custom domain: its TLD (delegated or made up, checked against IANA’s list), dnsmasq running and bound to loopback only, the resolver entry in place, and both <base> and a name under it resolving to your machine.