mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-02 09:09:17 +00:00
feat(credentials): manage declarative credentials through TOML (#2515)
* feat(credentials): manage declarative credentials through TOML Make managed credentials part of the canonical TOML configuration and load them before peers can authenticate. Reuse ConfigRpc hot patches to durably replace the configured credential set without restarting the instance. Serialize credential mutations so base, managed, and ephemeral keys cannot race into conflicts. Remove the managed overlay file format, digest protocol, capability negotiation, force reconciliation, and database CAS machinery. Redact credential secrets from debug output and management events. Write credential-bearing files atomically with private permissions. * fix(core): release JoinSet reapers with their owners Pass weak task-set references into background reapers so they cannot retain the JoinSet they are meant to collect. This lets stale smoltcp bridge tasks terminate when an IPv4 generation is replaced. Add ownership and TCP generation-replacement regressions covering the production port-forward failure.
This commit is contained in:
@@ -15,7 +15,8 @@ use easytier::{
|
||||
},
|
||||
instance::{InstanceIdentifier, instance_identifier},
|
||||
manage::{
|
||||
ConfigSource as RpcConfigSource, GetNetworkInstanceConfigRequest, NetworkConfig,
|
||||
ConfigSource as RpcConfigSource, GetNetworkInstanceConfigRequest,
|
||||
ManagedCredentialConfig, ManagedCredentialSet, NetworkConfig,
|
||||
RunNetworkInstanceRequest,
|
||||
},
|
||||
},
|
||||
@@ -66,6 +67,7 @@ fn hot_patch_base(config: &NetworkConfig) -> anyhow::Result<NetworkConfig> {
|
||||
// VPN portal clients are diffed separately; the listener identity
|
||||
// (address and private key) decides between patch and recreate.
|
||||
config.vpn_portal_config = None;
|
||||
config.managed_credentials.clear();
|
||||
if config.dhcp.unwrap_or_default() {
|
||||
config.virtual_ipv4 = None;
|
||||
config.network_length = None;
|
||||
@@ -256,6 +258,12 @@ fn client_name_only(name: &str) -> easytier::proto::api::manage::VpnPortalClient
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_managed_credentials(
|
||||
config: &NetworkConfig,
|
||||
) -> anyhow::Result<Vec<ManagedCredentialConfig>> {
|
||||
Ok(NetworkConfig::new_from_config(config.gen_config()?)?.managed_credentials)
|
||||
}
|
||||
|
||||
fn web_source_runtime_patch(
|
||||
current: &NetworkConfig,
|
||||
desired: &NetworkConfig,
|
||||
@@ -328,6 +336,13 @@ fn web_source_runtime_patch(
|
||||
(Some(_), None) | (None, Some(_)) => return Ok(None),
|
||||
(None, None) => {}
|
||||
}
|
||||
let current_managed_credentials = normalized_managed_credentials(current)?;
|
||||
let desired_managed_credentials = normalized_managed_credentials(desired)?;
|
||||
if current_managed_credentials != desired_managed_credentials {
|
||||
patch.managed_credentials = Some(ManagedCredentialSet {
|
||||
entries: desired_managed_credentials,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some(patch))
|
||||
}
|
||||
@@ -339,7 +354,7 @@ fn ensure_runtime_config_converged(
|
||||
let patch = web_source_runtime_patch(current, desired)?;
|
||||
match patch {
|
||||
Some(patch) if patch == InstanceConfigPatch::default() => Ok(()),
|
||||
Some(patch) => anyhow::bail!("runtime config still needs patch after reconcile: {patch:?}"),
|
||||
Some(_) => anyhow::bail!("runtime config still needs patch after reconcile"),
|
||||
None => anyhow::bail!("runtime config still needs full overwrite after reconcile"),
|
||||
}
|
||||
}
|
||||
@@ -771,6 +786,27 @@ mod tests {
|
||||
assert!(patch.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_patch_replaces_managed_credentials_without_full_run() {
|
||||
let current = config_with_port_forwards(Vec::new());
|
||||
let mut desired = current.clone();
|
||||
desired.managed_credentials = vec![ManagedCredentialConfig {
|
||||
credential_id: "managed".to_owned(),
|
||||
credential_secret: "credential-secret".to_owned(),
|
||||
expiry_unix: 2_000_000_000,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let action = prepare_web_source_runtime_reconcile_from_current(¤t, desired)
|
||||
.expect("prepare reconcile");
|
||||
let RuntimeReconcileAction::Patch(patch) = action else {
|
||||
panic!("managed credential change must use a hot patch");
|
||||
};
|
||||
let managed = patch.managed_credentials.expect("managed credential patch");
|
||||
assert_eq!(managed.entries.len(), 1);
|
||||
assert_eq!(managed.entries[0].credential_id, "managed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_patch_rejects_routes_change() {
|
||||
let mut current = config_with_port_forwards(Vec::new());
|
||||
|
||||
@@ -11,6 +11,8 @@ use sea_orm::{
|
||||
};
|
||||
use sea_orm_migration::MigratorTrait as _;
|
||||
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _, types::chrono};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::migrator;
|
||||
@@ -18,6 +20,35 @@ use async_trait::async_trait;
|
||||
|
||||
pub type UserIdInDb = i32;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn restrict_database_file_permissions(db_path: &str) -> anyhow::Result<()> {
|
||||
if db_path.ends_with(":memory:") || db_path.contains("mode=memory") {
|
||||
return Ok(());
|
||||
}
|
||||
let path = db_path
|
||||
.strip_prefix("sqlite://")
|
||||
.or_else(|| db_path.strip_prefix("sqlite:"))
|
||||
.unwrap_or(db_path);
|
||||
let path = path
|
||||
.strip_prefix("file:")
|
||||
.unwrap_or(path)
|
||||
.split('?')
|
||||
.next()
|
||||
.filter(|path| !path.is_empty());
|
||||
let Some(path) = path else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut permissions = std::fs::metadata(path)?.permissions();
|
||||
permissions.set_mode(0o600);
|
||||
std::fs::set_permissions(path, permissions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn restrict_database_file_permissions(_db_path: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Db {
|
||||
db_path: String,
|
||||
@@ -48,6 +79,7 @@ impl Db {
|
||||
tracing::info!("Database not found, creating a new one");
|
||||
Sqlite::create_database(db_path).await?;
|
||||
}
|
||||
restrict_database_file_permissions(db_path)?;
|
||||
|
||||
let db = sqlx::pool::PoolOptions::new()
|
||||
.max_lifetime(None)
|
||||
|
||||
Reference in New Issue
Block a user