Skip to content

IGNITE-28867 Implement TLS certificate hot redeployment via control.sh --ssl reload - #13335

Open
anton-vinogradov wants to merge 5 commits into
apache:masterfrom
anton-vinogradov:ignite-28867
Open

IGNITE-28867 Implement TLS certificate hot redeployment via control.sh --ssl reload#13335
anton-vinogradov wants to merge 5 commits into
apache:masterfrom
anton-vinogradov:ignite-28867

Conversation

@anton-vinogradov

Copy link
Copy Markdown
Contributor

Overview

Implements IGNITE-28867: hot redeployment of TLS certificates on running nodes, without a full restart.

TLS certificates were previously loaded once at node startup, so rotating them required restarting every node. This PR adds a new control.sh --ssl reload command that re-reads the configured key and trust stores from disk and replaces the active SSLContext. New connections use the updated certificates while established sessions are not interrupted.

Mechanism

  • AbstractSslContextFactory.reload() rebuilds the cached SSLContext from the current factory configuration (re-reading the key/trust stores from disk). On failure the previously cached context is kept intact.
  • GridNioSslFilter.sslCtx is now volatile with updateSslContext(...); new sessions build their SSLEngine from the new context, existing sessions keep their engines.
  • A new SslContextReloadable interface (reloadSslContext()) is implemented by each SSL-enabled transport.

Coverage per transport:

Transport How a reload is applied
Communication (node ↔ node) inbound — new context pushed into the NIO SSL filter; outbound — picked up automatically from the reloaded factory
Discovery (TCP) sslSockFactory / sslSrvSockFactory rebuilt
Thin client (client connector, JDBC/ODBC) new context pushed into the NIO SSL filter
Binary (TCP) REST new context pushed into the NIO SSL filter

Command

control.(sh|bat) --ssl reload

A new root --ssl command group with a reload sub-command. It broadcasts a VisorMultiNodeTask to all server nodes; each node reloads its own certificates locally and reports which transports were reloaded. Modelled after encryption change_master_key.

Known limitation (documented)

For the discovery transport, new outgoing connections use the reloaded certificates immediately, but the already-bound listening server socket keeps serving the previously loaded certificate to incoming connections until it is recreated (e.g. when the node re-enters the ring), because SSLServerSocket captures the SSL context at creation time. Recreating the discovery server socket at runtime is intentionally avoided as too disruptive for the ring.

The Jetty HTTP REST protocol manages its own SSL and is out of scope (the default GridRestProtocol.reloadSslContext() is a no-op).

Tests

  • SslContextFactoryReloadTest — a real loopback TLS handshake confirms that after the key store file is replaced on disk, reload() serves a different certificate, while create() keeps its caching semantics.
  • SslContextReloadNodeTest — a two-node SSL cluster runs the actual --ssl reload task; asserts communication/discovery/client-connector are reloaded and the cluster keeps operating (established sessions are not interrupted); a non-SSL node reports nothing to reload.

Both control.sh --help golden files were updated. Strict -Pcheckstyle passes.

Docs / Release Notes

  • docs/_docs/tools/control-script.adoc — new "TLS Certificate Hot Reload" section (Docs Required flag).

🤖 Generated with Claude Code

@anton-vinogradov
anton-vinogradov force-pushed the ignite-28867 branch 3 times, most recently from 2cc7619 to edaee64 Compare July 10, 2026 18:32
@anton-vinogradov

anton-vinogradov commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

/runall


🚀 RunAll queuedbuild 9200721. The verdict lands here when the run finishes.
🏁 Run finished — the verdict comment follows.

@anton-vinogradov

anton-vinogradov commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9200721 · 147 suites ran, 0 reused

⚠️ 1 broken suite(s) (no reliable run):

  • Platform .NET (Windows) 3: execution timeout · non-zero exit code

✅ No test blockers otherwise; 21 pre-existing/flaky filtered out.

♻️ Settled after 3 auto re-run wave(s): #1 — 2 blocker suite(s); #2 — 3 broken suite(s); #3 — 1 broken suite(s).

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9205392 · 147 suites ran, 0 reused

No blockers — nothing in this run looks caused by this PR. 31 pre-existing/flaky tests filtered out.

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9206015 · 17 suites ran, 130 reused

No blockers — nothing in this run looks caused by this PR. 10 pre-existing/flaky tests filtered out.

@anton-vinogradov

anton-vinogradov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9215889 · 147 suites ran, 0 reused

🔍 1 suite(s) ran fewer tests than on master (tests that never ran can't fail):

  • Thin Client: Java: 57 tests vs 439 on master (−87%)

✅ No test blockers otherwise; 26 pre-existing/flaky filtered out.

♻️ Settled after 1 auto re-run wave(s): #1 — 1 broken suite(s).

@anton-vinogradov
anton-vinogradov force-pushed the ignite-28867 branch 13 times, most recently from 1b83933 to 16b6dfa Compare July 25, 2026 21:16
@anton-vinogradov

anton-vinogradov commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

/runall


🚀 RunAll queuedbuild 9229101. The verdict lands here when the run finishes.
🏁 Run finished — analysing; the verdict comment follows.


🚀 RunAll queuedbuild 9232130 · live progress & verdict: Ignite PR Checker. The verdict lands here when the run finishes.
🏁 Run finished — the verdict comment has the full story.

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9232130 · 16 suites ran, 131 reused

No blockers — nothing in this run looks caused by this PR. 14 pre-existing/flaky tests filtered out.

@anton-vinogradov

anton-vinogradov commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

/run-all


🚀 RunAll queuedbuild 9235255 · live progress & verdict: Ignite PR Checker. The verdict lands here when the run finishes.
🏁 Run finished — the verdict comment has the full story.

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9235255 · 147 suites ran, 0 reused

No blockers — nothing in this run looks caused by this PR. 24 pre-existing/flaky tests filtered out.

…deployment

Reload TLS certificates on running nodes without a restart:

  control.sh --ssl reload   re-reads the configured key and trust stores and
                            puts them in use
  control.sh --ssl ensure   reports whether they can be put in use, without
                            changing anything

Covers the SSL-enabled transports of a server node: communication, discovery,
client connector and binary (TCP) REST. Connections opened after a reload use
the updated certificates, established sessions are not interrupted.

Discovery now binds a plain server socket and secures every accepted connection
with the context current at that moment, so reloaded certificates apply to
incoming connections as well.

Components register themselves in GridInternalSubscriptionProcessor once they
have set SSL up, so the commands do not reach into the transports.

Before a rebuilt context replaces the one in use on communication and discovery,
it has to complete a mutually authenticated TLS handshake in memory. A
certificate that the node's own trust store rejects therefore never reaches the
inter-node transports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hearse it

The reload now covers what connects to the cluster as well as the cluster
itself:

  control.sh --ssl reload [--dry-run]

--ssl ensure is gone: a dry run is a flag on the reload, so both forms take the
same path and cannot drift apart. The command reaches client nodes too, and a
node that leaves while it runs is reported as having left rather than failing
the run. Every node answers with what it reloaded and the certificate it serves
now, so a partial rotation is visible instead of silent.

HTTP REST is reloaded through its Jetty connector when the configuration points
at key store files. Jetty rebuilds in place and has no rollback of its own, so
the stores are verified on a throw-away factory first, and the context in use is
pinned back if the rebuild fails regardless.

A thin client rebuilds its context from disk after a refused TLS handshake, so
an application locked out by a rotation recovers once the right files are placed
next to it. A connection that merely dropped does not count: that happens on
every node restart, and rebuilding there would also pick up a key store that was
staged but is not meant to be presented yet.

For that to be possible at all, the reason a handshake failed now reaches
whoever waits on it. It used to be replaced by a generic "connection closed",
which a plain network drop produces just as well, so the real cause was lost to
callers and to the log alike. The field it was taken from was shared by every
session of a filter and never reset, which could attribute one session's failure
to another.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
anton-vinogradov and others added 3 commits July 31, 2026 00:48
Rotating a node certificate does not need a new key pair: the key the node
already runs on stays, and only the certificate on it changes. The steps showed
keytool -genkeypair and a signing request instead, which is both more work than
the rotation needs and the least likely way a certificate is issued in a
deployment that has a certificate authority to begin with.

The import is also one command rather than two whenever the authority returns
the whole chain, which is the usual case; a reply carrying the node certificate
alone is the one that needs the authority imported first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ry refused

The names components register under are also what the reload command reports,
so they are what an operator reads and scripts against. They were string
literals in five places and in the tests; SslContextReloadable now declares
them.

Discovery does not go through GridNioSslFilter, so it never got the hint the
other transports got when a handshake is refused. Its own message guessed one
cause and, during a rotation, the wrong one: it blamed missing SSL on the
remote node, while the node in fact presents a certificate this one no longer
trusts. It now carries the reason the TLS engine reported, and the rule a
rotation has to follow. Discovery is the transport where this matters most: a
refusal there is what drops a node out of the ring.

Tests: a second rotation is now covered, because nothing checked that a
component compares against the context in use rather than the one it started
with. Two assertions were satisfied by the report line that says the opposite
of what the test wanted ("not reloaded" contains "reloaded"), and one accepted
any exception at all; all three are anchored now. The rest is removal: two
nested factory classes that a lambda expresses, a temporary file that was an
immutable copy of an immutable resource, a hardcoded Jetty port that the
protocol is free not to bind, and assertions that could not fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Possible compatibility issues. Please, check rolling upgrade cases

This PR modifies protected classes (with Order annotation).
Changes to these classes can break rolling upgrade compatibility.

Affected files:

  • modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadCommandArg.java

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9241877 · 147 suites ran, 0 reused

No blockers — nothing in this run looks caused by this PR. 24 pre-existing/flaky tests filtered out.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant