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

quantranhong1999 pushed a commit to branch 3.8.x
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 0e13169072e602512450dc44f9aaf44f36a00c6b
Author: Benoit TELLIER <[email protected]>
AuthorDate: Tue Jul 28 23:11:07 2026 +0200

    [ENHANCEMENT] Webadmin: default to auto-generate creds
---
 CHANGELOG.md                                       |  7 +++
 .../docker-configuration/webadmin.properties       |  8 ++-
 .../sample-configuration/webadmin.properties       | 13 +++-
 .../docker-configuration/webadmin.properties       | 10 ++-
 .../modules/ROOT/pages/configure/webadmin.adoc     | 20 ++++++
 .../docs/modules/ROOT/pages/run/run-docker.adoc    |  3 +-
 .../sample-configuration/webadmin.properties       | 13 +++-
 .../docker-configuration/webadmin.properties       | 10 ++-
 .../sample-configuration/webadmin.properties       | 13 +++-
 .../docker-configuration/webadmin.properties       | 10 ++-
 .../sample-configuration/webadmin.properties       | 13 +++-
 .../docker-configuration/webadmin.properties       | 10 ++-
 .../sample-configuration/webadmin.properties       | 13 +++-
 .../docker-configuration/webadmin.properties       | 10 ++-
 .../sample-configuration/webadmin.properties       | 13 +++-
 .../src/test/resources/webadmin.properties         |  5 +-
 .../james/modules/server/WebAdminServerModule.java | 34 +++++++++-
 .../modules/server/WebAdminServerModuleTest.java   | 72 ++++++++++++++++++++++
 .../src/test/resources/webadmin.properties         |  5 +-
 .../src/test/resources/webadmin.properties         |  5 +-
 .../webadmin/authentication/PasswordGenerator.java | 45 ++++++++++++++
 21 files changed, 314 insertions(+), 18 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83127c9d1c..05c199a4e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/)
 
 ## [Unreleased]
 
+### Security
+
+ - Webadmin `password.generate`: generate a random password upon start up when 
none is configured, and log it.
+ **Breaking change**: this defaults to `true`, thus WebAdmin is no longer 
unauthenticated out of the box, existing
+ deployments included. Set `password.generate=false` in `webadmin.properties` 
to opt back into an unauthenticated
+ WebAdmin, or configure `password` to pin a stable secret.
+
 This release brings the following significant changes:
 
  - Upgrade TCP protocols to Netty 4
diff --git a/server/apps/cassandra-app/docker-configuration/webadmin.properties 
b/server/apps/cassandra-app/docker-configuration/webadmin.properties
index 5d72d99b74..3a8d67c93f 100644
--- a/server/apps/cassandra-app/docker-configuration/webadmin.properties
+++ b/server/apps/cassandra-app/docker-configuration/webadmin.properties
@@ -51,4 +51,10 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Generate a random password upon start up when no password is configured.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
diff --git a/server/apps/cassandra-app/sample-configuration/webadmin.properties 
b/server/apps/cassandra-app/sample-configuration/webadmin.properties
index b97c71829c..0727d221be 100644
--- a/server/apps/cassandra-app/sample-configuration/webadmin.properties
+++ b/server/apps/cassandra-app/sample-configuration/webadmin.properties
@@ -52,4 +52,15 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
+
+# Configure one or more passwords (comma separated) for WebAdmin authentication
+#password=secret1,secret2
diff --git 
a/server/apps/distributed-app/docker-configuration/webadmin.properties 
b/server/apps/distributed-app/docker-configuration/webadmin.properties
index 5d72d99b74..42c276ec18 100644
--- a/server/apps/distributed-app/docker-configuration/webadmin.properties
+++ b/server/apps/distributed-app/docker-configuration/webadmin.properties
@@ -51,4 +51,12 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
diff --git 
a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/webadmin.adoc 
b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/webadmin.adoc
index cf378154ea..49f7b42b53 100644
--- 
a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/webadmin.adoc
+++ 
b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/webadmin.adoc
@@ -35,6 +35,26 @@ to get some examples and hints.
 | cors.origin
 | Specify ths CORS origin (default: null)
 
+| password.generate
+| Generates a random password upon start up when no `password` is configured, 
allowing a secure setup without
+hardcoded credentials (default: true). As such, WebAdmin is never 
unauthenticated unless explicitly asked for.
+
+....
+password.generate=false
+....
+
+The generated password is written in the logs upon start up:
+
+....
+WARN  No WebAdmin password had been configured: a random one had been 
generated for this run. [...]
+Generated WebAdmin password: 8Kj2mXqT4vZ...
+....
+
+Beware: the generated password changes upon each restart, and is exposed to 
whoever can read the logs. Configure
+`password` explicitly for setups needing a stable secret, or set 
`password.generate=false` to opt back into an
+unauthenticated WebAdmin. This option is ignored when `password` is 
configured, when `jwt.enabled` is true, and when
+WebAdmin is disabled.
+
 | password
 | Uses a configured static value for authentication. It relies on the 
`Password` header.
 It supports several passwords, configured as a comma-separated list.
diff --git 
a/server/apps/distributed-app/docs/modules/ROOT/pages/run/run-docker.adoc 
b/server/apps/distributed-app/docs/modules/ROOT/pages/run/run-docker.adoc
index 449ac3039d..7d33238f5e 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/run/run-docker.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/run/run-docker.adoc
@@ -82,7 +82,8 @@ Where :
 
 - HOSTNAME: is the hostname you want to give to your James container. This DNS 
entry will be used to send mail to your James server.
 
-Webadmin port binding is restricted to loopback as users are not authenticated 
by default on webadmin server. Thus you should avoid exposing it in production.
+Webadmin port binding is restricted to loopback. Webadmin is protected by a 
password randomly generated upon
+each start up and written in the logs (see `password.generate`), yet you 
should avoid exposing it in production.
 Note that the above example assumes `127.0.0.1` is your loopback interface for 
convenience but you should change it if this is not the case on your machine.
 
 If you want to pass additional options to the underlying java command, you can 
configure a _JAVA_TOOL_OPTIONS_ env variable, for example add:
diff --git 
a/server/apps/distributed-app/sample-configuration/webadmin.properties 
b/server/apps/distributed-app/sample-configuration/webadmin.properties
index b97c71829c..0727d221be 100644
--- a/server/apps/distributed-app/sample-configuration/webadmin.properties
+++ b/server/apps/distributed-app/sample-configuration/webadmin.properties
@@ -52,4 +52,15 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
+
+# Configure one or more passwords (comma separated) for WebAdmin authentication
+#password=secret1,secret2
diff --git 
a/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties 
b/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties
index 5d72d99b74..42c276ec18 100644
--- a/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties
+++ b/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties
@@ -51,4 +51,12 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
diff --git 
a/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties 
b/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties
index b97c71829c..0727d221be 100644
--- a/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties
+++ b/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties
@@ -52,4 +52,15 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
+
+# Configure one or more passwords (comma separated) for WebAdmin authentication
+#password=secret1,secret2
diff --git a/server/apps/jpa-app/docker-configuration/webadmin.properties 
b/server/apps/jpa-app/docker-configuration/webadmin.properties
index 5d72d99b74..42c276ec18 100644
--- a/server/apps/jpa-app/docker-configuration/webadmin.properties
+++ b/server/apps/jpa-app/docker-configuration/webadmin.properties
@@ -51,4 +51,12 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
diff --git a/server/apps/jpa-app/sample-configuration/webadmin.properties 
b/server/apps/jpa-app/sample-configuration/webadmin.properties
index 5dc74740c5..c8e71f6842 100644
--- a/server/apps/jpa-app/sample-configuration/webadmin.properties
+++ b/server/apps/jpa-app/sample-configuration/webadmin.properties
@@ -46,4 +46,15 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
+
+# Configure one or more passwords (comma separated) for WebAdmin authentication
+#password=secret1,secret2
diff --git a/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties 
b/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties
index 5d72d99b74..42c276ec18 100644
--- a/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties
+++ b/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties
@@ -51,4 +51,12 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
diff --git a/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties 
b/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties
index 5dc74740c5..c8e71f6842 100644
--- a/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties
+++ b/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties
@@ -46,4 +46,15 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
+
+# Configure one or more passwords (comma separated) for WebAdmin authentication
+#password=secret1,secret2
diff --git a/server/apps/memory-app/docker-configuration/webadmin.properties 
b/server/apps/memory-app/docker-configuration/webadmin.properties
index 5d72d99b74..42c276ec18 100644
--- a/server/apps/memory-app/docker-configuration/webadmin.properties
+++ b/server/apps/memory-app/docker-configuration/webadmin.properties
@@ -51,4 +51,12 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
diff --git a/server/apps/memory-app/sample-configuration/webadmin.properties 
b/server/apps/memory-app/sample-configuration/webadmin.properties
index 3449eedc92..8227ddd772 100644
--- a/server/apps/memory-app/sample-configuration/webadmin.properties
+++ b/server/apps/memory-app/sample-configuration/webadmin.properties
@@ -53,4 +53,15 @@ https.enabled=false
 # List of fully qualified class names that should be exposed over webadmin
 # in addition to your product default routes. Routes needs to be located
 # within the classpath or in the ./extensions-jars folder.
-#extensions.routes=
\ No newline at end of file
+#extensions.routes=
+
+# Password authentication settings
+
+# Generate a random password upon start up when no password is configured 
below.
+# The generated password is written in the logs, and changes upon each restart.
+# Set it to false to run WebAdmin without any authentication.
+# Defaults to true
+password.generate=true
+
+# Configure one or more passwords (comma separated) for WebAdmin authentication
+#password=secret1,secret2
diff --git a/server/apps/webadmin-cli/src/test/resources/webadmin.properties 
b/server/apps/webadmin-cli/src/test/resources/webadmin.properties
index 3386a14238..435136da30 100644
--- a/server/apps/webadmin-cli/src/test/resources/webadmin.properties
+++ b/server/apps/webadmin-cli/src/test/resources/webadmin.properties
@@ -22,4 +22,7 @@
 
 enabled=true
 port=0
-host=127.0.0.1
\ No newline at end of file
+host=127.0.0.1
+
+# These tests exercise WebAdmin routes without authentication
+password.generate=false
diff --git 
a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
 
b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
index 41986bf9e8..469a3d6f36 100644
--- 
a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
+++ 
b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java
@@ -60,6 +60,7 @@ import 
org.apache.james.webadmin.authentication.AuthenticationFilter;
 import org.apache.james.webadmin.authentication.JwtFilter;
 import org.apache.james.webadmin.authentication.NoAuthenticationFilter;
 import org.apache.james.webadmin.authentication.PasswordFilter;
+import org.apache.james.webadmin.authentication.PasswordGenerator;
 import org.apache.james.webadmin.dto.DTOModuleInjections;
 import org.apache.james.webadmin.mdc.RequestLogger;
 import org.apache.james.webadmin.utils.JsonTransformer;
@@ -83,6 +84,7 @@ public class WebAdminServerModule extends AbstractModule {
 
     private static final boolean DEFAULT_JWT_DISABLED = false;
     private static final boolean DEFAULT_DISABLED = false;
+    private static final boolean DEFAULT_PASSWORD_GENERATION_ENABLED = true;
     private static final String DEFAULT_NO_CORS_ORIGIN = null;
     private static final boolean DEFAULT_CORS_DISABLED = false;
     private static final String DEFAULT_NO_KEYSTORE = null;
@@ -142,9 +144,10 @@ public class WebAdminServerModule extends AbstractModule {
             Configuration configurationFile = 
propertiesProvider.getConfiguration("webadmin");
 
             List<String> additionalRoutes = 
additionalRoutes(configurationFile);
+            boolean webAdminEnabled = configurationFile.getBoolean("enabled", 
DEFAULT_DISABLED);
 
             return WebAdminConfiguration.builder()
-                .enable(configurationFile.getBoolean("enabled", 
DEFAULT_DISABLED))
+                .enable(webAdminEnabled)
                 .port(port(configurationFile))
                 .tls(readHttpsConfiguration(configurationFile))
                 .enableCORS(configurationFile.getBoolean("cors.enable", 
DEFAULT_CORS_DISABLED))
@@ -155,7 +158,7 @@ public class WebAdminServerModule extends AbstractModule {
                     
Optional.ofNullable(configurationFile.getString("jwt.publickeypem.url", null))))
                 
.maxThreadCount(Optional.ofNullable(configurationFile.getInteger("maxThreadCount",
 null)))
                 
.minThreadCount(Optional.ofNullable(configurationFile.getInteger("minThreadCount",
 null)))
-                
.password(Optional.ofNullable(configurationFile.getString("password", null)))
+                .password(password(configurationFile, webAdminEnabled))
                 .build();
         } catch (FileNotFoundException e) {
             LOGGER.info("No webadmin.properties file. Disabling WebAdmin 
interface.");
@@ -172,6 +175,33 @@ public class WebAdminServerModule extends AbstractModule {
         return new FixedPortSupplier(portNumber);
     }
 
+    @VisibleForTesting
+    Optional<String> password(Configuration configurationFile, boolean 
webAdminEnabled) {
+        Optional<String> configuredPassword = 
Optional.ofNullable(configurationFile.getString("password", null));
+
+        if (configuredPassword.isPresent()) {
+            return configuredPassword;
+        }
+        if (shouldGeneratePassword(configurationFile, webAdminEnabled)) {
+            return Optional.of(generateAndLogPassword());
+        }
+        return Optional.empty();
+    }
+
+    private boolean shouldGeneratePassword(Configuration configurationFile, 
boolean webAdminEnabled) {
+        return configurationFile.getBoolean("password.generate", 
DEFAULT_PASSWORD_GENERATION_ENABLED)
+            && webAdminEnabled
+            && !configurationFile.getBoolean("jwt.enabled", 
DEFAULT_JWT_DISABLED);
+    }
+
+    private String generateAndLogPassword() {
+        String password = PasswordGenerator.generate();
+        LOGGER.warn("No WebAdmin password had been configured: a random one 
had been generated for this run. " +
+            "Supply it within the `Password` header of your WebAdmin requests, 
or pin it with the `password` entry " +
+            "of webadmin.properties. Generated WebAdmin password: {}", 
password);
+        return password;
+    }
+
     @VisibleForTesting
     ImmutableList<String> additionalRoutes(Configuration configurationFile) {
         return 
ImmutableList.copyOf(configurationFile.getStringArray("extensions.routes"));
diff --git 
a/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java
 
b/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java
index 7d81f0b423..bbe213d2ed 100644
--- 
a/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java
+++ 
b/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java
@@ -21,11 +21,18 @@ package org.apache.james.modules.server;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.util.Optional;
+
 import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.PropertiesConfiguration;
 import org.apache.james.utils.PropertiesProvider;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 
 class WebAdminServerModuleTest {
+    private static final boolean WEBADMIN_ENABLED = true;
+    private static final boolean WEBADMIN_DISABLED = false;
+
     @Test
     void shouldReturnEmptyWhenNoField() throws Exception {
         Configuration configuration = getConfiguration("webadmin-none");
@@ -61,4 +68,69 @@ class WebAdminServerModuleTest {
     private Configuration getConfiguration(String name) throws Exception {
         return PropertiesProvider.forTesting().getConfiguration(name);
     }
+
+    @Nested
+    class PasswordGeneration {
+        @Test
+        void passwordShouldBeGeneratedByDefault() {
+            assertThat(new WebAdminServerModule().password(new 
PropertiesConfiguration(), WEBADMIN_ENABLED))
+                .isNotEmpty();
+        }
+
+        @Test
+        void passwordShouldBeEmptyWhenGenerationIsDisabled() {
+            assertThat(new 
WebAdminServerModule().password(configuration("password.generate", false), 
WEBADMIN_ENABLED))
+                .isEmpty();
+        }
+
+        @Test
+        void passwordShouldBeGeneratedWhenGenerationIsEnabled() {
+            assertThat(new 
WebAdminServerModule().password(configuration("password.generate", true), 
WEBADMIN_ENABLED))
+                .isNotEmpty();
+        }
+
+        @Test
+        void generatedPasswordShouldNotContainThePasswordSeparator() {
+            assertThat(new 
WebAdminServerModule().password(configuration("password.generate", true), 
WEBADMIN_ENABLED))
+                .hasValueSatisfying(password -> 
assertThat(password).doesNotContain(","));
+        }
+
+        @Test
+        void generatedPasswordsShouldBeRandom() {
+            Optional<String> firstPassword = new 
WebAdminServerModule().password(configuration("password.generate", true), 
WEBADMIN_ENABLED);
+            Optional<String> secondPassword = new 
WebAdminServerModule().password(configuration("password.generate", true), 
WEBADMIN_ENABLED);
+
+            assertThat(firstPassword).isNotEqualTo(secondPassword);
+        }
+
+        @Test
+        void configuredPasswordShouldTakePrecedenceOverGeneration() {
+            PropertiesConfiguration configuration = 
configuration("password.generate", true);
+            configuration.addProperty("password", "secret");
+
+            assertThat(new WebAdminServerModule().password(configuration, 
WEBADMIN_ENABLED))
+                .contains("secret");
+        }
+
+        @Test
+        void passwordShouldNotBeGeneratedWhenWebAdminIsDisabled() {
+            assertThat(new 
WebAdminServerModule().password(configuration("password.generate", true), 
WEBADMIN_DISABLED))
+                .isEmpty();
+        }
+
+        @Test
+        void passwordShouldNotBeGeneratedWhenJwtIsEnabled() {
+            PropertiesConfiguration configuration = 
configuration("password.generate", true);
+            configuration.addProperty("jwt.enabled", true);
+
+            assertThat(new WebAdminServerModule().password(configuration, 
WEBADMIN_ENABLED))
+                .isEmpty();
+        }
+
+        private PropertiesConfiguration configuration(String key, Object 
value) {
+            PropertiesConfiguration configuration = new 
PropertiesConfiguration();
+            configuration.addProperty(key, value);
+            return configuration;
+        }
+    }
 }
\ No newline at end of file
diff --git 
a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties
 
b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties
index 78a176aabd..6602aa0f0a 100644
--- 
a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties
+++ 
b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties
@@ -24,4 +24,7 @@ enabled=true
 port=0
 host=127.0.0.1
 
-extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes
\ No newline at end of file
+extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes
+
+# These tests exercise WebAdmin routes without authentication
+password.generate=false
diff --git 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties
 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties
index 78a176aabd..6602aa0f0a 100644
--- 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties
+++ 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties
@@ -24,4 +24,7 @@ enabled=true
 port=0
 host=127.0.0.1
 
-extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes
\ No newline at end of file
+extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes
+
+# These tests exercise WebAdmin routes without authentication
+password.generate=false
diff --git 
a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordGenerator.java
 
b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordGenerator.java
new file mode 100644
index 0000000000..da032ba61f
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordGenerator.java
@@ -0,0 +1,45 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.authentication;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+
+/**
+ * Generates ephemeral WebAdmin passwords, enabling a secure setup without 
hardcoded credentials.
+ *
+ * The base64 URL alphabet is used as it excludes the comma, which {@link 
PasswordFilter} relies on
+ * as a password separator.
+ */
+public class PasswordGenerator {
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+    private static final Base64.Encoder ENCODER = 
Base64.getUrlEncoder().withoutPadding();
+    private static final int ENTROPY_BYTE_COUNT = 32;
+
+    public static String generate() {
+        byte[] entropy = new byte[ENTROPY_BYTE_COUNT];
+        SECURE_RANDOM.nextBytes(entropy);
+        return ENCODER.encodeToString(entropy);
+    }
+
+    private PasswordGenerator() {
+
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to