Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions nifi-docs/src/main/asciidoc/administration-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2758,10 +2758,16 @@ documentation of the proxy for guidance for your deployment environment and use

* Additional NiFi proxy configuration must be updated to allow expected Host and context paths HTTP headers.

** By default, if NiFi is running securely it will only accept HTTP requests with a Host header matching the host[:port] that it is bound to. If NiFi is to accept requests directed to a different
host[:port] the expected values need to be configured. This may be required when running behind a proxy or in a containerized environment. This is configured in a comma
separated list in _nifi.properties_ using the `nifi.web.proxy.host` property (e.g. `localhost:18443, proxyhost:443`). IPv6 addresses are accepted. Please refer to
RFC 5952 Sections link:https://tools.ietf.org/html/rfc5952#section-4[4] and link:https://tools.ietf.org/html/rfc5952#section-6[6] for additional details.
** When configured with HTTPS enabled, the name in the `Host` header must match one of the DNS Subject Alternative Names
included on the configured server X.509 certificate. The framework server also validates the name provided in the Server
Name Indication extension during the TLS handshake against the configured server certificate. The server returns an HTTP
400 Bad Request status when the requested `Host` header does not meet these requirements.

** The application supports providing alternative host addresses through either the `X-ProxyHost` or `X-Forwarded-Host`
headers. The host address provided in one of these headers must be configured in the `nifi.web.proxy.host` property.
The application uses the provided alternative host address to construct URLs that match the public address of the
reverse proxy. The server returns an HTTP 421 Misdirected Request status when the requested proxy header value is not
listed in the proxy host address property.

** NiFi will only accept HTTP requests with a X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header if the value is allowed in the `nifi.web.proxy.context.path` property in
_nifi.properties_. This property accepts a comma separated list of expected values. In the event an incoming request has an X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header value that is not
Expand Down Expand Up @@ -3520,10 +3526,16 @@ The value can be set to `h2` to require HTTP/2 and disable HTTP/1.1.
|`nifi.web.jetty.working.directory`|The location of the Jetty working directory. The default value is `./work/jetty`.
|`nifi.web.jetty.threads`|The number of Jetty threads. The default value is `200`.
|`nifi.web.max.header.size`|The maximum size allowed for request and response headers. The default value is `16 KB`.
|`nifi.web.proxy.host`|A comma separated list of allowed HTTP Host header values to consider when NiFi is running securely and will be receiving requests to a different host[:port] than it is bound to.
For example, when running in a Docker container or behind a proxy (e.g. localhost:18443, proxyhost:443). By default, this value is blank meaning NiFi should only allow requests sent to the
host[:port] that NiFi is bound to.
Requests containing an invalid port in the Host or authority header return an HTTP 421 Misdirected Request status.
|`nifi.web.proxy.host`|A comma-separated list of allowed header values to consider when the application is running with
HTTPS enabled and receives requests through a reverse proxy. Each value may include a domain name such as
`nifi.apache.org` or a domain name with a port number such as `nifi.apache.org:8443`.

Requests containing an invalid port in the `Host` or authority header return an HTTP 421 Misdirected Request status.
Requests containing an `X-ProxyHost` or `X-Forwarded-Host` header with a value not listed in this property return an HTTP
421 Misdirected Request status.

Reverse proxy servers are responsible for filtering input request headers and providing allowed proxy host values to the
application. Proxy host header values must be limited to including the domain name, without the port number.
|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header values to consider. By default, this value is
blank meaning all requests containing a proxy context path are rejected. Configuring this property would allow requests where the proxy path is contained in this listing.
|`nifi.web.max.content.size`|The maximum size (HTTP `Content-Length`) for PUT and POST requests. No default value is set for backward compatibility. Providing a value for this property enables the `Content-Length` filter on all incoming API requests (except Site-to-Site and cluster communications). A suggested value is `20 MB`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class FrameworkServerConnectorFactory extends StandardServerConnectorFact

private static final String CIPHER_SUITE_SEPARATOR_PATTERN = ",\\s*";

private static final Pattern HOST_PORT_SEPARATOR = Pattern.compile(":");

private static final Pattern HOST_PORT_PATTERN = Pattern.compile(".+?:(\\d+)$");

private static final int PORT_GROUP = 1;
Expand All @@ -54,6 +56,8 @@ public class FrameworkServerConnectorFactory extends StandardServerConnectorFact

private final String excludeCipherSuites;

private final Set<String> validProxyHosts;

private final Set<Integer> validPorts;

private SslContextFactory.Server sslContextFactory;
Expand All @@ -70,6 +74,7 @@ public FrameworkServerConnectorFactory(final Server server, final NiFiProperties
includeCipherSuites = properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE);
excludeCipherSuites = properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE);
headerSize = DataUnit.parseDataSize(properties.getWebMaxHeaderSize(), DataUnit.B).intValue();
validProxyHosts = getValidProxyHosts(properties);
validPorts = getValidPorts(properties);

if (properties.isHTTPSConfigured()) {
Expand Down Expand Up @@ -99,9 +104,12 @@ protected HttpConfiguration getHttpConfiguration() {
httpConfiguration.setResponseHeaderSize(headerSize);
httpConfiguration.setIdleTimeout(IDLE_TIMEOUT);

// Add HostHeaderCustomizer to set Host Header for HTTP/2 and HostHeaderHandler
// Add HostHeaderCustomizer to set Host Header for HTTP/2
httpConfiguration.addCustomizer(new HostHeaderCustomizer());

final ProxyHeaderValidatorCustomizer proxyHeaderValidatorCustomizer = new ProxyHeaderValidatorCustomizer(validProxyHosts);
httpConfiguration.addCustomizer(proxyHeaderValidatorCustomizer);

final HostPortValidatorCustomizer hostPortValidatorCustomizer = new HostPortValidatorCustomizer(validPorts);
httpConfiguration.addCustomizer(hostPortValidatorCustomizer);

Expand Down Expand Up @@ -159,6 +167,19 @@ private static int getPort(final NiFiProperties properties) {
return ObjectUtils.getIfNull(httpsPort, httpPort);
}

private static Set<String> getValidProxyHosts(final NiFiProperties properties) {
final Set<String> validProxyHosts = new HashSet<>();

final List<String> allowedHosts = properties.getAllowedHostsAsList();
for (final String allowedHost : allowedHosts) {
final String[] hostPort = HOST_PORT_SEPARATOR.split(allowedHost);
final String host = hostPort[0];
validProxyHosts.add(host);
}

return validProxyHosts;
}

private static Set<Integer> getValidPorts(final NiFiProperties properties) {
final Set<Integer> validPorts = new HashSet<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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.nifi.web.server.connector;

import org.apache.nifi.web.servlet.shared.ProxyHeader;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;

import java.util.Objects;
import java.util.Set;

/**
* Jetty Request Customizer implementing validation for supported Proxy Request Headers
*/
public class ProxyHeaderValidatorCustomizer implements HttpConfiguration.Customizer {
private static final String MISDIRECTED_REQUEST_REASON = "Invalid Proxy Host Requested";

private static final Set<String> SUPPORTED_PROXY_HOST_HEADERS = Set.of(
ProxyHeader.PROXY_HOST.getHeader(),
ProxyHeader.FORWARDED_HOST.getHeader()
);

private final Set<String> validProxyHosts;

public ProxyHeaderValidatorCustomizer(final Set<String> validProxyHosts) {
this.validProxyHosts = Objects.requireNonNull(validProxyHosts, "Valid Proxy Hosts required");
}

/**
* Validate requested proxy host header values against allowed proxy hosts and throw HTTP 421 on invalid values
*
* @param request HTTP Request to be evaluated
* @param responseHeaders HTTP Response headers
* @return Valid HTTP Request
*/
@Override
public Request customize(final Request request, final HttpFields.Mutable responseHeaders) {
final Request customized;

if (request.isSecure()) {
customized = customizeSecureRequest(request);
} else {
customized = request;
}

return customized;
}

private Request customizeSecureRequest(final Request request) {
final HttpURI requestUri = request.getHttpURI();
final String requestHost = requestUri.getHost();

final HttpFields requestHeaders = request.getHeaders();
for (final String proxyHostHeader : SUPPORTED_PROXY_HOST_HEADERS) {
final String hostHeader = requestHeaders.get(proxyHostHeader);
// Include empty and blank values for enforced validation of request headers
if (hostHeader == null) {
continue;
}
// Allow proxy host header matching request host header based on TLS SNI and DNS SAN requirements
if (requestHost.equals(hostHeader)) {
continue;
}
if (validProxyHosts.contains(hostHeader)) {
continue;
}

throw new HttpException.RuntimeException(HttpStatus.MISDIRECTED_REQUEST_421, MISDIRECTED_REQUEST_REASON);
}

return request;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.nifi.security.ssl.StandardSslContextBuilder;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.server.handler.HeaderWriterHandler;
import org.apache.nifi.web.servlet.shared.ProxyHeader;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
Expand Down Expand Up @@ -83,7 +84,11 @@ class StandardServerProviderTest {

private static final String PUBLIC_HOST = "nifi.apache.org";

private static final String PUBLIC_UNKNOWN_HOST = "nifi.staged.apache.org";
private static final String PUBLIC_STAGED_HOST = "nifi.staged.apache.org";

private static final String PROXY_HOST_PROPERTY = "nifi.apache.org,nifi.staged.apache.org:8443";

private static final String PUBLIC_UNKNOWN_HOST = "nifi.unknown.apache.org";

private static final String ALLOW_RESTRICTED_HEADERS_PROPERTY = "jdk.httpclient.allowRestrictedHeaders";

Expand Down Expand Up @@ -174,7 +179,7 @@ void testGetServerStart() throws Exception {
void testGetServerHttpsRequestsCompleted() throws Exception {
final Properties applicationProperties = new Properties();
applicationProperties.setProperty(NiFiProperties.WEB_HTTPS_PORT, RANDOM_PORT);
applicationProperties.setProperty(NiFiProperties.WEB_PROXY_HOST, PUBLIC_HOST);
applicationProperties.setProperty(NiFiProperties.WEB_PROXY_HOST, PROXY_HOST_PROPERTY);
final NiFiProperties properties = NiFiProperties.createBasicNiFiProperties((String) null, applicationProperties);

final StandardServerProvider provider = new StandardServerProvider(sslContext);
Expand Down Expand Up @@ -254,6 +259,24 @@ void assertRedirectRequestsCompleted(final HttpClient httpClient, final URI loca
.header(HOST_HEADER, PUBLIC_HOST)
.build();
assertResponseStatusCode(httpClient, alternativeNameRequest, HttpStatus.MOVED_TEMPORARILY_302);

final HttpRequest proxyHostRequest = HttpRequest.newBuilder(localhostUri)
.version(HttpClient.Version.HTTP_1_1)
.header(ProxyHeader.PROXY_HOST.getHeader(), localhostUri.getHost())
.build();
assertResponseStatusCode(httpClient, proxyHostRequest, HttpStatus.MOVED_TEMPORARILY_302);

final HttpRequest proxyHostPublicHostRequest = HttpRequest.newBuilder(localhostUri)
.version(HttpClient.Version.HTTP_1_1)
.header(ProxyHeader.PROXY_HOST.getHeader(), PUBLIC_HOST)
.build();
assertResponseStatusCode(httpClient, proxyHostPublicHostRequest, HttpStatus.MOVED_TEMPORARILY_302);

final HttpRequest forwardedHostRequest = HttpRequest.newBuilder(localhostUri)
.version(HttpClient.Version.HTTP_1_1)
.header(ProxyHeader.FORWARDED_HOST.getHeader(), PUBLIC_STAGED_HOST)
.build();
assertResponseStatusCode(httpClient, forwardedHostRequest, HttpStatus.MOVED_TEMPORARILY_302);
}

void assertBadRequestsCompleted(final HttpClient httpClient, final URI localhostUri) throws IOException, InterruptedException {
Expand All @@ -276,6 +299,18 @@ void assertMisdirectedRequestsCompleted(final HttpClient httpClient, final URI l
.header(HOST_HEADER, LOCALHOST_HTTP_PORT)
.build();
assertResponseStatusCode(httpClient, localhostPortRequest, HttpStatus.MISDIRECTED_REQUEST_421);

final HttpRequest publicUnknownProxyHostRequest = HttpRequest.newBuilder(localhostUri)
.version(HttpClient.Version.HTTP_1_1)
.header(ProxyHeader.PROXY_HOST.getHeader(), PUBLIC_UNKNOWN_HOST)
.build();
assertResponseStatusCode(httpClient, publicUnknownProxyHostRequest, HttpStatus.MISDIRECTED_REQUEST_421);

final HttpRequest publicUnknownForwardedHostRequest = HttpRequest.newBuilder(localhostUri)
.version(HttpClient.Version.HTTP_1_1)
.header(ProxyHeader.FORWARDED_HOST.getHeader(), PUBLIC_UNKNOWN_HOST)
.build();
assertResponseStatusCode(httpClient, publicUnknownForwardedHostRequest, HttpStatus.MISDIRECTED_REQUEST_421);
}

void assertStandardResponseHeadersFound(final HttpResponse<Void> response) {
Expand Down
Loading