Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ members = [
"internal-dns/types/versions",
"ipcc",
"key-manager",
"key-manager/types",
"live-tests",
"live-tests/macros",
"nexus",
Expand Down Expand Up @@ -247,6 +248,7 @@ default-members = [
"internal-dns/types/versions",
"ipcc",
"key-manager",
"key-manager/types",
"live-tests",
"live-tests/macros",
"nexus",
Expand Down Expand Up @@ -553,6 +555,7 @@ ipnetwork = { version = "0.21", features = ["schemars", "serde"] }
ispf = { git = "https://github.com/oxidecomputer/ispf" }
jiff = "0.2.15"
key-manager = { path = "key-manager" }
key-manager-types = { path = "key-manager/types" }
kstat-rs = "0.2.4"
libc = "0.2.174"
libipcc = { git = "https://github.com/oxidecomputer/ipcc-rs", rev = "524eb8f125003dff50b9703900c6b323f00f9e1b" }
Expand Down Expand Up @@ -829,6 +832,7 @@ wicketd-client = { path = "clients/wicketd-client" }
xshell = "0.2.7"
zerocopy = "0.8.26"
zeroize = { version = "1.8.1", features = ["zeroize_derive", "std"] }
zfs-atomic-change-key = { git = "https://github.com/oxidecomputer/zfs-atomic-change-key" }
zfs-test-harness = { path = "sled-storage/zfs-test-harness" }
zip = { version = "4.2.0", default-features = false, features = ["deflate","bzip2"] }
zone = { version = "0.3.1", default-features = false, features = ["async"] }
Expand Down
2 changes: 2 additions & 0 deletions illumos-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ futures.workspace = true
http.workspace = true
ipnetwork.workspace = true
itertools.workspace = true
key-manager-types.workspace = true
libc.workspace = true
macaddr.workspace = true
nix.workspace = true
Expand All @@ -44,6 +45,7 @@ tokio.workspace = true
uuid.workspace = true
whoami.workspace = true
zone.workspace = true
zfs-atomic-change-key.workspace = true
tofino.workspace = true
rustix.workspace = true

Expand Down
142 changes: 135 additions & 7 deletions illumos-utils/src/zfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,42 @@ pub struct GetValueError {
#[error("Failed to list snapshots: {0}")]
pub struct ListSnapshotsError(#[from] crate::ExecutionError);

/// Error returned by [`Zfs::change_key`].
#[derive(thiserror::Error, Debug)]
#[error("Failed to change encryption key for dataset '{name}'")]
pub struct ChangeKeyError {
pub name: String,
#[source]
pub err: anyhow::Error,
}

/// Error returned by [`Zfs::load_key`].
#[derive(thiserror::Error, Debug)]
#[error("Failed to load encryption key for dataset '{name}'")]
pub struct LoadKeyError {
pub name: String,
#[source]
pub err: crate::ExecutionError,
}

/// Error returned by [`Zfs::dataset_exists`].
#[derive(thiserror::Error, Debug)]
#[error("Failed to check if dataset '{name}' exists")]
pub struct DatasetExistsError {
pub name: String,
#[source]
pub err: crate::ExecutionError,
}

/// Error returned by [`Zfs::unload_key`].
#[derive(thiserror::Error, Debug)]
#[error("Failed to unload encryption key for dataset '{name}'")]
pub struct UnloadKeyError {
pub name: String,
#[source]
pub err: crate::ExecutionError,
}

#[derive(Debug, thiserror::Error)]
#[error(
"Failed to create snapshot '{snap_name}' from filesystem '{filesystem}': {err}"
Expand Down Expand Up @@ -523,11 +559,14 @@ pub struct DatasetProperties {
/// string so that unexpected compression formats don't prevent inventory
/// from being collected.
pub compression: String,
/// The encryption key epoch for this dataset.
///
/// Only present on encrypted datasets (e.g., crypt datasets on U.2s).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth noting here that this is not present on datasets that inherit encryption from a parent (if I understand that correctly)?

pub epoch: Option<u64>,
}

impl DatasetProperties {
const ZFS_GET_PROPS: &'static str =
"oxide:uuid,name,mounted,avail,used,quota,reservation,compression";
const ZFS_GET_PROPS: &'static str = "oxide:uuid,oxide:epoch,name,mounted,avail,used,quota,reservation,compression";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be formatted.

}

impl TryFrom<&DatasetProperties> for SharedDatasetConfig {
Expand Down Expand Up @@ -648,6 +687,18 @@ impl DatasetProperties {
.get("compression")
.map(|(prop, _source)| prop.to_string())
.ok_or_else(|| anyhow!("Missing 'compression'"))?;
// The epoch property is only present on encrypted datasets.
// Like oxide:uuid, we ignore inherited values.
let epoch = props
.get("oxide:epoch")
.filter(|(prop, source)| {
!source.starts_with("inherited") && *prop != "-"
})
.map(|(prop, _source)| {
prop.parse::<u64>()
.context("Failed to parse 'oxide:epoch'")
})
.transpose()?;

Ok(DatasetProperties {
id,
Expand All @@ -658,6 +709,7 @@ impl DatasetProperties {
quota,
reservation,
compression,
epoch,
})
})
.collect::<Result<Vec<_>, _>>()
Expand Down Expand Up @@ -1197,7 +1249,7 @@ impl Zfs {
name: &str,
mountpoint: &Mountpoint,
) -> Result<(), EnsureDatasetErrorRaw> {
let mount_info = Self::dataset_exists(name, mountpoint).await?;
let mount_info = Self::dataset_mount_info(name, mountpoint).await?;
if !mount_info.exists {
return Err(EnsureDatasetErrorRaw::DoesNotExist);
}
Expand Down Expand Up @@ -1246,7 +1298,7 @@ impl Zfs {
additional_options,
}: DatasetEnsureArgs<'_>,
) -> Result<(), EnsureDatasetErrorRaw> {
let dataset_info = Self::dataset_exists(name, &mountpoint).await?;
let dataset_info = Self::dataset_mount_info(name, &mountpoint).await?;

// Non-zoned datasets with an explicit mountpoint and the
// "canmount=on" property should be mounted within the global zone.
Expand Down Expand Up @@ -1365,9 +1417,29 @@ impl Zfs {
Ok(())
}

// Return (true, mounted) if the dataset exists, (false, false) otherwise,
// where mounted is if the dataset is mounted.
async fn dataset_exists(
/// Check if a ZFS dataset exists.
pub async fn dataset_exists(
name: &str,
) -> Result<bool, DatasetExistsError> {
let mut cmd = Command::new(ZFS);
cmd.args(&["list", "-H", name]);
match execute_async(&mut cmd).await {
Ok(_) => Ok(true),
Err(crate::ExecutionError::CommandFailure(ref info))
if info.stderr.contains("does not exist") =>
{
Ok(false)
}
Err(err) => Err(DatasetExistsError { name: name.to_string(), err }),
}
}

/// Get mount info for a dataset, validating its mountpoint.
///
/// Returns (exists=true, mounted) if the dataset exists with the expected
/// mountpoint, (exists=false, mounted=false) if it doesn't exist.
/// Returns an error if the dataset exists but has an unexpected mountpoint.
async fn dataset_mount_info(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed this internal function because it was previously named dataset_exists and I wanted to use that name, and it was not a good name for what this function actually returns (DatasetMountInfo).

name: &str,
mountpoint: &Mountpoint,
) -> Result<DatasetMountInfo, EnsureDatasetErrorRaw> {
Expand Down Expand Up @@ -1523,6 +1595,62 @@ impl Zfs {
})
}

/// Atomically change the encryption key and set the oxide:epoch property.
///
/// This operation is used for ZFS key rotation when a new Trust Quorum
/// epoch is committed.
pub async fn change_key(
dataset: &str,
key: &key_manager_types::VersionedAes256GcmDiskEncryptionKey,
) -> Result<(), ChangeKeyError> {
// FIXME: Replace the use of `zfs_atomic_change_key` with a native
// invocation of `zfs change-key` using the `-o oxide:epoch` option to
// set the epoch. At time of writing, the `zfs change-key` command does
// not support setting user properties inline, but a patch is pending to
// add this feature.

let ds = zfs_atomic_change_key::Dataset::new(dataset).map_err(|e| {
ChangeKeyError {
name: dataset.to_string(),
err: anyhow::anyhow!("invalid dataset name: {e}"),
}
})?;

ds.change_key(zfs_atomic_change_key::Key::hex(*key.expose_secret()))
.property("oxide:epoch", key.epoch().to_string())
.await
.map_err(|e| ChangeKeyError {
name: dataset.to_string(),
err: anyhow::anyhow!("{e}"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this should be

Suggested change
err: anyhow::anyhow!("{e}"),
err: anyhow::Error::from(e),

rather than formatting it, as that will eat the structured representation of the error's source chain, while Error::from preserves it.

})
}

/// Load the encryption key for an encrypted ZFS dataset.
///
/// This makes the dataset accessible for mounting. The key must have
/// previously been written to the dataset's keylocation.
pub async fn load_key(name: &str) -> Result<(), LoadKeyError> {
let mut cmd = Command::new(PFEXEC);
cmd.args(&[ZFS, "load-key", name]);
execute_async(&mut cmd)
.await
.map(|_| ())
.map_err(|err| LoadKeyError { name: name.to_string(), err })
}

/// Unload the encryption key for an encrypted ZFS dataset.
///
/// This is used for cleanup after failed key operations or during
/// trial decryption recovery. The dataset must not be mounted.
pub async fn unload_key(name: &str) -> Result<(), UnloadKeyError> {
let mut cmd = Command::new(PFEXEC);
cmd.args(&[ZFS, "unload-key", name]);
execute_async(&mut cmd)
.await
.map(|_| ())
.map_err(|err| UnloadKeyError { name: name.to_string(), err })
}

/// Calls "zfs get" to acquire multiple values
///
/// - `names`: The properties being acquired
Expand Down
1 change: 1 addition & 0 deletions key-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ workspace = true
[dependencies]
async-trait.workspace = true
hkdf.workspace = true
key-manager-types.workspace = true
omicron-common.workspace = true
secrecy.workspace = true
sha3.workspace = true
Expand Down
Loading
Loading