diff --git a/README.md b/README.md index 64ced54..704acfb 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers | **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location | | **config_tls_address** | 0.0.0.0:3001 | HTTPS API address for pushing upstreams.yaml from remote location | | **config_tls_certificate** | etc/server.crt | Certificate file path for API. Mandatory if proxy_address_tls is set, else optional | +| **proxy_tls_grade** | (a+, a, b, c, unsafe) | Grade of TLS suite for proxy, matching grades of Qualys SSL Labs (Optional defaults to b) | | **config_tls_key_file** | etc/key.pem | Private Key file path. Mandatory if proxy_address_tls is set, else optional | | **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address | | **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) | diff --git a/etc/main.yaml b/etc/main.yaml index 4c25722..2adc0c0 100644 --- a/etc/main.yaml +++ b/etc/main.yaml @@ -10,12 +10,13 @@ upgrade_sock: /tmp/aralez.sock # Path to socket file config_api_enabled: true # Boolean to enable/disable remote config push capability. config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location config_tls_address: 0.0.0.0:3001 # HTTP TLS API address for pushing upstreams.yaml from remote location -config_tls_certificate: /opt/Rust/Projects/asyncweb/etc/server.crt # Mandatory if config_tls_address is set -config_tls_key_file: /opt/Rust/Projects/asyncweb/etc/key.pem # Mandatory if config_tls_address is set +config_tls_certificate: /etc/server.crt # Mandatory if config_tls_address is set +config_tls_key_file: /etc/key.pem # Mandatory if config_tls_address is set proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address -proxy_certificates: /opt/Rust/Projects/asyncweb/etc/yoyo # Mandatory if proxy_address_tls set, should contain certificate and key files strictly in a format {NAME}.crt, {NAME}.key. -upstreams_conf: /opt/Rust/Projects/asyncweb/etc/upstreams.yaml # the location of upstreams file +proxy_certificates: /etc/yoyo # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key. +proxy_tls_grade: a+ # Grade of TLS suite for proxy (a+, a, b, c, unsafe), matching grades of Qualys SSL Labs +upstreams_conf: /etc/upstreams.yaml # the location of upstreams file file_server_folder: /opt/storage # Optional, local folder to serve file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access. log_level: info # info, warn, error, debug, trace, off diff --git a/src/utils/metrics.rs b/src/utils/metrics.rs index bebe215..3bc8e0e 100644 --- a/src/utils/metrics.rs +++ b/src/utils/metrics.rs @@ -61,27 +61,3 @@ pub fn calc_metrics(metric_types: &MetricTypes) { REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).inc(); RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64()); } -/* -pub fn calc_metrics(method: String, code: u16, latency: Duration) { - REQUEST_COUNT.inc(); - let timer = REQUEST_LATENCY.start_timer(); - timer.observe_duration(); - RESPONSE_CODES.with_label_values(&[&code.to_string()]).inc(); - REQUESTS_BY_METHOD.with_label_values(&[&method]).inc(); - RESPONSE_LATENCY.observe(latency.as_secs_f64()); -} - -tokio::spawn(async move { - let mut interval = tokio::time::interval(std::time::Duration::from_secs(5)); - loop { - interval.tick().await; - - // read Pingora stats - let stats = pingora.get_stats(); - - // update Prometheus metrics accordingly - REQUEST_COUNT.set(stats.requests_total); - // ... etc - } -}); -*/ diff --git a/src/utils/parceyaml.rs b/src/utils/parceyaml.rs index 8b0c28a..8a8fe76 100644 --- a/src/utils/parceyaml.rs +++ b/src/utils/parceyaml.rs @@ -2,8 +2,8 @@ use crate::utils::structs::*; use dashmap::DashMap; use log::{error, info, warn}; use std::collections::HashMap; -use std::fs; use std::sync::atomic::AtomicUsize; +use std::{env, fs}; pub fn load_configuration(d: &str, kind: &str) -> Option { let yaml_data = match kind { @@ -116,14 +116,6 @@ fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) { header_list.insert(path.clone(), hl); for server in &path_config.servers { - // let mut rate: Option = None; - // let size: isize = path_config.servers.len() as isize; - // if let Some(limit) = &path_config.rate_limit { - // if size > 0 { - // rate = Some(limit / size); - // } - // } - if let Some((ip, port_str)) = server.split_once(':') { if let Ok(port) = port_str.parse::() { server_list.push(InnerMap { @@ -138,10 +130,8 @@ fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) { } } } - path_map.insert(path.clone(), (server_list, AtomicUsize::new(0))); } - config.headers.insert(hostname.clone(), header_list); config.upstreams.insert(hostname.clone(), path_map); } @@ -149,11 +139,11 @@ fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) { } pub fn parce_main_config(path: &str) -> AppConfig { - info!("Parsing configuration"); let data = fs::read_to_string(path).unwrap(); let reply = DashMap::new(); let cfg: HashMap = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); let mut cfo: AppConfig = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); + log_builder(&cfo); cfo.hc_method = cfo.hc_method.to_uppercase(); for (k, v) in cfg { reply.insert(k.to_string(), v.to_string()); @@ -170,5 +160,60 @@ pub fn parce_main_config(path: &str) -> AppConfig { } } }; + cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone()); cfo } + +fn parce_tls_grades(what: Option) -> Option { + match what { + Some(g) => match g.to_ascii_lowercase().as_str() { + "a+" => { + info!("TLS grade set to: [ A+ ]"); + Some("a+".to_string()) + } + "a" => { + info!("TLS grade set to: [ A ]"); + Some("a".to_string()) + } + "b" => { + info!("TLS grade set to: [ B ]"); + Some("b".to_string()) + } + "c" => { + info!("TLS grade set to: [ C ]"); + Some("c".to_string()) + } + "unsafe" => { + info!("TLS grade set to: [ UNSAFE ]"); + Some("unsafe".to_string()) + } + _ => { + warn!("Error parsing TLS grade, defaulting to: `B`"); + Some("b".to_string()) + } + }, + None => { + warn!("TLS grade not set, defaulting to: medium"); + Some("b".to_string()) + } + } +} + +fn log_builder(conf: &AppConfig) { + let log_level = conf.log_level.clone(); + unsafe { + match log_level.as_str() { + "info" => env::set_var("RUST_LOG", "info"), + "error" => env::set_var("RUST_LOG", "error"), + "warn" => env::set_var("RUST_LOG", "warn"), + "debug" => env::set_var("RUST_LOG", "debug"), + "trace" => env::set_var("RUST_LOG", "trace"), + "off" => env::set_var("RUST_LOG", "off"), + _ => { + println!("Error reading log level, defaulting to: INFO"); + env::set_var("RUST_LOG", "info") + } + } + } + env_logger::builder().init(); +} diff --git a/src/utils/structs.rs b/src/utils/structs.rs index c10cb67..9c6b44f 100644 --- a/src/utils/structs.rs +++ b/src/utils/structs.rs @@ -3,25 +3,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::atomic::AtomicUsize; -// pub type InnerMap = BackendConfig; pub type UpstreamsDashMap = DashMap, AtomicUsize)>>; -// #[derive(Debug, Default)] -// pub struct UpstreamsMap { -// pub upstreams: DashMap, AtomicUsize)>>, -// pub ratelimit: DashMap>, -// } -// impl UpstreamsMap { -// pub fn new() -> Self { -// Self { -// upstreams: Default::default(), -// ratelimit: Default::default(), -// } -// } -// } -// -// pub type XUpstreamsDashMap = DashMap; - pub type UpstreamsIdMap = DashMap; pub type Headers = DashMap>>; @@ -103,6 +86,7 @@ pub struct AppConfig { pub proxy_port_tls: Option, pub local_server: Option<(String, u16)>, pub proxy_certificates: Option, + pub proxy_tls_grade: Option, pub file_server_address: Option, pub file_server_folder: Option, } diff --git a/src/utils/tls.rs b/src/utils/tls.rs index 088de6e..8f2922a 100644 --- a/src/utils/tls.rs +++ b/src/utils/tls.rs @@ -1,5 +1,5 @@ use dashmap::DashMap; -use log::error; +use log::{error, warn}; use pingora::tls::ssl::{select_next_proto, AlpnError, NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef}; use rustls_pemfile::{read_one, Item}; use serde::Deserialize; @@ -37,12 +37,12 @@ pub struct Certificates { } impl Certificates { - pub fn new(configs: &Vec) -> Option { + pub fn new(configs: &Vec, _grade: &str) -> Option { let default_cert = configs.first().expect("At least one TLS certificate required"); let mut cert_infos = Vec::new(); let name_map: DashMap = DashMap::new(); for config in configs { - let cert_info = load_cert_info(&config.cert_path, &config.key_path); + let cert_info = load_cert_info(&config.cert_path, &config.key_path, _grade); match cert_info { Some(cert) => { for name in &cert.common_names { @@ -106,7 +106,7 @@ impl Certificates { } } -fn load_cert_info(cert_path: &str, key_path: &str) -> Option { +fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option { let mut common_names = HashSet::new(); let mut alt_names = HashSet::new(); @@ -162,7 +162,7 @@ fn load_cert_info(cert_path: &str, key_path: &str) -> Option { } } - if let Ok(ssl_context) = create_ssl_context(cert_path, key_path) { + if let Ok(ssl_context) = create_ssl_context(cert_path, key_path, _grade) { Some(CertificateInfo { cert_path: cert_path.to_string(), key_path: key_path.to_string(), @@ -176,13 +176,137 @@ fn load_cert_info(cert_path: &str, key_path: &str) -> Option { } } -fn create_ssl_context(cert_path: &str, key_path: &str) -> Result> { +// fn create_ssl_context(cert_path: &str, key_path: &str) -> Result> { +// let mut ctx = SslContext::builder(SslMethod::tls())?; +// ctx.set_certificate_chain_file(cert_path)?; +// ctx.set_private_key_file(key_path, SslFiletype::PEM)?; +// ctx.set_alpn_select_callback(prefer_h2); +// let built = ctx.build(); +// Ok(built) +// } + +struct TlsConfig { + options: pingora::tls::ssl::SslOptions, + ciphers: &'static str, +} +enum TlsGrade { + APlus, + A, + B, + C, + Unsafe, +} + +impl TlsGrade { + fn to_config(&self) -> TlsConfig { + match self { + // A+ (A+ on Qualys SSL Labs) + TlsGrade::APlus => TlsConfig { + options: pingora::tls::ssl::SslOptions::NO_SSL_MASK + | pingora::tls::ssl::SslOptions::NO_TLSV1 + | pingora::tls::ssl::SslOptions::NO_TLSV1_1 + | pingora::tls::ssl::SslOptions::NO_TLSV1_2, + ciphers: concat!( + // TLS 1.3 ciphers (in order of preference) + "TLS_AES_256_GCM_SHA384:", + "TLS_CHACHA20_POLY1305_SHA256:", + "TLS_AES_128_GCM_SHA256:", + // TLS 1.2 ciphers with PFS and AEAD + "ECDHE-ECDSA-AES256-GCM-SHA384:", + "ECDHE-RSA-AES256-GCM-SHA384:", + "ECDHE-ECDSA-CHACHA20-POLY1305:", + "ECDHE-RSA-CHACHA20-POLY1305:", + "ECDHE-ECDSA-AES128-GCM-SHA256:", + "ECDHE-RSA-AES128-GCM-SHA256" + ), + }, + // A (A on Qualys SSL Labs) + TlsGrade::A => TlsConfig { + options: pingora::tls::ssl::SslOptions::NO_SSL_MASK | pingora::tls::ssl::SslOptions::NO_TLSV1 | pingora::tls::ssl::SslOptions::NO_TLSV1_1, + ciphers: concat!( + // TLS 1.3 ciphers + "TLS_AES_256_GCM_SHA384:", + "TLS_CHACHA20_POLY1305_SHA256:", + "TLS_AES_128_GCM_SHA256:", + // TLS 1.2 ciphers + "ECDHE-ECDSA-AES256-GCM-SHA384:", + "ECDHE-RSA-AES256-GCM-SHA384:", + "ECDHE-ECDSA-CHACHA20-POLY1305:", + "ECDHE-RSA-CHACHA20-POLY1305:", + "ECDHE-ECDSA-AES128-GCM-SHA256:", + "ECDHE-RSA-AES128-GCM-SHA256:", + "DHE-RSA-AES256-GCM-SHA384:", + "DHE-RSA-AES128-GCM-SHA256" + ), + }, + // B (B on Qualys SSL Labs) + TlsGrade::B => TlsConfig { + options: pingora::tls::ssl::SslOptions::NO_SSL_MASK | pingora::tls::ssl::SslOptions::NO_TLSV1, + ciphers: concat!( + "ECDHE-ECDSA-AES256-GCM-SHA384:", + "ECDHE-RSA-AES256-GCM-SHA384:", + "ECDHE-ECDSA-AES128-GCM-SHA256:", + "ECDHE-RSA-AES128-GCM-SHA256:", + "DHE-RSA-AES256-GCM-SHA384:", + "DHE-RSA-AES128-GCM-SHA256:", + "ECDHE-ECDSA-AES256-SHA384:", + "ECDHE-RSA-AES256-SHA384:", + "ECDHE-ECDSA-AES128-SHA256:", + "ECDHE-RSA-AES128-SHA256" + ), + }, + // C (C on Qualys SSL Labs) + TlsGrade::C => TlsConfig { + options: pingora::tls::ssl::SslOptions::NO_SSL_MASK, + ciphers: concat!( + "ECDHE-ECDSA-AES256-GCM-SHA384:", + "ECDHE-RSA-AES256-GCM-SHA384:", + "ECDHE-ECDSA-AES128-GCM-SHA256:", + "ECDHE-RSA-AES128-GCM-SHA256:", + "DHE-RSA-AES256-GCM-SHA384:", + "DHE-RSA-AES128-GCM-SHA256:", + "ECDHE-ECDSA-AES256-SHA384:", + "ECDHE-RSA-AES256-SHA384:", + "ECDHE-ECDSA-AES128-SHA256:", + "ECDHE-RSA-AES128-SHA256:", + "AES256-GCM-SHA384:", + "AES128-GCM-SHA256:", + "AES256-SHA256:", + "AES128-SHA256" + ), + }, + // Unsafe (F on Qualys SSL Labs) + TlsGrade::Unsafe => TlsConfig { + options: pingora::tls::ssl::SslOptions::empty(), + ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH", + }, + } + } + fn from_str(s: &str) -> Option { + match s.to_ascii_lowercase().as_str() { + "a+" => Some(TlsGrade::APlus), + "a" => Some(TlsGrade::A), + "b" => Some(TlsGrade::B), + "c" => Some(TlsGrade::C), + "unsafe" => Some(TlsGrade::Unsafe), + _ => None, + } + } +} + +fn create_ssl_context(cert_path: &str, key_path: &str, grade: &str) -> Result> { let mut ctx = SslContext::builder(SslMethod::tls())?; + let config = TlsGrade::from_str(grade).map(|g| g.to_config()).unwrap_or_else(|| { + warn!("Invalid TLS grade '{}', defaulting to UNSAFE", grade); + TlsGrade::Unsafe.to_config() + }); + ctx.set_options(config.options); ctx.set_certificate_chain_file(cert_path)?; ctx.set_private_key_file(key_path, SslFiletype::PEM)?; + ctx.set_cipher_list(config.ciphers)?; ctx.set_alpn_select_callback(prefer_h2); - let built = ctx.build(); - Ok(built) + + Ok(ctx.build()) } pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> { diff --git a/src/web/proxyhttp.rs b/src/web/proxyhttp.rs index 27ff689..081e21c 100644 --- a/src/web/proxyhttp.rs +++ b/src/web/proxyhttp.rs @@ -214,7 +214,6 @@ impl ProxyHttp for LB { redirect_response.insert_header("Content-Length", "0")?; session.write_response_header(Box::new(redirect_response), false).await?; } - // match return_header_host(&session) { match ctx.hostname.as_ref() { Some(host) => { let path = session.req_header().uri.path(); diff --git a/src/web/start.rs b/src/web/start.rs index b483f82..d440c11 100644 --- a/src/web/start.rs +++ b/src/web/start.rs @@ -13,7 +13,7 @@ use pingora_core::prelude::{background_service, Opt}; use pingora_core::server::Server; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::Arc; -use std::{env, thread}; +use std::thread; pub fn run() { // default_provider().install_default().expect("Failed to install rustls crypto provider"); @@ -47,22 +47,25 @@ pub fn run() { extraparams: ec_config, }; - let log_level = cfg.log_level.clone(); - unsafe { - match log_level.as_str() { - "info" => env::set_var("RUST_LOG", "info"), - "error" => env::set_var("RUST_LOG", "error"), - "warn" => env::set_var("RUST_LOG", "warn"), - "debug" => env::set_var("RUST_LOG", "debug"), - "trace" => env::set_var("RUST_LOG", "trace"), - "off" => env::set_var("RUST_LOG", "off"), - _ => { - println!("Error reading log level, defaulting to: INFO"); - env::set_var("RUST_LOG", "info") - } - } - } - env_logger::builder().init(); + // let log_level = cfg.log_level.clone(); + // unsafe { + // match log_level.as_str() { + // "info" => env::set_var("RUST_LOG", "info"), + // "error" => env::set_var("RUST_LOG", "error"), + // "warn" => env::set_var("RUST_LOG", "warn"), + // "debug" => env::set_var("RUST_LOG", "debug"), + // "trace" => env::set_var("RUST_LOG", "trace"), + // "off" => env::set_var("RUST_LOG", "off"), + // _ => { + // println!("Error reading log level, defaulting to: INFO"); + // env::set_var("RUST_LOG", "info") + // } + // } + // } + // env_logger::builder().init(); + + let grade = cfg.proxy_tls_grade.clone().unwrap_or("b".to_string()); + info!("TLS grade set to: {}", grade); let bg_srvc = background_service("bgsrvc", lb.clone()); let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb.clone()); @@ -77,12 +80,12 @@ pub fn run() { watch_folder(certs_path, tx).unwrap(); }); let certificate_configs = rx.recv().unwrap(); - let first_set = tls::Certificates::new(&certificate_configs).unwrap_or_else(|| panic!("Unable to load initial certificate info")); + let first_set = tls::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info")); let certificates = Arc::new(ArcSwap::from_pointee(first_set)); let certs_for_callback = certificates.clone(); let certs_for_watcher = certificates.clone(); - let new_certs = tls::Certificates::new(&certificate_configs); + let new_certs = tls::Certificates::new(&certificate_configs, grade.as_str()); certs_for_watcher.store(Arc::new(new_certs.unwrap())); let mut tls_settings = @@ -95,7 +98,7 @@ pub fn run() { let certs_for_watcher = certificates.clone(); thread::spawn(move || { while let Ok(new_configs) = rx.recv() { - let new_certs = tls::Certificates::new(&new_configs); + let new_certs = tls::Certificates::new(&new_configs, grade.as_str()); match new_certs { Some(new_certs) => { certs_for_watcher.store(Arc::new(new_certs));