-
Notifications
You must be signed in to change notification settings - Fork 3.3k
feat(ingestion): Add secret masking framework #15188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat(ingestion): Add secret masking framework #15188
Conversation
kyungsoo-datahub
commented
Nov 3, 2025
- Thread-safe SecretRegistry with copy-on-write pattern
- Logging layer masking filter
- Bootstrap initialization for different contexts
- CLI integration
- Unit and integration tests
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (69.49%) is below the target coverage (75.00%). You can increase the patch coverage or adjust the target coverage. 📢 Thoughts on this report? Let us know! |
| """Ingest metadata into DataHub.""" | ||
|
|
||
| # Initialize secret masking (before any logging) | ||
| initialize_secret_masking() |
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.
does this mean that the masking only happens in datahub ingest command?
instead, shouldn't we make it by default for all output when using the python sdk?
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.
Some downstream libraries was stuck when I broaden the scope of this change. We can broaden the scope on next phase.
| class ExecutionContext(Enum): | ||
| """Execution context for DataHub ingestion.""" | ||
|
|
||
| CLI = "cli" | ||
| UI_BACKEND = "ui_backend" | ||
| REMOTE_EXECUTOR = "remote" | ||
| SCHEDULED = "scheduled" | ||
| UNKNOWN = "unknown" |
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.
I understand different context will have different sources of secret. So far, cli masks env vars. Remote executor will mask secrets from secret store, and so on.
Wondering if being aware of the context is responsibility of the masking component or instead the target component should be responsible to register the secrets?
Basically, I would like to reduce the coupling of this masking component; ideally, it should only depend on logging.
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.
Thank you for the comment. I removed contexts and sources as we discussed.
| uninstall_masking_filter, | ||
| ) | ||
| from datahub.ingestion.masking.secret_registry import SecretRegistry | ||
|
|
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.
we could have some mechanism (env var) to disable masking
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.
I added the flag to disable this.
|
|
||
| # Detect UI backend | ||
| if os.environ.get("DATAHUB_UI_INGESTION") == "1": | ||
| return ExecutionContext.UI_BACKEND |
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.
UI_BACKEND
SCHEDULED
what are those context? may both refer to ingestion from managed ingestion ui?
managed ingestion can be scheduled or not, is that relevant for the masking?
ingestion can be triggered from managed ui or cli, shouldn't we also cover the second?
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.
Removed. Thanks
|
|
||
| # Detect remote executor | ||
| if os.environ.get("DATAHUB_EXECUTOR_ID"): | ||
| return ExecutionContext.REMOTE_EXECUTOR |
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.
is it relevant for the masking whether remote or embedded executor?
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.
I removed following our discussion.
| Args: | ||
| context: Execution context (auto-detected if None) | ||
| secret_sources: List of sources to load (auto-detected if None) | ||
| max_message_size: Maximum log message size before truncation |
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.
truncating log... that's sort of new feature, is that required?
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.
After the revision, it would be truncated only when log masking is enabled. FYI, all debug logs will be enabled in debug mode and log masking would be disabled in debug mode.
| # Disable HTTP debug output (prevent deadlock) | ||
| try: | ||
| import http.client | ||
|
|
||
| http.client.HTTPConnection.debuglevel = 0 | ||
| except Exception: | ||
| pass | ||
|
|
||
| # Set HTTP-related loggers to INFO (not DEBUG) | ||
| for logger_name in [ | ||
| "urllib3", | ||
| "urllib3.connectionpool", | ||
| "urllib3.util.retry", | ||
| "requests", | ||
| ]: | ||
| try: | ||
| logging.getLogger(logger_name).setLevel(logging.INFO) | ||
| except Exception: | ||
| pass |
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.
do we lose ability to have debug logs from these 3rd party libs?
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.
After the revision, the 3rd party libs debug log would be showing up when log masking is disabled.
| elif source == "datahub_secrets_store": | ||
| # TODO: Implement when backend API available | ||
| logger.debug("DataHub secrets store not yet implemented") | ||
| else: |
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.
what about SecretStr from configs? are we covering them?
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.
Good point. I added them. Thanks.
| # Note: Never add variables containing secrets, passwords, keys, or tokens. | ||
| # Examples to NEVER add: AWS_ACCESS_KEY_ID, DATABASE_PASSWORD, API_KEY, | ||
| # SECRET_KEY. If unsure, don't add it. | ||
| _SYSTEM_ENV_VARS = { |
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.
We may rename with a more descriptive name such as:
DATAHUB_MASKING_ENV_VARS_ALLOWED
DATAHUB_MASKING_ENV_VARS_SKIPPED
...
or something like that
Additionally, we may have a mechanism to incorporate values without requiring a release:
DATAHUB_MASKING_ENV_VARS_ALLOWED_PATTERN
so we can provide a regex pattern to skip additional env vars
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.
Thank you for the suggestion. Revised.
sgomezvillamor
left a comment
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.
I left some comments
My main concern is on the context because it will couple this masking components to eg datahub secret store and so on 🤔
ed608a2 to
ebd9743
Compare
|
✅ Meticulous spotted 0 visual differences across 1038 screens tested: view results. Meticulous evaluated ~8 hours of user flows against your PR. Expected differences? Click here. Last updated for commit 482d3f5. This comment will update as new commits are pushed. |
Bundle ReportBundle size has no change ✅ |
| if PYDANTIC_VERSION_2: | ||
| from pydantic import SecretStr, model_validator | ||
| else: | ||
| from pydantic import SecretStr, root_validator |
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.
I recently merged #15057
So there should be no more pydantic v1 root_validator or validator
We should stop introducing pydantic v1 compatibility code and just focus (assume) python v1
And if we find some component still supporting pydantic v1 in codebase, masking will only work if pydantic v2 at runtime.
We need to move and push towards pydantic v2 only.
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.
Thank you for the info. After rebasing master, the problem with Pydantic 1 is gone. I removed.
| try: | ||
| from datahub.masking.secret_registry import SecretRegistry | ||
|
|
||
| _MASKING_AVAILABLE = True | ||
| except ImportError: | ||
| _MASKING_AVAILABLE = False |
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.
In which scenario this import will fail?
IMO the optional dependency is not necessary
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.
As per my understanding:
- there should be no code dependant on imports availability
- if we have some conditional code, it should be dependant on
is_masking_enabled
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.
Make sense. I removed this. Thanks.
b224932 to
f63874a
Compare
0787e93 to
a8c5618
Compare
a8c5618 to
06fa600
Compare
|
As we discussed last week, I wonder if this environment variable name regexp matching is a bit overkill. I feel like with that we could remove most of the inefficient part of the code. Wdyt? |
Thank you for the review and comment. We still need the regex-based logging filter. While SecretStr protects direct logging, it can't prevent leaks in:
The logging filter provides defense-in-depth by catching secrets regardless of how they leak into logs or exceptions. I removed should_mask_env_var as we are registering only on recipe file. |
06fa600 to
ba7c5ba
Compare
- Add test for __init__.py imports to cover module exports - Add tests for is_bootstrapped() and get_bootstrap_error() functions - Add test for initialization with masking disabled - Add test for exception hook masking failure handling These tests improve coverage of edge cases and error paths in the masking framework.
af57963 to
482d3f5
Compare