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
9 changes: 9 additions & 0 deletions nixos/frost-gate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,15 @@ in
assertion = cfg.volumeDevice != null;
message = "keepNode.frostGate.enable requires keepNode.frostGate.volumeDevice.";
}
{
# Loaded as a systemd EnvironmentFile holding KEEP_PASSWORD. Its
# description says to pass a runtime path; nothing enforced it, so a
# Nix-path literal would put the vault password in /nix/store at 0444.
assertion =
cfg.keepPasswordEnvFile == null
|| !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.keepPasswordEnvFile));
message = "keepNode.frostGate.keepPasswordEnvFile must be a runtime path (e.g. /run/secrets/...), not a Nix store path: KEEP_PASSWORD would be world-readable in /nix/store.";
}
{
assertion = cfg.quorum.threshold <= cfg.quorum.total && cfg.quorum.threshold >= 1;
message = "keepNode.frostGate.quorum: need 1 <= threshold <= total.";
Expand Down
12 changes: 12 additions & 0 deletions nixos/ingress.nix
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ in
assertion = config.keepNode.vaultwarden.enable;
message = "keepNode.ingress requires keepNode.vaultwarden.enable (it proxies to Vaultwarden).";
}
{
# Both descriptions say to pass a runtime path; nothing enforced it. A
# Nix-path literal copies the TLS private key into /nix/store at 0444.
assertion =
cfg.tlsKeyFile == null || !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.tlsKeyFile));
message = "keepNode.ingress.tlsKeyFile must be a runtime path (e.g. /run/secrets/...), not a Nix store path: the TLS private key would be world-readable in /nix/store.";
}
{
assertion =
cfg.tlsCertFile == null || !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.tlsCertFile));
message = "keepNode.ingress.tlsCertFile must be a runtime path (e.g. /run/secrets/...), not a Nix store path.";
}
];

# Vaultwarden must know its public URL (links, WebAuthn, etc.) and must trust the proxy's
Expand Down
9 changes: 5 additions & 4 deletions nixos/keep-web.nix
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,18 @@ in
}
{
assertion =
cfg.passwordFile == null || !(lib.hasPrefix builtins.storeDir (toString cfg.passwordFile));
cfg.passwordFile == null || !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.passwordFile));
message = "keepNode.keepWeb.passwordFile must be a runtime path (e.g. /run/secrets/...), not a Nix store path: the password would be world-readable in /nix/store.";
}
{
assertion =
cfg.authTokenFile == null || !(lib.hasPrefix builtins.storeDir (toString cfg.authTokenFile));
cfg.authTokenFile == null || !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.authTokenFile));
message = "keepNode.keepWeb.authTokenFile must be a runtime path (e.g. /run/secrets/...), not a Nix store path: the token would be world-readable in /nix/store.";
}
{
assertion =
cfg.stateIdentityFile == null
|| !(lib.hasPrefix builtins.storeDir (toString cfg.stateIdentityFile));
|| !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.stateIdentityFile));
message = "keepNode.keepWeb.stateIdentityFile must be a runtime path, not a Nix store path: the shared cluster nsec would be world-readable in /nix/store.";
}
{
Expand All @@ -149,7 +149,8 @@ in
}
{
assertion =
cfg.storageKeyFile == null || !(lib.hasPrefix builtins.storeDir (toString cfg.storageKeyFile));
cfg.storageKeyFile == null
|| !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.storageKeyFile));
message = "keepNode.keepWeb.storageKeyFile must be a runtime path, not a Nix store path: the shared cluster vault key would be world-readable in /nix/store.";
}
];
Expand Down
2 changes: 1 addition & 1 deletion nixos/mesh.nix
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ in
# off the world-readable /nix/store. A path literal (`./secrets/a`) coerces into the store, 0444
# and pushed to any binary cache -- exactly the cleartext-key leak the FROST gate exists to stop.
# Reject it at eval time instead of leaving it to the option's prose.
assertion = cfg.identityDir == null || !lib.hasPrefix builtins.storeDir cfg.identityDir;
assertion = cfg.identityDir == null || !lib.hasPrefix "${builtins.storeDir}/" cfg.identityDir;
message = "keepNode.mesh.identityDir (${toString cfg.identityDir}) is inside the Nix store: that copies the mesh Nostr secret key into the world-readable /nix/store. Deliver it out-of-band to a path on the target host (e.g. /run/secrets/... via agenix/sops), never as a Nix path literal.";
}
{
Expand Down
9 changes: 9 additions & 0 deletions nixos/vault-replication.nix
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ in
assertion = config.services.vaultwarden.enable;
message = "keepNode.vaultReplication.rsaKeyFile is set but services.vaultwarden.enable is false: the shared-key installer has nothing to seed a key for.";
}
{
# The option description already says "not a Nix-path literal"; nothing
# enforced it. `rsaKeyFile = "${./secrets/rsa_key.pem}"` type-checks and
# copies the cluster-wide JWT signing key into /nix/store at 0444, and CI
# pushes store paths to a public Cachix cache. Mirrors the assertions
# keepWeb and mesh already carry for the same wording.
assertion = !(lib.hasPrefix "${builtins.storeDir}/" (toString cfg.rsaKeyFile));
message = "keepNode.vaultReplication.rsaKeyFile must be a runtime path (e.g. /run/secrets/...), not a Nix store path: the cluster's JWT signing key would be world-readable in /nix/store.";
}
];

systemd.services.keep-node-vault-rsa-key = {
Expand Down
18 changes: 13 additions & 5 deletions tests/ha-failover.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,28 @@
nvpnPackage,
...
}:
let
rsaDir = "/run/keep-node-vault-rsa";
in
{
name = "keep-node-ha-failover";

# nodeA and nodeB are identical; share one module across both.
#
# Test-only: this deliberately does the anti-pattern the rsaKeyFile option warns against, feeding
# a Nix-store path so the key sits world-readable in /nix/store. Safe here because the fixture is
# an ephemeral per-build key, never a real secret. A real deploy must pass an out-of-band path.
defaults =
{ pkgs, ... }:
{
imports = [ ../nixos/keep-node.nix ];
keepNode.vaultReplication.rsaKeyFile = "${vaultRsaKeyFixture}/rsa_key.pem";
keepNode.vaultReplication.rsaKeyPubFile = "${vaultRsaKeyFixture}/rsa_key.pub.pem";
# Staged into /run rather than referenced in the store directly: the
# rsaKeyFile assertion rejects store paths, because a real deploy passing
# one would leave the cluster's JWT signing key world-readable in
# /nix/store. Same tmpfiles "C" copy the mesh tests use for identityDir.
systemd.tmpfiles.rules = [
"C ${rsaDir}/rsa_key.pem 0600 root root - ${vaultRsaKeyFixture}/rsa_key.pem"
"C ${rsaDir}/rsa_key.pub.pem 0644 root root - ${vaultRsaKeyFixture}/rsa_key.pub.pem"
];
keepNode.vaultReplication.rsaKeyFile = "${rsaDir}/rsa_key.pem";
keepNode.vaultReplication.rsaKeyPubFile = "${rsaDir}/rsa_key.pub.pem";
# sqlite: write/read the WAL probe; litestream: restore the replica on the peer.
environment.systemPackages = [
pkgs.sqlite
Expand Down
19 changes: 17 additions & 2 deletions tests/ingress.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{ ... }:
let
hostName = "vault.test";
tlsDir = "/run/keep-node-ingress-tls";
in
{
name = "keep-node-ingress";
Expand All @@ -22,11 +23,25 @@ in
imports = [ ../nixos/keep-node.nix ];

keepNode.vaultwarden.enable = true;
# Staged into /run rather than referenced in the store directly: the
# tlsKeyFile assertion rejects store paths, because a real deploy passing
# one would leave the TLS private key world-readable in /nix/store. Same
# tmpfiles "C" copy the mesh tests use for identityDir.
# The key is owned by nginx, not root: the module points
# services.nginx.sslCertificateKey straight at this path, and nginx runs
# its config test unprivileged, so a root-only mode fails the pre-start
# with "cannot load certificate key ... Permission denied". A real deploy
# has the same requirement. The certificate is public, so it stays 0644.
systemd.tmpfiles.rules = [
"C ${tlsDir}/cert.pem 0644 root root - ${cert}/cert.pem"
"C ${tlsDir}/key.pem 0400 nginx nginx - ${cert}/key.pem"
];

keepNode.ingress = {
enable = true;
hostName = hostName;
tlsCertFile = "${cert}/cert.pem";
tlsKeyFile = "${cert}/key.pem";
tlsCertFile = "${tlsDir}/cert.pem";
tlsKeyFile = "${tlsDir}/key.pem";
};

environment.systemPackages = [
Expand Down
21 changes: 17 additions & 4 deletions tests/mesh-replication.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,28 @@
vaultRsaKeyFixture,
...
}:
let
rsaDir = "/run/keep-node-vault-rsa";
in
{
name = "keep-node-mesh-replication";

nodes.active =
{ pkgs, ... }:
{
imports = [ ../nixos/keep-node.nix ];
# Staged into /run: the rsaKeyFile assertion rejects store paths, because a
# real deploy passing one would leave the cluster's JWT signing key
# world-readable in /nix/store. Same tmpfiles "C" copy the mesh tests use.
systemd.tmpfiles.rules = [
"C ${rsaDir}/rsa_key.pem 0600 root root - ${vaultRsaKeyFixture}/rsa_key.pem"
];
keepNode.mesh = {
enable = true;
package = nvpnPackage;
};
keepNode.vaultReplication = {
# Test-only anti-pattern (exactly what rsaKeyFile warns against): a Nix-store path leaves the key world-readable in /nix/store. Safe only because this is an ephemeral per-build fixture, never a real cluster signing key; a real deploy must pass an out-of-band path.
rsaKeyFile = "${vaultRsaKeyFixture}/rsa_key.pem";
rsaKeyFile = "${rsaDir}/rsa_key.pem";
litestream.enable = true;
role = "active";
meshReplication.enable = true;
Expand All @@ -39,13 +47,18 @@
{ pkgs, ... }:
{
imports = [ ../nixos/keep-node.nix ];
# Staged into /run: the rsaKeyFile assertion rejects store paths, because a
# real deploy passing one would leave the cluster's JWT signing key
# world-readable in /nix/store. Same tmpfiles "C" copy the mesh tests use.
systemd.tmpfiles.rules = [
"C ${rsaDir}/rsa_key.pem 0600 root root - ${vaultRsaKeyFixture}/rsa_key.pem"
];
keepNode.mesh = {
enable = true;
package = nvpnPackage;
};
keepNode.vaultReplication = {
# Test-only anti-pattern (exactly what rsaKeyFile warns against): a Nix-store path leaves the key world-readable in /nix/store. Safe only because this is an ephemeral per-build fixture, never a real cluster signing key; a real deploy must pass an out-of-band path.
rsaKeyFile = "${vaultRsaKeyFixture}/rsa_key.pem";
rsaKeyFile = "${rsaDir}/rsa_key.pem";
role = "standby";
meshReplication.enable = true;
# Low threshold so the lag test can prove the healthy->stale transition without a long wait.
Expand Down
11 changes: 10 additions & 1 deletion tests/oprf-gate-2of3.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
}:
let
vwClient = import ./lib/vw-client.nix { inherit pkgs; };
passEnvFile = "/run/keep-node-oprf/keep-pass-env";
passEnvFixture = pkgs.writeText "keep-pass-env" "KEEP_PASSWORD=fixturepass123";
in
{
name = "keep-node-oprf-gate-2of3-test";
Expand Down Expand Up @@ -58,6 +60,13 @@ in
}:
{
imports = [ ../nixos/keep-node.nix ];
# Staged into /run: the keepPasswordEnvFile assertion rejects store paths,
# because a real deploy passing one would leave KEEP_PASSWORD world-readable
# in /nix/store. The consuming provision unit is operator-run and not
# wantedBy boot, so it cannot race systemd-tmpfiles-setup.
systemd.tmpfiles.rules = [
"C ${passEnvFile} 0600 root root - ${passEnvFixture}"
];

keepNode.frostGate = {
enable = true;
Expand All @@ -81,7 +90,7 @@ in
bootUnlockTimeoutSec = 100;
keepPasswordCred = "/var/lib/keep-node/keep-password.cred";
oprfShareCred = "/var/lib/keep-node/oprf-share.cred";
keepPasswordEnvFile = "${pkgs.writeText "keep-pass-env" "KEEP_PASSWORD=fixturepass123"}";
keepPasswordEnvFile = passEnvFile;
tpmTcti = "device:/dev/tpmrm0";
allowInsecureWs = true;
};
Expand Down
13 changes: 10 additions & 3 deletions tests/oprf-gate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ let
# Headless Bitwarden client for the M0 vault round-trip (crypto validated by the vw-client-check
# test): register an account, store a login, read it back -- no browser, no bw/rbw agent.
vwClient = import ./lib/vw-client.nix { inherit pkgs; };
passEnvFile = "/run/keep-node-oprf/keep-pass-env";
passEnvFixture = pkgs.writeText "keep-pass-env" "KEEP_PASSWORD=fixturepass123";
in
{
name = "keep-node-oprf-gate-test";
Expand Down Expand Up @@ -59,6 +61,13 @@ in
}:
{
imports = [ ../nixos/keep-node.nix ];
# Staged into /run: the keepPasswordEnvFile assertion rejects store paths,
# because a real deploy passing one would leave KEEP_PASSWORD world-readable
# in /nix/store. The consuming provision unit is operator-run and not
# wantedBy boot, so it cannot race systemd-tmpfiles-setup.
systemd.tmpfiles.rules = [
"C ${passEnvFile} 0600 root root - ${passEnvFixture}"
];

keepNode.frostGate = {
enable = true;
Expand All @@ -76,9 +85,7 @@ in
};
keepPasswordCred = "/var/lib/keep-node/keep-password.cred";
oprfShareCred = "/var/lib/keep-node/oprf-share.cred";
# Test-only: the provision unit reads KEEP_PASSWORD from here. A real deploy uses a
# 0400 secret, not a store path. Matches the fixture DB's password.
keepPasswordEnvFile = "${pkgs.writeText "keep-pass-env" "KEEP_PASSWORD=fixturepass123"}";
keepPasswordEnvFile = passEnvFile;
tpmTcti = "device:/dev/tpmrm0";
# Test-only: forward KEEP_ALLOW_WS into the confined boot scope so it can reach the in-VM
# ws:// relay. Never set in production, where the boot OPRF exchange must stay over wss://.
Expand Down
Loading