feat(smart-proxy): add hot-reloadable global ingress security policy across Rust and TypeScript proxy layers

This commit is contained in:
2026-04-26 15:11:10 +00:00
parent 8fa3a51b03
commit af4908b63f
53 changed files with 2350 additions and 1196 deletions
+14 -13
View File
@@ -4,8 +4,7 @@
//! Account credentials are ephemeral — the consumer owns all persistence.
use instant_acme::{
Account, NewAccount, NewOrder, Identifier, ChallengeType, OrderStatus,
AccountCredentials,
Account, AccountCredentials, ChallengeType, Identifier, NewAccount, NewOrder, OrderStatus,
};
use rcgen::{CertificateParams, KeyPair};
use thiserror::Error;
@@ -89,7 +88,11 @@ impl AcmeClient {
F: FnOnce(PendingChallenge) -> Fut,
Fut: std::future::Future<Output = Result<(), AcmeError>>,
{
info!("Starting ACME provisioning for {} via {}", domain, self.directory_url());
info!(
"Starting ACME provisioning for {} via {}",
domain,
self.directory_url()
);
// 1. Get or create ACME account
let account = self.get_or_create_account().await?;
@@ -170,14 +173,14 @@ impl AcmeClient {
debug!("Order ready, finalizing...");
// 6. Generate CSR and finalize
let key_pair = KeyPair::generate().map_err(|e| {
AcmeError::FinalizationFailed(format!("Key generation failed: {}", e))
})?;
let key_pair = KeyPair::generate()
.map_err(|e| AcmeError::FinalizationFailed(format!("Key generation failed: {}", e)))?;
let mut params = CertificateParams::new(vec![domain.to_string()]).map_err(|e| {
AcmeError::FinalizationFailed(format!("CSR params failed: {}", e))
})?;
params.distinguished_name.push(rcgen::DnType::CommonName, domain);
let mut params = CertificateParams::new(vec![domain.to_string()])
.map_err(|e| AcmeError::FinalizationFailed(format!("CSR params failed: {}", e)))?;
params
.distinguished_name
.push(rcgen::DnType::CommonName, domain);
let csr = params.serialize_request(&key_pair).map_err(|e| {
AcmeError::FinalizationFailed(format!("CSR serialization failed: {}", e))
@@ -219,9 +222,7 @@ impl AcmeClient {
.certificate()
.await
.map_err(|e| AcmeError::FinalizationFailed(e.to_string()))?
.ok_or_else(|| {
AcmeError::FinalizationFailed("No certificate returned".to_string())
})?;
.ok_or_else(|| AcmeError::FinalizationFailed("No certificate returned".to_string()))?;
let private_key_pem = key_pair.serialize_pem();
+20 -22
View File
@@ -2,8 +2,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
use tracing::info;
use crate::cert_store::{CertStore, CertBundle, CertMetadata, CertSource};
use crate::acme::AcmeClient;
use crate::cert_store::{CertBundle, CertMetadata, CertSource, CertStore};
#[derive(Debug, Error)]
pub enum CertManagerError {
@@ -45,17 +45,13 @@ impl CertManager {
/// Create an ACME client using this manager's configuration.
/// Returns None if no ACME email is configured.
pub fn acme_client(&self) -> Option<AcmeClient> {
self.acme_email.as_ref().map(|email| {
AcmeClient::new(email.clone(), self.use_production)
})
self.acme_email
.as_ref()
.map(|email| AcmeClient::new(email.clone(), self.use_production))
}
/// Load a static certificate into the store (infallible — pure cache insert).
pub fn load_static(
&mut self,
domain: String,
bundle: CertBundle,
) {
pub fn load_static(&mut self, domain: String, bundle: CertBundle) {
self.store.store(domain, bundle);
}
@@ -108,23 +104,25 @@ impl CertManager {
F: FnOnce(String, String) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let acme_client = self.acme_client()
.ok_or(CertManagerError::NoEmail)?;
let acme_client = self.acme_client().ok_or(CertManagerError::NoEmail)?;
info!("Renewing certificate for {}", domain);
let domain_owned = domain.to_string();
let result = acme_client.provision(&domain_owned, |pending| {
let token = pending.token.clone();
let key_auth = pending.key_authorization.clone();
async move {
challenge_setup(token, key_auth).await;
Ok(())
}
}).await.map_err(|e| CertManagerError::AcmeFailure {
domain: domain.to_string(),
message: e.to_string(),
})?;
let result = acme_client
.provision(&domain_owned, |pending| {
let token = pending.token.clone();
let key_auth = pending.key_authorization.clone();
async move {
challenge_setup(token, key_auth).await;
Ok(())
}
})
.await
.map_err(|e| CertManagerError::AcmeFailure {
domain: domain.to_string(),
message: e.to_string(),
})?;
let (cert_pem, key_pem) = result;
let now = SystemTime::now()
+15 -6
View File
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Certificate metadata stored alongside certs.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -90,8 +90,10 @@ mod tests {
fn make_test_bundle(domain: &str) -> CertBundle {
CertBundle {
key_pem: "-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----\n".to_string(),
cert_pem: "-----BEGIN CERTIFICATE-----\ntest-cert\n-----END CERTIFICATE-----\n".to_string(),
key_pem: "-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----\n"
.to_string(),
cert_pem: "-----BEGIN CERTIFICATE-----\ntest-cert\n-----END CERTIFICATE-----\n"
.to_string(),
ca_pem: None,
metadata: CertMetadata {
domain: domain.to_string(),
@@ -122,7 +124,8 @@ mod tests {
let mut store = CertStore::new();
let mut bundle = make_test_bundle("secure.com");
bundle.ca_pem = Some("-----BEGIN CERTIFICATE-----\nca-cert\n-----END CERTIFICATE-----\n".to_string());
bundle.ca_pem =
Some("-----BEGIN CERTIFICATE-----\nca-cert\n-----END CERTIFICATE-----\n".to_string());
store.store("secure.com".to_string(), bundle);
let loaded = store.get("secure.com").unwrap();
@@ -147,7 +150,10 @@ mod tests {
fn test_remove_cert() {
let mut store = CertStore::new();
store.store("remove-me.com".to_string(), make_test_bundle("remove-me.com"));
store.store(
"remove-me.com".to_string(),
make_test_bundle("remove-me.com"),
);
assert!(store.has("remove-me.com"));
let removed = store.remove("remove-me.com");
@@ -165,7 +171,10 @@ mod tests {
fn test_wildcard_domain() {
let mut store = CertStore::new();
store.store("*.example.com".to_string(), make_test_bundle("*.example.com"));
store.store(
"*.example.com".to_string(),
make_test_bundle("*.example.com"),
);
assert!(store.has("*.example.com"));
let loaded = store.get("*.example.com").unwrap();
+3 -3
View File
@@ -3,11 +3,11 @@
//! TLS certificate management for RustProxy.
//! Handles ACME (Let's Encrypt), static certificates, and dynamic SNI resolution.
pub mod cert_store;
pub mod cert_manager;
pub mod acme;
pub mod cert_manager;
pub mod cert_store;
pub mod sni_resolver;
pub use cert_store::*;
pub use cert_manager::*;
pub use cert_store::*;
pub use sni_resolver::*;