GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [dbsp] Exchange data between hosts over TLS with client certificates by blp · Pull Request #7713 · feldera/feldera

Conversation

@blp

@blp blp commented Oct 29, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

The hosts of a multihost pipeline exchange data over raw TCP on port 9000. #7684 added client certificates (mTLS) to the HTTP servers, but not to exchange. With FELDERA_REQUIRE_CLIENT_CERTIFICATES (Helm mtls.enabled), this PR moves exchange to TLS, and each host admits only hosts of the same pipeline. Without it, exchange stays plaintext, as before.

Identity

An X.509 certificate lists the names that it is valid for in its "subject alternative name" (SAN) extension. Most entries are DNS names or IP addresses, but an entry can also be a URI. The runner gives each pipeline certificate the URI urn:feldera:pipeline:<pipeline id>, signed by the private CA, and feldera_mtls::Caller already reads it. On both ends of an exchange connection:

Check Why
The peer presents a certificate Both sides authenticate
The chain ends at the private CA, the first certificate after the leaf Same rule as the existing client certificate verifier: other CAs under the same root are not trusted
The peer's pipeline URI equals our own Refuses other pipelines, the control plane and external clients

Hosts connect by pod IP address, which no certificate lists, so the client cannot use rustls's built-in server verifier, which checks the DNS name or IP address. It uses a custom verifier, which rustls allows only through ClientConfig::dangerous(). The custom verifier does what the built-in one does without CRLs (chain, validity period, server auth EKU, handshake signatures), except that it checks the pipeline URI instead of the name. The first commit message explains why the alternatives that avoid dangerous() are worse.

Commits

Commit Change
[feldera-mtls] PipelinePeerTls::from_chain(chain, key) builds TLS 1.3 client and server configurations with the checks above
[dbsp] CircuitConfig::exchange_tls. The server now sends the exchange magic back, and the client waits for it while it connects. In TLS 1.3 the client finishes its handshake before the server checks the client certificate, so this reply is how the client learns that the server accepted its certificate. A refused certificate in either direction then fails connect and waits for the backoff, instead of reconnecting at once. The server does the handshake in the task for the connection, not in the accept loop. On both sides, the handshake and the magic exchange each time out after 10 s. Flushes after each message, ping, magic and acknowledgement, because tokio-rustls can hold back encrypted bytes until a flush. Plain TCP still uses into_split. The dev tweak tls_exchange, on by default, turns exchange TLS off without turning off mTLS elsewhere
[connectors] With client certificates required, the pipeline builds PipelinePeerTls from --https-tls-cert-path and --https-tls-key-path and passes it through the new CircuitParams::exchange_tls to the static engine. The three ControllerBuilder::open_* methods now share ControllerBuilder::finish, so that they pass on the same settings. Docs: HTTPS guide, Helm chart reference, changelog
[feldera-mtls] (review) Both verifiers refuse TLS 1.2 signatures, although the configurations already allow only TLS 1.3. Setup errors name the cause: a key that does not match the certificate, or a missing CA chain from caSecretRef
[connectors] (review) The panic for a bad exchange TLS setup names the certificate and key paths and where they come from

Tests

Each new check was disabled once to confirm that its test fails.

Test Fails when
feldera-mtls peer_tls_admits_only_own_pipeline: other pipeline, control plane, sibling CA, root CA, no certificate, with the client and the server side each checked alone the server identity check, the client identity check, the trust of only the first CA, or the required client certificate is removed
feldera-mtls peer_tls_refuses_tls12: TLS 1.2 as client and as server the configurations allow TLS 1.2
feldera-mtls peer_tls_needs_pipeline_chain, key case: a key from another certificate the error does not say that the key does not match
dbsp multihost_tls, multihost_mixed_size_tls, with fault injection, so reconnections go through the handshake. The servers count handshakes ExchangeClients ignores the configuration
dbsp tls_client_starts_with_handshake the client ignores TLS
dbsp tls_server_refuses_plaintext the server ignores TLS
dbsp client_connect_fails_when_server_refuses_certificate: a client without a certificate against a TLS server the client does not wait for the server's magic
dbsp client_connect_waits_for_server_magic the server does not send its magic back
dbsp server_closes_connection_without_magic (tests use a 2 s handshake timeout) the server reads the magic without a timeout
dbsp multihost_tls_exchange_off the runtime ignores tls_exchange
connectors open_passes_exchange_tls ControllerBuilder::finish drops the exchange TLS

All 37 dbsp operator::communication and multihost tests pass. Clippy is clean on the changed crates, and scripts/validate-crypto-deps.sh passes: tokio-rustls is added with default-features = false.

Not tested: a FIPS build (the configurations use the same crypto_provider() as the existing verifier, which the FIPS tests cover), an end-to-end run on Kubernetes (the cloud CI fresh install has mtls.enabled and runs 2-host pipelines, so the submodule bump will cover it).

Throughput

Release build, 2 hosts of 4 workers on localhost, fault injection off, 5 runs of each mode:

Workload Plaintext TLS Change
20,000 rounds of small messages 6,050 to 6,910 rounds/s 6,050 to 7,770 rounds/s none measurable
200 rounds of 1 MiB messages 1,107 to 1,193 MB/s 805 to 841 MB/s about 29% lower

The flushes add no measurable latency. Encrypting bulk data costs about 29% of throughput, which shows only when the network is faster than about 800 MB/s. Set dev_tweaks.tls_exchange to false to exchange data in plaintext.

Compatibility

  • Off unless client certificates are required, and then on unless dev_tweaks.tls_exchange is false. The cloud Helm chart already sets the variable and mounts the pipeline certificate and key, so it needs no change.
  • The server's magic reply changes the wire protocol, also without TLS, so the magic becomes feldera-xchg-v2. All hosts of a pipeline run the same image, so they agree on the protocol and on TLS.
  • CircuitParams has a new field. An engine that destructures it must name the field.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@blp blp self-assigned this Oct 29, 2026
@blp blp added DBSP core Related to the core DBSP library ft Fault tolerant, distributed, and scale-out implementation performance security Issues that are critical for security rust Pull requests that update Rust code enterprise Issue related to Feldera Enterprise features. multihost Related to multihost or distributed pipelines labels Oct 29, 2026
@blp
blp force-pushed the exchange-tls branch from 34f4588 to bc0e934 Compare October 29, 2026 19:18
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 29, 2026
View reviewed changes
Comment thread crates/dbsp/src/operator/communication/exchange.rs
let description = format!("{remote_address} for exchange with workers {remote_workers:?}");
loop {
let mut n_failures = 0u64;
let stream = loop {

@mythical-fred-oss mythical-fred-oss Bot Oct 29, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug (medium): a refused client certificate skips the backoff. In TLS 1.3 the client finishes its handshake before the server checks the client certificate. So when the server refuses our cert, connect() still returns Ok. The server's alert only shows up later, as an error from run_connection (a few lines below), and the outer loop then reconnects at once with no sleep(backoff_time()). That produces a hot loop of full handshakes and connection … dropped warnings, and the InvalidData warn branch here never fires for this case.

I confirmed this with a scratch test: a client with with_no_client_auth() against test_exchange_tls().0.server gives connect() = Ok, while the server gets Err(InvalidData, NoCertificatesPresented).

Suggested fix: back off when run_connection fails before anything has been received, or always sleep backoff_time() after a dropped connection. Please also add a test where the server refuses the client cert.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 29, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, you're right. The backoff only covered the client refusing the server's certificate, not the other way around.

Fixed in ab24b66. The server now sends the exchange magic back after it reads the client's, and ExchangeClient::connect waits for that reply, under the same 10 s timeout as the TLS handshake. With a refused client certificate, the server's alert arrives while the client waits for the reply, so connect fails with InvalidData. It then goes through the existing backoff and the warn branch, instead of the immediate reconnect after a dropped connection. The reply is also sent without TLS, so the magic is now feldera-xchg-v2.

New tests:

  • client_connect_fails_when_server_refuses_certificate: a client without a certificate against a TLS listener. connect fails with InvalidData, and a client with a certificate connects. The test fails if the client does not wait for the server's magic.
  • client_connect_waits_for_server_magic: fails if the server does not send the magic back.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Review: TLS for multihost exchange

What I ran

Command Result
cargo test --locked -p feldera-mtls 5/5 pass
cargo test --locked -p dbsp --lib operator::communication 25/25 pass
multihost_* TLS tests run 3 more times stable, about 10 s each
Scratch test: server refuses the client cert client connect() returns Ok, see the inline comment

Gates: tests, docs, crypto/FIPS (tokio-rustls with default-features = false, aws-lc provider) and commit messages all pass. There is no unsafe code and no flakiness risk that I could see.

Test cases the PR does not cover

  • missing (medium): the server refuses the client's cert while the client accepts the server's. This is the bug in the inline comment, and no test covers it.
  • missing (low-medium): the dbsp tests use a stock WebPkiServerVerifier with a 127.0.0.1 SAN, not PipelinePeerTls. So no test runs the real configuration end to end (pod IP as ServerName, the pipeline URI check) over ExchangeClient and ExchangeListener. The server.rs wiring from require_client_certificates to exchange_tls is also untested. A connectors-level test, or a PipelinePeerTls-backed multihost test in a crate that can depend on both, would close this.

Pitfalls

  • pitfall (medium): the exchange configuration requires both the serverAuth and clientAuth EKUs on the pipeline certificate. Please confirm that the runner-issued certificates carry both; the k8s CI run should show it.
  • pitfall (medium): the certificate and key are loaded once at startup. If they rotate, long-running pipelines keep the old certificate, and reconnects fail once it expires. Please document this or confirm it matches how the HTTPS server already behaves.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp force-pushed the exchange-tls branch from bc0e934 to 7839c17 Compare October 29, 2026 19:52
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 29, 2026
View reviewed changes
Comment thread crates/connectors/src/server.rs
.flatten()
.collect();
let key = PrivateKeyDer::from_pem_file(key_path).expect("HTTPS TLS key should be read");
let tls = PipelinePeerTls::from_chain(chain, key)

@mythical-fred-oss mythical-fred-oss Bot Oct 29, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pitfall (medium): the chain and key are read once at startup, and the peer verifier checks the validity period on every handshake. When cert-manager renews the pipeline certificate, the running process keeps the old one. Once it expires, every exchange reconnect (a network blip, or a peer pod restart) fails the handshake on both sides. The pipeline then stalls in a backoff loop and only logs warnings. For HTTPS this only breaks the API, but here it stops the data plane. Can you either reload the cert files per handshake, with a ResolvesServerCert/ResolvesClientCert that rereads them on change, or confirm that pipeline certificates always outlive a deployment? A test with an expired peer certificate would also help.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 29, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cert-manager does not issue pipeline certificates, only the control-plane client certificate. The runner issues a pipeline's certificate once, when the pipeline starts (cloud crates/cluster-control-plane/src/kubernetes_runner/runner.rs, construct_secret). The certificate is valid for 355 days (objects/secret.rs), and nothing renews it while the pipeline runs. So the files under /https-config do not change during a pipeline's life, and rereading them per handshake would get the same expired certificate.

The pipeline's HTTPS server and the coordinator already load the same certificate once, at startup. A pipeline that runs for 355 days without a restart therefore already loses its API when the certificate expires, and with this PR exchange also fails on its next reconnect. Renewing certificates of running pipelines would need the runner to reissue them and all three users to reload them. That is a separate change, so I left it out of this PR.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Ran (debug, locked)

Command Result
cargo test -p feldera-mtls ✅ 5/5, including peer_tls_admits_only_own_pipeline and peer_tls_needs_pipeline_chain
cargo test -p dbsp --lib operator::communication, run 6× to look for flakiness ✅ 27/27 every run, including all the TLS, handshake and magic tests
cargo check -p dbsp_connectors -p static-connectors ✅
pre-commit run --files <changed> ⏱️ timed out after 10 min while setting up hook environments here, so not verified

The gates pass: there are tests for the changed behaviour, docs and changelog are updated, there are no unsafe blocks, and tokio-rustls is MIT/Apache with default-features = false and uses the aws-lc provider. I found no flaky patterns: the timeouts are generous and the tests do not depend on them.

Missing (medium): uncovered cases

  1. An expired, or renewed but not reloaded, peer certificate. See the inline comment.
  2. The server.rs wiring has no test: require_client_certificates → PipelinePeerTls → CircuitParams → CircuitConfig. Nothing would catch it if with_exchange_tls were dropped from one of the three ControllerBuilder paths. The PR relies on cloud CI for this.
  3. A layout with TLS on some hosts and plaintext on others. Unit tests cover each direction, but no multihost test checks that such a layout fails loudly instead of hanging.
  4. The server has no timeout for reading the magic after the TLS handshake. HANDSHAKE_TIMEOUT covers only accept, so an authenticated peer that stalls keeps its task alive until a newer connection replaces it. This is low risk, since only the same pipeline can get that far, but the description says that the magic exchange times out on both sides.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp force-pushed the exchange-tls branch from 7839c17 to 769bbee Compare October 29, 2026 21:31
@blp

blp commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Thanks for the rereview. Pushed 769bbee:

Point Change
The server has no timeout on the magic after the TLS handshake Fixed in the dbsp commit. The server's magic read now uses the same timeout as the handshake, so the description's claim holds on both sides. This also closes plaintext connections that never send the magic. Tests use a 2 s timeout, and server_closes_connection_without_magic fails without the fix.
The server.rs and ControllerBuilder wiring is untested Fixed in the connectors commit. The three open_* methods now share ControllerBuilder::finish, and open_passes_exchange_tls checks that opening a controller carries exchange_tls into CircuitParams. It fails if finish drops it. open_checkpoint needs real storage, so the test covers the other two paths, and the shared finish covers the third.
A layout with TLS on some hosts and plaintext on others No change. Every host of a pipeline gets the same image and environment, so a mixed layout cannot happen. If it did, both directions fail in connect (unit-tested), the server logs a warning, and the client retries with backoff, as it does for any unreachable peer.
Both EKUs on runner-issued certificates Confirmed: cloud crates/cluster-control-plane/src/kubernetes_runner/objects/secret.rs sets ServerAuth and ClientAuth.
No end-to-end test with PipelinePeerTls below Kubernetes Covered by cloud CI: the fresh install sets mtls.enabled: true and runs the python-multihost suite with 2-host pipelines, so the submodule bump exercises the real certificates, pod IPs and the pipeline URI check.
Certificate rotation See the reply on the inline comment: pipeline certificates are issued once per start and are never renewed while the pipeline runs.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp requested a review from gz October 29, 2026 22:05
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 29, 2026
View reviewed changes
Comment thread crates/connectors/src/server.rs
.flatten()
.collect();
let key = PrivateKeyDer::from_pem_file(key_path).expect("HTTPS TLS key should be read");
let tls = PipelinePeerTls::from_chain(chain, key)

@mythical-fred-oss mythical-fred-oss Bot Oct 29, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pitfall (medium): the chain and key are read once at startup. The control plane rereads its renewed client certificate every 60 s (CLIENT_CERTIFICATE_RELOAD_INTERVAL in pipeline-manager config.rs), but this config never picks up the renewal. Once the pipeline certificate expires, both verifiers refuse every new handshake, so the next exchange reconnect (network blip, pod reschedule) retries forever and the multihost pipeline stalls. The pipeline HTTPS server also loads its certificate only once, but exchange makes it worse because exchange needs reconnects to recover. Could this use a reloading ResolvesServerCert/ResolvesClientCert, or can you confirm that pipeline certificates outlive any deployment? Then a test for an expired peer certificate on reconnect would be good too.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 30, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same point as the earlier thread, so see #7713 (comment). The 60-second reload in pipeline-manager is for the control plane's own client certificate, which cert-manager issues and renews. Pipeline certificates are different: the runner issues one when the pipeline starts, and nothing renews it while the pipeline runs. A reload would read the same file again, so a reloading resolver would not help. Renewing the certificates of running pipelines is a separate change, and it would cover the HTTPS server and the coordinator too.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 29, 2026
View reviewed changes
Comment thread crates/dbsp/src/operator/communication/exchange.rs
IoSlice::advance_slices(&mut bufs, n);
}

// TLS can hold back encrypted data until a flush, which would

@mythical-fred-oss mythical-fred-oss Bot Oct 29, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pitfall (medium): this puts TLS (encrypt plus a flush per message, ping and ack) on the hot data path of every multihost pipeline with mtls.enabled, and the PR says throughput has not been measured. There is also no way to turn exchange TLS off without turning off mTLS everywhere. Can you gate it behind a dev_tweaks opt-out (or opt-in at first) and run a quick plaintext vs. TLS exchange benchmark before this becomes the default for mTLS deployments?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 30, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 6267b2b: the dev tweak tls_exchange, on by default. Setting it to false makes exchange use plaintext even when the pipeline has client certificates, and leaves mTLS on for everything else. multihost_tls_exchange_off checks that no handshake happens with the tweak off, and it fails if the runtime ignores the tweak. The HTTPS guide and the changelog describe the tweak.

I measured it with a release build, 2 hosts of 4 workers on localhost, fault injection off, and 5 runs of each mode:

Workload Plaintext TLS Change
20,000 rounds of small messages 6,050 to 6,910 rounds/s 6,050 to 7,770 rounds/s none measurable
200 rounds of 1 MiB messages 1,107 to 1,193 MB/s 805 to 841 MB/s about 29% lower

So the flushes do not add measurable latency. The cost is the encryption of bulk data, and it shows only when the network is faster than about 800 MB/s. The handshake count stayed the same in every TLS run, so the connections were reused for the whole run.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

What I ran (debug, merge commit):

Check Result
cargo test -p feldera-mtls 5/5 pass
cargo test -p dbsp --lib operator::communication 28/28 pass, including multihost_tls and multihost_mixed_size_tls
refused-cert / plaintext / server-magic tests ×150–200, also on a saturated CPU 0 failures (I looked for an RST-before-alert race; none showed up)
cargo test -p dbsp_connectors controller_init_tests 7/7 pass
cargo check -p static-connectors, validate-crypto-deps.sh, pre-commit on changed files pass

Gates: tests, docs, changelog, commit messages, deps (tokio-rustls is MIT/Apache, default-features = false, aws-lc provider) all pass. No unsafe code and no workflow changes.

Not covered by tests (medium):

  1. A peer certificate that expires or gets renewed while the pipeline runs, followed by an exchange reconnect (see inline).
  2. Throughput with TLS compared to plaintext. No benchmark, and there is no opt-out short of turning off mTLS (see inline). This is the "risky feature behind a flag" gate.
  3. Only the in-memory feldera-mtls test covers an exchange-level refusal of a peer from another pipeline. No ExchangeClient/ExchangeListener test covers it, but the unit test is a reasonable substitute.

The design looks sound to me: the custom verifier checks only the CA and the URI SAN, and the server sends the magic back so a refused certificate fails connect and the client backs off. The two inline items are what I would like settled before merge.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

gz
gz approved these changes Oct 30, 2026
View reviewed changes
Comment thread crates/feldera-mtls/src/lib.rs Outdated
let provider = verifier.provider.clone();
let server = ServerConfig::builder_with_provider(provider.clone())
.with_protocol_versions(&[&rustls::version::TLS13])
.map_err(|e| format!("peer TLS server: {e}"))?

@gz gz Oct 30, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could make these more actionable, I wonder if they can fail for some setup problem (helm) etc. we can tell people where to look

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 30, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2b9fff0 and 832af79. The errors that a setup problem can cause now say what is wrong: a private key that does not match the pipeline certificate, or a certificate file without the CA chain, which the runner appends from the private CA in the Helm value caSecretRef. The panic in server.rs now names both file paths and says that the runner writes them from the pipeline certificate, which it signs with that CA. The TLS 1.3 error can only come from a code bug, so it just says that the crypto provider does not support TLS 1.3.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/feldera-mtls/src/lib.rs Outdated
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
verify_tls12_signature(

@gz gz Oct 30, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can just block 1.2?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 30, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2b9fff0. The configurations already negotiate only TLS 1.3, but the rustls traits require verify_tls12_signature. Both verifiers now refuse in it instead of checking the signature, so TLS 1.2 is blocked twice. peer_tls_refuses_tls12 checks that a host refuses TLS 1.2 as client and as server. When I let the configurations allow TLS 1.2, the test failed, and the handshake stopped at the new refusal in the verifier.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/feldera-mtls/src/lib.rs Outdated
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
self.client_verifier

@gz gz Oct 30, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same q

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 30, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same change, in 2b9fff0: the client certificate verifier refuses TLS 1.2 signatures too.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@blp
blp force-pushed the exchange-tls branch 2 times, most recently from 832af79 to 90f6ee6 Compare October 30, 2026 18:12
@blp
blp enabled auto-merge October 30, 2026 18:15
@blp
blp disabled auto-merge October 30, 2026 21:56
@blp
blp added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 31, 2026
@blp
blp enabled auto-merge November 2, 2026 17:44
@blp
blp disabled auto-merge November 2, 2026 18:28
@blp
blp force-pushed the exchange-tls branch from 955422d to ce610cd Compare November 2, 2026 18:33
@blp
blp enabled auto-merge November 2, 2026 18:33
@blp
blp disabled auto-merge November 2, 2026 19:24
@blp
blp added this pull request to the merge queue Nov 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 2, 2026
@blp
blp force-pushed the exchange-tls branch from ce610cd to 363f961 Compare November 3, 2026 17:48
@blp
blp added this pull request to the merge queue Nov 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 3, 2026
blp added 5 commits November 3, 2026 14:14
@blp
[feldera-mtls] Add TLS configurations for the hosts of a multihost pi…
4da9b2b
…peline.

The hosts of a multihost pipeline exchange data over raw TCP.  To move that
to TLS, each host needs a client and a server configuration that
authenticate the other host as a host of the same pipeline.

The identity to check is already in each pipeline certificate.  An X.509
certificate lists the names that it is valid for in its "subject
alternative name" (SAN) extension.  Most SAN entries are DNS names or IP
addresses, which name a network location, but an entry can also be a URI.
The runner gives each pipeline certificate the URI entry
`urn:feldera:pipeline:<pipeline id>`.  Because the private CA signs it, a
peer that completes a TLS handshake with that certificate proves that it
belongs to that pipeline, wherever it runs.  The control plane's
certificate has `urn:feldera:control-plane` instead.  `Caller` reads these
URIs, and the compiler, runner, pipeline and coordinator servers already
use it to admit clients.

`PipelinePeerTls::from_chain` builds both configurations from the pipeline
certificate chain and its key.  Both ends present the pipeline
certificate.  Both trust only the private CA, the first certificate after
the leaf, as the existing client certificate verifier does.  Both admit
only a peer whose certificate has the same pipeline URI, so the control
plane, external clients and other pipelines are refused.  The server
requires a client certificate.

The client needs a custom server certificate verifier, which rustls allows
only through `ClientConfig::dangerous()`.  The built-in verifier checks
that a server's DNS name or IP address entries include the name that the
client connected to.  Hosts connect by pod IP address, which no
certificate lists, and the pipeline URI is the identity that matters.  The
custom verifier does what the built-in one does without CRLs (chain to the
private CA, validity period, server auth extended key usage, handshake
signatures), except that it checks the pipeline URI instead of the name.
Two alternatives avoid `dangerous()`, but each is worse:

- Verify against a DNS name that matches the wildcard DNS entry of our own
  certificate, `*.<pipeline service>.<namespace>.svc.cluster.local`.  This
  makes the runner's DNS naming the identity, separate from the pipeline
  URI that the servers check.

- Verify against a name that every pipeline certificate has, such as
  `localhost`, and check the peer's pipeline URI after the handshake.  This
  splits verification in two, and the connection is only safe if every
  caller remembers the second check.

The tests run in-memory handshakes.  The client and the server side are
each checked on their own, against another pipeline, the control plane, a
sibling CA, the root CA, and no certificate.  Each test fails if the check
it covers is removed.

Signed-off-by: Ben Pfaff <[email protected]>
@blp
[dbsp] Exchange data between hosts over TLS, if configured.
475412a
`CircuitConfig::exchange_tls` holds rustls client and server
configurations.  When it is set, every exchange connection between hosts
starts with a TLS handshake, and the exchange magic and messages go inside
TLS.  Without it, there is no TLS.

The server now sends the exchange magic back after it reads the client's,
and the client waits for it before it uses the connection.  This is how
the client learns that the server accepted its certificate: in TLS 1.3,
the client finishes its handshake before the server checks the client
certificate, so a refused certificate shows up only when the client next
reads.  Without the reply, `connect` succeeded, the refusal arrived later
as a dropped connection, and the client reconnected at once, in a loop of
full handshakes.  With it, a refused certificate in either direction fails
`connect`, which waits for the backoff before it tries again and logs a
warning.  The reply is also sent without TLS, so the magic's version
becomes v2.

On both sides, the TLS handshake and the magic exchange each time out
after 10 seconds, so a peer that stalls, or a stray client that connects
and sends nothing, does not keep a task forever.  The server does the
handshake in the task for the connection, not in the accept loop, so a
slow client cannot hold up other connections.  It logs
refused and timed-out handshakes with the peer address, as it does for a
bad exchange magic.

tokio-rustls can return from a write before the encrypted bytes reach the
socket, so both sides flush after the magic, the client after each
message and each ping, and the server after each acknowledgement.  Without
TLS, a flush does nothing.  Plain TCP connections still use `into_split`.

The new dev tweak `tls_exchange`, on by default, turns exchange TLS off,
so that data goes in plaintext even when `exchange_tls` is set.  On
localhost, with 2 hosts of 4 workers, TLS did not change the rate of
rounds with small messages, and it lowered the throughput of 1 MiB
messages by about 29%, from about 1,150 MB/s to about 820 MB/s.

The tests run the multihost exchange tests over TLS with fault injection,
so reconnections go through the handshake.  A handshake counter shows that
TLS was used.  Other tests check that a TLS client does not send the magic
in plaintext, that a TLS server closes a plaintext connection, that
`connect` fails when the server refuses the client's certificate, that
`connect` succeeds once the server answers the magic, and that the server
closes a connection that sends no magic, and that exchange does not use
TLS with the dev tweak off.  Tests use a 2-second timeout.
Each of these tests fails if the behavior it checks is removed.

Signed-off-by: Ben Pfaff <[email protected]>
@blp
[connectors] Use TLS for multihost exchange with client certificates.
af7fdf4
With `--require-client-certificates` (`FELDERA_REQUIRE_CLIENT_CERTIFICATES`,
Helm `mtls.enabled`), the pipeline loads its certificate and key from
`--https-tls-cert-path` and `--https-tls-key-path` and builds
`PipelinePeerTls`.  The controller passes it to the engine in the new
`CircuitParams::exchange_tls`, and the static engine sets it in dbsp's
`CircuitConfig`.  So the hosts of a multihost pipeline exchange data over
TLS and admit only hosts of the same pipeline.  Without client
certificates, exchange stays plaintext.

`CircuitParams` carries the `feldera-mtls` type, not the dbsp one, because
the engine interface does not depend on dbsp.  An engine that destructures
`CircuitParams` must name the new field.

The three `ControllerBuilder::open_*` methods each passed the builder's
settings on to `ControllerInit` with the same chain of calls.  They now
share `ControllerBuilder::finish`, so that a setting cannot reach some of
them and not others.  A new test checks that opening a controller passes
the exchange TLS on to the circuit, and fails if `finish` drops it.

Document it in the HTTPS guide, the Helm chart reference and the changelog.

Signed-off-by: Ben Pfaff <[email protected]>
@blp
[feldera-mtls] Refuse TLS 1.2 between hosts and explain setup errors.
89b92f5
The peer configurations negotiate only TLS 1.3, but the rustls verifier
traits still require `verify_tls12_signature`, and both verifiers
implemented it by checking the signature.  Now both refuse, so a TLS 1.2
handshake fails even if the protocol versions change.

`PipelinePeerTls::from_chain` errors now say what is wrong in the setup:
a private key that does not match the pipeline certificate, or a
certificate file without the CA chain, which the runner appends from the
private CA in the Helm value `caSecretRef`.

`peer_tls_refuses_tls12` checks that a host refuses TLS 1.2 as client
and as server.  It fails if the configurations allow TLS 1.2.
`peer_tls_needs_pipeline_chain` now also checks the error for a key that
does not match the certificate.

Signed-off-by: Ben Pfaff <[email protected]>
@blp
[connectors] Say where exchange TLS files come from when they are wrong.
c0fcb7e
When the exchange TLS configuration cannot be built, the panic now names
the certificate and key paths, and says that the runner writes them from
the pipeline certificate, which it signs with the private CA in the Helm
value `caSecretRef`.

Signed-off-by: Ben Pfaff <[email protected]>
@blp
blp force-pushed the exchange-tls branch from 363f961 to c0fcb7e Compare November 3, 2026 22:24
@blp
blp enabled auto-merge November 3, 2026 22:25
@blp
blp disabled auto-merge November 3, 2026 23:15
@blp
blp added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit f90776a Nov 4, 2026
1 check passed

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp deleted the exchange-tls branch November 4, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@mythical-fred-oss mythical-fred-oss[bot] mythical-fred-oss[bot] left review comments
@gz gz gz approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@blp blp

Labels

DBSP core Related to the core DBSP library enterprise Issue related to Feldera Enterprise features. ft Fault tolerant, distributed, and scale-out implementation multihost Related to multihost or distributed pipelines performance rust Pull requests that update Rust code security Issues that are critical for security

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@blp @gz
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.