use super::security::ClientAuthMode;
use super::Config;
use std::path::Path;
pub fn validate_config_security(config: &Config) -> anyhow::Result<()> {
let is_prod = is_production_mode();
if config.security.require_metrics_auth {
let token_present = config
.security
.metrics_auth_token
.as_ref()
.map(|t| !t.is_empty())
.unwrap_or(false);
if !token_present {
anyhow::bail!(
"\nCRITICAL: Metrics authentication is enabled but no credentials are configured!\n\
===================================================================\n\
Configure a shared bearer token:\n\
export SIGNAL_FISH__SECURITY__METRICS_AUTH_TOKEN=\"$(openssl rand -hex 32)\"\n\
\n\
To disable metrics auth (NOT recommended), set:\n\
export SIGNAL_FISH__SECURITY__REQUIRE_METRICS_AUTH=false\n\
===================================================================\n"
);
}
if let Some(token) = &config.security.metrics_auth_token {
if token.len() < 16 {
eprintln!(
"\nWARNING: Metrics auth token is very short ({} chars).\n\
Recommended: At least 32 characters for security.\n\
Generate a strong token: openssl rand -hex 32\n",
token.len()
);
}
}
} else if is_prod {
eprintln!(
"\nSECURITY WARNING: Metrics Authentication Disabled in Production!\n\
===================================================================\n\
Your /metrics endpoint is publicly accessible without authentication.\n\
This exposes sensitive application data and usage statistics.\n\
\n\
To enable metrics authentication:\n\
export SIGNAL_FISH__SECURITY__REQUIRE_METRICS_AUTH=true\n\
export SIGNAL_FISH__SECURITY__METRICS_AUTH_TOKEN=\"$(openssl rand -hex 32)\"\n\
===================================================================\n"
);
}
if config.security.transport.tls.enabled {
let tls = &config.security.transport.tls;
let cert_path = tls
.certificate_path
.as_deref()
.map(str::trim)
.filter(|p| !p.is_empty())
.ok_or_else(|| {
anyhow::anyhow!(
"security.transport.tls.certificate_path must be provided when TLS is enabled"
)
})?;
if !Path::new(cert_path).exists() {
anyhow::bail!("TLS certificate file not found at {cert_path}");
}
let key_path = tls
.private_key_path
.as_deref()
.map(str::trim)
.filter(|p| !p.is_empty())
.ok_or_else(|| {
anyhow::anyhow!(
"security.transport.tls.private_key_path must be provided when TLS is enabled"
)
})?;
if !Path::new(key_path).exists() {
anyhow::bail!("TLS private key file not found at {key_path}");
}
if matches!(
tls.client_auth,
ClientAuthMode::Optional | ClientAuthMode::Require
) {
let ca_path = tls
.client_ca_cert_path
.as_deref()
.map(str::trim)
.filter(|p| !p.is_empty())
.ok_or_else(|| {
anyhow::anyhow!(
"security.transport.tls.client_ca_cert_path must be set when client_auth is {:?}",
tls.client_auth
)
})?;
if !Path::new(ca_path).exists() {
anyhow::bail!("Client CA bundle not found at {ca_path}");
}
}
}
if config.security.transport.token_binding.enabled {
let binding = &config.security.transport.token_binding;
if binding.required && !config.security.transport.tls.enabled {
anyhow::bail!(
"security.transport.token_binding.required=true requires TLS termination \
(set security.transport.tls.enabled=true)"
);
}
if binding.require_client_fingerprint
&& !matches!(
config.security.transport.tls.client_auth,
ClientAuthMode::Optional | ClientAuthMode::Require
)
{
anyhow::bail!(
"security.transport.token_binding.require_client_fingerprint=true requires \
client certificate authentication (client_auth must be \"optional\" or \"require\")"
);
}
if binding.subprotocol.trim().is_empty() {
anyhow::bail!("security.transport.token_binding.subprotocol must not be empty");
}
}
config.websocket.validate()?;
Ok(())
}
pub fn is_production_mode() -> bool {
use std::env;
if let Ok(mode) = env::var("SIGNAL_FISH__ENVIRONMENT") {
return mode.to_lowercase() == "production" || mode.to_lowercase() == "prod";
}
env::var("SIGNAL_FISH_PRODUCTION").is_ok()
|| env::var("PRODUCTION").is_ok()
|| env::var("PROD").is_ok()
}