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
222 changes: 218 additions & 4 deletions common/infrastructure/src/app/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use actix_web::{
use anyhow::{Context, anyhow};
use bytesize::ByteSize;
use clap::{Arg, ArgMatches, Args, Command, Error, FromArgMatches, value_parser};
use openssl::ssl::SslFiletype;
use openssl::ssl::{SslFiletype, SslVersion};
use opentelemetry_instrumentation_actix_web::{RequestMetrics, RequestTracing, RouteFormatter};
use std::{
fmt::Debug,
fmt::{self, Debug},
marker::PhantomData,
net::{IpAddr, Ipv6Addr, SocketAddr, TcpListener},
ops::Deref,
Expand Down Expand Up @@ -110,6 +110,70 @@ impl<E: Endpoint> FromArgMatches for BindPort<E> {
}
}

/// TLS security profile, aligned with OpenShift TLS Security Profiles.
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Default)]
pub enum TlsSecurityProfile {
/// TLS 1.0+, broadest compatibility.
#[clap(name = "old")]
Old,
/// TLS 1.2+, broad compatibility.
#[clap(name = "intermediate")]
Intermediate,
/// TLS 1.3 only, most secure.
#[default]
#[clap(name = "modern")]
Modern,
/// User-defined min TLS version and cipher suites.
#[clap(name = "custom")]
Custom,
}

impl fmt::Display for TlsSecurityProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TlsSecurityProfile::Old => write!(f, "old"),
TlsSecurityProfile::Intermediate => write!(f, "intermediate"),
TlsSecurityProfile::Modern => write!(f, "modern"),
TlsSecurityProfile::Custom => write!(f, "custom"),
}
}
}

/// Minimum TLS protocol version.
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
pub enum TlsMinVersion {
#[clap(name = "1.0")]
Tls1_0,
#[clap(name = "1.1")]
Tls1_1,
#[clap(name = "1.2")]
Tls1_2,
#[clap(name = "1.3")]
Tls1_3,
}

impl fmt::Display for TlsMinVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TlsMinVersion::Tls1_0 => write!(f, "1.0"),
TlsMinVersion::Tls1_1 => write!(f, "1.1"),
TlsMinVersion::Tls1_2 => write!(f, "1.2"),
TlsMinVersion::Tls1_3 => write!(f, "1.3"),
}
}
}

impl TlsMinVersion {
fn to_ssl_version(self) -> SslVersion {
match self {
TlsMinVersion::Tls1_0 => SslVersion::TLS1,
TlsMinVersion::Tls1_1 => SslVersion::TLS1_1,
TlsMinVersion::Tls1_2 => SslVersion::TLS1_2,
TlsMinVersion::Tls1_3 => SslVersion::TLS1_3,
}
}
}

#[derive(Clone, Debug, clap::Args)]
#[command(
rename_all_env = "SCREAMING_SNAKE_CASE",
Expand Down Expand Up @@ -186,6 +250,35 @@ where
)]
pub tls_certificate_file: Option<PathBuf>,

/// TLS security profile (old, intermediate, modern, custom)
#[arg(
id = "http-server-tls-security-profile",
long,
env = "HTTP_SERVER_TLS_SECURITY_PROFILE",
default_value_t = TlsSecurityProfile::Modern,
)]
pub tls_security_profile: TlsSecurityProfile,

/// Minimum TLS version (only used with custom profile)
#[arg(
id = "http-server-tls-min-version",
long,
env = "HTTP_SERVER_TLS_MIN_VERSION"
)]
pub tls_min_version: Option<TlsMinVersion>,

/// TLS 1.2 cipher list in OpenSSL format (only used with custom profile)
#[arg(id = "http-server-tls-ciphers", long, env = "HTTP_SERVER_TLS_CIPHERS")]
pub tls_ciphers: Option<String>,

/// TLS 1.3 ciphersuites in OpenSSL format (only used with custom profile)
#[arg(
id = "http-server-tls-ciphersuites",
long,
env = "HTTP_SERVER_TLS_CIPHERSUITES"
)]
pub tls_ciphersuites: Option<String>,

/// Disable the request log
#[arg(id = "http-disable-log", long, env = "HTTP_SERVER_DISABLE_LOG")]
pub disable_log: bool,
Expand Down Expand Up @@ -224,6 +317,10 @@ where
tls_enabled: false,
tls_key_file: None,
tls_certificate_file: None,
tls_security_profile: TlsSecurityProfile::Modern,
tls_min_version: None,
tls_ciphers: None,
tls_ciphersuites: None,
disable_log: false,
_marker: Default::default(),
}
Expand Down Expand Up @@ -264,13 +361,24 @@ where
.json_limit(value.json_limit.0.0 as _);

if value.tls_enabled {
if value.tls_security_profile == TlsSecurityProfile::Custom
&& value.tls_min_version.is_none()
{
return Err(anyhow!(
"Custom TLS profile requires --http-server-tls-min-version"
));
}
result = result.tls(TlsConfiguration {
key: value.tls_key_file.ok_or_else(|| {
anyhow!("TLS enabled but no key file configured (use --http-server-tls-key-file)")
})?,
certificate: value.tls_certificate_file.ok_or_else(|| {
anyhow!("TLS enabled but no certificate file configured (use --http-server-tls-certificate-file)")
})?,
security_profile: value.tls_security_profile,
min_version: value.tls_min_version,
ciphers: value.tls_ciphers,
ciphersuites: value.tls_ciphersuites,
});
}

Expand Down Expand Up @@ -309,6 +417,51 @@ pub struct HttpServerBuilder {
pub struct TlsConfiguration {
certificate: PathBuf,
key: PathBuf,
security_profile: TlsSecurityProfile,
min_version: Option<TlsMinVersion>,
ciphers: Option<String>,
ciphersuites: Option<String>,
}

/// Build an SSL acceptor configured according to the given TLS profile.
fn build_ssl_acceptor(tls: &TlsConfiguration) -> anyhow::Result<openssl::ssl::SslAcceptorBuilder> {
let acceptor = match tls.security_profile {
TlsSecurityProfile::Old => {
let mut builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server())
.context("creating Old TLS profile acceptor")?;
builder
.set_min_proto_version(None)
.context("clearing min TLS version for Old profile")?;
builder
}
TlsSecurityProfile::Intermediate => {
SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server())
.context("creating Intermediate TLS profile acceptor")?
}
TlsSecurityProfile::Modern => SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())
.context("creating Modern TLS profile acceptor")?,
TlsSecurityProfile::Custom => {
let mut builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server())
.context("creating Custom TLS profile acceptor")?;
if let Some(min_version) = tls.min_version {
builder
.set_min_proto_version(Some(min_version.to_ssl_version()))
.context("setting custom min TLS version")?;
}
if let Some(ref ciphers) = tls.ciphers {
builder
.set_cipher_list(ciphers)
.context("setting custom TLS 1.2 cipher list")?;
}
if let Some(ref ciphersuites) = tls.ciphersuites {
builder
.set_ciphersuites(ciphersuites)
.context("setting custom TLS 1.3 ciphersuites")?;
}
builder
}
};
Ok(acceptor)
}

pub enum Bind {
Expand Down Expand Up @@ -535,8 +688,8 @@ impl HttpServerBuilder {

let tls = match self.tls {
Some(tls) => {
log::info!("Enabling TLS support");
let mut acceptor = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?;
log::info!("Enabling TLS support (profile: {})", tls.security_profile);
let mut acceptor = build_ssl_acceptor(&tls)?;
acceptor
.set_certificate_chain_file(tls.certificate)
.context("setting certificate chain")?;
Expand Down Expand Up @@ -641,4 +794,65 @@ mod test {
fn default_config_converts() {
HttpServerBuilder::try_from(HttpServerConfig::<MockEndpoint>::default()).unwrap();
}

/// Verifies that the default config uses the Modern TLS profile.
#[test]
fn default_tls_profile_is_modern() {
let config = HttpServerConfig::<MockEndpoint>::default();
assert_eq!(config.tls_security_profile, TlsSecurityProfile::Modern);
}

/// Verifies that a Custom profile without min_version is rejected.
#[test]
fn custom_profile_requires_min_version() {
let config = HttpServerConfig::<MockEndpoint> {
tls_enabled: true,
tls_key_file: Some(PathBuf::from("/tmp/key.pem")),
tls_certificate_file: Some(PathBuf::from("/tmp/cert.pem")),
tls_security_profile: TlsSecurityProfile::Custom,
tls_min_version: None,
..Default::default()
};
let err = match HttpServerBuilder::try_from(config) {
Err(e) => e,
Ok(_) => panic!("expected error for Custom profile without min_version"),
};
assert!(
err.to_string().contains("--http-server-tls-min-version"),
"error should mention --http-server-tls-min-version, got: {err}"
);
}

/// Verifies that all named profiles are accepted during config conversion.
#[test]
fn all_profiles_accepted() {
for profile in [
TlsSecurityProfile::Old,
TlsSecurityProfile::Intermediate,
TlsSecurityProfile::Modern,
] {
let config = HttpServerConfig::<MockEndpoint> {
tls_enabled: true,
tls_key_file: Some(PathBuf::from("/tmp/key.pem")),
tls_certificate_file: Some(PathBuf::from("/tmp/cert.pem")),
tls_security_profile: profile,
..Default::default()
};
HttpServerBuilder::try_from(config).unwrap();
}
}

/// Verifies that Custom profile with min_version is accepted.
#[test]
fn custom_profile_with_min_version_accepted() {
let config = HttpServerConfig::<MockEndpoint> {
tls_enabled: true,
tls_key_file: Some(PathBuf::from("/tmp/key.pem")),
tls_certificate_file: Some(PathBuf::from("/tmp/cert.pem")),
tls_security_profile: TlsSecurityProfile::Custom,
tls_min_version: Some(TlsMinVersion::Tls1_2),
..Default::default()
};
HttpServerBuilder::try_from(config).unwrap();
}
}
4 changes: 4 additions & 0 deletions docs/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
| `HTTP_SERVER_JSON_LIMIT` | JSON request limit | `2 MiB` |
| `HTTP_SERVER_REQUEST_LIMIT` | Overall request limit | `256 KiB` |
| `HTTP_SERVER_TLS_CERTIFICATE_FILE` | Path to the TLS certificate in PEM format | |
| `HTTP_SERVER_TLS_CIPHERS` | TLS 1.2 cipher list in OpenSSL format (custom profile only) | |
| `HTTP_SERVER_TLS_CIPHERSUITES` | TLS 1.3 ciphersuites in OpenSSL format (custom profile only) | |
| `HTTP_SERVER_TLS_ENABLED` | Enable TLS | `false` |
| `HTTP_SERVER_TLS_KEY_FILE` | Path to the TLS key file in PEM format | |
| `HTTP_SERVER_TLS_MIN_VERSION` | Minimum TLS version for custom profile: 1.0, 1.1, 1.2, 1.3 | |
| `HTTP_SERVER_TLS_SECURITY_PROFILE` | TLS security profile: old, intermediate, modern, custom | `modern` |
| `HTTP_SERVER_WORKERS` | Number of worker threads, defaults to zero, which falls back to the number of cores | `0` |
| `IMPORTER_CONCURRENCY` | The maximum number of jobs run simultaneously by the importer | `1` |
| `IMPORTER_WORKING_DIR` | Where the importer downloads documents prior to ingesting them | `tempdir` |
Expand Down
Loading