fix(#2391): mandatory hive- prefixed snapshot label, nest subvol snapshot create/delete

Per mara's PR review:
- snapshot label is now mandatory (was optional w/ timestamp default)
  and must start with "hive-" — hive-priv enforces this as an
  allow-list on top of the existing credential-name charset check, so
  only hivectl-issued labels can reach the btrfs shellout.
- nest under `subvol snapshot create`/`subvol snapshot delete`
  instead of othering delete as a separate top-level `delete-snapshot`
  verb.

Per argus's review:
- regenerate docs/tools/hivectl-cli.md (hivectl markdown-docs) to
  include the new subcommands — CI's hivectl-docs-fresh check compares
  this file against generated output.
This commit is contained in:
atlas 2026-07-14 18:26:12 +02:00 committed by mara
commit 8f8076b8ed
4 changed files with 106 additions and 32 deletions

View file

@ -370,7 +370,7 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
ref snapshot_name,
} => {
validate_agent_name(agent_name)?;
validate_credential_name(snapshot_name)?;
validate_snapshot_name(snapshot_name)?;
snapshot_agent_subvolume(agent_name, snapshot_name).await
}
@ -379,7 +379,7 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
ref snapshot_name,
} => {
validate_agent_name(agent_name)?;
validate_credential_name(snapshot_name)?;
validate_snapshot_name(snapshot_name)?;
delete_agent_snapshot(agent_name, snapshot_name).await
}
@ -430,6 +430,20 @@ fn handle_write_nspawn_flags(
Ok((String::new(), String::new()))
}
/// A btrfs snapshot label must start with `hive-` — this doubles as an
/// allow-list: only names hivectl itself constructs (or an operator who
/// knows the convention) can reach the `btrfs subvolume snapshot`/`delete`
/// shellouts, so an arbitrary caller can't use the snapshot ops to probe or
/// churn unrelated paths under `AGENT_STATE_ROOT`. Beyond the prefix, the
/// same charset restriction as [`validate_credential_name`] applies (it's
/// interpolated straight into a filesystem path).
fn validate_snapshot_name(name: &str) -> Result<()> {
if !name.starts_with("hive-") {
bail!("invalid snapshot label {name:?}: must start with \"hive-\"");
}
validate_credential_name(name)
}
/// A systemd credential id must be a short token — restrict to
/// `[A-Za-z0-9_.-]` so it can't inject extra `--load-credential` argv or
/// break the `name:path` shape.