mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
Warn on Windows about fips.yaml and fips.key in \etc\fips
The config search still probes the drive-relative \etc\fips\fips.yaml on Windows, where any local user can create files, and merges it under the real config. A node upgraded from a hand-made service that ran from \etc\fips now reads only C:\ProgramData\fips and silently comes up with a new identity. Log a warning when a config was loaded from \etc\fips, saying the search stops looking there in v0.6.0; it is logged before identity resolution, so a start that fails on a key the file supplied still names the file. After resolution, warn when fips.yaml or fips.key is there but this run did not use it, counting a key generated this run as in use. Paths are compared the way Windows does, with either separator, any case and any drive prefix, so an explicit C:\etc\fips\fips.yaml given with -c or FIPS_CONFIG is recognised. The decision is a pure function tested on every platform; the key is only checked for presence, never read.
This commit is contained in:
@@ -138,9 +138,9 @@ Configuration:
|
||||
%USERPROFILE%\.fips.yaml and .\fips.yaml. The key file sits
|
||||
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.
|
||||
create it; the daemon warns when it loads it, and from v0.6.0
|
||||
the search no longer looks there. 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,
|
||||
|
||||
@@ -145,6 +145,11 @@ async fn run_daemon(
|
||||
#[cfg(windows)]
|
||||
fips::config::warn_legacy(&loaded_paths);
|
||||
|
||||
// Earlier releases could run a Windows node from \etc\fips, where any
|
||||
// local user can create files; flag a config the search loaded there.
|
||||
#[cfg(windows)]
|
||||
fips::config::warn_legacy_etc_config(&loaded_paths);
|
||||
|
||||
// Identity provisioning: config nsec > key file > generate ephemeral
|
||||
let mut resolved = match resolve_identity(&config, &loaded_paths) {
|
||||
Ok(r) => r,
|
||||
@@ -164,6 +169,11 @@ async fn run_daemon(
|
||||
IdentitySource::Ephemeral => info!("Using ephemeral identity (new keypair each start)"),
|
||||
}
|
||||
|
||||
// Flag a config or key left in \etc\fips that this run did not use. After
|
||||
// identity resolution, which decides whether that key was used.
|
||||
#[cfg(windows)]
|
||||
fips::config::warn_legacy_etc_unused(&loaded_paths, &resolved.source);
|
||||
|
||||
// Create node with resolved identity
|
||||
let mut config = config;
|
||||
// Take the nsec rather than move it: `ResolvedIdentity` clears its copy
|
||||
|
||||
+270
-1
@@ -171,6 +171,128 @@ pub fn warn_legacy(loaded: &[PathBuf]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A file in the drive-relative `\etc\fips` that a Windows run reports.
|
||||
///
|
||||
/// Earlier releases could run a Windows node from `\etc\fips`, a directory
|
||||
/// any local user can create files in, and the config search still probes
|
||||
/// `fips.yaml` there.
|
||||
#[cfg(any(windows, test))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum EtcFile {
|
||||
/// `fips.yaml` there was loaded, found by the config search or named
|
||||
/// with `-c` or `FIPS_CONFIG`.
|
||||
Loaded(PathBuf),
|
||||
/// `fips.yaml` or `fips.key` is there and this run did not use it, so a
|
||||
/// node that used to run from it now runs on another config or identity.
|
||||
Unused(PathBuf),
|
||||
}
|
||||
|
||||
/// Whether two paths name the same file in the way Windows compares them:
|
||||
/// either separator, any case, and with any drive prefix ignored.
|
||||
///
|
||||
/// The drive-relative `/etc/fips\fips.yaml` the config search probes and an
|
||||
/// explicit `C:\etc\fips\fips.yaml` given with `-c` or `FIPS_CONFIG` are
|
||||
/// different `Path`s, since one has a drive prefix, and `Path` comparison is
|
||||
/// case-sensitive while Windows file names are not. Comparing the text lets
|
||||
/// the rule run on every platform.
|
||||
#[cfg(any(windows, test))]
|
||||
fn same_windows_path(a: &Path, b: &Path) -> bool {
|
||||
fn key(path: &Path) -> String {
|
||||
let text = path.to_string_lossy().replace('/', "\\");
|
||||
let text = text.strip_prefix(r"\\?\").unwrap_or(&text);
|
||||
let text = match text.as_bytes() {
|
||||
[drive, b':', ..] if drive.is_ascii_alphabetic() => &text[2..],
|
||||
_ => text,
|
||||
};
|
||||
text.to_ascii_lowercase()
|
||||
}
|
||||
key(a) == key(b)
|
||||
}
|
||||
|
||||
/// Decide what to report about `fips.yaml` and `fips.key` in `etc`.
|
||||
///
|
||||
/// `loaded` is the list of config files the daemon loaded, and `key_used`
|
||||
/// the key file its identity came from, if any. `present` reports whether a
|
||||
/// file exists; nothing else about the files is looked at, and the key is
|
||||
/// never read.
|
||||
#[cfg(any(windows, test))]
|
||||
fn etc_files(
|
||||
loaded: &[PathBuf],
|
||||
key_used: Option<&Path>,
|
||||
etc: &Path,
|
||||
present: impl Fn(&Path) -> bool,
|
||||
) -> Vec<EtcFile> {
|
||||
let config = etc.join(CONFIG_FILENAME);
|
||||
let key = etc.join(KEY_FILENAME);
|
||||
let mut found = Vec::new();
|
||||
if let Some(path) = loaded.iter().find(|p| same_windows_path(p, &config)) {
|
||||
found.push(EtcFile::Loaded(path.clone()));
|
||||
} else if present(config.as_path()) {
|
||||
found.push(EtcFile::Unused(config));
|
||||
}
|
||||
if !key_used.is_some_and(|k| same_windows_path(k, &key)) && present(key.as_path()) {
|
||||
found.push(EtcFile::Unused(key));
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// The key file this run's identity is held in: the one it loaded, or the one
|
||||
/// it generated and saved. An identity from the config, or an ephemeral one,
|
||||
/// uses no key file.
|
||||
#[cfg(any(windows, test))]
|
||||
fn key_in_use(identity: &IdentitySource) -> Option<&Path> {
|
||||
match identity {
|
||||
IdentitySource::KeyFile(path) | IdentitySource::Generated(path) => Some(path),
|
||||
IdentitySource::Config | IdentitySource::Ephemeral => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Warn about a config loaded from the drive-relative `\etc\fips`.
|
||||
///
|
||||
/// Any local user may have written it, whether the config search found it or
|
||||
/// it was named explicitly, and from v0.6.0 the search no longer looks
|
||||
/// there. Called before identity resolution, so the warning is logged even
|
||||
/// when a key the file supplies fails to resolve.
|
||||
#[cfg(windows)]
|
||||
pub fn warn_legacy_etc_config(loaded: &[PathBuf]) {
|
||||
let etc = Path::new(LEGACY_SYSTEM_CONFIG_DIR);
|
||||
// With nothing reported present, only a loaded config can be found.
|
||||
for file in etc_files(loaded, None, etc, |_| false) {
|
||||
if let EtcFile::Loaded(path) = file {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
current = %Path::new(SYSTEM_CONFIG_DIR).join(CONFIG_FILENAME).display(),
|
||||
"Config loaded from \\etc\\fips, where any local user can create files; \
|
||||
from v0.6.0 the config search no longer looks there. Move the settings \
|
||||
this node needs into C:\\ProgramData\\fips\\fips.yaml and delete the file"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Warn about `fips.yaml` or `fips.key` left in the drive-relative
|
||||
/// `\etc\fips` and not used by this run.
|
||||
///
|
||||
/// A node that ran from them before an upgrade now has a different config,
|
||||
/// and possibly a different identity. Called after identity resolution,
|
||||
/// which decides which key file the run uses.
|
||||
#[cfg(windows)]
|
||||
pub fn warn_legacy_etc_unused(loaded: &[PathBuf], identity: &IdentitySource) {
|
||||
let etc = Path::new(LEGACY_SYSTEM_CONFIG_DIR);
|
||||
for file in etc_files(loaded, key_in_use(identity), etc, Path::exists) {
|
||||
if let EtcFile::Unused(path) = file {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
current = %Path::new(SYSTEM_CONFIG_DIR).display(),
|
||||
"File in \\etc\\fips is not used by this run; a node that ran from it \
|
||||
before now runs on another config or identity. If it is this node's, \
|
||||
move fips.yaml and fips.key into C:\\ProgramData\\fips and run \
|
||||
install-service.ps1 again; if not, delete it"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an identity key stranded at the legacy system config directory.
|
||||
///
|
||||
/// Adding a second system config directory to the search path moves the
|
||||
@@ -1064,7 +1186,7 @@ impl Config {
|
||||
|
||||
// System config — /etc/fips is always probed so existing installs
|
||||
// keep working after an upgrade.
|
||||
paths.push(PathBuf::from("/etc/fips").join(CONFIG_FILENAME));
|
||||
paths.push(PathBuf::from(LEGACY_SYSTEM_CONFIG_DIR).join(CONFIG_FILENAME));
|
||||
|
||||
// macOS and FreeBSD packaging install config under /usr/local/etc/fips,
|
||||
// and the Windows service installer under C:\ProgramData\fips; probe
|
||||
@@ -1890,6 +2012,153 @@ node:
|
||||
);
|
||||
}
|
||||
|
||||
/// A presence check that reports exactly `files` as existing.
|
||||
fn only(files: &[&Path]) -> impl Fn(&Path) -> bool {
|
||||
let files: Vec<PathBuf> = files.iter().map(|f| f.to_path_buf()).collect();
|
||||
move |p| files.iter().any(|f| f == p)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn etc_files_reports_a_config_the_search_loaded_from_etc_fips() {
|
||||
let etc = Path::new("/etc-legacy/fips");
|
||||
let planted = etc.join(CONFIG_FILENAME);
|
||||
let system = Path::new("/sys-cfg/fips").join(CONFIG_FILENAME);
|
||||
|
||||
assert_eq!(
|
||||
etc_files(
|
||||
&[planted.clone(), system.clone()],
|
||||
None,
|
||||
etc,
|
||||
only(&[&planted])
|
||||
),
|
||||
[EtcFile::Loaded(planted.clone())],
|
||||
"a config loaded from \\etc\\fips under the real one must be reported as loaded"
|
||||
);
|
||||
assert_eq!(
|
||||
etc_files(
|
||||
std::slice::from_ref(&planted),
|
||||
Some(&etc.join(KEY_FILENAME)),
|
||||
etc,
|
||||
only(&[&planted, &etc.join(KEY_FILENAME)])
|
||||
),
|
||||
[EtcFile::Loaded(planted)],
|
||||
"a node running from \\etc\\fips is told the config goes away, and its key, \
|
||||
which it uses, is not reported"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn etc_files_reports_a_config_and_key_left_in_etc_fips_that_the_run_did_not_use() {
|
||||
let etc = Path::new("/etc-legacy/fips");
|
||||
let old_config = etc.join(CONFIG_FILENAME);
|
||||
let old_key = etc.join(KEY_FILENAME);
|
||||
let system = Path::new("/sys-cfg/fips");
|
||||
let loaded = [system.join(CONFIG_FILENAME)];
|
||||
|
||||
assert_eq!(
|
||||
etc_files(&loaded, None, etc, only(&[&old_config, &old_key])),
|
||||
[
|
||||
EtcFile::Unused(old_config.clone()),
|
||||
EtcFile::Unused(old_key.clone())
|
||||
],
|
||||
"after an upgrade to FIPS_CONFIG the old config and key must both be reported"
|
||||
);
|
||||
assert_eq!(
|
||||
etc_files(
|
||||
&loaded,
|
||||
Some(&system.join(KEY_FILENAME)),
|
||||
etc,
|
||||
only(&[&old_key])
|
||||
),
|
||||
[EtcFile::Unused(old_key)],
|
||||
"a key left in \\etc\\fips while the identity comes from another key file \
|
||||
must be reported"
|
||||
);
|
||||
assert_eq!(
|
||||
etc_files(&loaded, None, etc, only(&[&old_config])),
|
||||
[EtcFile::Unused(old_config)],
|
||||
"a config left in \\etc\\fips and not loaded must be reported"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_windows_path_ignores_the_drive_the_separator_and_case() {
|
||||
let probed = PathBuf::from("/etc/fips").join(CONFIG_FILENAME);
|
||||
for explicit in [
|
||||
r"C:\etc\fips\fips.yaml",
|
||||
r"c:/ETC/Fips/FIPS.yaml",
|
||||
r"\\?\C:\etc\fips\fips.yaml",
|
||||
r"\etc\fips\fips.yaml",
|
||||
] {
|
||||
assert!(
|
||||
same_windows_path(&probed, Path::new(explicit)),
|
||||
"{explicit} must match the probed /etc/fips\\fips.yaml"
|
||||
);
|
||||
}
|
||||
for other in [
|
||||
r"C:\ProgramData\fips\fips.yaml",
|
||||
r"C:\etc\fips\fips.key",
|
||||
r"C:etc\fips\fips.yaml",
|
||||
r"etc\fips\fips.yaml",
|
||||
r"C:\x\etc\fips\fips.yaml",
|
||||
] {
|
||||
assert!(
|
||||
!same_windows_path(&probed, Path::new(other)),
|
||||
"{other} must not match the probed /etc/fips\\fips.yaml"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn etc_files_reports_an_explicit_drive_path_to_etc_fips_as_loaded_and_its_key_as_used() {
|
||||
let etc = PathBuf::from("/etc/fips");
|
||||
let explicit = PathBuf::from(r"C:\etc\fips\fips.yaml");
|
||||
let key = PathBuf::from(r"C:\etc\fips\fips.key");
|
||||
|
||||
assert_eq!(
|
||||
etc_files(std::slice::from_ref(&explicit), Some(&key), &etc, |_| true),
|
||||
[EtcFile::Loaded(explicit)],
|
||||
"a config named as C:\\etc\\fips\\fips.yaml is the legacy file, loaded, and \
|
||||
the key beside it is in use"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_in_use_counts_a_key_generated_this_run_as_well_as_one_loaded() {
|
||||
let key = PathBuf::from("/etc-legacy/fips").join(KEY_FILENAME);
|
||||
assert_eq!(
|
||||
key_in_use(&IdentitySource::KeyFile(key.clone())),
|
||||
Some(key.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
key_in_use(&IdentitySource::Generated(key.clone())),
|
||||
Some(key.as_path()),
|
||||
"a key generated and saved this run is the one in use, not an unused leftover"
|
||||
);
|
||||
assert_eq!(key_in_use(&IdentitySource::Config), None);
|
||||
assert_eq!(key_in_use(&IdentitySource::Ephemeral), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn etc_files_reports_nothing_when_etc_fips_holds_neither_file() {
|
||||
let etc = Path::new("/etc-legacy/fips");
|
||||
let system = Path::new("/sys-cfg/fips");
|
||||
let config = system.join(CONFIG_FILENAME);
|
||||
let key = system.join(KEY_FILENAME);
|
||||
|
||||
assert_eq!(
|
||||
etc_files(
|
||||
std::slice::from_ref(&config),
|
||||
Some(&key),
|
||||
etc,
|
||||
only(&[&config, &key])
|
||||
),
|
||||
Vec::new(),
|
||||
"files present only in the current directory must not be reported"
|
||||
);
|
||||
assert_eq!(etc_files(&[], None, etc, only(&[])), Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_key_fallback_finds_a_key_in_the_old_appdata_dir() {
|
||||
let root = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user