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
|
%USERPROFILE%\.fips.yaml and .\fips.yaml. The key file sits
|
||||||
beside the last config loaded. \etc\fips\fips.yaml was the
|
beside the last config loaded. \etc\fips\fips.yaml was the
|
||||||
system config of earlier releases, and any local user can
|
system config of earlier releases, and any local user can
|
||||||
create it; the daemon warns when it loads it, and v0.6.0 stops
|
create it; the daemon warns when it loads it, and from v0.6.0
|
||||||
reading it. Move what you need from it into
|
the search no longer looks there. Move what you need from it
|
||||||
C:\ProgramData\fips\fips.yaml and delete it.
|
into C:\ProgramData\fips\fips.yaml and delete it.
|
||||||
|
|
||||||
A service that earlier releases ran from \etc\fips reads
|
A service that earlier releases ran from \etc\fips reads
|
||||||
only C:\ProgramData\fips once install-service.ps1 has run,
|
only C:\ProgramData\fips once install-service.ps1 has run,
|
||||||
|
|||||||
@@ -145,6 +145,11 @@ async fn run_daemon(
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fips::config::warn_legacy(&loaded_paths);
|
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
|
// Identity provisioning: config nsec > key file > generate ephemeral
|
||||||
let mut resolved = match resolve_identity(&config, &loaded_paths) {
|
let mut resolved = match resolve_identity(&config, &loaded_paths) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -164,6 +169,11 @@ async fn run_daemon(
|
|||||||
IdentitySource::Ephemeral => info!("Using ephemeral identity (new keypair each start)"),
|
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
|
// Create node with resolved identity
|
||||||
let mut config = config;
|
let mut config = config;
|
||||||
// Take the nsec rather than move it: `ResolvedIdentity` clears its copy
|
// 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.
|
/// Find an identity key stranded at the legacy system config directory.
|
||||||
///
|
///
|
||||||
/// Adding a second system config directory to the search path moves the
|
/// 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
|
// System config — /etc/fips is always probed so existing installs
|
||||||
// keep working after an upgrade.
|
// 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,
|
// macOS and FreeBSD packaging install config under /usr/local/etc/fips,
|
||||||
// and the Windows service installer under C:\ProgramData\fips; probe
|
// 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]
|
#[test]
|
||||||
fn windows_key_fallback_finds_a_key_in_the_old_appdata_dir() {
|
fn windows_key_fallback_finds_a_key_in_the_old_appdata_dir() {
|
||||||
let root = TempDir::new().unwrap();
|
let root = TempDir::new().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user