narrowd is a security-hardened, single-user SSH daemon written in Rust and
built on russh. It gives one local account everything needed for remote work:
an interactive shell, command execution, SFTP, and TCP forwarding through
standard ssh and scp clients.
Its focused model is the point. narrowd leaves out multi-user account
management, legacy protocols, and policy machinery it does not need. The
result is a compact remote-access service with modern cryptography, bounded
pre-authentication resource use, and a separate post-authentication executor
process on Unix.
- public-key authentication against one
authorized_keysfile - interactive shell as the daemon process user
execrequests through a configurable shell wrapper (ExecMode)- SFTP backed by the local filesystem
- local and remote TCP forwarding
- automatic
authorized_keysreload with last-known-good fallback - user-scoped deployment on Linux and Windows
narrowd is purpose-built for one trusted user, not a reduced clone of a
multi-user sshd. Its deliberately small scope keeps operation and review
straightforward:
- one local account per daemon instance; use
suafter login if local policy permits switching users - configuration changes take effect after a restart
- no legacy SCP protocol; current
scpclients use SFTP by default - no X11 forwarding; use Waypipe over a TCP tunnel instead
Every accepted key is trusted to act as the daemon process user. A successful
login therefore receives shell, exec, SFTP, and forwarding access with that
account's permissions. This explicit trust boundary is the foundation of the
single-user design.
- Exact identity mapping. The SSH login username must match the daemon process user's account name. Other usernames are rejected rather than silently mapped to that account.
- Modern cryptography. The public-exposure profile accepts Ed25519 host keys and Ed25519-family user keys, a narrow set of modern KEX algorithms, ciphers, and MACs, and no SSH compression.
- Built-in admission controls. Global, per-IP, and per-subnet connection caps, per-IP rate limiting, temporary bans after repeated authentication failures, short banner and KEX timeouts, and an absolute login deadline bound unauthenticated resource use.
- Process separation on Unix. A dedicated executor owns shells, PTYs, SFTP state, and forwarding sockets. The network-facing SSH parser communicates with it over a constrained control channel.
- Parser sandboxing on Linux. After spawning the executor, the parser
applies
no_new_privs, a default-deny seccomp allowlist, and a Landlock filesystem sandbox. It cannot execute programs and sees only the read-onlyauthorized_keysdirectory tree, while authenticated sessions retain the account's normal local capabilities. This path requires Landlock support from the running kernel. - Resilient key reloads. The in-memory
authorized_keyscache reloads automatically after file changes. A failed reload is logged while the last known-good cache remains active. - Account-level SFTP. SFTP follows the daemon user's filesystem permissions. It is not chrooted or limited to a separate subtree, because a trusted key already has shell access as the same account.
- Plain allow-or-deny keys. Entries with OpenSSH options such as
command=,from=, or forwarding restrictions are rejected completely rather than treated as unrestricted keys. - Trusted forwarding. TCP forwarding is deliberately permissive when enabled, so accepted keys can use the host as a tunnel endpoint.
For unrestricted shell access to the same local account, narrowd and OpenSSH
grant broadly comparable post-authentication permissions. The difference is in
their focus: narrowd offers a compact single-user design with a tightly
controlled pre-authentication surface, while OpenSSH offers a far broader
policy and compatibility surface backed by decades of audit and operational
history.
OpenSSH remains a mature, heavily reviewed implementation, but its history also shows the security cost of that breadth:
- the 2024
regreSSHionsignal-handler race made unauthenticated root code execution possible on some systems after a 2006 fix was accidentally lost during a logging refactor; - an experimental client roaming feature, enabled by default despite having no released server counterpart, could disclose client memory including private keys;
- forwarded
ssh-agentaccess could be combined with PKCS#11 shared-library loading to execute code on the client; and - protocol, policy, and legacy-tool interactions have caused weaknesses such
as the Terrapin attack,
forwarding settings that did not
enforce all documented restrictions, and
unexpected local file writes by
scp.
These problems did not have one root cause. Some involved C memory or asynchronous-signal safety, while others came from protocol details, legacy compatibility, powerful forwarding features, helper processes, or the large configuration matrix. OpenSSH mitigates these risks with extensive review, privilege separation, sandboxing, conservative defaults, and a long record of rapid fixes. Its history is therefore an argument both for its maturity and for keeping an SSH service no larger than its actual job requires.
narrowd also benefits from Rust's safety model. One of the most direct and
historically productive paths from unauthenticated network input to arbitrary
native code execution in traditional C daemons has been memory corruption:
buffer overflows, out-of-bounds access, use-after-free, double-free, and
related pointer errors. Successful exploitation may turn these faults into
code injection or code-reuse attacks that take control of the process.
Safe Rust removes this path from narrowd's own implementation structurally
rather than merely making it less likely. Ownership and the type system
prevent use-after-free, double-free, and data races, while bounds checks turn
invalid indexing into a controlled panic instead of unchecked memory
corruption. The narrowd library and binaries all use
#![forbid(unsafe_code)], so this guarantee cannot be bypassed accidentally
inside the project's own code.
For a network-facing service, that language-level protection is reinforced by four layers:
- Memory-safe project code. Classic native memory-corruption primitives
are unavailable in
narrowd's own safe Rust implementation. - A narrow integration surface.
narrowdomits PAM, GSSAPI, X11, agent forwarding, runtime provider loading, and other subsystems that its single-user use case does not need. - Parser isolation. The network-facing parser and post-authentication
executor are separate processes on Unix, and Linux additionally confines
the parser with
no_new_privs, seccomp, and Landlock. - User-level execution.
narrowdis intended to run as the target account rather than as a privileged root daemon, so a compromised process starts with that user's permissions instead of immediate control of the machine.
Rust is not a complete RCE prevention mechanism. Logic errors, command injection, authentication bypasses, unsafe dependency code, and operating system flaws still require review and timely updates. Even if such a flaw remains, however, an attacker must cross additional process, sandbox, and account-privilege boundaries to progress from network input to full system control. The path to a complete takeover is therefore materially longer.
For narrowd's stated use case—occasional remote access to a personal machine,
development box, lab host, or VM—the smaller model is fully sufficient when
every accepted key is trusted to act as the local account and the service and
host are kept up to date. This is especially valuable for occasional operators
and less-experienced administrators: the strong baseline does not depend on
mastering OpenSSH's full policy language or remembering to disable unrelated
subsystems. With fewer features and fewer ways to weaken the intended model
accidentally, a well-maintained narrowd deployment can achieve a stronger
effective security posture than an OpenSSH installation operated without
comparable SSH expertise.
That is a comparison of realistic deployments, not a claim that narrowd is
universally safer than current OpenSSH in expert hands. Choose narrowd when
its trusted-key, single-user model matches the deployment. Choose OpenSSH when
you need multi-user administration, per-key restrictions, legacy
compatibility, or an implementation mandated by policy or compliance.
- personal remote access to your own machine, dev box, lab host, or VM, whether reached over a LAN, VPN, or a deliberately exposed SSH port
- occasional operation by people who need dependable personal SSH access without becoming experts in a general-purpose SSH policy engine
- setups where every accepted key is fully trusted to act as the daemon process user
- environments where unrestricted shell and port forwarding are desired features rather than policy violations
- sensitive infrastructure protected by an additional trust boundary such as Tailscale, WireGuard, a VPN, or a strict source-IP firewall
- a general-purpose internet-facing SSH service for multiple users
- systems that rely on
authorized_keysrestrictions or fine-grained SSH policy enforcement instead of plain allow-or-deny keys - locked-down SFTP-only environments, chrooted file access, or reduced-blast-radius account separation
- environments that require a formally audited SSH implementation or OpenSSH's long operational track record
For the detailed threat model and completed public-exposure hardening checklist
under the "attacker has no stolen key" assumption, see
docs/public-exposure-roadmap.md.
- GitHub Actions covers formatting, clippy, rustdoc with warnings denied, Linux tests on stable and beta, Windows tests on stable, a macOS build check, and a Debian package smoke test.
- Security automation also includes
cargo audit,cargo deny, CodeQL analysis, and GitHub dependency review on pull requests. Dependency review fails closed when the repository dependency graph is unavailable. cargo auditruns without advisory suppressions, and CI asserts that the vulnerable RSA crate does not re-enter the resolved dependency graph.
cargo run -- --print-sample-config
cargo run -- --check-config
cargo runBy default narrowd looks for ~/.config/narrowd/narrowd.conf, generates an
Ed25519 host key if one does not exist yet, and authenticates against
~/.ssh/authorized_keys.
./scripts/build-deb.sh
sudo apt install ./target/debian/narrowd_*.deb
mkdir -p ~/.config/narrowd
cp /usr/share/doc/narrowd/examples/narrowd.conf.example ~/.config/narrowd/narrowd.conf
sudo loginctl enable-linger "$USER"
systemctl --user enable --now narrowd.serviceThe generated package is intentionally set up for a systemd --user service,
not a root-owned system daemon. The packaged launcher refuses to start as
root, so the SSH session, SFTP access, and port forwarding all run with the
permissions of the target login account. no_new_privs is applied by the main
narrowd process itself after it has started the separate post-auth executor
process. The packaged user service intentionally does not use
RestrictAddressFamilies= because systemd applies that with seccomp, which
forces NoNewPrivs=1 onto the whole service tree and would break post-auth
tools such as sudo.
powershell -ExecutionPolicy Bypass -File .\Build-NarrowdMsix.ps1
powershell -ExecutionPolicy Bypass -File .\Install-NarrowdMsix.ps1For the purpose of the MSIX mode, how it differs from the Session 0 service,
and how automatic Windows logon fits in when you want the user session to come
up on its own after boot, see
docs/windows-session-modes.md.
The MSIX build produces a signed package and companion certificate under
target\msix. Installing it registers a per-user startup task, so narrowd
starts automatically in the signed-in user's own session after the next
logon instead of running in Session 0 as a machine service. The package keeps
its config, host key, and logs under %LOCALAPPDATA%\narrowd, and the first
launch writes a default config that points AuthorizedKeysFile at
%USERPROFILE%\.ssh\authorized_keys, uses ExecMode powershell, and listens
on TCP 2223 by default.
The packaged manifest also declares the default inbound TCP 2223 firewall
rule. If you later change the port in %LOCALAPPDATA%\narrowd\narrowd.conf,
adjust the firewall rule manually so it matches the new port.
cargo build --release
powershell -ExecutionPolicy Bypass -File .\Install-Narrowd.ps1See docs/windows-session-modes.md for when
to choose this Session 0 service versus the MSIX user-session mode.
That older installer still exists when you explicitly want a native Windows
service with Log on as a service, but the MSIX route is the better fit for a
user-session daemon that should come up automatically when the user logs in.
ssh -N -T -o ExitOnForwardFailure=yes \
-L 127.0.0.1:13389:127.0.0.1:3389 \
-p 2222 \
your-login-user@narrowd-hostThen connect your RDP client to 127.0.0.1:13389.
If the Windows machine is not the same host that runs narrowd, replace the
target side of the -L argument with the address that is reachable from the
narrowd host, for example -L 127.0.0.1:13389:10.0.0.50:3389.