Skip to content

Add EnvVarHeaderContentGuard for env-backed header secret validation #8007

Description

@decko

Problem

HeaderContentGuard stores the expected secret in header_value on the guard record. When the shared secret is managed outside Pulp (injected by a reverse proxy, Kubernetes secret → pod env var, etc.), rotating it requires updating every guard that references that secret.

HeaderContentGuard expects the request header to be Base64-encoded and compares against the decoded value. That encoding keeps arbitrary UTF-8 secrets transport-safe over HTTP headers. We need the same wire-format behavior for env-backed secrets, while still reading the expected plaintext from os.environ at request time so rotation is a deployment/config change rather than a bulk API update across guard records.

Proposed solution

Add an EnvVarHeaderContentGuard content guard type to pulpcore (alongside HeaderContentGuard):

Field Purpose
header_name Request header to inspect (operator-configured)
env_var Name of the environment variable holding the expected secret (plaintext UTF-8)

permit() behavior:

  1. Read request.headers[header_name]; deny if missing.
  2. Base64-decode the header value; deny if not valid Base64.
  3. Decode the result as UTF-8; deny if not valid UTF-8.
  4. Read os.environ[env_var]; deny if unset or empty (after strip).
  5. Compare decoded header bytes to the env value using hmac.compare_digest on UTF-8-encoded strings (supports non-ASCII secrets).
  6. Deny on mismatch; allow otherwise.

Proxies send base64(utf-8(secret)) in the header. The pod environment variable holds the plaintext secret.

API: New viewset at /pulp/api/v3/contentguards/core/envvar_header/ (or equivalent TYPE = "envvar_header" naming), with RBAC/access policy matching HeaderContentGuard.

v1 scope (suggested):

  • Base64-encoded header on the wire (plaintext secret in env var), matching HeaderContentGuard transport behavior.
  • No jq_filter (can be added later if needed).
  • No allowlist fields.

Use case

Any deployment where:

  • A trusted edge component injects a shared secret header on authorized requests.
  • The secret is provisioned via environment variables (K8s env, Clowder, etc.).
  • Operators want rotation without touching guard records in the database.
  • Secrets may include non-ASCII UTF-8 characters when Base64-encoded on the wire.

Alternatives considered

  1. Extend HeaderContentGuard with an optional env_var field instead of a new type — fewer models, but mixes DB-stored and env-sourced secrets in one guard; harder to document and permission separately.
  2. Keep as a downstream plugin only — we prototyped this in pulp-service (PULP-2257: Add EnvVarHeaderContentGuard for env-backed header validation pulp-service#1420); workable but duplicates a generic content-guard primitive that belongs in core.
  3. Continue using HeaderContentGuard + DB updates — rotation requires updating every guard instance; does not scale for shared secrets across many distributions.
  4. Raw header compare (no Base64) — simpler for ASCII-only secrets, but breaks non-ASCII secrets and is less consistent with HeaderContentGuard and HTTP proxy behavior.

Acceptance criteria

  • EnvVarHeaderContentGuard model, migration, serializer, viewset registered in pulpcore
  • permit() Base64-decodes the header, then validates against os.environ[env_var] at request time
  • Missing header, invalid Base64, invalid UTF-8, unset/empty env var, and wrong value → deny (403 on content app)
  • Correct Base64-encoded header matching env plaintext → allow
  • Unit tests for permit() edge cases (missing header, invalid Base64, wrong value, env unset/empty, trailing newline in env, non-ASCII UTF-8 secret)
  • Functional test: guarded distribution denies without header, allows with matching Base64-encoded header when env is set
  • Access policy registered for the new viewset (same pattern as HeaderContentGuard)
  • OpenAPI / client bindings updated

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions