Skip to content

Commit

Permalink
NIFI-13297 Removed Kerberos SPENGO Authentication
Browse files Browse the repository at this point in the history
This closes apache#8879

Signed-off-by: Joseph Witt <joewitt@apache.org>
  • Loading branch information
exceptionfactory authored and joewitt committed May 25, 2024
1 parent de11b6c commit 43cc2b4
Show file tree
Hide file tree
Showing 23 changed files with 14 additions and 506 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,6 @@ public class NiFiProperties extends ApplicationProperties {
public static final String KERBEROS_KRB5_FILE = "nifi.kerberos.krb5.file";
public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.kerberos.service.principal";
public static final String KERBEROS_SERVICE_KEYTAB_LOCATION = "nifi.kerberos.service.keytab.location";
public static final String KERBEROS_SPNEGO_PRINCIPAL = "nifi.kerberos.spnego.principal";
public static final String KERBEROS_SPNEGO_KEYTAB_LOCATION = "nifi.kerberos.spnego.keytab.location";
public static final String KERBEROS_AUTHENTICATION_EXPIRATION = "nifi.kerberos.spnego.authentication.expiration";

// state management
public static final String STATE_MANAGEMENT_CONFIG_FILE = "nifi.state.management.configuration.file";
Expand Down Expand Up @@ -982,43 +979,6 @@ public String getKerberosServiceKeytabLocation() {
}
}

public String getKerberosSpnegoPrincipal() {
final String spengoPrincipal = getProperty(KERBEROS_SPNEGO_PRINCIPAL);
if (!StringUtils.isBlank(spengoPrincipal)) {
return spengoPrincipal.trim();
} else {
return null;
}
}

public String getKerberosSpnegoKeytabLocation() {
final String keytabLocation = getProperty(KERBEROS_SPNEGO_KEYTAB_LOCATION);
if (!StringUtils.isBlank(keytabLocation)) {
return keytabLocation.trim();
} else {
return null;
}
}

public String getKerberosAuthenticationExpiration() {
final String authenticationExpirationString = getProperty(KERBEROS_AUTHENTICATION_EXPIRATION, DEFAULT_KERBEROS_AUTHENTICATION_EXPIRATION);
if (!StringUtils.isBlank(authenticationExpirationString)) {
return authenticationExpirationString.trim();
} else {
return null;
}
}

/**
* Returns true if the Kerberos service principal and keytab location
* properties are populated.
*
* @return true if Kerberos service support is enabled
*/
public boolean isKerberosSpnegoSupportEnabled() {
return !StringUtils.isBlank(getKerberosSpnegoPrincipal()) && !StringUtils.isBlank(getKerberosSpnegoKeytabLocation());
}

/**
* Returns true if the login identity provider has been configured.
*
Expand Down Expand Up @@ -1402,7 +1362,6 @@ public String getSamlHttpClientReadTimeout() {
*/
public boolean isClientAuthRequiredForRestApi() {
return !isLoginIdentityProviderEnabled()
&& !isKerberosSpnegoSupportEnabled()
&& !isOidcEnabled()
&& !isKnoxSsoEnabled()
&& !isSamlEnabled()
Expand Down
58 changes: 1 addition & 57 deletions nifi-docs/src/main/asciidoc/administration-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3176,57 +3176,6 @@ link:https://nginx.org/[Nginx] supports session affinity in the upstream module
link:https://nginx.org/en/docs/http/ngx_http_upstream_module.html#sticky[sticky] directive. The *sticky* directive
supports different strategies, including *cookie* and *route* options.

[[kerberos_service]]
== Kerberos Service
NiFi can be configured to use Kerberos SPNEGO (or "Kerberos Service") for authentication. In this scenario, users will hit the REST endpoint `/access/kerberos` and the server will respond with a `401` status code and the challenge response header `WWW-Authenticate: Negotiate`. This communicates to the browser to use the GSS-API and load the user's Kerberos ticket and provide it as a Base64-encoded header value in the subsequent request. It will be of the form `Authorization: Negotiate YII...`. NiFi will attempt to validate this ticket with the KDC. If it is successful, the user's _principal_ will be returned as the identity, and the flow will follow login/credential authentication, in that a JWT will be issued in the response to prevent the unnecessary overhead of Kerberos authentication on every subsequent request. If the ticket cannot be validated, it will return with the appropriate error response code. The user will then be able to provide their Kerberos credentials to the login form if the `KerberosLoginIdentityProvider` has been configured. See <<kerberos_login_identity_provider>> login identity provider for more details.

NiFi will only respond to Kerberos SPNEGO negotiation over an HTTPS connection, as unsecured requests are never authenticated.

The following properties must be set in _nifi.properties_ to enable Kerberos service authentication.

|====
|*Property*|*Required*|*Description*
|`Service Principal`|true|The service principal used by NiFi to communicate with the KDC
|`Keytab Location`|true|The file path to the keytab containing the service principal
|====

See <<kerberos_properties>> for complete documentation.

[[kerberos_service_notes]]
=== Notes

* Kerberos is case-sensitive in many places and the error messages (or lack thereof) may not be sufficiently explanatory. Check the case sensitivity of the service principal in your configuration files. Convention is `HTTP/fully.qualified.domain@REALM`.
* Browsers have varying levels of restriction when dealing with SPNEGO negotiations. Some will provide the local Kerberos ticket to any domain that requests it, while others explicitly specify the trusted domains in advance via an allow list. See link:http://docs.spring.io/autorepo/docs/spring-security-kerberos/1.0.2.BUILD-SNAPSHOT/reference/htmlsingle/#browserspnegoconfig[Spring Security Kerberos - Reference Documentation: Appendix E. Configure browsers for SPNEGO Negotiation^] for common browsers.
* Some browsers (legacy IE) do not support recent encryption algorithms such as AES, and are restricted to legacy algorithms (DES). This should be noted when generating keytabs.
* The KDC must be configured and a service principal defined for NiFi and a keytab exported. Comprehensive instructions for Kerberos server configuration and administration are beyond the scope of this document (see link:http://web.mit.edu/kerberos/krb5-current/doc/admin/index.html[MIT Kerberos Admin Guide^]), but an example is below:


Adding a service principal for a server at `nifi.nifi.apache.org` and exporting the keytab from the KDC:

....
root@kdc:/etc/krb5kdc# kadmin.local
Authenticating as principal admin/admin@NIFI.APACHE.ORG with password.
kadmin.local: listprincs
K/M@NIFI.APACHE.ORG
admin/admin@NIFI.APACHE.ORG
...
kadmin.local: addprinc -randkey HTTP/nifi.nifi.apache.org
WARNING: no policy specified for HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG; defaulting to no policy
Principal "HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG" created.
kadmin.local: ktadd -k /http-nifi.keytab HTTP/nifi.nifi.apache.org
Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/http-nifi.keytab.
Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des-cbc-crc added to keytab WRFILE:/http-nifi.keytab.
kadmin.local: listprincs
HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG
K/M@NIFI.APACHE.ORG
admin/admin@NIFI.APACHE.ORG
...
kadmin.local: q
root@kdc:~# ll /http*
-rw------- 1 root root 162 Mar 14 21:43 /http-nifi.keytab
root@kdc:~#
....

[[analytics_framework]]
== Analytics Framework
NiFi has an internal analytics framework which can be enabled to predict back pressure occurrence, given the configured settings for threshold on a queue. The model used by default for prediction is an ordinary least squares (OLS) linear regression. It uses recent observations from a queue (either number of objects or content size over time) and calculates a regression line for that data. The line's equation is then used to determine the next value that will be reached within a given time interval (e.g. number of objects in queue in the next 5 minutes). Below is an example graph of the linear regression model for Queue/Object Count over time which is used for predictions:
Expand Down Expand Up @@ -4142,18 +4091,13 @@ Changing this property *requires* setting `jute.maxbuffer` on ZooKeeper servers.
|====
|*Property*|*Description*
|`nifi.kerberos.krb5.file`*|The location of the krb5 file, if used. It is blank by default. At this time, only a single krb5 file is allowed to
be specified per NiFi instance, so this property is configured here to support SPNEGO and service principals rather than in individual Processors.
be specified per NiFi instance, so this property is configured here to support service principals rather than in individual Processors.
If necessary the krb5 file can support multiple realms.
Example: `/etc/krb5.conf`
|`nifi.kerberos.service.principal`*|The name of the NiFi Kerberos service principal, if used. It is blank by default. Note that this property is for NiFi to authenticate as a client other systems.
Example: `nifi/nifi.example.com` or `nifi/nifi.example.com@EXAMPLE.COM`
|`nifi.kerberos.service.keytab.location`*|The file path of the NiFi Kerberos keytab, if used. It is blank by default. Note that this property is for NiFi to authenticate as a client other systems.
Example: `/etc/nifi.keytab`
|`nifi.kerberos.spnego.principal`*|The name of the NiFi Kerberos service principal, if used. It is blank by default. Note that this property is used to authenticate NiFi users.
Example: `HTTP/nifi.example.com` or `HTTP/nifi.example.com@EXAMPLE.COM`
|`nifi.kerberos.spnego.keytab.location`*|The file path of the NiFi Kerberos keytab, if used. It is blank by default. Note that this property is used to authenticate NiFi users.
Example: `/etc/http-nifi.keytab`
|`nifi.kerberos.spengo.authentication.expiration`*|The expiration duration of a successful Kerberos user authentication, if used. The default value is `12 hours`.
|====

[[analytics_properties]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,3 @@ nifi.kerberos.krb5.file=
# kerberos service principal #
nifi.kerberos.service.principal=
nifi.kerberos.service.keytab.location=

# kerberos spnego principal #
nifi.kerberos.spnego.principal=
nifi.kerberos.spnego.keytab.location=
nifi.kerberos.spnego.authentication.expiration=12 hours
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,3 @@ nifi.kerberos.krb5.file=
# kerberos service principal #
nifi.kerberos.service.principal=
nifi.kerberos.service.keytab.location=

# kerberos spnego principal #
nifi.kerberos.spnego.principal=
nifi.kerberos.spnego.keytab.location=
nifi.kerberos.spnego.authentication.expiration=12 hours
3 changes: 0 additions & 3 deletions nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,6 @@
<nifi.kerberos.krb5.file> </nifi.kerberos.krb5.file>
<nifi.kerberos.service.principal />
<nifi.kerberos.service.keytab.location />
<nifi.kerberos.spnego.principal />
<nifi.kerberos.spnego.keytab.location />
<nifi.kerberos.spnego.authentication.expiration>12 hours</nifi.kerberos.spnego.authentication.expiration>

<!-- nifi.properties: analytics properties -->
<nifi.analytics.predict.enabled>false</nifi.analytics.predict.enabled>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,6 @@ nifi.kerberos.krb5.file=${nifi.kerberos.krb5.file}
nifi.kerberos.service.principal=${nifi.kerberos.service.principal}
nifi.kerberos.service.keytab.location=${nifi.kerberos.service.keytab.location}

# kerberos spnego principal #
nifi.kerberos.spnego.principal=${nifi.kerberos.spnego.principal}
nifi.kerberos.spnego.keytab.location=${nifi.kerberos.spnego.keytab.location}
nifi.kerberos.spnego.authentication.expiration=${nifi.kerberos.spnego.authentication.expiration}

# analytics properties #
nifi.analytics.predict.enabled=${nifi.analytics.predict.enabled}
nifi.analytics.predict.interval=${nifi.analytics.predict.interval}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,6 @@
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.kerberos</groupId>
<artifactId>spring-security-kerberos-core</artifactId>
<version>1.0.1.RELEASE</version>
<scope>provided</scope> <!-- expected to be provided by parent classloader -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
Expand Down Expand Up @@ -57,7 +56,6 @@
import org.apache.nifi.authorization.user.NiFiUserDetails;
import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.apache.nifi.authorization.util.IdentityMappingUtil;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
import org.apache.nifi.web.api.dto.AccessStatusDTO;
import org.apache.nifi.web.api.dto.AccessTokenExpirationDTO;
Expand All @@ -71,7 +69,6 @@
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
import org.apache.nifi.web.security.jwt.revocation.JwtLogoutListener;
import org.apache.nifi.web.security.kerberos.KerberosService;
import org.apache.nifi.web.security.knox.KnoxService;
import org.apache.nifi.web.security.logout.LogoutRequest;
import org.apache.nifi.web.security.logout.LogoutRequestManager;
Expand Down Expand Up @@ -112,7 +109,6 @@ public class AccessResource extends ApplicationResource {
private BearerTokenProvider bearerTokenProvider;
private BearerTokenResolver bearerTokenResolver;
private KnoxService knoxService;
private KerberosService kerberosService;
private LogoutRequestManager logoutRequestManager;

/**
Expand Down Expand Up @@ -296,80 +292,6 @@ public Response getAccessStatus(@Context HttpServletRequest httpServletRequest,
return generateOkResponse(entity).build();
}

/**
* Creates a token for accessing the REST API via Kerberos ticket exchange / SPNEGO negotiation.
*
* @param httpServletRequest the servlet request
* @return A JWT (string)
*/
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
@Path("/kerberos")
@Operation(
summary = "Creates a token for accessing the REST API via Kerberos ticket exchange / SPNEGO negotiation",
description = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " +
"the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " +
"in the format 'Authorization: Bearer <token>'. It is also stored in the browser as a cookie.",
responses = @ApiResponse(content = @Content(schema = @Schema(implementation = String.class)))
)
@ApiResponses(
value = {
@ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@ApiResponse(
responseCode = "401", description = "NiFi was unable to complete the request because it did not contain a valid Kerberos " +
"ticket in the Authorization header. Retry this request after initializing a ticket with kinit and " +
"ensuring your browser is configured to support SPNEGO."
),
@ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it."),
@ApiResponse(responseCode = "500", description = "Unable to create access token because an unexpected error occurred.")
}
)
public Response createAccessTokenFromTicket(@Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse) {

// only support access tokens when communicating over HTTPS
if (!httpServletRequest.isSecure()) {
throw new AuthenticationNotSupportedException("Access tokens are only issued over HTTPS.");
}

// If Kerberos Service Principal and keytab location not configured, throws exception
if (!properties.isKerberosSpnegoSupportEnabled() || kerberosService == null) {
final String message = "Kerberos ticket login not supported by this NiFi.";
logger.debug(message);
return Response.status(Response.Status.CONFLICT).entity(message).build();
}

String authorizationHeaderValue = httpServletRequest.getHeader(KerberosService.AUTHORIZATION_HEADER_NAME);

if (!kerberosService.isValidKerberosHeader(authorizationHeaderValue)) {
return generateNotAuthorizedResponse().header(KerberosService.AUTHENTICATION_CHALLENGE_HEADER_NAME, KerberosService.AUTHORIZATION_NEGOTIATE).build();
} else {
try {
// attempt to authenticate
Authentication authentication = kerberosService.validateKerberosTicket(httpServletRequest);

if (authentication == null) {
throw new IllegalArgumentException("Request is not HTTPS or Kerberos ticket missing or malformed");
}

final String expirationFromProperties = properties.getKerberosAuthenticationExpiration();
final long expirationDuration = Math.round(FormatUtils.getPreciseTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS));
final Instant expiration = Instant.now().plusMillis(expirationDuration);

final String rawIdentity = authentication.getName();
final String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties));

final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, Collections.emptySet());
final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken);
final URI uri = URI.create(generateResourceUri("access", "kerberos"));
setBearerToken(httpServletResponse, token);
return generateCreatedResponse(uri, token).build();
} catch (final AuthenticationException e) {
throw new AccessDeniedException(e.getMessage(), e);
}
}
}

/**
* Creates a token for accessing the REST API via username/password stored as a cookie in the browser.
*
Expand Down Expand Up @@ -608,10 +530,6 @@ public void setJwtLogoutListener(final JwtLogoutListener jwtLogoutListener) {
this.jwtLogoutListener = jwtLogoutListener;
}

public void setKerberosService(KerberosService kerberosService) {
this.kerberosService = kerberosService;
}

public void setX509AuthenticationProvider(X509AuthenticationProvider x509AuthenticationProvider) {
this.x509AuthenticationProvider = x509AuthenticationProvider;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,6 @@
<property name="jwtLogoutListener" ref="jwtLogoutListener"/>
<property name="bearerTokenProvider" ref="bearerTokenProvider"/>
<property name="bearerTokenResolver" ref="bearerTokenResolver"/>
<property name="kerberosService" ref="kerberosService"/>
<property name="properties" ref="nifiProperties"/>
<property name="clusterCoordinator" ref="clusterCoordinator"/>
<property name="requestReplicator" ref="requestReplicator" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,3 @@ nifi.zookeeper.root.node=${nifi.zookeeper.root.node}
nifi.kerberos.krb5.file=${nifi.kerberos.krb5.file}
nifi.kerberos.service.principal=${nifi.kerberos.service.principal}
nifi.kerberos.service.keytab.location=${nifi.kerberos.service.keytab.location}
nifi.kerberos.spnego.principal=${nifi.kerberos.spnego.principal}
nifi.kerberos.spnego.keytab.location=${nifi.kerberos.spnego.keytab.location}
nifi.kerberos.spnego.authentication.expiration=${nifi.kerberos.spnego.authentication.expiration}
Loading

0 comments on commit 43cc2b4

Please sign in to comment.