Skip to content

Proposal: static marker + per-secret identifier placeholders (dynamic secret lookups) #122

Description

@mitsuhiko

Note

This is AI slop, triggered by @mitsuhiko. It's a brainstorm written up as a proposal — a point to discuss, not yet to implement.

Context

#121 adds runtime add/update/delete for HTTP-hook secrets, so secrets can change after VM startup and newly-spawned processes pick up the current placeholders. This proposal questions one assumption underneath that PR — the per-secret, dynamically-generated, high-entropy placeholder — and sketches an alternative that would largely dissolve the runtime-propagation problem #121 is solving.

How placeholders work today

A placeholder does two jobs at once:

  1. It's the env var's valueAPI_KEY=GONDOLIN_SECRET_<48 hex> — so tools that read os.environ["API_KEY"] transparently pick it up.
  2. It's the detection tokenreplaceSecretPlaceholdersInString exact-substring-matches each placeholder against outbound headers/query and swaps in the real value only when the destination host is allowed.

The entropy is not a confidentiality property — the guest already sees every placeholder in its env. It exists purely for collision avoidance: so real request content never accidentally matches a placeholder, and so placeholders don't overlap each other (hence assertSecretPlaceholderIsSafe).

Because placeholders are dynamic and per-secret, every newly-added secret introduces a new opaque token that must be propagated to every newly-spawned process. That propagation is most of #121's surface area: rewriting /etc/gondolin/secrets.env, the mergeExecEnvWithRuntimeSecrets filter-and-merge, deleted-name stripping, and the reuse-the-old-placeholder-on-re-add logic.

The idea

We don't actually need high entropy per secret. We only need a high-entropy marker to reliably locate an injection point. So: use one static high-entropy marker for "secret injection here," and append a well-known per-secret identifier.

$GONDOLIN_SECRET.github   →   <MARKER>.github

The host scans outbound content for <MARKER>.<identifier>, looks the identifier up in a host-side map, and substitutes the real value (subject to the existing per-secret host allowlist). Detection is now structured parsing of a static marker, not exact matching against a dynamically-known set. That decouples detection from enrollment timing.

Why this helps

  • Only one env var (GONDOLIN_SECRET) is ever in the guest env, and it never changes — no env churn on add/update/delete.
  • Add/delete becomes a pure host-side map mutation; newly-spawned processes need nothing refreshed.
  • Re-adding a deleted secret reuses its "placeholder" for free (it's deterministic from the identifier).
  • Per-secret overlap checks collapse into a single grammar.
  • Dynamic lookups: a secret can be referenced before it's added; resolution simply happens (or not) host-side once the secret exists.

The acknowledged downside

You lose customizable placeholders. Some clients validate token shape (e.g. ghp_…), and the marker scheme can't mimic that. So this should be an additional mode, not a removal of the current placeholder?: string | (() => string) option.

The fork that decides everything: the env-var question

  • Model A — explicit reference only. Guest references $GONDOLIN_SECRET.github by convention. One env var total. Simplest, best dynamic-lookup story. But tools expecting a natural OPENAI_API_KEY won't find one — transparent pickup is lost.
  • Model B — keep per-secret env vars, derived values. OPENAI_API_KEY=<MARKER>.openai_api_key. Transparent pickup preserved; values deterministic, so they can be populated the instant a secret is named (before its value exists host-side), and add/remove toggles resolution without rewriting guest env.

Recommendation: B as default, A always available. That delivers dynamic lookups and keeps #121's propagation near-zero.

Open design questions

  1. Marker scope. Recommend static within a session but randomly generated per createHttpHooks — keeps collision-avoidance, avoids a marker shared across tenants/VMs, still stable across processes and restarts. "Static" = "not per-secret," not necessarily "compiled-in constant."
  2. Suffix grammar / maximal munch. Pick an identifier charset (e.g. [A-Za-z0-9_-]) with . as separator. Maximal-munch is unambiguous as long as identifiers are bounded by non-identifier chars (<MARKER>.github/reposgithub). Footgun: <MARKER>.githubX → id githubX, not github+X. Document it.
  3. Unknown / deleted identifier policy. <MARKER>.doesnotexist → leave literal, blank, or block? Lean toward blank (consistent with deleted-secret behavior, avoids leaking the marker downstream).
  4. Coexistence with custom placeholders. Keep the existing custom-placeholder mode for shape-sensitive tokens; the marker scheme is opt-in/default-able alongside it.
  5. Security: discoverability. With random per-secret placeholders, a process without a secret in its env effectively can't use it (can't guess the token). With well-known identifiers, any process can reference any secret by name, toward that secret's allowed hosts. The host allowlist was always the real boundary, so this is not a new exfiltration path — but it widens "secrets a given process can exercise" from "those in its env" to "all of them." Worth stating as a considered trade-off.
  6. Orthogonal machinery that stays. Value revocation (revokedValues) and the defense-in-depth "real value to disallowed host" checks are independent of the placeholder scheme and carry over unchanged.

Ask

Discuss the approach (especially the A-vs-B env-var question and marker scope) before anything is built. If we like it, this likely simplifies — rather than extends — the direction in #121.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions