This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/syncope.git
commit b63fee6b313e5ca6d7e079060b4e21ecdeb24c92 Author: Francesco Chicchiriccò <[email protected]> AuthorDate: Tue Sep 8 14:39:48 2026 +0200 [SYNCOPE-1996] Security production mode --- .../core/logic/UserSelfLogicPasswordResetTest.java | 1 + .../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 | 2 +- .../asciidoc/getting-started/movingForward.adoc | 32 +++--- .../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 | 9 +- src/main/asciidoc/reference-guide/usage/core.adoc | 124 ++++++++++----------- 29 files changed, 344 insertions(+), 181 deletions(-) diff --git a/core/idrepo/logic/src/test/java/org/apache/syncope/core/logic/UserSelfLogicPasswordResetTest.java b/core/idrepo/logic/src/test/java/org/apache/syncope/core/logic/UserSelfLogicPasswordResetTest.java index 78fadb0577..94d9d979de 100644 --- a/core/idrepo/logic/src/test/java/org/apache/syncope/core/logic/UserSelfLogicPasswordResetTest.java +++ b/core/idrepo/logic/src/test/java/org/apache/syncope/core/logic/UserSelfLogicPasswordResetTest.java @@ -66,6 +66,7 @@ public class UserSelfLogicPasswordResetTest { ConfParamOps confParamOps = mock(ConfParamOps.class); userDAO = mock(UserDAO.class); securityProperties = new SecurityProperties(); + securityProperties.setProductionMode(false); when(confParamOps.get(any(), eq(StandardConfParams.PASSWORD_RESET_ALLOWED), eq(false), eq(boolean.class))). thenReturn(true); 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 a8995b97ab..9f9aec48be 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 @@ -34,6 +34,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; @@ -120,7 +121,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 23c11998f5..f812d7427f 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 cc0bd75b71..7c41140def 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 @@ -65,31 +65,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 @@ -109,9 +123,11 @@ public class SecurityContext { final JWSAlgorithm jwsAlgorithm) { return new DefaultCredentialChecker( + props.getAesSecretKey(), jwsKey(jwsAlgorithm, props), props.getAdminPassword(), - props.getAnonymousKey()); + props.getAnonymousKey(), + props.isProductionMode()); } @ConditionalOnMissingBean @@ -164,8 +180,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 2470d84051..b01cb3b419 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 @@ -150,6 +150,8 @@ public class SecurityProperties { } } + private boolean productionMode = true; + private String adminUser; private String adminPassword; @@ -180,6 +182,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 16210f3ba4..27c75d568e 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 b08a6d261f..0acb282450 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 e35c4832fd..01f2c2d65d 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 @@ -180,6 +180,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; @@ -1231,6 +1232,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 37616a8adf..685021a239 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 @@ -638,7 +638,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 1bac452fdc..e8621ee5cc 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 @@ -134,7 +134,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 bb3e47eaa7..1361785bd6 100644 --- a/pom.xml +++ b/pom.xml @@ -485,7 +485,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>5.0.0</wicket-spring-boot.version> diff --git a/src/main/asciidoc/getting-started/movingForward.adoc b/src/main/asciidoc/getting-started/movingForward.adoc index 293517b593..ac67242f85 100644 --- a/src/main/asciidoc/getting-started/movingForward.adoc +++ b/src/main/asciidoc/getting-started/movingForward.adoc @@ -19,33 +19,27 @@ == 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.1reference-guide.html[Apache Syncope Reference Guide.] +https://syncope.apache.org/docs/4.1/reference-guide.html[Apache Syncope Reference Guide.] endif::[] ifeval::["{backend}" == "pdf"] https://syncope.apache.org/docs/4.1/reference-guide.pdf[Apache Syncope Reference Guide.] 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 8d04ac598f..4485826cda 100644 --- a/src/main/asciidoc/reference-guide/architecture/core.adoc +++ b/src/main/asciidoc/reference-guide/architecture/core.adoc @@ -95,6 +95,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 77c789c34b..ca001e6124 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 cee923a196..cc3ed1a593 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); * `mfa.enabled` - when `true`, an OTP value generated by the enrolled app is additionally requested as 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 f4988a5c7c..a990db5575 100644 --- a/src/main/asciidoc/reference-guide/howto/setadmincredentials.adoc +++ b/src/main/asciidoc/reference-guide/howto/setadmincredentials.adoc @@ -21,13 +21,8 @@ [WARNING] The procedures 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`) -* `security.adminMfaSecret` - administrator MFA secret (no default) +The admin credentials (password and MFA secret) 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 c1176ef129..1b192fabcb 100644 --- a/src/main/asciidoc/reference-guide/usage/core.adoc +++ b/src/main/asciidoc/reference-guide/usage/core.adoc @@ -48,68 +48,6 @@ protocol://host:port/syncope/ .... ==== -==== 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> ----- - ==== REST Authentication and Authorization The <<core>> authentication and authorization is based on https://spring.io/projects/spring-security[Spring Security^]. @@ -370,6 +308,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. +
