test: qualify Redis certificate replacement on restart
Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
2 files changed
+47
-2
No files matched your search
@@ -32,8 +32,18 @@ verify authenticated TLS, untrusted-certificate rejection, wrong-host rejection,
|
||||
and no plaintext fallback. Mutual-TLS tests require a separate trusted client
|
||||
identity, reject missing or untrusted client certificates, and verify framework
|
||||
health after a successful authenticated connection. Certificates and keys are
|
||||
generated only for the disposable fixture. Certificate rotation and failover
|
||||
acceptance remain open; only standalone endpoints are implemented.
|
||||
generated only for the disposable fixture.
|
||||
|
||||
The TLS provider is resolved when the Redis runtime starts. Replacing its context
|
||||
alone does not reload existing pools or subscribers. For certificate replacement,
|
||||
stop dependent services and restart the Redis runtime through the application
|
||||
lifecycle, then restart its dependents. Qualification replaces both server and
|
||||
client certificates on the same Redis endpoint: stale material fails a fresh
|
||||
handshake, health becomes DOWN, and runtime restart with updated material restores
|
||||
PONG and health UP while closing the old pool. This is a restart procedure, not
|
||||
zero-downtime certificate rotation. Live reload, trust-chain/revocation policies,
|
||||
subscriber recovery across certificate changes and failover acceptance remain open;
|
||||
only standalone endpoints are implemented.
|
||||
|
||||
QQQ's application launcher starts the registered runtime service. Startup validates
|
||||
configuration and pings every connection before exposing any of the named pools.
|
||||
|
||||
@@ -32,6 +32,7 @@ import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import redis.clients.jedis.Protocol;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
@@ -146,6 +147,40 @@ class RedisTlsTest
|
||||
assertEquals("PONG", RedisRuntimeService.connection(instance, "presence").client().ping());
|
||||
assertEquals(com.kingsrook.qqq.backend.core.health.HealthStatus.UP, new RedisHealthCheck().run(
|
||||
new com.kingsrook.qqq.backend.core.health.HealthCheckInput().withQInstance(instance)).getStatus());
|
||||
|
||||
// Rotate both identities on the same endpoint; existing TLS sessions do not re-handshake.
|
||||
Path rotatedServer = Files.createDirectory(temporary.resolve("rotated-server"));
|
||||
Path rotatedClient = Files.createDirectory(temporary.resolve("rotated-client"));
|
||||
certificate(rotatedServer, identity);
|
||||
certificate(rotatedClient, "dns:rotated-client-fixture.invalid");
|
||||
redis.copyFileToContainer(MountableFile.forHostPath(rotatedServer.resolve("cert.pem"), 0444), "/rotated-server-cert.pem");
|
||||
redis.copyFileToContainer(MountableFile.forHostPath(rotatedServer.resolve("key.pem"), 0444), "/rotated-server-key.pem");
|
||||
redis.copyFileToContainer(MountableFile.forHostPath(rotatedClient.resolve("cert.pem"), 0444), "/rotated-client-trust.pem");
|
||||
var oldClient = RedisRuntimeService.connection(instance, "presence").client();
|
||||
var configured = oldClient.sendCommand(Protocol.Command.CONFIG, "SET",
|
||||
"tls-cert-file", "/rotated-server-cert.pem", "tls-key-file", "/rotated-server-key.pem",
|
||||
"tls-ca-cert-file", "/rotated-client-trust.pem");
|
||||
assertEquals("OK", new String((byte[]) configured, java.nio.charset.StandardCharsets.UTF_8));
|
||||
oldClient.getPool().clear();
|
||||
assertThrows(redis.clients.jedis.exceptions.JedisException.class, oldClient::ping,
|
||||
"A fresh connection with stale trust and client keys must fail");
|
||||
assertEquals(0, oldClient.getPool().getNumActive());
|
||||
var healthInput = new com.kingsrook.qqq.backend.core.health.HealthCheckInput().withQInstance(instance);
|
||||
assertEquals(com.kingsrook.qqq.backend.core.health.HealthStatus.DOWN, new RedisHealthCheck().run(healthInput).getStatus());
|
||||
|
||||
TrustedContext.context = clientContext(rotatedServer, rotatedClient);
|
||||
assertThrows(redis.clients.jedis.exceptions.JedisException.class, oldClient::ping,
|
||||
"Changing the provider alone must not be mistaken for live TLS reload");
|
||||
runtime.stop();
|
||||
runtime.afterApplicationStop();
|
||||
assertTrue(oldClient.getPool().isClosed());
|
||||
assertThrows(IllegalStateException.class, () -> RedisRuntimeService.connection(instance, "presence"));
|
||||
runtime.start(instance);
|
||||
var newClient = RedisRuntimeService.connection(instance, "presence").client();
|
||||
assertNotSame(oldClient, newClient);
|
||||
assertEquals("PONG", newClient.ping());
|
||||
assertEquals(com.kingsrook.qqq.backend.core.health.HealthStatus.UP, new RedisHealthCheck().run(healthInput).getStatus());
|
||||
assertEquals(0, newClient.getPool().getNumActive());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user