Skip to main content
Version: V3

Secret redaction

Secret redaction​

Failure messages can include credentials from an adapter, an HTTP library or a configuration parser. When adding an error path, sanitize the text before returning it through an API, forwarding it to Prefect or storing it for remote inspection. Never include credentials in messages you write yourself.

The rules below describe the released redaction implementation. They apply to both registered service runs and direct execution, with different callers responsible for the final output.

Redact at the boundary, not at the source​

Keep failure translation at the boundary that exposes the message. Choose that boundary from the route you are changing:

RouteFailure handling and obligation
Registered CLI diff, sync, applyThe CLI calls SyncClient over HTTP. The client replaces transport failures with fixed errors and suppresses their causes. It constructs API errors from classification fields rather than returning the response's message text. Preserve those choices when adding client errors.
Sync HTTP APIservice/app.py formats error responses and handles unhandled request failures. RunService redacts provider observation text before returning run resources. Keep response and observation handling safe independently of worker exception handling.
Registered service workerservice_sync_run calls execute_run for its stages. Its failure path records available evidence, then raises a sanitized exception to Prefect. Extend this worker boundary when adding stage failures.
Direct Python and direct Prefectexecute_run preserves lifecycle exceptions; callers must sanitize them before exposing them remotely. The direct Prefect flow calls run_remote_request, which resolves a local configuration and wraps execution failures. This integration executes plans only.

See the CLI, HTTP client, API handlers, observation rendering and worker failure path for the implementations. See the shared execution surface for the operations each route supports.

The registered CLI is a remote API caller; having credentials in the local shell does not make a returned failure safe to display. The adapter rule still applies: never include credentials in logs or exceptions. Boundary redaction also replaces collected values in messages from dependencies that the adapter did not write.

Redact the whole cause chain​

A traceback can display both the wrapper message and the exception that caused it. Reuse the helpers in infrahub_sync/execution.py:

  • collect_secret_values gathers known values from the environment and, when supplied, a resolved SyncInstance.
  • redact replaces exact occurrences of the supplied values with ***.
  • sanitize_exception_chain rebuilds the displayed cause or context chain as RuntimeError copies, preserving the original type names in redacted messages and suppressing the original contexts.
# Bad: the traceback can still display the original cause.
raise RunExecutionError(redact(msg, secrets)) from exc

# Good: both the wrapper and the displayed cause are sanitized.
raise RunExecutionError(redact(msg, secrets)) from sanitize_exception_chain(exc, secrets)

When a cause adds no useful information, raise a safe summary from None instead. Test the full traceback.format_exception(...) output, including nested causes and implicit contexts, rather than checking only str(error).

run_remote_request uses this wrapping pattern for execution failures and passes RunValidationError through unchanged. Validation errors on that route must therefore be safe when raised. The service worker uses its own boundary, not run_remote_request.

Logs need separate handling. RunLoggerBridge redacts the formatted message from the infrahub_sync logger hierarchy before forwarding it to the Prefect run logger. The direct flow installs this bridge; the service worker installs it when a Prefect run context exists. This is not a filter for every logger, return value or artifact. See Prefect logging.

A boundary that catches Exception to sanitize and re-raise it may need a targeted # noqa: BLE001 with a comment explaining the translation. Limit that lint suppression to this boundary. It does not permit discarding a failure instead of re-raising it.

Collect from the environment by name shape — and from every value's URL userinfo​

Endpoint variables such as NETBOX_ADDRESS may contain credentials even though nothing in their names refers to authentication. The collector checks environment values in two ways:

  • A variable with a credential-shaped name contributes its whole value.
  • Every variable contributes any URL userinfo and password found in its value, regardless of the variable name.

Environment name matching is case-insensitive. A name qualifies when it contains TOKEN, PASSWORD, PASSWD, SECRET, CREDENTIAL or APIKEY, ends with _KEY or _AUTH, or equals KEY, AUTH or INFRAHUB_API_TOKEN.

URL userinfo is the username:password portion of scheme://username:password@host. The collector adds the whole userinfo string and the password separately, subject to the minimum length. It also scans string values in configuration settings for URL userinfo, except for entries under *_env_vars keys. Those entries are variable names, not strings to scan for userinfo. The collector reads the referenced values only when the key qualifies as a credential key, including through inherited context.

Match key names at a boundary, never as bare substrings​

KEY and AUTH match by suffix or exact name. Matching them anywhere in a name would also collect ordinary values from KEYCHAIN, SSH_AUTH_SOCK or these settings:

response_key_pattern: "objects"
auth_method: "api-key"

Redacting those values would obscure useful diagnostics, such as which authentication method requires a token.

For configuration settings, the collector converts each key to lowercase and checks whether it contains token, password, passwd, secret, credential, apikey or authorization, ends with _key or _auth, or equals key or auth.

Walk settings recursively, with a cycle guard and a depth cap​

Credentials can occur in nested source, destination and optional store settings. The shared collector applies these rules to all three settings blocks:

  • Inherited context. Values beneath a credential-shaped key remain candidates, including plainly named entries inside a credentials mapping.
  • Bounded traversal. The collector tracks (id(container), secret_context) to stop cycles while allowing the same YAML alias under both ordinary and credential-shaped keys. It stops descending into containers at depth 64. Values beyond that limit may be missed.
  • Supported scalars. The collector accepts strings and converts int, float and Decimal values to text. It skips None, boolean values and unsupported leaf objects.
  • Environment references. For a qualifying *_env_vars key, the collector reads the values of the named environment variables, not their names.

run_remote_request and service_sync_run collect environment values before configuration resolution, then refresh the collection with settings after resolution. Do not assume inline settings values have been collected before that refresh.

Drop values below a length floor​

collect_secret_values drops candidate values shorter than six characters. This avoids replacing every 1 in a diagnostic when a variable such as SKIP_TOKEN=1 qualifies by name. The returned values are ordered longest first so overlapping secrets are replaced in that order.

The cutoff is a collection limit, not a guarantee that short credentials are safe. A password shorter than six characters is not collected on its own. A longer URL userinfo string containing it may be collected, but the standalone password can remain visible elsewhere.

redact performs exact string replacement; it does not discover additional secrets or decode transformed values. Do not assume that an encoded value, a value under an unmatched settings key or a value omitted by the collection limits will be masked. Omit sensitive detail from new messages and test the values the changed route can expose.

Never chain a validation library's raw detail​

A validation error can include the input it rejected. Validation happens before a usable configuration exists. At that point, the collector has no settings values from the invalid file. It cannot reliably sanitize credentials it has not collected.

For direct configuration resolution, resolve_sync_instance reports the logical name and, for a matched invalid file, its path. It suppresses the original validation cause with from None. Preserve that summary instead of chaining Pydantic's raw detail. For HTTP request validation, the API returns a fixed request-invalid message rather than the rejected body. See the resolver and API handlers.

Anti-patterns​

Anti-patternDo instead
Sanitizing only the wrapper messageSanitize the displayed cause chain or suppress it with from None.
Treating a CLI failure as local engine outputTrace the HTTP client, API and worker boundaries separately.
Assuming collected values cover every credentialTest short, uncollected and transformed values at the changed output.
Relying on exception redaction for logsCheck the logger and forwarding path used by the failing code.
Returning validation detail from an invalid configurationReturn a bounded summary and suppress the original cause.

Verifying it​

Use synthetic credential values in the environment and configuration, then exercise a failure through the route you changed. Inspect every output that route exposes: rendered tracebacks, API responses, Prefect logs, state messages and retained failure records. A check of one wrapper string does not establish that the other outputs are safe.

Cover nested causes, URL userinfo in endpoint variables, nested settings and environment references. Check that ordinary diagnostics remain readable, and make the collection limits explicit in tests. Review changes to a collector against both missed values and unnecessary replacements.

Existing examples are in execution tests, service worker tests and direct Prefect tests. Follow the testing guidelines when changing those contracts.

See also​