Repository navigation
Conversation
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
|
The Python checkstyle failed. Please run You can install the pre-commit hooks with |
|
The Python checkstyle failed. Please run You can install the pre-commit hooks with |
|
The Python checkstyle failed. Please run You can install the pre-commit hooks with |
62b5fb9 to
53e8b9c
Compare
Map-valued connection properties (Kafka/Redpanda consumerConfig and schemaRegistryConfig, dbt dbtHttpHeaders, CustomLLM headers) were invisible to the secret framework: PasswordAnnotator only marked scalar format:password fields, and the SecretsManager/PasswordEntityMasker walkers had no branch for java.util.Map. Their values were stored unencrypted and returned unmasked on reads. Declare these maps as additionalProperties with format: password, teach the annotator to mark the generated getAdditionalProperties() accessor, and handle annotated maps entry-by-entry in the encrypt, decrypt, get-secret, delete and mask/unmask paths, keying each entry by its own key. The ingestion client resolves secret: references in the Kafka maps and unwraps dbt header values before use.
Masking every string entry of a password-valued map hid edits to non-secret entries (e.g. an OAuth token endpoint URL) from ConnectionSecretsGuard's masked comparison, so a stored map secret could be restored onto a redirected connection - the redirect ADR:2026-10-24 exists to block. The guard now treats a typed value at a masked password-map entry as a retarget unless it still equals the stored entry, scoped to map entries so re-entering a scalar secret stays accepted. Also record the map-of-secrets contract (per-entry masking, encryption and secret-id format, client-side resolution) as ADR:2026-10-30, and isolate KafkaConnectionMapSecretsTest from the SecretsManagerFactory singleton leaked by other tests.
…entry A typed value at a masked map entry is indistinguishable from a redirected setting, so the guard refuses it while other secrets stay masked - the same rule the unmasked maps produced before they were declared secret, and the only alternative is a per-key secret declaration. Pin the behavior in ConnectionSecretsGuardTest and name the cost in the ADR amendments.
53e8b9c to
74c9b61
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved flaws allow OAuth endpoint retargeting with restored credentials and external-secret overwrites from colliding header keys, while additional regressions alter empty headers and literal secret: credentials.
4 open issues
What changed in this PR
This PR extends OpenMetadata’s secret handling to map-valued connection properties, covering server-side protection and ingestion-time resolution for Kafka, Redpanda, dbt, and custom LLM headers.
Changes:
- Marks map values as passwords and adds per-entry encryption, masking, restoration, and deletion.
- Resolves secret references before Kafka connections and dbt HTTP requests, and extends the retarget guard.
- Adds regression tests and decision records documenting the map-secret contract.
| File | Description |
|---|---|
openmetadata-ui/src/main/resources/ui/src/jsons/ingestionSchemas/workflow.json |
Updates embedded messaging and dbt password-map schemas. |
openmetadata-ui/src/main/resources/ui/src/jsons/ingestionSchemas/testSuitePipeline.json |
Updates embedded messaging password-map schemas. |
openmetadata-ui/src/main/resources/ui/src/jsons/ingestionSchemas/dbtPipeline.json |
Marks dbt header values as passwords. |
openmetadata-ui/src/main/resources/ui/src/jsons/ingestionSchemas/dbtconfig/dbtHttpConfig.json |
Updates the dbt HTTP form schema. |
openmetadata-ui/src/main/resources/ui/public/jsons/connectionSchemas/connections/serviceConnection.json |
Updates bundled messaging connection schemas. |
openmetadata-ui/src/main/resources/ui/public/jsons/connectionSchemas/connections/messaging/redpandaConnection.json |
Declares Redpanda configuration maps as password-valued. |
openmetadata-ui/src/main/resources/ui/public/jsons/connectionSchemas/connections/messaging/kafkaConnection.json |
Declares Kafka configuration maps as password-valued. |
openmetadata-ui/src/main/resources/ui/public/jsons/connectionSchemas/connections/llm/customLLMConnection.json |
Marks custom LLM headers as passwords. |
openmetadata-spec/src/main/resources/json/schema/metadataIngestion/dbtconfig/dbtHttpConfig.json |
Defines password-valued dbt headers. |
openmetadata-spec/src/main/resources/json/schema/entity/services/connections/messaging/redpandaConnection.json |
Defines password-valued Redpanda configuration maps. |
openmetadata-spec/src/main/resources/json/schema/entity/services/connections/messaging/kafkaConnection.json |
Defines password-valued Kafka configuration maps. |
openmetadata-spec/src/main/resources/json/schema/entity/services/connections/llm/customLLMConnection.json |
Defines password-valued custom LLM headers. |
openmetadata-service/src/test/java/org/openmetadata/service/secrets/SecretsManagerLifecycleTest.java |
Tests map-entry storage, decryption, and deletion. |
openmetadata-service/src/test/java/org/openmetadata/service/secrets/masker/PasswordEntityMaskerTest.java |
Tests masking and restoration across affected configurations. |
openmetadata-service/src/test/java/org/openmetadata/service/secrets/masker/ConnectionSecretsGuardTest.java |
Tests map edits, masked echoes, and secret rotation. |
openmetadata-service/src/test/java/org/openmetadata/service/secrets/KafkaConnectionMapSecretsTest.java |
Tests Kafka map encryption and read masking. |
openmetadata-service/src/main/java/org/openmetadata/service/secrets/SecretsManager.java |
Adds per-entry secret lifecycle handling. |
openmetadata-service/src/main/java/org/openmetadata/service/secrets/masker/PasswordEntityMasker.java |
Visits and updates string-valued secret-map entries. |
openmetadata-service/src/main/java/org/openmetadata/service/secrets/masker/ConnectionSecretsGuard.java |
Detects edits hidden by blanket map masking. |
ingestion/tests/unit/test_dbt_http_config.py |
Tests header secret-reference resolution. |
ingestion/tests/unit/source/messaging/kafka/test_connection.py |
Tests configuration-map reference resolution and passthrough. |
ingestion/src/metadata/ingestion/source/messaging/kafka/connection.py |
Resolves map references before constructing clients. |
ingestion/src/metadata/ingestion/source/database/dbt/dbt_config.py |
Unwraps header secrets before HTTP requests. |
docs/decisions/2026-10-30-password-format-on-additionalproperties-makes-a-map-of-secrets.md |
Records map-secret behavior and tradeoffs. |
docs/decisions/2026-10-24-a-masked-connection-secret-is-kept-only-while-the-endpoint-is-unchanged.md |
Documents retarget rules for masked map edits. |
common/src/main/java/org/openmetadata/annotations/PasswordAnnotator.java |
Annotates generated password-map holders and accessors. |
Review effort: Balanced
|
…xternal stores Entry keys that normalize to the same secret id (X.Auth.Token and X_Auth_Token) are now refused with a 400 before any entry is written, instead of overwriting each other. An empty map entry stays an empty string while its previously stored secret is removed, rather than becoming null. A deliberate 400 from the secrets manager now reaches the caller instead of the generic Fernet-key message.
Only the mask, the stored value itself, or a value whose local cleartext equals the stored entry's now keep a map entry unchanged. A different secret: reference or a Fernet token of another value counts as an edit, so the guard asks for the secrets again.
…ovider The default DB provider never hands out references, so Kafka config map values and dbt HTTP headers that start with secret: are passed through as written instead of having the prefix stripped.
Code Review
|
| Compact |
|
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source
There was a problem hiding this comment.
🟡 Changes recommended
Removed map credentials leave orphaned external-store secrets even after hard deletion, while unchanged Fernet-wrapped references can incorrectly cause saves to return 400.
2 open issues
4 resolved since last review
Review effort: Balanced
| new ArrayList<>(secretMap.entrySet()) | ||
| .forEach( | ||
| entry -> { | ||
| if (entry.getValue() instanceof String stringValue) { |
| /** | ||
| * The cleartext a map entry holds when it can be recovered locally: the value itself for a | ||
| * plaintext entry (rows saved before maps were encrypted), a Fernet decrypt for a token. An | ||
| * external {@code secret:} reference, which the server never fetches, or a token that does not | ||
| * decrypt yields null: such a value cannot be shown to equal anything. | ||
| */ | ||
| private static String localCleartext(String value) { | ||
| if (value == null || value.startsWith(SecretsManager.SECRET_FIELD_PREFIX)) { |
|





Summary by Gitar
consumerConfigadditionalPropertiesholders inPasswordAnnotatorso secret maps are visible to walkersformat: passwordsecret:references in Kafka config maps before they reach librdkafkasecret:references resolve before requestsThis will update automatically on new commits.