Documentation
¶
Overview ¶
Package egress is the egress-time credential-injection subsystem: the shared substitution engine that rewrites vault placeholders into their secret values on outbound requests, and the host matcher both it and the per-session gate use to decide which hosts a request — or a credential — may reach. It holds no I/O, and that is the invariant to preserve: internal/gate drives it against real HTTP requests — constructing the substitution engine (gate.go) and calling Substitute for header and body locations — while internal/gate/policy.go reuses NewHostSet and NormalizeHost for the environment's allowed-host policy, and internal/vaultresolve supplies the credentials read from the store.
Index ¶
Constants ¶
const PlaceholderPrefix = "vltph_"
PlaceholderPrefix marks the opaque tokens the platform injects into a sandbox in place of a vault secret. The reference calls the sandbox-visible value an "opaque placeholder" and defines no format; this prefix and the derived suffix are ours (recorded as a deliberate divergence). The token is a valid environment-variable value — no spaces or shell metacharacters — so it injects cleanly through sandbox.Spec.Env.
Variables ¶
This section is empty.
Functions ¶
func NormalizeHost ¶ added in v0.3.0
NormalizeHost lowercases and strips a single trailing FQDN dot so "Example.com." and "example.com" compare equal. Exported because the gate's MCP endpoint set has to answer a host exactly the way this package's HostSet does (internal/gate, policy.go) while staying an exact-match map.
func Placeholder ¶
Placeholder derives the opaque token the sandbox sees in place of the vault secret named secretName for the given session: the prefix plus 128 bits from SHA-256 of (sessionID, secretName), in hex.
It is deterministic on purpose. The sandbox binds its environment at container create and keeps it across the idempotent re-provisions of a session (sandbox.Spec.Env is "fixed at create"), so every executor pass — and later the egress gate resolving live secret values — must derive the exact token already in the sandbox, not mint a fresh one that would no longer match. Stability is per (session, secret_name): a rotated credential (same secret_name, new secret) keeps its placeholder and the gate resolves the new value under it. The token is opaque and not itself a secret — the per-session gate only substitutes a session's own placeholders, and only for a host the credential's allowed_hosts admit, so correctness needs a stable derivation, not an unguessable one. The NUL separator keeps ("a","bc") from colliding with ("ab","c").
func ValidateHostEntry ¶
ValidateHostEntry checks one allowed-hosts entry against the grammar this package matches: a bare hostname, an IPv4 literal, or a "*."-prefixed wildcard on a hostname. It is the single source for the grammar — the vault API's allowed_hosts validation wraps it, and the executor's WEBTOOL_ALLOWED_DOMAINS fails startup on the first bad entry, because an out-of-grammar entry silently matches nothing (a typo would read as the operator's fence when it is really a hole in it, or a deny-all).
Types ¶
type Credential ¶
type Credential struct {
Placeholder string
Secret string
Hosts *HostSet
Unrestricted bool
Header bool
Body bool
}
Credential is one resolved environment-variable vault credential: its sandbox-visible Placeholder, the Secret it stands for, the hosts it may be used against, and which injection locations it is enabled for. Secrets live here only for the substitution call path — never logged, never stored.
Unrestricted is the credential's own networking arm: when set, the secret may be substituted for any request host and Hosts is ignored (the credential was created with networking "unrestricted"). Otherwise Hosts is the limited allow-list — a nil/empty set matches nothing (fail-closed).
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine holds the resolved credentials for one substitution pass, keyed by placeholder. internal/vaultresolve reads them from the store and the control plane hands them to the gate (api/gateconfig.go), which builds an engine per session and drives Substitute over each outbound request.
func NewEngine ¶
func NewEngine(creds []Credential) *Engine
NewEngine builds an engine over a set of resolved credentials. The slice is small (a session's attached env-var credentials), so Substitute scans it directly rather than pre-indexing.
func (*Engine) Substitute ¶
func (e *Engine) Substitute(host string, loc Location, s string) (out string, unreachable []*Credential)
Substitute rewrites s for a request bound to host, in injection location loc. A credential enabled for loc whose placeholder appears in s is substituted with its secret when host is admitted by the credential's allowed_hosts; when it is not, the placeholder is left literal (the opaque token, never the secret, reaches the third party — the reference's documented behavior, not an error) and the credential is returned in unreachable as a diagnostic; the wire-visible credential_host_unreachable_error is a config conflict the controlplane emits at gate-config render, never a per-request report. A placeholder whose credential is not enabled for loc is left literal and is not unreachable — the documented "a disabled injection_location is neither substituted nor stripped". Each unreachable credential is reported once.
This path is uninstrumented: no span, no metric, no decision log. Plan 12 intended a per-substitution span and it was never built, so an operator asking "why was my credential not substituted?" has only the gate's egress_request span, which carries the method and server address and no verdict. Said here rather than in that plan alone, because the absence is the answer to a question this function invites.
type HostSet ¶
type HostSet struct {
// contains filtered or unexported fields
}
HostSet matches a request host against an allowed_hosts list in the grammar the vault API validates (internal/api/vaultcredauth.go): a bare hostname, an IPv4 literal, or a "*."-prefixed wildcard. It is the one matcher shared by a credential's allowed_hosts (may this secret be used for this host?) and an environment's networking allow-list (may this request leave at all?).
A wildcard "*.example.com" matches any subdomain but never the apex (example.com) — the reference's recorded behavior (anthropic-sdk-go betavaultcredential.go: "a `*.`-prefixed entry matches any subdomain of the named domain but not the domain itself"). "Any subdomain" is read as any label depth (a.example.com, a.b.example.com), the one residual the SDK wording does not pin (recorded in DIVERGENCES).
func NewHostSet ¶
NewHostSet builds a matcher from allowed_hosts entries. Entries are assumed to have passed the API's validateAllowedHost; malformed entries simply never match. A nil or empty list matches nothing.
func (*HostSet) CoversEntry ¶
CoversEntry reports whether every host an allowed_hosts entry names is admitted by the set — the config-conflict probe behind the reference's credential_host_unreachable_error ("a credential's allowed_hosts includes a host the environment's network policy does not permit"). An exact entry is covered iff the set matches it. A wildcard entry names a whole subdomain family, which only a wildcard of the set can cover: "*.D" is covered iff the set has a suffix S with D == S or D under S — a set's exact host can never cover a family, and a broader wildcard is not covered by a narrower one. A malformed entry covers nothing and is reported uncovered (fail-closed: it names something the policy cannot admit).
func (*HostSet) Match ¶
Match reports whether host is admitted by the set. Matching is case-insensitive and tolerant of a trailing FQDN dot. A malformed host — empty, or carrying an empty label (a leading dot or a ".." run) — never matches: the API validates entries against the same grammar, so admitting an out-of-grammar request host would let ".example.com" slip past the "*.example.com" boundary. A nil set (a credential resolved without a host list) matches nothing.