Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Part of the [Keep](https://github.com/privkeyio/keep) ecosystem; the node daemon
- **Threshold custody**: the box holds one FROST share; steal it and get nothing.
- **Multi-node HA**: nodes sync, so a single failure doesn't take your vault down.
- **Seedless**: recovery via a device quorum, no 24 words to lose.
- **Hardware-backed admin**: optional YubiKey/FIDO2 SSH, PIN + touch on every login.
- **Open**: MIT software on commodity hardware.

## Quick Start
Expand Down Expand Up @@ -77,6 +78,36 @@ HA), see [Hardware bring-up](docs/hardware.md).

To open the Vaultwarden web vault before the mesh is set up, tunnel it over that SSH session (it binds localhost only): `ssh -L 8222:localhost:8222 keepadmin@<node-ip>`, then browse `http://localhost:8222` (localhost is a secure context, so the web vault loads). Then onboard the node onto the encrypted mesh and redeploy with `keepNode.adminAccess.lanBringup = false` for the mesh-only posture.

### YubiKey / FIDO2 (optional, recommended)

`--ssh-key` accepts a FIDO2 public key (`ed25519-sk` / `ecdsa-sk`) as well as a software key, so you can
install straight onto hardware-backed admin auth. Generate one on your laptop, the private key never
leaves the token:

```bash
ssh-keygen -t ed25519-sk -O resident -O verify-required -C yubikey-primary
```

Enroll **two** tokens (primary + off-site backup), then require them:

```nix
keepNode.security.yubikey = {
enable = true;
authorizedKeys = [ "sk-ssh-ed25519@openssh.com AAAA... yubikey-primary" /* ...backup */ ];
requireHardwareKey = true; # sshd then refuses every non-hardware-backed key
};
```

`requireHardwareKey` narrows sshd's accepted algorithms to `sk-` only, so a stolen laptop is no longer
enough to reach the node; `verify-required` puts a PIN and a physical touch on every login. It is off by
default, so existing software-key deployments are unaffected, and the build refuses to enable it with no
usable hardware key (anti-lockout). On an already-installed node, add a token with
`keepnode-enroll-yubikey <sk-pubkey>`. Full walkthrough, including Vaultwarden WebAuthn: [Deployment](docs/deployment.md).

> **A YubiKey is node access, not vault recovery.** It does not replace or weaken the FROST threshold
> model. Lose every token and you lose SSH (physical console is the permanent break-glass), never the
> vault, recovery is still the device quorum.
>
> **Hardened by default, reachable by your key.** The installer image is the hardened profile: no known password, key-only SSH (the `keepadmin` account, your enrolled key), `debugAccess` off, Vaultwarden bound to localhost, signups default-deny. During bring-up SSH is reachable on the LAN (`keepNode.adminAccess.lanBringup`) because a fresh node has no mesh yet; once it joins the mesh you redeploy mesh-only. See [Deployment](docs/deployment.md) for the declarative multi-node + admin-access how-to. The legacy `keepnode-debug` profile (known root password, password SSH, open signups) still exists as an explicit opt-in for keyless evaluation, but is never the installed default. `frost-gate` is off, so Vaultwarden data sits on the plain disk with no TPM unlock yet.

## License
Expand Down
21 changes: 21 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ true of all full-disk encryption. The quorum protects the key at rest and gates
unlock on a second holder; it does not protect a live, compromised, running system. Keep
Node does not claim to.

### A stolen operator laptop

Admin access is key-only SSH as `keepadmin`, reachable only over the mesh. With a software key, that
key is a **file**: an attacker who takes the laptop, or lands malware on it, inherits full node admin
(including passwordless sudo), silently and replayably, for as long as the key exists.

`keepNode.security.yubikey` narrows that. With a FIDO2 credential (`ed25519-sk` / `ecdsa-sk`) the
private key lives in the token's secure element and cannot be extracted or copied, and
`verify-required` makes every single authentication need a PIN *and* a physical touch. Possession of
the laptop stops being sufficient; remote malware cannot authenticate without the operator physically
present at the token. `requireHardwareKey = true` enforces this at sshd (`PubkeyAcceptedAlgorithms`
restricted to `sk-` algorithms), so a software key is refused by the daemon rather than merely absent
from a file.

The boundary: this protects the *authentication*, not a session already established. An attacker who
compromises the laptop while the operator is logged in can act inside that live session; the touch
requirement bounds the damage to the moments the operator is present, and the credential cannot be
stolen for later use. It is also strictly node access , it does not gate the vault, whose protection is
the FROST quorum above. Losing every token costs SSH (physical console is the break-glass), never the
vault.

### Duress and coercion

The quorum protects against theft and remote compromise. It does not, on its own, protect
Expand Down
97 changes: 97 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,103 @@ Then, from your (mesh-joined) laptop:
ssh keepadmin@10.44.x.y # the node's mesh IP; not reachable from the LAN
```

## 4. Harden admin access with a YubiKey (FIDO2)

An SSH key is a *file on your laptop*: steal the laptop and you inherit node admin, silently. A FIDO2
credential (`ed25519-sk` / `ecdsa-sk`) keeps the private key inside the token's secure element, where it
cannot be copied, and `verify-required` demands a PIN **and** a physical touch on every login.

**This is node access only. It is not FROST.** Losing every YubiKey costs you SSH to the box (physical
console remains the permanent break-glass); it never costs you the vault. Vault recovery is the FROST
threshold quorum, unchanged by anything on this page.

### Generate the credential (on your laptop)

Do this **twice**: a primary token and a backup you store off-site. One hardware key is a single point
of failure for node access.

```bash
ssh-keygen -t ed25519-sk -O resident -O verify-required -C yubikey-primary
```

- `-O resident` stores the credential *on the token*, so you can recover it onto a new laptop with
`ssh-keygen -K` (run in `~/.ssh`). Non-resident keys work too, but then the key handle file is
something you must back up yourself.
- `-O verify-required` is what forces the PIN in addition to the touch.
- `ecdsa-sk` instead of `ed25519-sk` if your token's firmware predates FIDO2 ed25519 support.

The `.pub` file it writes is what you enroll; the private half never leaves the YubiKey.

### Enroll it

```nix
keepNode.security.yubikey = {
enable = true;
authorizedKeys = [
"sk-ssh-ed25519@openssh.com AAAA... yubikey-primary"
"sk-ssh-ed25519@openssh.com AAAA... yubikey-backup"
];
requireHardwareKey = true; # refuse every non-hardware-backed key at sshd
};
```

- `requireHardwareKey` narrows sshd's `PubkeyAcceptedAlgorithms` to the `sk-` algorithms, so a software
key is **rejected by the daemon**, not merely left out of a file. Leave it `false` (the default) while
you still need software-key access; the module is fully backward compatible in that mode, and the
hardware keys still carry a per-key `verify-required`.
- `keyTypes` (default `[ "ed25519-sk" "ecdsa-sk" ]`) is the declarative allow-list of credential types.
Narrow it to `[ "ed25519-sk" ]` to refuse ecdsa-sk outright.
- **Anti-lockout:** `requireHardwareKey = true` with no usable hardware key fails the build; if the key
lives in the runtime `authorizedKeysFile` (which the build cannot inspect), the
`keep-node-hardware-key-check` unit fails loudly at boot instead, without blocking boot or sshd.
- Password authentication stays disabled throughout. This module only ever *removes* an authentication
path.

> **What the anti-lockout guards cannot see.** Both of them , the build assertion and the boot check ,
> read *key algorithms in a file*. They cannot see whether the credential on your token was created
> with `-O verify-required`, and they cannot see whether the key body is intact. So with
> `requireVerification = true` (the default), a token enrolled **without** that flag satisfies both
> guards and is still refused by sshd at login: both report healthy on a node you cannot reach.
> **Actually log in over the mesh with the YubiKey , from a second terminal, keeping your current
> session open , before you flip `requireHardwareKey` on or remove your software key.** That login is
> the only real proof; everything else is a file check.

### Enroll after install

On a node installed from the generic ISO the closure is fixed, so add a token at runtime:

```bash
ssh keepadmin@<node>
keepnode-enroll-yubikey "sk-ssh-ed25519@openssh.com AAAA... yubikey-backup"
```

It validates the key, refuses anything that is not `sk-`, de-duplicates, appends `verify-required`, and
re-runs the anti-lockout checks. Enroll the backup token **before** flipping `requireHardwareKey` on.

Pass **one** key per invocation: a multi-line paste is refused outright, because only the first line
would be validated while every line got written.

### Vaultwarden: YubiKey as 2FA / passkey

Vaultwarden supports WebAuthn, but it binds every credential to the origin in its `DOMAIN` setting: if
that does not match the URL you actually browse, registration appears to work and then fails on the next
login. Declare it:

```nix
keepNode.vaultwarden.domain = "http://localhost:8222"; # the SSH-tunnelled default
```

`http://localhost` is a secure context, so WebAuthn works over the tunnel without TLS. Do **not** put a
mesh IP or LAN name behind plain `http://`: browsers treat that as a non-secure context and refuse
WebAuthn entirely (and Vaultwarden drops secure cookies), so registration cannot even start. Off
localhost, use HTTPS. If you enable
`keepNode.ingress`, it sets `DOMAIN` to its public HTTPS hostname and takes precedence , register your
token against whichever origin you actually use, and re-register if you change it.

Then in the web vault: **Settings -> Security -> Two-step login -> FIDO2 WebAuthn**, add both tokens, and
save the recovery code somewhere offline. Vaultwarden's own passkey/2FA state lives in the vault DB on
the FROST-gated volume, so it is covered by the same threshold guarantee as everything else.

## Installer bring-up (generic ISO)

Mesh-only admin assumes your laptop is already a rostered mesh peer. For a co-owned cluster you author
Expand Down
Loading
Loading