This is an automated email from the ASF dual-hosted git repository.

piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new f4b0af5f7 feat(security): generate random JWT secrets when not 
configured (#2974)
f4b0af5f7 is described below

commit f4b0af5f7065e559b2ec52829e84dac1cd1b5d7e
Author: Piotr Gankiewicz <[email protected]>
AuthorDate: Fri Mar 20 14:37:29 2026 +0100

    feat(security): generate random JWT secrets when not configured (#2974)
    
    Deploying the HTTP server with hardcoded default JWT secrets
    is a security risk — every instance shares the same signing
    key. Empty defaults now trigger secure random secret
    generation at startup, with a warning logged showing a
    redacted preview of the generated value.
    
    When both encoding and decoding secrets are empty, a single
    random secret is generated and used for both (symmetric
    HMAC). Tokens are invalidated on restart, nudging operators
    to set persistent secrets in production.
---
 Cargo.lock                          |  2 +-
 DEPENDENCIES.md                     |  2 +-
 core/server/Cargo.toml              |  2 +-
 core/server/config.toml             |  6 +++--
 core/server/src/http/http_server.rs | 46 +++++++++++++++++++++++++++++++++----
 5 files changed, 48 insertions(+), 10 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 5421abad9..711b658df 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9625,7 +9625,7 @@ dependencies = [
 
 [[package]]
 name = "server"
-version = "0.7.2-edge.1"
+version = "0.7.3-edge.1"
 dependencies = [
  "ahash 0.8.12",
  "anyhow",
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index ca963183c..95c211adc 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -835,7 +835,7 @@ serde_with_macros: 3.17.0, "Apache-2.0 OR MIT",
 serde_yaml_ng: 0.10.0, "MIT",
 serial_test: 3.4.0, "MIT",
 serial_test_derive: 3.4.0, "MIT",
-server: 0.7.2-edge.1, "Apache-2.0",
+server: 0.7.3-edge.1, "Apache-2.0",
 sha1: 0.10.6, "Apache-2.0 OR MIT",
 sha2: 0.10.9, "Apache-2.0 OR MIT",
 sha3: 0.10.8, "Apache-2.0 OR MIT",
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index b1eb2a74f..5138288c3 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "server"
-version = "0.7.2-edge.1"
+version = "0.7.3-edge.1"
 edition = "2024"
 license = "Apache-2.0"
 
diff --git a/core/server/config.toml b/core/server/config.toml
index 03d0135f8..b3be68e58 100644
--- a/core/server/config.toml
+++ b/core/server/config.toml
@@ -114,10 +114,12 @@ clock_skew = "5 s"
 not_before = "0 s"
 
 # Secret key for encoding JWTs.
-encoding_secret = "top_secret$iggy123$_jwt_HS256_key#!"
+# If left empty, a secure random secret will be generated on each server start.
+encoding_secret = ""
 
 # Secret key for decoding JWTs.
-decoding_secret = "top_secret$iggy123$_jwt_HS256_key#!"
+# If left empty, a secure random secret will be generated on each server start.
+decoding_secret = ""
 
 # Indicates if the secret key is base64 encoded.
 # `true` means the secret is base64 encoded.
diff --git a/core/server/src/http/http_server.rs 
b/core/server/src/http/http_server.rs
index 4e71f3fe1..177e4f7ef 100644
--- a/core/server/src/http/http_server.rs
+++ b/core/server/src/http/http_server.rs
@@ -30,6 +30,7 @@ use crate::shard::task_registry::ShutdownToken;
 use crate::shard::tasks::periodic::spawn_jwt_token_cleaner;
 use crate::shard::transmission::event::ShardEvent;
 use crate::streaming::persistence::persister::PersisterKind;
+use crate::streaming::utils::crypto;
 use axum::extract::DefaultBodyLimit;
 use axum::extract::connect_info::Connected;
 use axum::http::Method;
@@ -45,7 +46,7 @@ use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 use tower_http::cors::{AllowOrigin, CorsLayer};
-use tracing::{error, info};
+use tracing::{error, info, warn};
 
 #[derive(Debug, Clone, Copy)]
 pub struct CompioSocketAddr(pub SocketAddr);
@@ -268,12 +269,47 @@ async fn build_app_state(
         tokens_path = shard.config.system.get_state_tokens_path();
     }
 
-    let jwt_manager = JwtManager::from_config(persister, &tokens_path, 
&config.jwt);
-    if let Err(e) = jwt_manager {
-        panic!("Failed to initialize JWT manager: {e}");
+    let mut jwt_config = config.jwt.clone();
+    let encoding_empty = jwt_config.encoding_secret.is_empty();
+    let decoding_empty = jwt_config.decoding_secret.is_empty();
+    match (encoding_empty, decoding_empty) {
+        (true, true) => {
+            let secret = crypto::generate_secret(32..64);
+            let redacted: String = secret.chars().take(3).collect();
+            warn!(
+                "JWT encoding and decoding secrets are not configured - 
generated a random secret: {redacted}***. JWT tokens will be invalidated on 
server restart. Set 'encoding_secret' and 'decoding_secret' in the config to 
use persistent secrets."
+            );
+            jwt_config.encoding_secret = secret.clone();
+            jwt_config.decoding_secret = secret;
+        }
+        (true, false) => {
+            warn!(
+                "JWT encoding secret is not configured but decoding secret is 
set - using decoding secret for both. Set 'encoding_secret' in the config to 
avoid this warning."
+            );
+            jwt_config.encoding_secret = jwt_config.decoding_secret.clone();
+        }
+        (false, true) => {
+            warn!(
+                "JWT decoding secret is not configured but encoding secret is 
set - using encoding secret for both. Set 'decoding_secret' in the config to 
avoid this warning."
+            );
+            jwt_config.decoding_secret = jwt_config.encoding_secret.clone();
+        }
+        (false, false) => {
+            if jwt_config.encoding_secret != jwt_config.decoding_secret
+                && jwt_config.algorithm.starts_with("HS")
+            {
+                warn!(
+                    "JWT encoding and decoding secrets are different but 
algorithm is {} (HMAC) - both secrets must be identical for symmetric 
algorithms.",
+                    jwt_config.algorithm
+                );
+            }
+        }
     }
 
-    let jwt_manager = jwt_manager.unwrap();
+    let jwt_manager = match JwtManager::from_config(persister, &tokens_path, 
&jwt_config) {
+        Ok(manager) => manager,
+        Err(error) => panic!("Failed to initialize JWT manager: {error}"),
+    };
     if jwt_manager.load_revoked_tokens().await.is_err() {
         panic!("Failed to load revoked access tokens");
     }

Reply via email to