From b0f910c8cd2e2393f31d9132911f21dfcc5768d2 Mon Sep 17 00:00:00 2001 From: Linda Yu Date: Tue, 5 Sep 2023 16:15:18 +0800 Subject: [PATCH 1/5] cdh/storage: add api for secure mount Signed-off-by: Linda Yu --- Cargo.toml | 1 + confidential-data-hub/hub/Cargo.toml | 1 + confidential-data-hub/hub/protos/api.proto | 17 + confidential-data-hub/hub/src/api.rs | 3 + .../hub/src/bin/confidential-data-hub/api.rs | 358 +++++++++++++++++- .../bin/confidential-data-hub/api_ttrpc.rs | 48 +++ .../hub/src/bin/confidential-data-hub/main.rs | 3 + .../bin/confidential-data-hub/server/mod.rs | 38 +- confidential-data-hub/hub/src/error.rs | 3 + confidential-data-hub/hub/src/hub.rs | 9 + 10 files changed, 472 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 99d52a06e..7ec7bd99d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "confidential-data-hub/kms", "confidential-data-hub/image", "confidential-data-hub/secret", + "confidential-data-hub/storage", "image-rs", "ocicrypt-rs", ] diff --git a/confidential-data-hub/hub/Cargo.toml b/confidential-data-hub/hub/Cargo.toml index 5eed01edb..4dd0a4c3b 100644 --- a/confidential-data-hub/hub/Cargo.toml +++ b/confidential-data-hub/hub/Cargo.toml @@ -21,6 +21,7 @@ lazy_static.workspace = true log.workspace = true protobuf = { workspace = true, optional = true } secret.path = "../secret" +storage.path = "../storage" serde = { workspace = true, optional = true } serde_json.workspace = true sev = { path = "../../attestation-agent/deps/sev", optional = true } diff --git a/confidential-data-hub/hub/protos/api.proto b/confidential-data-hub/hub/protos/api.proto index 97a3c6266..ee3a247be 100644 --- a/confidential-data-hub/hub/protos/api.proto +++ b/confidential-data-hub/hub/protos/api.proto @@ -26,6 +26,19 @@ message KeyProviderKeyWrapProtocolOutput { bytes KeyProviderKeyWrapProtocolOutput = 1; } +message SecureMountRequest { + string driver = 1; + repeated string driver_options = 2; + string source = 3; + string fstype = 4; + repeated string options = 5; + string mount_point = 6; +} + +message SecureMountResponse { + string mountPath = 1; +} + service SealedSecretService { rpc UnsealSecret(UnsealSecretInput) returns (UnsealSecretOutput) {}; } @@ -37,3 +50,7 @@ service GetResourceService { service KeyProviderService { rpc UnWrapKey(KeyProviderKeyWrapProtocolInput) returns (KeyProviderKeyWrapProtocolOutput) {}; } + +service SecureMountService { + rpc SecureMount(SecureMountRequest) returns (SecureMountResponse) {}; +} diff --git a/confidential-data-hub/hub/src/api.rs b/confidential-data-hub/hub/src/api.rs index 3069bff3d..880db037e 100644 --- a/confidential-data-hub/hub/src/api.rs +++ b/confidential-data-hub/hub/src/api.rs @@ -6,6 +6,7 @@ use async_trait::async_trait; use crate::Result; +use storage::volume_type::Storage; /// The APIs of the DataHub. See /// for @@ -26,4 +27,6 @@ pub trait DataHub { /// URI is defined in /// async fn get_resource(&self, uri: String) -> Result>; + + async fn secure_mount(&self, storage: Storage) -> Result; } diff --git a/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs b/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs index a58d8c2eb..347803681 100644 --- a/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs +++ b/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs @@ -757,6 +757,340 @@ impl ::protobuf::reflect::ProtobufValue for KeyProviderKeyWrapProtocolOutput { type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; } +#[derive(PartialEq,Clone,Default,Debug)] +// @@protoc_insertion_point(message:api.SecureMountRequest) +pub struct SecureMountRequest { + // message fields + // @@protoc_insertion_point(field:api.SecureMountRequest.driver) + pub driver: ::std::string::String, + // @@protoc_insertion_point(field:api.SecureMountRequest.driver_options) + pub driver_options: ::std::vec::Vec<::std::string::String>, + // @@protoc_insertion_point(field:api.SecureMountRequest.source) + pub source: ::std::string::String, + // @@protoc_insertion_point(field:api.SecureMountRequest.fstype) + pub fstype: ::std::string::String, + // @@protoc_insertion_point(field:api.SecureMountRequest.options) + pub options: ::std::vec::Vec<::std::string::String>, + // @@protoc_insertion_point(field:api.SecureMountRequest.mount_point) + pub mount_point: ::std::string::String, + // special fields + // @@protoc_insertion_point(special_field:api.SecureMountRequest.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a SecureMountRequest { + fn default() -> &'a SecureMountRequest { + ::default_instance() + } +} + +impl SecureMountRequest { + pub fn new() -> SecureMountRequest { + ::std::default::Default::default() + } + + fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { + let mut fields = ::std::vec::Vec::with_capacity(6); + let mut oneofs = ::std::vec::Vec::with_capacity(0); + fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( + "driver", + |m: &SecureMountRequest| { &m.driver }, + |m: &mut SecureMountRequest| { &mut m.driver }, + )); + fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( + "driver_options", + |m: &SecureMountRequest| { &m.driver_options }, + |m: &mut SecureMountRequest| { &mut m.driver_options }, + )); + fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( + "source", + |m: &SecureMountRequest| { &m.source }, + |m: &mut SecureMountRequest| { &mut m.source }, + )); + fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( + "fstype", + |m: &SecureMountRequest| { &m.fstype }, + |m: &mut SecureMountRequest| { &mut m.fstype }, + )); + fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( + "options", + |m: &SecureMountRequest| { &m.options }, + |m: &mut SecureMountRequest| { &mut m.options }, + )); + fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( + "mount_point", + |m: &SecureMountRequest| { &m.mount_point }, + |m: &mut SecureMountRequest| { &mut m.mount_point }, + )); + ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( + "SecureMountRequest", + fields, + oneofs, + ) + } +} + +impl ::protobuf::Message for SecureMountRequest { + const NAME: &'static str = "SecureMountRequest"; + + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => { + self.driver = is.read_string()?; + }, + 18 => { + self.driver_options.push(is.read_string()?); + }, + 26 => { + self.source = is.read_string()?; + }, + 34 => { + self.fstype = is.read_string()?; + }, + 42 => { + self.options.push(is.read_string()?); + }, + 50 => { + self.mount_point = is.read_string()?; + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if !self.driver.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.driver); + } + for value in &self.driver_options { + my_size += ::protobuf::rt::string_size(2, &value); + }; + if !self.source.is_empty() { + my_size += ::protobuf::rt::string_size(3, &self.source); + } + if !self.fstype.is_empty() { + my_size += ::protobuf::rt::string_size(4, &self.fstype); + } + for value in &self.options { + my_size += ::protobuf::rt::string_size(5, &value); + }; + if !self.mount_point.is_empty() { + my_size += ::protobuf::rt::string_size(6, &self.mount_point); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if !self.driver.is_empty() { + os.write_string(1, &self.driver)?; + } + for v in &self.driver_options { + os.write_string(2, &v)?; + }; + if !self.source.is_empty() { + os.write_string(3, &self.source)?; + } + if !self.fstype.is_empty() { + os.write_string(4, &self.fstype)?; + } + for v in &self.options { + os.write_string(5, &v)?; + }; + if !self.mount_point.is_empty() { + os.write_string(6, &self.mount_point)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> SecureMountRequest { + SecureMountRequest::new() + } + + fn clear(&mut self) { + self.driver.clear(); + self.driver_options.clear(); + self.source.clear(); + self.fstype.clear(); + self.options.clear(); + self.mount_point.clear(); + self.special_fields.clear(); + } + + fn default_instance() -> &'static SecureMountRequest { + static instance: SecureMountRequest = SecureMountRequest { + driver: ::std::string::String::new(), + driver_options: ::std::vec::Vec::new(), + source: ::std::string::String::new(), + fstype: ::std::string::String::new(), + options: ::std::vec::Vec::new(), + mount_point: ::std::string::String::new(), + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +impl ::protobuf::MessageFull for SecureMountRequest { + fn descriptor() -> ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); + descriptor.get(|| file_descriptor().message_by_package_relative_name("SecureMountRequest").unwrap()).clone() + } +} + +impl ::std::fmt::Display for SecureMountRequest { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for SecureMountRequest { + type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; +} + +#[derive(PartialEq,Clone,Default,Debug)] +// @@protoc_insertion_point(message:api.SecureMountResponse) +pub struct SecureMountResponse { + // message fields + // @@protoc_insertion_point(field:api.SecureMountResponse.mountPath) + pub mountPath: ::std::string::String, + // special fields + // @@protoc_insertion_point(special_field:api.SecureMountResponse.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a SecureMountResponse { + fn default() -> &'a SecureMountResponse { + ::default_instance() + } +} + +impl SecureMountResponse { + pub fn new() -> SecureMountResponse { + ::std::default::Default::default() + } + + fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { + let mut fields = ::std::vec::Vec::with_capacity(1); + let mut oneofs = ::std::vec::Vec::with_capacity(0); + fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( + "mountPath", + |m: &SecureMountResponse| { &m.mountPath }, + |m: &mut SecureMountResponse| { &mut m.mountPath }, + )); + ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( + "SecureMountResponse", + fields, + oneofs, + ) + } +} + +impl ::protobuf::Message for SecureMountResponse { + const NAME: &'static str = "SecureMountResponse"; + + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => { + self.mountPath = is.read_string()?; + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if !self.mountPath.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.mountPath); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if !self.mountPath.is_empty() { + os.write_string(1, &self.mountPath)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> SecureMountResponse { + SecureMountResponse::new() + } + + fn clear(&mut self) { + self.mountPath.clear(); + self.special_fields.clear(); + } + + fn default_instance() -> &'static SecureMountResponse { + static instance: SecureMountResponse = SecureMountResponse { + mountPath: ::std::string::String::new(), + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +impl ::protobuf::MessageFull for SecureMountResponse { + fn descriptor() -> ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); + descriptor.get(|| file_descriptor().message_by_package_relative_name("SecureMountResponse").unwrap()).clone() + } +} + +impl ::std::fmt::Display for SecureMountResponse { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for SecureMountResponse { + type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; +} + static file_descriptor_proto_data: &'static [u8] = b"\ \n\tapi.proto\x12\x03api\"+\n\x11UnsealSecretInput\x12\x16\n\x06secret\ \x18\x01\x20\x01(\x0cR\x06secret\"2\n\x12UnsealSecretOutput\x12\x1c\n\tp\ @@ -766,12 +1100,20 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \n\x1fKeyProviderKeyWrapProtocolInput\x12H\n\x1fKeyProviderKeyWrapProtoc\ olInput\x18\x01\x20\x01(\x0cR\x1fKeyProviderKeyWrapProtocolInput\"n\n\ \x20KeyProviderKeyWrapProtocolOutput\x12J\n\x20KeyProviderKeyWrapProtoco\ - lOutput\x18\x01\x20\x01(\x0cR\x20KeyProviderKeyWrapProtocolOutput2V\n\ - \x13SealedSecretService\x12?\n\x0cUnsealSecret\x12\x16.api.UnsealSecretI\ - nput\x1a\x17.api.UnsealSecretOutput2V\n\x12GetResourceService\x12@\n\x0b\ - GetResource\x12\x17.api.GetResourceRequest\x1a\x18.api.GetResourceRespon\ - se2n\n\x12KeyProviderService\x12X\n\tUnWrapKey\x12$.api.KeyProviderKeyWr\ - apProtocolInput\x1a%.api.KeyProviderKeyWrapProtocolOutputb\x06proto3\ + lOutput\x18\x01\x20\x01(\x0cR\x20KeyProviderKeyWrapProtocolOutput\"\xbe\ + \x01\n\x12SecureMountRequest\x12\x16\n\x06driver\x18\x01\x20\x01(\tR\x06\ + driver\x12%\n\x0edriver_options\x18\x02\x20\x03(\tR\rdriverOptions\x12\ + \x16\n\x06source\x18\x03\x20\x01(\tR\x06source\x12\x16\n\x06fstype\x18\ + \x04\x20\x01(\tR\x06fstype\x12\x18\n\x07options\x18\x05\x20\x03(\tR\x07o\ + ptions\x12\x1f\n\x0bmount_point\x18\x06\x20\x01(\tR\nmountPoint\"3\n\x13\ + SecureMountResponse\x12\x1c\n\tmountPath\x18\x01\x20\x01(\tR\tmountPath2\ + V\n\x13SealedSecretService\x12?\n\x0cUnsealSecret\x12\x16.api.UnsealSecr\ + etInput\x1a\x17.api.UnsealSecretOutput2V\n\x12GetResourceService\x12@\n\ + \x0bGetResource\x12\x17.api.GetResourceRequest\x1a\x18.api.GetResourceRe\ + sponse2n\n\x12KeyProviderService\x12X\n\tUnWrapKey\x12$.api.KeyProviderK\ + eyWrapProtocolInput\x1a%.api.KeyProviderKeyWrapProtocolOutput2V\n\x12Sec\ + ureMountService\x12@\n\x0bSecureMount\x12\x17.api.SecureMountRequest\x1a\ + \x18.api.SecureMountResponseb\x06proto3\ "; /// `FileDescriptorProto` object which was a source for this generated file @@ -789,13 +1131,15 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { file_descriptor.get(|| { let generated_file_descriptor = generated_file_descriptor_lazy.get(|| { let mut deps = ::std::vec::Vec::with_capacity(0); - let mut messages = ::std::vec::Vec::with_capacity(6); + let mut messages = ::std::vec::Vec::with_capacity(8); messages.push(UnsealSecretInput::generated_message_descriptor_data()); messages.push(UnsealSecretOutput::generated_message_descriptor_data()); messages.push(GetResourceRequest::generated_message_descriptor_data()); messages.push(GetResourceResponse::generated_message_descriptor_data()); messages.push(KeyProviderKeyWrapProtocolInput::generated_message_descriptor_data()); messages.push(KeyProviderKeyWrapProtocolOutput::generated_message_descriptor_data()); + messages.push(SecureMountRequest::generated_message_descriptor_data()); + messages.push(SecureMountResponse::generated_message_descriptor_data()); let mut enums = ::std::vec::Vec::with_capacity(0); ::protobuf::reflect::GeneratedFileDescriptor::new_generated( file_descriptor_proto(), diff --git a/confidential-data-hub/hub/src/bin/confidential-data-hub/api_ttrpc.rs b/confidential-data-hub/hub/src/bin/confidential-data-hub/api_ttrpc.rs index a86192fa2..47b16b330 100644 --- a/confidential-data-hub/hub/src/bin/confidential-data-hub/api_ttrpc.rs +++ b/confidential-data-hub/hub/src/bin/confidential-data-hub/api_ttrpc.rs @@ -165,3 +165,51 @@ pub fn create_key_provider_service(service: Arc Self { + SecureMountServiceClient { + client, + } + } + + pub async fn secure_mount(&self, ctx: ttrpc::context::Context, req: &super::api::SecureMountRequest) -> ::ttrpc::Result { + let mut cres = super::api::SecureMountResponse::new(); + ::ttrpc::async_client_request!(self, ctx, req, "api.SecureMountService", "SecureMount", cres); + } +} + +struct SecureMountMethod { + service: Arc>, +} + +#[async_trait] +impl ::ttrpc::r#async::MethodHandler for SecureMountMethod { + async fn handler(&self, ctx: ::ttrpc::r#async::TtrpcContext, req: ::ttrpc::Request) -> ::ttrpc::Result<::ttrpc::Response> { + ::ttrpc::async_request_handler!(self, ctx, req, api, SecureMountRequest, secure_mount); + } +} + +#[async_trait] +pub trait SecureMountService: Sync { + async fn secure_mount(&self, _ctx: &::ttrpc::r#async::TtrpcContext, _: super::api::SecureMountRequest) -> ::ttrpc::Result { + Err(::ttrpc::Error::RpcStatus(::ttrpc::get_status(::ttrpc::Code::NOT_FOUND, "/api.SecureMountService/SecureMount is not supported".to_string()))) + } +} + +pub fn create_secure_mount_service(service: Arc>) -> HashMap { + let mut ret = HashMap::new(); + let mut methods = HashMap::new(); + let streams = HashMap::new(); + + methods.insert("SecureMount".to_string(), + Box::new(SecureMountMethod{service: service.clone()}) as Box); + + ret.insert("api.SecureMountService".to_string(), ::ttrpc::r#async::Service{ methods, streams }); + ret +} diff --git a/confidential-data-hub/hub/src/bin/confidential-data-hub/main.rs b/confidential-data-hub/hub/src/bin/confidential-data-hub/main.rs index 2737c9da3..6455b98b1 100644 --- a/confidential-data-hub/hub/src/bin/confidential-data-hub/main.rs +++ b/confidential-data-hub/hub/src/bin/confidential-data-hub/main.rs @@ -8,6 +8,7 @@ use std::{path::Path, sync::Arc}; use anyhow::{Context, Result}; use api_ttrpc::{ create_get_resource_service, create_key_provider_service, create_sealed_secret_service, + create_secure_mount_service, }; use clap::Parser; use log::info; @@ -58,11 +59,13 @@ async fn main() -> Result<()> { let sealed_secret_service = ttrpc_service!(create_sealed_secret_service); let get_resource_service = ttrpc_service!(create_get_resource_service); let key_provider_service = ttrpc_service!(create_key_provider_service); + let secure_mount_service = ttrpc_service!(create_secure_mount_service); let mut server = TtrpcServer::new() .bind(&cli.socket) .context("cannot bind cdh ttrpc service")? .register_service(sealed_secret_service) .register_service(get_resource_service) + .register_service(secure_mount_service) .register_service(key_provider_service); server.start().await?; diff --git a/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs b/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs index 4565ca9de..32115e423 100644 --- a/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs +++ b/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs @@ -10,15 +10,17 @@ use async_trait::async_trait; use confidential_data_hub::{hub::Hub, DataHub}; use lazy_static::lazy_static; use log::debug; +use storage::volume_type::Storage; use tokio::sync::RwLock; use ttrpc::{asynchronous::TtrpcContext, Code, Error, Status}; use crate::{ api::{ GetResourceRequest, GetResourceResponse, KeyProviderKeyWrapProtocolInput, - KeyProviderKeyWrapProtocolOutput, UnsealSecretInput, UnsealSecretOutput, + KeyProviderKeyWrapProtocolOutput, SecureMountRequest, SecureMountResponse, + UnsealSecretInput, UnsealSecretOutput, }, - api_ttrpc::{GetResourceService, KeyProviderService, SealedSecretService}, + api_ttrpc::{GetResourceService, KeyProviderService, SealedSecretService, SecureMountService}, server::message::{KeyProviderInput, KeyUnwrapOutput, KeyUnwrapResults}, }; @@ -151,3 +153,35 @@ impl KeyProviderService for Server { Ok(reply) } } + +#[async_trait] +impl SecureMountService for Server { + async fn secure_mount( + &self, + _ctx: &TtrpcContext, + req: SecureMountRequest, + ) -> ::ttrpc::Result { + debug!("get new Secure mount request"); + let reader = HUB.read().await; + let reader = reader.as_ref().expect("must be initialized"); + let storage = Storage { + driver: req.driver, + driver_options: req.driver_options, + source: req.source, + fstype: req.fstype, + options: req.options, + mount_point: req.mount_point, + }; + let resource = reader.secure_mount(storage).await.map_err(|e| { + let mut status = Status::new(); + status.set_code(Code::INTERNAL); + status.set_message(format!("[CDH] [ERROR]: secure mount failed: {e}")); + Error::RpcStatus(status) + })?; + + let mut reply = SecureMountResponse::new(); + reply.mountPath = resource; + debug!("send back the resource"); + Ok(reply) + } +} diff --git a/confidential-data-hub/hub/src/error.rs b/confidential-data-hub/hub/src/error.rs index 15169480d..07abc2bf2 100644 --- a/confidential-data-hub/hub/src/error.rs +++ b/confidential-data-hub/hub/src/error.rs @@ -20,4 +20,7 @@ pub enum Error { #[error("unseal secret failed: {0}")] UnsealSecret(String), + + #[error("secure mount failed: {0}")] + SecureMount(String), } diff --git a/confidential-data-hub/hub/src/hub.rs b/confidential-data-hub/hub/src/hub.rs index 704fc75fb..c0feee146 100644 --- a/confidential-data-hub/hub/src/hub.rs +++ b/confidential-data-hub/hub/src/hub.rs @@ -8,6 +8,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use image::AnnotationPacket; use kms::{Annotations, ProviderSettings}; use secret::secret::Secret; +use storage::volume_type::Storage; use crate::{DataHub, Error, Result}; @@ -73,4 +74,12 @@ impl DataHub for Hub { .map_err(|e| Error::GetResource(format!("get rersource failed: {e}")))?; Ok(res) } + + async fn secure_mount(&self, storage: Storage) -> Result { + let res = storage + .mount() + .await + .map_err(|e| Error::SecureMount(format!("mount failed: {e}")))?; + Ok(res) + } } From 3e6bb40874b08e28fc9322168c10cae31358998b Mon Sep 17 00:00:00 2001 From: Linda Yu Date: Fri, 15 Sep 2023 14:36:56 +0800 Subject: [PATCH 2/5] cdh/storage: mount encrypted alibaba cloud oss Signed-off-by: Linda Yu --- Cargo.lock | 16 ++ confidential-data-hub/hub/src/hub.rs | 2 +- confidential-data-hub/storage/Cargo.toml | 27 +++ confidential-data-hub/storage/src/error.rs | 14 ++ confidential-data-hub/storage/src/lib.rs | 9 + .../src/volume_type/alibaba_cloud_oss/mod.rs | 6 + .../src/volume_type/alibaba_cloud_oss/oss.rs | 171 ++++++++++++++++++ .../storage/src/volume_type/mod.rs | 55 ++++++ 8 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 confidential-data-hub/storage/Cargo.toml create mode 100644 confidential-data-hub/storage/src/error.rs create mode 100644 confidential-data-hub/storage/src/lib.rs create mode 100644 confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/mod.rs create mode 100644 confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs create mode 100644 confidential-data-hub/storage/src/volume_type/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e78ea89b0..a57c5c9d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -888,6 +888,7 @@ dependencies = [ "secret", "serde_json", "sev 0.1.0", + "storage", "thiserror", "tokio", "ttrpc", @@ -5463,6 +5464,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "storage" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.4", + "log", + "rstest", + "secret", + "serde", + "serde_json", + "thiserror", + "tokio", +] + [[package]] name = "string_cache" version = "0.8.7" diff --git a/confidential-data-hub/hub/src/hub.rs b/confidential-data-hub/hub/src/hub.rs index c0feee146..a6e95f2d1 100644 --- a/confidential-data-hub/hub/src/hub.rs +++ b/confidential-data-hub/hub/src/hub.rs @@ -79,7 +79,7 @@ impl DataHub for Hub { let res = storage .mount() .await - .map_err(|e| Error::SecureMount(format!("mount failed: {e}")))?; + .map_err(|e| Error::SecureMount(e.to_string()))?; Ok(res) } } diff --git a/confidential-data-hub/storage/Cargo.toml b/confidential-data-hub/storage/Cargo.toml new file mode 100644 index 000000000..fdcd5b685 --- /dev/null +++ b/confidential-data-hub/storage/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "storage" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = "1" +serde_json = "1" +thiserror.workspace = true +tokio = { workspace = true, features = ["fs"] } +anyhow.workspace = true +secret = { path = "../secret" } +base64.workspace = true +log.workspace = true + +[dev-dependencies] +rstest.workspace = true +tokio = { workspace = true, features = ["rt", "macros" ] } + +[build-dependencies] +anyhow.workspace = true + +[features] +default = ["aliyun"] +aliyun = [] diff --git a/confidential-data-hub/storage/src/error.rs b/confidential-data-hub/storage/src/error.rs new file mode 100644 index 000000000..80dd80c38 --- /dev/null +++ b/confidential-data-hub/storage/src/error.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2023 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum Error { + #[error("secure mount failed: {0}")] + SecureMountFailed(String), +} diff --git a/confidential-data-hub/storage/src/lib.rs b/confidential-data-hub/storage/src/lib.rs new file mode 100644 index 000000000..391985321 --- /dev/null +++ b/confidential-data-hub/storage/src/lib.rs @@ -0,0 +1,9 @@ +// Copyright (c) 2023 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// + +pub mod error; +pub mod volume_type; + +pub use error::*; diff --git a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/mod.rs b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/mod.rs new file mode 100644 index 000000000..aa50157f8 --- /dev/null +++ b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) 2023 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// + +pub mod oss; diff --git a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs new file mode 100644 index 000000000..ad5742775 --- /dev/null +++ b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs @@ -0,0 +1,171 @@ +// Copyright (c) 2023 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// + +use base64::{engine::general_purpose::STANDARD, Engine}; +use secret::secret::Secret; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::Write; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +use crate::{Error, Result}; + +const OSSFS_PASSWD_FILE: &str = "/tmp/ossfs_passwd"; +const GOCRYPTFS_PASSWD_FILE: &str = "/tmp/gocryptfs_passwd"; +const OSSFS_BIN: &str = "/usr/local/bin/ossfs"; +const GOCRYPTFS_BIN: &str = "/usr/local/bin/gocryptfs"; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Oss { + #[serde(rename = "akId")] + pub ak_id: String, + #[serde(rename = "akSecret")] + pub ak_secret: String, + #[serde(default)] + pub annotations: String, + pub bucket: String, + #[serde(default)] + pub encrypted: String, + #[serde(rename = "encPasswd", default)] + pub enc_passwd: String, + #[serde(rename = "kmsKeyId", default)] + pub kms_key_id: String, + #[serde(rename = "otherOpts")] + pub other_opts: String, + pub path: String, + pub readonly: String, + #[serde(rename = "targetPath")] + pub target_path: String, + pub url: String, + #[serde(rename = "volumeId")] + pub volume_id: String, +} + +async fn unseal_secret(secret: Vec) -> Result> { + // TODO: verify the jws signature using the key specified by `kid` + // in header. Here we directly get the JWS payload + let payload = secret.split(|c| *c == b'.').nth(1).ok_or_else(|| { + Error::SecureMountFailed("illegal input sealed secret (not a JWS)".into()) + })?; + + let secret_json = STANDARD.decode(payload).map_err(|e| { + Error::SecureMountFailed(format!( + "illegal input sealed secret (JWS body is not standard base64 encoded): {e}" + )) + })?; + let secret: Secret = serde_json::from_slice(&secret_json).map_err(|e| { + Error::SecureMountFailed(format!( + "illegal input sealed secret format (json deseralization failed): {e}" + )) + })?; + + let res = secret + .unseal() + .await + .map_err(|e| Error::SecureMountFailed(format!("unseal failed: {e}")))?; + Ok(res) +} + +async fn get_plain(secret: &str) -> Result { + if secret.starts_with("sealed.") { + let tmp = secret + .strip_prefix("sealed.") + .ok_or(Error::SecureMountFailed( + "strip_prefix \"sealed.\" failed".to_string(), + ))?; + let unsealed = unseal_secret(tmp.into()) + .await + .map_err(|e| Error::SecureMountFailed(format!("unseal secret failed: {e}")))?; + + return String::from_utf8(unsealed) + .map_err(|e| Error::SecureMountFailed(format!("convert to String failed: {e}"))); + } + Err(Error::SecureMountFailed( + "sealed secret format error!".to_string(), + )) +} + +impl Oss { + pub(crate) async fn mount(&self, source: String, mount_point: String) -> Result { + // unseal secret + let plain_ak_id = get_plain(&self.ak_id) + .await + .map_err(|e| Error::SecureMountFailed(format!("get_plain failed: {e}")))?; + let plain_ak_secret = get_plain(&self.ak_secret) + .await + .map_err(|e| Error::SecureMountFailed(format!("get_plain failed: {e}")))?; + + // create ossfs passwd file + let mut ossfs_passwd = File::create(OSSFS_PASSWD_FILE) + .map_err(|e| Error::SecureMountFailed(format!("create file failed: {e}")))?; + let metadata = ossfs_passwd + .metadata() + .map_err(|e| Error::SecureMountFailed(format!("create metadata failed: {e}")))?; + let mut permissions = metadata.permissions(); + permissions.set_mode(0o600); + ossfs_passwd + .set_permissions(permissions) + .map_err(|e| Error::SecureMountFailed(format!("set permissions failed: {e}")))?; + ossfs_passwd + .write_all(format!("{}:{}:{}", self.bucket, plain_ak_id, plain_ak_secret).as_bytes()) + .map_err(|e| Error::SecureMountFailed(format!("write file failed: {e}")))?; + + // generate parameters for ossfs command, and execute + let mut opts = self + .other_opts + .split_whitespace() + .map(str::to_string) + .collect(); + let s = if self.encrypted == "gocryptfs" { + "/tmp/oss/".to_string() + } else { + source.clone() + }; + let mut parameters = vec![ + format!("{}:{}", self.bucket, self.path), + s.clone(), + format!("-ourl={}", self.url), + format!("-opasswd_file={}", OSSFS_PASSWD_FILE), + ]; + parameters.append(&mut opts); + + Command::new(OSSFS_BIN) + .args(parameters) + .spawn() + .expect("failed to mount oss"); + std::thread::sleep(std::time::Duration::from_secs(3)); + + // decrypt with gocryptfs if needed + if self.encrypted == "gocryptfs" { + // unseal secret + let plain_passwd = get_plain(&self.enc_passwd) + .await + .map_err(|e| Error::SecureMountFailed(format!("get_plain failed: {e}")))?; + + // create gocryptfs passwd file + let mut gocryptfs_passwd = File::create(GOCRYPTFS_PASSWD_FILE) + .map_err(|e| Error::SecureMountFailed(format!("create file failed: {e}")))?; + gocryptfs_passwd + .write_all(plain_passwd.as_bytes()) + .map_err(|e| Error::SecureMountFailed(format!("write file failed: {e}")))?; + + // generate parameters for gocryptfs, and execute + let parameters = vec![ + s, + source, + "-passfile".to_string(), + GOCRYPTFS_PASSWD_FILE.to_string(), + "-nosyslog".to_string(), + ]; + Command::new(GOCRYPTFS_BIN) + .args(parameters) + .spawn() + .expect("failed to decrypt oss"); + std::thread::sleep(std::time::Duration::from_secs(3)); + } + Ok(mount_point) + } +} diff --git a/confidential-data-hub/storage/src/volume_type/mod.rs b/confidential-data-hub/storage/src/volume_type/mod.rs new file mode 100644 index 000000000..683aaa64f --- /dev/null +++ b/confidential-data-hub/storage/src/volume_type/mod.rs @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[cfg(feature = "aliyun")] +pub mod alibaba_cloud_oss; + +#[cfg(feature = "aliyun")] +use self::alibaba_cloud_oss::oss::Oss; +use crate::{Error, Result}; +use log::warn; + +#[derive(PartialEq, Clone, Debug)] +pub struct Storage { + pub driver: String, + pub driver_options: Vec, + pub source: String, + pub fstype: String, + pub options: Vec, + pub mount_point: String, +} + +impl Storage { + pub async fn mount(&self) -> Result { + for driver_option in &self.driver_options { + let (volumetype, metadata) = + driver_option + .split_once('=') + .ok_or(Error::SecureMountFailed( + "split by \"=\" failed".to_string(), + ))?; + + match volumetype { + #[cfg(feature = "aliyun")] + "alibaba-cloud-oss" => { + let oss: Oss = serde_json::from_str(metadata).map_err(|e| { + Error::SecureMountFailed(format!( + "illegal mount info format (json deseralization failed): {e}" + )) + })?; + return oss + .mount(self.source.clone(), self.mount_point.clone()) + .await; + } + other => { + warn!("skip mount info with unsupported volumetype: {other}"); + } + }; + } + Err(Error::SecureMountFailed( + "illegal mount info as no expected driver_options".to_string(), + )) + } +} From e64697c00e962293fe2718a0f6069740d50971df Mon Sep 17 00:00:00 2001 From: Linda Yu Date: Wed, 27 Sep 2023 11:01:11 +0800 Subject: [PATCH 3/5] cdh/storage: refine error message Signed-off-by: Linda Yu --- confidential-data-hub/hub/protos/api.proto | 2 +- .../hub/src/bin/confidential-data-hub/api.rs | 32 +++++++++---------- .../bin/confidential-data-hub/server/mod.rs | 2 +- confidential-data-hub/storage/src/error.rs | 6 ++++ .../src/volume_type/alibaba_cloud_oss/oss.rs | 32 +++++++------------ .../storage/src/volume_type/mod.rs | 6 ++-- 6 files changed, 39 insertions(+), 41 deletions(-) diff --git a/confidential-data-hub/hub/protos/api.proto b/confidential-data-hub/hub/protos/api.proto index ee3a247be..8a8ddaf26 100644 --- a/confidential-data-hub/hub/protos/api.proto +++ b/confidential-data-hub/hub/protos/api.proto @@ -36,7 +36,7 @@ message SecureMountRequest { } message SecureMountResponse { - string mountPath = 1; + string mount_path = 1; } service SealedSecretService { diff --git a/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs b/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs index 347803681..302ac804e 100644 --- a/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs +++ b/confidential-data-hub/hub/src/bin/confidential-data-hub/api.rs @@ -973,8 +973,8 @@ impl ::protobuf::reflect::ProtobufValue for SecureMountRequest { // @@protoc_insertion_point(message:api.SecureMountResponse) pub struct SecureMountResponse { // message fields - // @@protoc_insertion_point(field:api.SecureMountResponse.mountPath) - pub mountPath: ::std::string::String, + // @@protoc_insertion_point(field:api.SecureMountResponse.mount_path) + pub mount_path: ::std::string::String, // special fields // @@protoc_insertion_point(special_field:api.SecureMountResponse.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -995,9 +995,9 @@ impl SecureMountResponse { let mut fields = ::std::vec::Vec::with_capacity(1); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "mountPath", - |m: &SecureMountResponse| { &m.mountPath }, - |m: &mut SecureMountResponse| { &mut m.mountPath }, + "mount_path", + |m: &SecureMountResponse| { &m.mount_path }, + |m: &mut SecureMountResponse| { &mut m.mount_path }, )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "SecureMountResponse", @@ -1018,7 +1018,7 @@ impl ::protobuf::Message for SecureMountResponse { while let Some(tag) = is.read_raw_tag_or_eof()? { match tag { 10 => { - self.mountPath = is.read_string()?; + self.mount_path = is.read_string()?; }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; @@ -1032,8 +1032,8 @@ impl ::protobuf::Message for SecureMountResponse { #[allow(unused_variables)] fn compute_size(&self) -> u64 { let mut my_size = 0; - if !self.mountPath.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.mountPath); + if !self.mount_path.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.mount_path); } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); @@ -1041,8 +1041,8 @@ impl ::protobuf::Message for SecureMountResponse { } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.mountPath.is_empty() { - os.write_string(1, &self.mountPath)?; + if !self.mount_path.is_empty() { + os.write_string(1, &self.mount_path)?; } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) @@ -1061,13 +1061,13 @@ impl ::protobuf::Message for SecureMountResponse { } fn clear(&mut self) { - self.mountPath.clear(); + self.mount_path.clear(); self.special_fields.clear(); } fn default_instance() -> &'static SecureMountResponse { static instance: SecureMountResponse = SecureMountResponse { - mountPath: ::std::string::String::new(), + mount_path: ::std::string::String::new(), special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -1105,10 +1105,10 @@ static file_descriptor_proto_data: &'static [u8] = b"\ driver\x12%\n\x0edriver_options\x18\x02\x20\x03(\tR\rdriverOptions\x12\ \x16\n\x06source\x18\x03\x20\x01(\tR\x06source\x12\x16\n\x06fstype\x18\ \x04\x20\x01(\tR\x06fstype\x12\x18\n\x07options\x18\x05\x20\x03(\tR\x07o\ - ptions\x12\x1f\n\x0bmount_point\x18\x06\x20\x01(\tR\nmountPoint\"3\n\x13\ - SecureMountResponse\x12\x1c\n\tmountPath\x18\x01\x20\x01(\tR\tmountPath2\ - V\n\x13SealedSecretService\x12?\n\x0cUnsealSecret\x12\x16.api.UnsealSecr\ - etInput\x1a\x17.api.UnsealSecretOutput2V\n\x12GetResourceService\x12@\n\ + ptions\x12\x1f\n\x0bmount_point\x18\x06\x20\x01(\tR\nmountPoint\"4\n\x13\ + SecureMountResponse\x12\x1d\n\nmount_path\x18\x01\x20\x01(\tR\tmountPath\ + 2V\n\x13SealedSecretService\x12?\n\x0cUnsealSecret\x12\x16.api.UnsealSec\ + retInput\x1a\x17.api.UnsealSecretOutput2V\n\x12GetResourceService\x12@\n\ \x0bGetResource\x12\x17.api.GetResourceRequest\x1a\x18.api.GetResourceRe\ sponse2n\n\x12KeyProviderService\x12X\n\tUnWrapKey\x12$.api.KeyProviderK\ eyWrapProtocolInput\x1a%.api.KeyProviderKeyWrapProtocolOutput2V\n\x12Sec\ diff --git a/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs b/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs index 32115e423..501f0850f 100644 --- a/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs +++ b/confidential-data-hub/hub/src/bin/confidential-data-hub/server/mod.rs @@ -180,7 +180,7 @@ impl SecureMountService for Server { })?; let mut reply = SecureMountResponse::new(); - reply.mountPath = resource; + reply.mount_path = resource; debug!("send back the resource"); Ok(reply) } diff --git a/confidential-data-hub/storage/src/error.rs b/confidential-data-hub/storage/src/error.rs index 80dd80c38..ed29ab401 100644 --- a/confidential-data-hub/storage/src/error.rs +++ b/confidential-data-hub/storage/src/error.rs @@ -11,4 +11,10 @@ pub type Result = std::result::Result; pub enum Error { #[error("secure mount failed: {0}")] SecureMountFailed(String), + + #[error("file error: {0}")] + FileError(String), + + #[error("unseal secret failed: {0}")] + UnsealSecretFailed(String), } diff --git a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs index ad5742775..2d9b17b94 100644 --- a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs +++ b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs @@ -65,20 +65,18 @@ async fn unseal_secret(secret: Vec) -> Result> { let res = secret .unseal() .await - .map_err(|e| Error::SecureMountFailed(format!("unseal failed: {e}")))?; + .map_err(|e| Error::UnsealSecretFailed(format!("unseal failed: {e}")))?; Ok(res) } -async fn get_plain(secret: &str) -> Result { +async fn get_plaintext_secret(secret: &str) -> Result { if secret.starts_with("sealed.") { let tmp = secret .strip_prefix("sealed.") .ok_or(Error::SecureMountFailed( "strip_prefix \"sealed.\" failed".to_string(), ))?; - let unsealed = unseal_secret(tmp.into()) - .await - .map_err(|e| Error::SecureMountFailed(format!("unseal secret failed: {e}")))?; + let unsealed = unseal_secret(tmp.into()).await?; return String::from_utf8(unsealed) .map_err(|e| Error::SecureMountFailed(format!("convert to String failed: {e}"))); @@ -91,27 +89,23 @@ async fn get_plain(secret: &str) -> Result { impl Oss { pub(crate) async fn mount(&self, source: String, mount_point: String) -> Result { // unseal secret - let plain_ak_id = get_plain(&self.ak_id) - .await - .map_err(|e| Error::SecureMountFailed(format!("get_plain failed: {e}")))?; - let plain_ak_secret = get_plain(&self.ak_secret) - .await - .map_err(|e| Error::SecureMountFailed(format!("get_plain failed: {e}")))?; + let plain_ak_id = get_plaintext_secret(&self.ak_id).await?; + let plain_ak_secret = get_plaintext_secret(&self.ak_secret).await?; // create ossfs passwd file let mut ossfs_passwd = File::create(OSSFS_PASSWD_FILE) - .map_err(|e| Error::SecureMountFailed(format!("create file failed: {e}")))?; + .map_err(|e| Error::FileError(format!("create file failed: {e}")))?; let metadata = ossfs_passwd .metadata() - .map_err(|e| Error::SecureMountFailed(format!("create metadata failed: {e}")))?; + .map_err(|e| Error::FileError(format!("create metadata failed: {e}")))?; let mut permissions = metadata.permissions(); permissions.set_mode(0o600); ossfs_passwd .set_permissions(permissions) - .map_err(|e| Error::SecureMountFailed(format!("set permissions failed: {e}")))?; + .map_err(|e| Error::FileError(format!("set permissions failed: {e}")))?; ossfs_passwd .write_all(format!("{}:{}:{}", self.bucket, plain_ak_id, plain_ak_secret).as_bytes()) - .map_err(|e| Error::SecureMountFailed(format!("write file failed: {e}")))?; + .map_err(|e| Error::FileError(format!("write file failed: {e}")))?; // generate parameters for ossfs command, and execute let mut opts = self @@ -141,16 +135,14 @@ impl Oss { // decrypt with gocryptfs if needed if self.encrypted == "gocryptfs" { // unseal secret - let plain_passwd = get_plain(&self.enc_passwd) - .await - .map_err(|e| Error::SecureMountFailed(format!("get_plain failed: {e}")))?; + let plain_passwd = get_plaintext_secret(&self.enc_passwd).await?; // create gocryptfs passwd file let mut gocryptfs_passwd = File::create(GOCRYPTFS_PASSWD_FILE) - .map_err(|e| Error::SecureMountFailed(format!("create file failed: {e}")))?; + .map_err(|e| Error::FileError(format!("create file failed: {e}")))?; gocryptfs_passwd .write_all(plain_passwd.as_bytes()) - .map_err(|e| Error::SecureMountFailed(format!("write file failed: {e}")))?; + .map_err(|e| Error::FileError(format!("write file failed: {e}")))?; // generate parameters for gocryptfs, and execute let parameters = vec![ diff --git a/confidential-data-hub/storage/src/volume_type/mod.rs b/confidential-data-hub/storage/src/volume_type/mod.rs index 683aaa64f..b0daa76cf 100644 --- a/confidential-data-hub/storage/src/volume_type/mod.rs +++ b/confidential-data-hub/storage/src/volume_type/mod.rs @@ -24,14 +24,14 @@ pub struct Storage { impl Storage { pub async fn mount(&self) -> Result { for driver_option in &self.driver_options { - let (volumetype, metadata) = + let (volume_type, metadata) = driver_option .split_once('=') .ok_or(Error::SecureMountFailed( "split by \"=\" failed".to_string(), ))?; - match volumetype { + match volume_type { #[cfg(feature = "aliyun")] "alibaba-cloud-oss" => { let oss: Oss = serde_json::from_str(metadata).map_err(|e| { @@ -44,7 +44,7 @@ impl Storage { .await; } other => { - warn!("skip mount info with unsupported volumetype: {other}"); + warn!("skip mount info with unsupported volume_type: {other}"); } }; } From e51f244bbf69d64a89df93f91b0b9ce277e13884 Mon Sep 17 00:00:00 2001 From: Linda Yu Date: Wed, 25 Oct 2023 21:31:04 +0800 Subject: [PATCH 4/5] cdh/storage: create tmp dir for oss Signed-off-by: Linda Yu --- .../storage/src/volume_type/alibaba_cloud_oss/oss.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs index 2d9b17b94..42cf891d9 100644 --- a/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs +++ b/confidential-data-hub/storage/src/volume_type/alibaba_cloud_oss/oss.rs @@ -6,6 +6,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use secret::secret::Secret; use serde::{Deserialize, Serialize}; +use std::fs; use std::fs::File; use std::io::Write; use std::os::unix::fs::PermissionsExt; @@ -114,6 +115,8 @@ impl Oss { .map(str::to_string) .collect(); let s = if self.encrypted == "gocryptfs" { + fs::create_dir_all("/tmp/oss") + .map_err(|e| Error::FileError(format!("create dir failed: {e}")))?; "/tmp/oss/".to_string() } else { source.clone() From 3c47f3f827ed9f564d5e103780f7beb4d116253d Mon Sep 17 00:00:00 2001 From: Linda Yu Date: Mon, 6 Nov 2023 15:39:24 +0800 Subject: [PATCH 5/5] cdh/storage: add docs for secure storage Signed-off-by: Linda Yu --- confidential-data-hub/docs/SECURE_STORAGE.md | 13 +++++++++++++ .../docs/images/secure_storage.png | Bin 0 -> 77666 bytes 2 files changed, 13 insertions(+) create mode 100644 confidential-data-hub/docs/SECURE_STORAGE.md create mode 100644 confidential-data-hub/docs/images/secure_storage.png diff --git a/confidential-data-hub/docs/SECURE_STORAGE.md b/confidential-data-hub/docs/SECURE_STORAGE.md new file mode 100644 index 000000000..d93bec0c4 --- /dev/null +++ b/confidential-data-hub/docs/SECURE_STORAGE.md @@ -0,0 +1,13 @@ +# Secure Storage + +## Purpose +The Purpose of this secure storage feature is: +1. Mounting external storage from guest instead of host which would then share it to guest, this is due to performance consideration. +2. The unencrypted data in storage could only be accessed within TEE, that is why we call it secure storage. + +## Architecture +![architecture](./images/secure_storage.png) + +First of all, the sensitive information of external storage is sealed by the key from KBS/KMS, and store in [sealed secret](https://github.com/confidential-containers/guest-components/blob/main/confidential-data-hub/docs/SEALED_SECRET.md). The sensitive information includes access key id/access key secret to storage, the encryption key of the data(such as AI model) stored in the storage, which also means we supported client encryption. +We reuse [direct block device assigned volume feature](https://github.com/kata-containers/kata-containers/blob/main/docs/design/direct-blk-device-assignment.md) to mount external storage from guest directly. CSI plugin, such as [alibaba cloud OSS CSI plugin](https://github.com/kubernetes-sigs/alibaba-cloud-csi-driver/blob/master/docs/oss.md) reads the sensitve information from sealed secret and pass it to kata agent. When secure mount service in CDH receives secure mount request, it calls sealed secret service to unseal the sensitive information mentioned above, this process could be based on remote attestation. If success, the secure mount service would use the unsealed sensitive information to mount the external storage and decrypt the data in storage. + diff --git a/confidential-data-hub/docs/images/secure_storage.png b/confidential-data-hub/docs/images/secure_storage.png new file mode 100644 index 0000000000000000000000000000000000000000..225d946e908cf717e66563963050edfea01042ef GIT binary patch literal 77666 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 literal 0 HcmV?d00001