presidio guardrail: require explicit URL scheme for PRESIDIO_*_API_BASE

The previous behavior silently prepended `http://` to PRESIDIO_ANALYZER_API_BASE
and PRESIDIO_ANONYMIZER_API_BASE when the operator-supplied value lacked a URL
scheme. The maintainer comment said: "assume communicating over private network".

This default sent the full unredacted prompt to the analyzer in plaintext over
any non-loopback transit. Realized deployment shapes that fail:

- Presidio in a separate Kubernetes namespace, no service mesh
- Presidio in a separate VPC or VNet
- Presidio behind a managed service (Azure Container Instances, AWS App Runner,
  etc.) reached over the public internet
- Any case where the customer relies on Presidio for HIPAA / GDPR / PCI scoping
  but the analyzer hop crosses an untrusted network

The redaction has not happened yet at the moment of transmission. Presidio is
the entity that performs it. So plaintext on the wire IS the unredacted PII.

This change refuses to guess. Both PRESIDIO_ANALYZER_API_BASE and
PRESIDIO_ANONYMIZER_API_BASE must now include an explicit `https://` or `http://`
scheme. The startup-time ValueError tells the operator how to set it:

- `https://...` for direct TLS to a public or routable endpoint
- `http://...` if encryption is handled at a lower layer (loopback, service
  mesh sidecar like Istio or Linkerd, Tailscale, etc.)

Existing deployments that relied on the implicit `http://` default will hit
this error at startup and must add a 7-character prefix to silence it. Loud
breakage is preferable to silent PII leakage.

No new dependencies, no test changes. The patch is to one branch each in the
analyzer and anonymizer validation paths.
This commit is contained in:
nuclide-research 2026-06-07 18:40:53 -05:00
parent aaf1e2444b
commit dc31f466b3

View file

@ -183,9 +183,20 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
self.presidio_analyzer_api_base.startswith("http://")
or self.presidio_analyzer_api_base.startswith("https://")
):
# add http:// if unset, assume communicating over private network - e.g. render
self.presidio_analyzer_api_base = (
"http://" + self.presidio_analyzer_api_base
# PRESIDIO_ANALYZER_API_BASE must include an explicit scheme.
# Previous behavior silently prepended http://, which sent the
# unredacted prompt to the analyzer in plaintext over any non-
# loopback transit (separate pod, namespace, VPC, or managed
# service). Refuse to guess so the operator makes a conscious
# choice between https:// (direct TLS) and http:// (encryption
# handled below by loopback, service mesh, Tailscale, etc.).
raise ValueError(
"PRESIDIO_ANALYZER_API_BASE must include a URL scheme "
"(`https://...` for direct TLS, or `http://...` if your "
"deployment encrypts at a lower layer such as loopback, "
"a service mesh, or Tailscale). The previous default of "
"silently prepending `http://` could send PII in plaintext "
"over any non-loopback transit."
)
if self.presidio_anonymizer_api_base is None:
@ -196,9 +207,14 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
self.presidio_anonymizer_api_base.startswith("http://")
or self.presidio_anonymizer_api_base.startswith("https://")
):
# add http:// if unset, assume communicating over private network - e.g. render
self.presidio_anonymizer_api_base = (
"http://" + self.presidio_anonymizer_api_base
# See PRESIDIO_ANALYZER_API_BASE note above. Refuse to guess.
raise ValueError(
"PRESIDIO_ANONYMIZER_API_BASE must include a URL scheme "
"(`https://...` for direct TLS, or `http://...` if your "
"deployment encrypts at a lower layer such as loopback, "
"a service mesh, or Tailscale). The previous default of "
"silently prepending `http://` could send PII in plaintext "
"over any non-loopback transit."
)
@asynccontextmanager