Skip to content

Static credential placeholders are destination-agnostic — consider binding them to the endpoint (token-grant already does) #2353

Closed as not planned

Description

@CraigAllsopp

Summary

The static credential flow issues a deterministic placeholder (openshell:resolve:env:<KEY>) into the sandbox environment and substitutes the real value at the L7 proxy on egress. This is a genuinely good design — the secret never lands in the sandbox filesystem or environment, so a full container compromise yields nothing to exfiltrate.

The gap: the placeholder carries no binding to the destination it's intended for. Any process in the sandbox that can reach any policy-allowed L7 endpoint can include the placeholder in a request and receive the substituted secret. Egress policy then becomes the single control preventing a credential being redirected to an attacker-chosen — but allowed — endpoint.

This risk class is already acknowledged in the docs (exfiltration through allowed endpoints). The suggestion below is about not depending on the allowlist alone.

Why it looks worth closing

The codebase already contains the pattern. l7/token_grant_injection.rs binds dynamic tokens to their endpoint — credentials are keyed on a tab-separated host \t port \t path \t provider_key tuple, matched by host pattern + exact port + path glob, with the most-specific key winning. The static path (openshell-core/src/secrets.rs) has no equivalent: SecretResolver is a flat HashMap<String, SecretValue> keyed on the placeholder alone, and resolve_placeholder(&self, value: &str) takes no host, port or path.

The contrast is visible in a single struct — L7EvalContext carries both mechanisms side by side, and only one of them is documented as endpoint-bound:

/// Supervisor-only placeholder resolver for outbound headers.
pub(crate) secret_resolver: Option<Arc<SecretResolver>>,
...
/// Dynamic credentials (token grants) keyed by endpoint-bound provider metadata.
pub(crate) dynamic_credentials: Option<...>,

Concretely: on the balanced / open tiers — which permit POST to general-purpose third-party endpoints — an agent that is prompt-injected, or simply goal-seeking, can place openshell:resolve:env:GITHUB_TOKEN in a header or body aimed at an allowed endpoint, and the proxy will faithfully substitute the real token on the way out.

Two properties compound it:

  • the placeholder is deterministic — derivable from the credential name alone, with no read access required
  • substitution has no per-destination binding — the resolver matches on the placeholder only, so a credential resolves for any endpoint the connection is already permitted to reach

To be precise about the scope: this isn't unrestricted egress — L4/L7 policy still governs whether the connection happens at all, and placeholder scanning fails closed if a marker can't be resolved. The gap is credential cross-binding within the allow-list: a token intended for one allowed endpoint can be substituted into a request to a different allowed one.

Suggested change

Key the resolver on (env_key, host, port, path_glob) rather than env_key alone:

  • resolve a credential only when the request is bound for its declared destination
  • treat a placeholder appearing in a request bound elsewhere as a policy violation — reject with an audit event, rather than substituting

Optionally, a per-sandbox random salt in the placeholder would stop it being guessable from the credential name.

The effect is to move the mechanism from "obfuscation backed by a firewall" to "capability bound to an endpoint" — which is what the dynamic arm already achieves.

Notes

Filed as a design/hardening suggestion rather than a security report, since the underlying risk is already documented. Happy to move this to a private channel if you'd prefer.

Based on a read-only review of the public repo and docs — not run in anger, so I may have missed a binding that happens elsewhere in the pipeline.

Activity

  1. matthewgrossman commented on Jul 22, 2026

    @matthewgrossman
    Member

    📋 triage-agent

    Triage Assessment

    Classification: duplicate

    Summary

    The credential cross-binding concern is valid and remains present in current OpenShell, but #896 already tracks this provider-profile security hardening. That issue explicitly describes today's endpoint-blind placeholder injection and proposes binding credentials to provider endpoints and binaries.

    Investigation

    SecretResolver resolves static placeholders without destination context, while dynamic token grants match endpoint metadata. #896 defines the canonical provider-profile design for closing that gap. Closed, unmerged PR #1891 is relevant prior implementation work; its review exposed additional binding hazards that should inform #896, but it is not an active fix.

    Recommendation

    Continue the design and implementation under #896. Preserve this issue's fail-closed wrong-destination behavior and endpoint/path binding details as acceptance criteria there. Closing this issue as a duplicate.

  2. added
    duplicateThis issue or pull request already exists
    topic:l7Application-layer policy and inspection work
    and removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Jul 22, 2026
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

    area:providersarea:supervisorProxy and routing-path workduplicateThis issue or pull request already existstopic:l7Application-layer policy and inspection worktopic:securitySecurity issues

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions