set default allowed periphery public key

This commit is contained in:
mbecker20
2025-09-21 15:26:18 -07:00
parent 25da97ac1a
commit 52453d1320
20 changed files with 85 additions and 34 deletions

1
Cargo.lock generated
View File

@@ -2971,6 +2971,7 @@ dependencies = [
"interpolate",
"komodo_client",
"logger",
"noise",
"periphery_client",
"portable-pty",
"resolver_api",

View File

@@ -16,7 +16,7 @@ pub async fn handle(command: &KeyCommand) -> anyhow::Result<()> {
Ok(())
}
KeyCommand::Compute { private_key } => {
let public_key = noise::compute_public_key(&private_key)
let public_key = noise::compute_public_key(private_key)
.context("Failed to compute public key")?;
println!("\nPublic Key: {}", public_key.green().bold());
Ok(())

View File

@@ -7,6 +7,7 @@ use database::{
mongo_indexed::doc, mungos::mongodb::bson::oid::ObjectId,
};
use formatting::format_serror;
use komodo_client::entities::optional_string;
use komodo_client::{
api::write::*,
entities::{
@@ -442,7 +443,7 @@ async fn get_on_host_periphery(
} else {
config.private_key
},
config.public_key,
optional_string(config.public_key),
)
.await?;
// Poll for connection to be estalished

View File

@@ -180,6 +180,7 @@ pub fn core_config() -> &'static CoreConfig {
},
// Non secrets
periphery_public_key: env.komodo_periphery_public_key.or(config.periphery_public_key),
title: env.komodo_title.unwrap_or(config.title),
host: env.komodo_host.unwrap_or(config.host),
port: env.komodo_port.unwrap_or(config.port),

View File

@@ -6,7 +6,7 @@ use formatting::muted;
use komodo_client::entities::{
Version,
builder::{AwsBuilderConfig, Builder, BuilderConfig},
komodo_timestamp,
komodo_timestamp, optional_string,
server::Server,
update::{Log, Update},
};
@@ -59,7 +59,7 @@ pub async fn get_builder_periphery(
} else {
config.private_key
},
config.public_key,
optional_string(config.public_key),
)
.await?;
periphery
@@ -125,7 +125,7 @@ async fn get_aws_builder(
} else {
config.private_key
},
config.public_key,
optional_string(config.public_key),
)
.await?;

View File

@@ -48,7 +48,7 @@ async fn app() -> anyhow::Result<()> {
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
// Init public key to crash on failure
info!("Public Key: {}", core_public_key());
info!("Core Public Key: {}", core_public_key());
match (
config.pretty_startup_config,

View File

@@ -20,6 +20,7 @@ pub async fn handler(
periphery_client::connection::server::handler(
server,
core_config().private_key.clone(),
core_config().periphery_public_key.clone(),
headers,
query,
ws,

View File

@@ -27,6 +27,7 @@ command.workspace = true
config.workspace = true
logger.workspace = true
cache.workspace = true
noise.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }

View File

@@ -11,6 +11,15 @@ use komodo_client::entities::{
logger::{LogConfig, LogLevel},
};
/// Should call in startup to ensure Periphery errors without valid private key.
pub fn periphery_public_key() -> &'static String {
static PERIPHERY_PUBLIC_KEY: OnceLock<String> = OnceLock::new();
PERIPHERY_PUBLIC_KEY.get_or_init(|| {
noise::compute_public_key(&periphery_config().private_key)
.unwrap()
})
}
pub fn periphery_config() -> &'static PeripheryConfig {
static PERIPHERY_CONFIG: OnceLock<PeripheryConfig> =
OnceLock::new();

View File

@@ -81,7 +81,6 @@ async fn handler(
};
if let Err(e) = handler.handle::<ServerLoginFlow>().await {
warn!("Core failed to login to connection | {e:#}");
return;
}
}))
}

View File

@@ -1,5 +1,7 @@
use anyhow::anyhow;
use crate::config::periphery_public_key;
#[macro_use]
extern crate tracing;
@@ -17,6 +19,8 @@ async fn app() -> anyhow::Result<()> {
logger::init(&config.logging)?;
info!("Komodo Periphery version: v{}", env!("CARGO_PKG_VERSION"));
// Init public key to crash on failure
info!("Periphery Public Key: {}", periphery_public_key());
if config.pretty_startup_config {
info!("{:#?}", config.sanitized());

View File

@@ -75,6 +75,8 @@ pub struct Env {
pub komodo_private_key: Option<String>,
/// Override `private_key` with file
pub komodo_private_key_file: Option<PathBuf>,
/// Override `periphery_public_key`
pub komodo_periphery_public_key: Option<String>,
/// Override `timezone`
#[serde(alias = "tz", alias = "TZ")]
pub komodo_timezone: Option<String>,
@@ -317,11 +319,20 @@ pub struct CoreConfig {
#[serde(default)]
pub internet_interface: String,
/// Default private key to use with Noise handshake
/// with Periphery agents.
/// Default private key to use with Noise handshake to authenticate with Periphery agents.
/// If not provided, will use random default.
/// Note. The private key can be overridden on individual Server / Builds
#[serde(default = "default_private_key")]
pub private_key: String,
/// 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 Server / Builds
#[serde(skip_serializing_if = "Option::is_none")]
pub periphery_public_key: Option<String>,
/// A TZ Identifier. If not provided, will use Core local timezone.
/// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
/// This will be populated by TZ env variable in addition to KOMODO_TIMEZONE.
@@ -703,6 +714,7 @@ impl Default for CoreConfig {
bind_ip: default_core_bind_ip(),
internet_interface: Default::default(),
private_key: default_private_key(),
periphery_public_key: Default::default(),
timezone: Default::default(),
ui_write_disabled: Default::default(),
disable_confirm_dialog: Default::default(),
@@ -765,6 +777,7 @@ impl CoreConfig {
port: config.port,
bind_ip: config.bind_ip,
private_key: empty_or_redacted(&config.private_key),
periphery_public_key: config.periphery_public_key,
timezone: config.timezone,
first_server: config.first_server,
first_server_name: config.first_server_name,

View File

@@ -195,20 +195,24 @@ pub struct Env {
#[derive(Debug, Clone, Deserialize)]
pub struct PeripheryConfig {
/// The private key used with noise handshake.
/// If not provided, will generate random default.
#[serde(default = "default_private_key")]
pub private_key: String,
/// Optionally pin a specific Core public key
/// for additional trust.
#[serde(skip_serializing_if = "Option::is_none")]
pub core_public_key: Option<String>,
// ============================
// = OUTBOUND CONNECTION MODE =
// ============================
/// Address of Komodo Core when connecting outbound
#[serde(skip_serializing_if = "Option::is_none")]
pub core_host: Option<String>,
/// Server name / id to connect as
/// TODO: explore using device identifier like MAC
#[serde(skip_serializing_if = "Option::is_none")]
pub connect_as: Option<String>,
// ===========================
@@ -245,10 +249,12 @@ pub struct PeripheryConfig {
/// Path to the ssl key.
/// Default: `${root_directory}/ssl/key.pem`.
#[serde(skip_serializing_if = "Option::is_none")]
pub ssl_key_file: Option<PathBuf>,
/// Path to the ssl cert.
/// Default: `${root_directory}/ssl/cert.pem`.
#[serde(skip_serializing_if = "Option::is_none")]
pub ssl_cert_file: Option<PathBuf>,
// ==================
@@ -270,16 +276,19 @@ pub struct PeripheryConfig {
/// The system directory where Komodo managed repos will be cloned.
/// If not provided, will default to `${root_directory}/repos`.
/// Default: empty
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_dir: Option<PathBuf>,
/// The system directory where stacks will managed.
/// If not provided, will default to `${root_directory}/stacks`.
/// Default: empty
#[serde(skip_serializing_if = "Option::is_none")]
pub stack_dir: Option<PathBuf>,
/// The system directory where builds will managed.
/// If not provided, will default to `${root_directory}/builds`.
/// Default: empty
#[serde(skip_serializing_if = "Option::is_none")]
pub build_dir: Option<PathBuf>,
/// Whether to disable the create terminal

View File

@@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
use anyhow::Context;
use axum::http::HeaderValue;
use komodo_client::entities::server::Server;
use komodo_client::entities::{optional_string, server::Server};
use rustls::{ClientConfig, client::danger::ServerCertVerifier};
use tokio_tungstenite::Connector;
use tracing::{info, warn};
@@ -102,7 +102,7 @@ pub async fn manage_client_connections(
} else {
private_key.clone()
},
expected_public_key.clone(),
optional_string(expected_public_key),
)
.await
{
@@ -118,7 +118,7 @@ pub async fn spawn_client_connection(
server_id: String,
address: String,
private_key: String,
expected_public_key: String,
expected_public_key: Option<String>,
) -> anyhow::Result<()> {
let url = ::url::Url::parse(&address)
.context("Failed to parse server address")?;
@@ -168,7 +168,7 @@ pub async fn spawn_client_connection(
query: &[],
},
private_key: &private_key,
expected_public_key: &expected_public_key,
expected_public_key: expected_public_key.as_deref(),
write_receiver: &mut write_receiver,
connection: &connection,
handler: &handler,

View File

@@ -33,7 +33,7 @@ pub struct WebsocketHandler<'a, W> {
pub socket: W,
pub connection_identifiers: ConnectionIdentifiers<'a>,
pub private_key: &'a str,
pub expected_public_key: &'a str,
pub expected_public_key: Option<&'a str>,
pub write_receiver: &'a mut BufferedReceiver<Bytes>,
pub connection: &'a PeripheryConnection,
pub handler: &'a MessageHandler,
@@ -133,14 +133,17 @@ impl<W: Websocket> WebsocketHandler<'_, W> {
}
pub struct PeripheryPublicKeyValidator<'a> {
pub expected: &'a str,
/// If None, ignore public key.
pub expected: Option<&'a str>,
}
impl PublicKeyValidator for PeripheryPublicKeyValidator<'_> {
fn validate(&self, public_key: String) -> anyhow::Result<()> {
if self.expected.is_empty() || self.expected == public_key {
Ok(())
} else {
if let Some(expected) = self.expected
&& public_key != expected
{
Err(anyhow!("Public key does not match expected"))
} else {
Ok(())
}
}
}

View File

@@ -1,3 +1,4 @@
use anyhow::Context;
use axum::{
extract::WebSocketUpgrade,
http::{HeaderMap, StatusCode},
@@ -18,6 +19,7 @@ use crate::connection::{
pub async fn handler(
server: Server,
default_private_key: String,
default_public_key: Option<String>,
mut headers: HeaderMap,
query: String,
ws: WebSocketUpgrade,
@@ -25,6 +27,12 @@ pub async fn handler(
let identifiers = ServerHeaderIdentifiers::extract(&mut headers)
.status_code(StatusCode::UNAUTHORIZED)?;
let expected_public_key = if server.config.public_key.is_empty() {
default_public_key.context("Must either configure Server 'Periphery Public Key' or set KOMODO_PERIPHERY_PUBLIC_KEY")?
} else {
server.config.public_key
};
let handler = MessageHandler::new(&server.id).await;
let (connection, mut write_receiver) =
@@ -47,7 +55,7 @@ pub async fn handler(
} else {
&server.config.private_key
},
expected_public_key: &server.config.public_key,
expected_public_key: Some(&expected_public_key),
write_receiver: &mut write_receiver,
connection: &connection,
handler: &handler,
@@ -59,7 +67,6 @@ pub async fn handler(
server.name
);
connection.set_error(e).await;
return;
}
}))
}

View File

@@ -51,7 +51,7 @@ impl PeripheryClient {
server_id: String,
address: &str,
private_key: String,
expected_public_key: String,
expected_public_key: Option<String>,
) -> anyhow::Result<PeripheryClient> {
if address.is_empty() {
return Err(anyhow!("Server address cannot be empty"));

View File

@@ -187,10 +187,12 @@ impl Stream for ReceiverStream {
mut self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.receiver.poll_recv(cx).map(|bytes| {
bytes.map(|bytes| data_from_transport_bytes(bytes))
}) {
Poll::Ready(Some(Ok(bytes))) if &bytes == END_OF_OUTPUT => {
match self
.receiver
.poll_recv(cx)
.map(|bytes| bytes.map(data_from_transport_bytes))
{
Poll::Ready(Some(Ok(bytes))) if bytes == END_OF_OUTPUT => {
self.cleanup();
Poll::Ready(None)
}

View File

@@ -136,13 +136,13 @@ const AwsBuilderConfig = ({ id }: { id: string }) => {
labelHidden: true,
components: {
public_key: {
label: "Inbound Public Key",
label: "Periphery Public Key",
description:
"If provided, the associated private key must be set as Periphery 'private_key'. For Periphery -> Core connection, this is required for Periphery to be able to connect.",
"If provided, the associated private key must be set as Periphery 'private_key'. For Periphery -> Core connection, either this or using 'periphery_public_key' in Core config is required for Periphery to be able to connect.",
placeholder: "custom-public-key",
},
private_key: {
label: "Outbound 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",
@@ -354,13 +354,13 @@ const UrlBuilderConfig = ({ id }: { id: string }) => {
labelHidden: true,
components: {
public_key: {
label: "Inbound Public Key",
label: "Periphery Public Key",
description:
"If provided, the associated private key must be set as Periphery 'private_key'. For Periphery -> Core connection, this is required for Periphery to be able to connect.",
"If provided, the associated private key must be set as Periphery 'private_key'. For Periphery -> Core connection, either this or using 'periphery_public_key' in Core config is required for Periphery to be able to connect.",
placeholder: "custom-public-key",
},
private_key: {
label: "Outbound 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",

View File

@@ -64,13 +64,13 @@ export const ServerConfig = ({
labelHidden: true,
components: {
public_key: {
label: "Inbound Public Key",
label: "Periphery Public Key",
description:
"If provided, the associated private key must be set as Periphery 'private_key'. For Periphery -> Core connection, this is required for Periphery to be able to connect.",
"If provided, the associated private key must be set as Periphery 'private_key'. For Periphery -> Core connection, either this or using 'periphery_public_key' in Core config is required for Periphery to be able to connect.",
placeholder: "custom-public-key",
},
private_key: {
label: "Outbound 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",