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 ¶
var ErrNotFound = errors.New("no source could resolve this secret")
ErrNotFound means no source could resolve a reference.
Functions ¶
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.
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 ¶
New returns a broker. Sources are consulted in order, so the most specific should come first.
func (*Broker) Resolve ¶
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.
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
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.
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.
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 ¶
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.
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.
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.