Skip to content

NATS: add inline credentials setting - #110733

Merged
alexey-milovidov merged 11 commits into
ClickHouse:masterfrom
addshore:nats-credentials
Aug 4, 2026
Merged

NATS: add inline credentials setting#110733
alexey-milovidov merged 11 commits into
ClickHouse:masterfrom
addshore:nats-credentials

Conversation

@addshore

@addshore addshore commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Changelog category (leave one):

  • New Feature

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Add nats_credentials setting to the NATS table engine, allowing users to specify NATS credentials inline as a string (matching the payload of a .creds file).


Detailed Description

This pull request adds support for specifying NATS credentials directly as a string via a new nats_credentials setting, in addition to the existing file-based option. It also ensures proper validation and updates the documentation and codebase accordingly.

New NATS credentials support:

  • Added a new nats_credentials setting to allow specifying NATS credentials content directly as a string (matching the payload of a .creds file with user JWT and seed).
  • Updated the connection logic in NATSConnection.cpp to use nats_credentials from memory if provided, falling back to nats_credential_file otherwise.

Validation and security:

  • Added a check in StorageNATS.cpp to ensure only one of nats_credential_file or nats_credentials can be specified at a time, throwing a BAD_ARGUMENTS exception if both are set.
  • Updated the value masking logic (SETTINGS_TO_HIDE in NATS_fwd.h) to include nats_credentials, ensuring sensitive data is not exposed in logs or system tables.

Documentation updates:

  • Updated user documentation (integrations/nats.md and nats.mdx) to describe the new nats_credentials parameter and its usage syntax.

Related issues
At least somewhat related to #85213

Validation

I noticed in #69396

The changes looks okay, however there is one issue: the tests for NATS were removed a while ago because they were flaky. Unfortunately those tests have to be added back in order to add new features to NATS. You can either take a look or wait for somebody else to add those tests back.

So I have tried to provide validation that can be reviewed alongside this change in this commit on my fork addshore@0649f97

It checks happy and sad paths, and validates the existing behavior of using a file as well as then passing that file content in directly as a string.

The "evidence" is in https://github.com/addshore/ClickHouse/blob/0649f97219a7d27c0398cc99dbbff034cf47e3bd/v/VALIDATION.md

Version info

  • Merged into: 26.8.1.788 (included in 26.8 and later)

@fm4v fm4v added the can be tested Allows running workflows for external contributors label Jul 22, 2026
@clickhouse-gh

clickhouse-gh Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [10838a5]

Summary:


AI Review

Summary

This PR adds inline nats_credentials support for the NATS table engine and the follow-up fixes needed to keep credential-source resolution unambiguous and secret masking consistent across SETTINGS, named-collection overrides, malformed engine arguments, and config fallback. After reviewing the current diff, the full prior discussion, and the latest green CI run, I did not find any remaining blockers or majors in the current PR head.

Final Verdict

✅ No remaining review findings in the current PR head.

LLVM Coverage Report

Metric Baseline Current Δ
Lines 86.50% 86.50% +0.00%
Functions 91.90% 91.90% +0.00%
Branches 78.70% 78.70% +0.00%

Changed lines: Changed C/C++ lines covered: 151/157 (96.18%) · Uncovered code

Full report · Diff report

@clickhouse-gh clickhouse-gh Bot added the pr-feature Pull request with new product feature label Jul 22, 2026
Comment thread src/Storages/NATS/StorageNATS.cpp Outdated
@alexey-milovidov alexey-milovidov self-assigned this Jul 30, 2026

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@addshore

Copy link
Copy Markdown
Contributor Author

I do have a potential patch for the AI/ CI reported potential trial issue, but I haven't pushed it as I was not yet find time to rebuild everything to validate it...

alexey-milovidov and others added 2 commits July 31, 2026 10:07
A table-level `nats_credential_file` or `nats_credentials` setting now
overrides both config-level sources, so a table with
`nats_credential_file` no longer silently authenticates with a
server-level `nats.credentials`. When both credential sources are set
in the server configuration and none in the table settings, throw
`BAD_ARGUMENTS` instead of silently preferring the inline credentials.

Addresses the AI review finding on ClickHouse#110733

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexey-milovidov
alexey-milovidov requested a review from a team as a code owner July 31, 2026 10:14
@clickhouse-gh clickhouse-gh Bot added the manual approve Manual approve required to run CI label Jul 31, 2026
Comment thread src/Storages/NATS/NATS_fwd.h
Comment thread src/Storages/NATS/StorageNATS.cpp Outdated
…amed-collection overrides

`NATS` takes its arguments as overrides of a named collection
(`ENGINE = NATS(collection, nats_credentials = '...')`), so the credentials can appear as
engine arguments and not only in the `SETTINGS` clause:

* `FunctionSecretArgumentsFinder` had no `NATS` branch, so `nats_password`, `nats_token`,
  `nats_credential_file` and `nats_credentials` passed as named overrides leaked verbatim into
  `SHOW CREATE TABLE` and the query log. Mask them there too, next to the `SETTINGS` clause
  masking done by `NATS::SETTINGS_TO_HIDE`.
* The mutual-exclusion check of `nats_credential_file` and `nats_credentials` ran on the raw
  `changed` bits after `loadFromNamedCollection`, so a collection providing one source plus a
  query override providing the other threw `BAD_ARGUMENTS` instead of letting the more specific
  query value replace the collection value. Resolve the effective source instead: a source
  specified in the query (`SETTINGS` clause or named-collection override) replaces the
  collection source, while two sources specified at the same level stay ambiguous.

Also drop the hand edit of `docs/reference/engines/table-engines/integrations/nats.mdx`: that
page is generated from the `Documentation` block of `registerStorageNATS`, which is updated in
this pull request, and a direct edit is rejected by the autogenerated-region guard of
`Docs check (Mintlify)`.
@alexey-milovidov

Copy link
Copy Markdown
Member

🕵 Pushed 85f922a1f2e: merged master (the branch was 670 commits behind) and addressed both AI review findings.

  • nats_credentials, nats_credential_file, nats_password and nats_token are now masked on the table-engine argument path as well (ENGINE = NATS(nats1, nats_credentials = '...')), via a new NATS branch in FunctionSecretArgumentsFinder. Covered by the unit test ParserCreateQuery.MaskNATSTableEngineCredentials.
  • The mutual-exclusion check no longer uses the raw changed bits: a credential source specified in the query (SETTINGS clause or named-collection override) now replaces a source stored in the named collection, and BAD_ARGUMENTS is raised only when both sources come from the same level. Covered by the new stateless test 04665_nats_credentials_named_collection.
  • Reverted the hand edit of docs/reference/engines/table-engines/integrations/nats.mdx — that page is generated from the Documentation block of registerStorageNATS, which this pull request updates, and the direct edit is what made Docs check (Mintlify) red.

On the remaining reds of the previous run, none of which are related to this change:

Comment thread src/Parsers/FunctionSecretArgumentsFinder.cpp Outdated
@clickhouse-gh

clickhouse-gh Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 86.50% 86.50% +0.00%
Functions 91.90% 91.90% +0.00%
Branches 78.70% 78.70% +0.00%

Changed lines: Changed C/C++ lines covered: 99/104 (95.19%) · Uncovered code

Full report · Diff report

pull Bot pushed a commit to erickirt/ClickHouse that referenced this pull request Aug 2, 2026
…berParser`

`Poco::strToInt` accumulated digits guarding only against `result > max / base`. With
`result == max / base` there is still room for only `max % base` in the last digit, so appending
a larger digit overflowed the accumulator. For a signed type that is undefined behavior, and the
wrapped-around value was returned as a successfully parsed number.

This is reachable from user input through `Poco::JSON`, which parses every integer with
`NumberParser::tryParse64`: a JSON number above the `UInt64` maximum was silently parsed as a
wrong value instead of being rejected, so
`SELECT formatQueryFromJSON('{"type":"Literal","value":{"field_type":"UInt64","value":18446744073709551617}}')`
returned `1`. The same reason made a `UInt64` literal above the `Int64` maximum fail to survive a
`parseQueryToJSON` / `formatQueryFromJSON` round trip: it wrapped to a negative number, and
`Poco::JSON` never reached its `parseUnsigned64` fallback.

The magnitude is now accumulated in the unsigned counterpart of the target type, both because the
carry check needs the exact remaining room for the last digit and because the magnitude of the most
negative value of a signed type does not fit into that type (`-9223372036854775808` parsed only by
accident of the overflow before).

Found by the AST fuzzer:
`runtime error: signed integer overflow: 9223372036854775800 + 9 cannot be represented in type 'long'`
in `Poco::strToInt<long>` via `Poco::JSON::ParserImpl::handle` and `DB::IAST::createFromJSON`.
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110733&sha=b94fe95fe4bb45b4c9f48dc6241e7ff0dd623581&name_0=PR&name_1=AST%20fuzzer%20%28arm_asan_ubsan%29
ClickHouse#110733
`findNATSTableEngineSecretArguments` only hid the named overrides listed in
`nats_secret_keys`, so `ENGINE = NATS(collection, nats_url =
'nats://user:password@host:4222')` leaked the password in `SHOW CREATE TABLE`
and in the query log, even though the `SETTINGS` clause form already masks
`nats_url` through `NATS::SETTINGS_TO_HIDE`.

The named overrides are now walked once: a `nats_url` value is masked with
`maskURIPassword`, keeping everything but the userinfo password visible, and a
key or a `nats_url` value that cannot be read as a plain literal is hidden
whole (fail closed) - an unevaluable key can name a secret setting, and a url
built from an expression can embed the credentials in its pieces.

Unit tests `ParserCreateQuery.MaskNATSTableEngineURLPassword` and
`ParserCreateQuery.MaskNATSTableEngineNonLiteralArguments`.

Also tag `04665_nats_credentials_named_collection` as `no-parallel,
no-replicated-database`: named collections are server-global, and the flaky
check runs the same test concurrently, so the first finishing repetition drops
the collections while the others still use them
(`NAMED_COLLECTION_DOESNT_EXIST`).
@alexey-milovidov

Copy link
Copy Markdown
Member

🕵 Pushed 5c8312eea34: merged master again and addressed the remaining AI review blocker plus the only related CI red.

  • nats_url is now masked on the table-engine argument path as well: ENGINE = NATS(collection, nats_url = 'nats://user:password@host:4222') prints nats://user:[HIDDEN]@host:4222, matching how NATS::SETTINGS_TO_HIDE masks the SETTINGS clause form. A key or a nats_url value that is not a plain literal is hidden whole (fail closed). Unit tests ParserCreateQuery.MaskNATSTableEngineURLPassword and ParserCreateQuery.MaskNATSTableEngineNonLiteralArguments.
  • 04665_nats_credentials_named_collection is tagged no-parallel, no-replicated-database. Named collections are server-global, and the flaky check runs the same test concurrently, so the first repetition to finish dropped the collections while the others were still using them, which is the NAMED_COLLECTION_DOESNT_EXIST seen in all four Stateless tests (..., flaky check) jobs. The regular stateless runs were green.

The remaining red of the previous run is unrelated to this change: Stress test (arm_release) aborted with Sizes of nested column and null map of Nullable column are not equal after deserialization inside MergeTreeIndexBulkGranulesSet::deserializeBinary - a stale set skip index read, #112213, with a fix in #106988. It has nothing to do with the NATS engine.

Comment thread src/Parsers/FunctionSecretArgumentsFinder.cpp
Comment thread src/Parsers/FunctionSecretArgumentsFinder.cpp
alexey-milovidov and others added 2 commits August 3, 2026 21:34
`formatForLogging` runs before the engine rejects the argument list, so a
malformed query such as `ENGINE = NATS(nc, '/var/nats.creds')` logged the
literal secret even though the engine later throws `BAD_ARGUMENTS`. The only
positional argument the engine accepts is the named-collection name in the
first position; any other non-`key = value` argument is now hidden whole
(fail closed). Unit test `ParserCreateQuery.MaskNATSTableEnginePositionalArguments`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexey-milovidov

Copy link
Copy Markdown
Member

🕵 Pushed 56ee2c1976e: merged master and addressed the remaining AI review blocker.

  • Malformed positional NATS(...) engine arguments are now hidden fail-closed in findNATSTableEngineSecretArguments instead of being skipped: ENGINE = NATS(nc, '/var/nats.creds') or ENGINE = NATS(nc, 'nats://user:password@host:4222') no longer log the literal secret before the engine throws BAD_ARGUMENTS. Only the named-collection identifier in the first position stays visible. Covered by the unit test ParserCreateQuery.MaskNATSTableEnginePositionalArguments; all four MaskNATS* parser tests pass locally.

The only red of the previous run is unrelated to this change: Stress test (arm_asan_ubsan)Hung check failed, possible deadlock found. The hung thread is inside Planner::buildPlanForQueryNode/FilterStep, nothing near the NATS engine, and the same hung check failed on dozens of unrelated pull requests and on master itself over the last two weeks (41 failures on 2026-08-03 alone, per CIDB).

Comment thread src/Storages/NATS/StorageNATS.cpp
alexey-milovidov and others added 2 commits August 4, 2026 17:13
The legacy Docusaurus tree docs/en was removed on master; the PR's edit of
docs/en/engines/table-engines/integrations/nats.md is dropped because the
content already lives in the embedded Documentation block of registerStorageNATS.
…ential source

The `credential_file_from_collection` / `credentials_from_collection` flags were
captured before `loadFromQuery`, so a `SETTINGS` assignment of the key the named
collection already provides was still classified as collection-sourced. As a result
`ENGINE = NATS(collection) SETTINGS nats_credential_file = '...', nats_credentials = '...'`
was accepted and `resolveCredentialSource` silently kept one source instead of
throwing `BAD_ARGUMENTS`. Clear the collection provenance for keys assigned in the
`SETTINGS` clause after `loadFromQuery`, and cover both mixed directions in
04665_nats_credentials_named_collection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexey-milovidov

Copy link
Copy Markdown
Member

🕵 Pushed 10838a560f5: merged master and addressed the remaining AI review blocker.

  • The conflict was on docs/en/engines/table-engines/integrations/nats.md: the legacy Docusaurus tree was removed on master, so the file is dropped — the documentation of nats_credentials already lives in the embedded Documentation block of registerStorageNATS.
  • Credential-source provenance is now recomputed after loadFromQuery: a SETTINGS assignment of a key the named collection already provides is query-level, so ENGINE = NATS(collection) SETTINGS nats_credential_file = '...', nats_credentials = '...' throws BAD_ARGUMENTS instead of silently keeping one source. Both mixed directions are covered in 04665_nats_credentials_named_collection.

The two CI reds on 56ee2c1976e were unrelated: 02932_refreshable_materialized_views_2 (msan flake) and the stress-test ColumnBLOB logical error (#113165).

@alexey-milovidov
alexey-milovidov added this pull request to the merge queue Aug 4, 2026
Merged via the queue into ClickHouse:master with commit 48ebef1 Aug 4, 2026
180 checks passed
@robot-ch-test-poll3 robot-ch-test-poll3 added the pr-synced-to-cloud The PR is synced to the cloud repo label Aug 5, 2026
-- A `SETTINGS` assignment of the key the collection already has is query-level as well,
-- so providing both sources through the `SETTINGS` clause is ambiguous in both directions.
CREATE TABLE nats_both_in_settings_over_file (key UInt64) ENGINE = NATS(04665_nats_credential_file)
SETTINGS nats_credential_file = '/var/other.creds', nats_credentials = 'user JWT and seed'; -- { serverError BAD_ARGUMENTS }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@groeneai if there is a user_files_path server setting limiting where files can be read from, is this going to throw an error? It shouldn't be allowed, I think

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No, it does not throw, and I agree it should not be allowed.

I checked master (604017b) and confirmed it empirically on a server with user_files_path set. nats_credential_file reaches libnats unvalidated: StorageNATS.cpp:119 expands the setting and stores it in the connection configuration, and NATSConnection.cpp:28 hands it straight to natsOptions_SetUserCredentialsFromFiles, which fopens the path (util.c nats_ReadFile). There is no fileOrSymlinkPathStartsWith / getUserFilesPath check anywhere on that path, unlike StorageFileLog.cpp:193 or StorageEmbeddedRocksDB.cpp:264.

Two observable consequences with user_files_path configured:

  1. Filesystem probe. The error text distinguishes the three cases, so CREATE TABLE becomes an existence/readability oracle for arbitrary absolute paths:

    • /tmp/does_not_exist -> error opening file '...': No such file or directory
    • /etc/shadow -> error opening file '/etc/shadow': Permission denied
    • /etc/passwd -> no nkey user seed found in '/etc/passwd'
  2. Content exfiltration to an attacker-chosen endpoint, since nats_url is also user-supplied. Against a listener of mine that advertises auth_required plus a nonce, a file outside user_files_path whose last line is a valid seed had its entire preceding content sent in the CONNECT frame's jwt field:
    "jwt":"TOPSECRET_DB_PASSWORD=hunter2\nTOPSECRET_TOKEN=ghp_abcdef\nSUACSSL3..."
    That comes from _getJwtOrSeed (conn.c) falling back to NATS_STRDUP of the whole buffer when no JWT header is found. A file with no seed at all fails before the CONNECT is sent, so the leak needs a seed line in the file, but the path itself is unrestricted.

Mitigation that does exist: NATS declares .source_access_type = AccessTypeObjects::Source::NATS (StorageNATS.cpp:941), so a user without the grant gets Code: 497 ... necessary to have the grant TABLE ENGINE ON NATS. So this needs the NATS source grant, which is why I read it as a privilege-boundary gap rather than an unauthenticated hole.

Note this is not from #110733. nats_credential_file was added in e822ba3 (Feb 2024) and b597a8597054fd3 (master before this merge) has the same unguarded SetUserCredentialsFromFiles call. #110733 added the inline nats_credentials alternative, which reads no file at all.

Unless you would rather own it, I will open a PR adding the ctor-time fileOrSymlinkPathStartsWith(..., getUserFilesPath()) guard, matching FileLog, with a stateless test asserting a path outside user_files_path is refused.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

cc @alexey-milovidov decide on this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I decided reverting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Understood.

One thing to flag before this closes: the nats_credential_file path gap is not from #110733, and the revert does not close it. The setting came in e822ba3a47dea82 (2024-02-02) and the merge parent b597a8597054fd3 has the same unguarded natsOptions_SetUserCredentialsFromFiles call. After the revert it is also the only way to pass NATS credentials, so restricting SQL-supplied paths the way StorageMySQL::getSSLParams does (e700bbec4c84c585) would leave no SQL alternative.

Should I restrict it anyway, or leave it as is? I am not opening anything until you say.

anandheritage pushed a commit to anandheritage/ClickHouse that referenced this pull request Aug 13, 2026
This reverts ClickHouse#110733 (merge
commit 48ebef1): the `nats_credentials`
setting of the `NATS` table engine, the mutual-exclusion check against
`nats_credential_file`, the masking of the `NATS` table-engine arguments
in `FunctionSecretArgumentsFinder`, and the tests
`04665_nats_credentials_named_collection` and `ParserCreateQuery.MaskNATS*`.

`src/Parsers/FunctionSecretArgumentsFinder.h` was resolved by hand: only
the `NATS` declarations are removed, while the TLS-credentials and
BigQuery argument finders added to the same lists afterwards are kept.

`docs/reference/engines/table-engines/integrations/nats.mdx` still
mentions `nats_credentials` inside its `{/*AUTOGENERATED_START*/}`
region. It is generated from the `Documentation` block of
`registerStorageNATS`, which this commit updates, and direct edits of a
generated region are rejected by the docs check, so the page is left to
the nightly documentation autogeneration.
alexey-milovidov added a commit that referenced this pull request Aug 13, 2026
…n file

`nats_credential_file` is a path on the server filesystem: the server opens it
with its own privileges, and during authentication the credentials are sent to
`nats_url`, which comes from the same query. So a path taken from SQL lets
anyone who can define a `NATS` source probe the local filesystem and
exfiltrate files the server can read to a NATS server they control:
#110733 (comment)

The path is now accepted only from a named collection defined in the server
configuration file, or as `nats.credential_file` in the server configuration
itself; every SQL spelling throws `BAD_ARGUMENTS` and points to the inline
`nats_credentials` setting instead. Loading from previously-validated metadata
(server startup, force-restore, and short-syntax `ATTACH`) is exempt, so
tables created before this restriction keep working after an upgrade.

This fixes the reason of the revert #114178, allowing this revert of the
revert: #114644

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors manual approve Manual approve required to run CI pr-feature Pull request with new product feature pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants