diff --git a/.vscode/resolver.code-snippets b/.vscode/resolver.code-snippets index bcf82a222..375f5658d 100644 --- a/.vscode/resolver.code-snippets +++ b/.vscode/resolver.code-snippets @@ -3,8 +3,8 @@ "scope": "rust", "prefix": "resolve", "body": [ - "impl Resolve<${2}> for ${1} {", - "\tasync fn resolve(self, _: &${2}) -> serror::Result<${0}> {", + "impl Resolve<${0}> for ${1} {", + "\tasync fn resolve(self, _: &${0}) -> Result {", "\t\ttodo!()", "\t}", "}" diff --git a/bin/cli/src/command/execute.rs b/bin/cli/src/command/execute.rs index f0510ee83..12fa685de 100644 --- a/bin/cli/src/command/execute.rs +++ b/bin/cli/src/command/execute.rs @@ -230,6 +230,9 @@ pub async fn handle( Execution::GlobalAutoUpdate(data) => { println!("{}: {data:?}", "Data".dimmed()) } + Execution::RotateAllServerKeys(data) => { + println!("{}: {data:?}", "Data".dimmed()) + } Execution::Sleep(data) => { println!("{}: {data:?}", "Data".dimmed()) } @@ -494,6 +497,10 @@ pub async fn handle( .execute(request) .await .map(|u| ExecutionResult::Single(u.into())), + Execution::RotateAllServerKeys(request) => client + .execute(request) + .await + .map(|u| ExecutionResult::Single(u.into())), Execution::Sleep(request) => { let duration = Duration::from_millis(request.duration_ms as u64); diff --git a/bin/core/src/api/execute/maintenance.rs b/bin/core/src/api/execute/maintenance.rs index 072c5443d..aa57ad1d2 100644 --- a/bin/core/src/api/execute/maintenance.rs +++ b/bin/core/src/api/execute/maintenance.rs @@ -1,16 +1,21 @@ -use std::sync::OnceLock; +use std::{fmt::Write as _, sync::OnceLock}; use anyhow::{Context, anyhow}; use command::run_komodo_command; use database::mungos::{find::find_collect, mongodb::bson::doc}; use formatting::{bold, format_serror}; +use futures::StreamExt; use komodo_client::{ - api::execute::{ - BackupCoreDatabase, ClearRepoCache, GlobalAutoUpdate, + api::{ + execute::{ + BackupCoreDatabase, ClearRepoCache, GlobalAutoUpdate, + RotateAllServerKeys, + }, + write::RotateServerKeys, }, entities::{ deployment::DeploymentState, server::ServerState, - stack::StackState, + stack::StackState, user::system_user, }, }; use reqwest::StatusCode; @@ -19,8 +24,9 @@ use serror::AddStatusCodeError; use tokio::sync::Mutex; use crate::{ - api::execute::{ - ExecuteArgs, pull_deployment_inner, pull_stack_inner, + api::{ + execute::{ExecuteArgs, pull_deployment_inner, pull_stack_inner}, + write::WriteArgs, }, config::core_config, helpers::update::update_update, @@ -317,3 +323,94 @@ impl Resolve for GlobalAutoUpdate { Ok(update) } } + +// + +impl Resolve for RotateAllServerKeys { + async fn resolve( + self, + ExecuteArgs { user, update }: &ExecuteArgs, + ) -> Result { + if !user.admin { + return Err( + anyhow!("This method is admin only.") + .status_code(StatusCode::FORBIDDEN), + ); + } + + let mut update = update.clone(); + + update_update(update.clone()).await?; + + let mut servers = db_client() + .servers + .find(doc! { "config.enabled": true }) + .await + .context("Failed to query servers from database")?; + + let server_status_cache = server_status_cache(); + + let mut log = String::new(); + + while let Some(server) = servers.next().await { + let server = match server { + Ok(server) => server, + Err(e) => { + warn!("Failed to parse Server | {e:#}"); + continue; + } + }; + let Some(status) = server_status_cache.get(&server.id).await + else { + let _ = write!( + &mut log, + "\nSkipping {}: No Status ⚠️", + bold(&server.name) + ); + continue; + }; + if !matches!(status.state, ServerState::Ok) { + let _ = write!( + &mut log, + "\nSkipping {}: {} ⚠️", + bold(&server.name), + status.state + ); + continue; + } + match (RotateServerKeys { server: server.id }) + .resolve(&WriteArgs { + user: system_user().to_owned(), + }) + .await + { + Ok(_) => { + let _ = write!( + &mut log, + "\nRotated keys for {} ✅", + bold(&server.name) + ); + } + Err(e) => { + update.push_error_log( + "Key Rotation Failure", + format_serror( + &e.error + .context(format!( + "Failed to rotate {} keys", + bold(&server.name) + )) + .into(), + ), + ); + } + } + } + + update.push_simple_log("Rotate Server Keys", log); + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index 5c78b887a..e797f2aea 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -149,6 +149,7 @@ pub enum ExecuteRequest { ClearRepoCache(ClearRepoCache), BackupCoreDatabase(BackupCoreDatabase), GlobalAutoUpdate(GlobalAutoUpdate), + RotateAllServerKeys(RotateAllServerKeys), } pub fn router() -> Router { diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index b00a1a65f..77a786f3b 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -92,7 +92,7 @@ pub enum WriteRequest { CreateTerminal(CreateTerminal), DeleteTerminal(DeleteTerminal), DeleteAllTerminals(DeleteAllTerminals), - RotateServerPrivateKey(RotateServerPrivateKey), + RotateServerKeys(RotateServerKeys), // ==== STACK ==== CreateStack(CreateStack), diff --git a/bin/core/src/api/write/server.rs b/bin/core/src/api/write/server.rs index fbacef075..c352e1e1c 100644 --- a/bin/core/src/api/write/server.rs +++ b/bin/core/src/api/write/server.rs @@ -199,7 +199,7 @@ impl Resolve for DeleteAllTerminals { // -impl Resolve for RotateServerPrivateKey { +impl Resolve for RotateServerKeys { #[instrument(name = "RotateServerPrivateKey", skip(args))] async fn resolve(self, args: &WriteArgs) -> serror::Result { let server = get_check_permissions::( diff --git a/bin/core/src/cloud/aws/ec2.rs b/bin/core/src/cloud/aws/ec2.rs index 5c7500e5d..790446103 100644 --- a/bin/core/src/cloud/aws/ec2.rs +++ b/bin/core/src/cloud/aws/ec2.rs @@ -84,7 +84,6 @@ pub async fn launch_ec2_instance( assign_public_ip, use_public_ip, user_data, - core_private_key: _, periphery_public_key: _, port: _, use_https: _, diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index 562a20e6c..7a126b2b4 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -42,36 +42,6 @@ pub fn core_public_key() -> &'static String { }) } -pub fn periphery_public_keys() -> Option<&'static [SpkiPublicKey]> { - static PERIPHERY_PUBLIC_KEYS: OnceLock>> = - OnceLock::new(); - PERIPHERY_PUBLIC_KEYS - .get_or_init(|| { - core_config().periphery_public_keys.as_ref().map( - |public_keys| { - public_keys - .iter() - .map(|public_key| { - let maybe_pem = if let Some(path) = - public_key.strip_prefix("file:") - { - std::fs::read_to_string(path) - .with_context(|| { - format!("Failed to read public key at {path:?}") - }) - .unwrap() - } else { - public_key.clone() - }; - SpkiPublicKey::from_maybe_pem(&maybe_pem).unwrap() - }) - .collect() - }, - ) - }) - .as_deref() -} - pub fn core_connection_query() -> &'static String { static CORE_HOSTNAME: OnceLock = OnceLock::new(); CORE_HOSTNAME.get_or_init(|| { @@ -245,8 +215,6 @@ pub fn core_config() -> &'static CoreConfig { }, // Non secrets - periphery_public_keys: env.komodo_periphery_public_keys - .or(config.periphery_public_keys), title: env.komodo_title.unwrap_or(config.title), host: env.komodo_host.unwrap_or(config.host), port: env.komodo_port.unwrap_or(config.port), diff --git a/bin/core/src/connection/mod.rs b/bin/core/src/connection/mod.rs index 6f6fa6109..074ee4b19 100644 --- a/bin/core/src/connection/mod.rs +++ b/bin/core/src/connection/mod.rs @@ -35,8 +35,7 @@ use transport::{ }; use crate::{ - config::{core_private_key, periphery_public_keys}, - periphery::ConnectionChannels, + config::core_private_key, periphery::ConnectionChannels, state::db_client, }; @@ -96,7 +95,6 @@ pub struct PeripheryConnectionArgs<'a> { /// Usually the server id pub id: &'a str, pub address: Option<&'a str>, - core_private_key: Option<&'a str>, periphery_public_key: Option<&'a str>, } @@ -106,43 +104,26 @@ impl PublicKeyValidator for PeripheryConnectionArgs<'_> { &self, public_key: String, ) -> anyhow::Result { - // Make sure all cases get the same error, - // including what the public key should be. - let invalid_error = || { - spawn_update_attempted_public_key( - self.id.to_string(), - Some(public_key.clone()), - ); - anyhow!("{public_key} is invalid") - .context( - "Ensure public key matches configured Periphery Public Key", - ) - .context("Core failed to validate Periphery public key") - }; - // Handle explicit public key - if let Some(expected) = self.periphery_public_key { - return if public_key == expected { - Ok(public_key) - } else { - Err(invalid_error()) - }; - } - // Core -> Periphery connections with no explicit - // Periphery public key are not validated. - if self.address.is_some() { - return Ok(public_key); - } - // Periphery -> Core connections fall back to - // 'periphery_public_keys' in Core config. - let expected = - periphery_public_keys().ok_or_else(invalid_error)?; - if expected - .iter() - .any(|expected| public_key == expected.as_str()) - { - Ok(public_key) - } else { - Err(invalid_error()) + let core_to_periphery = self.address.is_some(); + match (self.periphery_public_key, core_to_periphery) { + // The key matches expected. + (Some(expected), _) if public_key == expected => Ok(public_key), + // Core -> Periphery connections with no explicit + // Periphery public key are not validated. + (None, true) => Ok(public_key), + // Auth failed. + (Some(_), _) | (None, false) => { + spawn_update_attempted_public_key( + self.id.to_string(), + Some(public_key.clone()), + ); + let e = anyhow!("{public_key} is invalid") + .context( + "Ensure public key matches configured Periphery Public Key", + ) + .context("Core failed to validate Periphery public key"); + Err(e) + } } } } @@ -152,7 +133,6 @@ impl<'a> PeripheryConnectionArgs<'a> { Self { id: &server.id, address: optional_str(&server.config.address), - core_private_key: optional_str(&server.config.core_private_key), periphery_public_key: optional_str( &server.config.periphery_public_key, ), @@ -166,7 +146,6 @@ impl<'a> PeripheryConnectionArgs<'a> { Self { id, address: optional_str(&config.address), - core_private_key: optional_str(&config.core_private_key), periphery_public_key: optional_str( &config.periphery_public_key, ), @@ -181,7 +160,6 @@ impl<'a> PeripheryConnectionArgs<'a> { Self { id, address: Some(address), - core_private_key: optional_str(&config.core_private_key), periphery_public_key: optional_str( &config.periphery_public_key, ), @@ -192,7 +170,6 @@ impl<'a> PeripheryConnectionArgs<'a> { OwnedPeripheryConnectionArgs { id: self.id.to_string(), address: self.address.map(str::to_string), - core_private_key: self.core_private_key.map(str::to_string), periphery_public_key: self .periphery_public_key .map(str::to_string), @@ -214,8 +191,6 @@ pub struct OwnedPeripheryConnectionArgs { /// Specify outbound connection address. /// Inbound connections have this as None pub address: Option, - /// The private key to use, or None for core private key - pub core_private_key: Option, /// The public key to expect Periphery to have. /// If None, must have 'periphery_public_keys' set /// in Core config, or will error @@ -227,7 +202,6 @@ impl OwnedPeripheryConnectionArgs { PeripheryConnectionArgs { id: &self.id, address: self.address.as_deref(), - core_private_key: self.core_private_key.as_deref(), periphery_public_key: self.periphery_public_key.as_deref(), } } @@ -312,15 +286,10 @@ impl PeripheryConnection { socket: &mut W, identifiers: ConnectionIdentifiers<'_>, ) -> anyhow::Result<()> { - let private_key = self - .args - .core_private_key - .as_ref() - .unwrap_or(core_private_key()); L::login(LoginFlowArgs { socket, identifiers, - private_key, + private_key: core_private_key(), public_key_validator: self.args.borrow(), }) .await?; diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index 4b61df739..f5eb1e44a 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -1209,6 +1209,23 @@ async fn execute_execution( ) .await? } + Execution::RotateAllServerKeys(req) => { + let req = ExecuteRequest::RotateAllServerKeys(req); + let update = init_execution_update(&req, &user).await?; + let ExecuteRequest::RotateAllServerKeys(req) = req else { + unreachable!() + }; + let update_id = update.id.clone(); + handle_resolve_result( + req + .resolve(&ExecuteArgs { user, update }) + .await + .map_err(|e| e.error) + .context("Failed at RotateAllServerKeys"), + &update_id, + ) + .await? + } Execution::Sleep(req) => { let duration = Duration::from_millis(req.duration_ms as u64); tokio::time::sleep(duration).await; diff --git a/bin/core/src/helpers/update.rs b/bin/core/src/helpers/update.rs index cde85b8e6..1fab63eb8 100644 --- a/bin/core/src/helpers/update.rs +++ b/bin/core/src/helpers/update.rs @@ -520,6 +520,9 @@ pub async fn init_execution_update( ExecuteRequest::GlobalAutoUpdate(_data) => { (Operation::GlobalAutoUpdate, ResourceTarget::system()) } + ExecuteRequest::RotateAllServerKeys(_data) => { + (Operation::RotateAllServerKeys, ResourceTarget::system()) + } }; let mut update = make_update(target, operation, user); diff --git a/bin/core/src/resource/procedure.rs b/bin/core/src/resource/procedure.rs index 8a43a8d1b..38a567a9b 100644 --- a/bin/core/src/resource/procedure.rs +++ b/bin/core/src/resource/procedure.rs @@ -774,6 +774,13 @@ async fn validate_config( )); } } + Execution::RotateAllServerKeys(_params) => { + if !user.admin { + return Err(anyhow!( + "Non admin user cannot trigger rotate all server keys" + )); + } + } Execution::Sleep(_) => {} } } diff --git a/bin/core/src/startup.rs b/bin/core/src/startup.rs index 0537b67da..60ba678fb 100644 --- a/bin/core/src/startup.rs +++ b/bin/core/src/startup.rs @@ -12,7 +12,8 @@ use komodo_client::{ api::{ auth::SignUpLocalUser, execute::{ - BackupCoreDatabase, Execution, GlobalAutoUpdate, RunAction, + BackupCoreDatabase, Execution, GlobalAutoUpdate, + RotateAllServerKeys, RunAction, }, write::{ CreateBuilder, CreateProcedure, CreateServer, CreateTag, @@ -422,6 +423,58 @@ async fn ensure_init_user_and_resources() { ); } }.await; + + // RotateAllServerKeys + async { + let Ok(config) = ProcedureConfig::builder() + .stages(vec![ProcedureStage { + name: String::from("Stage 1"), + enabled: true, + executions: vec![ + EnabledExecution { + execution: Execution::RotateAllServerKeys(RotateAllServerKeys {}), + enabled: true + } + ] + }]) + .schedule(String::from("Every day at 06:00")) + .build() + .inspect_err(|e| error!("Failed to initialize Server key rotation Procedure | Failed to build Procedure | {e:?}")) else { + return; + }; + let procedure = match (CreateProcedure { + name: String::from("Rotate Server Keys"), + config: config.into(), + }) + .resolve(&write_args) + .await + { + Ok(procedure) => procedure, + Err(e) => { + error!( + "Failed to initialize Server key rotation Procedure | Failed to create Procedure | {:#}", + e.error + ); + return; + } + }; + if let Err(e) = (UpdateResourceMeta { + target: ResourceTarget::Procedure(procedure.id), + tags: Some(default_tags.clone()), + description: Some(String::from( + "Rotates all currently connected Server keys.", + )), + template: None, + }) + .resolve(&write_args) + .await + { + warn!( + "Failed to update Server key rotation Procedure tags / description | {:#}", + e.error + ); + } + }.await; } /// v1.17.5 removes the ServerTemplate resource. diff --git a/bin/core/src/sync/resources.rs b/bin/core/src/sync/resources.rs index f27389e89..f214b2d6b 100644 --- a/bin/core/src/sync/resources.rs +++ b/bin/core/src/sync/resources.rs @@ -692,6 +692,7 @@ impl ResourceSyncTrait for Procedure { Execution::ClearRepoCache(_) => {} Execution::BackupCoreDatabase(_) => {} Execution::GlobalAutoUpdate(_) => {} + Execution::RotateAllServerKeys(_) => {} Execution::Sleep(_) => {} } } diff --git a/bin/core/src/sync/toml.rs b/bin/core/src/sync/toml.rs index c0a08ef6a..a5009e089 100644 --- a/bin/core/src/sync/toml.rs +++ b/bin/core/src/sync/toml.rs @@ -810,7 +810,8 @@ impl ToToml for Procedure { | Execution::Sleep(_) | Execution::ClearRepoCache(_) | Execution::BackupCoreDatabase(_) - | Execution::GlobalAutoUpdate(_) => {} + | Execution::GlobalAutoUpdate(_) + | Execution::RotateAllServerKeys(_) => {} } } } diff --git a/client/core/rs/src/api/execute/maintenance.rs b/client/core/rs/src/api/execute/maintenance.rs index 9fba2852a..161993040 100644 --- a/client/core/rs/src/api/execute/maintenance.rs +++ b/client/core/rs/src/api/execute/maintenance.rs @@ -8,7 +8,7 @@ use crate::entities::update::Update; use super::KomodoExecuteRequest; -/// Clears all repos from the Core repo cache. Admin only. +/// **Admin only.** Clears all repos from the Core repo cache. /// Response: [Update] #[typeshare] #[derive( @@ -26,8 +26,10 @@ use super::KomodoExecuteRequest; #[error(serror::Error)] pub struct ClearRepoCache {} -/// Backs up the Komodo Core database to compressed jsonl files. -/// Admin only. Response: [Update] +// + +/// **Admin only.** Backs up the Komodo Core database to compressed jsonl files. +/// Response: [Update] /// /// Mount a folder to `/backups`, and Core will use it to create /// timestamped database dumps, which can be restored using @@ -50,9 +52,11 @@ pub struct ClearRepoCache {} #[error(serror::Error)] pub struct BackupCoreDatabase {} -/// Trigger a global poll for image updates on Stacks and Deployments +// + +/// **Admin only.** Trigger a global poll for image updates on Stacks and Deployments /// with `poll_for_updates` or `auto_update` enabled. -/// Admin only. Response: [Update] +/// Response: [Update] /// /// 1. `docker compose pull` any Stacks / Deployments with `poll_for_updates` or `auto_update` enabled. This will pick up any available updates. /// 2. Redeploy Stacks / Deployments that have updates found and 'auto_update' enabled. @@ -71,3 +75,23 @@ pub struct BackupCoreDatabase {} #[response(Update)] #[error(serror::Error)] pub struct GlobalAutoUpdate {} + +// + +/// **Admin only.** Rotates all connected Server keys. +/// Response: [Update] +#[typeshare] +#[derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Resolve, + EmptyTraits, + Parser, +)] +#[empty_traits(KomodoExecuteRequest)] +#[response(Update)] +#[error(serror::Error)] +pub struct RotateAllServerKeys {} diff --git a/client/core/rs/src/api/execute/mod.rs b/client/core/rs/src/api/execute/mod.rs index a1f6550d7..6f07602aa 100644 --- a/client/core/rs/src/api/execute/mod.rs +++ b/client/core/rs/src/api/execute/mod.rs @@ -163,6 +163,7 @@ pub enum Execution { ClearRepoCache(ClearRepoCache), BackupCoreDatabase(BackupCoreDatabase), GlobalAutoUpdate(GlobalAutoUpdate), + RotateAllServerKeys(RotateAllServerKeys), // SLEEP Sleep(Sleep), diff --git a/client/core/rs/src/api/write/server.rs b/client/core/rs/src/api/write/server.rs index 870ede716..23e1b6bdf 100644 --- a/client/core/rs/src/api/write/server.rs +++ b/client/core/rs/src/api/write/server.rs @@ -209,7 +209,7 @@ pub struct DeleteAllTerminals { // -/// Rotate the private key on the server. +/// Rotates the private / public keys for the server. /// Response: [NoData] #[typeshare] #[derive( @@ -218,7 +218,7 @@ pub struct DeleteAllTerminals { #[empty_traits(KomodoWriteRequest)] #[response(NoData)] #[error(serror::Error)] -pub struct RotateServerPrivateKey { +pub struct RotateServerKeys { /// Server Id or name pub server: String, } diff --git a/client/core/rs/src/entities/builder.rs b/client/core/rs/src/entities/builder.rs index de7a3824b..c0d32fdc4 100644 --- a/client/core/rs/src/entities/builder.rs +++ b/client/core/rs/src/entities/builder.rs @@ -247,9 +247,6 @@ impl MergePartial for BuilderConfig { BuilderConfig::Url(config) => { let config = UrlBuilderConfig { address: partial.address.unwrap_or(config.address), - core_private_key: partial - .core_private_key - .unwrap_or(config.core_private_key), periphery_public_key: partial .periphery_public_key .unwrap_or(config.periphery_public_key), @@ -292,9 +289,6 @@ impl MergePartial for BuilderConfig { .unwrap_or(config.use_public_ip), port: partial.port.unwrap_or(config.port), use_https: partial.use_https.unwrap_or(config.use_https), - core_private_key: partial - .core_private_key - .unwrap_or(config.core_private_key), periphery_public_key: partial .periphery_public_key .unwrap_or(config.periphery_public_key), @@ -329,11 +323,6 @@ pub struct UrlBuilderConfig { #[builder(default = default_address())] #[partial(default(default_address()))] pub address: String, - /// A custom private key to use to authenticate with the Periphery agent. - /// Otherwise, use the default Core private key. - #[serde(default)] - #[builder(default)] - pub core_private_key: String, /// An expected public key associated with Periphery private key. /// If empty, doesn't validate Periphery public key. #[serde(default)] @@ -356,7 +345,6 @@ impl Default for UrlBuilderConfig { fn default() -> Self { Self { address: default_address(), - core_private_key: Default::default(), periphery_public_key: Default::default(), passkey: Default::default(), } @@ -469,10 +457,6 @@ pub struct AwsBuilderConfig { #[builder(default)] pub user_data: String, - /// A custom private key to use to authenticate with the Periphery agent. - /// Otherwise, use the default Core private key. - #[serde(default)] - pub core_private_key: String, /// An expected public key associated with Periphery private key. /// If empty, doesn't validate Periphery public key. #[serde(default)] @@ -511,7 +495,6 @@ impl Default for AwsBuilderConfig { assign_public_ip: Default::default(), use_public_ip: Default::default(), user_data: Default::default(), - core_private_key: Default::default(), periphery_public_key: Default::default(), git_providers: Default::default(), docker_registries: Default::default(), diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 2455f3263..1ac229902 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -13,13 +13,10 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr}; use serde::Deserialize; -use crate::{ - deserializers::option_string_list_deserializer, - entities::{ - Timelength, - config::DatabaseConfig, - logger::{LogConfig, LogLevel, StdioLogMode}, - }, +use crate::entities::{ + Timelength, + config::DatabaseConfig, + logger::{LogConfig, LogLevel, StdioLogMode}, }; use super::{DockerRegistry, GitProvider, empty_or_redacted}; @@ -78,9 +75,6 @@ pub struct Env { pub komodo_private_key: Option, /// Override `private_key` with file pub komodo_private_key_file: Option, - /// Override `periphery_public_keys` - #[serde(alias = "komodo_periphery_public_key")] - pub komodo_periphery_public_keys: Option>, /// Override `passkey` pub komodo_passkey: Option, /// Override `passkey` from file @@ -342,25 +336,6 @@ pub struct CoreConfig { #[serde(default = "default_private_key")] pub private_key: String, - /// Default accepted public keys to allow Periphery to connect. - /// Core gains knowledge of the Periphery public key through the noise handshake. - /// If not provided, Periphery -> Core connected Servers must - /// configure accepted public key individually. - /// - /// Supports multiple public keys seperated by commas or newlines. - /// - /// Supports openssl generated pem file, `openssl pkey -in private.key -pubout -out public.key`. - /// To load from file, include `file:/path/to/public.key` in the list. - /// - /// Note: If used, the accepted public key can still be overridden on individual Servers / Builders - #[serde( - default, - alias = "periphery_public_key", - deserialize_with = "option_string_list_deserializer", - skip_serializing_if = "Option::is_none" - )] - pub periphery_public_keys: Option>, - /// Deprecated. Legacy v1 compatibility. /// Users should upgrade to private / public key authentication. #[serde(skip_serializing_if = "Option::is_none")] @@ -749,7 +724,6 @@ impl Default for CoreConfig { bind_ip: default_core_bind_ip(), internet_interface: Default::default(), private_key: Default::default(), - periphery_public_keys: Default::default(), passkey: Default::default(), timezone: Default::default(), ui_write_disabled: Default::default(), @@ -817,7 +791,6 @@ impl CoreConfig { } else { empty_or_redacted(&self.private_key) }, - periphery_public_keys: config.periphery_public_keys, passkey: config.passkey.as_deref().map(empty_or_redacted), timezone: config.timezone, first_server_address: config.first_server_address, diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index e7e021a1a..3a2e1164b 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -42,6 +42,8 @@ pub mod deployment; pub mod docker; /// Subtypes of [LogConfig][logger::LogConfig]. pub mod logger; +/// Subtypes of [CreationKey][creation_key::CreationKey] +pub mod onboarding_key; /// Subtypes of [Permission][permission::Permission]. pub mod permission; /// Subtypes of [Procedure][procedure::Procedure]. @@ -56,8 +58,6 @@ pub mod resource; pub mod schedule; /// Subtypes of [Server][server::Server]. pub mod server; -/// Subtypes of [CreationKey][creation_key::CreationKey] -pub mod onboarding_key; /// Subtypes of [Stack][stack::Stack] pub mod stack; /// Subtypes for server stats reporting. @@ -1173,6 +1173,7 @@ pub enum Operation { ClearRepoCache, BackupCoreDatabase, GlobalAutoUpdate, + RotateAllServerKeys, // variable CreateVariable, diff --git a/client/core/rs/src/entities/server.rs b/client/core/rs/src/entities/server.rs index e69c3d56c..80d0ab5b7 100644 --- a/client/core/rs/src/entities/server.rs +++ b/client/core/rs/src/entities/server.rs @@ -105,13 +105,6 @@ pub struct ServerConfig { #[partial_default(default_enabled())] pub enabled: bool, - /// An optional override private key to use - /// to authenticate with Periphery agent. - /// If this is empty, will use private key in core config. - #[serde(default)] - #[builder(default)] - pub core_private_key: String, - /// The expected public key associated with /// private key of the periphery agent. /// If this is empty, falls back to 'periphery_public_key' @@ -294,7 +287,6 @@ impl Default for ServerConfig { send_disk_alerts: default_send_alerts(), send_version_mismatch_alerts: default_send_alerts(), region: Default::default(), - core_private_key: Default::default(), periphery_public_key: Default::default(), passkey: Default::default(), cpu_warning: default_cpu_warning(), diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 47bc868ab..cb4ffbf36 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -223,7 +223,7 @@ export type WriteResponses = { CreateTerminal: Types.NoData; DeleteTerminal: Types.NoData; DeleteAllTerminals: Types.NoData; - RotateServerPrivateKey: Types.NoData; + RotateServerKeys: Types.NoData; // ==== STACK ==== CreateStack: Types.Stack; @@ -426,4 +426,5 @@ export type ExecuteResponses = { ClearRepoCache: Types.Update; BackupCoreDatabase: Types.Update; GlobalAutoUpdate: Types.Update; + RotateAllServerKeys: Types.Update; }; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 5bd12cdbe..f0306cda3 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -452,6 +452,7 @@ export enum Operation { ClearRepoCache = "ClearRepoCache", BackupCoreDatabase = "BackupCoreDatabase", GlobalAutoUpdate = "GlobalAutoUpdate", + RotateAllServerKeys = "RotateAllServerKeys", CreateVariable = "CreateVariable", UpdateVariableValue = "UpdateVariableValue", DeleteVariable = "DeleteVariable", @@ -884,6 +885,7 @@ export type Execution = | { type: "ClearRepoCache", params: ClearRepoCache } | { type: "BackupCoreDatabase", params: BackupCoreDatabase } | { type: "GlobalAutoUpdate", params: GlobalAutoUpdate } + | { type: "RotateAllServerKeys", params: RotateAllServerKeys } | { type: "Sleep", params: Sleep }; /** Allows to enable / disabled procedures in the sequence / parallel vec on the fly */ @@ -2118,12 +2120,6 @@ export interface ServerConfig { * Default: false */ enabled: boolean; - /** - * An optional override private key to use - * to authenticate with Periphery agent. - * If this is empty, will use private key in core config. - */ - core_private_key?: string; /** * The expected public key associated with * private key of the periphery agent. @@ -4294,11 +4290,6 @@ export interface AwsBuilderConfig { security_group_ids?: string[]; /** The user data to deploy the instance with. */ user_data?: string; - /** - * A custom private key to use to authenticate with the Periphery agent. - * Otherwise, use the default Core private key. - */ - core_private_key?: string; /** * An expected public key associated with Periphery private key. * If empty, doesn't validate Periphery public key. @@ -4313,8 +4304,8 @@ export interface AwsBuilderConfig { } /** - * Backs up the Komodo Core database to compressed jsonl files. - * Admin only. Response: [Update] + * **Admin only.** Backs up the Komodo Core database to compressed jsonl files. + * Response: [Update] * * Mount a folder to `/backups`, and Core will use it to create * timestamped database dumps, which can be restored using @@ -4579,7 +4570,7 @@ export interface CancelRepoBuild { } /** - * Clears all repos from the Core repo cache. Admin only. + * **Admin only.** Clears all repos from the Core repo cache. * Response: [Update] */ export interface ClearRepoCache { @@ -6697,9 +6688,9 @@ export interface GetVersionResponse { } /** - * Trigger a global poll for image updates on Stacks and Deployments + * **Admin only.** Trigger a global poll for image updates on Stacks and Deployments * with `poll_for_updates` or `auto_update` enabled. - * Admin only. Response: [Update] + * Response: [Update] * * 1. `docker compose pull` any Stacks / Deployments with `poll_for_updates` or `auto_update` enabled. This will pick up any available updates. * 2. Redeploy Stacks / Deployments that have updates found and 'auto_update' enabled. @@ -7811,10 +7802,17 @@ export interface RestartStack { } /** - * Rotate the private key on the server. + * **Admin only.** Rotates all connected Server keys. + * Response: [Update] + */ +export interface RotateAllServerKeys { +} + +/** + * Rotates the private / public keys for the server. * Response: [NoData] */ -export interface RotateServerPrivateKey { +export interface RotateServerKeys { /** Server Id or name */ server: string; } @@ -8606,11 +8604,6 @@ export interface UpdateVariableValue { export interface UrlBuilderConfig { /** The address of the Periphery agent */ address: string; - /** - * A custom private key to use to authenticate with the Periphery agent. - * Otherwise, use the default Core private key. - */ - core_private_key?: string; /** * An expected public key associated with Periphery private key. * If empty, doesn't validate Periphery public key. @@ -8744,7 +8737,8 @@ export type ExecuteRequest = | { type: "RunSync", params: RunSync } | { type: "ClearRepoCache", params: ClearRepoCache } | { type: "BackupCoreDatabase", params: BackupCoreDatabase } - | { type: "GlobalAutoUpdate", params: GlobalAutoUpdate }; + | { type: "GlobalAutoUpdate", params: GlobalAutoUpdate } + | { type: "RotateAllServerKeys", params: RotateAllServerKeys }; /** * One representative IANA zone for each distinct base UTC offset in the tz database. @@ -9029,7 +9023,7 @@ export type WriteRequest = | { type: "CreateTerminal", params: CreateTerminal } | { type: "DeleteTerminal", params: DeleteTerminal } | { type: "DeleteAllTerminals", params: DeleteAllTerminals } - | { type: "RotateServerPrivateKey", params: RotateServerPrivateKey } + | { type: "RotateServerKeys", params: RotateServerKeys } | { type: "CreateStack", params: CreateStack } | { type: "CopyStack", params: CopyStack } | { type: "DeleteStack", params: DeleteStack } diff --git a/config/core.config.toml b/config/core.config.toml index 3b3eb9633..45a1710e5 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -43,20 +43,10 @@ port = 9120 bind_ip = "[::]" ## Default private key to use with Noise handshake to authenticate with Periphery agents. -## If not provided, will use random default, and the public key can be queried using /read/GetCoreInfo -## Note. The private key used can be overridden for individual Servers / Builders +## Note. The private key used can be overridden for individual Servers / Builders. ## Env: KOMODO_PRIVATE_KEY or KOMODO_PRIVATE_KEY_FILE -## Default: Random 32 bytes in memory. Changes every restart. -private_key = "default-core-pk" - -## Default accepted public key to allow Periphery to connect. -## Core gains knowledge of the Periphery public key through the noise handshake. -## If not provided, Periphery -> Core connected Servers must -## configure accepted public keys individually. -## Note: If used, the public key can still be overridden on individual Servers / Builders -## Env: KOMODO_PERIPHERY_PUBLIC_KEY -## Default: None -# periphery_public_key = "pEkD8DWfrGCmoJa35m0gZkNksZsTZMAXwcqVwT1fbwY=" +## Default: file:/config/keys/core.key +private_key = "file:/config/keys/core.key" ## Deprecated. Legacy v1 compatibility. ## Users should upgrade to private / public key authentication. diff --git a/frontend/public/client/responses.d.ts b/frontend/public/client/responses.d.ts index 0eed3925f..d4f9ac73e 100644 --- a/frontend/public/client/responses.d.ts +++ b/frontend/public/client/responses.d.ts @@ -164,7 +164,7 @@ export type WriteResponses = { CreateTerminal: Types.NoData; DeleteTerminal: Types.NoData; DeleteAllTerminals: Types.NoData; - RotateServerPrivateKey: Types.NoData; + RotateServerKeys: Types.NoData; CreateStack: Types.Stack; CopyStack: Types.Stack; DeleteStack: Types.Stack; @@ -319,4 +319,5 @@ export type ExecuteResponses = { ClearRepoCache: Types.Update; BackupCoreDatabase: Types.Update; GlobalAutoUpdate: Types.Update; + RotateAllServerKeys: Types.Update; }; diff --git a/frontend/public/client/types.d.ts b/frontend/public/client/types.d.ts index 9b4f7405c..660e6dfaa 100644 --- a/frontend/public/client/types.d.ts +++ b/frontend/public/client/types.d.ts @@ -457,6 +457,7 @@ export declare enum Operation { ClearRepoCache = "ClearRepoCache", BackupCoreDatabase = "BackupCoreDatabase", GlobalAutoUpdate = "GlobalAutoUpdate", + RotateAllServerKeys = "RotateAllServerKeys", CreateVariable = "CreateVariable", UpdateVariableValue = "UpdateVariableValue", DeleteVariable = "DeleteVariable", @@ -1018,6 +1019,9 @@ export type Execution = } | { type: "GlobalAutoUpdate"; params: GlobalAutoUpdate; +} | { + type: "RotateAllServerKeys"; + params: RotateAllServerKeys; } | { type: "Sleep"; params: Sleep; @@ -2245,12 +2249,6 @@ export interface ServerConfig { * Default: false */ enabled: boolean; - /** - * An optional override private key to use - * to authenticate with Periphery agent. - * If this is empty, will use private key in core config. - */ - core_private_key?: string; /** * The expected public key associated with * private key of the periphery agent. @@ -4194,11 +4192,6 @@ export interface AwsBuilderConfig { security_group_ids?: string[]; /** The user data to deploy the instance with. */ user_data?: string; - /** - * A custom private key to use to authenticate with the Periphery agent. - * Otherwise, use the default Core private key. - */ - core_private_key?: string; /** * An expected public key associated with Periphery private key. * If empty, doesn't validate Periphery public key. @@ -4212,8 +4205,8 @@ export interface AwsBuilderConfig { secrets?: string[]; } /** - * Backs up the Komodo Core database to compressed jsonl files. - * Admin only. Response: [Update] + * **Admin only.** Backs up the Komodo Core database to compressed jsonl files. + * Response: [Update] * * Mount a folder to `/backups`, and Core will use it to create * timestamped database dumps, which can be restored using @@ -4460,7 +4453,7 @@ export interface CancelRepoBuild { repo: string; } /** - * Clears all repos from the Core repo cache. Admin only. + * **Admin only.** Clears all repos from the Core repo cache. * Response: [Update] */ export interface ClearRepoCache { @@ -6393,9 +6386,9 @@ export interface GetVersionResponse { version: string; } /** - * Trigger a global poll for image updates on Stacks and Deployments + * **Admin only.** Trigger a global poll for image updates on Stacks and Deployments * with `poll_for_updates` or `auto_update` enabled. - * Admin only. Response: [Update] + * Response: [Update] * * 1. `docker compose pull` any Stacks / Deployments with `poll_for_updates` or `auto_update` enabled. This will pick up any available updates. * 2. Redeploy Stacks / Deployments that have updates found and 'auto_update' enabled. @@ -7397,10 +7390,16 @@ export interface RestartStack { services?: string[]; } /** - * Rotate the private key on the server. + * **Admin only.** Rotates all connected Server keys. + * Response: [Update] + */ +export interface RotateAllServerKeys { +} +/** + * Rotates the private / public keys for the server. * Response: [NoData] */ -export interface RotateServerPrivateKey { +export interface RotateServerKeys { /** Server Id or name */ server: string; } @@ -8131,11 +8130,6 @@ export interface UpdateVariableValue { export interface UrlBuilderConfig { /** The address of the Periphery agent */ address: string; - /** - * A custom private key to use to authenticate with the Periphery agent. - * Otherwise, use the default Core private key. - */ - core_private_key?: string; /** * An expected public key associated with Periphery private key. * If empty, doesn't validate Periphery public key. @@ -8403,6 +8397,9 @@ export type ExecuteRequest = { } | { type: "GlobalAutoUpdate"; params: GlobalAutoUpdate; +} | { + type: "RotateAllServerKeys"; + params: RotateAllServerKeys; }; /** * One representative IANA zone for each distinct base UTC offset in the tz database. @@ -8989,8 +8986,8 @@ export type WriteRequest = { type: "DeleteAllTerminals"; params: DeleteAllTerminals; } | { - type: "RotateServerPrivateKey"; - params: RotateServerPrivateKey; + type: "RotateServerKeys"; + params: RotateServerKeys; } | { type: "CreateStack"; params: CreateStack; diff --git a/frontend/public/client/types.js b/frontend/public/client/types.js index 8ea00038a..891a20f47 100644 --- a/frontend/public/client/types.js +++ b/frontend/public/client/types.js @@ -171,6 +171,7 @@ export var Operation; Operation["ClearRepoCache"] = "ClearRepoCache"; Operation["BackupCoreDatabase"] = "BackupCoreDatabase"; Operation["GlobalAutoUpdate"] = "GlobalAutoUpdate"; + Operation["RotateAllServerKeys"] = "RotateAllServerKeys"; Operation["CreateVariable"] = "CreateVariable"; Operation["UpdateVariableValue"] = "UpdateVariableValue"; Operation["DeleteVariable"] = "DeleteVariable"; diff --git a/frontend/src/components/resources/builder/config.tsx b/frontend/src/components/resources/builder/config.tsx index eeb9ba783..13e0df3fb 100644 --- a/frontend/src/components/resources/builder/config.tsx +++ b/frontend/src/components/resources/builder/config.tsx @@ -135,12 +135,6 @@ const AwsBuilderConfig = ({ id }: { id: string }) => { label: "Auth", labelHidden: true, components: { - core_private_key: { - label: "Core Private Key", - description: - "Optional. A custom private key used to authenticate Periphery connection. The associated public key must match Periphery 'core_public_key'. If not provided, will use 'private_key' in Core config. Max length of 32 characters.", - placeholder: "custom-private-key", - }, periphery_public_key: { label: "Periphery Public Key", description: @@ -353,12 +347,6 @@ const UrlBuilderConfig = ({ id }: { id: string }) => { label: "Auth", labelHidden: true, components: { - core_private_key: { - label: "Core Private Key", - description: - "Optional. A custom private key used to authenticate Periphery connection. The associated public key must match Periphery 'core_public_key'. If not provided, will use 'private_key' in Core config. Max length of 32 characters.", - placeholder: "custom-private-key", - }, periphery_public_key: { label: "Periphery Public Key", description: diff --git a/frontend/src/components/resources/procedure/config.tsx b/frontend/src/components/resources/procedure/config.tsx index e8deb690a..043634dbc 100644 --- a/frontend/src/components/resources/procedure/config.tsx +++ b/frontend/src/components/resources/procedure/config.tsx @@ -134,7 +134,7 @@ export const ProcedureConfig = ({ id }: { id: string }) => { useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false; const [update, set] = useLocalStorage>( `procedure-${id}-update-v1`, - {}, + {} ); const { mutateAsync } = useWrite("UpdateProcedure"); const { integrations } = useWebhookIntegrations(); @@ -177,7 +177,7 @@ export const ProcedureConfig = ({ id }: { id: string }) => { setStage={(stage) => set({ stages: stages.map((s, i) => - index === i ? stage : s, + index === i ? stage : s ), }) } @@ -557,7 +557,7 @@ const Stage = ({ ].params, }, } as Types.EnabledExecution) - : item, + : item ), }) } @@ -589,7 +589,7 @@ const Stage = ({ ...item, execution: { type, params }, } - : item, + : item ) as Types.EnabledExecution[], }) } @@ -624,7 +624,7 @@ const Stage = ({ setStage({ ...stage, executions: stage.executions!.filter( - (_, i) => i !== index, + (_, i) => i !== index ), }) } @@ -651,7 +651,7 @@ const Stage = ({ setStage({ ...stage, executions: stage.executions!.map((item, i) => - i === index ? { ...item, enabled: !enabled } : item, + i === index ? { ...item, enabled: !enabled } : item ), }) } @@ -676,7 +676,7 @@ const ExecutionTypeSelector = ({ disabled: boolean; }) => { const execution_types = Object.keys(TARGET_COMPONENTS).filter( - (c) => !["None"].includes(c), + (c) => !["None"].includes(c) ); const [open, setOpen] = useState(false); @@ -1681,6 +1681,10 @@ const TARGET_COMPONENTS: ExecutionConfigs = { params: {}, Component: () => <>, }, + RotateAllServerKeys: { + params: {}, + Component: () => <>, + }, SendAlert: { params: { message: "" }, diff --git a/frontend/src/components/resources/server/config.tsx b/frontend/src/components/resources/server/config.tsx index 1eb1b2633..09f6df295 100644 --- a/frontend/src/components/resources/server/config.tsx +++ b/frontend/src/components/resources/server/config.tsx @@ -37,9 +37,8 @@ export const ServerConfig = ({ invalidate(["ListAlerts"]); }, }); - const { mutate: rotate, isPending: rotatePending } = useWrite( - "RotateServerPrivateKey" - ); + const { mutate: rotate, isPending: rotatePending } = + useWrite("RotateServerKeys"); if (!config) return null; @@ -71,12 +70,6 @@ export const ServerConfig = ({ label: "Auth", labelHidden: true, components: { - core_private_key: { - label: "Core Private Key", - description: - "Optional. A custom private key used to authenticate Periphery connection. The associated public key must match Periphery 'core_public_key'. If not provided, will use 'private_key' in Core config. Max length of 32 characters.", - placeholder: "custom-private-key", - }, periphery_public_key: (public_key, set) => ( , ) -> anyhow::Result { let path = path.as_ref(); - // Ensure the parent directory exists - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!( - "Failed to create private key parent directory {parent:?}" - ) - })?; - } // Generate and write pems to path let keys = EncodedKeyPair::generate()?; - keys.private.write_pem(&path)?; + keys.private.write_pem(path)?; keys.public.write_pem(path.with_extension("pub"))?; Ok(keys) } diff --git a/lib/noise/src/key/private.rs b/lib/noise/src/key/private.rs index df7a51157..8ebe06828 100644 --- a/lib/noise/src/key/private.rs +++ b/lib/noise/src/key/private.rs @@ -47,6 +47,14 @@ impl Pkcs8PrivateKey { path: P, ) -> anyhow::Result<()> { let path = path.as_ref(); + // Ensure the parent directory exists + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!( + "Failed to create private key parent directory {parent:?}" + ) + })?; + } tracing::info!("Writing private key to {path:?}"); std::fs::write(path, self.as_pem()).with_context(|| { format!("Failed to write private key pem to {path:?}") diff --git a/lib/noise/src/key/public.rs b/lib/noise/src/key/public.rs index 2f6eec39c..310beebde 100644 --- a/lib/noise/src/key/public.rs +++ b/lib/noise/src/key/public.rs @@ -48,6 +48,14 @@ impl SpkiPublicKey { path: P, ) -> anyhow::Result<()> { let path = path.as_ref(); + // Ensure the parent directory exists + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!( + "Failed to create private key parent directory {parent:?}" + ) + })?; + } tracing::info!("Writing public key to {path:?}"); std::fs::write(path, self.as_pem()).with_context(|| { format!("Failed to write private key pem to {path:?}")