-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend KBS to provide the resources required to create an encrypted overlay network #451
Conversation
Once confidential-containers/guest-components#634 is merged, I can update this PR and remove its draft label. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. A few preliminary comments. I haven't gotten into the meat of the nebula.rs
yet
Move the kbs-config.toml to its own directory. Other kbs related config files can then be stored under the same directory. Signed-off-by: Claudio Carvalho <cclaudio@linux.ibm.com>
Currently, a resource is requested to the KBS through the resource URI, which takes the resource description in the format '/<repository>/<type>/<tag>'. This patch re-purposes the repository='plugin' to allow getting resources through plugins. A plugin can take additional parameters via a URL query string and can also do some processing before returning the resource. When the repository='plugin', the get-resource() interface will then call the plugin manager to handle the request, where: - the type provided is the name of the plugin to be invoked - the tag provided is the name of the resource requested - additional parameters can be provided through query string The example below shows a plugin resource request for the nebula plugin where the resource name is credential. Additional parameters are provided in the query string. GET /kbs/v0/resource/plugin/nebula/credential?ip[ip]=10.11.12.13&ip[netbits]=21&name=pod1 set-resource() is not supported when repository='plugin'. Plugins are required to implement two traits, both defined by the plugin manager: - PluginBuild: functions required to create/initialize a plugin if it is included in the 'plugin_manager_config.enabled_plugins' in the kbs-config.toml. - Plugin: functions required to invoke the plugin that will handle a get-resource request received. Signed-off-by: Claudio Carvalho <cclaudio@linux.ibm.com>
…orage The current resource storage path /opt/confidential-containers/kbs/repository is misleading because repository is actually part of the resource description (repository/type/tag). Signed-off-by: Claudio Carvalho <cclaudio@linux.ibm.com>
The nebula plugin can be used to deliver credentials for nodes (confidential PODs or VMs) to join a Nebula overlay network. Within the nebula network, the communication between nodes is automatically encrypted by Nebula. A nebula credential can be requested using the kbs-client: kbs-client --url http://127.0.0.1:8080 \ get-resource \ --path 'plugin/nebula/credential?ip[ip]=10.11.12.13&ip[netbits]=21&name=pod1' at least the IPv4 address (in CIDR notation) and the name of the node must be provided in the query string. The other parameters supported can be found in the struct NebulaCredentialParams. After receiving a credential request, the nebula plugin will call the nebula-cert binary to create a key pair and sign a certificate using the Nebula CA. The generated node.crt and node.key, as well as the ca.rt are then returned to the caller. During the nebula-plugin initialization, a self signed Nebula CA can be created if 'ca_generation_policy = 1' in the nebula-config.toml, the file contains all parameters supported. Another option is to pre-install a ca.key and ca.crt, and set 'ca_generation_policy = 2'. The nebula-plugin cargo feature is set by default, however the plugin itself is not initialized by default. In order to initialize it, you need to add 'nebula' to 'manager_plugin_config.enabled_plugins' in the kbs-config.toml. Closes confidential-containers#396 Signed-off-by: Claudio Carvalho <cclaudio@linux.ibm.com>
49865eb
to
baf0bd6
Compare
Feedback applied, thanks @fitzthum I also updated the guest-components revision in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments
#[cfg(feature = "nebula-plugin")] | ||
use crate::resource::plugin::nebula::NebulaPluginConfig; | ||
|
||
trait PluginBuild { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe PluginBuilder
would be better
.with_context(|| "Create resource plugin dir".to_string())?; | ||
} | ||
|
||
#[allow(unused_mut)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need this? Seems like you modify the manager below. Btw you might also think about creating the vector first and then making the manager from it at the end.
plugin_name, | ||
))?; | ||
|
||
let plugin_dir = format!("{}/{}", self.work_dir, builder.get_plugin_name()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think there is any need to cleanup these directories? I guess when we're running in a container it isn't too significant.
} | ||
|
||
match config.ca_generation_policy { | ||
x if x == CaGenerationPolicy::GenerateIfNotFound as u32 => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't use x
for anything, so I don't see the point of x if x ==
. Why not just check directly again the enum?
match config.ca_generation_policy {
CaGenerationPolicy::GenerateIfNotFound => {
If you need to do some casting, you can do it in the first line
match config.ca_generation_policy as u32{
CaGenerationPolicy::GenerateIfNotFound => {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#11 132.6 70 | match config.ca_generation_policy {
#11 132.6 | --------------------------- this expression has type `u32`
#11 132.6 71 | CaGenerationPolicy::GenerateIfNotFound => {
#11 132.6 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found `CaGenerationPolicy`
Rust does not allow casting in the left side of a match case. So we have to either convert config.ca_generation_policy
to CaGenerationPolicy
or use the expression trick x if x ==
in the match case.
|
||
match config.ca_generation_policy { | ||
x if x == CaGenerationPolicy::GenerateIfNotFound as u32 => { | ||
if !ca.crt.exists() || !ca.key.exists() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really big match statement. It looks kind of weird to me. You might want to try nesting matches rather than using a bunch of if
blocks inside the match. For instance you could turn this if into another match.
It might also be cleaner if you created a helper function to generate the cert.
Finally, you might think about switching the order of these conditions. It might be cleaner to first cleanup incomplete state (which you should do in any case). Then check if the cert is there and then if it isn't there, check what the regeneration policy is.
if the key/cert need to be generated and then check the CaGenerationPolicy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, makes sense. I will create a helper function to generate the cert. Thanks.
} | ||
} | ||
x => { | ||
bail!("CaGenerationPolicy {x} not supported"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something is definitely fishy with this match statement. If you implement a match on an enum and the enum only has two values, you won't need this kind of fallback case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can convert config.ca_generation_policy
to CaGenerationPolicy
outside of the match to avoid this fallback.
|
||
pub fn test_all(&self) -> Result<()> { | ||
self.test_nebula_cert_sign() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you call this test anywhere?
It's pretty common to put test code in a separate module rather than implementing it alongside the real methods. Not sure it's a big deal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is called when the plugin is initialized. It runs simple tests to check if the provided nebula-cert
is working.
} | ||
|
||
/// Run "nebula-cert sign" to generate a credential | ||
pub fn generate( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe call this generate_credential
params.push("-out-key".into()); | ||
params.push(ca.key.as_path().into()); | ||
|
||
let status = Command::new(NEBULA_CERT_BIN) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check if this binary actually exists before trying to use it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @cclaudio for this. I made some suggestions from a user perspective.
@@ -29,17 +29,18 @@ config = "0.13.3" | |||
env_logger = "0.10.0" | |||
hex = "0.4.3" | |||
jwt-simple = "0.11" | |||
kbs_protocol = { git = "https://github.com/confidential-containers/guest-components.git", rev="9bd6f06a9704e01808e91abde130dffb20e632a5", default-features = false } | |||
kbs_protocol = { git = "https://github.com/confidential-containers/guest-components.git", rev="0d08cccf3c72647de273fee90716d6842b9ddcfd", default-features = false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also update the version of kms
as they come from the same repo.
@@ -7,7 +7,7 @@ use anyhow::{Context, Result}; | |||
use serde::Deserialize; | |||
use std::path::{Path, PathBuf}; | |||
|
|||
pub const DEFAULT_REPO_DIR_PATH: &str = "/opt/confidential-containers/kbs/repository"; | |||
pub const DEFAULT_REPO_DIR_PATH: &str = "/opt/confidential-containers/kbs/storage"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change will change the mount path inside KBS container. I am not sure if it will influence current CI settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this would force changes in a lot of CI and documentation across repos. I hope we can avoid changing that.
let resource_byte = if resource_description.repository_name == "plugin" { | ||
repository_plugin | ||
.read() | ||
.await | ||
.get_resource( | ||
resource_description.resource_type.as_str(), | ||
resource_description.resource_tag.as_str(), | ||
request.query_string(), | ||
) | ||
.await | ||
.map_err(|e| Error::ReadSecretFailed(e.to_string()))? | ||
} else { | ||
repository | ||
.read() | ||
.await | ||
.read_secret_resource(resource_description) | ||
.await | ||
.map_err(|e| Error::ReadSecretFailed(e.to_string()))? | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need to update the KBS PROTOCOL document under docs to show that plugin
is reserved.
[plugin_manager_config] | ||
work_dir = "/opt/confidential-containers/kbs/plugin" | ||
enabled_plugins = [] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would have an overall workdir for all plugins. Maybe we should let every single plugin has its own configs, like the workdir of them own?
Some config format in my mind
insecure_http = true
insecure_api = true
[attestation_token_config]
attestation_token_type = "CoCo"
[grpc_config]
as_addr = "http://127.0.0.1:50004"
pool_size = 200
[repository_config]
type = "LocalFs"
dir_path = "/opt/confidential-containers/kbs/storage"
[plugins.nebula]
config_path = "/etc/kbs/plugin/nebula-config.toml"
[plugins.another]
config_a=xxx
config_b=yyy
If only [plugins.nebula]
is explicitly specified in the toml, nebula
be initialized. In this way every single plugin could have its own config, or specify the config path inside that.
This could be implemented via
#[derive(Clone, Debug, Deserialize)]
pub struct KbsConfig {
...
pub plugins: Option<PluginManagerConfig>,
}
#[derive(Clone, Debug, Deserialize)]
struct PluginManagerConfig {
#[cfg(feature = "nebula-plugin")]
nebula: Option<NebulaConfig>,
#[cfg(feature = "another-plugin")]
another: Option<AnotherPluginConfig>,
}
#[cfg(feature = "nebula-plugin")]
#[derive(Clone, Debug, Deserialize)]
struct NebulaConfig {
config_path: String,
}
#[cfg(feature = "another-plugin")]
#[derive(Clone, Debug, Deserialize)]
struct AnotherPluginConfig {
config_a: String,
config_b: String,
}
@@ -2,18 +2,24 @@ FROM rust:latest as builder | |||
ARG ARCH=x86_64 | |||
ARG HTTPS_CRYPTO=rustls | |||
ARG ALIYUN=false | |||
ARG PLUGINS="nebula-plugin" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest that we leave default plugins empty. Also, we can add some guides in docs to show how to enable nebula when building KBS image.
For this we might need to add a new doc about plugins here.
.await | ||
.read_secret_resource(resource_description) | ||
.await | ||
.map_err(|e| Error::ReadSecretFailed(e.to_string()))? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map_err(|e| Error::ReadSecretFailed(e.to_string()))? | |
.map_err(|e| Error::ReadSecretFailed(format!("{e:?}")))?; |
request.query_string(), | ||
) | ||
.await | ||
.map_err(|e| Error::ReadSecretFailed(e.to_string()))? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map_err(|e| Error::ReadSecretFailed(e.to_string()))? | |
.map_err(|e| Error::ReadSecretFailed(format!("{e:?}")))?; |
@@ -7,7 +7,7 @@ use anyhow::{Context, Result}; | |||
use serde::Deserialize; | |||
use std::path::{Path, PathBuf}; | |||
|
|||
pub const DEFAULT_REPO_DIR_PATH: &str = "/opt/confidential-containers/kbs/repository"; | |||
pub const DEFAULT_REPO_DIR_PATH: &str = "/opt/confidential-containers/kbs/storage"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this would force changes in a lot of CI and documentation across repos. I hope we can avoid changing that.
|
||
#[async_trait::async_trait] | ||
trait Plugin { | ||
async fn get_name(&self) -> &str; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for pure accessor method it's more idiomatic to use the field name as fn. also why should it be async?
async fn get_name(&self) -> &str; | |
fn name(&self) -> &str; |
enabled_plugins: Vec<String>, | ||
} | ||
|
||
impl PluginManagerConfig { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure the PluginManager/PluginManagerConfig/PluginBuilder
abstractions are required? I am concerned they introduce accidental complexity without a use case. Afaict we don't really need to "manage" plugins, they are activated and configured at launch, and do not change at runtime.
IMO a plugin could be an implementation of this trait:
trait Plugin {
async fn get_resource(&self, resource: &str, query_string: &str) -> Result<Vec<u8>>;
}
when parsing KbsConfig
a Plugin can be initialized and registered in a lookup map.
// we don't want to have dynamic strings for plugin lookup, they are known at compile-time
const NEBULA: &str = "nebula";
...
// this needs to be in the app's state for lookup in the route handlers
let mut enabled_plugins: BTreeMap<&'static str, Box<dyn Plugin + Send + Sync>>;
...
let nebula_plugin = nebula::new(&kbs_config)?;
enabled_plugins.insert(NEBULA, Box::new(nebula_plugin));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made an initial pass over the changes -- great work. A couple comments below:
resource: &str, | ||
query_string: &str, | ||
) -> Result<Vec<u8>> { | ||
for plugin in self.plugins.iter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would a map work better than iterating over a vector here (or maybe there are only a handful of plugins anyway)?
}; | ||
|
||
log::info!("nebula-cert binary: {}", ca.get_version()?.trim()); | ||
ca.test_all()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line looks like you want to move it to a test section now
.with_context(|| format!("Remove {} file", ca.crt.display()))?; | ||
} | ||
if ca.key.exists() { | ||
fs::remove_file(ca.crt.as_path()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe paste mistake: ca.key
This plugin (based on the Nebula plugin interface) delivers credentials (keys and certificates) to a sandbox (i.e., confidential PODs or VMs), specifically to the kata agent to initiate the SplitAPI proxy server so that a workload owner can communicate with the proxy server using a secure tunnel. The proxy server's credentials can be requested using the kbs-client: kbs-client --url http://127.0.0.1:8080 \ get-resource \ --path 'plugin/splitapi/credential?ip=60.11.12.13&name=pod6&id=32348' The IPv4 address, name, and the ID of the sandbox must be provided in the query string to obtain the credential resources from the kbs. After receiving the credential request, the splitapi plugin will create a key pair for the server and client and sign them using the self-signed CA. The generated ca.crt, server.crt, and server.key are stored in a directory specific to the sandbox (the caller) and returned to the caller. In addition, ca.key, client.key, and client.crt are also generated and stored to that particular directory specific to the sandbox (i.e., caller) During the credential generation, a directory manager creates a unique directory specific to the sandbox (i.e., the caller). The manager creates the unique directory using the sandbox parameters passed in the query string. A mapping file is also maintained to store the mapping between the sandbox name and the unique directory created for the sandbox. The splitapi plugin itself is not initialized by default. To initialize it, you need to add 'splitapi' to 'manager_plugin_config.enabled_plugins' in the kbs-config.toml. Depends on: confidential-containers#451 Signed-off-by: Salman Ahmed <sahmed@ibm.com>
New PR submitted #539 for nebula. Closing this one. |
This PR requires confidential-containers/guest-components#634 to be merged first.
This PR resolves #396 by adding the following changes:
Steps to test it
enabled_plugins = ["nebula"]
docker compose build
docker compose up
cd kbs && ATTESTER=snp-attester make cli && sudo make install-cli
kbs-client --url http://127.0.0.1:8080 config --auth-private-key kbs/config/private.key set-resource-policy --policy-file kbs/sample_policies/allow_all.rego
kbs-client --url http://127.0.0.1:8080 get-resource --path 'plugin/nebula/credential?ip[ip]=10.11.12.13&ip[netbits]=21&name=pod1' | base64 -d
The last command should return the credential requested, e.g.: