Stop writing the identity key file in ephemeral mode

An ephemeral node wrote the private key of an identity it discards on
every restart to fips.key, and overwrote any key already at that path,
including an operator's key in the case where persistent: true was
forgotten. It now writes only fips.pub, so the running npub stays
visible, and holds the private key in memory only.

A fips.key found at an ephemeral start is moved aside to
fips.key.unused with a warning rather than used or overwritten, so an
operator's key is recoverable and a stale key from an earlier release
stops being read by fipsctl address. If that name is already taken or
the rename fails, the file is left in place, a warning says so, and the
start continues.

Persistent and explicit-nsec identities are unchanged. The keygen
note, the Windows service installer's closing note, and the
documentation that described the old write are corrected, including
the persistent identity how-to, which told operators to start once in
ephemeral mode and then pin the key it wrote.
This commit is contained in:
Johnathan Corgan
2026-09-27 19:35:11 +00:00
parent d7f079618f
commit b4e9bfbc08
9 changed files with 312 additions and 66 deletions
+21 -19
View File
@@ -35,19 +35,12 @@ nodes, and tests where you actively want a fresh identity per run.
The Debian/Ubuntu `.deb` and the Arch `fips` AUR package both ship a The Debian/Ubuntu `.deb` and the Arch `fips` AUR package both ship a
default `/etc/fips/fips.yaml` with `node.identity.persistent` left as default `/etc/fips/fips.yaml` with `node.identity.persistent` left as
the upstream default (false), so the daemon writes a fresh keypair to the upstream default (false). In that mode the daemon generates a
`/etc/fips/fips.{key,pub}` on every start until you set fresh keypair on every start, holds the private key only in memory,
`persistent: true`. To pin the current keypair: and writes only `/etc/fips/fips.pub`. To give the node a stable
identity:
1. Install the package and start the daemon once so it generates 1. Install the package, then edit `/etc/fips/fips.yaml` and set:
`fips.key` / `fips.pub`:
```sh
sudo systemctl start fips
sudo systemctl status fips # confirm it came up
```
2. Edit `/etc/fips/fips.yaml` and set:
```yaml ```yaml
node: node:
@@ -55,22 +48,31 @@ the upstream default (false), so the daemon writes a fresh keypair to
persistent: true persistent: true
``` ```
3. Restart the daemon and verify the identity is reused: 2. Start or restart the daemon. On this first persistent start it
generates a keypair and saves it to `/etc/fips/fips.key`:
```sh ```sh
sudo systemctl restart fips sudo systemctl restart fips
sudo systemctl status fips # confirm it came up
```
3. Verify the identity:
```sh
fipsctl show status | grep -E '"npub"|"node_addr"' fipsctl show status | grep -E '"npub"|"node_addr"'
cat /etc/fips/fips.pub cat /etc/fips/fips.pub
``` ```
The npub printed by `fipsctl show status` should match The npub printed by `fipsctl show status` should match
`/etc/fips/fips.pub` and remain stable across subsequent restarts. `/etc/fips/fips.pub`. If the daemon was already running
ephemeral, the npub changes once at this restart and is stable
from then on.
The package's `postinst` script does **not** generate the keypair — The package's `postinst` script does **not** generate the keypair.
the daemon does, on first start. This means the keypair is only The first successful daemon start with `persistent: true` does, so
present after the first successful daemon start. If the daemon never `fips.key` is only present after that start. If the daemon never came
came up cleanly (config error, permission problem), the key files up cleanly (config error, permission problem), the key file will be
will be missing. missing.
### macOS note ### macOS note
+7 -5
View File
@@ -76,16 +76,18 @@ first, then `/usr/local/etc/fips`, so the packaged file wins over a
leftover `/etc/fips` copy from an earlier install. Windows likewise leftover `/etc/fips` copy from an earlier install. Windows likewise
probes `\etc\fips` on the current drive, then `C:\ProgramData\fips`. probes `\etc\fips` on the current drive, then `C:\ProgramData\fips`.
Adjacent to the highest-priority config file the daemon reads (or Adjacent to the highest-priority config file the daemon keeps the
writes, on first start) the identity files: identity files:
| File | Mode | Purpose | | File | Mode | Purpose |
| ---- | ---- | ------- | | ---- | ---- | ------- |
| `fips.key` | `0600` | Bech32 nsec for the persistent identity (Unix; on Windows the file takes its directory's ACL, which `install-service.ps1` restricts to SYSTEM and Administrators). | | `fips.key` | `0600` | Bech32 nsec for the persistent identity, written only in persistent mode (Unix; on Windows the file takes its directory's ACL, which `install-service.ps1` restricts to SYSTEM and Administrators). |
| `fips.pub` | `0644` | Bech32 npub corresponding to `fips.key`. | | `fips.pub` | `0644` | Bech32 npub of the running identity, written on every start. In persistent mode it corresponds to `fips.key`. |
When `node.identity.persistent` is `false` (the default), a fresh When `node.identity.persistent` is `false` (the default), a fresh
keypair is written to these files on every start. keypair is generated on every start and only `fips.pub` is written.
A `fips.key` found there is moved aside to `fips.key.unused` and a
warning is logged.
On Windows the service writes its log to `C:\ProgramData\fips\fips.log`, On Windows the service writes its log to `C:\ProgramData\fips\fips.log`,
rolled at 10 MiB with four old files kept; a foreground run logs to the rolled at 10 MiB with four old files kept; a foreground run logs to the
+4 -2
View File
@@ -108,8 +108,10 @@ Identity resolution follows a three-tier priority:
3. **Ephemeral** — when `persistent: false` (default) and no `nsec`, generates a fresh 3. **Ephemeral** — when `persistent: false` (default) and no `nsec`, generates a fresh
keypair on each start keypair on each start
Key files (`fips.key` with mode 0600, `fips.pub` with mode 0644) are written adjacent `fips.pub` (mode 0644) is written adjacent to the highest-priority config file
to the highest-priority config file for operator visibility, even in ephemeral mode. on every start. `fips.key` (mode 0600) is written only in persistent mode. In
ephemeral mode a `fips.key` found at startup is moved aside to
`fips.key.unused` with a warning.
### General ### General
+2 -2
View File
@@ -70,8 +70,8 @@ operator-editable runtime state:
| Path | Purpose | Writable | Seeded from | | Path | Purpose | Writable | Seeded from |
|---|---|---|---| |---|---|---|---|
| `/var/lib/fips/fips.yaml` | Main config | yes | `services.fips.configFile` (first run only) | | `/var/lib/fips/fips.yaml` | Main config | yes | `services.fips.configFile` (first run only) |
| `/var/lib/fips/fips.key` | Node identity (private) | yes | generated by fips on first start | | `/var/lib/fips/fips.key` | Node identity (private) | yes | generated by fips on first start with `persistent: true` |
| `/var/lib/fips/fips.pub` | Node identity (public) | yes | generated by fips on first start | | `/var/lib/fips/fips.pub` | Node identity (public) | yes | written by fips on every start |
| `/etc/fips/hosts` | Static hostname → npub map | yes | shipped `hosts` (first run only) | | `/etc/fips/hosts` | Static hostname → npub map | yes | shipped `hosts` (first run only) |
| `/etc/fips/peers.allow` | Peer allowlist (ACL) | yes | operator-created | | `/etc/fips/peers.allow` | Peer allowlist (ACL) | yes | operator-created |
| `/etc/fips/peers.deny` | Peer denylist (ACL) | yes | operator-created | | `/etc/fips/peers.deny` | Peer denylist (ACL) | yes | operator-created |
+2 -2
View File
@@ -17,8 +17,8 @@ sudo ./install.sh
| fipstop (TUI) | /usr/local/bin/fipstop | | fipstop (TUI) | /usr/local/bin/fipstop |
| fips-gateway (LAN bridge) | /usr/local/bin/fips-gateway | | fips-gateway (LAN bridge) | /usr/local/bin/fips-gateway |
| Configuration | /etc/fips/fips.yaml | | Configuration | /etc/fips/fips.yaml |
| Identity key | /etc/fips/fips.key (auto-generated) | | Identity key | /etc/fips/fips.key (generated on first start with `persistent: true`) |
| Public key | /etc/fips/fips.pub (auto-generated) | | Public key | /etc/fips/fips.pub (written on every start) |
| Hosts file | /etc/fips/hosts | | Hosts file | /etc/fips/hosts |
| Firewall baseline | /etc/fips/fips.nft | | Firewall baseline | /etc/fips/fips.nft |
| Firewall drop-in directory | /etc/fips/fips.d/ | | Firewall drop-in directory | /etc/fips/fips.d/ |
+3 -3
View File
@@ -101,9 +101,9 @@ Control Socket:
Configuration: Configuration:
The service reads C:\ProgramData\fips\fips.yaml, where The service reads C:\ProgramData\fips\fips.yaml, where
install-service.ps1 puts it, and keeps fips.key, hosts, install-service.ps1 puts it, and keeps hosts, peers.allow,
peers.allow and peers.deny beside it. Edit fips.yaml there peers.deny and, with node.identity.persistent: true, fips.key
before starting the service. beside it. Edit fips.yaml there before starting the service.
install-service.ps1 restricts C:\ProgramData\fips to SYSTEM install-service.ps1 restricts C:\ProgramData\fips to SYSTEM
and Administrators before writing into it. Reading or editing and Administrators before writing into it. Reading or editing
+2 -1
View File
@@ -484,7 +484,8 @@ mod service {
"Configuration: the service reads {}", "Configuration: the service reads {}",
dir.join("fips.yaml").display() dir.join("fips.yaml").display()
); );
println!(" keep fips.key, hosts, peers.allow and peers.deny beside it."); println!(" keep hosts, peers.allow and peers.deny beside it, and fips.key");
println!(" too when node.identity.persistent is true.");
println!( println!(
"Logs: the service writes {}", "Logs: the service writes {}",
dir.join("fips.log").display() dir.join("fips.log").display()
+6 -2
View File
@@ -593,8 +593,12 @@ fn main() {
eprintln!("{npub}"); eprintln!("{npub}");
eprintln!("Key files written to: {}/", dir.display()); eprintln!("Key files written to: {}/", dir.display());
eprintln!(); eprintln!();
eprintln!("NOTE: Set 'node.identity.persistent: true' in fips.yaml"); eprintln!(
eprintln!(" or these keys will be overwritten on next daemon start."); "NOTE: Set 'node.identity.persistent: true' in fips.yaml before the next daemon start."
);
eprintln!(
" Without it the daemon does not use this key: it moves fips.key aside to fips.key.unused."
);
return; return;
} }
+265 -30
View File
@@ -595,13 +595,60 @@ pub fn write_pub_file(path: &Path, npub: &str) -> Result<(), ConfigError> {
Ok(()) Ok(())
} }
/// Move a key file found at an ephemeral start to `<name>.unused` beside it,
/// so it is neither used nor overwritten and stays recoverable.
///
/// Returns the new path when the file was moved. Does nothing when no file is
/// at `key_path`; a dangling symlink counts as a file and is moved as a link.
/// An existing file at the aside path is never replaced: the key is left in
/// place and a warning says so, as it does when the rename fails. Neither case
/// stops the start.
fn retire_key(key_path: &Path) -> Option<PathBuf> {
// symlink_metadata rather than exists: a dangling symlink at the key path
// reports exists() == false but is still a file the operator put there.
key_path.symlink_metadata().ok()?;
let mut name = key_path.file_name()?.to_os_string();
name.push(".unused");
let aside = key_path.with_file_name(name);
let failure = match aside.symlink_metadata() {
Ok(_) => "a file already exists at the aside path".to_string(),
Err(e) if e.kind() != std::io::ErrorKind::NotFound => e.to_string(),
Err(_) => match std::fs::rename(key_path, &aside) {
Ok(()) => {
tracing::warn!(
path = %key_path.display(),
moved_to = %aside.display(),
config_key = "node.identity.persistent",
"An identity key file was found in ephemeral mode and moved aside, not used; \
set node.identity.persistent: true and move it back to use it"
);
return Some(aside);
}
Err(e) => e.to_string(),
},
};
tracing::warn!(
path = %key_path.display(),
aside = %aside.display(),
error = %failure,
config_key = "node.identity.persistent",
"An identity key file was found in ephemeral mode and could not be moved aside; \
it is not used; set node.identity.persistent: true to use it, or remove it"
);
None
}
/// Resolve identity from config and key file. /// Resolve identity from config and key file.
/// ///
/// Behavior depends on `node.identity.persistent`: /// Behavior depends on `node.identity.persistent`:
/// ///
/// - **`persistent: false`** (default): generate a fresh ephemeral keypair /// - **`persistent: false`** (default): generate a fresh ephemeral keypair
/// every start. Key files are written for operator visibility but overwritten /// every start. Only `fips.pub` is written, so the running npub is visible;
/// on each restart. /// the private key is never written. A `fips.key` already at the path is
/// moved aside to `fips.key.unused` with a warning, not used or overwritten.
/// ///
/// - **`persistent: true`**: use three-tier resolution: /// - **`persistent: true`**: use three-tier resolution:
/// 1. Explicit nsec in config — highest priority /// 1. Explicit nsec in config — highest priority
@@ -737,8 +784,8 @@ pub fn resolve_identity(
} }
} }
} else { } else {
// Ephemeral mode (default): fresh keypair every start, write key files // Ephemeral mode (default): a fresh keypair every start, held only in
// for operator visibility // memory. Only the public key file is written.
let identity = Identity::generate(); let identity = Identity::generate();
// `keypair()` and `secret_key()` each hand back a whole private key // `keypair()` and `secret_key()` each hand back a whole private key
// rather than a handle, so both temporaries are bound and erased. // rather than a handle, so both temporaries are bound and erased.
@@ -753,25 +800,8 @@ pub fn resolve_identity(
let _ = std::fs::create_dir_all(parent); let _ = std::fs::create_dir_all(parent);
} }
// symlink_metadata rather than exists: a dangling symlink at the key retire_key(&key_path);
// path reports exists() == false but is still an existing file the
// write is about to act on.
if key_path.symlink_metadata().is_ok() {
tracing::warn!(
path = %key_path.display(),
config_key = "node.identity.persistent",
"An existing key file at this path is being replaced by a fresh ephemeral \
identity; set node.identity.persistent: true to keep the existing identity"
);
}
if let Err(e) = write_key_file(&key_path, &nsec) {
tracing::warn!(
path = %key_path.display(),
error = %e,
"Failed to write the ephemeral key file"
);
}
if let Err(e) = write_pub_file(&pub_path, &npub) { if let Err(e) = write_pub_file(&pub_path, &npub) {
tracing::warn!( tracing::warn!(
path = %pub_path.display(), path = %pub_path.display(),
@@ -2020,29 +2050,64 @@ node:
assert_eq!(fs::read_to_string(&victim).unwrap(), "victim contents\n"); assert_eq!(fs::read_to_string(&victim).unwrap(), "victim contents\n");
} }
/// The names in `dir`, sorted, so a test can assert on the whole directory.
fn dir_names(dir: &Path) -> Vec<String> {
let mut names: Vec<String> = fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
}
/// The npub a resolved identity runs as.
fn resolved_npub(resolved: &ResolvedIdentity) -> String {
crate::Identity::from_secret_str(&resolved.nsec)
.unwrap()
.npub()
}
#[test] #[test]
fn test_ephemeral_over_existing_key_warns() { fn ephemeral_start_moves_an_existing_key_aside_intact() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml"); let config_path = temp_dir.path().join("fips.yaml");
let key_path = temp_dir.path().join("fips.key"); let key_path = temp_dir.path().join("fips.key");
let aside_path = temp_dir.path().join("fips.key.unused");
fs::write(&config_path, "node:\n identity: {}\n").unwrap(); fs::write(&config_path, "node:\n identity: {}\n").unwrap();
let identity = crate::Identity::generate(); let identity = crate::Identity::generate();
let existing = crate::encode_nsec(&identity.keypair().secret_key()); let existing = crate::encode_nsec(&identity.keypair().secret_key());
write_key_file(&key_path, &existing).unwrap(); write_key_file(&key_path, &existing).unwrap();
let planted = fs::read(&key_path).unwrap();
let config = Config::load_file(&config_path).unwrap(); let config = Config::load_file(&config_path).unwrap();
let (resolved, logs) = let (resolved, logs) =
capture_logs(|| resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap()); capture_logs(|| resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap());
assert_ne!(resolved.nsec, existing); assert_ne!(resolved.nsec, existing);
assert!(
key_path.symlink_metadata().is_err(),
"the key file must be moved away from the path a persistent start reads"
);
assert_eq!(
fs::read(&aside_path).unwrap(),
planted,
"the key set aside must hold the planted bytes exactly"
);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
assert_eq!(fs::metadata(&aside_path).unwrap().mode() & 0o777, 0o600);
}
let warnings = logs.warnings(); let warnings = logs.warnings();
assert!( assert!(
warnings warnings
.iter() .iter()
.any(|w| w.contains(&key_path.display().to_string()) .any(|w| w.contains(&key_path.display().to_string())
&& w.contains("node.identity.persistent")), && w.contains(&aside_path.display().to_string())
"expected a warning naming the key path and the config key, got {warnings:?}" && w.contains("node.identity.persistent")
&& w.contains("and moved aside, not used")),
"expected the moved-aside warning naming both paths and the config key, got {warnings:?}"
); );
} }
@@ -2052,6 +2117,7 @@ node:
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml"); let config_path = temp_dir.path().join("fips.yaml");
let key_path = temp_dir.path().join("fips.key"); let key_path = temp_dir.path().join("fips.key");
let aside_path = temp_dir.path().join("fips.key.unused");
let target = temp_dir.path().join("absent-target"); let target = temp_dir.path().join("absent-target");
fs::write(&config_path, "node:\n identity: {}\n").unwrap(); fs::write(&config_path, "node:\n identity: {}\n").unwrap();
@@ -2065,8 +2131,21 @@ node:
assert!( assert!(
warnings warnings
.iter() .iter()
.any(|w| w.contains(&key_path.display().to_string())), .any(|w| w.contains(&key_path.display().to_string())
"expected a warning naming the key path, got {warnings:?}" && w.contains("and moved aside, not used")),
"expected the moved-aside warning naming the key path, got {warnings:?}"
);
assert!(
key_path.symlink_metadata().is_err(),
"the dangling symlink must be moved away from the key path"
);
assert!(
aside_path
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink(),
"the symlink itself must be what was moved aside, not a file written through it"
); );
assert!( assert!(
!target.exists(), !target.exists(),
@@ -2074,6 +2153,155 @@ node:
); );
} }
#[test]
fn ephemeral_start_leaves_a_key_in_place_when_the_aside_name_is_taken() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
let key_path = temp_dir.path().join("fips.key");
let aside_path = temp_dir.path().join("fips.key.unused");
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
let current = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
let earlier = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
write_key_file(&key_path, &current).unwrap();
write_key_file(&aside_path, &earlier).unwrap();
let key_bytes = fs::read(&key_path).unwrap();
let aside_bytes = fs::read(&aside_path).unwrap();
let config = Config::load_file(&config_path).unwrap();
let (resolved, logs) =
capture_logs(|| resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap());
assert!(matches!(resolved.source, IdentitySource::Ephemeral));
assert_ne!(resolved.nsec, current);
assert_eq!(
fs::read(&key_path).unwrap(),
key_bytes,
"the key must be left in place, not overwritten"
);
assert_eq!(
fs::read(&aside_path).unwrap(),
aside_bytes,
"the file already at the aside path must not be replaced"
);
let warnings = logs.warnings();
assert!(
warnings
.iter()
.any(|w| w.contains(&key_path.display().to_string())
&& w.contains(&aside_path.display().to_string())
&& w.contains("node.identity.persistent")
&& w.contains("could not be moved aside")),
"expected the could-not-move warning naming both paths and the config key, got {warnings:?}"
);
}
#[cfg(unix)]
#[test]
fn ephemeral_start_proceeds_when_the_key_cannot_be_moved() {
use std::os::unix::fs::PermissionsExt;
/// Puts the directory's mode back when dropped, so a failing
/// assertion does not leave a read-only directory behind that
/// `TempDir` cannot remove.
struct RestoreMode<'a>(&'a Path);
impl Drop for RestoreMode<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, fs::Permissions::from_mode(0o755));
}
}
// Coverage gap: root bypasses directory permissions, so the rename
// succeeds and this branch goes unexercised when the suite runs as
// root. The aside-name-taken test still covers the same warning.
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipped: running as root, which a read-only directory does not stop");
return;
}
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
let key_path = temp_dir.path().join("fips.key");
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
let existing = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
write_key_file(&key_path, &existing).unwrap();
let planted = fs::read(&key_path).unwrap();
let config = Config::load_file(&config_path).unwrap();
fs::set_permissions(temp_dir.path(), fs::Permissions::from_mode(0o555)).unwrap();
let _restore = RestoreMode(temp_dir.path());
let (resolved, logs) =
capture_logs(|| resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap());
assert!(matches!(resolved.source, IdentitySource::Ephemeral));
assert_ne!(resolved.nsec, existing);
assert_eq!(
fs::read(&key_path).unwrap(),
planted,
"a key that cannot be moved must be left as it was, not overwritten"
);
let warnings = logs.warnings();
assert!(
warnings
.iter()
.any(|w| w.contains(&key_path.display().to_string())
&& w.contains("could not be moved aside")
&& w.contains("os error")),
"expected a warning naming the key path and the rename error, got {warnings:?}"
);
}
#[test]
fn ephemeral_restarts_never_leave_a_private_key_file() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
let pub_path = temp_dir.path().join("fips.pub");
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
let config = Config::load_file(&config_path).unwrap();
for start in 1..=3 {
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
assert_eq!(
dir_names(temp_dir.path()),
["fips.pub", "fips.yaml"],
"after start {start} the directory must hold only the config and the public key"
);
assert_eq!(
fs::read_to_string(&pub_path).unwrap().trim(),
resolved_npub(&resolved),
"after start {start} fips.pub must name the running identity"
);
}
}
#[test]
fn persistent_start_ignores_a_key_set_aside() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
let key_path = temp_dir.path().join("fips.key");
let aside_path = temp_dir.path().join("fips.key.unused");
fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
let earlier = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
write_key_file(&aside_path, &earlier).unwrap();
let aside_bytes = fs::read(&aside_path).unwrap();
let config = Config::load_file(&config_path).unwrap();
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
assert!(matches!(resolved.source, IdentitySource::Generated(_)));
assert_ne!(resolved.nsec, earlier);
assert_eq!(read_key_file(&key_path).unwrap(), resolved.nsec);
assert_eq!(
fs::read(&aside_path).unwrap(),
aside_bytes,
"a persistent start must leave the key set aside untouched"
);
}
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn test_persistent_permissive_key_warns() { fn test_persistent_permissive_key_warns() {
@@ -2205,11 +2433,18 @@ node:
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap(); let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
assert!(matches!(resolved.source, IdentitySource::Ephemeral)); assert!(matches!(resolved.source, IdentitySource::Ephemeral));
// Key files should still be written for operator visibility // Only the public key is written: an ephemeral private key lives in
// memory and nowhere else.
let key_path = temp_dir.path().join("fips.key"); let key_path = temp_dir.path().join("fips.key");
let pub_path = temp_dir.path().join("fips.pub"); let pub_path = temp_dir.path().join("fips.pub");
assert!(key_path.exists()); assert_eq!(
assert!(pub_path.exists()); key_path.symlink_metadata().unwrap_err().kind(),
std::io::ErrorKind::NotFound
);
assert_eq!(
fs::read_to_string(&pub_path).unwrap().trim(),
resolved_npub(&resolved)
);
} }
#[test] #[test]