Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
572 changes: 535 additions & 37 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [
"crates/rginx-core",
"crates/rginx-http",
"crates/rginx-observability",
"crates/rginx-runtime",
"crates/rginx-runtime", "crates/rginx-sdk",
]
default-members = ["crates/rginx-app"]
resolver = "2"
Expand Down
44 changes: 44 additions & 0 deletions configs/control-plane-api-keys.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"keys": [
{
"id": "admin-key-001",
"secret": "sk_live_admin_secret_key_change_me",
"scopes": [
"runtime.read",
"runtime.reload",
"config.write",
"cache.write",
"metrics.read"
],
"created_at": 1704067200000,
"expires_at": 1735689600000,
Comment thread
vansour marked this conversation as resolved.
"allowed_ips": [
"10.0.0.0/8",
"192.168.0.0/16"
]
},
{
"id": "readonly-key-001",
"secret": "sk_live_readonly_secret_key_change_me",
"scopes": [
"runtime.read",
"metrics.read"
],
"created_at": 1704067200000,
"expires_at": null,
"allowed_ips": []
},
{
"id": "monitoring-key-001",
"secret": "sk_live_monitoring_secret_key_change_me",
"scopes": [
"metrics.read"
],
"created_at": 1704067200000,
"expires_at": 1767225600000,
"allowed_ips": [
"10.100.0.0/16"
]
}
]
}
44 changes: 44 additions & 0 deletions configs/control-plane-mtls.example.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Example configuration for mTLS client certificate authentication
// This enables mutual TLS authentication for the control plane

Config(
control_plane: Some(ControlPlane(
enabled: Some(true),
listen: Some("0.0.0.0:9443"),

tls: Some(ControlPlaneTls(
// Server certificate and key
cert_path: "/etc/rginx/control-plane.crt",
key_path: "/etc/rginx/control-plane.key",

// Client CA certificate for verifying client certificates
client_ca_path: Some("/etc/rginx/client-ca.crt"),

// Whether to require client certificates (true) or make them optional (false)
// - true: All clients MUST present a valid certificate
// - false: Clients MAY present a certificate, but can also use API keys
require_client_cert: Some(false),
)),

// API keys file (still used when client cert is not provided)
api_keys_path: Some("/etc/rginx/control-plane-api-keys.json"),

// IP whitelist (optional)
allowed_cidrs: [
"10.0.0.0/8",
"192.168.0.0/16",
],

// Node identity
node_id: Some("edge-node-001"),
region: Some("us-west-2"),
pop: Some("sfo1"),

labels: {
"env": "production",
"tier": "edge",
},
)),

// ... rest of your configuration ...
)
8 changes: 7 additions & 1 deletion crates/rginx-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ rginx-config = { path = "../rginx-config" }
rginx-http = { path = "../rginx-http" }
rginx-core = { path = "../rginx-core" }
bytes.workspace = true
futures-util = "0.3"
hex = "0.4"
http.workspace = true
http-body-util.workspace = true
hyper.workspace = true
Expand All @@ -28,9 +30,13 @@ serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["io-util", "net", "time"] }
tokio = { workspace = true, features = ["io-util", "net", "time", "fs"] }
tokio-rustls.workspace = true
tokio-tungstenite = "0.29"
tracing.workspace = true
tungstenite = "0.29"
prometheus = "0.14"
lazy_static = "1.5"

[dev-dependencies]
hyper-rustls.workspace = true
Expand Down
127 changes: 127 additions & 0 deletions crates/rginx-agent/src/audit.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::net::SocketAddr;
use std::time::{SystemTime, UNIX_EPOCH};

use http::Method;
use serde::Serialize;

use crate::auth::{AuthorizationRequirement, ControlPlaneIdentity};
use crate::error::Error;
Expand All @@ -14,11 +16,70 @@ pub(crate) struct AuditContext<'a> {
pub(crate) requirement: AuthorizationRequirement,
}

#[derive(Debug, Serialize)]
pub struct AuditLog {
pub timestamp: u64,
pub event: &'static str,
pub outcome: AuditOutcome,
pub request_id: Option<String>,

// Authentication info
pub actor_id: Option<String>,
pub auth_method: Option<String>,
pub scopes: Vec<String>,

// Request info
pub method: String,
pub path: String,
pub peer_addr: String,
pub user_agent: Option<String>,

// Resource info
pub resource: Option<String>,
pub requirement: String,

// Response info
pub status: Option<u16>,
pub duration_ms: Option<u64>,
pub error: Option<String>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AuditOutcome {
Allow,
Deny,
Error,
}

fn current_timestamp_ms() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
}
Comment thread
vansour marked this conversation as resolved.

pub(crate) fn log_allow(
context: &AuditContext<'_>,
identity: &ControlPlaneIdentity<'_>,
resource: ControlPlaneResource,
) {
let audit_log = AuditLog {
timestamp: current_timestamp_ms(),
event: "control_plane_audit",
outcome: AuditOutcome::Allow,
request_id: None,
actor_id: Some(identity.actor_id.to_string()),
auth_method: Some("api_key".to_string()),
scopes: identity.scope_labels.clone(),
Comment thread
vansour marked this conversation as resolved.
method: context.method.to_string(),
path: context.path.to_string(),
peer_addr: context.peer_addr.to_string(),
user_agent: None,
resource: Some(resource.label().to_string()),
requirement: context.requirement.label().to_string(),
status: None,
duration_ms: None,
error: None,
};

tracing::info!(
event = "control_plane_audit",
outcome = "allow",
Expand All @@ -31,6 +92,9 @@ pub(crate) fn log_allow(
requirement = %context.requirement.label(),
"control plane request authorized"
);

// Optionally write to audit log file
write_audit_log(&audit_log);
}

pub(crate) fn log_deny(
Expand All @@ -39,6 +103,25 @@ pub(crate) fn log_deny(
scopes: &[String],
error: &Error,
) {
let audit_log = AuditLog {
timestamp: current_timestamp_ms(),
event: "control_plane_audit",
outcome: AuditOutcome::Deny,
request_id: None,
actor_id: actor_id.map(|s| s.to_string()),
auth_method: if actor_id.is_some() { Some("api_key".to_string()) } else { None },
scopes: scopes.to_vec(),
method: context.method.to_string(),
path: context.path.to_string(),
peer_addr: context.peer_addr.to_string(),
user_agent: None,
resource: context.resource.map(|r| r.label().to_string()),
requirement: context.requirement.label().to_string(),
status: None,
duration_ms: None,
error: Some(error.to_string()),
};

tracing::warn!(
event = "control_plane_audit",
outcome = "deny",
Expand All @@ -55,6 +138,8 @@ pub(crate) fn log_deny(
error = %error,
"control plane request denied"
);

write_audit_log(&audit_log);
}

pub(crate) fn log_result(
Expand All @@ -63,6 +148,31 @@ pub(crate) fn log_result(
resource: ControlPlaneResource,
status: http::StatusCode,
) {
let audit_log = AuditLog {
timestamp: current_timestamp_ms(),
event: "control_plane_audit",
outcome: if status.is_success() {
AuditOutcome::Allow
} else if status.is_client_error() {
AuditOutcome::Deny
} else {
AuditOutcome::Error
},
request_id: None,
actor_id: Some(identity.actor_id.to_string()),
auth_method: Some("api_key".to_string()),
scopes: identity.scope_labels.clone(),
method: context.method.to_string(),
path: context.path.to_string(),
peer_addr: context.peer_addr.to_string(),
user_agent: None,
resource: Some(resource.label().to_string()),
requirement: context.requirement.label().to_string(),
status: Some(status.as_u16()),
duration_ms: None,
error: None,
};

tracing::info!(
event = "control_plane_audit",
outcome = "result",
Expand All @@ -76,4 +186,21 @@ pub(crate) fn log_result(
status = status.as_u16(),
"control plane request completed"
);

write_audit_log(&audit_log);
}

fn write_audit_log(log: &AuditLog) {
// Optionally write to a dedicated audit log file
// This can be configured via environment variable
if let Ok(audit_path) = std::env::var("RGINX_AUDIT_LOG_PATH")
&& let Ok(json) = serde_json::to_string(log)
{
let _ = std::fs::OpenOptions::new().create(true).append(true).open(&audit_path).and_then(
|mut f| {
use std::io::Write;
writeln!(f, "{}", json)
},
);
}
Comment thread
vansour marked this conversation as resolved.
Comment thread
vansour marked this conversation as resolved.
}
Loading
Loading