HBASE-30387: Support SingleEKU certificates - #8681
lucakovacs wants to merge 7 commits into
Conversation
2122bf1 to
b26c894
Compare
b26c894 to
08557a2
Compare
|
The newly added |
PDavid
left a comment
There was a problem hiding this comment.
Many thanks, looks really good. 👍
petersomogyi
left a comment
There was a problem hiding this comment.
Could you extend the TLS documentation?
https://github.com/apache/hbase/blob/master/hbase-website/app/pages/_docs/docs/_mdx/(multi-page)/security/tls.mdx
| String trustStoreLocation = resolveConfig(config, TLS_CONFIG_CLIENT_TRUSTSTORE_LOCATION, | ||
| TLS_CONFIG_TRUSTSTORE_LOCATION, ""); | ||
| char[] trustStorePassword = resolvePassword(config, TLS_CONFIG_CLIENT_TRUSTSTORE_PASSWORD, | ||
| TLS_CONFIG_TRUSTSTORE_PASSWORD); | ||
| String trustStoreType = | ||
| resolveConfig(config, TLS_CONFIG_CLIENT_TRUSTSTORE_TYPE, TLS_CONFIG_TRUSTSTORE_TYPE, ""); |
There was a problem hiding this comment.
The fallback happens per config key. Setting only hbase.rpc.tls.client.keystore.location means that legacy password and type will be matched with it causing an error. These should not be combined.
| X509Util.ClientAuth clientAuth = X509Util.ClientAuth | ||
| .fromPropertyValue(c.get(HBASE_UI_SSL_CLIENT_AUTH_MODE, X509Util.ClientAuth.NONE.name())); |
There was a problem hiding this comment.
It might be an edge case but when the config is in hbase-site.xml with empty value then it will use NEED not NONE when the config is not there at all.
/**
* Converts a property value to a ClientAuth enum. If the input string is empty or null, returns
* <code>ClientAuth.NEED</code>.
* @param prop the property string.
* @return the ClientAuth.
* @throws IllegalArgumentException if the property value is not "NONE", "WANT", "NEED", or
* empty/null.
*/
| // Truststore is entirely new for Thrift — no legacy fallback because there is no historical | ||
| // hbase.thrift.ssl.truststore.* configuration. When left unset, no truststore is configured | ||
| // on the connector and any hbase.thrift.ssl.server.client.auth.mode = WANT/NEED setting | ||
| // will fail the handshake for lack of a peer-cert trust root. |
There was a problem hiding this comment.
Opus 5.5 called out this: Jetty falls back to the JVM cacerts, so any publicly-CA-signed client cert is accepted and it will not cause a handshake fail. It should fail at startup if configs are missing or WARN about it to let the operator aware of the configuration issue.
There was a problem hiding this comment.
Same problem is present in REST and InfoServer.
| @Test | ||
| public void testClientAuthModeKeyIsRoleScoped() { | ||
| // Guard against a "double server.server." regression: the client-auth-mode config key must | ||
| // resolve to the single role-scoped key, not to a nested/prefixed form. | ||
| assertEquals("hbase.ui.ssl.server.client.auth.mode", InfoServer.HBASE_UI_SSL_CLIENT_AUTH_MODE); | ||
| } |
There was a problem hiding this comment.
What is the point of this test?
| conf.unset(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_LOCATION); | ||
| conf.unset(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_PASSWORD); | ||
| conf.unset(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_TYPE); | ||
| conf.unset(X509Util.TLS_CONFIG_CLIENT_TRUSTSTORE_LOCATION); | ||
| conf.unset(X509Util.TLS_CONFIG_CLIENT_TRUSTSTORE_PASSWORD); | ||
| conf.unset(X509Util.TLS_CONFIG_CLIENT_TRUSTSTORE_TYPE); |
There was a problem hiding this comment.
Isn't this a no-op?
Apart from that this test is the same as testCreateSSLContextWithoutCustomProtocol but that also validates the TLS protocols, not just if it is not empty.
| } | ||
|
|
||
| @TestTemplate | ||
| public void testCreateSSLContextForServerFallsBackToLegacyKeystore() throws Exception { |
There was a problem hiding this comment.
Same comment as testCreateSSLContextForClientFallsBackToLegacyKeystore
| public void testClientAuthNeedRejectsClientWithoutCert() throws Exception { | ||
| conf.set(Constants.REST_SSL_CLIENT_AUTH_MODE, "NEED"); | ||
| startRESTServerWithDefaultKeystoreType(); | ||
| assertThrows(SSLException.class, () -> sslClient.get("/version")); |
There was a problem hiding this comment.
This should be changed to IOException like in TestThriftServerSSLMutualAuth.
|
I made the following changes according to @petersomogyi's review:
If acceptable, I would to the documentation on an other PR or JIRA if needed |
| // resolveConfig falls back per key, so a store whose location comes from one prefix and whose | ||
| // password/type come from the other would open the wrong file, or the right file with the wrong | ||
| // credentials. A store is a unit: reject configs that straddle both prefixes. | ||
| public static void validateConfigPrefixConsistency(Configuration config, String rolePrefix, |
There was a problem hiding this comment.
Many thanks for adding this method which checks the consistency. 👍 Would it maybe make sense to add unit test for this?
| // resolveConfig falls back per key, so a store whose location comes from one prefix and whose | ||
| // password/type come from the other would open the wrong file, or the right file with the wrong | ||
| // credentials. A store is a unit: reject configs that straddle both prefixes. | ||
| public static void validateConfigPrefixConsistency(Configuration config, String rolePrefix, |
There was a problem hiding this comment.
While this validation is ok I think it can be problematic when an operator is migrating to the new configs.
It is not possible to add the new configs (all of the single EKU related) while the dual-use configs are still in place.
Wouldn't it make sense to fetch all the single EKU configs and if that misses any of the required key-value pairs then throw an exception?
IMO having all single use and all dual use configs is still a valid setup, maybe a WARN log can be added to "remove the legacy once migration is completed"
| X509Util.validateClientAuthTrustStore( | ||
| needsClientAuth | ||
| ? X509Util.ClientAuth.NEED | ||
| : (wantsClientAuth ? X509Util.ClientAuth.WANT : X509Util.ClientAuth.NONE), | ||
| trustStore, InfoServer.HBASE_UI_SSL_CLIENT_AUTH_MODE, | ||
| "hbase.ui.ssl.server.truststore.location", "hbase.ui.ssl.truststore.location", | ||
| "ssl.server.truststore.location"); |
There was a problem hiding this comment.
- the last three parameters are only used in the exception message, the actual check is just "clientAuth != NONE && blank(trustStore) → throw"
- The HttpServer is a generic base class, but those
hbase.ui.ssl.*key names belong toInfoServer. Could this validation live in InfoServer instead, right afterclientAuthis computed? That's also where the resolved truststore location and the correct key names are already in hand and it matches how REST/Thrift callvalidateClientAuthTrustStorefrom their own setup. - That move also removes the nested ternary here (InfoServer already has the ClientAuth enum, so it need not reconstruct it from the two booleans). It's hard to see which arguments are actually passed using this nested ternary operators.
There was a problem hiding this comment.
@lucakovacs this part wasn't touched in your last commit.
| private static void logOnce(Configuration config, String rolePrefix, String legacyPrefix, | ||
| Keys keys) { | ||
| StringBuilder unused = new StringBuilder(); | ||
| for (String postfix : keys.all()) { | ||
| if (config.get(legacyPrefix + postfix) != null) { | ||
| unused.append(unused.length() == 0 ? "" : ", ").append(legacyPrefix).append(postfix); | ||
| } | ||
| } | ||
| if (unused.length() > 0 && LOGGED_STORES.add(rolePrefix + keys.location)) { | ||
| LOG.warn("{} supplies this store, so these keys are unused: {}. Remove them once the" | ||
| + " migration is complete.", rolePrefix, unused); | ||
| } | ||
| } |
There was a problem hiding this comment.
This part looks ugly. String.join is exactly for this behavior. Also, the LOGGED_STORES could be evaluate prior so the for loop/stream is not executed when it was already logged.
if (!LOGGED_STORES.add(rolePrefix + keys.location)) {
return;
}
List<String> unused = Arrays.stream(keys.all())
.map(postfix -> legacyPrefix + postfix)
.filter(key -> config.get(key) != null)
.collect(Collectors.toList());
if (!unused.isEmpty()) {
LOG.warn("{} supplies this store, so these keys are unused: {}. Remove them once the"
+ " migration is complete.", rolePrefix, String.join(", ", unused));
}
| X509Util.validateClientAuthTrustStore( | ||
| needsClientAuth | ||
| ? X509Util.ClientAuth.NEED | ||
| : (wantsClientAuth ? X509Util.ClientAuth.WANT : X509Util.ClientAuth.NONE), | ||
| trustStore, InfoServer.HBASE_UI_SSL_CLIENT_AUTH_MODE, | ||
| "hbase.ui.ssl.server.truststore.location", "hbase.ui.ssl.truststore.location", | ||
| "ssl.server.truststore.location"); |
There was a problem hiding this comment.
@lucakovacs this part wasn't touched in your last commit.
No description provided.