Skip to content

enhancement(tls): add configurable minimum and maximum TLS versions - #26265

Open
sainad2222 wants to merge 5 commits into
vectordotdev:masterfrom
sainad2222:tls-min-max-version
Open

enhancement(tls): add configurable minimum and maximum TLS versions#26265
sainad2222 wants to merge 5 commits into
vectordotdev:masterfrom
sainad2222:tls-min-max-version

Conversation

@sainad2222

Copy link
Copy Markdown
Contributor

Summary

Adds min_tls_version and max_tls_version to Vector's shared tls configuration block, so users
can constrain which TLS protocol versions Vector negotiates. Accepted values are TLSv1,
TLSv1.1, TLSv1.2 and TLSv1.3.

Today the negotiated version is fixed by the library defaults and cannot be changed:

  • Accepting connections. TlsSettings::acceptor builds from SslAcceptor::mozilla_intermediate,
    which is Mozilla's v4 intermediate profile. It permits TLS v1.0 and v1.1, and explicitly sets
    SSL_OP_NO_TLSv1_3, so TLS v1.3 is unavailable. (That helper is marked
    // FIXME remove in next major version in the openssl crate.)
  • Initiating connections. tls_connector_builder uses SslConnector::builder, which sets no
    minimum protocol version at all.

This is the problem reported in #11959: compliance scanners flag Vector's listening sources for
accepting TLS v1.0/v1.1, and there is no way to turn them off.

PR #17191 previously proposed swapping the acceptor for mozilla_intermediate_v5. It was closed with
the direction to make the behavior configurable and go through a deprecation process rather than
change it outright, which is the approach taken here: both options are unset by default, so
existing configurations negotiate exactly the same versions as before.

Implementation notes

Both the acceptor and the connector funnel through TlsSettings::apply_context_base, so
enforcement lives in one place — a new apply_protocol_versions — and every component that reads
the tls block picks it up.

The subtlety is that OpenSSL treats the SSL_OP_NO_* options as a veto that outranks
SSL_CTX_set_min_proto_version/set_max_proto_version. Because the acceptor profile hard-sets
SSL_OP_NO_TLSv1_3, setting the bounds alone would leave TLS v1.3 excluded, and
min_tls_version: TLSv1.3 would produce a context with no usable version at all. The option is
therefore cleared for every version inside the requested window before the bounds are applied. A
unit test pins this against the profile so it cannot regress silently.

An inverted range (min_tls_version greater than max_tls_version) is rejected at config load
rather than producing an unusable context.

Components that cannot honor these options

Some components accept a tls block but hand the PEM material to a third-party TLS stack instead
of applying it to an OpenSSL context, so these settings cannot take effect there. Rather than
ignore a security setting silently, they now warn when either option is set:

  • mqtt source and sink (rumqttc)
  • gcp_pubsub source (tonic)
  • greptimedb_metrics sink — folded into its existing unsupported-options warning; that sink
    destructures TlsConfig exhaustively, so it needed updating regardless.

Components that never read Vector's tls block (kafka via librdkafka, the AWS SDK-based sinks)
are unaffected and unchanged.

Deliberately not included

The deprecation half of the #17191 review — warning when a connection negotiates below TLS v1.2,
and eventually defaulting min_tls_version to TLSv1.2 — is a behavior change with release-timing
implications, so I left it out of this PR. Happy to add it here or as a follow-up, whichever the
maintainers prefer.

References

Closes: #11959
Related: #17191

Vector configuration

The new options on any component that exposes a tls block:

sources:
  demo:
    type: http_server
    address: 0.0.0.0:8080
    decoding:
      codec: json
    tls:
      enabled: true
      crt_file: /path/to/localhost.cert.pem
      key_file: /path/to/localhost.key.pem
      min_tls_version: "TLSv1.2"

sinks:
  out:
    type: blackhole
    inputs: [demo]

Omitting both options preserves today's behavior exactly. Setting min_tls_version: TLSv1.2 as
above refuses TLS v1.0/v1.1 handshakes and, as a side effect of the SSL_OP_NO_* handling
described above, makes TLS v1.3 available on the listener.

How did you test this PR?

Six tests added in lib/vector-core/src/tls/settings.rs, run with
cargo test -p vector-core --lib tls:: (27 passed, 0 failed):

  • an inverted version range is rejected, and equal bounds (pinning a single version) are accepted
  • each version deserializes from its config name (TLSv1, TLSv1.1, TLSv1.2, TLSv1.3) and
    renders back to it
  • no-regression guard: with neither option set, the context's options bitfield and min/max
    protocol versions are unchanged from the library defaults
  • with bounds set, they land on the context
  • starting from SslAcceptor::mozilla_intermediate — and asserting as a precondition that the
    profile really does set SSL_OP_NO_TLSv1_3 — applying a window that includes TLS v1.3 clears it
  • end-to-end over a real socket: a server pinned to min/max of TLS v1.2 negotiates exactly
    TLSv1.2 with a default client, and a client configured with min_tls_version: TLSv1.3 fails
    the handshake with a protocol version alert

Also run locally: make fmt, cargo clippy -p vector-core --all-targets (clean),
vdev check generated-docs, vdev check changelog-fragments, vdev check fmt,
vdev check markdown — all passing.

The behavior is reproducible by hand against the config above with:

$ openssl s_client -connect localhost:8080 -tls1_1   # refused
$ openssl s_client -connect localhost:8080 -tls1_2   # accepted
$ openssl s_client -connect localhost:8080 -tls1_3   # accepted

Note that some distributions set a system-wide MinProtocol in openssl.cnf, which can refuse
TLS v1.0/v1.1 before Vector is consulted; overriding that is necessary to observe the old
permissive default.

Is this a breaking change?

  • Yes
  • No

Both options default to unset, which preserves the currently negotiated versions exactly. This is
covered by the no-regression test listed above.

Does this PR include user facing changes?

  • Yes. Please add a changelog fragment based on our guidelines.
  • No. A maintainer will apply the no-changelog label to this PR.

Changelog fragment: changelog.d/11959_tls_min_max_version.enhancement.md. The generated Cue
reference is regenerated in a separate commit (74 files, additions only).

Contributor Guidelines

  • Please read our Vector contributor resources.
  • Do not hesitate to use @vectordotdev/vector to reach out to us regarding this PR.
  • Some CI checks run only after we manually approve them. To catch issues early, add a pre-push hook (template) or run the following locally before pushing:
    • make fmt
    • make check-clippy (auto-fix with make clippy-fix)
    • make test
  • After a review is requested, please avoid force pushes to help us review incrementally.
    • Feel free to push as many commits as you want. They will be squashed into one before merging.
    • For example, you can run git merge origin master and git push.
  • If this PR introduces changes Vector dependencies (modifies Cargo.lock), please
    run make build-licenses to regenerate the license inventory and commit the changes (if any). More details on the dd-rust-license-tool.

Vector's negotiated TLS versions were fixed by the library defaults and could
not be changed. `TlsSettings::acceptor` builds from
`SslAcceptor::mozilla_intermediate` -- Mozilla's v4 intermediate profile --
which permits TLS v1.0 and v1.1 and explicitly sets `SSL_OP_NO_TLSv1_3`, and
`tls_connector_builder` sets no minimum protocol version at all. This is the
compliance problem reported in vectordotdev#11959.

Add `min_tls_version` and `max_tls_version` to `TlsConfig`, accepting `TLSv1`,
`TLSv1.1`, `TLSv1.2` and `TLSv1.3`. Both are unset by default, so existing
configurations negotiate exactly the same versions as before. vectordotdev#17191 attempted
to fix this by moving the acceptor to `mozilla_intermediate_v5` outright and
was closed with the direction to make the behavior configurable instead.

Enforcement lives in `apply_context_base`, which both the acceptor and the
connector already funnel through, so every component that reads the `tls` block
picks it up. OpenSSL treats the `SSL_OP_NO_*` options as a veto outranking
`set_min_proto_version`/`set_max_proto_version`, so the option is cleared for
each version inside the requested window before the bounds are applied;
otherwise the acceptor's `SSL_OP_NO_TLSv1_3` would keep TLS v1.3 excluded and
`min_tls_version: TLSv1.3` would yield a context with no usable version.

Components that pass the certificates to a third-party TLS stack cannot honor
these options, so they warn rather than ignore them silently: the `mqtt` source
and sink, the `gcp_pubsub` source, and the `greptimedb_metrics` sink.
Regenerated by `make generate-component-docs` after adding `min_tls_version`
and `max_tls_version` to `TlsConfig`. Every component that exposes a `tls`
block gains the two options in its generated Cue reference.
@sainad2222
sainad2222 requested review from a team as code owners August 30, 2026 14:32
@github-actions github-actions Bot added docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. domain: sources Anything related to the Vector's sources domain: sinks Anything related to the Vector's sinks domain: external docs Anything related to Vector's external, public documentation domain: core Anything related to core crates i.e. vector-core, core-common, etc labels Aug 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4172f3bada

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread website/cue/reference/components/sinks/generated/kafka.cue
Comment thread website/cue/reference/components/sources/generated/nats.cue
Comment thread changelog.d/11959_tls_min_max_version.enhancement.md Outdated
Comment thread website/cue/reference/components/sources/generated/amqp.cue
Review found three more components that accept `tls.min_tls_version` and
`tls.max_tls_version` -- so the options appear in their generated reference --
but never apply them, because they hand the configuration to a third-party TLS
implementation rather than to an OpenSSL context:

- `kafka` source and sink: `KafkaAuthConfig::apply` maps only verification, CA,
  certificate, key and password onto librdkafka properties.
- `nats` source and sink: `from_tls_auth_config` applies only `require_tls`,
  root certificates and the client certificate pair.
- `amqp` source and sink: `AmqpConfig::connect` builds lapin's `OwnedTLSConfig`
  from `cert_chain` and `identity` alone.

None of these expose protocol version selection through the API surface Vector
uses, so the bounds cannot be forwarded. Since they are a security control,
warn rather than accept them silently. Adds a shared
`warn_unenforceable_protocol_versions` helper and a `TlsConfig` counterpart to
the existing `TlsSettings::has_protocol_version_bounds`, and routes the
previously inline `mqtt` and `gcp_pubsub` warnings through it so all six sites
emit one message.

Also corrects the changelog fragment. It claimed the AWS SDK-based sinks do not
read the `tls` block; they do, via `aws::connector` ->
`MaybeTlsSettings::tls_client` -> `build_tls_connector` ->
`tls_connector_builder`, which reaches `apply_context_base` and enforces the
bounds. It made the same claim about `kafka`, which reads the block and maps
its other fields but cannot map these two.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f887d07660

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread lib/vector-core/src/tls/settings.rs Outdated
`set_min_proto_version(None)` compiles to `SSL_CTX_set_min_proto_version(ctx, 0)`,
which does not mean "leave unchanged" -- 0 selects the lowest version OpenSSL
supports, clearing any bound already in force. That includes a bound applied
from the host's OpenSSL configuration (`MinProtocol` in `openssl.cnf`) when the
context is created.

Because both setters were called whenever either option was set, a config
specifying only `max_tls_version` erased a host-configured minimum and
re-enabled the very protocol versions that policy forbids. Call each setter
only when Vector has an explicit bound for that side.

The `SSL_OP_NO_*` handling had the same flaw: it cleared the flag for every
version inside the requested window, so `min_tls_version: TLSv1.0` would relax
`NO_TLSV1`/`NO_TLSV1_1` had anything else set them. Only `NO_TLSV1_3` is
cleared now, and only when the window contains TLS v1.3 -- that is the one flag
Vector's own acceptor profile (`SslAcceptor::mozilla_intermediate`) sets, so it
is the only one there is standing to undo.

Adds four tests: a pre-set bound on the opposite side survives a one-sided
config (both directions), externally disabled versions stay disabled, and a
window excluding TLS v1.3 leaves `NO_TLSV1_3` in place. They set the bound
explicitly rather than relying on host configuration, so they hold on machines
with no system-wide policy -- which is why the original tests missed this.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5be1ab6809

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +409 to +412
if let Some(max) = self.max_tls_version {
context
.set_max_proto_version(Some(max.as_ssl_version()))
.context(SetTlsVersionSnafu)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate the effective TLS protocol window

On hosts whose OpenSSL configuration supplies the opposite bound, this can silently create an unusable context: for example, a system MinProtocol = TLSv1.3 combined with only max_tls_version: TLSv1.2 leaves the builder with a minimum greater than its maximum, yet both OpenSSL setters succeed and Vector starts before every handshake fails. Fresh evidence in this revision is that the setters now preserve the opposite host bound, while the earlier range validation still compares only the two Vector options; validate the builder's effective minimum and maximum after applying them.

Useful? React with 👍 / 👎.

Comment on lines +728 to +732
min_tls_version: {
description: """
Minimum TLS protocol version to negotiate.

Peers that cannot negotiate at least this version are rejected during the handshake.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop advertising ignored Databend TLS bounds

When a Databend sink sets either newly documented bound, the value is silently ignored: a repo-wide search of src/sinks/databend finds that DatabendConfig::tls is only declared and is never read by validation or build code, so this sink never constructs TlsSettings or emits the unsupported-stack warning. Although the parent field is deprecated, the generated reference now explicitly promises that these accepted suboptions constrain negotiation; hide them for this component or warn that they have no effect.

Useful? React with 👍 / 👎.

Preserving a host-supplied bound instead of clearing it means Vector's own two
options can be self-consistent while the window actually in force is empty: a
host `MinProtocol = TLSv1.3` combined with `max_tls_version: TLSv1.2` leaves a
minimum above the maximum. Both OpenSSL setters report success, so Vector would
start and then fail every handshake, with nothing in the configuration to
explain why.

Read the builder's effective bounds back after applying and fail when the
minimum exceeds the maximum. The existing `InvalidTlsVersionRange` check only
compares the two Vector options and cannot see this, so the new
`EmptyTlsVersionWindow` error names the host's OpenSSL configuration as the
other possible source of a bound.

`SslVersion` is not ordered and does not expose its underlying value, so
comparison goes through a new `TlsVersion::from_ssl_version`; versions Vector
does not model are skipped rather than guessed at.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5599da78f2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// only one side silently re-enable versions the host policy forbids.
if let Some(min) = self.min_tls_version {
context
.set_min_proto_version(Some(min.as_ssl_version()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Intersect explicit bounds with existing host policy

When the host already enforces MinProtocol = TLSv1.2, configuring min_tls_version: TLSv1 replaces that minimum and re-enables versions the host forbids; max_tls_version can similarly raise an existing host maximum. Preserving only the unconfigured side is insufficient—the configured bounds should be intersected with the context's existing bounds so Vector cannot weaken system TLS policy.

Useful? React with 👍 / 👎.

Comment on lines +441 to +442
if self.window_contains(TlsVersion::Tls13) {
context.clear_options(SslOptions::NO_TLSV1_3);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Limit TLS 1.3 option clearing to acceptors

For outgoing connections on a host whose OpenSSL configuration sets NO_TLSV1_3, configuring a window that includes TLS 1.3 clears that host restriction because this helper is also called through apply_context(..., false). The clearing is only needed to undo Vector's mozilla_intermediate acceptor profile, so it should be restricted to server contexts rather than relaxing policy on every connector.

Useful? React with 👍 / 👎.

Comment thread src/amqp.rs
Comment on lines +68 to +69
if tls.has_protocol_version_bounds() {
vector_lib::tls::warn_unenforceable_protocol_versions("lapin");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit the AMQP warning once per component

When an AMQP sink configures either bound, this warning runs for every call to AmqpConfig::connect; the deadpool channel manager calls that method whenever it creates or replaces a pooled channel. Pool expansion and especially repeated channel recreation during broker failures can therefore emit the identical configuration warning continuously, so it should be emitted during component build/validation or otherwise rate-limited rather than from the connection hot path.

Useful? React with 👍 / 👎.

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

Labels

docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. domain: core Anything related to core crates i.e. vector-core, core-common, etc domain: external docs Anything related to Vector's external, public documentation domain: sinks Anything related to the Vector's sinks domain: sources Anything related to the Vector's sources

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Set min. TLS version

1 participant