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
88 changes: 79 additions & 9 deletions src/agent/src/confidential_data_hub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,18 @@ pub fn is_cdh_client_initialized() -> bool {
CDH_CLIENT.get().is_some() // Returns true if CDH_CLIENT is initialized, false otherwise
}

pub(crate) fn is_sealed_env(env: &str) -> bool {
env.split_once('=')
.is_some_and(|(_, value)| value.starts_with(SEALED_SECRET_PREFIX))
}

pub async fn unseal_env(env: &str) -> Result<String> {
let cdh_client = CDH_CLIENT
.get()
.expect("Confidential Data Hub not initialized");

if let Some((key, value)) = env.split_once('=') {
if value.starts_with(SEALED_SECRET_PREFIX) {
if is_sealed_env(env) {
if let Some((key, value)) = env.split_once('=') {
let unsealed_value = cdh_client.unseal_secret_async(value).await?;
let unsealed_env = format!("{}={}", key, std::str::from_utf8(&unsealed_value)?);

Expand Down Expand Up @@ -308,11 +313,15 @@ mod tests {
use async_trait::async_trait;
use std::fs::File;
use std::io::{Read, Write};
use std::sync::Arc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use tempfile::{tempdir, NamedTempFile};
use test_utils::skip_if_not_root;
use tokio::signal::unix::{signal, SignalKind};
struct TestService;
struct TestService {
fail_unseal: Arc<AtomicBool>,
}

#[async_trait]
impl confidential_data_hub_ttrpc_async::SealedSecretService for TestService {
Expand All @@ -321,6 +330,13 @@ mod tests {
_ctx: &::ttrpc::asynchronous::TtrpcContext,
_req: confidential_data_hub::UnsealSecretInput,
) -> ttrpc::error::Result<confidential_data_hub::UnsealSecretOutput> {
if self.fail_unseal.load(Ordering::SeqCst) {
return Err(ttrpc::Error::RpcStatus(ttrpc::get_status(
ttrpc::Code::INTERNAL,
"injected unseal failure".to_string(),
)));
}

let mut output = confidential_data_hub::UnsealSecretOutput::new();
output.set_plaintext("unsealed".into());
Ok(output)
Expand Down Expand Up @@ -351,9 +367,9 @@ mod tests {
Ok(())
}

fn start_ttrpc_server(cdh_socket_uri: String) {
fn start_ttrpc_server(cdh_socket_uri: String, fail_unseal: Arc<AtomicBool>) {
tokio::spawn(async move {
let ss = Box::new(TestService {});
let ss = Box::new(TestService { fail_unseal });
let ss = Arc::new(*ss);
let ss_service = confidential_data_hub_ttrpc_async::create_sealed_secret_service(ss);

Expand All @@ -377,17 +393,17 @@ mod tests {

#[tokio::test]
async fn test_sealed_secret() {
skip_if_not_root!();
let test_dir = tempdir().expect("failed to create tmpdir");
let test_dir_path = test_dir.path();
let cdh_sock_uri = &format!(
"unix://{}",
test_dir_path.join("cdh.sock").to_str().unwrap()
);

let fail_unseal = Arc::new(AtomicBool::new(false));
let rt = tokio::runtime::Runtime::new().unwrap();
let _guard = rt.enter();
start_ttrpc_server(cdh_sock_uri.to_string());
start_ttrpc_server(cdh_sock_uri.to_string(), fail_unseal.clone());
std::thread::sleep(std::time::Duration::from_secs(2));
init_cdh_client(cdh_sock_uri).await.unwrap();

Expand Down Expand Up @@ -428,6 +444,60 @@ mod tests {
assert_eq!(contents, String::from("testdata"));
fs::remove_file(normal_filename).unwrap();

// The container-level handler leaves ordinary environment values alone.
let mut normal_spec = oci_spec::runtime::Spec::default();
let mut normal_process = oci_spec::runtime::Process::default();
normal_process.set_env(Some(vec![
"NORMAL=value".to_string(),
"SECRET=sealed.testdata".to_string(),
]));
normal_spec.set_process(Some(normal_process));
normal_spec.set_mounts(Some(Vec::new()));
crate::rpc::cdh_handler_sealed_secrets(&mut normal_spec)
.await
.unwrap();
assert_eq!(
normal_spec.process().as_ref().unwrap().env(),
&Some(vec![
"NORMAL=value".to_string(),
"SECRET=unsealed".to_string(),
])
);

// A declared sealed environment secret must not reach container creation
// when CDH cannot unseal it.
fail_unseal.store(true, Ordering::SeqCst);
let mut failing_env_spec = oci_spec::runtime::Spec::default();
let mut failing_env_process = oci_spec::runtime::Process::default();
failing_env_process.set_env(Some(vec!["SECRET=sealed.testdata".to_string()]));
failing_env_spec.set_process(Some(failing_env_process));
failing_env_spec.set_mounts(Some(Vec::new()));
assert!(
crate::rpc::cdh_handler_sealed_secrets(&mut failing_env_spec)
.await
.is_err(),
"sealed environment unseal failure must abort container setup"
);

// Candidate secret mounts must also fail closed. A missing source gives a
// deterministic unseal_file error without requiring a privileged mount.
let mut failing_mount_spec = oci_spec::runtime::Spec::default();
let mut failing_mount_process = oci_spec::runtime::Process::default();
failing_mount_process.set_env(Some(Vec::new()));
failing_mount_spec.set_process(Some(failing_mount_process));
let mut secret_mount = oci_spec::runtime::Mount::default();
secret_mount.set_source(Some(
"/run/kata-containers/shared/containers/missing-sealed-secret".into(),
));
secret_mount.set_destination("/run/secrets/example".into());
failing_mount_spec.set_mounts(Some(vec![secret_mount]));
assert!(
crate::rpc::cdh_handler_sealed_secrets(&mut failing_mount_spec)
.await
.is_err(),
"sealed mount unseal failure must abort container setup"
);

rt.shutdown_background();
std::thread::sleep(std::time::Duration::from_secs(2));
}
Expand Down
40 changes: 36 additions & 4 deletions src/agent/src/initdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub const INITDATA_PATH: &str = "/run/confidential-containers/initdata";
const AA_CONFIG_KEY: &str = "aa.toml";
const CDH_CONFIG_KEY: &str = "cdh.toml";
const POLICY_KEY: &str = "policy.rego";
const SECURE_VOLUMES_KEY: &str = "akash-secure-volumes.json";

/// Initdata key for the container image registry authentication file.
///
Expand All @@ -50,6 +51,9 @@ pub const AA_CONFIG_PATH: &str = concatcp!(INITDATA_PATH, "/aa.toml");
/// The path of CDH's config file
pub const CDH_CONFIG_PATH: &str = concatcp!(INITDATA_PATH, "/cdh.toml");

/// Path of the measured Akash confidential persistent-volume descriptor.
pub const SECURE_VOLUMES_PATH: &str = concatcp!(INITDATA_PATH, "/akash-secure-volumes.json");

/// Magic number of initdata device
#[cfg(feature = "init-data")]
pub const INITDATA_MAGIC_NUMBER: &[u8] = b"initdata";
Expand Down Expand Up @@ -147,9 +151,8 @@ pub struct InitdataReturnValue {
/// Missing keys are skipped. `policy.rego` is intentionally not written here: it
/// is returned to the caller instead.
///
/// The registry authentication file holds credentials, so it is created with
/// owner-only (0600) permissions to avoid exposing them to other processes in
/// the guest.
/// Registry authentication and secure-volume descriptor files are created with
/// owner-only (0600) permissions.
async fn materialize_initdata_files(
logger: &Logger,
initdata: &InitData,
Expand Down Expand Up @@ -182,6 +185,17 @@ async fn materialize_initdata_files(
info!(logger, "write registry auth file from initdata");
}

if let Some(descriptor) = initdata.get_coco_data(SECURE_VOLUMES_KEY) {
let path = base_dir.join(SECURE_VOLUMES_KEY);
tokio::fs::write(&path, descriptor)
.await
.context("write secure-volume descriptor failed")?;
tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
.await
.context("set secure-volume descriptor permissions failed")?;
info!(logger, "write secure-volume descriptor from initdata");
}

Ok(())
}

Expand Down Expand Up @@ -264,7 +278,10 @@ mod tests {

let path = dir.path().join(AUTH_FILE_KEY);
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert_eq!(content, TEST_AUTH_JSON, "auth file content must match initdata");
assert_eq!(
content, TEST_AUTH_JSON,
"auth file content must match initdata"
);

let mode = tokio::fs::metadata(&path)
.await
Expand All @@ -286,6 +303,7 @@ mod tests {
initdata.insert_data(AA_CONFIG_KEY, "aa-config");
initdata.insert_data(CDH_CONFIG_KEY, "cdh-config");
initdata.insert_data(AUTH_FILE_KEY, TEST_AUTH_JSON);
initdata.insert_data(SECURE_VOLUMES_KEY, r#"{"version":"1","volumes":[]}"#);

materialize_initdata_files(&test_logger(), &initdata, dir.path())
.await
Expand All @@ -309,6 +327,18 @@ mod tests {
.unwrap(),
TEST_AUTH_JSON
);
assert_eq!(
tokio::fs::read_to_string(dir.path().join(SECURE_VOLUMES_KEY))
.await
.unwrap(),
r#"{"version":"1","volumes":[]}"#
);
let secure_volume_mode = tokio::fs::metadata(dir.path().join(SECURE_VOLUMES_KEY))
.await
.unwrap()
.permissions()
.mode();
assert_eq!(secure_volume_mode & 0o777, 0o600);
}

// Absent keys (including the new auth key) must not create files: this
Expand All @@ -329,6 +359,7 @@ mod tests {
"auth file must not be created when absent from initdata"
);
assert!(!dir.path().join(AA_CONFIG_KEY).exists());
assert!(!dir.path().join(SECURE_VOLUMES_KEY).exists());
}

// An empty [data] map is a no-op and must not error.
Expand All @@ -344,5 +375,6 @@ mod tests {
assert!(!dir.path().join(AUTH_FILE_KEY).exists());
assert!(!dir.path().join(AA_CONFIG_KEY).exists());
assert!(!dir.path().join(CDH_CONFIG_KEY).exists());
assert!(!dir.path().join(SECURE_VOLUMES_KEY).exists());
}
}
Loading
Loading