How to configure Kerberos machine-to-machine authentication in Atoti Limits, covering client and server security configuration, KerberosRestTemplate, authentication providers, SpnegoEntryPoint, and the filter chain
As Kerberos is an authentication protocol rather than an implementation, this section
is more of a general guide than concrete instructions. However, we outline what parts of
Atoti Limits you need to configure to use Kerberos for machine-to-machine (MtM) authentication
with a Key Distribution Center (KDC).This guide only assumes that you have access to a keytab file containing a principal-user,
who is the service user that will be used to authenticate the MtM requests. This guide does not
assume anything else about your KDC.As the MtM communication is bidirectional, Atoti Limits and your connected Atoti Server
act as both a client and a server at different points in time. For this reason, we recommend placing
the security configurations in a shared module that can be used by both (or potentially more) applications.This guide consists of two sections. The first section shall outline from a high level the changes
that need to be made. The second section shall outline where these changes should go in Atoti Limits. We will provide code samples throughout.
In general, the configuration required to use Kerberos authentication in Spring is made up of two parts:
Client configuration: the configuration required to authenticate the machine sending the request.
Server configuration: the configuration required to authenticate the machine receiving the request.
First, get the latest version of the Spring Security Kerberos dependencies:
<dependency> <groupId>org.springframework.security.kerberos</groupId> <artifactId>spring-security-kerberos-core</artifactId> <version>${spring-security-kerberos.version}</version></dependency><!-- used for the server configuration --><dependency><groupId>org.springframework.security.kerberos</groupId><artifactId>spring-security-kerberos-web</artifactId><version>${spring-security-kerberos.version}</version></dependency> <!-- used for the client configuration --><dependency><groupId>org.springframework.security.kerberos</groupId><artifactId>spring-security-kerberos-client</artifactId><version>${spring-security-kerberos.version}</version></dependency>
When acting as a client, the machine sending the request needs to authenticate with the KDC. This can
be done using a KerberosRestTemplate, configured with the following:
keytabLocation: the location of the keytab file that contains the user’s credentials
servicePrincipal: the principal of the “service” user making the request
KerberosRestTemplate
@Beanpublic RestTemplate kerberosTemplate() { // the loginOptions can be useful if you want to customise Krb5LoginModule options Map<String, Object> loginOptions = new HashMap<>(); loginOptions.put("debug", "true"); loginOptions.put("storeKey", "true"); loginOptions.put("tryFirstPass", "true"); loginOptions.put("useFirstPass", "true"); // the template can also be instantiated with a username and password return new KerberosRestTemplate(keytabLocation, servicePrincipal, loginOptions); }
To use the KerberosRestTemplate, inject it into your service and use it to make requests.
KerberosServiceAuthenticationProvider: authenticates service requests.
KerberosAuthenticationProvider
@Beanpublic KerberosAuthenticationProvider kerberosAuthenticationProvider() { KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider(); SunJaasKerberosClient client = new SunJaasKerberosClient(); client.setDebug(true); provider.setKerberosClient(client); // the userDetailsService is used to fetch the user roles provider.setUserDetailsService(serviceUserDetailsService()); return provider;}
KerberosServiceAuthenticationProvider
@Bean public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() { KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider(); provider.setTicketValidator(sunJaasKerberosTicketValidator()); // the userDetailsService is used to fetch the user roles provider.setUserDetailsService(serviceUserDetailsService()); return provider; }
When an unauthorized actor, in our case Atoti Limits or the connected server, makes a request,
they are redirected to the authentication entry point. In many applications, this is in the form
of a login page. For Kerberos, the SpnegoEntryPoint is used to redirect the user to the KDC.
You can use this entry point to redirect to a login page. We won’t cover that here, because
we are focusing on machine-to-machine communication, but that is how to implement SSO using Kerberos.
SpnegoEntryPoint
@Beanpublic SpnegoEntryPoint spnegoEntryPoint() { return new SpnegoEntryPoint();}
The SpnegoAuthenticationProcessingFilter is responsible for processing the authentication request.
It authenticates the user once validated by the KDC and
sets the Spring authentication for the rest of the request.
This is where all of the above is put together. It is your responsibility to define your
filter chains. This is because the filter chain contains endpoints and user roles related to
your organization, including potentially other systems that are not part of Atoti Limits.The following considerations must be made for defining the Kerberos filter chain:
Redirect unauthorized requests to the SpnegoEntryPoint.
Add the SpnegoAuthenticationProcessingFilter to the filter chain to set the
Authorization Negotiate xxx header.
Make sure the request matchers are loose enough to allow any access to vital assets to happen, such as a login
page, but strict enough that private assets require
authentication, for example, sensitive endpoints.
A simple filter chain for Kerberos looks like this:
@Beanpublic SecurityFilterChain filterChain(final HttpSecurity http, SpnegoEntryPoint spnegoEntryPoint, SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter) throws Exception { return http .exceptionHandling(exception -> exception.authenticationEntryPoint(spnegoEntryPoint)) .authorizeHttpRequests(auth -> auth // we require that all endpoints are authentication .anyRequest().authenticated()) .addFilterBefore(spnegoAuthenticationProcessingFilter, BasicAuthenticationFilter.class) .build();}
The UserDetailsService, which usually fetches our user roles, is used only for the service user
here. Note that if you want to impose user roles for your service user, this is the place to do it.As an example only, to replicate the default admin user that is shipped with Atoti Limits
you can use the following:
UserDetailsService
public UserDetailsService serviceUserDetailsService() { return new UserDetailsService() { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return new User( username, "notUsed", true, true, true, true, AuthorityUtils.createAuthorityList( "ROLE_ADMIN", "ROLE_CS_ROOT", "ROLE_USERS", "ROLE_MANAGERS", "ROLE_USER", "ROLE_ACTIVITI_USER", "ROLE_ACTIVITI_ADMIN", "ROLE_LIMITS")); } };}
This serviceUserDetailsService is not exposed as a Spring Bean, to avoid conflicts
with other UserDetailsService beans that may be present in your application.
As mentioned above, we recommend placing all of the above in a shared module that can be used by both
Atoti Limits and your connected Atoti Server.
Import your security configuration created in the previous section into both Atoti Limits and your connected server.
It is up to you to determine if your security can be imported alongside your current security
configuration, or if it should replace any existing configurations. This is because security
configurations are architecture-specific, and so they differ from client to client.
2. ILimitsRestClientBuilder in your connected server
There is no spring-security-kerberos implementation at this time of a “Kerberos” RestClient like there is
for KerberosRestTemplate. You can instantiate
a RestClient with:
RestTemplate template = new KerberosRestTemplate(...); RestClient restClient = RestClient.create(template);
However, the Kerberos authentication fails, because the RestTemplate itself does not manage the
execution of the request.This means you will need to implement your own version of ILimitsConnector to
send the requests, which should be exposed in your connected server.
The API of ILimitsRestClientBuilder provides a RestClient rather than a RestTemplate as it
is the most modern Spring REST client.
We expect the spring-security-kerberos project to eventually implement a KerberosRestClient.
A sample of the changes required for your ILimitsConnector is provided below.
KerberosLimitsConnector
public class KerberosLimitsConnector extends LimitsConnector { ... protected final RestTemplate kerberosTemplate; ... protected boolean limitsServerIsStarted() { String limitsRestUrl = autoconfigurationProperties.getLimitsRestUrl(); if (limitsRestUrl != null) { try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(APPLICATION_JSON); ResponseEntity<String> response = kerberosTemplate.exchange( URI.create(autoconfigurationProperties.getLimitsPingUrl()).toString(), HttpMethod.GET, new HttpEntity<>(headers), String.class); return response.getStatusCode().value() == 200 && response.getBody().equals("pong"); } catch (ResourceAccessException ex) { log.warn("Could not ping Limits. Most likely the server is not started..."); log.debug(ex.getMessage(), ex); } } else { log.warn("Limits URL not provided but required! limits.rest-url = {}.", limitsRestUrl); } isConnected = false; return false; } ... protected boolean sendPropertiesToLimits() { String limitsRestUrl = autoconfigurationProperties.getLimitsRestUrl(); if (limitsRestUrl != null) { // Limits might not be up, so let's check log.debug("Connecting to Limits..."); try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(APPLICATION_JSON); HttpEntity<LimitsConnectionProperties> body = new HttpEntity<>(autoconfigurationProperties, headers); ResponseEntity<String> response = kerberosTemplate.exchange( URI.create( limitsRestUrl + LIMITS_AUTO_CONFIG_REST_SERVICE_ADDRESS + CONNECT_ENDPOINT) .toString(), HttpMethod.PUT, body, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody().equals("true")) { log.debug("...properties now sent to Limits!"); log.info("...connected to Limits"); return true; } else { log.warn( "Could not connect to Limits. HTTP Status : {}", response.getStatusCode().value()); } log.debug(response.getBody()); } catch (RestClientException e) { log.warn("Could not connect to Limits. Perhaps the server is not up.", e); } } else { log.warn( "Limits Rest URL and Auth have not been specified in properties. This Active Pivot application will not try to connect to Limits."); } return false; } ...}
As in the previous section, there is no implementation at this time of a “Kerberos” RestClient.Builder.
In this case, it means you have to provide an implementation of IWebClientServicein
Atoti Limits. We have provided KerberosWebClientService as an example.
KerberosWebClientService
@Service@RequiredArgsConstructorpublic class KerberosWebClientService implements IWebClientService { @Qualifier("kerberosTemplate") protected final RestTemplate kerberosTemplate; @Override public String post(String serverName, String path, String jsonBody, String errorMessage) { return post(serverName, path, jsonBody, errorMessage, false); } @Override public String post( String serverName, String path, String jsonBody, String errorMessage, boolean extractResponseBody) { try { // Note that the `serverName` is not used because the KDC is now responsible for authentication rather than the target server HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/json"); HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers); ResponseEntity<String> response = kerberosTemplate.postForEntity(path, entity, String.class); return response.getBody(); } catch (RestClientException e) { throw new LimitsWebClientServiceException(errorMessage, e); } } @Override public String get(String url, String server, String errorMessage) { return get(url, server, errorMessage, false); } @Override public String get(String url, String server, String errorMessage, boolean extractResponseBody) { try { // Note that the `server` is not used because the KDC is now responsible for authentication rather than the target server ResponseEntity<String> response = kerberosTemplate.getForEntity(url, String.class); return response.getBody(); } catch (RestClientException e) { throw new LimitsWebClientServiceException(errorMessage, e); } } @Override public String getWithAuth(String url, String authorization, String errorMessage) { return getWithAuth(url, authorization, errorMessage, false); } @Override public String getWithAuth( String url, String authorization, String errorMessage, boolean extractResponseBody) { try { HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/json"); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<String> response = kerberosTemplate.exchange(url, HttpMethod.GET, entity, String.class); return response.getBody(); } catch (RestClientException e) { throw new LimitsWebClientServiceException(errorMessage, e); } } @Override public <T> ResponseEntity<T> put( String serverName, String path, Object body, Class<T> responseType) { try { // Note that the `serverName` is not used because the KDC is now responsible for authentication rather than the target server HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/json"); HttpEntity<Object> entity = new HttpEntity<>(body, headers); return kerberosTemplate.exchange(path, HttpMethod.PUT, entity, responseType); } catch (RestClientException e) { throw new LimitsWebClientServiceException("Error during PUT request", e); } }}