GUACAMOLE-990: Add extension for automatically blocking brute-force auth attempts.

This commit is contained in:
Michael Jumper
2022-01-05 15:08:54 -08:00
parent e6a61b7223
commit 275b5bee13
9 changed files with 734 additions and 0 deletions

View File

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-ban</artifactId>
<packaging>jar</packaging>
<version>1.4.0</version>
<name>guacamole-auth-ban</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>extensions</artifactId>
<version>1.4.0</version>
<relativePath>../</relativePath>
</parent>
<dependencies>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.4.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0
http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include licenses and extension .jar -->
<fileSets>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,123 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ban;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The current status of an authentication failure, including the number of
* times the failure has occurred.
*/
public class AuthenticationFailureStatus {
/**
* The timestamp of the last authentication failure, as returned by
* System.nanoTime().
*/
private long lastFailure;
/**
* The number of failures that have occurred.
*/
private final AtomicInteger failureCount;
/**
* The maximum number of failures that may occur before the user/address
* causing the failures is blocked.
*/
private final int maxAttempts;
/**
* The amount of time that a user/address must remain blocked after they
* have reached the maximum number of failures. Unlike the value provided
* at construction time, this value is maintained in nanoseconds.
*/
private final long duration;
/**
* Creates an AuthenticationFailureStatus that represents a single failure
* and is subject to the given restrictions. Additional failures may be
* flagged after creation with {@link #notifyFailed()}.
*
* @param maxAttempts
* The maximum number of failures that may occur before the
* user/address causing the failures is blocked.
*
* @param duration
* The amount of time, in seconds, that a user/address must remain
* blocked after they have reached the maximum number of failures.
*/
public AuthenticationFailureStatus(int maxAttempts, int duration) {
this.lastFailure = System.nanoTime();
this.failureCount = new AtomicInteger(1);
this.maxAttempts = maxAttempts;
this.duration = TimeUnit.SECONDS.toNanos(duration);
}
/**
* Updates this authentication failure, noting that the failure it
* represents has recurred.
*/
public void notifyFailed() {
lastFailure = System.nanoTime();
failureCount.incrementAndGet();
}
/**
* Returns whether this authentication failure is recent enough that it
* should still be tracked. This function will return false for
* authentication failures that have not recurred for at least the duration
* provided at construction time.
*
* @return
* true if this authentication failure is recent enough that it should
* still be tracked, false otherwise.
*/
public boolean isValid() {
return System.nanoTime() - lastFailure <= duration;
}
/**
* Returns whether the user/address causing this authentication failure
* should be blocked based on the restrictions provided at construction
* time.
*
* @return
* true if the user/address causing this failure should be blocked,
* false otherwise.
*/
public boolean isBlocked() {
return isValid() && failureCount.get() >= maxAttempts;
}
/**
* Returns the total number of authentication failures that have been
* recorded through creating this object and invoking
* {@link #notifyFailed()}.
*
* @return
* The total number of failures that have occurred.
*/
public int getFailures() {
return failureCount.get();
}
}

View File

@@ -0,0 +1,278 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ban;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleClientTooManyException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.net.auth.Credentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides automated tracking and blocking of IP addresses that repeatedly
* fail authentication.
*/
public class AuthenticationFailureTracker {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AuthenticationFailureTracker.class);
/**
* All authentication failures currently being tracked, stored by the
* associated IP address.
*/
private final ConcurrentMap<String, AuthenticationFailureStatus> failures =
new ConcurrentHashMap<>();
/**
* The maximum number of failed authentication attempts allowed before an
* address is temporarily banned.
*/
private final int maxAttempts;
/**
* The length of time that each address should be banned after reaching the
* maximum number of failed authentication attempts, in seconds.
*/
private final int banDuration;
/**
* Creates a new AuthenticationFailureTracker that automatically blocks
* authentication attempts based on the provided blocking criteria.
*
* @param maxAttempts
* The maximum number of failed authentication attempts allowed before
* an address is temporarily banned.
*
* @param banDuration
* The length of time that each address should be banned after reaching
* the maximum number of failed authentication attempts, in seconds.
*/
public AuthenticationFailureTracker(int maxAttempts, int banDuration) {
this.maxAttempts = maxAttempts;
this.banDuration = banDuration;
// Inform administrator of configured behavior
if (maxAttempts <= 0) {
logger.info("Maximum failed authentication attempts has been set "
+ "to {}. Automatic banning of brute-force authentication "
+ "attempts will be disabled.", maxAttempts);
}
else if (banDuration <= 0) {
logger.info("Ban duration for addresses that repeatedly fail "
+ "authentication has been set to {}. Automatic banning "
+ "of brute-force authentication attempts will be "
+ "disabled.", banDuration);
}
else {
logger.info("Addresses will be automatically banned for {} "
+ "seconds after {} failed authentication attempts.",
banDuration, maxAttempts);
}
}
/**
* Returns whether the given Credentials do not contain any specific
* authentication parameters, including HTTP parameters. An authentication
* request that contains no parameters whatsoever will tend to be the
* first, anonymous, credential-less authentication attempt that results in
* the initial login screen rendering.
*
* @param credentials
* The Credentials object to test.
*
* @return
* true if the given Credentials contain no authentication parameters
* whatsoever, false otherwise.
*/
private boolean isEmpty(Credentials credentials) {
// An authentication request that contains an explicit username or
// password (even if blank) is non-empty, regardless of how the values
// were passed
if (credentials.getUsername() != null || credentials.getPassword() != null)
return false;
// All further tests depend on HTTP request details
HttpServletRequest request = credentials.getRequest();
if (request == null)
return true;
// An authentication request is non-empty if it contains any HTTP
// parameters at all or contains an authentication token
return !request.getParameterNames().hasMoreElements()
&& request.getHeader("Guacamole-Token") == null;
}
/**
* Reports that the given address has just failed to authenticate and
* returns the AuthenticationFailureStatus that represents that failure. If
* the address isn't already being tracked, it will begin being tracked as
* of this call. If the address is already tracked, the returned
* AuthenticationFailureStatus will represent past authentication failures,
* as well.
*
* @param address
* The address that has failed to authenticate.
*
* @return
* An AuthenticationFailureStatus that represents this latest
* authentication failure for the given address, as well as any past
* failures.
*/
private AuthenticationFailureStatus getAuthenticationFailure(String address) {
AuthenticationFailureStatus newFailure = new AuthenticationFailureStatus(maxAttempts, banDuration);
AuthenticationFailureStatus status = failures.putIfAbsent(address, newFailure);
if (status == null)
return newFailure;
status.notifyFailed();
return status;
}
/**
* Reports that an authentication request has been received, as well as
* whether that request is known to have failed. If the associated address
* is currently being blocked, an exception will be thrown.
*
* @param credentials
* The credentials associated with the authentication request.
*
* @param failed
* Whether the request is known to have failed. If the status of the
* request is not yet known, this should be false.
*
* @throws GuacamoleException
* If the authentication request is being blocked due to brute force
* prevention rules.
*/
private void notifyAuthenticationStatus(Credentials credentials,
boolean failed) throws GuacamoleException {
// Do not track/ban if tracking or banning are disabled
if (maxAttempts <= 0 || banDuration <= 0)
return;
// Ignore requests that do not contain explicit parameters of any kind
if (isEmpty(credentials))
return;
// Determine originating address of the authentication request
String address = credentials.getRemoteAddress();
if (address == null)
throw new GuacamoleServerException("Source address cannot be determined.");
// Get current failure status for the address associated with the
// authentication request, adding/updating that status if the request
// was itself a failure
AuthenticationFailureStatus status;
if (failed) {
status = getAuthenticationFailure(address);
logger.debug("Authentication has failed for address \"{}\" (current total failures: {}/{}).",
address, status.getFailures(), maxAttempts);
}
else
status = failures.get(address);
if (status != null) {
// Explicitly block further processing of authentication/authorization
// if too many failures have occurred
if (status.isBlocked()) {
logger.debug("Blocking authentication attempt from address \"{}\" due to number of authentication failures.", address);
throw new GuacamoleClientTooManyException("Too many failed "
+ "authentication attempts. Please try again later.");
}
// Clean up tracking of failures if the address is no longer
// relevant (all failures are sufficiently old)
else if (!status.isValid()) {
logger.debug("Removing address \"{}\" from tracking as there are no recent authentication failures.", address);
failures.remove(address);
}
}
}
/**
* Reports that an authentication request has been received, but it is
* either not yet known whether the request has succeeded or failed. If the
* associated address is currently being blocked, an exception will be
* thrown.
*
* @param credentials
* The credentials associated with the authentication request.
*
* @throws GuacamoleException
* If the authentication request is being blocked due to brute force
* prevention rules.
*/
public void notifyAuthenticationRequestReceived(Credentials credentials)
throws GuacamoleException {
notifyAuthenticationStatus(credentials, false);
}
/**
* Reports that an authentication request has been received and has
* succeeded. If the associated address is currently being blocked, an
* exception will be thrown.
*
* @param credentials
* The credentials associated with the successful authentication
* request.
*
* @throws GuacamoleException
* If the authentication request is being blocked due to brute force
* prevention rules.
*/
public void notifyAuthenticationSuccess(Credentials credentials)
throws GuacamoleException {
notifyAuthenticationStatus(credentials, false);
}
/**
* Reports that an authentication request has been received and has
* failed. If the associated address is currently being blocked, an
* exception will be thrown.
*
* @param credentials
* The credentials associated with the failed authentication request.
*
* @throws GuacamoleException
* If the authentication request is being blocked due to brute force
* prevention rules.
*/
public void notifyAuthenticationFailed(Credentials credentials)
throws GuacamoleException {
notifyAuthenticationStatus(credentials, true);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ban;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.event.AuthenticationFailureEvent;
import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
import org.apache.guacamole.net.event.listener.Listener;
/**
* Listener implementation which automatically tracks authentication failures
* such that further authentication attempts may be automatically blocked by
* {@link BanningAuthenticationProvider} if they match configured criteria.
*/
public class BanningAuthenticationListener implements Listener {
/**
* Shared tracker of addresses that have repeatedly failed authentication.
*/
private static AuthenticationFailureTracker tracker;
/**
* Assigns the shared tracker instance used by both the {@link BanningAuthenticationProvider}
* and this listener. This function MUST be invoked with the tracker
* created for BanningAuthenticationProvider as soon as possible (during
* construction of BanningAuthenticationProvider), or processing of
* received events will fail internally.
*
* @param tracker
* The tracker instance to use for received authentication events.
*/
public static void setAuthenticationFailureTracker(AuthenticationFailureTracker tracker) {
BanningAuthenticationListener.tracker = tracker;
}
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof AuthenticationFailureEvent) {
AuthenticationFailureEvent failure = (AuthenticationFailureEvent) event;
// Requests for additional credentials are not failures per se,
// but continuations of a multi-request authentication attempt that
// has not yet succeeded OR failed
if (failure.getFailure() instanceof GuacamoleInsufficientCredentialsException) {
tracker.notifyAuthenticationRequestReceived(failure.getCredentials());
return;
}
// Consider all other errors to be failed auth attempts
tracker.notifyAuthenticationFailed(failure.getCredentials());
}
else if (event instanceof AuthenticationSuccessEvent) {
AuthenticationSuccessEvent success = (AuthenticationSuccessEvent) event;
tracker.notifyAuthenticationSuccess(success.getCredentials());
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.ban;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
/**
* AuthenticationProvider implementation that blocks further authentication
* attempts that are related to past authentication failures flagged by
* {@link BanningAuthenticationListener}.
*/
public class BanningAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* The maximum number of failed authentication attempts allowed before an
* address is temporarily banned.
*/
private static final IntegerGuacamoleProperty MAX_ATTEMPTS = new IntegerGuacamoleProperty() {
@Override
public String getName() {
return "ban-max-invalid-attempts";
}
};
/**
* The length of time that each address should be banned after reaching the
* maximum number of failed authentication attempts, in seconds.
*/
private static final IntegerGuacamoleProperty IP_BAN_DURATION = new IntegerGuacamoleProperty() {
@Override
public String getName() {
return "ban-address-duration";
}
};
/**
* The default maximum number of failed authentication attempts allowed
* before an address is temporarily banned.
*/
private static final int DEFAULT_MAX_ATTEMPTS = 5;
/**
* The default length of time that each address should be banned after
* reaching the maximum number of failed authentication attempts, in
* seconds.
*/
private static final int DEFAULT_IP_BAN_DURATION = 300;
/**
* Shared tracker of addresses that have repeatedly failed authentication.
*/
private final AuthenticationFailureTracker tracker;
/**
* Creates a new BanningAuthenticationProvider which automatically bans
* further authentication attempts from addresses that have repeatedly
* failed to authenticate. The ban duration and maximum number of failed
* attempts allowed before banning are configured within
* guacamole.properties.
*
* @throws GuacamoleException
* If an error occurs parsing the configuration properties used by this
* extension.
*/
public BanningAuthenticationProvider() throws GuacamoleException {
Environment environment = LocalEnvironment.getInstance();
int maxAttempts = environment.getProperty(MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS);
int banDuration = environment.getProperty(IP_BAN_DURATION, DEFAULT_IP_BAN_DURATION);
tracker = new AuthenticationFailureTracker(maxAttempts, banDuration);
BanningAuthenticationListener.setAuthenticationFailureTracker(tracker);
}
@Override
public String getIdentifier() {
return "ban";
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException {
tracker.notifyAuthenticationRequestReceived(credentials);
return null;
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException {
tracker.notifyAuthenticationRequestReceived(authenticatedUser.getCredentials());
return null;
}
}

View File

@@ -0,0 +1,16 @@
{
"guacamoleVersion" : "1.4.0",
"name" : "Brute-force Authentication Detection/Prevention",
"namespace" : "ban",
"authProviders" : [
"org.apache.guacamole.auth.ban.BanningAuthenticationProvider"
],
"listeners" : [
"org.apache.guacamole.auth.ban.BanningAuthenticationListener"
]
}

View File

@@ -40,6 +40,7 @@
<modules> <modules>
<!-- Authentication extensions --> <!-- Authentication extensions -->
<module>guacamole-auth-ban</module>
<module>guacamole-auth-duo</module> <module>guacamole-auth-duo</module>
<module>guacamole-auth-header</module> <module>guacamole-auth-header</module>
<module>guacamole-auth-jdbc</module> <module>guacamole-auth-jdbc</module>