# How to handle high-stakes credentials

> **Tracker:** SH-03 (mgf-apiprobe `SECURITY_HARDENING.md`).
> **Audience:** operators running mgf-apiprobe against production
> APIs with long-lived credentials, or sharing a single Python
> process across multiple tenants.

## The problem

Every concrete `AuthFlow` in `mgf.apiprobe.auth.flows` stores its
credential string on the flow instance:

- `BearerAuth(token=...)` → `self._token: str`
- `BearerAuth(env="MY_TOKEN")` → resolves at first `prepare()`, then
  caches on the flow instance until GC.
- Similarly for `BasicAuth`, `ApiKeyAuth`, `CookieAuth`.

That string lives until the flow instance is garbage-collected,
typically at suite-run end. **Python does not permit deterministic
erasure of `str` memory** — strings are immutable and interned;
`del` only drops the reference, not the bytes. CPython's small-
string cache can retain the value past GC.

For most consumers this is fine. The suite is a short-lived
process; secrets live in memory between probes regardless; the
attack window is small.

**For high-stakes contexts**, you want stronger guarantees than
in-process retention can give you.

## Threat models this recipe addresses

- **Memory-snapshot exfiltration.** Attacker dumps the Python
  process's RSS (e.g., via `/proc/<pid>/mem`, a coredump on
  crash, or a debugger attaching). A long-running mgf-apiprobe
  process accumulates secrets from every suite run; a snapshot
  captures them all.
- **Multi-tenant test harnesses.** A single Python process runs
  suites for multiple tenants in sequence. Tenant A's credential
  remains in memory while Tenant B's suite runs.
- **Long-running CI/cron daemons.** A persistent process that
  rotates through many providers' credentials accumulates secrets
  over hours or days.

If your threat model is none of these (single-tenant, short-lived
CI runs, no memory-access threat), the default in-process
retention is fine. Skip this recipe.

## The fix: process isolation

The cleanest answer is **one suite per OS process**. The OS
reclaims the address space at process exit — pages are returned to
the allocator and can be zeroed before reuse. Strings cached in
the small-string interner are gone too, because the interner is
process-local.

```bash
# Instead of running 10 tenants' suites in one mgf-apiprobe call:
mgf-apiprobe run multi-tenant.yml          # DON'T (long-lived)

# Run one suite per subprocess:
for tenant in tenant-a tenant-b tenant-c; do
  mgf-apiprobe run suites/${tenant}.yml    # DO (per-tenant subprocess)
done
```

CI runners that fork per-job give this for free. A long-lived
operator session or a multi-tenant orchestrator needs to spawn the
subprocess explicitly.

## Bracket the pattern with credential rotation

Subprocess isolation limits the **forward** value of a memory
snapshot. **Credential rotation** limits its **historical** value:
a snapshot from an hour ago is worthless if the credential was
rotated 30 minutes ago.

Combine the two:

```bash
# 1. Acquire a short-lived credential from a vault (STS, Vault).
export TENANT_A_TOKEN="$(vault read -field=token secret/tenant-a)"

# 2. Run the suite in a subprocess. The token lives for the
#    subprocess lifetime only.
mgf-apiprobe run suites/tenant-a.yml

# 3. The token in the parent shell expires on its own (e.g., STS
#    15-minute TTL). No explicit cleanup needed — the vault
#    enforces the TTL.
```

For the tightest posture: have the vault issue tokens with a TTL
shorter than the suite run takes. A token that's already expired
by the time you exfiltrate it is the cleanest possible memory
hygiene.

## What NOT to do

- **Don't try to zero-out `str` memory in Python.** Tricks involving
  `ctypes.memset` on a string's internal buffer are fragile, version-
  specific, and easy to get wrong. They give a false sense of
  security: even if the original buffer is zeroed, the interner may
  hold a separate copy.
- **Don't switch credentials to `bytes` thinking it's safer.** Same
  immutability story; the same interning happens for short byte
  strings.
- **Don't disable Python's GC to shorten retention.** That makes
  retention *longer*, not shorter.

The actionable lever is the process boundary. Use it.

## Related

- `mgf.apiprobe.auth.flows` — the module docstring carries the
  in-process memory model.
- `mgf-common`'s `EncryptedFileVault` — the storage-side answer
  (encrypted at rest; decrypted only on access).
- mgf-apiprobe SH-03 in `docs/inprogress/SECURITY_HARDENING.md`
  for the audit context.
