feat(smart-proxy): add hot-reloadable global ingress security policy across Rust and TypeScript proxy layers
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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::*;
|
||||
|
||||
Reference in New Issue
Block a user