Skip to content

feat(security): mask opaque credential values in log output - #77020

Open
andrexibiza wants to merge 2 commits into
NousResearch:mainfrom
andrexibiza:fix/security-redact-passwords-and-bws-token
Open

feat(security): mask opaque credential values in log output#77020
andrexibiza wants to merge 2 commits into
NousResearch:mainfrom
andrexibiza:fix/security-redact-passwords-and-bws-token

Conversation

@andrexibiza

@andrexibiza andrexibiza commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Related #77008 #77012 #77014

What changed and why

RedactingFormatter (used by every Hermes log handler) already ran the shape-based regex redactor — vendor prefixes (sk-, ghp_), auth headers, URLs — on every log record. But opaque credential values with no recognizable prefix passed through log output unmasked: MY_SERVICE_TOKEN=abc123randomstring, BWS_ACCESS_TOKEN, *_PASSWORD values.

This PR adds an exact-value pass to RedactingFormatter.format() that masks the literal values of credential-named env vars (*_API_KEY, *_TOKEN, *_SECRET, *_KEY, *_PASSWORD). The change is self-contained — it does not depend on any other PR.

Why this matters to you as a user

A log line that happens to include a secret's value — an error message echoing a token, a debug line printing a connection string — no longer writes that value into your log files. The diagnostic text survives; the secret doesn't. This closes the value-in-logs half of the disclosure class for every Hermes user, not just BWS users.

Reproduction steps (current behavior on main)

  1. export MY_SERVICE_TOKEN=opaque-secret-value-xyz
  2. Log a message containing that value (e.g. logger.warning("starting with opaque-secret-value-xyz")).
  3. Observe the gateway/log file — the value appears verbatim.

Current: opaque credential values written to logs.
Expected: value replaced with ***; surrounding diagnostic preserved.

How to test

  • scripts/run_tests.sh tests/agent/test_redact.py → 76 passed (3 new: opaque _TOKEN value, _PASSWORD value, and BWS_ACCESS_TOKEN value masked in formatted log records).

Platforms tested

  • Windows 11 (git-bash), Python 3.11, canonical scripts/run_tests.sh.
  • git diff --check clean; check-windows-footguns.py clean on changed files.

Related

Part of #77162
Part of #77165

RedactingFormatter already ran the shape-based regex redactor (vendor
prefixes, auth headers, URLs) on every log record. Opaque credential
values with no recognizable prefix — MY_SERVICE_TOKEN=abc123,
BWS_ACCESS_TOKEN, *_PASSWORD vars — passed through log output unmasked.

This adds an exact-value pass to RedactingFormatter.format() that masks
the literal values of credential-named env vars (self-contained: no
dependency on other secret-redaction changes).

Why this matters to users: a log line that happens to include a secret's
value — an error echoing a token, a debug line printing a connection
string — no longer writes that value to your log files. The diagnostic
text survives; the secret doesn't.

Tests: 76 redact tests pass (3 new: opaque _TOKEN value, _PASSWORD
value, and BWS_ACCESS_TOKEN value masked in formatted log records) via
scripts/run_tests.sh.
@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 2, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for targeting a real logging disclosure gap: current RedactingFormatter only delegates to redact_sensitive_text (agent/redact.py:1006-1008), and a bare opaque literal is not covered by the existing assignment/prefix matchers.

Problems

  • agent/redact.py:1047 in this PR unconditionally applies _mask_known_env_values(...). That bypasses the existing opt-out at agent/redact.py:710-711: when security.redact_secrets: false is configured, redact_sensitive_text intentionally returns the original text. The documented contract says this setting controls logs as well (website/docs/user-guide/configuration.md:2091; website/docs/reference/environment-variables.md:818).

Suggested changes

  • Run the exact-value formatter pass only while _REDACT_ENABLED is true, and add a formatter regression test for the disabled setting. The enabled opaque-token, password, and BWS cases are otherwise well scoped.

Automated hermes-sweeper review.

Comment thread agent/redact.py
# Shape-based regex first (vendor prefixes, headers, URLs), then the
# exact-value pass so opaque credential values with no recognizable
# prefix (e.g. MY_SERVICE_TOKEN=abc123randomstring, BWS_ACCESS_TOKEN)
# are masked from log output too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please preserve the global redaction opt-out here. redact_sensitive_text() returns raw text when _REDACT_ENABLED is false (agent/redact.py:710-711), and the documented security.redact_secrets setting controls logs too. Gate this exact-value pass on _REDACT_ENABLED and add a disabled-formatter regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bddf34a091 — the exact-value formatter pass is gated on _REDACT_ENABLED, honoring security.redact_secrets: false (agent/redact.py:710), so the opt-out is respected in the exact-value pass, not only the shape-based one. Verified at head bddf34a091: 77/77 redact tests pass.

…pass

The opaque credential-value pass in RedactingFormatter.format ran
unconditionally, masking *_TOKEN/*_PASSWORD env values even when
security.redact_secrets: false (HERMES_REDACT_SECRETS=false) was set.
redact_sensitive_text honors that opt-out and returns the original
text, but the exact-value pass masked around it, violating the
documented contract that the setting controls logs too.

Gate the exact-value pass on _REDACT_ENABLED and add a formatter
regression test asserting the disabled setting passes an opaque
credential value through unmasked.
@andrexibiza

Copy link
Copy Markdown
Contributor Author

Fixed in bddf34a091.

The exact-value formatter pass now honors the global redaction opt-out:

  • Gated on _REDACT_ENABLEDRedactingFormatter.format now computes redact_sensitive_text(original) first, then runs _mask_known_env_values only when _REDACT_ENABLED is true (agent/redact.py). With security.redact_secrets: false (or HERMES_REDACT_SECRETS=false), the formatter returns the same original text redact_sensitive_text already produces — no masking around the opt-out.
  • Regression test addedtest_disabled_setting_passes_opaque_value_through in tests/agent/test_redact.py asserts an opaque MY_SERVICE_TOKEN value passes through the formatter unmasked when redaction is disabled, and masked (existing tests) when enabled.

Validation: scripts/run_tests.sh tests/agent/test_redact.py → 77 passed, 0 failed; git diff --check clean; scripts/check-windows-footguns.py → no footguns.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Aug 2, 2026
andrexibiza added a commit to andrexibiza/hermes-agent that referenced this pull request Aug 2, 2026
Address review feedback on the end-to-end gate:

1. The capture handler is now installed BEFORE load_hermes_dotenv() runs,
   so it observes records emitted during external-secret loading instead
   of only records produced after the load completed. The mocked fetch
   path emits a warning record per leaked value (the realistic
   backend-echo case), so the formatted-output assertions prove the
   formatter actually masked records that carried the values.

2. Every value the test claims to cover is now passed through the
   formatter: the mocked load path logs ALL _LEAK_VALUES (both
   prefix-shaped), and the assertion iterates all of them with a
   non-vacuous guard (capture must contain at least one record per
   value). The second value's assertion is no longer vacuous.

Both values are prefix-shaped so main's shape-based regex masks them;
opaque values with no vendor prefix remain pinned by NousResearch#77020's own
regression tests (documented in the test docstring).

Tests: gate passes standalone; 92 total across the gate + env_loader +
redact suites via scripts/run_tests.sh.
andrexibiza added a commit to andrexibiza/hermes-agent that referenced this pull request Aug 3, 2026
…cription, skill test

Addresses teknium1's review on NousResearch#77097:

1. 'Not true on main' — the security contract is now explicitly scoped
   as implemented by the secrets-exfiltration hardening series
   (NousResearch#77008/NousResearch#77012/NousResearch#77020/NousResearch#77027/NousResearch#77031/NousResearch#77039). The docs state current
   main behavior plainly (plaintext bws_cache.json read/written when
   encryption disabled, default false) and keep the rotation instruction
   mandatory today, since that exposure already exists on main. The
   posture framing stays — this eliminates an entire vulnerability
   class — but the claim is now sequenced truthfully.
2. Skill description shortened to 53 chars, one sentence, ends with a
   period (AGENTS.md hardline).
3. tests/skills/test_bitwarden_secrets_skill.py added: validates
   frontmatter, description length, required sections, user-only
   rotation + clipboard discipline, honest series scoping (no claim the
   gate test is on main), and docs-page metadata consistency.
4. Clipboard discipline added to rotation instructions (docs + skill):
   create token, copy to clipboard, paste into terminal, save nowhere
   in between.

@GottZ GottZ left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This was generated by AI during triage.

Summary

Two PRs address the opaque-credential disclosure: #77014 combines formatter-level exact-value masking with external-secret-source stderr masking, while #77020 isolates the formatter fix and preserves the documented global redaction opt-out.

Related pull requests

  • #77014 [closed] related — (+201/-5) — superseded overlap: The closed PR contains substantially the same formatter-level exact-value masking as #77020, plus separate masking for external-secret-source errors, hints, warnings, and conflicts; it remains relevant as the broader predecessor whose formatter portion was superseded by #77020.
  • #77020 related — (+123/-1) — keep open with a salvage path: The diff directly closes the reported formatter gap by masking values from credential-named environment variables and adds coverage for opaque tokens, passwords, and BWS tokens. It also addresses the contributor's keep_open review by gating the new pass on _REDACT_ENABLED and adding a disabled-setting regression test, preserving the documented opt-out.

Duplicates

#77014 and #77020 substantially duplicate the formatter-level exact-value masking; #77014 additionally covers external-secret-source stderr output.

Suggested consolidation

Keep #77020 open with a salvage path: retain its self-contained formatter change, _REDACT_ENABLED gate, and enabled/disabled regression coverage. Treat the already-closed #77014 as superseded by #77020 for the overlapping formatter work; its separate external-secret-source stderr masking should remain associated with the stated #77012 follow-up rather than being folded into #77020.

Cross-PR triage: Reviewed 2 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 17 kB of PR diffs, 5 kB of issue/PR text, 3 kB of discussion (4 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@andrexibiza

Copy link
Copy Markdown
Contributor Author

Review receipt — comment from 2026-08-02 on agent/redact.py (RedactingFormatter exact-value pass):

Addressed in bddf34a — the exact-value pass is now gated on _REDACT_ENABLED, matching redact_sensitive_text's own opt-out: with security.redact_secrets: false (or HERMES_REDACT_SECRETS=false) the documented contract that logs are NOT redacted is preserved, and no masking runs around the returned original text.

Regression test added per your ask: test_disabled_setting_passes_opaque_value_through (asserts an opaque credential value passes through the formatter unmasked when the setting is disabled). 77/77 test_redact.py tests pass.

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The patch fixes the reported current-main leak for contiguous opaque environment credentials in handlers using RedactingFormatter and honors the documented opt-out. However, two active stderr sinks still use plain formatters and can emit the same credentials verbatim.

  • [P2] Plugin debug stderr bypasses opaque-value redaction
    When plugin debug logging is enabled, the direct stderr handler uses a plain formatter. A plugin record containing a current *_TOKEN, *_PASSWORD, or similar opaque environment value can therefore be emitted verbatim; propagation only adds another emission and does not sanitize the first one.
    Remediation: Use RedactingFormatter for the plugin debug handler with the existing prefix and level format, and add a regression test that enables plugin debug logging and asserts an opaque environment credential is absent from handler output.

  • [P2] Standalone hermes-tools MCP logging bypasses the shared redactor
    The standalone stdio server configures a plain stderr formatter instead of RedactingFormatter, so diagnostics that echo an opaque environment credential can leak it while redaction is enabled. The companion MCP server entry point has the same residual property.
    Remediation: Install a RedactingFormatter-backed stderr handler before server startup, setting the redaction environment bridge before constructing it, or route this entry point through centralized logging, and add an opaque-credential diagnostic regression test.

Security evidence:

  • trust boundary: Credential values originate in the process environment and flow into fully formatted logging records. Central handlers use RedactingFormatter, while separately constructed plugin-debug and standalone MCP stderr handlers are unredacted sinks.
  • source/sink/invariant: When redaction is enabled, current nonempty environment values of length at least six with credential-name suffixes are replaced after shape-based redaction. The documented opt-out leaves the original text unchanged; coverage is limited to contiguous values currently present in the process environment.
  • current-main reproduction: The baseline formatter leaves an opaque environment credential unchanged, confirming the reported leak on current main.
  • PR-head or patch-replay validation: The reviewed patch masks the opaque value while preserving surrounding text, and the focused test suite passes.
  • positive/negative cases: Coverage includes opaque *_TOKEN, *_PASSWORD, and BWS_ACCESS_TOKEN values with surrounding text retained. The opt-out preserves the opaque value; validation confirms the plain stderr sink still emits it.
  • residual bypass search: Central logging, ACP, gateway stderr, and gateway tool-log handlers use RedactingFormatter. The plugin-debug and standalone MCP stderr handlers remain plain sinks. Split/control-obfuscated, rotated, profile-only, short, and non-environment credentials are outside this helper.
  • reviewer validation: Changed source and tests were inspected; the current-main leak and fix were independently verified; the focused test suite passed.

Not checked:

  • ruff lint

Signed: GPT-5.6-luna-max in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants