212 lines
7.3 KiB
Rust
212 lines
7.3 KiB
Rust
use anyhow::{Context, Result, bail};
|
|
use std::path::PathBuf;
|
|
use tonic::transport::{Certificate, Identity, ServerTlsConfig};
|
|
use tracing::{info, warn};
|
|
|
|
const DEFAULT_PKI_DIR: &str = "/etc/lesavka/pki";
|
|
|
|
pub fn server_tls_config() -> Result<Option<ServerTlsConfig>> {
|
|
let cert_path = env_path("LESAVKA_TLS_CERT").unwrap_or_else(|| default_path("server.crt"));
|
|
let key_path = env_path("LESAVKA_TLS_KEY").unwrap_or_else(|| default_path("server.key"));
|
|
let require_tls = bool_env("LESAVKA_REQUIRE_TLS");
|
|
|
|
if !cert_path.exists() || !key_path.exists() {
|
|
if require_tls {
|
|
bail!(
|
|
"LESAVKA_REQUIRE_TLS=1 but TLS identity files are missing: cert={} key={}",
|
|
cert_path.display(),
|
|
key_path.display()
|
|
);
|
|
}
|
|
warn!(
|
|
cert = %cert_path.display(),
|
|
key = %key_path.display(),
|
|
"🔓 TLS identity not present; serving plaintext gRPC for local/dev use"
|
|
);
|
|
return Ok(None);
|
|
}
|
|
|
|
let cert = std::fs::read(&cert_path)
|
|
.with_context(|| format!("reading TLS server certificate {}", cert_path.display()))?;
|
|
let key = std::fs::read(&key_path)
|
|
.with_context(|| format!("reading TLS server key {}", key_path.display()))?;
|
|
let mut tls = ServerTlsConfig::new().identity(Identity::from_pem(cert, key));
|
|
|
|
let client_ca_path =
|
|
env_path("LESAVKA_TLS_CLIENT_CA").unwrap_or_else(|| default_path("ca.crt"));
|
|
if client_ca_path.exists() {
|
|
let client_ca = std::fs::read(&client_ca_path)
|
|
.with_context(|| format!("reading TLS client CA {}", client_ca_path.display()))?;
|
|
tls = tls.client_ca_root(Certificate::from_pem(client_ca));
|
|
if bool_env("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL") {
|
|
tls = tls.client_auth_optional(true);
|
|
warn!("🔐 TLS enabled with optional client certificates");
|
|
} else {
|
|
info!("🔐 TLS enabled with required client certificate authentication");
|
|
}
|
|
} else {
|
|
warn!(
|
|
ca = %client_ca_path.display(),
|
|
"🔐 TLS enabled without client certificate authentication; install a client CA for production"
|
|
);
|
|
}
|
|
|
|
Ok(Some(tls))
|
|
}
|
|
|
|
fn env_path(name: &str) -> Option<PathBuf> {
|
|
std::env::var_os(name)
|
|
.filter(|value| !value.is_empty())
|
|
.map(PathBuf::from)
|
|
}
|
|
|
|
fn default_path(file_name: &str) -> PathBuf {
|
|
PathBuf::from(DEFAULT_PKI_DIR).join(file_name)
|
|
}
|
|
|
|
fn bool_env(name: &str) -> bool {
|
|
std::env::var(name)
|
|
.map(|value| {
|
|
let value = value.trim().to_ascii_lowercase();
|
|
!value.is_empty() && value != "0" && value != "false" && value != "no"
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{bool_env, default_path, env_path, server_tls_config};
|
|
use temp_env::with_vars;
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
fn missing_identity_is_allowed_unless_tls_is_required() {
|
|
with_vars(
|
|
[
|
|
("LESAVKA_REQUIRE_TLS", None::<&str>),
|
|
("LESAVKA_TLS_CERT", Some("/tmp/lesavka-missing-cert")),
|
|
("LESAVKA_TLS_KEY", Some("/tmp/lesavka-missing-key")),
|
|
],
|
|
|| {
|
|
assert!(
|
|
server_tls_config()
|
|
.expect("optional tls should not error")
|
|
.is_none()
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn require_tls_errors_when_identity_is_missing() {
|
|
with_vars(
|
|
[
|
|
("LESAVKA_REQUIRE_TLS", Some("1")),
|
|
("LESAVKA_TLS_CERT", Some("/tmp/lesavka-missing-cert")),
|
|
("LESAVKA_TLS_KEY", Some("/tmp/lesavka-missing-key")),
|
|
],
|
|
|| {
|
|
let err = server_tls_config().expect_err("required tls should fail");
|
|
assert!(err.to_string().contains("LESAVKA_REQUIRE_TLS=1"));
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn helpers_parse_paths_and_boolean_overrides() {
|
|
with_vars(
|
|
[
|
|
("LESAVKA_TLS_CERT", Some("/tmp/server.crt")),
|
|
("LESAVKA_REQUIRE_TLS", Some("yes")),
|
|
],
|
|
|| {
|
|
assert_eq!(
|
|
env_path("LESAVKA_TLS_CERT").unwrap(),
|
|
std::path::PathBuf::from("/tmp/server.crt")
|
|
);
|
|
assert!(bool_env("LESAVKA_REQUIRE_TLS"));
|
|
},
|
|
);
|
|
with_vars(
|
|
[
|
|
("LESAVKA_TLS_CERT", Some("")),
|
|
("LESAVKA_REQUIRE_TLS", Some("false")),
|
|
],
|
|
|| {
|
|
assert!(env_path("LESAVKA_TLS_CERT").is_none());
|
|
assert!(!bool_env("LESAVKA_REQUIRE_TLS"));
|
|
assert_eq!(
|
|
default_path("server.crt"),
|
|
std::path::PathBuf::from("/etc/lesavka/pki/server.crt")
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tls_identity_can_load_with_and_without_client_ca() {
|
|
let dir = tempdir().expect("tls dir");
|
|
let cert = dir.path().join("server.crt");
|
|
let key = dir.path().join("server.key");
|
|
let ca = dir.path().join("ca.crt");
|
|
std::fs::write(&cert, b"not a real cert").expect("cert");
|
|
std::fs::write(&key, b"not a real key").expect("key");
|
|
std::fs::write(&ca, b"not a real ca").expect("ca");
|
|
|
|
with_vars(
|
|
[
|
|
("LESAVKA_REQUIRE_TLS", Some("1")),
|
|
("LESAVKA_TLS_CERT", Some(cert.to_string_lossy().as_ref())),
|
|
("LESAVKA_TLS_KEY", Some(key.to_string_lossy().as_ref())),
|
|
("LESAVKA_TLS_CLIENT_CA", Some(ca.to_string_lossy().as_ref())),
|
|
("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL", Some("1")),
|
|
],
|
|
|| {
|
|
assert!(server_tls_config().expect("tls config").is_some());
|
|
},
|
|
);
|
|
|
|
with_vars(
|
|
[
|
|
("LESAVKA_REQUIRE_TLS", Some("1")),
|
|
("LESAVKA_TLS_CERT", Some(cert.to_string_lossy().as_ref())),
|
|
("LESAVKA_TLS_KEY", Some(key.to_string_lossy().as_ref())),
|
|
(
|
|
"LESAVKA_TLS_CLIENT_CA",
|
|
Some(dir.path().join("missing-ca.crt").to_string_lossy().as_ref()),
|
|
),
|
|
("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL", Some("0")),
|
|
],
|
|
|| {
|
|
assert!(
|
|
server_tls_config()
|
|
.expect("tls config without ca")
|
|
.is_some()
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn existing_but_unreadable_identity_reports_context() {
|
|
let dir = tempdir().expect("tls dir");
|
|
let key = dir.path().join("server.key");
|
|
std::fs::write(&key, b"not a real key").expect("key");
|
|
|
|
with_vars(
|
|
[
|
|
("LESAVKA_REQUIRE_TLS", Some("1")),
|
|
(
|
|
"LESAVKA_TLS_CERT",
|
|
Some(dir.path().to_string_lossy().as_ref()),
|
|
),
|
|
("LESAVKA_TLS_KEY", Some(key.to_string_lossy().as_ref())),
|
|
],
|
|
|| {
|
|
let err = server_tls_config().expect_err("directory cert should fail");
|
|
assert!(err.to_string().contains("reading TLS server certificate"));
|
|
},
|
|
);
|
|
}
|
|
}
|