Stop the Windows installer when \etc\fips holds the only identity key

A service that earlier releases ran from \etc\fips reads only
C:\ProgramData\fips once the installer sets FIPS_CONFIG, so it came up
with a new npub and address with nothing said. The installer now stops,
before it creates any file or copies the binaries, when
\etc\fips\fips.key exists and C:\ProgramData\fips\fips.key does not, and
says to move the key and its settings and delete the old fips.yaml. It
does not move the key itself, since any local user can write to
\etc\fips. A packaging test pins the condition, the Write-Error and
exit, and the refusal's place ahead of file creation, the binary copy and
service registration.

The ZIP README gave the foreground config search as starting at
C:\ProgramData\fips, but \etc\fips\fips.yaml on the current drive is
probed first and any local user can create it. List it, with the
daemon's warning and the search no longer looking there in v0.6.0.
Describe the installer's stop, and give the upgrade steps: rerunning the
installer while the service runs fails copying fips.exe.
This commit is contained in:
Johnathan Corgan
2026-10-01 14:20:06 +00:00
parent da9a12a3dd
commit ee453c56ea
3 changed files with 128 additions and 9 deletions
+24 -1
View File
@@ -83,6 +83,13 @@ Windows Service:
# Install (requires Administrator)
powershell -File install-service.ps1
# Upgrade: stop the service first, or the installer fails
# copying fips.exe after it has already changed the config
# directory. Then rerun the installer and start the service.
sc stop fips
powershell -File install-service.ps1
sc start fips
# Manage
sc start fips
sc stop fips
@@ -125,10 +132,26 @@ Configuration:
saying so, or may fail with an access error.
A foreground run takes -c <file>, or reads
\etc\fips\fips.yaml on the current drive, then
C:\ProgramData\fips\fips.yaml and then, as per-user overrides
the service does not read, %APPDATA%\fips\fips.yaml,
%USERPROFILE%\.fips.yaml and .\fips.yaml. The key file sits
beside the last config loaded.
beside the last config loaded. \etc\fips\fips.yaml was the
system config of earlier releases, and any local user can
create it; the daemon warns when it loads it, and v0.6.0 stops
reading it. Move what you need from it into
C:\ProgramData\fips\fips.yaml and delete it.
A service that earlier releases ran from \etc\fips reads
only C:\ProgramData\fips once install-service.ps1 has run,
so it loses that config and may come up with a new identity.
The installer stops if it finds \etc\fips\fips.key with no
fips.key in C:\ProgramData\fips. If the key is this node's,
move it there, carry the settings you need from
\etc\fips\fips.yaml, node.identity.persistent: true among
them, into C:\ProgramData\fips\fips.yaml, delete
\etc\fips\fips.yaml, and run the installer again. If you did
not put the key there, delete it.
fipsctl keygen writes to C:\ProgramData\fips by default and
needs an elevated prompt. Run install-service.ps1 before it:
+13 -2
View File
@@ -134,9 +134,9 @@ Write-Host " Restricted $ConfigDir to SYSTEM and Administrators"
# the system drive, where any local user can create files, and the service
# still reads a file there when it is missing from the config directory.
# Stop rather than enforce, or silently drop, a list nobody has reviewed.
$legacyAclDir = "$env:SystemDrive\etc\fips"
$legacyDir = "$env:SystemDrive\etc\fips"
foreach ($name in @("peers.allow", "peers.deny")) {
$legacy = Join-Path $legacyAclDir $name
$legacy = Join-Path $legacyDir $name
$current = "$ConfigDir\$name"
if ((Test-Path -LiteralPath $legacy) -and -not (Test-Path -LiteralPath $current)) {
Write-Error "$legacy exists and $current does not, so the service would enforce the old file. Earlier releases read it, and any local user can write there. Review it, then move it to $current or delete it, and run install-service.ps1 again."
@@ -144,6 +144,17 @@ foreach ($name in @("peers.allow", "peers.deny")) {
}
}
# A service that earlier releases ran from \etc\fips reads only $ConfigDir
# once FIPS_CONFIG is set below, and would come up with a new identity. Stop
# until the key is moved into $ConfigDir or deleted. The installer does not
# move it itself: any local user can write \etc\fips, so a key there may not
# be this node's.
$legacyKey = Join-Path $legacyDir "fips.key"
if ((Test-Path -LiteralPath $legacyKey) -and -not (Test-Path -LiteralPath "$ConfigDir\fips.key")) {
Write-Error "$legacyKey exists and $ConfigDir\fips.key does not, so a service that ran from \etc\fips would come up with a new identity. If that key is this node's, move it to $ConfigDir\fips.key and carry the settings you need from \etc\fips\fips.yaml, node.identity.persistent: true among them, into $ConfigDir\fips.yaml, then delete \etc\fips\fips.yaml, which the service warns about on every start until it is gone. If you did not put the key there, delete it: any local user can write to \etc\fips. Then run install-service.ps1 again."
exit 1
}
# Empty peer ACL files allow every peer. Having them here means the service
# never falls back to the \etc\fips copies. Empty them to clear a list; do not
# delete them.
+91 -6
View File
@@ -874,11 +874,9 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() {
.unwrap_or_else(|| panic!("install-service.ps1: no line {what}"))
};
let legacy_dir = first("assigning $legacyAclDir", &|l| {
l.starts_with("$legacyAclDir = ")
});
let legacy_dir = first("assigning $legacyDir", &|l| l.starts_with("$legacyDir = "));
assert_eq!(
lines[legacy_dir], r#"$legacyAclDir = "$env:SystemDrive\etc\fips""#,
lines[legacy_dir], r#"$legacyDir = "$env:SystemDrive\etc\fips""#,
"install-service.ps1: the legacy peer ACL directory is not \\etc\\fips on the \
system drive"
);
@@ -906,7 +904,7 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() {
let creation_body = creation + 1..creation_end;
for text in [
"$legacy = Join-Path $legacyAclDir $name",
"$legacy = Join-Path $legacyDir $name",
r#"$current = "$ConfigDir\$name""#,
] {
assert!(
@@ -967,7 +965,7 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() {
let is_check = |l: &str| l == "& $refuseEntries";
let order = [
("check after the reset", all(&is_check).get(2).copied()),
("$legacyAclDir", Some(legacy_dir)),
("$legacyDir", Some(legacy_dir)),
("refusal loop", Some(refusal)),
("refusal of a legacy file", Some(check)),
("end of the refusal loop", Some(refusal_end)),
@@ -998,6 +996,93 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() {
}
}
/// Guards install-service.ps1's refusal of an identity key left in
/// `\etc\fips`.
///
/// A service that earlier releases ran from `\etc\fips` reads only
/// `C:\ProgramData\fips` once the installer sets `FIPS_CONFIG`, so a key left
/// in `\etc\fips` with none in the config directory means the node would
/// come up with a new identity. The installer must stop in exactly that case,
/// and must decide it before it creates any file in the config directory,
/// copies the binaries or registers the service, so a refusal leaves an
/// existing install as it was.
#[test]
fn windows_installer_refuses_a_legacy_identity_key_with_none_in_the_config_dir() {
let lines = ps_lines(&repo_file("packaging/windows/install-service.ps1"));
let first = |what: &str, pred: &dyn Fn(&str) -> bool| -> usize {
lines
.iter()
.position(|l| pred(l))
.unwrap_or_else(|| panic!("install-service.ps1: no line {what}"))
};
let dir = first("assigning $legacyDir", &|l| l.starts_with("$legacyDir = "));
let key = first("assigning $legacyKey", &|l| l.starts_with("$legacyKey = "));
assert_eq!(
lines[key], r#"$legacyKey = Join-Path $legacyDir "fips.key""#,
"install-service.ps1: the legacy key is not fips.key in $legacyDir"
);
let checks: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, l)| l.starts_with("if (") && l.contains("$legacyKey"))
.map(|(i, _)| i)
.collect();
assert_eq!(
checks.len(),
1,
"install-service.ps1: expected one test of the legacy key, found {}",
checks.len()
);
let check = checks[0];
assert_eq!(
lines[check],
r#"if ((Test-Path -LiteralPath $legacyKey) -and -not (Test-Path -LiteralPath "$ConfigDir\fips.key")) {"#,
"install-service.ps1: the refusal must hold only when the legacy key exists and \
the config directory has none"
);
refuses_at(&lines, check, "refusal of a legacy identity key");
let order = [
("$legacyDir", Some(dir)),
("$legacyKey", Some(key)),
("refusal of a legacy identity key", Some(check)),
(
"creation of a peer ACL file",
lines
.iter()
.position(|l| l.contains("New-Item") && l.contains("-ItemType File")),
),
(
"binary copy",
lines.iter().position(|l| l.contains("$Binaries")),
),
(
"default config copy",
lines
.iter()
.position(|l| l.contains("Copy-Item") && l.contains("$ConfigDir\\fips.yaml")),
),
(
"service registration",
lines.iter().position(|l| l.contains("--install-service")),
),
];
for pair in order.windows(2) {
let [(a, ia), (b, ib)] = pair else {
unreachable!("windows(2) yields pairs")
};
let (ia, ib) = (
ia.unwrap_or_else(|| panic!("install-service.ps1: no {a}")),
ib.unwrap_or_else(|| panic!("install-service.ps1: no {b}")),
);
assert!(
ia < ib,
"install-service.ps1: {a} (code line {ia}) must come before {b} (code line {ib})"
);
}
}
const COMMON_CONFIG: &str = "packaging/common/fips.yaml";
const OPENWRT_CONFIG: &str = "packaging/openwrt-ipk/files/etc/fips/fips.yaml";