diff --git a/extensions/guacamole-auth-ban/.ratignore b/extensions/guacamole-auth-ban/.ratignore new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/guacamole-auth-ban/pom.xml b/extensions/guacamole-auth-ban/pom.xml new file mode 100644 index 000000000..2be082fc2 --- /dev/null +++ b/extensions/guacamole-auth-ban/pom.xml @@ -0,0 +1,60 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-auth-ban + jar + 1.4.0 + guacamole-auth-ban + http://guacamole.apache.org/ + + + org.apache.guacamole + extensions + 1.4.0 + ../ + + + + + + + javax.servlet + servlet-api + 2.5 + provided + + + + + org.apache.guacamole + guacamole-ext + 1.4.0 + provided + + + + + diff --git a/extensions/guacamole-auth-ban/src/main/assembly/dist.xml b/extensions/guacamole-auth-ban/src/main/assembly/dist.xml new file mode 100644 index 000000000..d046ae699 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/assembly/dist.xml @@ -0,0 +1,54 @@ + + + + + dist + ${project.artifactId}-${project.version} + + + + tar.gz + + + + + + + + + target/licenses + + + + + target + + + *.jar + + + + + + diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java new file mode 100644 index 000000000..87bdc5515 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java @@ -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(); + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java new file mode 100644 index 000000000..4f77875fc --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java @@ -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 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); + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java new file mode 100644 index 000000000..38fd575fd --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java @@ -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()); + } + + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java new file mode 100644 index 000000000..12195f5e4 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java @@ -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; + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..87f75ea61 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json @@ -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" + ] + +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 3bab33257..b16b3ed53 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -40,6 +40,7 @@ + guacamole-auth-ban guacamole-auth-duo guacamole-auth-header guacamole-auth-jdbc