From 84ccf257befa2c1a78a7a7a67e43d4e17284fe8f Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:48:42 +0200 Subject: [PATCH 1/5] feat(auth): Default auth enforcement to true Auth enforcement has been enabled in all regions and tenants. Flip the default so config or templating issues fail closed instead of open. --- devservices/config.yml | 1 + .../config/emulators.example.yaml | 3 + objectstore-server/config/local.example.yaml | 3 + objectstore-server/docs/architecture.md | 10 ++-- objectstore-server/src/config.rs | 58 ++++++++++++++++++- objectstore-server/src/state.rs | 38 ++++++++++++ objectstore-server/tests/limits.rs | 28 +++++++++ 7 files changed, 133 insertions(+), 8 deletions(-) diff --git a/devservices/config.yml b/devservices/config.yml index a4c1ed39..eef642e1 100644 --- a/devservices/config.yml +++ b/devservices/config.yml @@ -27,6 +27,7 @@ services: OS__STORAGE__TYPE: "filesystem" OS__STORAGE__PATH: "/data" OS__LOGGING__LEVEL: "debug" + OS__AUTH__ENFORCE: "false" command: run healthcheck: test: ["CMD", "/bin/entrypoint", "healthcheck"] diff --git a/objectstore-server/config/emulators.example.yaml b/objectstore-server/config/emulators.example.yaml index c4e51e98..73db982d 100644 --- a/objectstore-server/config/emulators.example.yaml +++ b/objectstore-server/config/emulators.example.yaml @@ -14,6 +14,9 @@ storage: endpoint: http://localhost:8087 bucket: test-bucket +auth: + enforce: false + logging: level: trace format: auto diff --git a/objectstore-server/config/local.example.yaml b/objectstore-server/config/local.example.yaml index 8a33256c..b12a34e2 100644 --- a/objectstore-server/config/local.example.yaml +++ b/objectstore-server/config/local.example.yaml @@ -5,6 +5,9 @@ storage: type: filesystem path: data +auth: + enforce: false + logging: level: trace format: auto diff --git a/objectstore-server/docs/architecture.md b/objectstore-server/docs/architecture.md index 69440a01..b31f6d1f 100644 --- a/objectstore-server/docs/architecture.md +++ b/objectstore-server/docs/architecture.md @@ -62,9 +62,9 @@ A request flows through several layers before reaching the storage service: ## Authentication & Authorization Objectstore uses **JWT tokens with EdDSA signatures** (Ed25519) for -authentication. Auth enforcement is optional and controlled by the -[`auth.enforce`](config) config flag, allowing unauthenticated development -setups. +authentication. Auth enforcement is **enabled by default** and controlled by the +[`auth.enforce`](config) config flag. Set `enforce: false` explicitly for +unauthenticated development setups. ### Token Structure @@ -100,8 +100,8 @@ this precedence (highest wins): 1. **Environment variables** — prefixed with `OS__`, using `__` as a nested separator. Example: `OS__STORAGE__TYPE=tiered` 2. **YAML file** — passed via the `-c` / `--config` CLI flag -3. **Defaults** — sensible development defaults (local filesystem backend, - auth disabled) +3. **Defaults** — sensible defaults (local filesystem backend, + auth enabled) Key configuration sections: - `storage` — backend type and connection parameters; use `type: tiered` for diff --git a/objectstore-server/src/config.rs b/objectstore-server/src/config.rs index e9ad02b6..297ffecd 100644 --- a/objectstore-server/src/config.rs +++ b/objectstore-server/src/config.rs @@ -333,12 +333,13 @@ pub struct AuthZVerificationKey { } /// Configuration for content-based authorization. -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct AuthZ { /// Whether to enforce content-based authorization or not. /// - /// If this is set to `false`, checks are still performed but failures will not result - /// in `403 Unauthorized` responses. + /// Defaults to `true`. If this is set to `false`, checks are still performed but failures + /// will not result in `403 Unauthorized` responses. + #[serde(default = "default_enforce")] pub enforce: bool, /// Keys that may be used to verify a request's auth token. @@ -352,6 +353,19 @@ pub struct AuthZ { pub keys: BTreeMap, } +fn default_enforce() -> bool { + true +} + +impl Default for AuthZ { + fn default() -> Self { + Self { + enforce: true, + keys: BTreeMap::new(), + } + } +} + /// Main configuration struct for the objectstore server. /// /// This is the top-level configuration that combines all server settings including networking, @@ -892,6 +906,44 @@ mod tests { }); } + #[test] + fn auth_enforce_defaults_to_true() { + figment::Jail::expect_with(|_jail| { + let config = Config::load(None).unwrap(); + assert!(config.auth.enforce); + Ok(()) + }); + } + + #[test] + fn auth_enforce_defaults_to_true_when_omitted_from_yaml() { + let mut tempfile = tempfile::NamedTempFile::new().unwrap(); + tempfile + .write_all( + br#" + auth: + keys: {} + "#, + ) + .unwrap(); + + figment::Jail::expect_with(|_jail| { + let config = Config::load(Some(tempfile.path())).unwrap(); + assert!(config.auth.enforce); + Ok(()) + }); + } + + #[test] + fn auth_enforce_can_be_disabled() { + figment::Jail::expect_with(|jail| { + jail.set_env("OS__AUTH__ENFORCE", "false"); + let config = Config::load(None).unwrap(); + assert!(!config.auth.enforce); + Ok(()) + }); + } + #[test] fn configure_killswitches_with_yaml() { let mut tempfile = tempfile::NamedTempFile::new().unwrap(); diff --git a/objectstore-server/src/state.rs b/objectstore-server/src/state.rs index 7ced6b77..9f54f489 100644 --- a/objectstore-server/src/state.rs +++ b/objectstore-server/src/state.rs @@ -64,6 +64,11 @@ impl Services { service.start(); let key_directory = PublicKeyDirectory::try_from(&config.auth)?; + if config.auth.enforce && key_directory.keys.is_empty() { + anyhow::bail!( + "Auth enforcement is enabled but no keys are configured. Either disable auth enforcement (dev/test environments) or configure a public key." + ); + } let rate_limiter = RateLimiter::new(config.rate_limits.clone()); rate_limiter.start(); @@ -132,3 +137,36 @@ async fn track_runtime_metrics(interval: Duration) { ); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn enforce_without_keys_fails_startup() { + let config = Config { + auth: crate::config::AuthZ { + enforce: true, + ..Default::default() + }, + ..Default::default() + }; + let err = Services::spawn(config).await.unwrap_err(); + assert!( + err.to_string() + .contains("auth enforcement is enabled but no keys are configured"), + ); + } + + #[tokio::test] + async fn no_enforce_without_keys_starts_ok() { + let config = Config { + auth: crate::config::AuthZ { + enforce: false, + ..Default::default() + }, + ..Default::default() + }; + assert!(Services::spawn(config).await.is_ok()); + } +} diff --git a/objectstore-server/tests/limits.rs b/objectstore-server/tests/limits.rs index f0b9d7e0..50399ed9 100644 --- a/objectstore-server/tests/limits.rs +++ b/objectstore-server/tests/limits.rs @@ -113,6 +113,10 @@ async fn test_killswitches() -> Result<()> { #[tokio::test] async fn test_throughput_global_rps_limit() -> Result<()> { let server = TestServer::with_config(Config { + auth: AuthZ { + enforce: false, + ..Default::default() + }, rate_limits: RateLimits { throughput: ThroughputLimits { global_rps: Some(2), @@ -159,6 +163,10 @@ async fn test_throughput_global_rps_limit() -> Result<()> { #[tokio::test] async fn test_throughput_usecase_pct_limit() -> Result<()> { let server = TestServer::with_config(Config { + auth: AuthZ { + enforce: false, + ..Default::default() + }, rate_limits: RateLimits { throughput: ThroughputLimits { global_rps: Some(100), @@ -213,6 +221,10 @@ async fn test_throughput_usecase_pct_limit() -> Result<()> { #[tokio::test] async fn test_throughput_scope_pct_limit() -> Result<()> { let server = TestServer::with_config(Config { + auth: AuthZ { + enforce: false, + ..Default::default() + }, rate_limits: RateLimits { throughput: ThroughputLimits { global_rps: Some(100), @@ -267,6 +279,10 @@ async fn test_throughput_scope_pct_limit() -> Result<()> { #[tokio::test] async fn test_throughput_rule() -> Result<()> { let server = TestServer::with_config(Config { + auth: AuthZ { + enforce: false, + ..Default::default() + }, rate_limits: RateLimits { throughput: ThroughputLimits { global_rps: None, @@ -331,6 +347,10 @@ async fn test_throughput_rule() -> Result<()> { #[tokio::test] async fn test_bandwidth_global_bps_limit() -> Result<()> { let server = TestServer::with_config(Config { + auth: AuthZ { + enforce: false, + ..Default::default() + }, rate_limits: RateLimits { bandwidth: BandwidthLimits { global_bps: Some(500), @@ -385,6 +405,10 @@ async fn test_bandwidth_global_bps_limit() -> Result<()> { #[tokio::test] async fn test_bandwidth_usecase_pct_limit() -> Result<()> { let server = TestServer::with_config(Config { + auth: AuthZ { + enforce: false, + ..Default::default() + }, rate_limits: RateLimits { bandwidth: BandwidthLimits { global_bps: Some(100_000), @@ -446,6 +470,10 @@ async fn test_bandwidth_usecase_pct_limit() -> Result<()> { #[tokio::test] async fn test_bandwidth_scope_pct_limit() -> Result<()> { let server = TestServer::with_config(Config { + auth: AuthZ { + enforce: false, + ..Default::default() + }, rate_limits: RateLimits { bandwidth: BandwidthLimits { global_bps: Some(100_000), From eb880bc465f3d636af133a05c0c2e54c7d95b7dc Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:52:49 +0200 Subject: [PATCH 2/5] wip --- objectstore-server/src/config.rs | 9 --------- objectstore-server/src/state.rs | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/objectstore-server/src/config.rs b/objectstore-server/src/config.rs index 297ffecd..70d6fbb6 100644 --- a/objectstore-server/src/config.rs +++ b/objectstore-server/src/config.rs @@ -357,15 +357,6 @@ fn default_enforce() -> bool { true } -impl Default for AuthZ { - fn default() -> Self { - Self { - enforce: true, - keys: BTreeMap::new(), - } - } -} - /// Main configuration struct for the objectstore server. /// /// This is the top-level configuration that combines all server settings including networking, diff --git a/objectstore-server/src/state.rs b/objectstore-server/src/state.rs index 9f54f489..35e608da 100644 --- a/objectstore-server/src/state.rs +++ b/objectstore-server/src/state.rs @@ -154,7 +154,7 @@ mod tests { let err = Services::spawn(config).await.unwrap_err(); assert!( err.to_string() - .contains("auth enforcement is enabled but no keys are configured"), + .contains("Auth enforcement is enabled but no keys are configured"), ); } From 6fe8fb3458f1d65db20751c2776635973dfff194 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:55:53 +0200 Subject: [PATCH 3/5] wip --- objectstore-server/src/config.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/objectstore-server/src/config.rs b/objectstore-server/src/config.rs index 70d6fbb6..297ffecd 100644 --- a/objectstore-server/src/config.rs +++ b/objectstore-server/src/config.rs @@ -357,6 +357,15 @@ fn default_enforce() -> bool { true } +impl Default for AuthZ { + fn default() -> Self { + Self { + enforce: true, + keys: BTreeMap::new(), + } + } +} + /// Main configuration struct for the objectstore server. /// /// This is the top-level configuration that combines all server settings including networking, From bb98ab00edf99719a9c2359b557940a7a36cf871 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:03:00 +0200 Subject: [PATCH 4/5] fix(test): Disable auth enforcement in tests that don't use tokens The stores_structured_keys e2e test and the stresstest spawn servers without auth tokens, so they need enforce=false after the default flip. --- clients/rust/tests/e2e.rs | 9 ++++++++- objectstore-server/tests/stresstest.rs | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/clients/rust/tests/e2e.rs b/clients/rust/tests/e2e.rs index 2982200c..6e0ba631 100644 --- a/clients/rust/tests/e2e.rs +++ b/clients/rust/tests/e2e.rs @@ -172,7 +172,14 @@ async fn stores_under_given_key() { #[tokio::test] async fn stores_structured_keys() { - let server = TestServer::new().await; + let server = TestServer::with_config(config::Config { + auth: config::AuthZ { + enforce: false, + ..Default::default() + }, + ..Default::default() + }) + .await; let client = Client::builder(server.url("/")).build().unwrap(); let usecase = Usecase::new("usecase"); diff --git a/objectstore-server/tests/stresstest.rs b/objectstore-server/tests/stresstest.rs index 64912530..595a413a 100644 --- a/objectstore-server/tests/stresstest.rs +++ b/objectstore-server/tests/stresstest.rs @@ -32,7 +32,8 @@ async fn test_basic() { .env("OS__HTTP_ADDR", &addr) .env("OS__STORAGE__TYPE", "filesystem") .env("OS__STORAGE__PATH", tempdir.path().display().to_string()) - .env("OS__LOGGING__LEVEL", "warn"); + .env("OS__LOGGING__LEVEL", "warn") + .env("OS__AUTH__ENFORCE", "false"); if let Ok(statsd_host) = std::env::var("STATSD_HOST") { cmd.env("OS__METRICS__ADDR", statsd_host); } From 1f7a6889e213fd57dd84b8509a97b26bf9948382 Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:00:31 +0200 Subject: [PATCH 5/5] fix(test): Use auth token in stores_structured_keys instead of disabling enforcement --- clients/rust/tests/e2e.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/clients/rust/tests/e2e.rs b/clients/rust/tests/e2e.rs index 6e0ba631..07e8707a 100644 --- a/clients/rust/tests/e2e.rs +++ b/clients/rust/tests/e2e.rs @@ -172,16 +172,12 @@ async fn stores_under_given_key() { #[tokio::test] async fn stores_structured_keys() { - let server = TestServer::with_config(config::Config { - auth: config::AuthZ { - enforce: false, - ..Default::default() - }, - ..Default::default() - }) - .await; + let server = test_server().await; - let client = Client::builder(server.url("/")).build().unwrap(); + let client = Client::builder(server.url("/")) + .token(test_token_generator()) + .build() + .unwrap(); let usecase = Usecase::new("usecase"); let session = client.session(usecase.for_project(12345, 1337)).unwrap();