broker

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package broker resolves ${secret:...} references and revokes what it minted.

It is deliberately a consumer rather than a store. The ecosystem converged on one shape years ago -- `op run --`, `sops exec-env`, `dotenvx run --`, `vault read`, `infisical run --` all inject values into a subprocess and hold nothing -- so the useful thing to build is something that speaks to all of them, not a competitor that asks people to move their secrets again.

What is worth building is the part none of them do: tying a credential's lifetime to a bay's. A long-lived key handed to an environment that a coding agent drives is a key that outlives every mistake made with it. Where a provider can mint a short-lived scoped credential, devbay mints one per bay and revokes it when the bay is destroyed.

Three rules hold throughout:

  • A value is fetched when a container is created, never earlier, and is never written to disk by devbay.
  • Every grant is recorded in an append-only log: which reference, which bay, which provider, when. The value itself is never recorded.
  • Anything minted is revoked on teardown. A credential that outlives the bay it was issued for is the same class of bug as a leaked container.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("no source could resolve this secret")

ErrNotFound means no source could resolve a reference.

Functions

func EnvName

func EnvName(ref string) string

EnvName is the variable a reference falls back to: "stripe/test" becomes DEVBAY_SECRET_STRIPE_TEST.

Types

type Audit

type Audit struct {
	// contains filtered or unexported fields
}

Audit is an append-only record of credential grants and revocations.

Append-only in the way that matters here: it is opened for append, each event is one line, and nothing rewrites it. That is not tamper-proofing -- anything with write access can still truncate the file -- but it does mean a crash cannot lose earlier entries and a concurrent writer cannot interleave half a line into another.

func OpenAudit

func OpenAudit(path string) (*Audit, error)

OpenAudit opens or creates the log. An empty path uses ~/.devbay/audit.jsonl.

func (*Audit) Events

func (a *Audit) Events() ([]Event, error)

Events reads the log back, oldest first.

func (*Audit) Path

func (a *Audit) Path() string

Path reports where the log lives, for `devbay doctor`.

func (*Audit) Record

func (a *Audit) Record(e Event) error

Record appends an event.

type Broker

type Broker struct {
	Log func(format string, args ...any)
	// contains filtered or unexported fields
}

Broker resolves references through an ordered list of sources.

func New

func New(audit *Audit, sc *scrub.Scrubber, logf func(string, ...any)) *Broker

New returns a broker. Sources are consulted in order, so the most specific should come first.

func (*Broker) Add

func (b *Broker) Add(s Source)

Add registers a source.

func (*Broker) Grants

func (b *Broker) Grants(bay string) []Grant

Grants returns the credentials currently held for a bay, without values.

func (*Broker) Lookup

func (b *Broker) Lookup(bay string) func(string) (string, bool)

Lookup adapts a broker to the resolver's signature.

func (*Broker) Resolve

func (b *Broker) Resolve(ctx context.Context, bay, ref string) (string, error)

Resolve fetches a value for a bay.

The value is registered with the scrubber in the same call that produces it. Resolving without doing so would hand an application a credential devbay could no longer recognise in that application's own logs, which is precisely where credentials leak.

func (*Broker) Revoke

func (b *Broker) Revoke(ctx context.Context, bay string) error

Revoke destroys every credential minted for a bay.

Called from teardown. A credential that outlives the bay it was issued for is the same class of bug as a container that survives `devbay rm`: invisible, and discovered later by someone else.

type CommandSource

type CommandSource struct {
	// Label names the source in the audit log.
	Label string
	// Argv is the command, with {ref} substituted.
	Argv []string
	// Prefix limits this source to references beginning with it, so several
	// managers can coexist.
	Prefix string
	// Timeout bounds the call; a secret manager that hangs must not hang a boot.
	Timeout time.Duration
}

CommandSource shells out to a secret manager.

The command is configured by the developer, not by a manifest, which is what makes it safe to run: a manifest cannot express a command, and this one is not derived from repository content. `{ref}` in the argv is replaced with the reference being resolved.

op:     op read op://{ref}
sops:   sops --extract '["{ref}"]' -d secrets.enc.yaml
vault:  vault kv get -field=value secret/{ref}

func (CommandSource) Handles

func (c CommandSource) Handles(ref string) bool

func (CommandSource) Name

func (c CommandSource) Name() string

func (CommandSource) Resolve

func (c CommandSource) Resolve(ctx context.Context, _, ref string) (string, *Grant, error)

type EnvSource

type EnvSource struct{}

EnvSource reads DEVBAY_SECRET_<REF> from the environment.

This is the integration with every tool listed at the top of this file: `op run -- devbay run ...` has already put the values in devbay's environment, and this finds them there without devbay knowing anything about 1Password.

func (EnvSource) Handles

func (EnvSource) Handles(string) bool

func (EnvSource) Name

func (EnvSource) Name() string

func (EnvSource) Resolve

func (EnvSource) Resolve(_ context.Context, _, ref string) (string, *Grant, error)

type Event

type Event struct {
	At        time.Time `json:"at"`
	Action    string    `json:"action"`
	Bay       string    `json:"bay,omitempty"`
	Ref       string    `json:"ref,omitempty"`
	Provider  string    `json:"provider,omitempty"`
	Minted    bool      `json:"minted,omitempty"`
	ExpiresAt time.Time `json:"expires_at,omitempty"`
	Detail    string    `json:"detail,omitempty"`
}

Event is one line in the credential log.

There is no value field, and there must never be one. The log exists so a developer can answer "what was this environment given, and when" months later; a log that answers it by storing the credentials would be a worse leak than the one it was meant to detect.

func (Event) String

func (e Event) String() string

String renders an event for a human.

type GitHubApp

type GitHubApp struct {
	// AppID is the application's numeric id.
	AppID string
	// PrivateKeyPEM is the app's signing key. Read from disk by the caller so
	// this type never touches the filesystem.
	PrivateKeyPEM []byte
	// InstallationID is used when a reference does not name one.
	InstallationID string
	// Repositories narrows the token. Empty means every repository the
	// installation can see, which is worth avoiding.
	Repositories []string
	// Permissions narrows it further, e.g. {"contents": "read"}.
	Permissions map[string]string

	// BaseURL is the API root; overridden in tests and for GitHub Enterprise.
	BaseURL string
	// HTTP is the client used; overridden in tests.
	HTTP *http.Client
	// Now is the clock, overridden in tests.
	Now func() time.Time
}

GitHubApp mints installation access tokens.

This provider exists because GitHub is the one common case where every part of the ephemeral-credential story actually works. An installation token lasts exactly one hour and cannot be extended; it can be narrowed to specific repositories and specific permissions; and, unusually, it can be revoked outright. Most providers offer at best a short lifetime and no way to end it early -- AWS STS has no per-session revocation at all, only a policy that invalidates every session issued before now.

So a bay that needs GitHub access gets a token scoped to the repositories it works on, and destroying the bay destroys the token rather than waiting an hour for it to lapse.

A reference is "github/<installation-id>" or just "github" when DEVBAY_GITHUB_INSTALLATION_ID is set.

func GitHubAppFromEnv

func GitHubAppFromEnv() (*GitHubApp, error)

GitHubAppFromEnv builds a provider from the environment, or returns nil when it is not configured.

Configuration comes from the environment rather than from a manifest by design: a private key is exactly the kind of thing that must never be expressible in a file the introspection agent can write.

func (*GitHubApp) Handles

func (g *GitHubApp) Handles(ref string) bool

func (*GitHubApp) Name

func (g *GitHubApp) Name() string

func (*GitHubApp) Resolve

func (g *GitHubApp) Resolve(ctx context.Context, bay, ref string) (string, *Grant, error)

Resolve mints a token for a bay.

type Grant

type Grant struct {
	Ref      string    `json:"ref"`
	Provider string    `json:"provider"`
	Bay      string    `json:"bay"`
	IssuedAt time.Time `json:"issued_at"`
	// ExpiresAt is zero for a credential with no known lifetime, which is
	// itself worth seeing in the log.
	ExpiresAt time.Time `json:"expires_at,omitempty"`
	// Minted distinguishes a credential devbay created, and must therefore
	// destroy, from one it merely read.
	Minted bool `json:"minted"`
	// contains filtered or unexported fields
}

Grant is one credential issued to one bay.

func (*Grant) Expired

func (g *Grant) Expired(now time.Time) bool

Expired reports whether a grant has passed its stated lifetime.

type Source

type Source interface {
	// Name identifies the source in the audit log.
	Name() string
	// Handles reports whether this source claims a reference.
	Handles(ref string) bool
	// Resolve returns the value and, when it minted one, a revocable grant.
	Resolve(ctx context.Context, bay, ref string) (string, *Grant, error)
}

Source resolves references of a particular shape.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL