NIFI-5752: Load balancing fails with wildcard certs#3110
NIFI-5752: Load balancing fails with wildcard certs#3110kotarot wants to merge 2 commits intoapache:masterfrom
Conversation
| final List<String> nodeIds = clusterCoordinator.getNodeIdentifiers().stream() | ||
| .map(NodeIdentifier::getApiAddress) | ||
| .collect(Collectors.toSet()); | ||
| .collect(Collectors.toList()); |
There was a problem hiding this comment.
Is there any reason to use toList instead?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
I think HostnameVerifier is thread-safe and can be an instance field instead of creating at each verification.
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
In a case where the cert contains exact nodeId, the nodeId is still informative to be logged. I'd suggest logging message something like:
| logger.debug("Authorizing Client to Load Balance data"); | |
| logger.debug("The request was verified with node ID '{}'. Authorizing Client to Load Balance data", nodeId); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
kotarot
left a comment
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
|
@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
Does this change seem to be no problem? Also, I modified a few tests related to @ijokarumawak Could you please review it? |
ijokarumawak
left a comment
There was a problem hiding this comment.
@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"); |
There was a problem hiding this comment.
We don't have to use String.format() here, please the String to logger.warn() directly.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Now we only call this authorize() method if socket is a SSLSocket. We can remove this block.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@ijokarumawak OK, I get it now. Thanks for kindly telling me that. I pushed a new commit. Please check it. Thanks!
|
@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
|
It looks good. +1. Merging. Thanks, @kotarot! |
|
@ijokarumawak Thanks for reviewing and merging my PR! |
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:
For documentation related changes:
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.