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

ilgrosso pushed a commit to branch 4_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit f050b7024603cda0968cb5eb60a86c5edf1c6e3c
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Tue Sep 8 14:39:48 2026 +0200

    [SYNCOPE-1996] Security production mode
---
 .../persistence/api/entity/PlainAttrValue.java     |  2 +-
 .../common/validation/PlainSchemaValidator.java    | 19 +++++
 .../persistence/jpa/PersistenceTestContext.java    |  4 +-
 .../core/persistence/jpa/inner/PlainAttrTest.java  |  2 +-
 .../persistence/neo4j/PersistenceTestContext.java  |  4 +-
 .../persistence/neo4j/inner/PlainAttrTest.java     |  2 +-
 .../src/test/resources/core-test.properties        |  1 +
 .../spring/security/DefaultCredentialChecker.java  | 63 +++++++++++++---
 .../core/spring/security/DefaultEncryptor.java     | 49 +++++++------
 .../spring/security/DefaultEncryptorManager.java   | 14 +++-
 .../core/spring/security/SecurityContext.java      | 59 +++++++++------
 .../core/spring/security/SecurityProperties.java   | 10 +++
 .../core/spring/SpringTestConfiguration.java       |  3 +-
 .../core/spring/security/DefaultEncryptorTest.java | 11 ++-
 core/starter/src/main/resources/core.properties    |  3 +-
 .../src/main/resources/core-embedded.properties    |  1 +
 .../org/apache/syncope/fit/AbstractITCase.java     |  5 +-
 .../org/apache/syncope/fit/core/GroupITCase.java   |  2 +-
 .../apache/syncope/fit/core/PlainSchemaITCase.java |  2 +-
 pom.xml                                            |  4 +-
 .../asciidoc/getting-started/movingForward.adoc    | 30 ++++----
 .../reference-guide/architecture/core.adoc         |  1 +
 .../reference-guide/concepts/extensions.adoc       |  2 +-
 .../configuration/configuration.adoc               |  4 +-
 .../configuration/configurationparameters.adoc     | 11 ---
 .../configuration/{jws.adoc => security.adoc}      | 83 ++++++++++++++++++----
 .../reference-guide/howto/setadmincredentials.adoc |  8 +--
 src/main/asciidoc/reference-guide/usage/core.adoc  | 62 ++++++++++++++++
 28 files changed, 343 insertions(+), 118 deletions(-)

diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/PlainAttrValue.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/PlainAttrValue.java
index 4029465cc2..f1a83a2d4e 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/PlainAttrValue.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/PlainAttrValue.java
@@ -45,7 +45,7 @@ public class PlainAttrValue implements Serializable {
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(PlainAttrValue.class);
 
-    private static final Pattern SPRING_ENV_PROPERTY = 
Pattern.compile("^\\$\\{.*\\}$");
+    public static final Pattern SPRING_ENV_PROPERTY = 
Pattern.compile("^\\$\\{.*\\}$");
 
     @JsonIgnore
     @NotNull
diff --git 
a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/validation/PlainSchemaValidator.java
 
b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/validation/PlainSchemaValidator.java
index 0d5c5302c1..a60a29ec9b 100644
--- 
a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/validation/PlainSchemaValidator.java
+++ 
b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/validation/PlainSchemaValidator.java
@@ -18,7 +18,12 @@
  */
 package org.apache.syncope.core.persistence.common.validation;
 
+import static 
org.apache.syncope.core.persistence.api.entity.PlainAttrValue.SPRING_ENV_PROPERTY;
+
 import jakarta.validation.ConstraintValidatorContext;
+import java.util.Optional;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.common.lib.types.EntityViolationType;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 
@@ -55,6 +60,20 @@ public class PlainSchemaValidator extends 
AbstractValidator<PlainSchemaCheck, Pl
                             
addPropertyNode("secretKey").addPropertyNode("cipherAlgorithm").addConstraintViolation();
                     return false;
                 }
+                if (schema.getCipherAlgorithm() == CipherAlgorithm.AES
+                        && 
!SyncopeConstants.ENCRYPTED_DECODE_CONVERSION_PATTERN.equals(schema.getConversionPattern())
+                        && 
!SPRING_ENV_PROPERTY.matcher(schema.getSecretKey()).matches()
+                        && Optional.ofNullable(schema.getSecretKey()).
+                                map(key -> key.length() != 16 && key.length() 
!= 24 && key.length() != 32).
+                                orElse(true)) {
+
+                    context.disableDefaultConstraintViolation();
+                    context.buildConstraintViolationWithTemplate(
+                            getTemplate(EntityViolationType.InvalidSchema,
+                                    "SecretKey length requirements not met for 
AES: must be 16 24 or 32")).
+                            
addPropertyNode("secretKey").addConstraintViolation();
+                    return false;
+                }
             }
 
             default -> {
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/PersistenceTestContext.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/PersistenceTestContext.java
index 3c8d31286d..42b530d87d 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/PersistenceTestContext.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/PersistenceTestContext.java
@@ -33,6 +33,7 @@ import 
org.apache.syncope.core.persistence.jpa.spring.CommonEntityManagerFactory
 import 
org.apache.syncope.core.persistence.jpa.spring.DomainRoutingEntityManagerFactory;
 import org.apache.syncope.core.provisioning.api.ConnectorManager;
 import org.apache.syncope.core.provisioning.api.ImplementationLookup;
+import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
 import org.apache.syncope.core.spring.security.DefaultEncryptorManager;
 import org.apache.syncope.core.spring.security.DefaultPasswordGenerator;
 import org.apache.syncope.core.spring.security.PasswordGenerator;
@@ -118,7 +119,8 @@ public class PersistenceTestContext {
     public EncryptorManager encryptorManager() {
         SecurityProperties securityProperties = new SecurityProperties();
         securityProperties.setAesSecretKey(StringUtils.EMPTY);
-        return new DefaultEncryptorManager(securityProperties);
+        securityProperties.setProductionMode(false);
+        return new DefaultEncryptorManager(new DefaultCredentialChecker("", 
"", "", "", false), securityProperties);
     }
 
     @Bean
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PlainAttrTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PlainAttrTest.java
index 324934de55..340ba1134f 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PlainAttrTest.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PlainAttrTest.java
@@ -167,7 +167,7 @@ public class PlainAttrTest extends AbstractTest {
         
obscureWithDecodeConversionPattern.setAnyTypeClass(anyTypeClassDAO.findById("other").orElseThrow());
         obscureWithDecodeConversionPattern.setType(AttrSchemaType.Encrypted);
         
obscureWithDecodeConversionPattern.setCipherAlgorithm(CipherAlgorithm.AES);
-        
obscureWithDecodeConversionPattern.setSecretKey(SecureRandomUtils.generateRandomUUID().toString());
+        
obscureWithDecodeConversionPattern.setSecretKey(SecureRandomUtils.generateRandomPassword(16));
 
         obscureWithDecodeConversionPattern = 
plainSchemaDAO.save(obscureWithDecodeConversionPattern);
 
diff --git 
a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/PersistenceTestContext.java
 
b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/PersistenceTestContext.java
index 29cb7fe845..dd26b09595 100644
--- 
a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/PersistenceTestContext.java
+++ 
b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/PersistenceTestContext.java
@@ -32,6 +32,7 @@ import 
org.apache.syncope.core.persistence.api.content.ContentLoader;
 import org.apache.syncope.core.persistence.neo4j.spring.DomainRoutingDriver;
 import org.apache.syncope.core.provisioning.api.ConnectorManager;
 import org.apache.syncope.core.provisioning.api.ImplementationLookup;
+import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
 import org.apache.syncope.core.spring.security.DefaultEncryptorManager;
 import org.apache.syncope.core.spring.security.DefaultPasswordGenerator;
 import org.apache.syncope.core.spring.security.PasswordGenerator;
@@ -110,7 +111,8 @@ public class PersistenceTestContext {
     public EncryptorManager encryptorManager() {
         SecurityProperties securityProperties = new SecurityProperties();
         securityProperties.setAesSecretKey(StringUtils.EMPTY);
-        return new DefaultEncryptorManager(securityProperties);
+        securityProperties.setProductionMode(false);
+        return new DefaultEncryptorManager(new DefaultCredentialChecker("", 
"", "", "", false), securityProperties);
     }
 
     @Bean
diff --git 
a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/PlainAttrTest.java
 
b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/PlainAttrTest.java
index 854a7ab463..df515d517c 100644
--- 
a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/PlainAttrTest.java
+++ 
b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/PlainAttrTest.java
@@ -180,7 +180,7 @@ public class PlainAttrTest extends AbstractTest {
         
obscureWithDecodeConversionPattern.setAnyTypeClass(anyTypeClassDAO.findById("other").orElseThrow());
         obscureWithDecodeConversionPattern.setType(AttrSchemaType.Encrypted);
         
obscureWithDecodeConversionPattern.setCipherAlgorithm(CipherAlgorithm.AES);
-        
obscureWithDecodeConversionPattern.setSecretKey(SecureRandomUtils.generateRandomUUID().toString());
+        
obscureWithDecodeConversionPattern.setSecretKey(SecureRandomUtils.generateRandomPassword(16));
 
         obscureWithDecodeConversionPattern = 
plainSchemaDAO.save(obscureWithDecodeConversionPattern);
 
diff --git a/core/provisioning-java/src/test/resources/core-test.properties 
b/core/provisioning-java/src/test/resources/core-test.properties
index 10da9c173c..c3e75041a9 100644
--- a/core/provisioning-java/src/test/resources/core-test.properties
+++ b/core/provisioning-java/src/test/resources/core-test.properties
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+security.productionMode=false
 security.adminUser=${adminUser}
 security.anonymousUser=${anonymousUser}
 security.jwsKey=${jwsKey}
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultCredentialChecker.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultCredentialChecker.java
index eef5fcb677..84e4732a59 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultCredentialChecker.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultCredentialChecker.java
@@ -28,43 +28,88 @@ public class DefaultCredentialChecker {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(DefaultCredentialChecker.class);
 
+    private static final String DEFAULT_AES_KEY_ERROR_MESSAGE =
+            "The default AES key property is being used. "
+            + "This must be changed to avoid a security breach!";
+
+    private static final String DEFAULT_AES_KEY = "1abcdefghilmnopqrstuvz2!";
+
+    private static final String DEFAULT_JWS_KEY_ERROR_MESSAGE =
+            "The default JWKS key property is being used. "
+            + "This must be changed to avoid a security breach!";
+
     private static final String DEFAULT_JWS_KEY = 
"ZW7pRixehFuNUtnY5Se47IemgMryTzazPPJ9CGX5LTCmsOJpOgHAQEuPQeV9A28f";
 
+    private static final String DEFAULT_ADMIN_PASSWORD_ERROR_MESSAGE =
+            "The default adminPassword property is being used. "
+            + "This must be changed to avoid a security breach!";
+
     private static final String DEFAULT_ADMIN_PASSWORD =
-        
"DE088591C00CC98B36F5ADAAF7DA2B004CF7F2FE7BBB45B766B6409876E2F3DB13C7905C6AA59464";
+            
"DE088591C00CC98B36F5ADAAF7DA2B004CF7F2FE7BBB45B766B6409876E2F3DB13C7905C6AA59464";
+
+    private static final String DEFAULT_ANON_KEY_ERROR_MESSAGE =
+            "The default anonymousKey property is being used. "
+            + "This must be changed to avoid a security breach!";
 
     private static final String DEFAULT_ANON_KEY = "anonymousKey";
 
-    private final boolean defaultAdminPasswordInUse;
+    private final boolean defaultAesKeyInUse;
 
     private final boolean defaultJwsKeyInUse;
 
+    private final boolean defaultAdminPasswordInUse;
+
     private final boolean defaultAnonymousKeyInUse;
 
-    public DefaultCredentialChecker(final String jwsKey, final String 
adminPassword, final String anonymousKey) {
+    private final boolean productionMode;
+
+    public DefaultCredentialChecker(
+            final String aesKey,
+            final String jwsKey,
+            final String adminPassword,
+            final String anonymousKey,
+            final boolean productionMode) {
+
+        defaultAesKeyInUse = DEFAULT_AES_KEY.equals(aesKey);
         defaultJwsKeyInUse = DEFAULT_JWS_KEY.equals(jwsKey);
         defaultAdminPasswordInUse = 
DEFAULT_ADMIN_PASSWORD.equals(adminPassword);
         defaultAnonymousKeyInUse = DEFAULT_ANON_KEY.equals(anonymousKey);
+        this.productionMode = productionMode;
+    }
+
+    public void checkIsDefaultAESKeyInUse() {
+        if (defaultAesKeyInUse) {
+            if (productionMode) {
+                throw new IllegalStateException(DEFAULT_AES_KEY_ERROR_MESSAGE);
+            }
+            LOG.warn(DEFAULT_AES_KEY_ERROR_MESSAGE);
+        }
     }
 
     public void checkIsDefaultJWSKeyInUse() {
         if (defaultJwsKeyInUse) {
-            LOG.warn("The default jwsKey property is being used. "
-                    + "This must be changed to avoid a security breach!");
+            if (productionMode) {
+                throw new IllegalStateException(DEFAULT_JWS_KEY_ERROR_MESSAGE);
+            }
+            LOG.warn(DEFAULT_JWS_KEY_ERROR_MESSAGE);
         }
     }
 
     public void checkIsDefaultAdminPasswordInUse() {
         if (defaultAdminPasswordInUse) {
-            LOG.warn("The default adminPassword property is being used. "
-                    + "This must be changed to avoid a security breach!");
+            if (productionMode) {
+                throw new 
IllegalStateException(DEFAULT_ADMIN_PASSWORD_ERROR_MESSAGE);
+            }
+            LOG.warn(DEFAULT_ADMIN_PASSWORD_ERROR_MESSAGE);
         }
     }
 
     public void checkIsDefaultAnonymousKeyInUse() {
         if (defaultAnonymousKeyInUse) {
-            LOG.warn("The default anonymousKey property is being used. "
-                    + "This must be changed to avoid a security breach!");
+            if (productionMode) {
+                throw new 
IllegalStateException(DEFAULT_ANON_KEY_ERROR_MESSAGE);
+            }
+            LOG.warn(DEFAULT_ANON_KEY_ERROR_MESSAGE);
         }
     }
 }
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptor.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptor.java
index 7b0824ba87..e415306134 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptor.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptor.java
@@ -51,6 +51,7 @@ public class DefaultEncryptor implements Encryptor {
 
     protected DefaultEncryptor(
             final String aesSecretKey,
+            final boolean productionMode,
             final SecurityProperties.DigesterProperties digesterProperties) {
 
         this.digesterProperties = digesterProperties;
@@ -60,30 +61,34 @@ public class DefaultEncryptor implements Encryptor {
         if (StringUtils.isNotBlank(aesSecretKey)) {
             String actualKey = aesSecretKey;
 
-            Integer pad = null;
-            boolean truncate = false;
-            if (actualKey.length() < 16) {
-                pad = 16 - actualKey.length();
-            } else if (actualKey.length() > 16 && actualKey.length() < 24) {
-                pad = 24 - actualKey.length();
-            } else if (actualKey.length() > 24 && actualKey.length() < 32) {
-                pad = 32 - actualKey.length();
-            } else if (actualKey.length() > 32) {
-                truncate = true;
-            }
+            if (!productionMode) {
+                Integer pad = null;
+                boolean truncate = false;
+                if (actualKey.length() < 16) {
+                    pad = 16 - actualKey.length();
+                } else if (actualKey.length() > 16 && actualKey.length() < 24) 
{
+                    pad = 24 - actualKey.length();
+                } else if (actualKey.length() > 24 && actualKey.length() < 32) 
{
+                    pad = 32 - actualKey.length();
+                } else if (actualKey.length() > 32) {
+                    truncate = true;
+                }
 
-            if (pad != null) {
-                StringBuilder actualKeyPadding = new StringBuilder(actualKey);
-                String randomChars = 
SecureRandomUtils.generateRandomPassword(pad);
+                if (pad != null) {
+                    StringBuilder actualKeyPadding = new 
StringBuilder(actualKey);
+                    String randomChars = 
SecureRandomUtils.generateRandomPassword(pad);
 
-                actualKeyPadding.append(randomChars);
-                actualKey = actualKeyPadding.toString();
-                LOG.warn("The configured AES secret key is too short (< {}), 
padding with random chars: {}",
-                        actualKey.length(), actualKey);
-            }
-            if (truncate) {
-                actualKey = actualKey.substring(0, 32);
-                LOG.warn("The configured AES secret key is too long (> 32), 
truncating: {}", actualKey);
+                    actualKeyPadding.append(randomChars);
+                    actualKey = actualKeyPadding.toString();
+                    LOG.warn("The configured AES secret key is too short (< 
{}), padding with random chars",
+                            actualKey.length());
+                    LOG.debug("Using\nsecurity.aesSecretKey={}", actualKey);
+                }
+                if (truncate) {
+                    actualKey = actualKey.substring(0, 32);
+                    LOG.warn("The configured AES secret key is too long (> 
32), truncating");
+                    LOG.debug("Using\nsecurity.aesSecretKey={}", actualKey);
+                }
             }
 
             try {
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptorManager.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptorManager.java
index f0d5776f78..1cee4bdeaf 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptorManager.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptorManager.java
@@ -26,11 +26,17 @@ import 
org.apache.syncope.core.persistence.api.EncryptorManager;
 
 public class DefaultEncryptorManager implements EncryptorManager {
 
+    protected final DefaultCredentialChecker credentialChecker;
+
     protected final SecurityProperties securityProperties;
 
     protected final Map<String, DefaultEncryptor> instances = new 
ConcurrentHashMap<>();
 
-    public DefaultEncryptorManager(final SecurityProperties 
securityProperties) {
+    public DefaultEncryptorManager(
+            final DefaultCredentialChecker credentialChecker,
+            final SecurityProperties securityProperties) {
+
+        this.credentialChecker = credentialChecker;
         this.securityProperties = securityProperties;
     }
 
@@ -41,7 +47,11 @@ public class DefaultEncryptorManager implements 
EncryptorManager {
 
     @Override
     public Encryptor getInstance(final String aesSecretKey) {
+        credentialChecker.checkIsDefaultAESKeyInUse();
+
         String actualKey = StringUtils.isBlank(aesSecretKey) ? 
securityProperties.getAesSecretKey() : aesSecretKey;
-        return instances.computeIfAbsent(actualKey, k -> new 
DefaultEncryptor(k, securityProperties.getDigester()));
+        return instances.computeIfAbsent(
+                actualKey,
+                k -> new DefaultEncryptor(k, 
securityProperties.isProductionMode(), securityProperties.getDigester()));
     }
 }
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
index fdcc2a14f6..5b7d10e7ba 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
@@ -58,31 +58,45 @@ public class SecurityContext {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(SecurityContext.class);
 
+    private static String JWS_KEY = null;
+
     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
     @Bean
     public static GrantedAuthorityDefaults grantedAuthorityDefaults() {
         return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
     }
 
-    protected static String jwsKey(final JWSAlgorithm jwsAlgorithm, final 
SecurityProperties props) {
-        String jwsKey = Optional.ofNullable(props.getJwsKey()).
-                orElseThrow(() -> new IllegalArgumentException("No JWS key 
provided"));
-
-        if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
-            int minLength = jwsAlgorithm.equals(JWSAlgorithm.HS256)
-                    ? 256 / 8
-                    : jwsAlgorithm.equals(JWSAlgorithm.HS384)
-                    ? 384 / 8
-                    : 512 / 8;
-            if (jwsKey.length() < minLength) {
-                jwsKey = SecureRandomUtils.generateRandomPassword(minLength);
-                props.setJwsKey(jwsKey);
-                LOG.warn("The configured key for {} must be at least {} bits, 
generating random: {}",
-                        jwsAlgorithm, minLength * 8, jwsKey);
+    private static String jwsKey(final JWSAlgorithm jwsAlgorithm, final 
SecurityProperties props) {
+        synchronized (LOG) {
+            if (JWS_KEY == null) {
+                String jwsKey = Optional.ofNullable(props.getJwsKey()).
+                        orElseThrow(() -> new IllegalArgumentException("No JWS 
key provided"));
+
+                if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
+                    int minLength = jwsAlgorithm.equals(JWSAlgorithm.HS256)
+                            ? 256 / 8
+                            : jwsAlgorithm.equals(JWSAlgorithm.HS384)
+                            ? 384 / 8
+                            : 512 / 8;
+                    if (jwsKey.length() < minLength) {
+                        if (props.isProductionMode()) {
+                            throw new IllegalArgumentException(
+                                    "The configured key for %s must be at 
least %d characters".
+                                            formatted(jwsAlgorithm, 
minLength));
+                        }
+
+                        jwsKey = 
SecureRandomUtils.generateRandomPassword(minLength);
+                        props.setJwsKey(jwsKey);
+                        LOG.warn("The configured key for {} must be at least 
{} characters, generating random",
+                                jwsAlgorithm, minLength);
+                        LOG.debug("Using\nsecurity.jwsKey={}", jwsKey);
+                    }
+                }
+
+                JWS_KEY = jwsKey;
             }
         }
-
-        return jwsKey;
+        return JWS_KEY;
     }
 
     @Bean
@@ -102,9 +116,11 @@ public class SecurityContext {
             final JWSAlgorithm jwsAlgorithm) {
 
         return new DefaultCredentialChecker(
+                props.getAesSecretKey(),
                 jwsKey(jwsAlgorithm, props),
                 props.getAdminPassword(),
-                props.getAnonymousKey());
+                props.getAnonymousKey(),
+                props.isProductionMode());
     }
 
     @ConditionalOnMissingBean
@@ -157,8 +173,11 @@ public class SecurityContext {
     }
 
     @Bean
-    public EncryptorManager encryptorManager(final SecurityProperties 
securityProperties) {
-        return new DefaultEncryptorManager(securityProperties);
+    public EncryptorManager encryptorManager(
+            final DefaultCredentialChecker credentialChecker,
+            final SecurityProperties securityProperties) {
+
+        return new DefaultEncryptorManager(credentialChecker, 
securityProperties);
     }
 
     @ConditionalOnMissingBean
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
index cb9ffeb6bb..03f5fc5fe4 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java
@@ -82,6 +82,8 @@ public class SecurityProperties {
         }
     }
 
+    private boolean productionMode = true;
+
     private String adminUser;
 
     private String adminPassword;
@@ -104,6 +106,14 @@ public class SecurityProperties {
 
     private final DigesterProperties digester = new DigesterProperties();
 
+    public boolean isProductionMode() {
+        return productionMode;
+    }
+
+    public void setProductionMode(final boolean productionMode) {
+        this.productionMode = productionMode;
+    }
+
     public String getAdminUser() {
         return adminUser;
     }
diff --git 
a/core/spring/src/test/java/org/apache/syncope/core/spring/SpringTestConfiguration.java
 
b/core/spring/src/test/java/org/apache/syncope/core/spring/SpringTestConfiguration.java
index 82313849c4..7d6d2d2757 100644
--- 
a/core/spring/src/test/java/org/apache/syncope/core/spring/SpringTestConfiguration.java
+++ 
b/core/spring/src/test/java/org/apache/syncope/core/spring/SpringTestConfiguration.java
@@ -25,6 +25,7 @@ import java.io.Reader;
 import org.apache.syncope.core.persistence.api.ApplicationContextProvider;
 import org.apache.syncope.core.persistence.api.EncryptorManager;
 import org.apache.syncope.core.provisioning.api.ImplementationLookup;
+import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
 import org.apache.syncope.core.spring.security.DefaultEncryptorManager;
 import org.apache.syncope.core.spring.security.DummyImplementationLookup;
 import org.apache.syncope.core.spring.security.SecurityProperties;
@@ -49,7 +50,7 @@ public class SpringTestConfiguration {
     public EncryptorManager encryptorManager() {
         SecurityProperties securityProperties = new SecurityProperties();
         securityProperties.setAesSecretKey(AES_SECRET_KEY);
-        return new DefaultEncryptorManager(securityProperties);
+        return new DefaultEncryptorManager(new DefaultCredentialChecker("", 
"", "", "", false), securityProperties);
     }
 
     @Primary
diff --git 
a/core/spring/src/test/java/org/apache/syncope/core/spring/security/DefaultEncryptorTest.java
 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/DefaultEncryptorTest.java
index bba0d18cf9..1ff092f3ca 100644
--- 
a/core/spring/src/test/java/org/apache/syncope/core/spring/security/DefaultEncryptorTest.java
+++ 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/DefaultEncryptorTest.java
@@ -21,8 +21,10 @@ package org.apache.syncope.core.spring.security;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.security.InvalidKeyException;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.core.persistence.api.ApplicationContextProvider;
 import org.apache.syncope.core.persistence.api.Encryptor;
@@ -44,7 +46,9 @@ public class DefaultEncryptorTest {
 
         SecurityProperties securityProperties = new SecurityProperties();
         
securityProperties.setAesSecretKey(SpringTestConfiguration.AES_SECRET_KEY);
-        ENCRYPTOR = new 
DefaultEncryptorManager(securityProperties).getInstance();
+
+        ENCRYPTOR = new DefaultEncryptorManager(
+                new DefaultCredentialChecker("", "", "", "", false), 
securityProperties).getInstance();
     }
 
     @Test
@@ -74,7 +78,10 @@ public class DefaultEncryptorTest {
 
     @Test
     public void smallKey() throws Exception {
-        DefaultEncryptor smallKeyEncryptor = new DefaultEncryptor("123", new 
SecurityProperties().getDigester());
+        DefaultEncryptor prodModeEncryptor = new DefaultEncryptor("123", true, 
new SecurityProperties().getDigester());
+        assertThrows(InvalidKeyException.class, () -> 
prodModeEncryptor.encode(PASSWORD_VALUE, CipherAlgorithm.AES));
+
+        DefaultEncryptor smallKeyEncryptor = new DefaultEncryptor("123", 
false, new SecurityProperties().getDigester());
         String encPassword = smallKeyEncryptor.encode(PASSWORD_VALUE, 
CipherAlgorithm.AES);
         String decPassword = smallKeyEncryptor.decode(encPassword, 
CipherAlgorithm.AES);
         assertEquals(PASSWORD_VALUE, decPassword);
diff --git a/core/starter/src/main/resources/core.properties 
b/core/starter/src/main/resources/core.properties
index 23d8724c3c..fd69ad8f98 100644
--- a/core/starter/src/main/resources/core.properties
+++ b/core/starter/src/main/resources/core.properties
@@ -80,6 +80,7 @@ spring.mail.properties.mail.smtp.starttls.enable=false
 # Security #
 ############
 
+security.productionMode=true
 security.adminUser=${adminUser}
 security.adminPassword=${adminPassword}
 security.adminPasswordAlgorithm=SSHA256
@@ -96,8 +97,6 @@ security.jwsKey=${jwsKey}
 # * 16 chars => AES-128
 # * 24 chars => AES-192
 # * 32 chars => AES-256
-#
-# Shorter keys will be padded to the nearest longer option available; keys > 
32 will be trucated
 security.aesSecretKey=${secretKey}
 
 security.groovyBlacklist=classpath:META-INF/groovy.blacklist
diff --git a/fit/core-reference/src/main/resources/core-embedded.properties 
b/fit/core-reference/src/main/resources/core-embedded.properties
index 42c2f8c1df..8b7378cdd2 100644
--- a/fit/core-reference/src/main/resources/core-embedded.properties
+++ b/fit/core-reference/src/main/resources/core-embedded.properties
@@ -27,6 +27,7 @@ service.discovery.address=http://localhost:9080/syncope/rest/
 spring.devtools.livereload.enabled=false
 spring.devtools.restart.enabled=false
 
+security.productionMode=false
 security.adminUser=${adminUser}
 security.anonymousUser=${anonymousUser}
 security.jwsKey=${jwsKey}
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 7fadbd72e3..56544e1691 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -175,6 +175,7 @@ import 
org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
 import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
 import 
org.apache.syncope.common.rest.api.service.wa.WebAuthnRegistrationService;
 import org.apache.syncope.core.persistence.api.EncryptorManager;
+import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
 import org.apache.syncope.core.spring.security.DefaultEncryptorManager;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.fit.AbstractITCase.KeymasterInitializer;
@@ -1167,6 +1168,8 @@ public abstract class AbstractITCase {
     protected AbstractITCase() {
         SecurityProperties securityProperties = new SecurityProperties();
         securityProperties.setAesSecretKey(StringUtils.EMPTY);
-        encryptorManager = new DefaultEncryptorManager(securityProperties);
+        securityProperties.setProductionMode(false);
+        encryptorManager = new DefaultEncryptorManager(
+                new DefaultCredentialChecker("", "", "", "", false), 
securityProperties);
     }
 }
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
index 585c5d61c9..14d9e3d39d 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
@@ -663,7 +663,7 @@ public class GroupITCase extends AbstractITCase {
         assertEquals("testvalue", 
group.getPlainAttr(encrypted.getKey()).orElseThrow().getValues().getFirst());
 
         // 6. update schema again to disallow cleartext values
-        encrypted.setConversionPattern(null);
+        encrypted.setConversionPattern("${obscureSecretKey}");
         SCHEMA_SERVICE.update(SchemaType.PLAIN, encrypted);
 
         group = GROUP_SERVICE.read(group.getKey());
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PlainSchemaITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PlainSchemaITCase.java
index 0d82fa5d5d..73ec069907 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PlainSchemaITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PlainSchemaITCase.java
@@ -133,7 +133,7 @@ public class PlainSchemaITCase extends AbstractITCase {
         schemaTO.setKey("encrypted");
         schemaTO.setType(AttrSchemaType.Encrypted);
         schemaTO.setCipherAlgorithm(CipherAlgorithm.AES);
-        schemaTO.setSecretKey("huhadfhsjfsfsdkj!####");
+        schemaTO.setSecretKey("huhadfhsjfsfsdkj");
 
         createSchema(SchemaType.PLAIN, schemaTO);
     }
diff --git a/pom.xml b/pom.xml
index 96f4911862..f15ebc8193 100644
--- a/pom.xml
+++ b/pom.xml
@@ -483,7 +483,7 @@ under the License.
     <chartjs.version>4.5.1</chartjs.version>
 
     <wicket.version>10.11.0</wicket.version>
-    <wicketstuff.version>10.10.0</wicketstuff.version>
+    <wicketstuff.version>10.11.0</wicketstuff.version>
     <wicket-bootstrap.version>7.0.15</wicket-bootstrap.version>
     <wicket-spring-boot.version>4.0.0</wicket-spring-boot.version>
 
@@ -530,7 +530,7 @@ under the License.
     <tomcat.version>10.1.57</tomcat.version>
     <wildfly.version>40.0.1.Final</wildfly.version>
     <payara.version>6.2025.11</payara.version>
-    <jakarta.faces.version>4.1.15</jakarta.faces.version>
+    <jakarta.faces.version>4.1.16</jakarta.faces.version>
 
     <docker.postgresql.version>17-alpine</docker.postgresql.version>
     <docker.mysql.version>9</docker.mysql.version>
diff --git a/src/main/asciidoc/getting-started/movingForward.adoc 
b/src/main/asciidoc/getting-started/movingForward.adoc
index 0c5a441710..aef70e8da7 100644
--- a/src/main/asciidoc/getting-started/movingForward.adoc
+++ b/src/main/asciidoc/getting-started/movingForward.adoc
@@ -19,8 +19,8 @@
 
 == Moving Forward
 
-Once you have obtained a working installation of Apache Syncope using one of 
the methods reported above, you should consider 
-reading the
+Once you have obtained a working installation of Apache Syncope using one of 
the methods reported above, you should
+consider  reading the
 ifeval::["{backend}" == "html5"]
 https://syncope.apache.org/docs/4.0/reference-guide.html[Apache Syncope 
Reference Guide.]
 endif::[]
@@ -29,23 +29,17 @@ 
https://syncope.apache.org/docs/4.0/reference-guide.pdf[Apache Syncope Reference
 endif::[]
 to understand how to configure, extend, customize and deploy your new Apache 
Syncope project.
 
-Before deploying your Apache Syncope installation into production, it is 
essential to ensure that the default values for 
-various security properties have been changed to values specific to your 
deployment. 
+Before deploying your Apache Syncope installation into production, it is 
[.underline]#essential to ensure that the
+default values for various security properties have been changed to values 
specific to your deployment#. 
 
-The following values must be changed from the defaults in the 
`core.properties` file:
+The following values **must be changed** from the defaults in the 
`core.properties` file:
 
-* *adminPassword* - The cleartext password as encoded per the 
`adminPasswordAlgorithm` value (`SSHA256` by default), the
-default value of which is "password".
-* *secretKey* - The secret key value used for AES ciphering; AES is used by 
the use cases below:
-  ** if the value for `adminPasswordAlgorithm` is `AES` or the configuration 
parameter `password.cipher.algorithm` is
-changed to `AES`
-  ** if set for Encrypted Plain Schema instances
-  ** for Linked Accounts' password values
-  ** to securely store Access Token's cached authorities
-  ** within some of the predefined rules used by Password Policies
-* *anonymousKey* - The key value to use for anonymous requests.
-* *jwsKey* - The symmetric signing key used to sign access tokens. See section 
4.4.1 "REST Authentication and 
-Authorization" of the Reference Guide for more information.
+* `security.adminPassword`
+* `security.anonymousKey`
+* `security.aesSecretKey`
+* `security.jwsKey`
 
 Note that if you installed Syncope using the maven archetype method, then you 
will have already supplied custom values
-for `secretKey`, `anonymousKey` and `jwsKey`.
+for `security.aesSecretKey`, `security.anonymousKey` and `security.jwsKey`.
+
+See the 
https://syncope.apache.org/docs/4.1/reference-guide.html#security[security 
configuration section] for more details.
diff --git a/src/main/asciidoc/reference-guide/architecture/core.adoc 
b/src/main/asciidoc/reference-guide/architecture/core.adoc
index 2ca963b9f9..f8fba3b596 100644
--- a/src/main/asciidoc/reference-guide/architecture/core.adoc
+++ b/src/main/asciidoc/reference-guide/architecture/core.adoc
@@ -93,6 +93,7 @@ with no code changes: PostgreSQL, MySQL, MariaDB and Oracle 
are fully supported
 <<domains>> allow to manage data belonging to different 
https://en.wikipedia.org/wiki/Multitenancy[tenants^] into
 separate database instances.
 
+[[security-layer]]
 ==== Security
 
 Rather than being a separate layer, Security features are triggered throughout 
incoming request processing.
diff --git a/src/main/asciidoc/reference-guide/concepts/extensions.adoc 
b/src/main/asciidoc/reference-guide/concepts/extensions.adoc
index 61eaaa6ab8..b05bdafa7e 100644
--- a/src/main/asciidoc/reference-guide/concepts/extensions.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/extensions.adoc
@@ -22,7 +22,7 @@ The _vanilla_ Apache Syncope deployment can be optional 
enriched with useful fea
 every single deployment with unneeded libraries and configurations.
 
 With reference to <<architecture,architecture>>, an extension might add a 
<<rest>> endpoint, manage the
-<<persistence,persistence>> of additional entities, extend the 
<<security,security>> mechanisms, tweak the
+<<persistence,persistence>> of additional entities, extend the 
<<security-layer,security>> mechanisms, tweak the
 <<provisioning-layer,provisioning layer>>, add features to the 
<<admin-console-component>> or
 the <<enduser-component>>, or even bring all such things together.
 
diff --git a/src/main/asciidoc/reference-guide/configuration/configuration.adoc 
b/src/main/asciidoc/reference-guide/configuration/configuration.adoc
index cdeab1759d..56a3d2f16f 100644
--- a/src/main/asciidoc/reference-guide/configuration/configuration.adoc
+++ b/src/main/asciidoc/reference-guide/configuration/configuration.adoc
@@ -48,6 +48,8 @@ include::deployment.adoc[]
 
 include::storage.adoc[]
 
+include::security.adoc[]
+
 include::highavailability.adoc[]
 
 include::domainsmanagement.adoc[]
@@ -58,6 +60,4 @@ include::connectorbundles.adoc[]
 
 include::email.adoc[]
 
-include::jws.adoc[]
-
 include::configurationparameters.adoc[]
diff --git 
a/src/main/asciidoc/reference-guide/configuration/configurationparameters.adoc 
b/src/main/asciidoc/reference-guide/configuration/configurationparameters.adoc
index 527869dbc5..3919183089 100644
--- 
a/src/main/asciidoc/reference-guide/configuration/configurationparameters.adoc
+++ 
b/src/main/asciidoc/reference-guide/configuration/configurationparameters.adoc
@@ -24,17 +24,6 @@ Most run-time configuration options are available as 
parameters and can be tuned
 * `password.cipher.algorithm` - which cipher algorithm shall be used for 
encrypting password values; supported 
 algorithms include `SHA-1`, `SHA-256`, `SHA-512`, `AES`, `S-MD5`, `S-SHA-1`, 
`S-SHA-256`, `S-SHA-512` and `BCRYPT`;
 salting options are available in the `core.properties` file;
-[WARNING]
-The value of the `security.aesSecretKey` property in the `core.properties` 
file is used for AES-based encryption /
-decryption: besides password values, this is also used whenever reversible 
encryption is needed, throughout the whole
-system. +
-The actual length of the `security.aesSecretKey` value is used to drive the 
AES algorithm variant selection:
-16 characters implies `AES-128`, 24 selects `AES-192` and 32 configures 
`AES-256`. +
-When the `security.aesSecretKey` value has length less than 16, between 17 and 
23 or between 25 and 31, it is
-right-padded by random characters during startup, to reach the nearest option. 
If the specified value is instead longer
-than 32 characters, it is truncated to 32. +
-It is *strongly* recommended to provide a value long exactly 16, 24 or 32 
characters, in order to avoid unexpected
-behaviors at runtime, expecially with high-availability. 
 * `jwt.lifetime.minutes` - validity of 
https://en.wikipedia.org/wiki/JSON_Web_Token[JSON Web Token^] values used for
 <<rest-authentication-and-authorization,authentication>> (in minutes);
 * `notificationjob.cronExpression` -
diff --git a/src/main/asciidoc/reference-guide/configuration/jws.adoc 
b/src/main/asciidoc/reference-guide/configuration/security.adoc
similarity index 59%
rename from src/main/asciidoc/reference-guide/configuration/jws.adoc
rename to src/main/asciidoc/reference-guide/configuration/security.adoc
index 704efe5b88..54fcd57fe1 100644
--- a/src/main/asciidoc/reference-guide/configuration/jws.adoc
+++ b/src/main/asciidoc/reference-guide/configuration/security.adoc
@@ -16,34 +16,93 @@
 // specific language governing permissions and limitations
 // under the License.
 //
-=== Control JWT signature
 
-As explained <<rest-authentication-and-authorization,above>>, the REST 
authentication process generates, in case of
-success, a unique signed JWT (JSON Web Token). +
-Such JWT values are signed by Apache Syncope according to the 
https://tools.ietf.org/html/rfc7515[JWS^]
-(JSON Web Signature) specification.
+=== Security
+
+Most security aspects can be configured with the following properties:
+
+[cols="1,1,3",options="header"]
+|===
+|Property
+|Default
+|Description
+|`security.productionMode`
+|`true`
+|Enables or disables the <<production-mode>>.
+|`security.adminUser`
+|`admin`
+|Super-administrator username.
+|`security.adminPasswordAlgorithm`
+|`SSHA256`
+|Supported values are `SHA1`, `SHA256`, `SHA512`, `SMD5`, `SSHA1`, `SSHA512` 
and `BCRYPT`.
+|`security.adminPassword`
+|Value from the Maven property `adminPassword`
+|Super-administrator password's hashed value for the selected 
`security.adminPasswordAlgorithm`. +
+Check <<set-admin-credentials,how to generate this value>>.
+|`security.adminMfaSecret`
+|Empty
+|Super-administrator's MFA secret (if <<authentiation-credentials,enabled>>). +
+Check <<set-admin-credentials,how to generate this value>>.
+|`security.anonymousUser`
+|`anonymous`
+|Username for <<rest-authorization-summary,anonymous authentication>>.
+|`security.anonymousKey`
+|Value from the Maven property `anonymousKey`
+|Password for <<rest-authorization-summary,anonymous authentication>>.
+|`security.jwtIssuer`
+|`ApacheSyncope`
+|Value used to set the Issuer claim for the JWT values as generated by the
+<<rest-authentication-and-authorization,REST authentication process>>.
+|`security.jwsAlgorithm`
+|`HS512`
+|Algorithm used to sign the JWT values according to the 
https://tools.ietf.org/html/rfc7515[JWS^] (JSON Web Signature)
+specification. See <<jws-signature>> for more details.
+|`security.jwsKey`
+|Value from the Maven property `jwsKey`
+|Key used to sign the JWT values according to the 
https://tools.ietf.org/html/rfc7515[JWS^] (JSON Web Signature)
+specification. See <<jws-signature>> for more details.
+|`security.aesSecretKey`
+|Value from the Maven property `secretKey`
+|Key used for AES-based encryption / decryption: besides password values, this 
is also used whenever reversible
+encryption is needed. +
+The actual value length is used to drive the AES algorithm variant selection:
+16 characters implies `AES-128`, 24 selects `AES-192` and 32 configures 
`AES-256`.
+|===
+
+==== Production Mode
+
+Core will refuse to start in case the overall security requirements are not 
met: for example, the default credentials
+are in use, invalid keys are configured and so on.
+
+The production mode can be temporarily disabled during the initial phases of 
the IAM project, to help out administrators
+with fine-tuning the several security aspects.
+
+For example, when the `security.aesSecretKey` value has length less than 16, 
between 17 and 23 or between 25 and 31,
+the configured value is right-padded by random characters during startup, to 
reach the nearest option.
+If the specified value is instead longer than 32 characters, it is truncated 
to 32. +
+Finally, the resulting value is sent to the debug logs for administrators to 
take note and report to configuration.
+
+==== JWS Signature
 
 [[jws-hmac]]
-==== Hash-based Message Authentication Code
+===== Hash-based Message Authentication Code
 
-This is the default configuration, where Core and clients posses a shared 
secret, configured under `core.properties`
-as the `jwsKey` property value.
+This is the default configuration, where Core and clients posses a shared 
secret.
 
-.Default JWS configuration
+.Example JWS HMAC configuration
 ====
 [source,properties]
 ----
 security.jwsAlgorithm=HS512 // <1>
-security.jwsKey=ZW7pRixehFuNUtnY5Se47IemgMryTzazPPJ9CGX5LTCmsOJpOgHAQEuPQeV9A28f
 // <2>
+security.jwsKey=<value> // <2>
 ----
 <1> Valid values are `HS256`, `HS384` and `HS512`
 <2> Any alphanumeric value satisfying the 
https://tools.ietf.org/html/rfc7518#section-3.2[length requirement^] can be
 used
 ====
 
-
 [[jws-rsa]]
-==== RSA Public-Key Cryptography
+===== RSA Public-Key Cryptography
 
 This configuration requires to specify a key pair: the former key value, said 
_private_, must be kept secret for internal
 Core usage while the latter key value, said _public_, is to be shared with 
clients.
diff --git a/src/main/asciidoc/reference-guide/howto/setadmincredentials.adoc 
b/src/main/asciidoc/reference-guide/howto/setadmincredentials.adoc
index a0c40a2d99..46b7a8dfdb 100644
--- a/src/main/asciidoc/reference-guide/howto/setadmincredentials.adoc
+++ b/src/main/asciidoc/reference-guide/howto/setadmincredentials.adoc
@@ -21,12 +21,8 @@
 [WARNING]
 The procedure below affects only the `Master` <<domains,domain>>; for other 
domains check <<domains-management,above>>.
 
-The credentials are defined in the `core.properties` file; text encoding must 
be set to UTF-8:
-
-* `security.adminUser` - administrator username (default `admin`)
-* `security.adminPassword` - administrator password (default `password`)'s 
hashed value
-* `security.adminPasswordAlgorithm` - algorithm to be used for hash evaluation 
(default `SSHA256`, also supported are
-`SHA1`, `SHA256`, `SHA512`, `SMD5`, `SSHA1`, `SSHA512` and `BCRYPT`)
+The admin password can be generated by the following commands and then 
reported into
+<<security,configuration>>.
 
 .Generate SHA1 password value on GNU / Linux
 ====
diff --git a/src/main/asciidoc/reference-guide/usage/core.adoc 
b/src/main/asciidoc/reference-guide/usage/core.adoc
index 65fada60d5..ba37c5f383 100644
--- a/src/main/asciidoc/reference-guide/usage/core.adoc
+++ b/src/main/asciidoc/reference-guide/usage/core.adoc
@@ -218,6 +218,68 @@ the <<entitlements,entitlements>> owned by the requesting 
user.
 When invoking the REST endpoint `/users/self` in `GET`, the 
`X-Syncope-Delegations` response header will list all
 delegating users for each <<delegation,Delegation>> for which the requesting 
user is delegated.
 
+==== REST Rate Limiting
+Rate limiting can be applied to REST requests before authentication and 
request processing.
+
+This feature is disabled by default and can be enabled with the following 
properties:
+
+[cols="1,1,3",options="header"]
+|===
+|Property
+|Default
+|Description
+|`rest.rateLimit.enabled`
+|`false`
+|Enables or disables REST request rate limiting.
+|`rest.rateLimit.maxRequests`
+|`300`
+|Maximum number of requests allowed from the same client address within the 
configured time window.
+|`rest.rateLimit.window`
+|`1m`
+|Time window used to count requests from the same client address.
+|`rest.rateLimit.lock`
+|`1m`
+|Amount of time for which the client address is blocked after exceeding the 
configured request limit.
+|`rest.rateLimit.forwardedForHeader`
+|`X-Forwarded-For`
+|HTTP header used to resolve the original client address when the request 
comes from a trusted proxy.
+|`rest.rateLimit.trustedProxies`
+|Empty
+|Set of proxy addresses whose `X-Forwarded-For` header value is
+trusted. The header is ignored for requests not coming from one of these 
addresses.
+|`rest.rateLimit.excludedAddresses`
+|Empty
+|Set of client addresses excluded from rate limiting. This is where addresses 
for trusted internal clients,
+such as Admin Console or Enduser instances calling the Core REST API, should 
be configured when their
+traffic must not be rate limited.
+|===
+
+Addresses are matched exactly as reported by the servlet request remote 
address. CIDR ranges and wildcard
+patterns are not supported.
+
+When Core is deployed behind a reverse proxy, configure the proxy address under
+`rest.rateLimit.trustedProxies` to allow for the original client from the 
configured `rest.rateLimit.forwardedForHeader`
+to be identified. +
+Add addresses to `rest.rateLimit.excludedAddresses` only for callers that 
should bypass rate limiting
+entirely, for example trusted Console instances or internal monitoring clients.
+
+Example:
+[source,properties]
+----
+rest.rateLimit.enabled=true
+rest.rateLimit.maxRequests=300
+rest.rateLimit.window=1m
+rest.rateLimit.lock=1m
+rest.rateLimit.trustedProxies=127.0.0.1
+rest.rateLimit.excludedAddresses=127.0.0.1,10.0.0.10
+----
+When the limit is exceeded, the following HTTP response is returned:
+[source]
+----
+HTTP/1.1 429 Too Many Requests
+Retry-After: <seconds>
+----
+
 ==== Batch
 
 Batch requests allow grouping multiple operations into a single HTTP request 
payload. +

Reply via email to