use std::collections::BTreeMap;
use std::fmt::Write as _;
use serde::Deserialize;
const MT_MAX_UPLOAD: u64 = 100 * 1024 * 1024;
const MT_MAX_BLOB: u64 = 64 * 1024 * 1024;
const MT_MAX_COMPONENT: u64 = 64 * 1024 * 1024;
const ST_MAX_UPLOAD: u64 = 1024 * 1024 * 1024;
const ST_MAX_BLOB: u64 = 256 * 1024 * 1024;
const ST_MAX_COMPONENT: u64 = 128 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum SecurityError {
#[error(
"unknown security profile {0:?} (built-ins: multi-tenant, single-tenant, dev; \
or define it under `security.profiles`)"
)]
UnknownProfile(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityProfile {
MultiTenant,
SingleTenant,
Dev,
}
impl std::fmt::Display for SecurityProfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for SecurityProfile {
type Err = SecurityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_name(s).ok_or_else(|| SecurityError::UnknownProfile(s.to_string()))
}
}
impl SecurityProfile {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"multi-tenant" => Some(Self::MultiTenant),
"single-tenant" => Some(Self::SingleTenant),
"dev" => Some(Self::Dev),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::MultiTenant => "multi-tenant",
Self::SingleTenant => "single-tenant",
Self::Dev => "dev",
}
}
pub fn preset(self) -> SecurityPosture {
match self {
Self::MultiTenant => SecurityPosture {
allow_unauthenticated_public_bind: false,
max_upload_bytes: MT_MAX_UPLOAD,
allow_site_unix_upstreams: false,
allow_site_private_upstreams: false,
max_handler_blob_bytes: MT_MAX_BLOB,
max_component_bytes: MT_MAX_COMPONENT,
oidc_require_audience: true,
domain_verify_allow_private: false,
domain_verify_self_serve: true,
allow_shared_kernel_compute: false,
ratelimit_fail_open: false,
allow_implicit_routing: false,
require_pop: false,
require_domain_verification: true,
},
Self::SingleTenant => SecurityPosture {
allow_unauthenticated_public_bind: false,
max_upload_bytes: ST_MAX_UPLOAD,
allow_site_unix_upstreams: true,
allow_site_private_upstreams: true,
max_handler_blob_bytes: ST_MAX_BLOB,
max_component_bytes: ST_MAX_COMPONENT,
oidc_require_audience: true,
domain_verify_allow_private: true,
domain_verify_self_serve: true,
allow_shared_kernel_compute: true,
ratelimit_fail_open: false,
allow_implicit_routing: true,
require_pop: false,
require_domain_verification: true,
},
Self::Dev => SecurityPosture {
allow_unauthenticated_public_bind: true,
max_upload_bytes: 0,
allow_site_unix_upstreams: true,
allow_site_private_upstreams: true,
max_handler_blob_bytes: 0,
max_component_bytes: 0,
oidc_require_audience: false,
domain_verify_allow_private: true,
domain_verify_self_serve: true,
allow_shared_kernel_compute: true,
ratelimit_fail_open: true,
allow_implicit_routing: true,
require_pop: false,
require_domain_verification: false,
},
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PostureOverrides {
pub allow_unauthenticated_public_bind: Option<bool>,
pub max_upload_bytes: Option<u64>,
pub allow_site_unix_upstreams: Option<bool>,
pub allow_site_private_upstreams: Option<bool>,
pub max_handler_blob_bytes: Option<u64>,
pub max_component_bytes: Option<u64>,
pub oidc_require_audience: Option<bool>,
pub domain_verify_allow_private: Option<bool>,
pub domain_verify_self_serve: Option<bool>,
pub allow_shared_kernel_compute: Option<bool>,
pub ratelimit_fail_open: Option<bool>,
pub allow_implicit_routing: Option<bool>,
pub require_pop: Option<bool>,
pub require_domain_verification: Option<bool>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct SecurityConfig {
pub profile: Option<String>,
pub profiles: BTreeMap<String, PostureOverrides>,
pub overrides: PostureOverrides,
}
impl SecurityConfig {
fn base_for(&self, name: &str) -> Result<SecurityPosture, SecurityError> {
if let Some(builtin) = SecurityProfile::from_name(name) {
Ok(builtin.preset())
} else if let Some(custom) = self.profiles.get(name) {
Ok(apply(SecurityProfile::MultiTenant.preset(), custom))
} else {
Err(SecurityError::UnknownProfile(name.to_string()))
}
}
pub fn resolve(&self) -> Result<SecurityPosture, SecurityError> {
let name = self.profile.as_deref().unwrap_or("multi-tenant");
Ok(apply(self.base_for(name)?, &self.overrides))
}
pub fn explain(&self) -> Result<String, SecurityError> {
let name = self.profile.as_deref().unwrap_or("multi-tenant");
let p = self.resolve()?;
let o = &self.overrides;
let mut out = String::new();
let _ = writeln!(out, "security profile: {name}");
let mut row = |label: &str, value: String, overridden: bool| {
let src = if overridden { "override" } else { "profile" };
let _ = writeln!(out, " {label:<34} {value:<12} ({src})");
};
row(
"allow_unauthenticated_public_bind",
p.allow_unauthenticated_public_bind.to_string(),
o.allow_unauthenticated_public_bind.is_some(),
);
row(
"max_upload_bytes",
fmt_cap(p.max_upload_bytes),
o.max_upload_bytes.is_some(),
);
row(
"allow_site_unix_upstreams",
p.allow_site_unix_upstreams.to_string(),
o.allow_site_unix_upstreams.is_some(),
);
row(
"allow_site_private_upstreams",
p.allow_site_private_upstreams.to_string(),
o.allow_site_private_upstreams.is_some(),
);
row(
"max_handler_blob_bytes",
fmt_cap(p.max_handler_blob_bytes),
o.max_handler_blob_bytes.is_some(),
);
row(
"max_component_bytes",
fmt_cap(p.max_component_bytes),
o.max_component_bytes.is_some(),
);
row(
"oidc_require_audience",
p.oidc_require_audience.to_string(),
o.oidc_require_audience.is_some(),
);
row(
"domain_verify_allow_private",
p.domain_verify_allow_private.to_string(),
o.domain_verify_allow_private.is_some(),
);
row(
"domain_verify_self_serve",
p.domain_verify_self_serve.to_string(),
o.domain_verify_self_serve.is_some(),
);
row(
"allow_shared_kernel_compute",
p.allow_shared_kernel_compute.to_string(),
o.allow_shared_kernel_compute.is_some(),
);
row(
"ratelimit_fail_open",
p.ratelimit_fail_open.to_string(),
o.ratelimit_fail_open.is_some(),
);
row(
"allow_implicit_routing",
p.allow_implicit_routing.to_string(),
o.allow_implicit_routing.is_some(),
);
row(
"require_pop",
p.require_pop.to_string(),
o.require_pop.is_some(),
);
Ok(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecurityPosture {
pub allow_unauthenticated_public_bind: bool,
pub max_upload_bytes: u64,
pub allow_site_unix_upstreams: bool,
pub allow_site_private_upstreams: bool,
pub max_handler_blob_bytes: u64,
pub max_component_bytes: u64,
pub oidc_require_audience: bool,
pub domain_verify_allow_private: bool,
pub domain_verify_self_serve: bool,
pub allow_shared_kernel_compute: bool,
pub ratelimit_fail_open: bool,
pub allow_implicit_routing: bool,
pub require_pop: bool,
pub require_domain_verification: bool,
}
impl Default for SecurityPosture {
fn default() -> Self {
SecurityProfile::MultiTenant.preset()
}
}
fn apply(mut base: SecurityPosture, o: &PostureOverrides) -> SecurityPosture {
if let Some(v) = o.allow_unauthenticated_public_bind {
base.allow_unauthenticated_public_bind = v;
}
if let Some(v) = o.max_upload_bytes {
base.max_upload_bytes = v;
}
if let Some(v) = o.allow_site_unix_upstreams {
base.allow_site_unix_upstreams = v;
}
if let Some(v) = o.allow_site_private_upstreams {
base.allow_site_private_upstreams = v;
}
if let Some(v) = o.max_handler_blob_bytes {
base.max_handler_blob_bytes = v;
}
if let Some(v) = o.max_component_bytes {
base.max_component_bytes = v;
}
if let Some(v) = o.oidc_require_audience {
base.oidc_require_audience = v;
}
if let Some(v) = o.domain_verify_allow_private {
base.domain_verify_allow_private = v;
}
if let Some(v) = o.domain_verify_self_serve {
base.domain_verify_self_serve = v;
}
if let Some(v) = o.allow_shared_kernel_compute {
base.allow_shared_kernel_compute = v;
}
if let Some(v) = o.ratelimit_fail_open {
base.ratelimit_fail_open = v;
}
if let Some(v) = o.allow_implicit_routing {
base.allow_implicit_routing = v;
}
if let Some(v) = o.require_pop {
base.require_pop = v;
}
if let Some(v) = o.require_domain_verification {
base.require_domain_verification = v;
}
base
}
fn fmt_cap(bytes: u64) -> String {
if bytes == 0 {
"unlimited".to_string()
} else {
bytes.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_posture_is_multi_tenant_strict() {
let p = SecurityPosture::default();
assert_eq!(p, SecurityProfile::MultiTenant.preset());
assert!(!p.allow_unauthenticated_public_bind);
assert!(!p.allow_site_unix_upstreams);
assert!(!p.allow_site_private_upstreams);
assert!(p.oidc_require_audience);
assert!(!p.domain_verify_allow_private);
assert!(p.domain_verify_self_serve);
assert!(!p.allow_shared_kernel_compute);
assert!(!p.ratelimit_fail_open);
assert!(!p.allow_implicit_routing);
assert!(!p.require_pop);
assert_eq!(p.max_upload_bytes, MT_MAX_UPLOAD);
}
#[test]
fn require_pop_defaults_off_everywhere_and_overrides() {
for profile in [
SecurityProfile::MultiTenant,
SecurityProfile::SingleTenant,
SecurityProfile::Dev,
] {
assert!(!profile.preset().require_pop, "{}", profile.as_str());
}
let cfg = SecurityConfig {
overrides: PostureOverrides {
require_pop: Some(true),
..Default::default()
},
..Default::default()
};
assert!(cfg.resolve().unwrap().require_pop);
assert!(cfg
.explain()
.unwrap()
.lines()
.any(|l| l.contains("require_pop") && l.contains("true") && l.contains("override")));
}
#[test]
fn empty_config_resolves_to_multi_tenant() {
let resolved = SecurityConfig::default().resolve().unwrap();
assert_eq!(resolved, SecurityProfile::MultiTenant.preset());
}
#[test]
fn dev_profile_is_loose() {
let cfg = SecurityConfig {
profile: Some("dev".into()),
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(p.allow_unauthenticated_public_bind);
assert!(!p.oidc_require_audience);
assert_eq!(p.max_upload_bytes, 0); assert!(p.ratelimit_fail_open);
assert!(p.allow_implicit_routing);
}
#[test]
fn override_beats_profile() {
let cfg = SecurityConfig {
profile: Some("dev".into()),
overrides: PostureOverrides {
oidc_require_audience: Some(true),
max_upload_bytes: Some(123),
..Default::default()
},
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(
p.oidc_require_audience,
"override must win over the profile"
);
assert_eq!(p.max_upload_bytes, 123);
assert!(p.allow_unauthenticated_public_bind);
}
#[test]
fn custom_profile_layers_over_multi_tenant_baseline() {
let mut profiles = BTreeMap::new();
profiles.insert(
"ci".to_string(),
PostureOverrides {
allow_unauthenticated_public_bind: Some(true),
..Default::default()
},
);
let cfg = SecurityConfig {
profile: Some("ci".into()),
profiles,
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(p.allow_unauthenticated_public_bind);
assert!(!p.allow_site_private_upstreams);
assert!(p.oidc_require_audience);
}
#[test]
fn unknown_profile_errors() {
let cfg = SecurityConfig {
profile: Some("nope".into()),
..Default::default()
};
assert!(matches!(
cfg.resolve(),
Err(SecurityError::UnknownProfile(name)) if name == "nope"
));
}
#[test]
fn explain_marks_value_source() {
let cfg = SecurityConfig {
profile: Some("multi-tenant".into()),
overrides: PostureOverrides {
max_upload_bytes: Some(0),
..Default::default()
},
..Default::default()
};
let text = cfg.explain().unwrap();
assert!(text.contains("security profile: multi-tenant"));
assert!(text.lines().any(|l| l.contains("max_upload_bytes")
&& l.contains("unlimited")
&& l.contains("override")));
assert!(text
.lines()
.any(|l| l.contains("oidc_require_audience") && l.contains("profile")));
}
}