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");
}