GUACAMOLE-839: Allow accepted subject DNs to be restricted via configuration.

This commit is contained in:
Michael Jumper
2023-02-21 17:06:37 -08:00
parent 8255326512
commit 0b5b82cc48
3 changed files with 247 additions and 51 deletions

View File

@@ -34,6 +34,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.naming.InvalidNameException; import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName; import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.HeaderParam; import javax.ws.rs.HeaderParam;
@@ -45,10 +46,11 @@ import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.auth.sso.NonceService;
import org.apache.guacamole.auth.sso.SSOResource; import org.apache.guacamole.auth.sso.SSOResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* REST API resource that allows the user to retrieve an opaque state value * REST API resource that allows the user to retrieve an opaque state value
@@ -66,6 +68,25 @@ public class SSLClientAuthenticationResource extends SSOResource {
*/ */
private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS";
/**
* The string value that the SSL termination service uses for its client
* verification header to represent that the client certificate is absent.
*/
private static final String CLIENT_VERIFIED_HEADER_NONE_VALUE = "NONE";
/**
* The string prefix that the SSL termination service uses for its client
* verification header to represent that the client certificate has failed
* validation. The error message describing the nature of the failure is
* provided by the SSL termination service after this prefix.
*/
private static final String CLIENT_VERIFIED_HEADER_FAILED_PREFIX = "FAILED:";
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(SSLClientAuthenticationResource.class);
/** /**
* Service for retrieving configuration information. * Service for retrieving configuration information.
*/ */
@@ -143,6 +164,67 @@ public class SSLClientAuthenticationResource extends SSOResource {
} }
} }
/**
* Extracts a user's username from the X.509 subject name, which should be
* in LDAP DN format. If specific username attributes are configured, only
* those username attributes are used to determine the name. If a specific
* base DN is configured, only subject names that are formatted as LDAP DNs
* within that base DN will be accepted.
*
* @param name
* The subject name to extract the username from.
*
* @return
* The username of the user represented by the given subject name.
*
* @throws GuacamoleException
* If any configuration parameters related to retrieving certificates
* from HTTP request cannot be parsed, or if the provided subject name
* cannot be parsed or is not acceptable (wrong base DN or wrong
* username attribute).
*/
public String getUsername(String name) throws GuacamoleException {
// Extract user's DN from their X.509 certificate
LdapName dn;
try {
dn = new LdapName(name);
}
catch (InvalidNameException e) {
throw new GuacamoleClientException("Subject \"" + name + "\" is "
+ "not a valid DN: " + e.getMessage(), e);
}
// Verify DN actually contains components
int numComponents = dn.size();
if (numComponents < 1)
throw new GuacamoleClientException("Subject DN is empty.");
// Verify DN is within configured base DN (if any)
LdapName baseDN = confService.getSubjectBaseDN();
if (baseDN != null && !(numComponents > baseDN.size() && dn.startsWith(baseDN)))
throw new GuacamoleClientException("Subject DN \"" + dn + "\" is "
+ "not within the configured base DN.");
// Retrieve the least significant attribute from the parsed DN - this
// will be the username
Rdn nameRdn = dn.getRdn(numComponents - 1);
// Verify that the username is specified with one of the allowed
// attributes
List<String> usernameAttributes = confService.getSubjectUsernameAttributes();
if (usernameAttributes != null && !usernameAttributes.stream().anyMatch(nameRdn.getType()::equalsIgnoreCase))
throw new GuacamoleClientException("Subject DN \"" + dn + "\" "
+ "does not contain an acceptable username attribute.");
// The DN is valid - extract the username from the least significant
// component
String username = nameRdn.getValue().toString();
logger.debug("Username \"{}\" extracted from subject DN \"{}\".", username, dn);
return username;
}
/** /**
* Authenticates a user using HTTP headers containing that user's verified * Authenticates a user using HTTP headers containing that user's verified
* X.509 certificate. It is assumed that this certificate is being passed * X.509 certificate. It is assumed that this certificate is being passed
@@ -154,15 +236,13 @@ public class SSLClientAuthenticationResource extends SSOResource {
* The raw bytes of the X.509 certificate retrieved from the request. * The raw bytes of the X.509 certificate retrieved from the request.
* *
* @return * @return
* A new SSOAuthenticatedUser representing the identity of the user * The username of the user asserted by the SSL termination service via
* asserted by the SSL termination service via that user's X.509 * that user's X.509 certificate.
* certificate.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If the provided X.509 certificate is not valid or cannot be parsed. * If any configuration parameters related to retrieving certificates
* It is expected that the SSL termination service will already have * from HTTP request cannot be parsed, or if the certificate is not
* validated the certificate; this function validates only the * valid/present.
* certificate timestamps.
*/ */
public String getUsername(byte[] certificate) throws GuacamoleException { public String getUsername(byte[] certificate) throws GuacamoleException {
@@ -179,36 +259,15 @@ public class SSLClientAuthenticationResource extends SSOResource {
} }
catch (CertificateException e) { catch (CertificateException e) {
throw new GuacamoleClientException("The X.509 certificate " throw new GuacamoleClientException("Certificate is not valid: " + e.getMessage(), e);
+ "presented is not valid.", e);
} }
catch (IOException e) { catch (IOException e) {
throw new GuacamoleServerException("Provided X.509 certificate " throw new GuacamoleClientException("Certificate could not be read: " + e.getMessage(), e);
+ "could not be read.", e);
} }
// Extract user's DN from their X.509 certificate // Extract user's DN from their X.509 certificate
LdapName dn; Principal principal = cert.getSubjectX500Principal();
try { return getUsername(principal.getName());
Principal principal = cert.getSubjectX500Principal();
dn = new LdapName(principal.getName());
}
catch (InvalidNameException e) {
throw new GuacamoleClientException("The X.509 certificate "
+ "presented does not contain a valid subject DN.", e);
}
// Verify DN actually contains components
int numComponents = dn.size();
if (numComponents < 1)
throw new GuacamoleClientException("The X.509 certificate "
+ "presented contains an empty subject DN.");
// Simply use first component of DN as username (TODO: Enforce
// requirements on the attribute providing the username and the base DN,
// and consider using components following the username to determine
// group memberships)
return dn.getRdn(numComponents - 1).getValue().toString();
} }
@@ -216,7 +275,7 @@ public class SSLClientAuthenticationResource extends SSOResource {
* Processes the X.509 certificate in the headers of the given HTTP * Processes the X.509 certificate in the headers of the given HTTP
* request, returning an authentication session token representing the * request, returning an authentication session token representing the
* identity in that certificate. If the certificate is invalid or not * identity in that certificate. If the certificate is invalid or not
* present, null is returned. * present, an invalid session token is returned.
* *
* @param credentials * @param credentials
* The credentials submitted in the HTTP request being processed. * The credentials submitted in the HTTP request being processed.
@@ -226,14 +285,10 @@ public class SSLClientAuthenticationResource extends SSOResource {
* *
* @return * @return
* An authentication session token representing the identity in the * An authentication session token representing the identity in the
* certificate in the given HTTP request, or null if the request does * certificate in the given HTTP request, or an invalid session token
* not contain a valid certificate. * if no valid identity was asserted.
*
* @throws GuacamoleException
* If any configuration parameters related to retrieving certificates
* from HTTP request cannot be parsed.
*/ */
private String processCertificate(HttpHeaders headers) throws GuacamoleException { private String processCertificate(HttpHeaders headers) {
// //
// NOTE: A result with an associated state is ALWAYS returned by // NOTE: A result with an associated state is ALWAYS returned by
@@ -245,18 +300,46 @@ public class SSLClientAuthenticationResource extends SSOResource {
// failures. // failures.
// //
// Verify that SSL termination has already verified the certificate try {
String verified = getHeader(headers, confService.getClientVerifiedHeader());
if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified))
return sessionManager.generateInvalid();
String certificate = getHeader(headers, confService.getClientCertificateHeader()); // Verify that SSL termination has already verified the certificate
if (certificate == null) String verified = getHeader(headers, confService.getClientVerifiedHeader());
return sessionManager.generateInvalid(); if (verified != null && verified.startsWith(CLIENT_VERIFIED_HEADER_FAILED_PREFIX)) {
String message = verified.substring(CLIENT_VERIFIED_HEADER_FAILED_PREFIX.length());
throw new GuacamoleClientException("Client certificate did "
+ "not pass validation. SSL termination reports the "
+ "following failure: \"" + message + "\"");
}
else if (CLIENT_VERIFIED_HEADER_NONE_VALUE.equals(verified)) {
throw new GuacamoleClientException("No client certificate was presented.");
}
else if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) {
throw new GuacamoleClientException("Client certificate did not pass validation.");
}
String username = getUsername(decode(certificate)); String certificate = getHeader(headers, confService.getClientCertificateHeader());
long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); if (certificate == null)
return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); throw new GuacamoleClientException("Client certificate missing from request.");
String username = getUsername(decode(certificate));
long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity());
return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration));
}
catch (GuacamoleClientException e) {
logger.warn("SSL/TLS client authentication attempt rejected: {}", e.getMessage());
logger.debug("SSL/TLS client authentication failed.", e);
}
catch (GuacamoleException e) {
logger.error("SSL/TLS client authentication attempt could not be processed: {}", e.getMessage());
logger.debug("SSL/TLS client authentication failed.", e);
}
catch (RuntimeException | Error e) {
logger.error("SSL/TLS client authentication attempt failed internally: {}", e.getMessage());
logger.debug("Internal failure processing SSL/TLS client authentication attempt.", e);
}
return sessionManager.generateInvalid();
} }

View File

@@ -22,12 +22,15 @@ package org.apache.guacamole.auth.ssl.conf;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.List;
import javax.naming.ldap.LdapName;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty;
import org.apache.guacamole.properties.StringListProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty; import org.apache.guacamole.properties.URIGuacamoleProperty;
/** /**
@@ -136,6 +139,34 @@ public class ConfigurationService {
}; };
/**
* The property defining the LDAP attribute or attributes that may be used
* to represent a username within the subject DN of a user's X.509
* certificate. If the least-significant attribute of the subject DN is not
* one of these attributes, the certificate will be rejected. By default,
* any attribute is accepted.
*/
private static final StringListProperty SSL_SUBJECT_USERNAME_ATTRIBUTE =
new StringListProperty () {
@Override
public String getName() { return "ssl-subject-username-attribute"; }
};
/**
* The property defining the base DN containing all valid subject DNs. If
* specified, only certificates asserting subject DNs beneath this base DN
* will be accepted. By default, all DNs are accepted.
*/
private static final LdapNameGuacamoleProperty SSL_SUBJECT_BASE_DN =
new LdapNameGuacamoleProperty () {
@Override
public String getName() { return "ssl-subject-base-dn"; }
};
/** /**
* The property representing the amount of time that the temporary, unique * The property representing the amount of time that the temporary, unique
* subdomain generated for SSL/TLS authentication may remain valid, in * subdomain generated for SSL/TLS authentication may remain valid, in
@@ -374,4 +405,36 @@ public class ConfigurationService {
return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY); return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY);
} }
/**
* Returns the base DN that contains all valid subject DNs. If there is no
* such base DN (and all subject DNs are valid), null is returned.
*
* @return
* The base DN that contains all valid subject DNs, or null if all
* subject DNs are valid.
*
* @throws GuacamoleException
* If the configured base DN cannot be read or is not a valid LDAP DN.
*/
public LdapName getSubjectBaseDN() throws GuacamoleException {
return environment.getProperty(SSL_SUBJECT_BASE_DN);
}
/**
* Returns a list of all attributes that may be used to represent a user's
* username within their subject DN. If all attributes may be accepted,
* null is returned.
*
* @return
* A list of all attributes that may be used to represent a user's
* username within their subject DN, or null if any attribute may be
* used.
*
* @throws GuacamoleException
* If the configured set of username attributes cannot be read.
*/
public List<String> getSubjectUsernameAttributes() throws GuacamoleException {
return environment.getProperty(SSL_SUBJECT_USERNAME_ATTRIBUTE);
}
} }

View File

@@ -0,0 +1,50 @@
/*
* 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.ssl.conf;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is an LDAP name, such as a distinguished
* name.
*/
public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName> {
@Override
public LdapName parseValue(String value) throws GuacamoleException {
if (value == null)
return null;
try {
return new LdapName(value);
}
catch (InvalidNameException e) {
throw new GuacamoleServerException("Value \"" + value
+ "\" is not a valid LDAP name.", e);
}
}
}