Split SecurityManagedConfiguration to mgmt and ddi starters (#2014)
* SecurityManagedConfiguration is moved to hawkbit-rest-core with commons for mgmt and ddi only * Configurations for DDI and Management API are moved to respective starters * hawkbit-http-security is removed - DosFilter (as common) is moved in hawkbit-rest-security, rest to the ddi starter as used only there * some classes are moved into different packages - it is a bad practice to have same packet into multiple artifacts _release_notes_ Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -29,18 +29,15 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-artifact-api</artifactId>
|
||||
<artifactId>hawkbit-security-integration</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-artifact-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
@@ -61,12 +58,27 @@
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.security.DdiSecurityProperties;
|
||||
import org.eclipse.hawkbit.rest.security.DosFilter;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.eclipse.hawkbit.security.PreAuthTokenSourceTrustAuthenticationProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.AdviceMode;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.firewall.FirewalledRequest;
|
||||
import org.springframework.security.web.firewall.HttpFirewall;
|
||||
import org.springframework.security.web.firewall.StrictHttpFirewall;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* All configurations related to HawkBit's authentication and authorization layer.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.ASPECTJ, proxyTargetClass = true, securedEnabled = true)
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@PropertySource("classpath:hawkbit-security-defaults.properties")
|
||||
public class SecurityManagedConfiguration {
|
||||
|
||||
public static final String ANONYMOUS_CONTROLLER_SECURITY_ENABLED_SHOULD_ONLY_BE_USED_FOR_DEVELOPMENT_PURPOSES = """
|
||||
******************
|
||||
** Anonymous controller security enabled, should only be used for development purposes **
|
||||
******************""";
|
||||
public static final int DOS_FILTER_ORDER = -200;
|
||||
|
||||
/**
|
||||
* Filter to protect the hawkBit server system management interface against too many requests.
|
||||
*
|
||||
* @param securityProperties for filter configuration
|
||||
* @return the spring filter registration bean for registering a denial of service protection filter in the filter chain
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true)
|
||||
public FilterRegistrationBean<DosFilter> dosSystemFilter(final HawkbitSecurityProperties securityProperties) {
|
||||
final FilterRegistrationBean<DosFilter> filterRegBean = dosFilter(Collections.emptyList(),
|
||||
securityProperties.getDos().getFilter(), securityProperties.getClients());
|
||||
filterRegBean.setUrlPatterns(List.of("/system/*"));
|
||||
filterRegBean.setOrder(DOS_FILTER_ORDER);
|
||||
filterRegBean.setName("dosSystemFilter");
|
||||
|
||||
return filterRegBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* HttpFirewall which enables to define a list of allowed host names.
|
||||
*
|
||||
* @return the http firewall.
|
||||
*/
|
||||
@Bean
|
||||
public HttpFirewall httpFirewall(final HawkbitSecurityProperties hawkbitSecurityProperties) {
|
||||
final List<String> allowedHostNames = hawkbitSecurityProperties.getAllowedHostNames();
|
||||
final IgnorePathsStrictHttpFirewall firewall = new IgnorePathsStrictHttpFirewall(
|
||||
hawkbitSecurityProperties.getHttpFirewallIgnoredPaths());
|
||||
|
||||
if (!CollectionUtils.isEmpty(allowedHostNames)) {
|
||||
firewall.setAllowedHostnames(hostName -> {
|
||||
log.debug("Firewall check host: {}, allowed: {}", hostName, allowedHostNames.contains(hostName));
|
||||
return allowedHostNames.contains(hostName);
|
||||
});
|
||||
}
|
||||
return firewall;
|
||||
}
|
||||
|
||||
public static FilterRegistrationBean<DosFilter> dosFilter(final Collection<String> includeAntPaths,
|
||||
final HawkbitSecurityProperties.Dos.Filter filterProperties,
|
||||
final HawkbitSecurityProperties.Clients clientProperties) {
|
||||
final FilterRegistrationBean<DosFilter> filterRegBean = new FilterRegistrationBean<>();
|
||||
|
||||
filterRegBean.setFilter(new DosFilter(includeAntPaths, filterProperties.getMaxRead(),
|
||||
filterProperties.getMaxWrite(), filterProperties.getWhitelist(), clientProperties.getBlacklist(),
|
||||
clientProperties.getRemoteIpHeader()));
|
||||
|
||||
return filterRegBean;
|
||||
}
|
||||
|
||||
private static class IgnorePathsStrictHttpFirewall extends StrictHttpFirewall {
|
||||
|
||||
private final Collection<String> pathsToIgnore;
|
||||
|
||||
public IgnorePathsStrictHttpFirewall(final Collection<String> pathsToIgnore) {
|
||||
super();
|
||||
this.pathsToIgnore = pathsToIgnore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FirewalledRequest getFirewalledRequest(final HttpServletRequest request) {
|
||||
if (pathsToIgnore != null && pathsToIgnore.contains(request.getRequestURI())) {
|
||||
return new FirewalledRequest(request) {
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
// nothing to do
|
||||
}
|
||||
};
|
||||
}
|
||||
return super.getFirewalledRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.security.SecurityConstants;
|
||||
import org.eclipse.hawkbit.util.IpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Filter for protection against denial of service attacks. It reduces the
|
||||
* maximum number of request per seconds which can be separately configured for
|
||||
* read (GET) and write (PUT/POST/DELETE) requests.
|
||||
*/
|
||||
@Slf4j
|
||||
public class DosFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Logger LOG_DOS =
|
||||
LoggerFactory.getLogger(SecurityConstants.SECURITY_LOG_PREFIX + ".dos");
|
||||
private static final Logger LOG_BLACKLIST =
|
||||
LoggerFactory.getLogger(SecurityConstants.SECURITY_LOG_PREFIX + ".blacklist");
|
||||
|
||||
private final AntPathMatcher antMatcher = new AntPathMatcher();
|
||||
private final Collection<String> includeAntPaths;
|
||||
|
||||
private final Pattern ipAdressBlacklist;
|
||||
|
||||
private final Cache<String, AtomicInteger> readCountCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(1, TimeUnit.SECONDS).build();
|
||||
|
||||
private final Cache<String, AtomicInteger> writeCountCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(1, TimeUnit.SECONDS).build();
|
||||
|
||||
private final int maxRead;
|
||||
private final int maxWrite;
|
||||
|
||||
private final Pattern whitelist;
|
||||
|
||||
private final String forwardHeader;
|
||||
|
||||
/**
|
||||
* Filter constructor including configuration.
|
||||
*
|
||||
* @param includeAntPaths paths where filter should hit
|
||||
* @param maxRead Maximum number of allowed REST read/GET requests per second
|
||||
* per client
|
||||
* @param maxWrite Maximum number of allowed REST write/(PUT/POST/etc.) requests
|
||||
* per second per client
|
||||
* @param ipDosWhiteListPattern {@link Pattern} with with white list of peer IP addresses for
|
||||
* DOS filter
|
||||
* @param ipBlackListPattern {@link Pattern} with black listed IP addresses
|
||||
* @param forwardHeader the header containing the forwarded IP address e.g.
|
||||
* {@code x-forwarded-for}
|
||||
*/
|
||||
public DosFilter(final Collection<String> includeAntPaths, final int maxRead, final int maxWrite,
|
||||
final String ipDosWhiteListPattern, final String ipBlackListPattern, final String forwardHeader) {
|
||||
this.includeAntPaths = includeAntPaths;
|
||||
this.maxRead = maxRead;
|
||||
this.maxWrite = maxWrite;
|
||||
this.forwardHeader = forwardHeader;
|
||||
|
||||
if (ipBlackListPattern != null && !ipBlackListPattern.isEmpty()) {
|
||||
ipAdressBlacklist = Pattern.compile(ipBlackListPattern);
|
||||
} else {
|
||||
ipAdressBlacklist = null;
|
||||
}
|
||||
|
||||
if (ipDosWhiteListPattern != null && !ipDosWhiteListPattern.isEmpty()) {
|
||||
whitelist = Pattern.compile(ipDosWhiteListPattern);
|
||||
} else {
|
||||
whitelist = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
|
||||
final FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
if (!shouldInclude(request)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean processChain;
|
||||
|
||||
final String ip = IpUtil.getClientIpFromRequest(request, forwardHeader).getHost();
|
||||
|
||||
if (checkIpFails(ip)) {
|
||||
processChain = handleMissingIpAddress(response);
|
||||
} else {
|
||||
processChain = checkAgainstBlacklist(response, ip);
|
||||
|
||||
if (processChain && (whitelist == null || !whitelist.matcher(ip).find())) {
|
||||
// read request
|
||||
if (HttpMethod.valueOf(request.getMethod()) == HttpMethod.GET) {
|
||||
processChain = handleReadRequest(response, ip);
|
||||
}
|
||||
// write request
|
||||
else {
|
||||
processChain = handleWriteRequest(response, ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (processChain) {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkIpFails(final String ip) {
|
||||
return ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip);
|
||||
}
|
||||
|
||||
private static boolean handleMissingIpAddress(final HttpServletResponse response) {
|
||||
log.error("Failed to get peer IP address");
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean shouldInclude(final HttpServletRequest request) {
|
||||
if (includeAntPaths == null || includeAntPaths.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return includeAntPaths.stream()
|
||||
.anyMatch(pattern -> antMatcher.match(request.getContextPath() + pattern, request.getRequestURI()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false if the given ip address is on the blacklist and further
|
||||
* processing of the request if forbidden
|
||||
*/
|
||||
private boolean checkAgainstBlacklist(final HttpServletResponse response, final String ip) {
|
||||
if (ipAdressBlacklist != null && ipAdressBlacklist.matcher(ip).find()) {
|
||||
LOG_BLACKLIST.info("Blacklisted client ({}) tries to access the server!", ip);
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleWriteRequest(final HttpServletResponse response, final String ip) {
|
||||
boolean processChain = true;
|
||||
final AtomicInteger count = writeCountCache.getIfPresent(ip);
|
||||
|
||||
if (count == null) {
|
||||
writeCountCache.put(ip, new AtomicInteger());
|
||||
} else if (count.getAndIncrement() > maxWrite) {
|
||||
LOG_DOS.info("Registered DOS attack! Client {} is above configured WRITE request threshold ({})!", ip,
|
||||
maxWrite);
|
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
processChain = false;
|
||||
}
|
||||
|
||||
return processChain;
|
||||
}
|
||||
|
||||
private boolean handleReadRequest(final HttpServletResponse response, final String ip) {
|
||||
boolean processChain = true;
|
||||
final AtomicInteger count = readCountCache.getIfPresent(ip);
|
||||
|
||||
if (count == null) {
|
||||
readCountCache.put(ip, new AtomicInteger());
|
||||
} else if (count.getAndIncrement() > maxRead) {
|
||||
LOG_DOS.info("Registered DOS attack! Client {} is above configured READ request threshold ({})!", ip,
|
||||
maxRead);
|
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
processChain = false;
|
||||
}
|
||||
|
||||
return processChain;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user