NIFIREG-415 Add Support for Unicode in X-ProxiedEntitiesChain

- Adds detection and encoding of non-ascii characters to creation of chain
- Adds unit tests and integration tests that use proxied entities with Unicode
- Refactors ProxiedEntityRequestConfig to compute header value in constructor
- Addressed peer review feedback

This closes #302.
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
index 3a89898..08da6b4 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
@@ -16,47 +16,32 @@
  */
 package org.apache.nifi.registry.client.impl.request;
 
-import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Validate;
 import org.apache.nifi.registry.client.RequestConfig;
 import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
 
-import java.util.Arrays;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 /**
  * Implementation of RequestConfig that produces headers for a request with proxied-entities.
  */
 public class ProxiedEntityRequestConfig implements RequestConfig {
 
-    private final String[] proxiedEntities;
+    private final String proxiedEntitiesChain;
 
     public ProxiedEntityRequestConfig(final String... proxiedEntities) {
-        this.proxiedEntities = Validate.notNull(proxiedEntities);
+        Validate.notNull(proxiedEntities);
+        this.proxiedEntitiesChain = ProxiedEntitiesUtils.getProxiedEntitiesChain(proxiedEntities);
     }
 
     @Override
     public Map<String, String> getHeaders() {
-        final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);
-
         final Map<String,String> headers = new HashMap<>();
-        if (proxiedEntitiesValue != null) {
-            headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue);
+        if (proxiedEntitiesChain != null) {
+            headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesChain);
         }
         return headers;
     }
 
-    private String getProxiedEntitesValue(final String[] proxiedEntities) {
-        if (proxiedEntities == null) {
-            return null;
-        }
-
-        final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities)
-                .map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
-        return StringUtils.join(proxiedEntityChain, "");
-    }
-
 }
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
index f850341..45a11f4 100644
--- a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
@@ -20,8 +20,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -38,6 +40,63 @@
     private static final String ESCAPED_LT = "\\\\<";
 
     private static final String ANONYMOUS_CHAIN = "<>";
+    private static final String ANONYMOUS_IDENTITY = "";
+
+    /**
+     * Formats a list of DN/usernames to be set as a HTTP header using well known conventions.
+     *
+     * @param proxiedEntities the raw identities (usernames and DNs) to be formatted as a chain
+     * @return the value to use in the X-ProxiedEntitiesChain header
+     */
+    public static String getProxiedEntitiesChain(final String... proxiedEntities) {
+        return getProxiedEntitiesChain(Arrays.asList(proxiedEntities));
+    }
+
+    /**
+     * Formats a list of DN/usernames to be set as a HTTP header using well known conventions.
+     *
+     * @param proxiedEntities the raw identities (usernames and DNs) to be formatted as a chain
+     * @return the value to use in the X-ProxiedEntitiesChain header
+     */
+    public static String getProxiedEntitiesChain(final List<String> proxiedEntities) {
+        if (proxiedEntities == null) {
+            return null;
+        }
+
+        final List<String> proxiedEntityChain = proxiedEntities.stream()
+                .map(ProxiedEntitiesUtils::formatProxyDn)
+                .collect(Collectors.toList());
+        return StringUtils.join(proxiedEntityChain, "");
+    }
+
+    /**
+     * Tokenizes the specified proxy chain.
+     *
+     * @param rawProxyChain raw chain
+     * @return tokenized proxy chain
+     */
+    public static List<String> tokenizeProxiedEntitiesChain(final String rawProxyChain) {
+        final List<String> proxyChain = new ArrayList<>();
+        if (!StringUtils.isEmpty(rawProxyChain)) {
+
+            if (!isValidChainFormat(rawProxyChain)) {
+                throw new IllegalArgumentException("Proxy chain format is not recognized and can not safely be converted to a list.");
+            }
+
+            if (rawProxyChain.equals(ANONYMOUS_CHAIN)) {
+                proxyChain.add(ANONYMOUS_IDENTITY);
+            } else {
+                // Split the String on the `><` token, use substring to remove leading `<` and trailing `>`
+                final String[] elements = StringUtils.splitByWholeSeparatorPreserveAllTokens(
+                        rawProxyChain.substring(1, rawProxyChain.length() - 1), "><");
+                // Unsanitize each DN and add it to the proxy chain list
+                Arrays.stream(elements)
+                        .map(ProxiedEntitiesUtils::unsanitizeDn)
+                        .forEach(proxyChain::add);
+            }
+        }
+        return proxyChain;
+    }
 
     /**
      * Formats the specified DN to be set as a HTTP header using well known conventions.
@@ -45,29 +104,59 @@
      * @param dn raw dn
      * @return the dn formatted as an HTTP header
      */
-    public static String formatProxyDn(String dn) {
+    public static String formatProxyDn(final String dn) {
         return LT + sanitizeDn(dn) + GT;
     }
 
     /**
-     * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user.
+     * Sanitizes a DN for safe and lossless transmission.
+     *
+     * Sanitization requires:
+     * <ol>
+     *   <li>Encoded so that it can be sent losslessly using US-ASCII (the character set of HTTP Header values)</li>
+     *   <li>Resilient to a DN with the sequence '><' to attempt to escape the tokenization process and impersonate another user.</li>
+     * </ol>
+     *
      * <p>
      * Example:
      * <p>
      * Provided DN: {@code jdoe><alopresto} -> {@code <jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe
+     * <p>Алйс
+     * Provided DN: {@code Алйс} -> {@code <Алйс>} cannot be encoded/decoded as ASCII
      *
      * @param rawDn the unsanitized DN
      * @return the sanitized DN
      */
-    private static String sanitizeDn(String rawDn) {
+    private static String sanitizeDn(final String rawDn) {
         if (StringUtils.isEmpty(rawDn)) {
             return rawDn;
         } else {
-            String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
-            if (!sanitizedDn.equals(rawDn)) {
-                logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]");
+
+            // First, escape any GT [>] or LT [<] characters, which are not safe
+            final String escapedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
+            if (!escapedDn.equals(rawDn)) {
+                logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + escapedDn + "]");
             }
-            return sanitizedDn;
+
+            // Second, check for characters outside US-ASCII.
+            // This is necessary because X509 Certs can contain international/Unicode characters,
+            // but this value will be passed in an HTTP Header which must be US-ASCII.
+            // If non-ascii characters are present, base64 encode the DN and wrap in <angled-brackets>,
+            // to indicate to the receiving end that the value must be decoded.
+            // Note: We could have decided to always base64 encode these values,
+            //       not only to avoid the isPureAscii(...) check, but also as a
+            //       method of sanitizing  GT [>] or LT [<] chars. However, there
+            //       are advantages to encoding only when necessary, namely:
+            //         1. Backwards compatibility
+            //         2. Debugging this X-ProxiedEntitiesChain headers is easier unencoded.
+            //       This algorithm can be revisited as part of the next major version change.
+            if (isPureAscii(escapedDn)) {
+                return escapedDn;
+            } else {
+                final String encodedDn = base64Encode(escapedDn);
+                logger.debug("The provided DN [" + rawDn + "] contained non-ASCII characters and was encoded as [" + encodedDn + "]");
+                return encodedDn;
+            }
         }
     }
 
@@ -77,16 +166,25 @@
      * Example:
      * <p>
      * {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
+     * <p>
+     * {@code %D0%90%D0%BB%D0%B9%D1%81} -> {@code Алйс}
      *
      * @param sanitizedDn the sanitized DN
      * @return the original DN
      */
-    private static String unsanitizeDn(String sanitizedDn) {
+    private static String unsanitizeDn(final String sanitizedDn) {
         if (StringUtils.isEmpty(sanitizedDn)) {
             return sanitizedDn;
         } else {
-            String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
-            if (!unsanitizedDn.equals(sanitizedDn)) {
+            final String decodedDn;
+            if (isBase64Encoded(sanitizedDn)) {
+                decodedDn = base64Decode(sanitizedDn);
+                logger.debug("The provided DN [" + sanitizedDn + "] had been encoded, and was reconstituted to the original DN [" + decodedDn + "]");
+            } else {
+                decodedDn = sanitizedDn;
+            }
+            final String unsanitizedDn = decodedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
+            if (!unsanitizedDn.equals(decodedDn)) {
                 logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
             }
             return unsanitizedDn;
@@ -94,34 +192,60 @@
     }
 
     /**
-     * Tokenizes the specified proxy chain.
+     * Base64 encodes a DN and wraps it in angled brackets to indicate the value is base64 and not a raw DN.
      *
-     * @param rawProxyChain raw chain
-     * @return tokenized proxy chain
+     * @param rawValue The value to encode
+     * @return A string containing a wrapped, encoded value.
      */
-    public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) {
-        final List<String> proxyChain = new ArrayList<>();
-        if (!StringUtils.isEmpty(rawProxyChain)) {
-            // Split the String on the >< token
-            List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><"));
+    private static String base64Encode(final String rawValue) {
+        final String base64String = Base64.getEncoder().encodeToString(rawValue.getBytes(StandardCharsets.UTF_8));
+        final String wrappedEncodedValue = LT + base64String + GT;
+        return wrappedEncodedValue;
+    }
 
-            // Unsanitize each DN and collect back
-            elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());
+    /**
+     * Performs the reverse of ${@link #base64Encode(String)}.
+     *
+     * @param encodedValue the encoded value to decode.
+     * @return The original, decoded string.
+     */
+    private static String base64Decode(final String encodedValue) {
+        final String base64String = encodedValue.substring(1, encodedValue.length() - 1);
+        return new String(Base64.getDecoder().decode(base64String), StandardCharsets.UTF_8);
+    }
 
-            // Remove the leading < from the first element
-            elements.set(0, elements.get(0).replaceFirst(LT, ""));
+    /**
+     * Check if a String is in the expected format and can be safely tokenized.
+     *
+     * @param rawProxiedEntitiesChain the value to check
+     * @return true if the value is in the valid format to tokenize, false otherwise.
+     */
+    private static boolean isValidChainFormat(final String rawProxiedEntitiesChain) {
+        return isWrappedInAngleBrackets(rawProxiedEntitiesChain);
+    }
 
-            // Remove the trailing > from the last element
-            int last = elements.size() - 1;
-            String lastElement = elements.get(last);
-            if (lastElement.endsWith(GT)) {
-                elements.set(last, lastElement.substring(0, lastElement.length() - 1));
-            }
+    /**
+     * Check if a value has been encoded by ${@link #base64Encode(String)}, and therefore needs to be decoded.
+     *
+     * @param token the value to check
+     * @return true if the value is encoded, false otherwise.
+     */
+    private static boolean isBase64Encoded(final String token) {
+        return isWrappedInAngleBrackets(token);
+    }
 
-            proxyChain.addAll(elements);
-        }
+    /**
+     * Check if a string is wrapped with  &lt;angle brackets&gt;.
+     *
+     * @param string the value to check
+     * @return true if the value starts with &lt; and ends with &gt; - false otherwise
+     */
+    private static boolean isWrappedInAngleBrackets(final String string) {
+        return string.startsWith(LT) && string.endsWith(GT);
+    }
 
-        return proxyChain;
+    private static boolean isPureAscii(final String stringWithUnknownCharacters) {
+        return StandardCharsets.US_ASCII.newEncoder().canEncode(stringWithUnknownCharacters);
     }
 
 }
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy b/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy
new file mode 100644
index 0000000..16b66e0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy
@@ -0,0 +1,334 @@
+/*
+ * 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.registry.security.util
+
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.nio.charset.StandardCharsets
+
+@RunWith(JUnit4.class)
+class ProxiedEntitiesUtilsTest {
+    private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class)
+
+    private static final String SAFE_USER_NAME_ANDY = "alopresto"
+    private static final String SAFE_USER_DN_ANDY = "CN=${SAFE_USER_NAME_ANDY}, OU=Apache NiFi"
+
+    private static final String SAFE_USER_NAME_JOHN = "jdoe"
+    private static final String SAFE_USER_DN_JOHN = "CN=${SAFE_USER_NAME_JOHN}, OU=Apache NiFi"
+
+    private static final String SAFE_USER_NAME_PROXY_1 = "proxy1.nifi.apache.org"
+    private static final String SAFE_USER_DN_PROXY_1 = "CN=${SAFE_USER_NAME_PROXY_1}, OU=Apache NiFi"
+
+    private static final String SAFE_USER_NAME_PROXY_2 = "proxy2.nifi.apache.org"
+    private static final String SAFE_USER_DN_PROXY_2 = "CN=${SAFE_USER_NAME_PROXY_2}, OU=Apache NiFi"
+
+    private static
+    final String MALICIOUS_USER_NAME_JOHN = "${SAFE_USER_NAME_JOHN}, OU=Apache NiFi><CN=${SAFE_USER_NAME_PROXY_1}"
+    private static final String MALICIOUS_USER_DN_JOHN = "CN=${MALICIOUS_USER_NAME_JOHN}, OU=Apache NiFi"
+
+    private static final String UNICODE_DN_1 = "CN=Алйс, OU=Apache NiFi"
+    private static final String UNICODE_DN_1_ENCODED = "<" + base64Encode(UNICODE_DN_1) + ">"
+
+    private static final String UNICODE_DN_2 = "CN=Боб, OU=Apache NiFi"
+    private static final String UNICODE_DN_2_ENCODED = "<" + base64Encode(UNICODE_DN_2) + ">"
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() {
+    }
+
+    @After
+    void tearDown() {
+    }
+
+    private static String sanitizeDn(String dn = "") {
+        dn.replaceAll(/>/, '\\\\>').replaceAll('<', '\\\\<')
+    }
+
+    private static String base64Encode(String dn = "") {
+        return Base64.getEncoder().encodeToString(dn.getBytes(StandardCharsets.UTF_8))
+    }
+
+    private static String printUnicodeString(final String raw) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < raw.size(); i++) {
+            int codePoint = Character.codePointAt(raw, i)
+            int charCount = Character.charCount(codePoint)
+            if (charCount > 1) {
+                i += charCount - 1 // 2.
+                if (i >= raw.length()) {
+                    throw new IllegalArgumentException("Code point indicated more characters than available")
+                }
+            }
+            sb.append(String.format("\\u%04x ", codePoint))
+        }
+        return sb.toString().trim()
+    }
+
+    @Test
+    void testSanitizeDnShouldHandleFuzzing() throws Exception {
+        // Arrange
+        final String DESIRED_NAME = SAFE_USER_NAME_JOHN
+        logger.info("  Desired name: ${DESIRED_NAME} |  ${printUnicodeString(DESIRED_NAME)}")
+
+        // Contains various attempted >< escapes, trailing NULL, and BACKSPACE + 'n'
+        final List MALICIOUS_NAMES = [MALICIOUS_USER_NAME_JOHN,
+                                      SAFE_USER_NAME_JOHN + ">",
+                                      SAFE_USER_NAME_JOHN + "><>",
+                                      SAFE_USER_NAME_JOHN + "\\>",
+                                      SAFE_USER_NAME_JOHN + "\u003e",
+                                      SAFE_USER_NAME_JOHN + "\u005c\u005c\u003e",
+                                      SAFE_USER_NAME_JOHN + "\u0000",
+                                      SAFE_USER_NAME_JOHN + "\u0008n"]
+
+        // Act
+        MALICIOUS_NAMES.each { String name ->
+            logger.info("      Raw name: ${name} | ${printUnicodeString(name)}")
+            String sanitizedName = ProxiedEntitiesUtils.sanitizeDn(name)
+            logger.info("Sanitized name: ${sanitizedName} | ${printUnicodeString(sanitizedName)}")
+
+            // Assert
+            assert sanitizedName != DESIRED_NAME
+        }
+    }
+
+    @Test
+    void testShouldFormatProxyDn() throws Exception {
+        // Arrange
+        final String DN = SAFE_USER_DN_JOHN
+        logger.info(" Provided proxy DN: ${DN}")
+
+        final String EXPECTED_PROXY_DN = "<${DN}>"
+        logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}")
+
+        // Act
+        String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN)
+        logger.info("Forjohned proxy DN: ${forjohnedProxyDn}")
+
+        // Assert
+        assert forjohnedProxyDn == EXPECTED_PROXY_DN
+    }
+
+    @Test
+    void testFormatProxyDnShouldHandleMaliciousInput() throws Exception {
+        // Arrange
+        final String DN = MALICIOUS_USER_DN_JOHN
+        logger.info(" Provided proxy DN: ${DN}")
+
+        final String SANITIZED_DN = sanitizeDn(DN)
+        final String EXPECTED_PROXY_DN = "<${SANITIZED_DN}>"
+        logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}")
+
+        // Act
+        String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN)
+        logger.info("Forjohned proxy DN: ${forjohnedProxyDn}")
+
+        // Assert
+        assert forjohnedProxyDn == EXPECTED_PROXY_DN
+    }
+
+    @Test
+    void testFormatProxyDnShouldEncodeNonAsciiCharacters() throws Exception {
+        // Arrange
+        logger.info(" Provided DN: ${UNICODE_DN_1}")
+        final String expectedFormattedDn = "<${UNICODE_DN_1_ENCODED}>"
+        logger.info(" Expected DN: expected")
+
+        // Act
+        String formattedDn = ProxiedEntitiesUtils.formatProxyDn(UNICODE_DN_1)
+        logger.info("Formatted DN: ${formattedDn}")
+
+        // Assert
+        assert formattedDn == expectedFormattedDn
+    }
+
+    @Test
+    void testGetProxiedEntitiesChain() throws Exception {
+        // Arrange
+        String[] input = [SAFE_USER_NAME_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2]
+        final String expectedOutput = "<${SAFE_USER_NAME_JOHN}><${SAFE_USER_DN_PROXY_1}><${SAFE_USER_DN_PROXY_2}>"
+
+        // Act
+        def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input)
+
+        // Assert
+        assert output == expectedOutput
+    }
+
+    @Test
+    void testGetProxiedEntitiesChainShouldHandleMaliciousInput() throws Exception {
+        // Arrange
+        String[] input = [MALICIOUS_USER_DN_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2]
+        final String expectedOutput = "<${sanitizeDn(MALICIOUS_USER_DN_JOHN)}><${SAFE_USER_DN_PROXY_1}><${SAFE_USER_DN_PROXY_2}>"
+
+        // Act
+        def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input)
+
+        // Assert
+        assert output == expectedOutput
+    }
+
+    @Test
+    void testGetProxiedEntitiesChainShouldEncodeUnicode() throws Exception {
+        // Arrange
+        String[] input = [SAFE_USER_NAME_JOHN, UNICODE_DN_1, UNICODE_DN_2]
+        final String expectedOutput = "<${SAFE_USER_NAME_JOHN}><${UNICODE_DN_1_ENCODED}><${UNICODE_DN_2_ENCODED}>"
+
+        // Act
+        def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input)
+
+        // Assert
+        assert output == expectedOutput
+    }
+
+    @Test
+    void testShouldTokenizeProxiedEntitiesChainWithUserNames() throws Exception {
+        // Arrange
+        final List NAMES = [SAFE_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2]
+        final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>"
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+        // Assert
+        assert tokenizedNames == NAMES
+    }
+
+    @Test
+    void testShouldTokenizeAnonymous() throws Exception {
+        // Arrange
+        final List NAMES = [""]
+        final String RAW_PROXY_CHAIN = "<>"
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+        // Assert
+        assert tokenizedNames == NAMES
+    }
+
+    @Test
+    void testShouldTokenizeDoubleAnonymous() throws Exception {
+        // Arrange
+        final List NAMES = ["", ""]
+        final String RAW_PROXY_CHAIN = "<><>"
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+        // Assert
+        assert tokenizedNames == NAMES
+    }
+
+    @Test
+    void testShouldTokenizeNestedAnonymous() throws Exception {
+        // Arrange
+        final List NAMES = [SAFE_USER_DN_PROXY_1, "", SAFE_USER_DN_PROXY_2]
+        final String RAW_PROXY_CHAIN = "<${SAFE_USER_DN_PROXY_1}><><${SAFE_USER_DN_PROXY_2}>"
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+        // Assert
+        assert tokenizedNames == NAMES
+    }
+
+    @Test
+    void testShouldTokenizeProxiedEntitiesChainWithDNs() throws Exception {
+        // Arrange
+        final List DNS = [SAFE_USER_DN_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2]
+        final String RAW_PROXY_CHAIN = "<${DNS.join("><")}>"
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedDns = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedDns.collect { "\"${it}\"" }}")
+
+        // Assert
+        assert tokenizedDns == DNS
+    }
+
+    @Test
+    void testShouldTokenizeProxiedEntitiesChainWithAnonymousUser() throws Exception {
+        // Arrange
+        final List NAMES = ["", SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2]
+        final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>"
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+        // Assert
+        assert tokenizedNames == NAMES
+    }
+
+    @Test
+    void testTokenizeProxiedEntitiesChainShouldHandleMaliciousUser() throws Exception {
+        // Arrange
+        final List NAMES = [MALICIOUS_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2]
+        final String RAW_PROXY_CHAIN = "<${NAMES.collect { sanitizeDn(it) }.join("><")}>"
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedNames.collect { "\"${it}\"" }}")
+
+        // Assert
+        assert tokenizedNames == NAMES
+        assert tokenizedNames.size() == NAMES.size()
+        assert !tokenizedNames.contains(SAFE_USER_NAME_JOHN)
+    }
+
+    @Test
+    void testTokenizeProxiedEntitiesChainShouldDecodeNonAsciiValues() throws Exception {
+        // Arrange
+        final String RAW_PROXY_CHAIN = "<${SAFE_USER_NAME_JOHN}><${UNICODE_DN_1_ENCODED}><${UNICODE_DN_2_ENCODED}>"
+        final List TOKENIZED_NAMES = [SAFE_USER_NAME_JOHN, UNICODE_DN_1, UNICODE_DN_2]
+        logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+        // Act
+        def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+        logger.info("Tokenized proxy chain: ${tokenizedNames.collect { "\"${it}\"" }}")
+
+        // Assert
+        assert tokenizedNames == TOKENIZED_NAMES
+        assert tokenizedNames.size() == TOKENIZED_NAMES.size()
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
index 3af1a0d..f6d1248 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -346,6 +346,8 @@
         assertEquals(200, logout_response.getStatus());
 
         // Then: the /access endpoint is queried using the logged out JWT
+        LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***");
+        LOGGER.info("*** We are validating the access token no longer works following logout ***");
         final Response retryResponse = client
                 .target(createURL("/access"))
                 .request()
@@ -365,6 +367,7 @@
     public void testLogoutWithJWTSignedByWrongKey() throws Exception {
 
         // Given: use the /access/logout endpoint with the JWT for the nifiadmin LDAP user to log out
+        LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***");
         final Response logoutResponse = client
                 .target(createURL("/access"))
                 .request()
@@ -652,6 +655,8 @@
         accessClient.logout(token);
 
         // check the status of the current user again and should be unauthorized
+        LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***");
+        LOGGER.info("*** We are validating the access token no longer works following logout ***");
         try {
             userClient.getAccessStatus();
             Assert.fail("Should have failed with an unauthorized exception");
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
index 92f5458..1401838 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
@@ -107,12 +107,12 @@
         assertEquals(INITIAL_ADMIN_IDENTITY, currentUser.getIdentity());
         assertFalse(currentUser.isAnonymous());
         assertNotNull(currentUser.getResourcePermissions());
-        Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+        final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
         assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
         assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets());
         assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants());
         assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies());
-        assertEquals(new Permissions().withCanWrite(true).withCanRead(true).withCanDelete(true), currentUser.getResourcePermissions().getProxy());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy());
     }
 
     @Test
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java
new file mode 100644
index 0000000..0401ece
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java
@@ -0,0 +1,229 @@
+/*
+ * 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.registry.web.api;
+
+import org.apache.nifi.registry.NiFiRegistryTestApiApplication;
+import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.client.NiFiRegistryClient;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.client.RequestConfig;
+import org.apache.nifi.registry.client.UserClient;
+import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+import org.apache.nifi.registry.client.impl.request.ProxiedEntityRequestConfig;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.ws.rs.core.Response;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        classes = NiFiRegistryTestApiApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITSecureProxy")
+@Import(SecureITClientConfiguration.class)
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql"})
+public class SecureProxyIT extends IntegrationTestBase {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(SecureProxyIT.class);
+
+    private static final String INITIAL_ADMIN_IDENTITY = "CN=user1, OU=nifi";
+    private static final String PROXY_IDENTITY = "CN=proxy, OU=nifi";
+    private static final String NEW_USER_IDENTITY = "CN=user2, OU=nifi";
+    private static final String UTF8_USER_IDENTITY = "CN=Алйс, OU=nifi";
+    private static final String ANONYMOUS_USER_IDENTITY = "";
+
+    private NiFiRegistryClient registryClient;
+
+    @Before
+    public void setup() {
+        final String baseUrl = createBaseURL();
+        LOGGER.info("Using base url = " + baseUrl);
+
+        final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl);
+        Assert.assertNotNull(clientConfig);
+
+        final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder()
+                .config(clientConfig)
+                .build();
+        Assert.assertNotNull(client);
+        this.registryClient = client;
+    }
+
+    @After
+    public void teardown() {
+        try {
+            registryClient.close();
+        } catch (final Exception e) {
+            // do nothing
+        }
+    }
+
+    @Test
+    public void testAccessStatus() throws Exception {
+
+        // Given: the client and server have been configured correctly for two-way TLS
+        final String expectedJson = "{" +
+                "\"identity\":\"CN=proxy, OU=nifi\"," +
+                "\"anonymous\":false," +
+                "\"resourcePermissions\":{" +
+                "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"buckets\":{\"canRead\":true,\"canWrite\":false,\"canDelete\":false}," +
+                "\"tenants\":{\"canRead\":false,\"canWrite\":false,\"canDelete\":false}," +
+                "\"policies\":{\"canRead\":false,\"canWrite\":false,\"canDelete\":false}," +
+                "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" +
+                "}";
+
+        // When: the /access endpoint is queried
+        final Response response = client
+                .target(createURL("access"))
+                .request()
+                .get(Response.class);
+
+        // Then: the server returns 200 OK with the expected client identity
+        assertEquals(200, response.getStatus());
+        final String actualJson = response.readEntity(String.class);
+        JSONAssert.assertEquals(expectedJson, actualJson, false);
+    }
+
+    @Test
+    public void testAccessStatusUsingRegistryClient() throws Exception {
+
+        // Given: the client and server have been configured correctly for two-way TLS
+        final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+        final Permissions readAccess = new Permissions().withCanRead(true).withCanWrite(false).withCanDelete(false);
+        final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+
+        // When: the /access endpoint is queried
+        final UserClient userClient = registryClient.getUserClient();
+        final CurrentUser currentUser = userClient.getAccessStatus();
+
+        // Then: the server returns the proxy identity with default nifi node access
+        assertEquals(PROXY_IDENTITY, currentUser.getIdentity());
+        assertFalse(currentUser.isAnonymous());
+        assertNotNull(currentUser.getResourcePermissions());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+        assertEquals(readAccess, currentUser.getResourcePermissions().getBuckets());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy());
+    }
+
+    @Test
+    public void testAccessStatusAsProxiedAdmin() throws Exception {
+
+        // Given: the client and server have been configured correctly for two-way TLS
+        final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+        final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(INITIAL_ADMIN_IDENTITY);
+
+        // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+        final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+        final CurrentUser currentUser = userClient.getAccessStatus();
+
+        // Then: the server returns the admin identity and access policies
+        assertEquals(INITIAL_ADMIN_IDENTITY, currentUser.getIdentity());
+        assertFalse(currentUser.isAnonymous());
+        assertNotNull(currentUser.getResourcePermissions());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies());
+        assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy());
+    }
+
+    @Test
+    public void testAccessStatusAsProxiedUser() throws Exception {
+
+        // Given: the client and server have been configured correctly for two-way TLS
+        final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+        final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(NEW_USER_IDENTITY);
+
+        // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+        final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+        final CurrentUser currentUser = userClient.getAccessStatus();
+
+        // Then: the server returns the user identity ad
+        assertEquals(NEW_USER_IDENTITY, currentUser.getIdentity());
+        assertFalse(currentUser.isAnonymous());
+        assertNotNull(currentUser.getResourcePermissions());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getProxy());
+    }
+
+    @Test
+    public void testAccessStatusAsProxiedAnonymousUser() throws Exception {
+
+        // Given: the client and server have been configured correctly for two-way TLS
+        final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+        final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(ANONYMOUS_USER_IDENTITY);
+
+        // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+        final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+        final CurrentUser currentUser = userClient.getAccessStatus();
+
+        // Then: the server returns the proxy identity with default nifi node access
+        assertEquals("anonymous", currentUser.getIdentity());
+        assertTrue(currentUser.isAnonymous());
+        assertNotNull(currentUser.getResourcePermissions());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getProxy());
+    }
+
+    @Test
+    public void testAccessStatusAsProxiedUtf8User() throws Exception {
+
+        // Given: the client and server have been configured correctly for two-way TLS
+        final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+        final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(UTF8_USER_IDENTITY);
+
+        // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+        final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+        final CurrentUser currentUser = userClient.getAccessStatus();
+
+        // Then: the server returns the proxy identity with default nifi node access
+        assertEquals(UTF8_USER_IDENTITY, currentUser.getIdentity());
+        assertFalse(currentUser.isAnonymous());
+        assertNotNull(currentUser.getResourcePermissions());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+        assertEquals(noAccess, currentUser.getResourcePermissions().getProxy());
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties
new file mode 100644
index 0000000..cab6d41
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+
+# Properties for Spring Boot integration tests
+# Documentation for common Spring Boot application properties can be found at:
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+
+# Custom (non-standard to Spring Boot) properties
+nifi.registry.properties.file: src/test/resources/conf/secure-proxy/nifi-registry.properties
+nifi.registry.client.properties.file: src/test/resources/conf/secure-proxy/nifi-registry-client.properties
+
+
+# Embedded Server SSL Context Config
+server.ssl.client-auth: need
+server.ssl.key-store: ./target/test-classes/keys/registry-ks.jks
+server.ssl.key-store-password: password
+server.ssl.key-password: password
+server.ssl.protocol: TLS
+server.ssl.trust-store: ./target/test-classes/keys/ca-ts.jks
+server.ssl.trust-store-password: password
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml
new file mode 100644
index 0000000..d4fedab
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the userGroupProviders, accessPolicyProviders, and authorizers to use when running securely. In order
+    to use a specific authorizer it must be configured here and its identifier must be specified in the nifi-registry.properties file.
+    If the authorizer is a managedAuthorizer, it may need to be configured with an accessPolicyProvider and an userGroupProvider.
+    This file allows for configuration of them, but they must be configured in order:
+
+    ...
+    all userGroupProviders
+    all accessPolicyProviders
+    all Authorizers
+    ...
+-->
+<authorizers>
+
+    <!--
+        The FileUserGroupProvider will provide support for managing users and groups which is backed by a file
+        on the local file system.
+
+        - Users File - The file where the FileUserGroupProvider will store users and groups.
+
+        - Initial User Identity [unique key] - The identity of a users and systems to seed the Users File. The name of
+            each property must be unique, for example: "Initial User Identity A", "Initial User Identity B",
+            "Initial User Identity C" or "Initial User Identity 1", "Initial User Identity 2", "Initial User Identity 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate).
+    -->
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/conf/secure-proxy/users.xml</property>
+        <property name="Initial User Identity 1">CN=user1, OU=nifi</property>
+        <property name="Initial User Identity 2">CN=user2, OU=nifi</property>
+        <property name="Initial User Identity 3">CN=Алйс, OU=nifi</property>
+        <property name="Initial User Identity 4">CN=proxy, OU=nifi</property>
+    </userGroupProvider>
+
+    <!--
+        The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
+        on the local file system.
+
+        - User Group Provider - The identifier for an User Group Provider defined above that will be used to access
+            users and groups for use in the managed access policies.
+
+        - Authorizations File - The file where the FileAccessPolicyProvider will store policies.
+
+        - Initial Admin Identity - The identity of an initial admin user that will be granted access to the UI and
+            given the ability to create additional users, groups, and policies. The value of this property could be
+            a DN when using certificates or LDAP. This property will only be used when there
+            are no other policies defined.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the initial admin identity,
+            so the value should be the unmapped identity. This identity must be found in the configured User Group Provider.
+
+        - NiFi Identity [unique key] - The identity of a NiFi node that will have access to this NiFi Registry and will be able
+            to act as a proxy on behalf of a NiFi Registry end user. A property should be created for the identity of every NiFi
+            node that needs to access this NiFi Registry.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the nifi identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate). This identity must be found
+            in the configured User Group Provider.
+    -->
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/conf/secure-proxy/authorizations.xml</property>
+        <property name="Initial Admin Identity">CN=user1, OU=nifi</property>
+        <property name="NiFi Identity 1">CN=proxy, OU=nifi</property>
+    </accessPolicyProvider>
+
+    <!--
+        The StandardManagedAuthorizer. This authorizer implementation must be configured with the
+        Access Policy Provider which it will use to access and manage users, groups, and policies.
+        These users, groups, and policies will be used to make all access decisions during authorization
+        requests.
+
+        - Access Policy Provider - The identifier for an Access Policy Provider defined above.
+    -->
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties
new file mode 100644
index 0000000..b4c1a6a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+# client security properties #
+nifi.registry.security.keystore=./target/test-classes/keys/proxy-ks.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=password
+nifi.registry.security.keyPasswd=password
+nifi.registry.security.truststore=./target/test-classes/keys/ca-ts.jks
+nifi.registry.security.truststoreType=JKS
+nifi.registry.security.truststorePasswd=password
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties
new file mode 100644
index 0000000..1ae5cc3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.https.host=localhost
+nifi.registry.web.https.port=0
+
+# security properties #
+#
+# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty **
+#
+nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-proxy/authorizers.xml
+nifi.registry.security.authorizer=managed-authorizer
+
+# providers properties #
+nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml
+
+# enabled revision checking #
+nifi.registry.revisions.enabled=true
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
index 24460cd..129b504 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
@@ -41,6 +41,10 @@
 | registry, localhost | registry-key.pem | NiFi Registry server private key | PEM | password |
 | registry, localhost | registry-ks.jks | NiFi Registry server key/cert keystore | JKS | password |
 | registry, localhost | registry-ks.p12 | NiFi Registry server key/cert keystore | PKCS12 | password |
+| proxy, localhost | proxy-cert.pem | Proxy server public cert | PEM (unencrypted) | N/A |
+| proxy, localhost | proxy-key.pem | Proxy server private key | PEM | password |
+| proxy, localhost | proxy-ks.jks | Proxy server key/cert keystore | JKS | password |
+| proxy, localhost | proxy-ks.p12 | Proxy server key/cert keystore | PKCS12 | password |
 | CN=user1, OU=nifi | user1-cert.pem | client (user="user1") public cert | PEM (unencrypted) | N/A |
 | CN=user1, OU=nifi | user1-key.pem | client (user="user1") private key | PEM | password |
 | CN=user1, OU=nifi | user1-ks.jks | client (user="user1") key/cert keystore | JKS | password |
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem
new file mode 100644
index 0000000..67d2960
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem
@@ -0,0 +1,51 @@
+Bag Attributes
+    friendlyName: proxy-key
+    localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34 
+subject=/OU=nifi/CN=proxy
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDbjCCAlagAwIBAgIKAXR0SvpRAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTIw
+MDkwOTE5MTUwNFoXDTQ4MDEyNTE5MTUwNFowHzENMAsGA1UECwwEbmlmaTEOMAwG
+A1UEAwwFcHJveHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2CeF8
+tYG/58pjW0nE5Y/JnZzSxQpjs9sFzvJx9V8SEviAPxCSv0tP4PhR09hMLdKA2bTB
+TM1S189fkN09fLLQtFucAJtsqseNFlZh/iJhmX1I1n5MIaLvffFNpXEaMbWOKSX7
+NfTT4mtS0jNJSpkR6bQEVrz8iiMrrMFGz9AiwEba/pg0hUTP+VP4pt7aKFwb8ZyL
+6Wxo+Ny6nMe4M7KHcQi4+Cwmgc5YChMAGfCID+xGyND4vR2WJxNYe1joT+NXtGzY
+zgYDU3NiYJN7lB3NKTZpXz9/Aga+NSo6pFzEFXiFNmFU50O7wkdhlhQfRlBSgjCY
+0vL3X6N1CXSOKCbrAgMBAAGjgZowgZcwHQYDVR0OBBYEFCFvl95yYwhk81eaG4mH
+dd94iKeXMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1UdDwEB
+/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
+ATAbBgNVHREEFDASggVwcm94eYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB
+AQA1rAxZX2ebm40XOFhecxUQhTYJW+VYJUXRoxtjaEL9NwDvTcI8CmmaZ5LNF73E
+doT75HOo+33bQz/Xnp9zFRWmiF4KH3u60cZ0obNpTCtWfY41UbN8La6FPokmaV+L
+7POigubeVZf/6d7Hf8PcBQeXE+CNKJMQ73RoKbMWcdEhEdio1sXdMNPBo4m5SeDe
+T/nbIbLJiFPo20lir3Q4OrzGOUnqwyT+7L64myjVkHgyjOzHC5PH4D5XGMNfQ6tQ
+O2/flOso0ALGTB9VBo20sITS+BnPlMsWGdjQya/d1Oc4CEifRiWFx6H9daCb29/o
+9ReApmPoZV801EkEAhPwqyZd
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: CN=Test CA (Do Not Trust),OU=nifi
+subject=/OU=nifi/CN=Test CA (Do Not Trust)
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4
+MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G
+A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l
+RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/
+keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU
+n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG
+GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj
+gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/
+BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2
+AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG
+CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh
+ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE
+iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI
+Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC
+bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv
+qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr
+qOi08D8F0w==
+-----END CERTIFICATE-----
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem
new file mode 100644
index 0000000..1a5417d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem
@@ -0,0 +1,85 @@
+Bag Attributes
+    friendlyName: proxy-key
+    localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34 
+Key Attributes: <No Attributes>
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIkocYj9mKFyMCAggA
+MBQGCCqGSIb3DQMHBAibNQqgCb2bzQSCBMiTWItLGuguyT/g4eZXZMZvZW2cnZ5+
+heTqXLAWChAt8IwiGYC1K40PcVQvNBPFoLqbdH2EsvmK8l7tNv1tq/kjpkuhVuwL
+06E9m9Y8ucS0Q60WlREaac2kth5rWdx586kwKsM7h79fKSS86F3tCbamIgdst7xQ
+WQ1z7Dcx6Et6VY4F8i6kEgp9b5zIHmbBxB/b9dfHDJSQ5WyzHjc1raQ6XJ/c7DFs
+URkafQbAQ5urHVS9+aWfCPXT9Wjl/4kYqcRWkYua/ulhzcIKyBr1BP+t4Xsoj3da
+4AZzR+ZSQiKj6UkBSssEJbPtOfonhQrFsoTm3sR1k6pXa4uySef42WGoex77ekco
+ReXiAI7Z1tPp3QI6ossYK5R7WY876/fblMNL96hGtCQOM0P9x5L58sVZJ0j2xbJx
+Tvu91yKA3vVn7cI0z99dz3ULuM2bdmKoZbwMlhgk1X7VRg43GHLVd9NlpUido7/Y
+b/KV0aGKrxBr3rdXu+rVZ/7ElhmQ3q7nkU6hxo3Lj8eF9dkJzhxPtRvUAqsRhaN0
+c/iNSNJsDX1g+LOzuGI9xxS8lg61C73jz4Dhfv+CPNZzVXiJctY74hMzTw0gkkSW
+4cN/0UoGdjVpeUScy9+IqF94Hp6w7xDXTJ6egoT60ZtT7Ls8zc6TTNl+qo/IGEnw
+QA+7Z6OY38Z0gMQivtDeOrLFFcTy98ZBibvJlw/LsW2KadXzGooXuJX2AgTfs3U5
+oIBjmubincvVhpcQDAdzifzURZuAnvEb1FvQIAC/jp8U009f2JByycobmviluNSp
+2EirUpA0XsW8B99EUlUfzbCOAb9YS8vesyzevJP43XjKlNNsptG4pe8jiMaxUFGE
+1fdseWJwiRfSXpwQBafovOVXItxZWkrfD+i7h6LfG79hDFNHTcDknoYjJzcAYOYF
+ju9vUhE9dgyZogppOQ32NURkyh8HkHZFGI9MlaoqNXl4z9TRMUVxJET/ZCJ0zsE+
+WV5KL6oiPU5VuGbIReouDtPd6lOtqsaE3PoKfriGhIKSGOHbxOZa3WLs/++BJO8A
+5eNKWuqwUtL4uPw4gnxJFIyK04lzMp5q5BT7fkrlenRvsOn8IGZp6e5Wvsb3dCx1
+LKR8ktyjnxSpSw9dACDeGGxTeCOUxM5ElafI7RKsJ1kCtUpxMyfTru1SmP3Hcepc
+GteHjm/vrv9TW0ZBhGFsuxo5AuZFnsjSAy6JVHRYQaNzdTs8qJTZvYhGjq/XSI34
+Jafugsu1uKMKu+RfvbSM30/70kXu6R0DT55w1j8AMbdchyJDBQT4Ua6zK/N2CHW2
+++jxwM7h7C4imFgsILjo41TT7Ve2vqG3PkLaSIgotZDs9zCYJxN/A9QGHTfzjQvy
+zp2yNCC5ebyGfCgBafdCasyyPuJGVagSvNky7fRlhu0HI+kPIarL6xASFDxDVBEF
+ko5kHDbKZHFp4skdi+Bu/DKtIoGUEJabRkkyOO0rOrq4ldEm3oAnn3PIUaVk0NNb
+pcSx9Hb/M2wWRMHdTpRrBYJ++GlcCn2oaIX4kkxk5ERvm17guHxWATq4spI3GM38
+kCcXwUCEjV1ILpn43m/+1RPTzFKpa7S79HjMWbQEx7VTgtVwxqFq3bGUTV7MtnSQ
+rbg=
+-----END ENCRYPTED PRIVATE KEY-----
+Bag Attributes
+    friendlyName: proxy-key
+    localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34 
+subject=/OU=nifi/CN=proxy
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDbjCCAlagAwIBAgIKAXR0SvpRAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTIw
+MDkwOTE5MTUwNFoXDTQ4MDEyNTE5MTUwNFowHzENMAsGA1UECwwEbmlmaTEOMAwG
+A1UEAwwFcHJveHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2CeF8
+tYG/58pjW0nE5Y/JnZzSxQpjs9sFzvJx9V8SEviAPxCSv0tP4PhR09hMLdKA2bTB
+TM1S189fkN09fLLQtFucAJtsqseNFlZh/iJhmX1I1n5MIaLvffFNpXEaMbWOKSX7
+NfTT4mtS0jNJSpkR6bQEVrz8iiMrrMFGz9AiwEba/pg0hUTP+VP4pt7aKFwb8ZyL
+6Wxo+Ny6nMe4M7KHcQi4+Cwmgc5YChMAGfCID+xGyND4vR2WJxNYe1joT+NXtGzY
+zgYDU3NiYJN7lB3NKTZpXz9/Aga+NSo6pFzEFXiFNmFU50O7wkdhlhQfRlBSgjCY
+0vL3X6N1CXSOKCbrAgMBAAGjgZowgZcwHQYDVR0OBBYEFCFvl95yYwhk81eaG4mH
+dd94iKeXMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1UdDwEB
+/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
+ATAbBgNVHREEFDASggVwcm94eYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB
+AQA1rAxZX2ebm40XOFhecxUQhTYJW+VYJUXRoxtjaEL9NwDvTcI8CmmaZ5LNF73E
+doT75HOo+33bQz/Xnp9zFRWmiF4KH3u60cZ0obNpTCtWfY41UbN8La6FPokmaV+L
+7POigubeVZf/6d7Hf8PcBQeXE+CNKJMQ73RoKbMWcdEhEdio1sXdMNPBo4m5SeDe
+T/nbIbLJiFPo20lir3Q4OrzGOUnqwyT+7L64myjVkHgyjOzHC5PH4D5XGMNfQ6tQ
+O2/flOso0ALGTB9VBo20sITS+BnPlMsWGdjQya/d1Oc4CEifRiWFx6H9daCb29/o
+9ReApmPoZV801EkEAhPwqyZd
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: CN=Test CA (Do Not Trust),OU=nifi
+subject=/OU=nifi/CN=Test CA (Do Not Trust)
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4
+MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G
+A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l
+RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/
+keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU
+n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG
+GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj
+gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/
+BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2
+AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG
+CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh
+ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE
+iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI
+Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC
+bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv
+qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr
+qOi08D8F0w==
+-----END CERTIFICATE-----
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks
new file mode 100644
index 0000000..444b43d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12 b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12
new file mode 100644
index 0000000..9eca9d8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12
Binary files differ