Add TLS truststore support to Redis sink - #135
Conversation
david-streamlio
left a comment
There was a problem hiding this comment.
Reviewed the TLS truststore wiring against Lettuce 6.5.1 and this repo's existing conventions. This is solid, well-tested work — the wiring is correct, FULL as the default means existing deployments are unaffected, and a few things here are better than typical:
sensitive = trueon the password and agetSecret()test. That is the part people forget.- The wrong-password negative check in
buildSslOptionsWithTruststorePathAndPasswordTestproves the password actually reaches Lettuce rather than being silently dropped. Most contributions assert only the happy path. - The comment explaining why
getTruststore()cannot be asserted andcreateSslContextBuilder()is used instead is accurate for 6.5.x — someone clearly read the library rather than guessing. tls/README.mddocuments the fixture's provenance and notes the private key was discarded. That is exactly the right way to check in a test keystore.
I also checked two things I was ready to flag and won't: redisTlsVerifyPeer as a String rather than an enum matches the existing clientMode, and getResource().getFile() matches 20 other uses in this repo including the pre-existing helper in RedisSinkConfigTest. Both are house style.
Nothing below blocks merge. The comments are ordered roughly by how much I care:
- Invalid verify-peer values pass
validate()and fail later, with a message that omits the offending value. - The truststore path is never checked for existence, so a typo becomes a connection-time IOException.
- The help text advertises "JKS or PKCS12" while the store type is not configurable.
- Disabling peer verification leaves no trace in the logs.
Plus two minor ones (locale on toUpperCase, and a pre-existing cluster-mode socketOptions gap you happen to be next to).
|
|
||
| public void validate() { | ||
| Preconditions.checkNotNull(clientMode, "clientMode property not set."); | ||
| Preconditions.checkArgument(redisTlsVerifyPeer != null, "redisTlsVerifyPeer property not set."); |
There was a problem hiding this comment.
validate() only null-checks this, so an invalid value still passes configuration validation and fails later, when RedisSession.create parses it. A typo ("Full ", "true", or an empty string) therefore surfaces as a connection-time failure rather than a config error.
Parsing the enum here instead would fail fast, and would let you drop the null check — SslVerifyMode.valueOf(null) throws anyway:
try {
SslVerifyMode.valueOf(redisTlsVerifyPeer.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException | NullPointerException e) {
throw new IllegalArgumentException("redisTlsVerifyPeer must be one of "
+ Arrays.asList(SslVerifyMode.values()) + ", got: " + redisTlsVerifyPeer);
}| try { | ||
| return SslVerifyMode.valueOf(value.toUpperCase()); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new IllegalArgumentException("Illegal Redis TLS verify-peer mode, valid values are: " |
There was a problem hiding this comment.
The message lists the valid values but not the one that was actually supplied, which is the single most useful thing for someone debugging their config. It also drops e as the cause.
throw new IllegalArgumentException("Illegal Redis TLS verify-peer mode '" + value
+ "', valid values are: " + Arrays.asList(SslVerifyMode.values()), e);If you move validation into RedisAbstractConfig.validate() (see my other comment), this becomes unreachable for user input and can just carry the value for safety.
| if (!config.isRedisUseTls() || StringUtils.isBlank(config.getRedisTlsTrustStorePath())) { | ||
| return SslOptions.create(); | ||
| } | ||
| File truststore = new File(config.getRedisTlsTrustStorePath()); |
There was a problem hiding this comment.
Nothing checks that this path exists or is readable, so a wrong path surfaces as an IOException from inside Lettuce/Netty when the SSL context is built at connection time — well away from the setting that caused it.
Worth an exists() / canRead() check that names redisTlsTrustStorePath and its value, ideally in validate() so it fails before any connection is attempted. loadFromMapWithTlsTrustFieldsTest shows the gap: it loads /tmp/fake-truststore.jks and validates fine.
| @FieldDoc( | ||
| required = false, | ||
| defaultValue = "", | ||
| help = "Filesystem path to a trust store (e.g. JKS or PKCS12) used to validate the Redis server's TLS " |
There was a problem hiding this comment.
The help advertises "(e.g. JKS or PKCS12)", but the store type is never configurable — SslOptions.Builder.keyStoreType(String) exists in Lettuce 6.5.1 and isn't surfaced, so the type is whatever the JVM defaults to.
The fixture added in this PR is the trap in miniature: per tls/README.md it is PKCS12 content in a file named .jks. It happens to load because the JDK's PKCS12 keystore reads JKS too under keystore.type.compat, but a user who disables that, or who has a store the default type can't read, has no escape hatch.
Either add a redisTlsTrustStoreType (blank = JVM default), or narrow this text to say only the JVM's default store type is supported.
| builder.withDatabase(config.getRedisDatabase()); | ||
| builder.withSsl(config.isRedisUseTls()); | ||
| if (config.isRedisUseTls()) { | ||
| builder.withVerifyPeer(parseVerifyMode(config.getRedisTlsVerifyPeer())); |
There was a problem hiding this comment.
This is the line that turns "peer verification is always on" — which is what the old redisUseTls help text promised — into something an operator can switch off. NONE disables both chain and hostname verification, which makes the connection trivially MITM-able.
The @FieldDoc says "should not be used in production", but nothing in the running system ever says so: a cluster silently accepting any certificate looks identical to a correctly secured one in the logs. A WARN here when the mode is NONE (arguably also CA, which skips the hostname check) would make the weakened posture visible where someone would actually notice it.
|
|
||
| private static SslVerifyMode parseVerifyMode(String value) { | ||
| try { | ||
| return SslVerifyMode.valueOf(value.toUpperCase()); |
There was a problem hiding this comment.
toUpperCase() uses the default locale. None of FULL/CA/NONE contain an i, so this is latent rather than a live bug, but Locale.ROOT is free and is what the rest of the ecosystem expects. (Same applies to line 93's getClientMode().toUpperCase(), which is pre-existing.)
| redisSession = new RedisSession(client, connection, connection.async()); | ||
| } else if (clientMode == ClientMode.CLUSTER) { | ||
| ClusterClientOptions.Builder clientOptions = ClusterClientOptions.builder() | ||
| .sslOptions(sslOptions) |
There was a problem hiding this comment.
Not this PR's doing, but you are editing this builder: the standalone branch above sets .socketOptions(socketOptions) and this one never has, so tcpNoDelay, keepAlive and connectTimeout are silently ignored in cluster mode. Adding .socketOptions(socketOptions) here would be a welcome drive-by if you are up for it — equally fine to leave for a separate PR.
|
Thanks for the detailed review! Noted — I'll work through these suggestions and push the updates once I get to them. |
|
Pushed updates addressing all six points from the review — replied inline
Also added
One more thing I found while building that integration test, flagging All existing tests plus the new ones pass locally |
|
Re-reviewed at All six points from my earlier review are addressed. I checked each against the code rather than taking the commit message for it: The integration test is the substantive addition here, and it earns its place. The previous round asserted One call that looks wrong and is not, worth a comment purely so the next reader does not stop on it as I did. Three notes, none blocking, in the order I care about them. 1. The If the flake was specific to your sandbox, gating it (an env var, or a 2. The private key is now committed, and I want to be explicit that I think that is fine, because my earlier review singled out its absence as the right way to check in a test keystore, and a reader comparing the two comments would otherwise find them contradictory. 3. Nothing above needs to change before merge as far as I am concerned. Thanks for the thorough follow-up on the first round — the negative-path coverage and the fixture documentation are both better than the norm here. |
Motivation
Adds TLS trust configuration to the Redis sink, allowing Redis deployments
using internally signed certificates to be configured without requiring a
custom NAR.
Changes
FULL,CA,NONE).Testing
./gradlew :redis:test --rerun-tasks./gradlew :redis:check --rerun-tasksBoth pass successfully.
Closes #132