ML Training Dataset Security Guard and Poisoning Prevention Gate
Audit ML training datasets for leaked credentials, API tokens, and covert prompt injection poisoning before fine-tuning.
Find ClickHouse dictionaries failing to load, reload them behind a dry-run and approval gate, then re-read system.dictionaries to prove each one loaded.
An external dictionary that stops loading does not take ClickHouse down. It sits in system.dictionaries with a last_exception. Every dictGet against it either throws or quietly serves the values from the last successful load. A dashboard shows labels that are weeks old, a join loses the rows added since, nothing surfaces the cause. This guard finds those dictionaries, prints the exception that broke each one, reloads them under a gate, then proves each one came back.
This blueprint was created by zkasuran.
Measured on ClickHouse 24.8 while building this.
status = 'LOADED'. Only last_exception changes. So a check written on status != 'LOADED' misses the dangerous case, which is the dictionary that looks healthy while serving stale values. This flow targets last_exception first for that reason.dictGet keeps answering from the last good load, with no warning. Measured: the source table was renamed away, the reload failed, dictGet still returned the old label while the source held a newer one.last_exception and moves last_successful_update_time, so the re-read is a real gate rather than a hopeful one.dry_run is true and auto_remediate is false, so a plain run reports the exceptions without touching anything.auto_remediate is true and dry_run is false. It then waits for a human to resume the Pause with approved set true. An unanswered gate reads as not approved.system.dictionaries is the real gate. A reload that fails again surfaces as a failed run, never as a false success.detect (io.kestra.plugin.jdbc.clickhouse.Query) reads system.dictionaries for the database and keeps every dictionary with a non empty last_exception, plus, when include_not_loaded is true, any whose status is not LOADED. It also reads element_count with last_successful_update_time to say whether the dictionary is still serving stale values or nothing at all.summarize (io.kestra.plugin.core.output.OutputValues) records the targeted names with the per dictionary exception once, so the log, the alert and the failure message all read the same value.log_detect (io.kestra.plugin.core.log.Log) prints the count, the names, the status of each one plus the exception text that broke it.guard (io.kestra.plugin.core.flow.If) branches on the targeted count. With none found, clean logs and the run ends.findings_alert (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts the failing dictionaries with their exceptions on every detection, in dry run and in remediate mode.mode (io.kestra.plugin.core.flow.If) enters the remediation branch only when auto_remediate is true and dry_run is false. Otherwise report_only lists what would be reloaded and stops.approval_gate (io.kestra.plugin.core.flow.Pause) holds the run until a human resumes it with approved. apply reloads only when approved is true.remediation_reload_loop (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs SYSTEM RELOAD DICTIONARY <database>.<name> once per targeted dictionary, with allowFailure so one reload that fails again does not stop the pass.reverify re-reads system.dictionaries for exactly those names. verify_result fails the run through io.kestra.plugin.core.execution.Fail if any of them still carries an exception or is still not LOADED.system.dictionaries, so it catches the dictionary that is quietly serving values from a load that happened days ago.LOADED with no exception.ClickHouse records the exception in system.dictionaries then leaves it there. It has no scheduler for this check, no approval step in front of SYSTEM RELOAD DICTIONARY, no way to prove a reload actually succeeded. Kestra adds the missing pieces. It sweeps on a schedule, shows a human the exception before anything is reloaded, gates the reload behind dry-run plus approval, re-reads system.dictionaries to turn a hopeful reload into a verified one, applies the same connection settings to every task then posts a Slack alert the moment a step fails.
Run on Kestra 2.0.5 against a ClickHouse 24.8 server, 12 executions covering every path. Two FLAT dictionaries were created over local MergeTree source tables with LIFETIME(MIN 0 MAX 0), both reaching LOADED with dictGet returning values. The source table behind one was then renamed away with SETTINGS check_table_dependencies = 0 and a reload forced, which left it carrying an UNKNOWN_TABLE exception while dictGet kept serving the old label. The second dictionary was left untouched throughout. Every number below was confirmed with clickhouse-client against system.dictionaries.
status = 'FAILED' with element_count = 0 then came back as the only row on the next run.dictGet returned widget-v3 where the source table held widget-v4, with last_successful_update_time sitting minutes in the past.element_count, last_successful_update_time plus the exception read identical afterwards.SYSTEM RELOAD DICTIONARY and the re-read reported 0 remaining. element_count went 2 to 3, last_successful_update_time moved, last_exception cleared, dictGet returned widget-v4 in place of widget-v3.UNKNOWN_TABLE exception. The re-read found it still failing so the run FAILED rather than reporting success.LOADED, left the third FAILED, then failed the run naming exactly that one.approved false reloaded nothing.last_successful_update_time did not move. With include_not_loaded false, three freshly restarted NOT_LOADED dictionaries returned 0, which is correct under lazy loading. Flipping include_not_loaded to true on the same server returned all 3. Scoping the run to one healthy dictionary returned 0 while a broken dictionary sat in the same database.A ClickHouse server reachable over the JDBC HTTP interface and a user that can read system.dictionaries and run SYSTEM RELOAD DICTIONARY on the target dictionaries.
CLICKHOUSE_USERNAME: ClickHouse user, read when use_secret_auth is true.CLICKHOUSE_PASSWORD: password for that user.SLACK_WEBHOOK_URL: used by findings_alert and the errors block.SECRET_.| Name | Type | Default | Description |
|---|---|---|---|
clickhouse_url |
STRING | jdbc:clickhouse://localhost:8123/default |
ClickHouse JDBC URL. |
use_secret_auth |
BOOL | true |
Read credentials from secrets. Set false only for inline test credentials. |
database |
STRING | default |
Database whose dictionaries are checked. |
dictionary |
STRING | "" |
Optional single dictionary filter. Empty checks every dictionary. |
include_not_loaded |
BOOL | false |
Also target dictionaries whose status is not LOADED. |
dry_run |
BOOL | true |
Report without reloading. |
auto_remediate |
BOOL | false |
Reload behind the approval gate when dry_run is false. |
CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD and SLACK_WEBHOOK_URL as namespace secrets.database pointed at a ClickHouse database that holds dictionaries. The default dry run reports which ones are failing plus why.dry_run false and auto_remediate true. Resume the paused execution with approved true.include_not_loaded true on a server with eager dictionary loading, where a NOT_LOADED status is itself a fault.element_count floor to the re-read, so a reload that succeeds against an empty source fails the run instead of silently emptying the dictionary.dictGet per dictionary after the reload, with the key and attribute for your own schema, to assert the value a dashboard will read.