feat: Add SSH authentication interceptor and update proto definitions
This commit is contained in:
@@ -10,6 +10,18 @@ rust-version.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tonic.workspace = true
|
tonic.workspace = true
|
||||||
prost.workspace = true
|
prost.workspace = true
|
||||||
|
tonic-prost.workspace = true
|
||||||
|
tonic-async-interceptor = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
# allow user to specify tonic server or client
|
||||||
|
[features]
|
||||||
|
default = ["server", "client"]
|
||||||
|
server = [
|
||||||
|
"tonic/server",
|
||||||
|
"tonic/tls-native-roots",
|
||||||
|
"dep:tonic-async-interceptor",
|
||||||
|
]
|
||||||
|
client = []
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-prost-build.workspace = true
|
tonic-prost-build.workspace = true
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package nxmesh.agent.v1;
|
|||||||
|
|
||||||
option go_package = "github.com/nxmesh/api/agent/v1";
|
option go_package = "github.com/nxmesh/api/agent/v1";
|
||||||
|
|
||||||
|
// For all file paths in this proto, we use forward slashes ("/") as the separator, even on Windows. This is because gRPC and protobuf are designed to be cross-platform and forward slashes are universally accepted as path separators in URLs and many programming languages. Using forward slashes ensures consistency and avoids issues with escaping backslashes on different platforms.
|
||||||
|
// All file paths MUST be relative paths from other config files, e.g. "site.conf", "private/example.com.conf". Absolute paths or path traversal above the config directory should be rejected by the agent for security reasons. The config files must live within the generated config directory, e.g. "/etc/nginx/conf-<timestamp>/site.conf". This allows the agent to manage the lifecycle of config files, e.g. cleanup old configs after successful apply.
|
||||||
|
|
||||||
// AgentService defines the bidirectional communication between master and agents
|
// AgentService defines the bidirectional communication between master and agents
|
||||||
service AgentService {
|
service AgentService {
|
||||||
// Stream establishes a persistent connection for real-time communication
|
// Stream establishes a persistent connection for real-time communication
|
||||||
@@ -43,13 +46,12 @@ message MasterMessage {
|
|||||||
|
|
||||||
// Registration
|
// Registration
|
||||||
message RegistrationRequest {
|
message RegistrationRequest {
|
||||||
string token = 1;
|
string hostname = 1;
|
||||||
string hostname = 2;
|
string ip_address = 2;
|
||||||
string ip_address = 3;
|
string version = 3;
|
||||||
string version = 4;
|
repeated string capabilities = 4;
|
||||||
repeated string capabilities = 5;
|
map<string, string> labels = 5;
|
||||||
map<string, string> labels = 6;
|
DeploymentMode deployment_mode = 6;
|
||||||
DeploymentMode deployment_mode = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message RegistrationResponse {
|
message RegistrationResponse {
|
||||||
@@ -104,94 +106,22 @@ message Alert {
|
|||||||
message ConfigUpdate {
|
message ConfigUpdate {
|
||||||
string config_id = 1;
|
string config_id = 1;
|
||||||
int64 version = 2;
|
int64 version = 2;
|
||||||
repeated VirtualHost virtual_hosts = 3;
|
repeated ConfigContent configs = 3;
|
||||||
repeated Upstream upstreams = 4;
|
repeated CertificateContent certificates = 4;
|
||||||
map<string, Certificate> certificates = 5;
|
|
||||||
GlobalSettings global_settings = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message VirtualHost {
|
message ConfigContent {
|
||||||
string id = 1;
|
// relative path from other config files, e.g. "site.conf", "private/example.com.conf"
|
||||||
string name = 2;
|
|
||||||
string server_name = 3;
|
|
||||||
uint32 listen_port = 4;
|
|
||||||
bool ssl_enabled = 5;
|
|
||||||
string ssl_certificate_id = 6;
|
|
||||||
bool http2_enabled = 7;
|
|
||||||
bool http3_enabled = 8;
|
|
||||||
repeated Location locations = 9;
|
|
||||||
map<string, string> custom_directives = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Location {
|
|
||||||
string path = 1;
|
string path = 1;
|
||||||
string proxy_pass = 2;
|
string content = 2;
|
||||||
string upstream_id = 3;
|
|
||||||
string root = 4;
|
|
||||||
string index = 5;
|
|
||||||
repeated Header custom_headers = 6;
|
|
||||||
repeated RewriteRule rewrite_rules = 7;
|
|
||||||
map<string, string> custom_directives = 8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Header {
|
message CertificateContent {
|
||||||
string name = 1;
|
|
||||||
string value = 2;
|
|
||||||
bool always = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RewriteRule {
|
|
||||||
string pattern = 1;
|
|
||||||
string replacement = 2;
|
|
||||||
string flag = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Upstream {
|
|
||||||
string id = 1;
|
string id = 1;
|
||||||
string name = 2;
|
// relative path from other config files, e.g. "certs/example.com.pem"
|
||||||
LoadBalanceAlgorithm algorithm = 3;
|
|
||||||
repeated UpstreamServer servers = 4;
|
|
||||||
HealthCheckConfig health_check = 5;
|
|
||||||
uint32 keepalive_connections = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum LoadBalanceAlgorithm {
|
|
||||||
LOAD_BALANCE_ALGORITHM_UNSPECIFIED = 0;
|
|
||||||
ROUND_ROBIN = 1;
|
|
||||||
LEAST_CONNECTIONS = 2;
|
|
||||||
IP_HASH = 3;
|
|
||||||
WEIGHTED_ROUND_ROBIN = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UpstreamServer {
|
|
||||||
string address = 1;
|
|
||||||
uint32 weight = 2;
|
|
||||||
bool backup = 3;
|
|
||||||
bool down = 4;
|
|
||||||
uint32 max_fails = 5;
|
|
||||||
uint32 fail_timeout_seconds = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message HealthCheckConfig {
|
|
||||||
bool enabled = 1;
|
|
||||||
string path = 2;
|
string path = 2;
|
||||||
uint32 interval_seconds = 3;
|
|
||||||
uint32 timeout_seconds = 4;
|
|
||||||
uint32 healthy_threshold = 5;
|
|
||||||
uint32 unhealthy_threshold = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Certificate {
|
|
||||||
string id = 1;
|
|
||||||
string domain = 2;
|
|
||||||
string certificate_pem = 3;
|
string certificate_pem = 3;
|
||||||
string private_key_pem = 4;
|
string private_key_pem = 4;
|
||||||
int64 expires_at = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GlobalSettings {
|
|
||||||
map<string, string> nginx_directives = 1;
|
|
||||||
map<string, string> env_vars = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ConfigStatus {
|
message ConfigStatus {
|
||||||
|
|||||||
1
crates/nxmesh-proto/src/auth/mod.rs
Normal file
1
crates/nxmesh-proto/src/auth/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod ssh_auth;
|
||||||
49
crates/nxmesh-proto/src/auth/ssh_auth.rs
Normal file
49
crates/nxmesh-proto/src/auth/ssh_auth.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tonic::{Request, Status, async_trait, transport::CertificateDer};
|
||||||
|
use tonic_async_interceptor::{AsyncInterceptor, AsyncInterceptorLayer, async_interceptor};
|
||||||
|
|
||||||
|
pub fn create_ssh_auth_interceptor(
|
||||||
|
certificate_provider: Arc<dyn CertificateValidationProvider>,
|
||||||
|
) -> AsyncInterceptorLayer<SshAuthInterceptor> {
|
||||||
|
async_interceptor(SshAuthInterceptor::new(certificate_provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SshAuthInterceptor {
|
||||||
|
certificate_provider: Arc<dyn CertificateValidationProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait CertificateValidationProvider: Send + Sync {
|
||||||
|
async fn is_authorized(&self, certs: &Arc<Vec<CertificateDer<'_>>>) -> Result<bool, Status>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncInterceptor for SshAuthInterceptor {
|
||||||
|
type Future =
|
||||||
|
std::pin::Pin<Box<dyn std::future::Future<Output = Result<Request<()>, Status>> + Send>>;
|
||||||
|
fn call(&mut self, req: Request<()>) -> Self::Future {
|
||||||
|
let this = self.clone();
|
||||||
|
Box::pin(async move { this.authenticate(req).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshAuthInterceptor {
|
||||||
|
pub fn new(certificate_provider: Arc<dyn CertificateValidationProvider>) -> Self {
|
||||||
|
SshAuthInterceptor {
|
||||||
|
certificate_provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticate(&self, req: Request<()>) -> Result<Request<()>, Status> {
|
||||||
|
let certs = req.peer_certs().ok_or(Status::unauthenticated("No cert"))?;
|
||||||
|
|
||||||
|
let is_authorized = self.certificate_provider.is_authorized(&certs).await?;
|
||||||
|
|
||||||
|
if is_authorized {
|
||||||
|
Ok(req)
|
||||||
|
} else {
|
||||||
|
Err(Status::permission_denied("Blocked"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
//! NxMesh Protocol Buffers
|
//! NxMesh Protocol Buffers
|
||||||
//!
|
//!
|
||||||
//! This crate contains the gRPC protocol definitions for master-agent communication.
|
//! This crate contains the gRPC protocol definitions for master-agent communication.
|
||||||
|
#![forbid(clippy::unwrap_used, clippy::panic, unsafe_code)]
|
||||||
|
#![deny(clippy::expect_used)]
|
||||||
|
|
||||||
pub mod agent {
|
pub mod agent {
|
||||||
tonic::include_proto!("nxmesh.agent.v1");
|
tonic::include_proto!("nxmesh.agent.v1");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use agent::*;
|
pub use agent::*;
|
||||||
|
pub mod auth;
|
||||||
|
pub use tonic_async_interceptor::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user