mirror of
https://github.com/hanzoai/egress.git
synced 2026-10-30 15:08:38 +00:00
A call and a fetch are each one request and one answer. A database is neither: a caller opens a connection and keeps it, so what egress brokers there is the whole connection. It listens for postgres clients, reads the caller's IAM access token out of the password field — the one field a postgres client has for a secret — and verifies it with the same Verifier that guards every other route. Two origins, and the difference is only where the connection then goes. A base of ours carries NO CREDENTIAL: hanzo-sql and the forks beside it are co-located and reached over the trust between them, with the caller's tenant presented as the database role, so the base's own grants are a second boundary under egress's first. A database that is not ours comes out of the caller's existing custody as a whole connection URL — the same custody.resolve the model and cloud paths read, with a URL where a bearer would be. Custody mode is the one place a caller decides an address, so a sealed origin resolves through a lookup that returns public addresses only and refuses a socket path outright; otherwise the broker is a tunnel to this host's own network. Nothing about a session comes from the environment: pgconn's parser fills an absent setting from PG* and ~/.pgpass, so every field that decides where the connection goes is written over its defaults. A session ends when its token does. pgproto3 is a codec here and never a reader — its own reader may fill past the message it was asked for, and after the handshake every byte read is a byte the database never sees. spend gains Session, which builds the connection URL a driver dials. It adds no dependency to the client module. Co-authored-by: Hanzo Dev <[email protected]>
188 lines
6.8 KiB
Go
188 lines
6.8 KiB
Go
package egress
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
kms "github.com/hanzoai/kms/sdk/go"
|
|
"github.com/luxfi/kms/pkg/zapclient"
|
|
)
|
|
|
|
// Secrets is where credentials are kept. It is the same read/write pair
|
|
// hanzoai/ai declares as object.SecretStore, method for method, so one value
|
|
// satisfies both interfaces and there is one seam in the estate rather than
|
|
// two. What differs is what sits behind it: in ai the store is in-process,
|
|
// here it is KMS across the network.
|
|
//
|
|
// ref is a flat reference, path segments then the name:
|
|
//
|
|
// orgs/acme/users/u-7/connectors/openai/default
|
|
type Secrets interface {
|
|
GetSecret(ctx context.Context, ref string) ([]byte, error)
|
|
PutSecret(ctx context.Context, ref string, value []byte) error
|
|
}
|
|
|
|
// ErrNoCredential means no credential is enrolled for this principal and
|
|
// provider. It is a refusal, never a fallback: reading a key out of this
|
|
// process's environment is the exposure egress exists to remove, so an absent
|
|
// credential ends the call.
|
|
var ErrNoCredential = errors.New("egress: no credential")
|
|
|
|
// Custody scopes. A call reports which one paid, because that is the difference
|
|
// between a customer's vendor bill and ours.
|
|
const (
|
|
// ScopeUser is the customer's own key, enrolled by them, spent only for
|
|
// them.
|
|
ScopeUser = "user"
|
|
// ScopeOrg is the platform key the tenant shares.
|
|
ScopeOrg = "org"
|
|
// ScopeTrust is no key at all: a base of ours, reached over the trust
|
|
// between it and this host. It is a scope because the record has to say
|
|
// what authorized the spend, and "nothing was read" is an answer worth
|
|
// having in a line beside the two that name a credential.
|
|
ScopeTrust = "trust"
|
|
)
|
|
|
|
// ownRef is where a principal's own key lives. Built from the validated
|
|
// principal — this line is the tenant boundary.
|
|
//
|
|
// p.Kind is one of two constants and never a claim, so a person and a program
|
|
// cannot be written into each other's half however either is named.
|
|
func ownRef(p Principal, provider, label string) string {
|
|
return "orgs/" + p.Org + "/" + p.Kind + "/" + p.Name + "/connectors/" + provider + "/" + label
|
|
}
|
|
|
|
// orgRef is where the tenant's shared platform key lives.
|
|
func orgRef(p Principal, provider, label string) string {
|
|
return "orgs/" + p.Org + "/cloud/" + provider + "/" + label
|
|
}
|
|
|
|
// segment reports whether s may be one segment of a custody path. The rule is
|
|
// an allowlist rather than a search for bad input: a separator or a parent
|
|
// reference cannot be spelled with these characters at all, so a path built
|
|
// from segments that pass cannot leave the tenant it was built for.
|
|
func segment(s string) bool {
|
|
if s == "" || len(s) > 64 {
|
|
return false
|
|
}
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
|
|
case (c == '-' || c == '_') && i > 0:
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// slug is the one mapping from a provider as ai spells it ("Amazon Bedrock") to
|
|
// the segment its credential lives under ("amazon-bedrock"). It rejects
|
|
// anything it cannot spell rather than dropping it, because two provider names
|
|
// that differ only in a dropped character would share one credential.
|
|
func slug(provider string) (string, bool) {
|
|
var b strings.Builder
|
|
for i := 0; i < len(provider); i++ {
|
|
c := provider[i]
|
|
switch {
|
|
case c >= 'A' && c <= 'Z':
|
|
b.WriteByte(c + ('a' - 'A'))
|
|
case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
|
|
b.WriteByte(c)
|
|
case c == ' ' && i > 0:
|
|
b.WriteByte('-')
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
s := b.String()
|
|
if !segment(s) {
|
|
return "", false
|
|
}
|
|
return s, true
|
|
}
|
|
|
|
// custody resolves credentials and keeps none. A value read lives for the call
|
|
// that read it and nowhere else — there is no map to find one in, so there is
|
|
// nothing for a core dump, a heap profile or another tenant's call to reach.
|
|
//
|
|
// Reading every time is also what makes rotation a KMS write rather than a
|
|
// deploy: a key rewritten there is in use on the next call, not the next
|
|
// window.
|
|
type custody struct {
|
|
store Secrets
|
|
}
|
|
|
|
func newCustody(store Secrets) *custody {
|
|
return &custody{store: store}
|
|
}
|
|
|
|
// resolve returns the credential to spend for this principal and provider, and
|
|
// which custody paid. The customer's own key wins over the platform's: a tenant
|
|
// that brought a key expects it to be the one spent.
|
|
//
|
|
// It never returns the credential to a caller — only to the call that is about
|
|
// to make an upstream request with it. There is no other reader.
|
|
func (c *custody) resolve(ctx context.Context, p Principal, provider, label string) (string, string, error) {
|
|
own, err := c.read(ctx, ownRef(p, provider, label))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if own != "" {
|
|
return own, ScopeUser, nil
|
|
}
|
|
shared, err := c.read(ctx, orgRef(p, provider, label))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if shared != "" {
|
|
return shared, ScopeOrg, nil
|
|
}
|
|
return "", "", ErrNoCredential
|
|
}
|
|
|
|
// enroll seals a customer's own key. It is write-only: nothing in this package
|
|
// reads a credential back out to a caller, so a key that goes in here can be
|
|
// spent and never shown — not to the customer who supplied it, not to an
|
|
// operator.
|
|
func (c *custody) enroll(ctx context.Context, p Principal, provider, label, key string) error {
|
|
if err := c.store.PutSecret(ctx, ownRef(p, provider, label), []byte(key)); err != nil {
|
|
return fmt.Errorf("egress: seal: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// read returns the value at ref. An absent secret is ("", nil) — the normal
|
|
// state of a tenant that brought no key of its own, and the reason the caller
|
|
// falls through to the shared one. A store that cannot answer is an error and
|
|
// ends the call: an unreachable KMS must not become a reason to look somewhere
|
|
// less guarded.
|
|
func (c *custody) read(ctx context.Context, ref string) (string, error) {
|
|
value, err := c.store.GetSecret(ctx, ref)
|
|
if err != nil {
|
|
if !absent(err) {
|
|
return "", fmt.Errorf("egress: read credential: %w", err)
|
|
}
|
|
return "", nil
|
|
}
|
|
return strings.TrimSpace(string(value)), nil
|
|
}
|
|
|
|
// absent reports whether an error means "no such secret" rather than "the store
|
|
// could not answer". The two are opposite signals: the first is the ordinary
|
|
// state of a tenant with no key of its own and sends the caller on to the shared
|
|
// key, the second is a fault that must end the call.
|
|
//
|
|
// Asked by identity, never by wording. The store reports a fault by echoing the
|
|
// response body into the error, so a 500 whose body happens to contain "not
|
|
// found" — an authorization failure, a proxy's error page, a tenant that no
|
|
// longer exists — used to read as absence. That is the one misreading whose cost
|
|
// is a customer's own key being passed over in favour of the platform's while
|
|
// theirs was merely unreadable, and the meter then recording it as intended.
|
|
func absent(err error) bool {
|
|
return errors.Is(err, kms.ErrSecretNotFound) || errors.Is(err, zapclient.ErrNotFound)
|
|
}
|