Skip to content

fix(weather): stop logging API keys and auth headers#188

Open
pdettori wants to merge 2 commits intokagenti:mainfrom
pdettori:fix/weather-agent-secret-logging
Open

fix(weather): stop logging API keys and auth headers#188
pdettori wants to merge 2 commits intokagenti:mainfrom
pdettori:fix/weather-agent-secret-logging

Conversation

@pdettori
Copy link
Copy Markdown
Contributor

@pdettori pdettori commented Mar 20, 2026

Summary

  • Remove the LogAuthorizationMiddleware that logged full Authorization headers (including API keys) on every request
  • Change root log level from DEBUG to INFO to prevent httpx/openai libraries from dumping request headers
  • Add SecretRedactionFilter on the root logger — redacts Bearer ... tokens and the literal configured LLM_API_KEY value from any log message
  • Add has_valid_api_key check in Configuration — returns a clear user-facing error when the API key is a placeholder and the API base is remote

Simplified from prior iteration per review feedback: dropped the sk-* regex (literal key + Bearer cover all cases), inlined is_local_llm, removed log_warnings(), and properly handle dict args in the filter.

Root Cause

Three issues combined to expose the OpenAI API key in K8s pod logs:

  1. agent.py — middleware explicitly logged the Authorization header value
  2. agent.pyDEBUG log level caused httpx/openai to also dump headers
  3. configuration.py — no validation of the API key, so blank/dummy keys produced confusing errors

Test Plan

  • 18 new tests in tests/a2a/test_weather_secret_redaction.py:
    • SecretRedactionFilter: redacts Bearer tokens (case-insensitive), literal configured key, handles tuple and dict args, preserves non-secret messages
    • ConfigurationApiKeyValidation: placeholder/empty keys invalid for remote APIs, valid for local LLMs, real keys valid
  • Existing configuration tests still pass
  • Manual: deploy weather agent with blank API key → verify clear error message in UI
  • Manual: deploy with invalid API key → verify key does NOT appear in kubectl logs

Closes #119

@pdettori pdettori requested a review from esnible March 20, 2026 03:13
Copy link
Copy Markdown
Contributor

@esnible esnible left a comment

Choose a reason for hiding this comment

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

This PR seems to solve the problem, but it seems nearly as large at the weather agent itself. Do we want a large weather agent? I think it should be very small, as it is our most typical example, so that humans and AI can easily read and understand it.

Copy link
Copy Markdown
Contributor Author

@pdettori pdettori left a comment

Choose a reason for hiding this comment

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

Review: Key validation and redaction with non-OpenAI providers

The key validation logic (has_valid_api_key) looks good — it only rejects exact placeholder matches (dummy, changeme, etc.), so real keys from any provider will pass fine.

However, the SecretRedactionFilter has a gap for non-OpenAI providers:

agent.py:31_API_KEY_RE only matches sk-* prefixed keys:

_API_KEY_RE = re.compile(r"(sk-[a-zA-Z0-9]{3})[a-zA-Z0-9]+")

In CI we use RHOAI MaaS (<model>--maas-apicast-production.apps.prod.rhoai.rh-aiservices-bu.com:443) where keys are 32-char lowercase alphanumeric strings with no sk- prefix. These would not be redacted if they appear in log messages outside of Authorization headers.

The Bearer \S+ pattern covers the auth header case, but httpx/openai debug logs or error messages may surface the key in other formats.

Suggestion: Broaden the API key regex to catch generic long alphanumeric tokens, or consider an approach that redacts the configured llm_api_key value directly (since Configuration already has it). For example:

# Option A: redact any long alphanumeric string that looks like a secret
_API_KEY_RE = re.compile(r"(sk-[a-zA-Z0-9]{3})[a-zA-Z0-9]+|(?<=[^a-zA-Z0-9])([a-z0-9]{4})[a-z0-9]{28,}(?=[^a-zA-Z0-9]|$)")

# Option B: redact the actual configured key value
_configured_key = os.environ.get("LLM_API_KEY", "")
if len(_configured_key) > 8:
    _LITERAL_KEY_RE = re.compile(re.escape(_configured_key))

Not a CI blocker — the validation itself is fine — but worth hardening before merge.

Copy link
Copy Markdown
Contributor

@mrsabath mrsabath left a comment

Choose a reason for hiding this comment

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

LGTM — clean security fix with solid defense-in-depth.

Well-structured fix that addresses a real vulnerability (API keys in pod logs via the log_authorization_header middleware and DEBUG-level logging). The three-layer redaction approach (Bearer pattern, sk-* pattern, literal configured key) is good defense-in-depth. Test coverage is thorough with 17 new tests covering edge cases including RHOAI MaaS keys.

Areas reviewed: Python, Tests, Security, Commit conventions, PR format
Commits: 4 commits, all signed-off ✓
CI status: all passing ✓

Minor suggestions below — none are blockers.

args = record.args if isinstance(record.args, tuple) else (record.args,)
new_args = []
for arg in args:
if isinstance(arg, str):
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.

suggestion: record.args can also be a dict (for %(key)s-style log formatting). The current code wraps non-tuple values in a tuple, which would wrap a dict rather than iterating its values. Low risk since dict-style logging is rare, but for completeness:

if isinstance(record.args, dict):
    record.args = {k: self._redact(v) if isinstance(v, str) else v for k, v in record.args.items()}
elif record.args:
    args = record.args if isinstance(record.args, tuple) else (record.args,)
    ...

Not a blocker — could be a follow-up.

"""Check if the API key is usable.

Local LLMs (Ollama, vLLM, etc.) accept any key including placeholders,
so placeholder keys are only flagged when pointing at a remote API.
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.

nit: from urllib.parse import urlparse — consider moving to top-level imports for consistency. Inline imports are fine for avoiding circular deps, but that's not the case here.

Remove the middleware that logged Authorization headers on every request,
exposing API keys in pod logs. Defense-in-depth changes:

- Change root log level from DEBUG to INFO
- Add SecretRedactionFilter (Bearer tokens + literal configured key)
- Add has_valid_api_key check with clear user-facing error message
- Simplified from prior iteration per review feedback (esnible, mrsabath):
  dropped sk-* regex (literal key covers it), inlined is_local_llm,
  removed log_warnings(), handled dict args in filter

18 new tests cover redaction and API key validation edge cases.

Closes kagenti#119

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
@pdettori pdettori force-pushed the fix/weather-agent-secret-logging branch from e822c2d to 13e4882 Compare April 2, 2026 02:58
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review / Needs Testing

Development

Successfully merging this pull request may close these issues.

bug: If the OpenAI key is invalid, the weather agent logs it, exposing it to anyone with K8s log read access

3 participants