SIGN IN SIGN UP

fix: redact Authorization header from websockets DEBUG logs (#731)

## Summary

Fixes an externally-reported security issue: the API key / access token
is written to application logs in clear text when DEBUG logging is
enabled.

The leak is **not** in Deepgram's code — it's the third-party
`websockets` library, which logs the full opening handshake at DEBUG,
one record per header, via `logger.debug("> %s: %s", name, value)`. That
includes `Authorization: Token <key>`. Any app that turns on DEBUG
(common in dev/troubleshooting) then writes the credential to its logs,
from where it's easily committed or shipped to a log aggregator.

## Fix

Install a `logging.Filter` on the `websockets.client` /
`websockets.server` loggers that masks the value of `Authorization` /
`Proxy-Authorization` headers **before** the record reaches any handler
— so the credential is never emitted, regardless of the app's log level.

- The auth scheme is preserved for debuggability; only the secret is
replaced: `Authorization: Token [REDACTED]`.
- Nothing is suppressed and no other log output changes.
- **On by default** (installed from both client constructors,
idempotent). Opt out with
`DeepgramClient(redact_credentials_in_logs=False)` to manage redaction
yourself.

Confirmed the filter reliably catches the record: `websockets >= 12`
(our floor) emits handshake headers directly on `websockets.client` with
no per-connection child loggers, so a filter there sees every header
record before propagation.

## Files
- `src/deepgram/_secure_logging.py` — hand-written
`RedactCredentialsFilter` + `install/uninstall` helpers (frozen in
`.fernignore`)
- `src/deepgram/client.py` — install from `DeepgramClient` /
`AsyncDeepgramClient` constructors + opt-out kwarg
- `tests/custom/test_secure_logging.py` — regression coverage (token
masked, scheme preserved, non-sensitive headers untouched, idempotent
install, opt-out)

## On the per-frame logging
The reporter also noted `BINARY ... [N bytes]` spam per audio frame.
That's also `websockets` DEBUG output, but it contains **no secret**
(payloads are shown only as byte counts), so it's a verbosity concern,
not a disclosure. Silencing it by default would require raising the
`websockets` logger level, which would also drop the handshake debug
some users rely on. Recommended app-side mitigation (documented, not
forced):

```python
import logging
logging.getLogger("websockets").setLevel(logging.INFO)  # quiet per-frame logs, keep your app at DEBUG
```

## Validation
- `ruff check` — clean
- `mypy` — clean
- `pytest` — 237 passed, 1 skipped (12 new)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
G
Greg Holmes committed
029d8771a2957fd5ffbc3c50fb40550098fcfad3
Parent: 88b0da0
Committed by GitHub <noreply@github.com> on 6/18/2026, 9:16:21 AM