Skip to content

NIFI-5752: Load balancing fails with wildcard certs#3110

Closed
kotarot wants to merge 2 commits intoapache:masterfrom
kotarot:NIFI-5752
Closed

NIFI-5752: Load balancing fails with wildcard certs#3110
kotarot wants to merge 2 commits intoapache:masterfrom
kotarot:NIFI-5752

Conversation

@kotarot
Copy link
Member

@kotarot kotarot commented Oct 25, 2018

Thank you for submitting a contribution to Apache NiFi.

In order to streamline the review of the contribution we ask you
to ensure the following steps have been taken:

For all changes:

  • Is there a JIRA ticket associated with this PR? Is it referenced
    in the commit message?

  • Does your PR title start with NIFI-XXXX where XXXX is the JIRA number you are trying to resolve? Pay particular attention to the hyphen "-" character.

  • Has your PR been rebased against the latest commit within the target branch (typically master)?

  • Is your initial contribution a single, squashed commit?

For code changes:

  • Have you ensured that the full suite of tests is executed via mvn -Pcontrib-check clean install at the root nifi folder?
  • Have you written or updated unit tests to verify your changes?
  • If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under ASF 2.0?
  • If applicable, have you updated the LICENSE file, including the main LICENSE file under nifi-assembly?
  • If applicable, have you updated the NOTICE file, including the main NOTICE file found under nifi-assembly?
  • If adding new Properties, have you added .displayName in addition to .name (programmatic access) for each of the new properties?

For documentation related changes:

  • Have you ensured that format looks appropriate for the output in which it is rendered?

Note:

Please ensure that once the PR is submitted, you check travis-ci for build issues and submit an update to your PR as soon as possible.

Copy link
Member

@ijokarumawak ijokarumawak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kotarot ! I think supporting wildcard cert is critical especially for better scalability. I'd like to hear comments from @markap14 , too, on what data we need to use for RECEIVE provenance events. I think using hostnames derived from socket address would be the most reliable one.

final List<String> nodeIds = clusterCoordinator.getNodeIdentifiers().stream()
.map(NodeIdentifier::getApiAddress)
.collect(Collectors.toSet());
.collect(Collectors.toList());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to use toList instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the existing code, we needed to search a target element in Set<String> nodeIds by the contains method. However, in this change, nodeIds is just iterated in a loop, so it is reasonable to change it List.

if (nodeIds.contains(clientId)) {
logger.debug("Client ID '{}' is in the list of Nodes in the Cluster. Authorizing Client to Load Balance data", clientId);
for (final String nodeId : nodeIds) {
final HostnameVerifier verifier = new DefaultHostnameVerifier();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think HostnameVerifier is thread-safe and can be an instance field instead of creating at each verification.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Instantiating in every iteration is wasteful. I'll fix it!

for (final String nodeId : nodeIds) {
final HostnameVerifier verifier = new DefaultHostnameVerifier();
if (verifier.verify(nodeId, sslSession)) {
logger.debug("Authorizing Client to Load Balance data");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a case where the cert contains exact nodeId, the nodeId is still informative to be logged. I'd suggest logging message something like:

Suggested change
logger.debug("Authorizing Client to Load Balance data");
logger.debug("The request was verified with node ID '{}'. Authorizing Client to Load Balance data", nodeId);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your idea. I'll fix it so!

final HostnameVerifier verifier = new DefaultHostnameVerifier();
if (verifier.verify(nodeId, sslSession)) {
logger.debug("Authorizing Client to Load Balance data");
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By #3109, we need to return the client peer description when authorization passes. For the best informative result for data provenance, we need to do:

  • If any SAN exists in the known nodeIds, then return the matched SAN (this can be done by the existing code), this way, we can identify which node sent the request at best. (If the cert contains multiple nodeIds as SAN, this logic can be broken, but I believe that is a corner-case that we don't need to support)
  • If none of SAN matches with any nodeId, then use hostname verifier to support wildcard cert. In this case, return hostname derived from the socket address

Alternatively, we just need to use the hostname verifier and use the hostname derived from the socket address in any case for provenance data. How do you think @markap14 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we just need to use HostnameVerifier to verify and use the hostname derived from the socket. The reason is that, anyway, HostnameVerifier could simply authorize a node using certs w/ or w/o wildcard, and I think the hostname derived from the socket is enough. If there are cases where the hostname derived from the socket and the hostname from Certificate Identities are different, please ignore my option.

I'd also like to hear comment from @markap14 . Thank you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think falling back to the hostname from the socket whenever there is not an exact match (i.e., the wildcard matches but not an exact string comparison) is fair. Originally, we used the hostname directly from the socket, but as Koji mentioned in #3109 we changed that behavior. This was done because when you look at Provenance data (and in logs), what you may see is something like a RECEIVE event with a transit URI of nifi://s7302.r720.y8302.mydomain.com because that's the FQDN but the user typically references this node as say nifi-01.mydomain.com. If the UI shows the node as nifi-01.mydomain.com in the cluster table, then it is best to show that in the Provenance and logs as well. This is especially true in virtual environments, running in Docker or in a publish Cloud/VM where often the hostname reported by socket.getInetAddress() is very different than what we typically like to see.

Does that make sense?

Also @kotarot thank you for noticing this and submitting this contribution!

Copy link
Member Author

@kotarot kotarot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review @ijokarumawak san!
I just replied to your comments. I'll fix the code later.

final List<String> nodeIds = clusterCoordinator.getNodeIdentifiers().stream()
.map(NodeIdentifier::getApiAddress)
.collect(Collectors.toSet());
.collect(Collectors.toList());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the existing code, we needed to search a target element in Set<String> nodeIds by the contains method. However, in this change, nodeIds is just iterated in a loop, so it is reasonable to change it List.

if (nodeIds.contains(clientId)) {
logger.debug("Client ID '{}' is in the list of Nodes in the Cluster. Authorizing Client to Load Balance data", clientId);
for (final String nodeId : nodeIds) {
final HostnameVerifier verifier = new DefaultHostnameVerifier();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Instantiating in every iteration is wasteful. I'll fix it!

for (final String nodeId : nodeIds) {
final HostnameVerifier verifier = new DefaultHostnameVerifier();
if (verifier.verify(nodeId, sslSession)) {
logger.debug("Authorizing Client to Load Balance data");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your idea. I'll fix it so!

final HostnameVerifier verifier = new DefaultHostnameVerifier();
if (verifier.verify(nodeId, sslSession)) {
logger.debug("Authorizing Client to Load Balance data");
return;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we just need to use HostnameVerifier to verify and use the hostname derived from the socket. The reason is that, anyway, HostnameVerifier could simply authorize a node using certs w/ or w/o wildcard, and I think the hostname derived from the socket is enough. If there are cases where the hostname derived from the socket and the hostname from Certificate Identities are different, please ignore my option.

I'd also like to hear comment from @markap14 . Thank you.

@kotarot
Copy link
Member Author

kotarot commented Oct 31, 2018

@markap14 Thank you for your kind advice! That makes sense to me.

In the new commit, I have left the existing authorization codes, followed by the authorization using HostnameVerifier (which I added). The authorization is performed as follows:

Does this change seem to be no problem?

Also, I modified a few tests related to LoadBalanceAuthorizer because the interface of authorize is changed.

@ijokarumawak Could you please review it?

Copy link
Member

@ijokarumawak ijokarumawak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kotarot Thanks for the updates, this PR now looks good. I was able to test load-balancing with a cluster using a wild card cert. I posted a few minor comments, though. Once those get addressed, I will merge this. Thanks again!

}
}

final String message = String.format("Authorization failed for Client ID's to Load Balance data because none of the ID's are known Cluster Node Identifiers");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to use String.format() here, please the String to logger.warn() directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it out. I fixed it by just removing String.format in this line because the next line also uses the message variable.


logger.debug("Will perform authorization against Client Identities '{}'", clientIdentities);

if (clientIdentities == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we only call this authorize() method if socket is a SSLSocket. We can remove this block.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the block L66-69? Do we always guarantee clientIdentities is not null if the socket is a SSLSocket? I suppose we still need this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing log message indicating that this block is meant for the case where NiFi clustering is not secured (not sending data via SSLSocket). This block contradicts with the other block such as following getCertificateIdentities method:

        final Certificate[] certs = sslSession.getPeerCertificates();
        if (certs == null || certs.length == 0) {
            throw new SSLPeerUnverifiedException("No certificates found");
        }

If we care about clientIdentities being null, then we should throw SSLPeerUnverifiedException("No client identities found"); instead of authorizing it. Having said that, I believe removing this block is safe as clientIdentities are populated using Collectors.toSet. If no SAN is found, the value will be an empty set instead of null.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijokarumawak OK, I get it now. Thanks for kindly telling me that. I pushed a new commit. Please check it. Thanks!

@kotarot
Copy link
Member Author

kotarot commented Nov 5, 2018

@ijokarumawak Thank you again for your review. I fixed the PR based on your comments. Can you check it?

NIFI-5752: Remove an unnecessary block
@ijokarumawak
Copy link
Member

It looks good. +1. Merging. Thanks, @kotarot!

@asfgit asfgit closed this in 13232c7 Nov 7, 2018
@kotarot
Copy link
Member Author

kotarot commented Nov 7, 2018

@ijokarumawak Thanks for reviewing and merging my PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants