From 341a318804c68c303f8f700cfdd11d7c053f6b5d Mon Sep 17 00:00:00 2001 From: Claudio Carvalho Date: Mon, 29 Jul 2024 17:35:54 +0300 Subject: [PATCH 1/4] docker-composer: Move kbs-config.toml to /etc/kbs 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 --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 75b493ca73..277cd0d7c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: command: [ "/usr/local/bin/kbs", "--config-file", - "/etc/kbs-config.toml", + "/etc/kbs/kbs-config.toml", ] restart: always # keep the server running ports: @@ -16,7 +16,7 @@ services: volumes: - ./kbs/data/kbs-storage:/opt/confidential-containers/kbs/repository:rw - ./kbs/config/public.pub:/opt/confidential-containers/kbs/user-keys/public.pub - - ./kbs/config/docker-compose/kbs-config.toml:/etc/kbs-config.toml + - ./kbs/config/docker-compose/kbs-config.toml:/etc/kbs/kbs-config.toml depends_on: - as From aaf1590ae975e1b9a20d10890dccb81bb1b1728b Mon Sep 17 00:00:00 2001 From: Claudio Carvalho Date: Tue, 30 Jul 2024 16:20:47 +0300 Subject: [PATCH 2/4] kbs/resource: Add plugin interface to get resources Currently, a resource is requested to the KBS through the resource URI, which takes the resource description in the format '///'. 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 --- Cargo.lock | 91 ++++++++++++------- Cargo.toml | 6 +- docker-compose.yml | 1 + kbs/Makefile | 12 ++- kbs/config/docker-compose/kbs-config.toml | 4 + kbs/config/kbs-config.toml | 4 + kbs/docker/coco-as-grpc/Dockerfile | 3 +- kbs/docs/config.md | 18 ++++ kbs/src/bin/kbs.rs | 2 + kbs/src/config.rs | 6 ++ kbs/src/http/config.rs | 6 ++ kbs/src/http/resource.rs | 28 ++++-- kbs/src/lib.rs | 11 +++ kbs/src/resource/mod.rs | 1 + kbs/src/resource/plugin/mod.rs | 102 ++++++++++++++++++++++ 15 files changed, 253 insertions(+), 42 deletions(-) create mode 100644 kbs/src/resource/plugin/mod.rs diff --git a/Cargo.lock b/Cargo.lock index ca9cf35d28..9fde0fe86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 0.8.11", "socket2", "tokio", "tracing", @@ -503,7 +503,7 @@ source = "git+https://github.com/confidential-containers/guest-components.git?re dependencies = [ "anyhow", "async-trait", - "attester", + "attester 0.1.0 (git+https://github.com/confidential-containers/guest-components.git?rev=9bd6f06a9704e01808e91abde130dffb20e632a5)", "base64 0.21.7", "config", "const_format", @@ -562,13 +562,13 @@ dependencies = [ [[package]] name = "attester" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=9bd6f06a9704e01808e91abde130dffb20e632a5#9bd6f06a9704e01808e91abde130dffb20e632a5" +source = "git+https://github.com/confidential-containers/guest-components.git?rev=0d08cccf3c72647de273fee90716d6842b9ddcfd#0d08cccf3c72647de273fee90716d6842b9ddcfd" dependencies = [ "anyhow", "async-trait", "az-snp-vtpm 0.6.0", "az-tdx-vtpm 0.6.0", - "base64 0.21.7", + "base64 0.22.1", "codicon", "csv-rs", "hex", @@ -592,6 +592,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "attester" +version = "0.1.0" +source = "git+https://github.com/confidential-containers/guest-components.git?rev=9bd6f06a9704e01808e91abde130dffb20e632a5#9bd6f06a9704e01808e91abde130dffb20e632a5" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "hex", + "kbs-types", + "log", + "serde", + "serde_json", + "serde_with", + "sha2", + "strum", + "thiserror", +] + [[package]] name = "atty" version = "0.2.14" @@ -1368,11 +1387,11 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=9bd6f06a9704e01808e91abde130dffb20e632a5#9bd6f06a9704e01808e91abde130dffb20e632a5" +source = "git+https://github.com/confidential-containers/guest-components.git?rev=0d08cccf3c72647de273fee90716d6842b9ddcfd#0d08cccf3c72647de273fee90716d6842b9ddcfd" dependencies = [ "aes-gcm", "anyhow", - "base64 0.21.7", + "base64 0.22.1", "ctr", "kbs-types", "rand", @@ -2793,18 +2812,18 @@ dependencies = [ [[package]] name = "kbs_protocol" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=9bd6f06a9704e01808e91abde130dffb20e632a5#9bd6f06a9704e01808e91abde130dffb20e632a5" +source = "git+https://github.com/confidential-containers/guest-components.git?rev=0d08cccf3c72647de273fee90716d6842b9ddcfd#0d08cccf3c72647de273fee90716d6842b9ddcfd" dependencies = [ "anyhow", "async-trait", - "attester", - "base64 0.21.7", + "attester 0.1.0 (git+https://github.com/confidential-containers/guest-components.git?rev=0d08cccf3c72647de273fee90716d6842b9ddcfd)", + "base64 0.22.1", "crypto", "jwt-simple 0.12.9", "kbs-types", "log", "reqwest 0.12.4", - "resource_uri", + "resource_uri 0.1.0 (git+https://github.com/confidential-containers/guest-components.git?rev=0d08cccf3c72647de273fee90716d6842b9ddcfd)", "serde", "serde_json", "sha2", @@ -2832,7 +2851,7 @@ dependencies = [ "prost 0.11.9", "rand", "reqwest 0.12.4", - "resource_uri", + "resource_uri 0.1.0 (git+https://github.com/confidential-containers/guest-components.git?rev=9bd6f06a9704e01808e91abde130dffb20e632a5)", "ring 0.17.8", "serde", "serde_json", @@ -2965,9 +2984,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchit" @@ -3043,6 +3062,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "mobc" version = "0.8.4" @@ -3228,16 +3259,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -4250,6 +4271,17 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "resource_uri" +version = "0.1.0" +source = "git+https://github.com/confidential-containers/guest-components.git?rev=0d08cccf3c72647de273fee90716d6842b9ddcfd#0d08cccf3c72647de273fee90716d6842b9ddcfd" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "url", +] + [[package]] name = "resource_uri" version = "0.1.0" @@ -5339,21 +5371,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.1", "parking_lot 0.12.2", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5368,9 +5399,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3d7bd6bbc9..57bb832863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,11 +29,11 @@ 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 } kbs-types = "0.6.0" kms = { git = "https://github.com/confidential-containers/guest-components.git", rev="9bd6f06a9704e01808e91abde130dffb20e632a5", default-features = false } jsonwebtoken = { version = "9", default-features = false } -log = "0.4.17" +log = "0.4.22" prost = "0.12" regorus = { version = "0.1.5", default-features = false, features = ["regex", "base64", "time"] } reqwest = "0.12" @@ -49,4 +49,4 @@ thiserror = "1.0" tokio = { version = "1", features = ["full"] } tempfile = "3.4.0" tonic = "0.11" -tonic-build = "0.11" \ No newline at end of file +tonic-build = "0.11" diff --git a/docker-compose.yml b/docker-compose.yml index 277cd0d7c5..e171c9c53f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - "8080:8080" volumes: - ./kbs/data/kbs-storage:/opt/confidential-containers/kbs/repository:rw + - ./kbs/data/kbs-plugin:/opt/confidential-containers/kbs/plugin:rw - ./kbs/config/public.pub:/opt/confidential-containers/kbs/user-keys/public.pub - ./kbs/config/docker-compose/kbs-config.toml:/etc/kbs/kbs-config.toml depends_on: diff --git a/kbs/Makefile b/kbs/Makefile index f1cef76e70..b2e1460f16 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -2,6 +2,7 @@ AS_TYPE ?= coco-as HTTPS_CRYPTO ?= rustls POLICY_ENGINE ?= ALIYUN ?= false +PLUGINS ?= ARCH := $(shell uname -m) # Check if ARCH is supported, otehrwise return error @@ -24,9 +25,18 @@ else endif ifeq ($(ALIYUN), true) - FEATURES += aliyun + FEATURE_LIST += aliyun endif +ifdef PLUGINS + FEATURE_LIST += $(PLUGINS) +endif + +NOOP = +SPACE = $(NOOP) $(NOOP) +COMMA = , +FEATURES=$(subst $(SPACE),$(COMMA),$(FEATURE_LIST)) + ifndef CLI_FEATURES ifdef ATTESTER CLI_FEATURES = "sample_only,$(ATTESTER)" diff --git a/kbs/config/docker-compose/kbs-config.toml b/kbs/config/docker-compose/kbs-config.toml index b999639892..fd9e31039f 100644 --- a/kbs/config/docker-compose/kbs-config.toml +++ b/kbs/config/docker-compose/kbs-config.toml @@ -2,6 +2,10 @@ sockets = ["0.0.0.0:8080"] auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" insecure_http = true +[plugin_manager_config] +work_dir = "/opt/confidential-containers/kbs/plugin" +enabled_plugins = [] + [attestation_token_config] attestation_token_type = "CoCo" diff --git a/kbs/config/kbs-config.toml b/kbs/config/kbs-config.toml index d04fd53408..1f55d65b48 100644 --- a/kbs/config/kbs-config.toml +++ b/kbs/config/kbs-config.toml @@ -8,6 +8,10 @@ attestation_token_type = "CoCo" type = "LocalFs" dir_path = "/opt/confidential-containers/kbs/repository" +[plugin_manager_config] +work_dir = "/opt/confidential-containers/kbs/plugin" +enabled_plugins = [] + [as_config] work_dir = "/opt/confidential-containers/attestation-service" policy_engine = "opa" diff --git a/kbs/docker/coco-as-grpc/Dockerfile b/kbs/docker/coco-as-grpc/Dockerfile index 2a96e9045d..6c5ba703e5 100644 --- a/kbs/docker/coco-as-grpc/Dockerfile +++ b/kbs/docker/coco-as-grpc/Dockerfile @@ -2,6 +2,7 @@ FROM rust:latest as builder ARG ARCH=x86_64 ARG HTTPS_CRYPTO=rustls ARG ALIYUN=false +ARG PLUGINS="" WORKDIR /usr/src/kbs COPY . . @@ -9,7 +10,7 @@ COPY . . RUN apt-get update && apt install -y protobuf-compiler git # Build and Install KBS -RUN cd kbs && make AS_FEATURE=coco-as-grpc HTTPS_CRYPTO=${HTTPS_CRYPTO} POLICY_ENGINE=opa ALIYUN=${ALIYUN} && \ +RUN cd kbs && make AS_FEATURE=coco-as-grpc HTTPS_CRYPTO=${HTTPS_CRYPTO} POLICY_ENGINE=opa ALIYUN=${ALIYUN} PLUGINS=${PLUGINS} && \ make install-kbs FROM ubuntu:22.04 diff --git a/kbs/docs/config.md b/kbs/docs/config.md index 5c9de577a2..97c47f2eda 100644 --- a/kbs/docs/config.md +++ b/kbs/docs/config.md @@ -75,6 +75,24 @@ type-specific properties. | `password` | String | AAP client key password | Yes | `8f9989c18d27...` | | `cert_pem` | String | CA cert for the KMS instance | Yes | `-----BEGIN CERTIFICATE----- ...` | +### Repository Plugin Configuration + +The following properties can be set under the `plugin_manager_config` section. + +This section is **optional**. When omitted, a default configuration is used. + +>This section is available only when the `resource` feature is enabled. + +| Property | Type | Description | Required | Default | +|----------------------------|----------------|------------------------------------------------------------|----------|---------| +| `work_dir` | String | Location for the Repository Plugin Manager to store data. | Yes | - | +| `enabled_plugins` | String Array | Name of the plugins that will be configured and available. | Yes | - | + +List of supported plugins that can be added to `enabled_plugins`. + +| Plugin name | Plugin Description | Available Cargo Features | +|-----------------------|--------------------------------------------------|-------------------------------| + ### Native Attestation The following properties can be set under the `as_config` section. diff --git a/kbs/src/bin/kbs.rs b/kbs/src/bin/kbs.rs index b2ae1b66b4..8d70cceaf8 100644 --- a/kbs/src/bin/kbs.rs +++ b/kbs/src/bin/kbs.rs @@ -67,6 +67,8 @@ async fn main() -> Result<()> { #[cfg(feature = "resource")] kbs_config.repository_config.unwrap_or_default(), #[cfg(feature = "resource")] + kbs_config.plugin_manager_config.unwrap_or_default(), + #[cfg(feature = "resource")] kbs_config.attestation_token_config, #[cfg(feature = "opa")] kbs_config.policy_engine_config.unwrap_or_default(), diff --git a/kbs/src/config.rs b/kbs/src/config.rs index 4359f0687d..0219093229 100644 --- a/kbs/src/config.rs +++ b/kbs/src/config.rs @@ -9,6 +9,8 @@ use crate::attestation::intel_trust_authority::IntelTrustAuthorityConfig; #[cfg(feature = "policy")] use crate::policy_engine::PolicyEngineConfig; #[cfg(feature = "resource")] +use crate::resource::plugin::PluginManagerConfig; +#[cfg(feature = "resource")] use crate::resource::RepositoryConfig; #[cfg(feature = "resource")] use crate::token::AttestationTokenVerifierConfig; @@ -34,6 +36,10 @@ pub struct KbsConfig { #[cfg(feature = "resource")] pub repository_config: Option, + /// Resource plugin repository manager. + #[cfg(feature = "resource")] + pub plugin_manager_config: Option, + /// Attestation token result broker config. #[cfg(feature = "resource")] pub attestation_token_config: AttestationTokenVerifierConfig, diff --git a/kbs/src/http/config.rs b/kbs/src/http/config.rs index 8328a380b7..0544e6c354 100644 --- a/kbs/src/http/config.rs +++ b/kbs/src/http/config.rs @@ -124,6 +124,12 @@ pub(crate) async fn set_resource( .to_string(), }; + if resource_description.repository_name == "plugin" { + return Err(Error::InvalidRequest(String::from( + "plugin set-resource not supported", + ))); + } + set_secret_resource(&repository, resource_description, data.as_ref()) .await .map_err(|e| Error::SetSecretFailed(format!("{e}")))?; diff --git a/kbs/src/http/resource.rs b/kbs/src/http/resource.rs index c0f17265b3..eb1aa9b057 100644 --- a/kbs/src/http/resource.rs +++ b/kbs/src/http/resource.rs @@ -15,7 +15,7 @@ use rsa::{BigUint, Pkcs1v15Encrypt, RsaPublicKey}; use serde::Deserialize; use serde_json::{json, Deserializer, Value}; -use crate::raise_error; +use crate::{raise_error, resource::plugin::PluginManager}; use super::*; @@ -30,6 +30,7 @@ const TOKEN_TEE_PUBKEY_PATH: &str = "/customized_claims/runtime_data/tee-pubkey" pub(crate) async fn get_resource( request: HttpRequest, repository: web::Data>>, + repository_plugin: web::Data>>, #[cfg(feature = "as")] map: web::Data, token_verifier: web::Data>>, #[cfg(feature = "policy")] policy_engine: web::Data, @@ -113,12 +114,25 @@ pub(crate) async fn get_resource( info!("Resource access request passes policy check."); } - let resource_byte = repository - .read() - .await - .read_secret_resource(resource_description) - .await - .map_err(|e| Error::ReadSecretFailed(format!("{e:?}")))?; + 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()))? + }; let jwe = jwe(pubkey, resource_byte)?; diff --git a/kbs/src/lib.rs b/kbs/src/lib.rs index 5d51775981..efc0203b3b 100644 --- a/kbs/src/lib.rs +++ b/kbs/src/lib.rs @@ -23,6 +23,8 @@ use anyhow::{anyhow, bail, Context, Result}; use attestation::AttestationService; use jwt_simple::prelude::Ed25519PublicKey; #[cfg(feature = "resource")] +use resource::plugin::PluginManagerConfig; +#[cfg(feature = "resource")] use resource::RepositoryConfig; #[cfg(feature = "as")] use std::sync::Arc; @@ -95,6 +97,8 @@ pub struct ApiServer { #[cfg(feature = "resource")] repository_config: RepositoryConfig, #[cfg(feature = "resource")] + plugin_manager_config: PluginManagerConfig, + #[cfg(feature = "resource")] attestation_token_config: AttestationTokenVerifierConfig, #[cfg(feature = "policy")] policy_engine_config: PolicyEngineConfig, @@ -114,6 +118,7 @@ impl ApiServer { http_timeout: i64, insecure_api: bool, #[cfg(feature = "resource")] repository_config: RepositoryConfig, + #[cfg(feature = "resource")] plugin_manager_config: PluginManagerConfig, #[cfg(feature = "resource")] attestation_token_config: AttestationTokenVerifierConfig, #[cfg(feature = "policy")] policy_engine_config: PolicyEngineConfig, ) -> Result { @@ -142,6 +147,8 @@ impl ApiServer { #[cfg(feature = "resource")] repository_config, #[cfg(feature = "resource")] + plugin_manager_config, + #[cfg(feature = "resource")] attestation_token_config, #[cfg(feature = "policy")] policy_engine_config, @@ -238,6 +245,9 @@ impl ApiServer { #[cfg(feature = "resource")] let repository = self.repository_config.initialize()?; + #[cfg(feature = "resource")] + let repository_plugin_manager = self.plugin_manager_config.create_plugin_manager()?; + #[cfg(feature = "resource")] let token_verifier = crate::token::create_token_verifier(self.attestation_token_config.clone())?; @@ -283,6 +293,7 @@ impl ApiServer { cfg_if::cfg_if! { if #[cfg(feature = "resource")] { server_app = server_app.app_data(web::Data::new(repository.clone())) + .app_data(web::Data::new(repository_plugin_manager.clone())) .app_data(web::Data::new(token_verifier.clone())) .service( web::resource([ diff --git a/kbs/src/resource/mod.rs b/kbs/src/resource/mod.rs index 7f5ce4228a..5a2f035267 100644 --- a/kbs/src/resource/mod.rs +++ b/kbs/src/resource/mod.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use tokio::sync::RwLock; mod local_fs; +pub mod plugin; #[cfg(feature = "aliyun")] mod aliyun_kms; diff --git a/kbs/src/resource/plugin/mod.rs b/kbs/src/resource/plugin/mod.rs new file mode 100644 index 0000000000..c8fb448623 --- /dev/null +++ b/kbs/src/resource/plugin/mod.rs @@ -0,0 +1,102 @@ +// Copyright (c) 2024 by IBM Inc. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "nebula-plugin")] +mod nebula; + +use anyhow::{anyhow, bail, Context, Result}; +use serde::Deserialize; +use std::fs; +use std::path::Path; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[cfg(feature = "nebula-plugin")] +use crate::resource::plugin::nebula::NebulaPluginConfig; + +trait PluginBuild { + fn get_plugin_name(&self) -> &str; + fn create_plugin(&self, work_dir: &str) -> Result>>; +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct PluginManagerConfig { + work_dir: String, + enabled_plugins: Vec, +} + +impl PluginManagerConfig { + fn get_plugin_builders(&self) -> Vec> { + #[allow(unused_mut)] + let mut p: Vec> = Vec::new(); + + // List of all plugins supported + #[cfg(feature = "nebula-plugin")] + p.push(Box::new(NebulaPluginConfig::default())); + + p + } + + pub fn create_plugin_manager(&self) -> Result>> { + if !Path::new(&self.work_dir).exists() { + fs::create_dir_all(&self.work_dir) + .with_context(|| "Create resource plugin dir".to_string())?; + } + + #[allow(unused_mut)] + let mut manager = PluginManager { + plugins: Vec::new(), + }; + + let plugin_builders = self.get_plugin_builders(); + + for plugin_name in self.enabled_plugins.iter() { + let builder = plugin_builders + .iter() + .find(|x| x.get_plugin_name() == plugin_name) + .ok_or(anyhow!( + "Cargo {}-plugin feature is either not set or not supported", + plugin_name, + ))?; + + let plugin_dir = format!("{}/{}", self.work_dir, builder.get_plugin_name()); + let plugin = builder.create_plugin(plugin_dir.as_str())?; + manager.plugins.push(plugin); + + log::info!("{} plugin loaded", builder.get_plugin_name()); + } + + log::info!("{} plugin(s) loaded", manager.plugins.len()); + + Ok(Arc::new(RwLock::new(manager))) + } +} + +#[async_trait::async_trait] +trait Plugin { + async fn get_name(&self) -> &str; + async fn get_resource(&self, resource: &str, query_string: &str) -> Result>; +} + +pub struct PluginManager { + plugins: Vec>>, +} + +impl PluginManager { + pub async fn get_resource( + &self, + plugin_name: &str, + resource: &str, + query_string: &str, + ) -> Result> { + for plugin in self.plugins.iter() { + let p = plugin.read().await; + + if *plugin_name == *p.get_name().await { + return p.get_resource(resource, query_string).await; + } + } + bail!("Plugin {} not found", plugin_name) + } +} From 6a8cc31a1efb635452b4509d1e9ce23e6e633a46 Mon Sep 17 00:00:00 2001 From: Claudio Carvalho Date: Tue, 30 Jul 2024 22:14:14 +0300 Subject: [PATCH 3/4] kbs/resource: Set storage path to /opt/confidential-containers/kbs/storage 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 --- docker-compose.yml | 2 +- kbs/config/docker-compose/kbs-config.toml | 4 ++++ kbs/config/kbs-config.toml | 2 +- kbs/src/resource/local_fs.rs | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e171c9c53f..956c36c136 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: ports: - "8080:8080" volumes: - - ./kbs/data/kbs-storage:/opt/confidential-containers/kbs/repository:rw + - ./kbs/data/kbs-storage:/opt/confidential-containers/kbs/storage:rw - ./kbs/data/kbs-plugin:/opt/confidential-containers/kbs/plugin:rw - ./kbs/config/public.pub:/opt/confidential-containers/kbs/user-keys/public.pub - ./kbs/config/docker-compose/kbs-config.toml:/etc/kbs/kbs-config.toml diff --git a/kbs/config/docker-compose/kbs-config.toml b/kbs/config/docker-compose/kbs-config.toml index fd9e31039f..59a91068ca 100644 --- a/kbs/config/docker-compose/kbs-config.toml +++ b/kbs/config/docker-compose/kbs-config.toml @@ -2,6 +2,10 @@ sockets = ["0.0.0.0:8080"] auth_public_key = "/opt/confidential-containers/kbs/user-keys/public.pub" insecure_http = true +[repository_config] +type = "LocalFs" +dir_path = "/opt/confidential-containers/kbs/storage" + [plugin_manager_config] work_dir = "/opt/confidential-containers/kbs/plugin" enabled_plugins = [] diff --git a/kbs/config/kbs-config.toml b/kbs/config/kbs-config.toml index 1f55d65b48..73919d1809 100644 --- a/kbs/config/kbs-config.toml +++ b/kbs/config/kbs-config.toml @@ -6,7 +6,7 @@ attestation_token_type = "CoCo" [repository_config] type = "LocalFs" -dir_path = "/opt/confidential-containers/kbs/repository" +dir_path = "/opt/confidential-containers/kbs/storage" [plugin_manager_config] work_dir = "/opt/confidential-containers/kbs/plugin" diff --git a/kbs/src/resource/local_fs.rs b/kbs/src/resource/local_fs.rs index 99640be00c..82f4a2a7b3 100644 --- a/kbs/src/resource/local_fs.rs +++ b/kbs/src/resource/local_fs.rs @@ -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"; #[derive(Debug, Deserialize, Clone)] pub struct LocalFsRepoDesc { From baf0bd61c023f1a03a14257c5345791eb6fcf83d Mon Sep 17 00:00:00 2001 From: Claudio Carvalho Date: Wed, 31 Jul 2024 00:02:10 +0300 Subject: [PATCH 4/4] kbs/resource: Add nebula plugin 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 #396 Signed-off-by: Claudio Carvalho --- Cargo.lock | 12 + Cargo.toml | 1 + docker-compose.yml | 1 + kbs/Cargo.toml | 6 +- kbs/config/plugin/nebula-config.toml | 69 ++++ kbs/docker/coco-as-grpc/Dockerfile | 7 +- kbs/docs/config.md | 1 + kbs/src/resource/plugin/nebula.rs | 462 +++++++++++++++++++++++++++ 8 files changed, 556 insertions(+), 3 deletions(-) create mode 100644 kbs/config/plugin/nebula-config.toml create mode 100644 kbs/src/resource/plugin/nebula.rs diff --git a/Cargo.lock b/Cargo.lock index 9fde0fe86f..005d78e918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2772,6 +2772,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_qs", "strum", "tempfile", "thiserror", @@ -4817,6 +4818,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_spanned" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 57bb832863..a4581efe27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ reqwest = "0.12" rstest = "0.18.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.89" +serde_qs = "0.13.0" serde_with = { version = "1.11.0", features = ["base64", "hex"] } serial_test = "0.9.0" sha2 = "0.10" diff --git a/docker-compose.yml b/docker-compose.yml index 956c36c136..312dbfbbd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: - ./kbs/data/kbs-plugin:/opt/confidential-containers/kbs/plugin:rw - ./kbs/config/public.pub:/opt/confidential-containers/kbs/user-keys/public.pub - ./kbs/config/docker-compose/kbs-config.toml:/etc/kbs/kbs-config.toml + - ./kbs/config/plugin/nebula-config.toml:/etc/kbs/plugin/nebula-config.toml depends_on: - as diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index a983769c56..731c710663 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -41,6 +41,7 @@ rustls = ["actix-web/rustls", "dep:rustls", "dep:rustls-pemfile"] # Use openssl crypto stack for KBS openssl = ["actix-web/openssl", "dep:openssl"] +nebula-plugin = [] # Use aliyun KMS as KBS backend aliyun = ["kms/aliyun"] @@ -76,16 +77,17 @@ semver = "1.0.16" serde = { workspace = true, features = ["derive"] } serde_json.workspace = true strum.workspace = true +serde_qs.workspace = true thiserror.workspace = true time = { version = "0.3.23", features = ["std"] } tokio.workspace = true tonic = { workspace = true, optional = true } uuid = { version = "1.2.2", features = ["serde", "v4"] } openssl = { version = "0.10.46", optional = true } +tempfile.workspace = true [dev-dependencies] -tempfile.workspace = true rstest.workspace = true [build-dependencies] -tonic-build = { workspace = true, optional = true } \ No newline at end of file +tonic-build = { workspace = true, optional = true } diff --git a/kbs/config/plugin/nebula-config.toml b/kbs/config/plugin/nebula-config.toml new file mode 100644 index 0000000000..18faf09b0e --- /dev/null +++ b/kbs/config/plugin/nebula-config.toml @@ -0,0 +1,69 @@ +# Required: +# CA certificate path +crt_path = "/opt/confidential-containers/kbs/plugin/nebula/ca/ca.crt" + +# Required: +# CA key path +key_path = "/opt/confidential-containers/kbs/plugin/nebula/ca/ca.key" + +# Required: +# Certificate Authority generation policy +# +# 1 = Create a self signed CA only if +# crt_path/key_path not found +# +# 2 = Never generate self signed CA as +# both crt_path and key_path are pre-installed +ca_generation_policy = 1 + +[self_signed_ca_config] + +# Required: +# Name of the certificate authority +name = "Nebula CA for Trustee KBS" + +# Optional: +# Argon2 iterations parameter used for encrypted +# private key passphrase (default 1) +## argon_iterations = 1 + +# Optional: +# Argon2 memory parameter (in KiB) used for encrypted +# private key passphrase (default 2097152) +## argon_memory = 2097152 + +# Optional: +# Argon2 parallelism parameter used for encrypted private +# key passphrase (default 4) +## argon_parallelism = 4 + +# Optional: +# EdDSA/ECDSA Curve (25519, P256) (default "25519") +## curve = "25519" + +# Optional: +# Amount of time the certificate should be valid for. +# Valid time units are seconds: +# "s", minutes: "m", hours: "h" (default 8760h0m0s) +## duration = "8760h0m0s" + +# Optional: +# Comma separated list of groups. This will limit which +# groups subordinate certs can use +## groups = "servers,ssh" + +# Optional: +# Comma separated list of ipv4 address and network +# in CIDR notation. This will limit which ipv4 addresses and +# networks subordinate certs can use for ip addresses +## ips = "192.168.100.10/24" + +# Optional: +# Path to write a QR code image (png) of the certificate +## out_qr = "/opt/confidential-containers/kbs/plugin/nebula/ca/ca.png" + +# Optional: +# Comma separated list of ipv4 address and network +# in CIDR notation. This will limit which ipv4 addresses and +# networks subordinate certs can use in subnets +## subnets = "192.168.86.0/24" \ No newline at end of file diff --git a/kbs/docker/coco-as-grpc/Dockerfile b/kbs/docker/coco-as-grpc/Dockerfile index 6c5ba703e5..0ec85c1ddf 100644 --- a/kbs/docker/coco-as-grpc/Dockerfile +++ b/kbs/docker/coco-as-grpc/Dockerfile @@ -2,7 +2,7 @@ FROM rust:latest as builder ARG ARCH=x86_64 ARG HTTPS_CRYPTO=rustls ARG ALIYUN=false -ARG PLUGINS="" +ARG PLUGINS="nebula-plugin" WORKDIR /usr/src/kbs COPY . . @@ -13,8 +13,13 @@ RUN apt-get update && apt install -y protobuf-compiler git RUN cd kbs && make AS_FEATURE=coco-as-grpc HTTPS_CRYPTO=${HTTPS_CRYPTO} POLICY_ENGINE=opa ALIYUN=${ALIYUN} PLUGINS=${PLUGINS} && \ make install-kbs +# Install Nebula +RUN wget https://github.com/slackhq/nebula/releases/download/v1.8.2/nebula-linux-amd64.tar.gz +RUN tar -C /usr/local/bin -xzf nebula-linux-amd64.tar.gz + FROM ubuntu:22.04 LABEL org.opencontainers.image.source="https://github.com/confidential-containers/trustee/kbs" COPY --from=builder /usr/local/bin/kbs /usr/local/bin/kbs +COPY --from=builder /usr/local/bin/nebula-cert /usr/local/bin/nebula-cert diff --git a/kbs/docs/config.md b/kbs/docs/config.md index 97c47f2eda..62681c2e0d 100644 --- a/kbs/docs/config.md +++ b/kbs/docs/config.md @@ -92,6 +92,7 @@ List of supported plugins that can be added to `enabled_plugins`. | Plugin name | Plugin Description | Available Cargo Features | |-----------------------|--------------------------------------------------|-------------------------------| +| `nebula` | Provide resources to support the creation of a Nebula encrypted overlay network between nodes. | `nebula-plugin` | ### Native Attestation diff --git a/kbs/src/resource/plugin/nebula.rs b/kbs/src/resource/plugin/nebula.rs new file mode 100644 index 0000000000..f6e4050761 --- /dev/null +++ b/kbs/src/resource/plugin/nebula.rs @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (c) 2024 by IBM Inc. + +//! The nebula plugin allows the KBS to deliver resources required to create +//! an encrypted overlay network between nodes using [Nebula](https://github.com/slackhq/nebula), +//! +//! Within the Nebula overlay network, all communications between nodes are +//! automatically encrypted. + +use anyhow::{anyhow, bail, Context, Result}; +use serde_qs; +use std::ffi::OsString; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Arc; +use tempfile::{tempdir_in, TempDir}; +use tokio::sync::RwLock; + +use super::{Plugin, PluginBuild}; + +pub const PLUGIN_NAME: &str = "nebula"; + +const NEBULA_CONFIG_PATH: &str = "/etc/kbs/plugin/nebula-config.toml"; +const CRT_FILENAME: &str = "node.crt"; +const KEY_FILENAME: &str = "node.key"; + +// Required binaries must be in PATH +const NEBULA_CERT_BIN: &str = "nebula-cert"; + +/// Policies that define when a Nebula CA must be generated. +/// They are documented in the nebula plugin config toml file +#[repr(u32)] +pub enum CaGenerationPolicy { + GenerateIfNotFound = 1, + NeverGenerate = 2, +} + +/// Plugin configuration +/// It is documented in the nebula plugin config toml file +#[derive(Debug, Default, serde::Deserialize)] +pub struct NebulaPluginConfig { + crt_path: String, + key_path: String, + ca_generation_policy: u32, + self_signed_ca_config: Option, +} + +impl PluginBuild for NebulaPluginConfig { + fn get_plugin_name(&self) -> &str { + PLUGIN_NAME + } + + fn create_plugin(&self, work_dir: &str) -> Result>> { + let config = Self::try_from(Path::new(NEBULA_CONFIG_PATH))?; + + let ca = NebulaCa { + crt: PathBuf::from(config.crt_path), + key: PathBuf::from(config.key_path), + work_dir: PathBuf::from(work_dir), + }; + + if let Some(parent) = ca.work_dir.as_path().parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Create {} dir", parent.display()))?; + } + + match config.ca_generation_policy { + x if x == CaGenerationPolicy::GenerateIfNotFound as u32 => { + if !ca.crt.exists() || !ca.key.exists() { + // Clean-up in case the CA failed to generate last time + if ca.crt.exists() { + fs::remove_file(ca.crt.as_path()) + .with_context(|| format!("Remove {} file", ca.crt.display()))?; + } + if ca.key.exists() { + fs::remove_file(ca.crt.as_path()) + .with_context(|| format!("Remove {} file", ca.key.display()))?; + } + // Create directories if the CA is being created for the first time + if let Some(parent) = ca.crt.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Create {} dir", parent.display()))?; + } + if let Some(parent) = ca.key.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Create {} dir", parent.display()))?; + } + + let ca_config = config.self_signed_ca_config.ok_or(anyhow!( + "self_signed_ca_config not found in {}", + NEBULA_CONFIG_PATH + ))?; + + let mut params: Vec = Vec::from(&ca_config); + params.push("-out-crt".into()); + params.push(ca.crt.as_path().into()); + params.push("-out-key".into()); + params.push(ca.key.as_path().into()); + + let status = Command::new(NEBULA_CERT_BIN) + .args(params) + .status() + .context("nebula-cert ca run")?; + + if !status.success() { + bail!("nebula-cert ca status"); + } + log::info!("Nebula CA generated"); + } else { + log::info!("Nebula CA already exists, loading it") + } + } + x if x == CaGenerationPolicy::NeverGenerate as u32 => { + if !ca.crt.exists() || !ca.key.exists() { + bail!("Nebula CA not found"); + } else { + log::info!("Nebula CA found, loading it") + } + } + x => { + bail!("CaGenerationPolicy {x} not supported"); + } + }; + + log::info!("nebula-cert binary: {}", ca.get_version()?.trim()); + ca.test_all()?; + + Ok(Arc::new(RwLock::new(NebulaPlugin { ca })) as Arc>) + } +} + +impl TryFrom<&Path> for NebulaPluginConfig { + type Error = anyhow::Error; + + fn try_from(config_path: &Path) -> Result { + log::info!("Loading plugin config file {}", config_path.display()); + let config = config::Config::builder() + .add_source(config::File::with_name( + config_path + .to_str() + .expect("Nebula config path is not valid unicode"), + )) + .build()?; + config + .try_deserialize() + .map_err(|e| anyhow!("invalid config: {}", e.to_string())) + } +} + +/// Configuration to generate a Nebula self signed CA. Further information +/// on these fields can be found running "nebula-cert ca --help", or +/// just looking at nebula plugin toml file. +#[derive(Debug, serde::Deserialize)] +struct SelfSignedCaConfig { + name: String, + argon_iterations: Option, + argon_memory: Option, + argon_parallelism: Option, + curve: Option, + duration: Option, + groups: Option, + ips: Option, + out_qr: Option, + subnets: Option, +} + +impl From<&SelfSignedCaConfig> for Vec { + fn from(config: &SelfSignedCaConfig) -> Self { + let mut params: Vec = Vec::new(); + + params.push("ca".into()); + params.push("-name".into()); + params.push((&config.name).into()); + + if let Some(value) = &config.argon_iterations { + params.push("-argon-iterations".into()); + params.push(value.to_string().into()); + } + if let Some(value) = &config.argon_memory { + params.push("-argon-memory".into()); + params.push(value.to_string().into()); + } + if let Some(value) = &config.argon_parallelism { + params.push("-argon-parallelism".into()); + params.push(value.to_string().into()); + } + if let Some(value) = &config.curve { + params.push("-curve".into()); + params.push(value.into()); + } + if let Some(value) = &config.duration { + params.push("-duration".into()); + params.push(value.into()); + } + if let Some(value) = &config.groups { + params.push("-groups".into()); + params.push(value.into()); + } + if let Some(value) = &config.ips { + params.push("-ips".into()); + params.push(format!("{}", value).into()); + } + if let Some(value) = &config.out_qr { + params.push("-out-qr".into()); + params.push(value.into()); + } + if let Some(value) = &config.subnets { + params.push("-subnets".into()); + params.push(format!("{}", value).into()); + } + + params + } +} + +#[derive(Debug, PartialEq, serde::Deserialize)] +struct Ipv4CidrList { + list: Vec, +} + +impl fmt::Display for Ipv4CidrList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for v in self.list.iter().take(1) { + write!(f, "{v}")?; + } + for v in self.list.iter().skip(1) { + write!(f, ",{v}")?; + } + Ok(()) + } +} + +#[derive(Debug, PartialEq, serde::Deserialize)] +struct Ipv4Cidr { + ip: String, + netbits: String, +} + +impl fmt::Display for Ipv4Cidr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}/{}", self.ip, self.netbits) + } +} + +/// Parameters taken by "nebula-cert sign" to create a credential +/// for a node to join a Nebula overlay network. These fields are +/// received as a query string in the get-resource URI +#[derive(Debug, PartialEq, serde::Deserialize)] +struct NebulaCredentialParams { + /// Required: ipv4 address and network in CIDR notation to assign the cert + ip: Ipv4Cidr, + /// Required: name of the cert, usually hostname or podname + name: String, + /// Optional: how long the cert should be valid for. + /// The default is 1 second before the signing cert expires. + /// Valid time units are seconds: "s", minutes: "m", hours: "h". + duration: Option, + /// Optional: comma separated list of groups. + groups: Option, + /// Optional: comma separated list of ipv4 address and network in CIDR notation. + /// Subnets this cert can serve for + subnets: Option, +} + +impl From<&NebulaCredentialParams> for Vec { + fn from(params: &NebulaCredentialParams) -> Self { + let mut v: Vec = Vec::new(); + + v.push("sign".into()); + v.push("-name".into()); + v.push((¶ms.name).into()); + v.push("-ip".into()); + v.push((¶ms.ip.to_string()).into()); + + if let Some(value) = ¶ms.duration { + v.push("-duration".into()); + v.push(value.into()); + } + if let Some(value) = ¶ms.groups { + v.push("-groups".into()); + v.push(value.into()); + } + if let Some(value) = ¶ms.subnets { + v.push("-subnets".into()); + v.push(value.to_string().into()); + } + + v + } +} + +#[derive(Debug, serde::Serialize)] +pub struct CredentialResource { + pub node_crt: Vec, + pub node_key: Vec, + pub ca_crt: Vec, +} + +/// Credential for a Nebula overlay network +/// It is created in a temporary directory to prevent +/// the same file from being accessed by multiple threads +#[derive(Debug)] +struct Credential { + _temp_dir: TempDir, + crt: PathBuf, + key: PathBuf, +} + +impl Credential { + pub fn new(work_dir: &Path) -> Result { + let temp_dir = tempdir_in(work_dir)?; + + let crt: PathBuf = temp_dir.path().join(CRT_FILENAME); + let key: PathBuf = temp_dir.path().join(KEY_FILENAME); + + Ok(Self { + _temp_dir: temp_dir, + crt, + key, + }) + } + + /// Run "nebula-cert sign" to generate a credential + pub fn generate( + &self, + ca_key: &Path, + ca_crt: &Path, + params: &NebulaCredentialParams, + ) -> Result<&Self> { + let mut args: Vec = Vec::from(params); + + args.push("-ca-key".into()); + args.push(ca_key.into()); + args.push("-ca-crt".into()); + args.push(ca_crt.into()); + args.push("-out-key".into()); + args.push(self.key.as_path().into()); + args.push("-out-crt".into()); + args.push(self.crt.as_path().into()); + + let status = Command::new(NEBULA_CERT_BIN) + .args(args) + .status() + .context("nebula-cert sign run")?; + + if !status.success() { + bail!("nebula-cert sign status"); + } + + Ok(self) + } +} + +/// The temp_dir is auto-deleted when it goes out-of-scope, but before that +/// we need to delete the generated credential +impl Drop for Credential { + fn drop(&mut self) { + if self.crt.exists() { + if let Err(e) = fs::remove_file(self.crt.as_path()) + .with_context(|| format!("Remove {} file", self.crt.display())) + { + log::warn!("{}", e.to_string()); + } + } + if self.key.exists() { + if let Err(e) = fs::remove_file(self.key.as_path()) + .with_context(|| format!("Remove {} file", self.key.display())) + { + log::warn!("{}", e.to_string()); + } + } + } +} + +/// Nebula Certificate Authority +#[derive(Debug, Default)] +struct NebulaCa { + key: PathBuf, + crt: PathBuf, + work_dir: PathBuf, +} + +impl NebulaCa { + pub fn get_credential_resource(&self, params: &NebulaCredentialParams) -> Result> { + let cred = Credential::new(self.work_dir.as_path())?; + + cred.generate(self.key.as_path(), self.crt.as_path(), params)?; + + let resource = CredentialResource { + node_crt: fs::read(cred.crt.as_path()) + .with_context(|| format!("read {}", cred.crt.display()))?, + node_key: fs::read(cred.key.as_path()) + .with_context(|| format!("read {}", cred.key.display()))?, + ca_crt: fs::read(self.crt.as_path()) + .with_context(|| format!("read {}", self.crt.display()))?, + }; + + Ok(serde_json::to_vec(&resource)?) + } + + pub fn get_version(&self) -> Result { + let output = Command::new(NEBULA_CERT_BIN).arg("--version").output()?; + Ok(String::from_utf8(output.stdout)?) + } + + pub fn test_all(&self) -> Result<()> { + self.test_nebula_cert_sign() + } + + pub fn test_nebula_cert_sign(&self) -> Result<()> { + let params = NebulaCredentialParams { + ip: Ipv4Cidr { + ip: "10.10.10.10".to_string(), + netbits: "21".to_string(), + }, + name: "node-test".to_string(), + duration: None, + groups: None, + subnets: None, + }; + + let _ = Credential::new(self.work_dir.as_path())?.generate( + self.key.as_path(), + self.crt.as_path(), + ¶ms, + )?; + + Ok(()) + } +} + +/// Nebula plugin +#[derive(Default, Debug)] +pub struct NebulaPlugin { + ca: NebulaCa, +} + +#[async_trait::async_trait] +impl Plugin for NebulaPlugin { + async fn get_name(&self) -> &str { + PLUGIN_NAME + } + + async fn get_resource(&self, resource: &str, query_string: &str) -> Result> { + let response: Vec = match resource { + // plugin/nebula/credential?{query_string} + // e.g. plugin/nebula/credential?ip[ip]=10.11.12.13&ip[netbits]=21&name=node1 + // the query_string will be used to generate the credential + "credential" => { + let params: NebulaCredentialParams = serde_qs::from_str(query_string)?; + self.ca.get_credential_resource(¶ms)? + } + // resource not supported + e => bail!("Nebula plugin resource {e} not supported"), + }; + + Ok(response) + } +}