# PRD 0064: LOG_FULL egress logging credential redaction

- **Status:** Active
- **Author:** claude
- **Created:** 2026-06-25
- **Issue:** #257

## Summary

The `LOG_FULL` egress logging path (`_log_request` and `_log_response` in `egress_addon.py`) writes request/response headers and bodies to stderr without redaction and includes the sidecar-injected upstream `Authorization` header verbatim. This PR applies `redact_tokens` to header values and bodies in both log functions and strips the injected `Authorization` header from request logs entirely.

## Problem

`LOG_FULL` (log level 2) is intended for debugging egress traffic. When active it calls `_log_request` and `_log_response`. Both functions have two related bugs:

1. **Injected `Authorization` header exposure.** `_log_request` is called *after* the sidecar injects upstream credentials (`flow.request.headers["authorization"] = decision.inject_authorization`). The full header dict — including the live credential — is serialized to stderr. Any log collector that ingests the egress container's stderr will receive the upstream bearer token in plaintext.

2. **Unredacted bodies and header values.** Neither `_log_request` nor `_log_response` passes body or header values through `redact_tokens`. By contrast, `_req_ctx` (used for block/warn events) already calls `redact_tokens` on path and host. Any provisioned secret or recognized token pattern that appears in a request body, response body, or non-Authorization header value will be logged verbatim under `LOG_FULL`.

These two bugs compose: an agent that enables `LOG_FULL` and simultaneously triggers a request that carries a known token gains a write path from credentials → egress logs.

## Goals / Success Criteria

- `_log_request` never logs the `authorization` header in any form.
- `_log_request` applies `redact_tokens(value, env=os.environ)` to every other header value before serializing.
- `_log_request` applies `redact_tokens(body, env=os.environ)` to the request body before logging.
- `_log_response` applies `redact_tokens(value, env=os.environ)` to every response header value before logging.
- `_log_response` applies `redact_tokens(body, env=os.environ)` to the response body before logging.
- Unit tests cover each of the five cases above.

## Non-goals

- Redacting host or path in the full-log path (already covered by `_req_ctx` for block/warn events; `_log_request` already calls `redact_tokens` on host and path).
- Suppressing `LOG_FULL` or adding a new log level.
- Changing the outbound DLP scan logic.

## Design

### `_log_request`

```python
def _log_request(self, flow: http.HTTPFlow) -> None:
    headers = {
        k: redact_tokens(v, env=os.environ)
        for k, v in flow.request.headers.items()
        if k.lower() != "authorization"
    }
    body = redact_tokens(flow.request.get_text(strict=False) or "", env=os.environ)
    sys.stderr.write(
        json.dumps({
            "event": "egress_request",
            "host": redact_tokens(flow.request.pretty_host, env=os.environ),
            "method": flow.request.method,
            "path": redact_tokens(flow.request.path, env=os.environ),
            "headers": headers,
            "body": body,
        })
        + "\n"
    )
```

The `authorization` key is excluded because by the time `_log_request` is called the sidecar has already injected the upstream credential (`decision.inject_authorization`). Logging it would write a live bearer token to stderr on every allowed request. There is no safe subset to log — the value is always a live credential or empty.

### `_log_response`

```python
def _log_response(self, flow: http.HTTPFlow) -> None:
    headers = {
        k: redact_tokens(v, env=os.environ)
        for k, v in flow.response.headers.items()
    }
    body = redact_tokens(flow.response.get_text(strict=False) or "", env=os.environ)
    sys.stderr.write(
        json.dumps({
            "event": "egress_response",
            "host": flow.request.pretty_host,
            "status": flow.response.status_code,
            "headers": headers,
            "body": body,
        })
        + "\n"
    )
```

Response headers don't carry injected credentials, so no header name is suppressed — only the values are scrubbed by `redact_tokens`.
