Documentation
¶
Overview ¶
Package secretbroker brokers scoped, time-limited credential grants to execution backends.
The problem it replaces: pkg/secret is a flat key→value map with an all-or-nothing EnvLines(). Every workload that got any secret got all of them, forever, as plain environment variables. That is workable when the only executor is "a child process of the control plane owned by the same person who wrote the secrets down". It is not workable once workloads run in containers, on remote edge devices, and on behalf of different tenants: a single leaked GitHub PAT then reaches every repository the operator can touch, from a machine the operator does not control.
The model here is three-part:
Secret what the credential is (kind + sealed payload). Never leaves
this package in plaintext except inside a Material.
Grant who may use it, under what constraints, until when. Constraints
are kind-specific and enforced at delivery, not documented and
hoped for.
Lease a short-lived, minimized materialisation of the grants that
match one (executor, project) subject. Executors receive leases;
they never receive the store.
Enforcement, honestly scoped. Three of the constraint kinds are enforced by construction, because the broker rewrites the payload before it is delivered and a narrower payload cannot be widened by its holder:
- kubeconfig is rewritten to contain only the allowed contexts, their referenced clusters/users, and an allowed namespace pinned on each;
- registry credentials are filtered to the allowed registries;
- env secrets are filtered to the allowed keys.
github_pat never reaches the sandbox when a GitGuard is configured — the hub's git proxy (executors.git_proxy) holds the token and the workload gets a session scoped to the grant (see gitguard.go). Without one it is enforced at the point of use rather than by construction: GitHub has no API to narrow an already-issued PAT, so instead of exporting a bare GITHUB_TOKEN the broker emits a git credential helper that releases the token only for paths matching the allowlist (see githubpat.go). A bare GITHUB_TOKEN is exported only when the allowlist is explicitly "*".
egress_proxy carries its allowlist to the executor, whose network policy is the enforcement point; in-process callers gate on Constraints.AllowsHost. This is the one kind whose enforcement lives outside this package, and it is called out here so nobody mistakes it for the kubeconfig guarantee.
Fail-closed is the rule throughout: an unparseable constraint, an unsatisfiable one, an expired or revoked grant, or a payload that minimizes to nothing all produce a denial and an audit row — never a wider credential.
Index ¶
- Constants
- Variables
- func AADFor(set, rowID string) string
- func ExecutorBindings(leaseID string, expiresAt time.Time, raw []LeaseBinding) []executor.SecretBinding
- func ExecutorSecretFiles(leaseID string, raw []DeliveredFile) []executor.SecretFile
- func GitHubAppPermissions(c Constraints) (map[string]string, error)
- func GitHubPATPayload(token, host string) ([]byte, error)
- func KubeconfigSummary(minimized []byte) string
- func LegacyKeyOpens(roots []string, saltHex string, sample []byte) bool
- func MarshalAppSlotScope(r AppSlotRecord) (string, error)
- func MinimizeKubeconfig(raw []byte, c Constraints) ([]byte, error)
- func NewLeaseDirPath(baseDir string) (string, error)
- func NormalizeHost(host string) (string, error)
- func NormalizeOwner(identity string) string
- func NormalizeProjectID(id string) string
- func NormalizeRepo(repo string) (string, error)
- func OnGrantRevoked(fn func(context.Context, GrantRevocation)) (cancel func())
- func ParseLocalRepoRoot(payload []byte) (string, error)
- func RedactString(s string) string
- func SafeRef(ref string) string
- func SandboxLeaseDir(leaseID string) string
- func SecretForgeHost(s Secret) (host string, known bool)
- func SelfRenewingKubeconfig(s Secret) bool
- func UnmarshalAppSlotScope(raw string, r *AppSlotRecord) error
- func ValidateName(name string) error
- type AccessRequest
- type AccessRequestInput
- type Action
- type AppCredential
- type AppInstallation
- type AppSlotRecord
- type AppSlotStore
- type AppTokenRefresh
- type Auditor
- type AuditorFunc
- type Broker
- func (b *Broker) AcknowledgeUnguarded(ctx context.Context, req UnguardedAckRequest) (Grant, error)
- func (b *Broker) AppTokensDue(leaseID string) bool
- func (b *Broker) ApproveRequest(ctx context.Context, in DecideInput) (AccessRequest, Grant, error)
- func (b *Broker) CheckRepoAccess(ctx context.Context, r Requester, repo, actor string) error
- func (b *Broker) DeleteSecret(ctx context.Context, ref, actor string) error
- func (b *Broker) DeleteSecretBecause(ctx context.Context, ref, actor string, cause DeletionCause, reason string) error
- func (b *Broker) DeleteSecretFor(ctx context.Context, ref string, v Viewer) error
- func (b *Broker) DenyRequest(ctx context.Context, in DecideInput) (AccessRequest, error)
- func (b *Broker) DescribeSecret(ref string) (Secret, error)
- func (b *Broker) DescribeSecretFor(ref string, v Viewer) (Secret, error)
- func (b *Broker) DiscoverGitHubAppInstallations(ctx context.Context, payload []byte) ([]AppInstallation, error)
- func (b *Broker) DropLeaseGrant(ctx context.Context, leaseID, grantID, reason string) (int, error)
- func (b *Broker) EnvKeyNamesFor(ref string, v Viewer) ([]string, error)
- func (b *Broker) EnvOffers(r Requester) ([]EnvOffer, error)
- func (b *Broker) ExpireRequests(ctx context.Context) ([]AccessRequest, error)
- func (b *Broker) Extend(ctx context.Context, leaseID string) (time.Time, error)
- func (b *Broker) GetRequest(id string) (AccessRequest, error)
- func (b *Broker) GitHubAppRepositories(ctx context.Context, ref string) ([]InstallationRepo, error)
- func (b *Broker) GitHubAppRepositoriesFor(ctx context.Context, ref string, v Viewer) ([]InstallationRepo, error)
- func (b *Broker) GitHubGrantHost(leaseID, grantID string) (string, error)
- func (b *Broker) GitHubUpstream(ctx context.Context, leaseID, grantID, sessionID string) (UpstreamGitHub, error)
- func (b *Broker) Grant(ctx context.Context, req GrantRequest) (Grant, error)
- func (b *Broker) HeldAppTokenExpiry(leaseID string) time.Time
- func (b *Broker) HeldElsewhere(leaseID string) bool
- func (b *Broker) HeldGrant(leaseID, grantID string) (Grant, error)
- func (b *Broker) HeldGrantIDs(leaseID string) []string
- func (b *Broker) HeldLease(leaseID string) (*Lease, bool)
- func (b *Broker) HeldOn(leaseID, grantID string) []string
- func (b *Broker) HoldsFileAppTokens(leaseID string) bool
- func (b *Broker) HubCredential(ref string) ([]byte, error)
- func (b *Broker) KeepsLeaseRevocations() bool
- func (b *Broker) Keyring() *Keyring
- func (b *Broker) KubeconfigUpstream(leaseID, grantID string) ([]byte, error)
- func (b *Broker) Lease(ctx context.Context, executorID, projectID string) (*Lease, error)
- func (b *Broker) LeaseFor(ctx context.Context, r Requester, actor string) (*Lease, error)
- func (b *Broker) LeaseHolder() string
- func (b *Broker) LeaseHorizon(r Requester) (first Grant, ok bool, err error)
- func (b *Broker) LeaseRecordFor(leaseID string) (LeaseRecord, error)
- func (b *Broker) LeaseRecords() ([]LeaseRecord, error)
- func (b *Broker) LeaseRevocationsFor(leaseID string) ([]LeaseRevocation, error)
- func (b *Broker) ListGrants(f GrantFilter) ([]Grant, error)
- func (b *Broker) ListGrantsFor(f GrantFilter, v Viewer) ([]Grant, error)
- func (b *Broker) ListRequests(f RequestFilter) ([]AccessRequest, error)
- func (b *Broker) ListSecrets() ([]Secret, error)
- func (b *Broker) ListSecretsFor(v Viewer) ([]Secret, error)
- func (b *Broker) LookupGrant(id string) (Grant, error)
- func (b *Broker) Mint(ctx context.Context, req MintRequest) (Secret, error)
- func (b *Broker) NoteLeaseRevocation(leaseID, grantID, detail string) error
- func (b *Broker) PendingLeaseRevocations() ([]LeaseRevocation, error)
- func (b *Broker) ReconfirmSupersededSlots()
- func (b *Broker) RecordHeldOn(rec LeaseRecord, grantID string) []string
- func (b *Broker) RecordLeaseRevocation(rev LeaseRevocation) (LeaseRevocation, bool, error)
- func (b *Broker) RedactionValues(leaseID string) []string
- func (b *Broker) RefreshAppToken(ctx context.Context, leaseID, grantID, held string) (AppTokenRefresh, error)
- func (b *Broker) RefreshLeaseFiles(ctx context.Context, lease *Lease) (*LeaseFileRefresh, error)
- func (b *Broker) RefuseRenewal(ref, actor, reason string) error
- func (b *Broker) Release(leaseID string)
- func (b *Broker) Renew(ctx context.Context, leaseID string) (*Lease, error)
- func (b *Broker) RenewSecret(ctx context.Context, req RenewSecretRequest) (Secret, error)
- func (b *Broker) RequestAccess(ctx context.Context, in AccessRequestInput) (AccessRequest, error)
- func (b *Broker) RequestUses(requestID string) ([]RequestUse, error)
- func (b *Broker) Restore(ctx context.Context, leaseID string) (*Lease, error)
- func (b *Broker) RetireRecord(rec LeaseRecord, reason string) (bool, error)
- func (b *Broker) RetireSuperseded(ctx context.Context, leaseID string)
- func (b *Broker) Revoke(ctx context.Context, grantID, actor string) error
- func (b *Broker) RevokeBecause(ctx context.Context, grantID, actor, reason string) error
- func (b *Broker) RevokeGrant(ctx context.Context, req RevokeGrantRequest) (GrantRevocation, bool, error)
- func (b *Broker) RevokeWithCause(ctx context.Context, grantID, actor string, cause RevocationCause, ...) error
- func (b *Broker) SettleLeaseRevocation(leaseID, grantID, by, detail string) (bool, error)
- func (b *Broker) Supersede(ctx context.Context, grantID, successorID, actor string) (GrantRevocation, bool, error)
- func (b *Broker) SuspendLeaseTokens(leaseID, reason string)
- func (b *Broker) SweepExpired() (expired, live map[Kind]int)
- func (b *Broker) Tombstone(secretID string) (Tombstone, bool)
- func (b *Broker) UpdateSecretMetadata(ref string, mutate func(map[string]string)) (Secret, error)
- func (b *Broker) WithdrawRequest(ctx context.Context, id, actor string) (AccessRequest, error)
- func (b *Broker) WithdrawRequestOnOffboard(ctx context.Context, id, actor, reason string) (AccessRequest, error)
- func (b *Broker) WithdrawUnguardedAck(ctx context.Context, req UnguardedAckRequest) (Grant, error)
- func (b *Broker) WithdrawnGrants(leaseID string) []WithdrawnGrant
- type CausedRevoker
- type Cipher
- type Constraints
- func (c Constraints) AllowsContext(name string) bool
- func (c Constraints) AllowsEnvKey(key string) bool
- func (c Constraints) AllowsHost(host string) bool
- func (c Constraints) AllowsNamespace(ns string) bool
- func (c Constraints) AllowsPermission(perm string) bool
- func (c Constraints) AllowsRegistry(reg string) bool
- func (c Constraints) AllowsRepo(repo string) bool
- func (c Constraints) BranchNames() []string
- func (c Constraints) BranchRefPatterns() []string
- func (c Constraints) CheckHost(host string) error
- func (c Constraints) CheckRepo(repo string) error
- func (c Constraints) Covers(old Constraints) (bool, string)
- func (c Constraints) KubeMonitorOnly() []string
- func (c Constraints) KubeReadOnly() bool
- func (c Constraints) KubeVerbs() []string
- func (c Constraints) RestrictsBranches() bool
- func (c Constraints) Summary() string
- func (c Constraints) ValidateFor(kind Kind) error
- type DecideInput
- type Decision
- type Delegation
- type DeletionCause
- type DeliveredFile
- type Delivery
- func (d *Delivery) Bindings() []LeaseBinding
- func (d *Delivery) Close() error
- func (d *Delivery) Devices() []GrantedDevice
- func (d *Delivery) Env() []string
- func (d *Delivery) Files() []DeliveredFile
- func (d *Delivery) Interfaces() []GrantedInterface
- func (d *Delivery) Mounts() []RepoMount
- func (d *Delivery) Withdraw(grantID string) int
- type EnvOffer
- type Envelope
- type Event
- type File
- type GitGuard
- type GitGuardRequest
- type GitGuardResult
- type GitHubAppAPI
- type Grant
- type GrantFilter
- type GrantRequest
- type GrantRevocation
- type GrantedDevice
- type GrantedInterface
- type InstallationRepo
- type InstallationToken
- type InstallationTokenRequest
- type KEKRecord
- type KeyAttributor
- type KeyCheck
- type KeyInfo
- type KeyStore
- type KeyUsage
- type Keyring
- func (k *Keyring) AddKey(actor string) (KeyInfo, error)
- func (k *Keyring) HasLegacy() bool
- func (k *Keyring) HasPrevious() bool
- func (k *Keyring) Keys() []KeyInfo
- func (k *Keyring) OpenEnvelope(aad string, env Envelope) ([]byte, error)
- func (k *Keyring) PreviousRootKeys() []string
- func (k *Keyring) PrimaryFromCurrent() bool
- func (k *Keyring) PrimaryID() string
- func (k *Keyring) Reload() error
- func (k *Keyring) RetireKey(id string) error
- func (k *Keyring) Rewrap(aad string, env Envelope) (Envelope, error)
- func (k *Keyring) RootFingerprints() (current, previous string, err error)
- func (k *Keyring) SealFor(aad string, plaintext []byte) (Envelope, error)
- func (k *Keyring) Underivable() []string
- type KeyringOption
- type Kind
- type KubeGrantDescription
- type KubeGuard
- type KubeGuardRequest
- type KubeGuardResult
- type Lease
- func (l *Lease) Deliver(dir string) (*Delivery, error)
- func (l *Lease) Empty() bool
- func (l *Lease) Expired(now time.Time) bool
- func (l *Lease) Kinds() []Kind
- func (l *Lease) Materialize(baseDir string) (*Mount, error)
- func (l *Lease) MaterializeAt(dir string) (*Mount, error)
- func (l *Lease) SecretNames() []string
- func (l *Lease) TTL(now time.Time) time.Duration
- type LeaseBinding
- type LeaseFileRefresh
- type LeaseGrantStore
- type LeaseRecord
- type LeaseRevocation
- type LeaseRevocationStore
- type LeaseStore
- type LegacyImportResult
- type Locator
- type Material
- type MintRequest
- type Mount
- type Option
- func WithAuditor(a Auditor) Option
- func WithCipher(c *Cipher) Option
- func WithClock(fn func() time.Time) Option
- func WithGitHubApp(api GitHubAppAPI) Option
- func WithKeyring(kr *Keyring) Option
- func WithLeaseRecords(holder string) Option
- func WithMaxLeaseTTL(d time.Duration) Option
- func WithoutKey() Option
- type RefusedCredential
- type RefusedRefresh
- type RenewSecretRequest
- type RenewedPayload
- type RepoMount
- type RequestFilter
- type RequestState
- type RequestStore
- type RequestUse
- type Requester
- type RetirePreviousOptions
- type RetirePreviousReport
- type RevocationCause
- type RevokeGrantRequest
- type RevokedLeaseError
- type RotateOptions
- type RotationRecord
- type RotationReport
- type RotationStatus
- type RotationStore
- type Rotator
- func (r *Rotator) RetireKey(id string) error
- func (r *Rotator) RetirePrevious(ctx context.Context, opts RetirePreviousOptions) (RetirePreviousReport, error)
- func (r *Rotator) Rotate(ctx context.Context, opts RotateOptions) (RotationReport, error)
- func (r *Rotator) Status(historyLimit int) (RotationStatus, error)
- func (r *Rotator) WithBatch(n int) *Rotator
- func (r *Rotator) WithHistory(h RotationStore) *Rotator
- func (r *Rotator) WithProgress(fn func(RotationRecord)) *Rotator
- type SealedRow
- type SealedSet
- type Secret
- func (s Secret) DeletableBy(v Viewer) bool
- func (s Secret) Envelope() Envelope
- func (s Secret) OwnedBy(identity string) bool
- func (s Secret) Personal() bool
- func (s Secret) SpendableBy(v Viewer) bool
- func (s Secret) Validate() error
- func (s Secret) VisibleTo(v Viewer) bool
- func (s Secret) WithEnvelope(env Envelope) Secret
- type SetReport
- type Store
- type Subject
- type SubjectType
- type SupersedingRevoker
- type Tombstone
- type TombstoneStore
- type UnavailableGitGuard
- type UnavailableKubeGuard
- type UnguardedAck
- type UnguardedAckRequest
- type UnguardedAcknowledger
- type UpstreamGitHub
- type Viewer
- type WithdrawnGrant
Constants ¶
const ( // GitPushRefsEnvKey is the hub's own ref allowlist for pushes, as full ref // patterns: "refs/heads/cloop/**" on a hub that kept the default. GitPushRefsEnvKey = "CLOOP_GIT_PUSH_REFS" // GitHubPushBranchesEnvKey is the grant's branch allowlist, as branch // names ("feature/*"), when it has one. A push must match both lists. GitHubPushBranchesEnvKey = "CLOOP_GITHUB_PUSH_BRANCHES" )
The variables a guarded lease uses to say where a push will be accepted. Exported for pkg/pm, which renders them into the agent's prompt.
const ( // LegacyKeyID names material sealed by the pre-envelope construction: // ciphertext directly under the passphrase-derived key, with no wrapped // DEK. Migration 0019 stamps it onto every pre-existing row so "sealed // the old way" is an explicit state rather than an inference from an // empty column. LegacyKeyID = "legacy" // KEK lifecycle states. Exactly one KEK is primary (new material is // sealed under it); any number may be active (openable, still referenced // by rows); retired KEKs have had their salt destroyed and can no longer // be derived at all. KEKStatePrimary = "primary" KEKStateActive = "active" KEKStateRetired = "retired" )
const ( SetSecrets = "secrets" SetSessions = "sessions" // SetHubTLS holds the private keys of the hub's own CA and of the // certificates it issues its credential monitors (pkg/hubca, Task 20409). SetHubTLS = "hub_tls" )
Sealed-set names. They are part of every envelope's associated data, so they are a wire format: changing one makes existing rows in that set unopenable.
Namespacing by set is not cosmetic. Without it, an attacker with database write access could copy a secret's (wrapped_dek, payload) into a sessions row whose id equals that secret's id, and the hub would authenticate it happily and then POST the kubeconfig to the identity provider as if it were a refresh token — an exfiltration channel out of the sealed store to an external host. Row ids alone cannot prevent that, because they are chosen by whoever writes the row.
const ( RootCurrent = "current" RootPrevious = "previous" )
Which configured root derives a key, as KeyInfo reports it.
const ( // KubeContextsEnvKey lists every context of the merged kubeconfig, // current-context first, comma-separated. KubeContextsEnvKey = "CLOOP_K8S_CONTEXTS" // KubeGrantsEnvKey describes every kubeconfig grant as a JSON array of // KubeGrantDescription, in the order of KubeContextsEnvKey. KubeGrantsEnvKey = "CLOOP_K8S_GRANTS" // KubeGuardSessionsEnvKey lists every monitored grant's session id, // comma-separated. Not a credential, for the reason KubeGuardSessionEnvKey // is not. KubeGuardSessionsEnvKey = "CLOOP_KUBEGUARD_SESSIONS" )
The variables a lease with several kubeconfig grants adds.
const ( // share's token. KubeShareMetaRenewal = "kube.renewal" KubeShareRenewalHub = "hub" )
const ( // DefaultRequestTTL is how long a request waits for a decision before it // lapses. // // Bounded at all because the failure mode of an unbounded queue is not that // it grows — it is that a stale request gets approved. Three days is long // enough to survive a weekend and short enough that an approval is still // being made against a reason the approver can evaluate. DefaultRequestTTL = 72 * time.Hour // MaxRequestTTL caps how long a request may wait, whatever the caller asks // for. Same argument, applied to the caller who would rather it never // lapsed. MaxRequestTTL = 30 * 24 * time.Hour // DefaultDelegationMaxTTL is the ceiling on a minted grant's lifetime when // an approver's Delegation does not state one. // // Deliberately shorter than a request may ask for: the default answer to // "how long should this credential live" should be days, and an approver who // genuinely needs longer states it rather than gets it by omission. DefaultDelegationMaxTTL = 7 * 24 * time.Hour // MaxDelegationTTL is the longest lifetime any approver may hand out, // whatever their Delegation says. // // Not a security boundary — an approver can always approve again, and one // with a shell on the hub can write the row by hand. What it is, is a floor // under the product's own honesty: the broker exists to replace the // forever-credential, and an approval path that could mint a ten-year grant // in one click would put the thing back exactly where it was found. Ninety // days matches the ceiling the Secrets panel already enforces on // hand-minted grants, so the two doors cannot disagree. MaxDelegationTTL = 90 * 24 * time.Hour // MaxJustificationBytes bounds the free text a request carries. Long enough // for a paragraph and a ticket link; short enough that the field cannot be // used to stuff the control-plane database. MaxJustificationBytes = 4096 )
const ( RotationRunning = "running" RotationCompleted = "completed" RotationInterrupted = "interrupted" RotationFailed = "failed" )
Rotation states.
const AppTokenRefreshWindow = 10 * time.Minute
AppTokenRefreshWindow is how much of an installation token's life may remain when it is re-minted.
Ten minutes leaves room for several attempts — a proxy session tries again after appTokenRefreshBackoff, the lease keepalive every minute — before a failing refresh lets the token lapse, and stays clear of appTokenMinLifetime, the floor below which a freshly minted token is refused outright.
const DefaultGrantTTL = 24 * time.Hour
DefaultGrantTTL is used when a grant is created without an explicit TTL from a path that requires one.
const DefaultMaxLeaseTTL = 15 * time.Minute
DefaultMaxLeaseTTL bounds how long any single lease is valid, regardless of how long its grants last.
The split between grant TTL and lease TTL is the point of the design. An operator reasonably grants CI access to a repository for a day; it does not follow that a container should hold a usable token for a day. Fifteen minutes with renewal keeps long runs working while bounding what a compromised or forgotten executor still holds — and because Renew re-evaluates every grant, a revocation lands within one lease period rather than at the end of the grant.
const DefaultRotationBatch = 128
DefaultRotationBatch is how many rows are listed per round.
Small enough that an interrupted rotation loses almost nothing and that the rotator never holds a large slice of ciphertext, large enough that a big registry does not turn into thousands of round trips.
const EnvPassphraseKey = rootkey.EnvKey
EnvPassphraseKey is the environment variable holding the passphrase that protects sealed payloads. It is the same variable pkg/secret uses, so an existing deployment's key keeps working across the migration. It is one of three places the key may come from; pkg/rootkey resolves them all.
const GitHubHostEnvKey = "CLOOP_GITHUB_HOST"
GitHubHostEnvKey names, in a workload's environment, the forge host its GitHub grants' repositories live on (Task 20423): github.com, or a GitHub Enterprise Server host — several, comma-separated, for a lease with grants on different hosts. Not a credential. Exported for pkg/pm, which tells the agent where to clone from.
const GitHubUsername = "x-access-token"
GitHubUsername is the basic-auth username GitHub expects alongside a PAT. GitHub ignores the value for a PAT but requires a non-empty one, and "x-access-token" is the name its own tooling uses.
const GitHubWriteWithheldEnvKey = "CLOOP_GITHUB_WRITE_WITHHELD"
GitHubWriteWithheldEnvKey carries, into a workload's environment, the reason a GitHub grant that authorises pushes was delivered read-only. Present only when that happened. Exported for pkg/pm, which tells the agent.
const GitProxySessionEnvKey = "CLOOP_GIT_PROXY_SESSION"
GitProxySessionEnvKey is the environment variable a guarded delivery carries the proxy session id in.
Exported because the hub reads it back to revoke the session when the lease is released, and a second copy of the literal in pkg/ui would be a string that could drift — leaving sessions alive with nothing pointing at them.
The id is not secret: the token it names lives in a separate 0600 file, and the proxy's own audit events already carry the id in the clear.
const KubeGuardSessionEnvKey = "CLOOP_KUBEGUARD_SESSION"
KubeGuardSessionEnvKey carries the monitor session's id into the delivered environment, so a lease's sessions can be found again at release time without this package having to learn what a monitor session is.
The id is not a credential — the bearer token is inside the kubeconfig file, at 0600 — so carrying it in the environment costs nothing and buys revocation that lands before the TTL.
const LegacySaltMetaKey = metaKeySalt
LegacySaltMetaKey is where the pre-envelope cipher's salt lives in the broker's metadata table, for callers reading a database without a Store.
const MaxBranchPatterns = 32
MaxBranchPatterns bounds a grant's branch allowlist. A handful is the real shape; the bound exists because the proxy matches every pushed ref against every pattern, and a caller should not get to choose how large that is.
const MaxHostDevices = 16
MaxHostDevices bounds how many devices one grant may open.
It matches executor.MaxDevices because they bound the same list at two ends of the same pipe, and a grant that could mint more than a Spec can carry would fail at dispatch with an error about the Spec rather than about the grant.
const MaxHostInterfaces = 8
MaxHostInterfaces bounds how many interfaces one grant may open.
It matches executor.MaxInterfaces because they bound the same list at two ends of the same pipe, and a grant that could mint more than a Spec can carry would fail at dispatch with an error about the Spec rather than the grant.
const MaxLocalRepos = 32
MaxLocalRepos bounds how many repositories one grant may open.
The limit is legibility, not resource use. A grant matching forty checkouts has stopped describing "these repositories" and started describing "the machine", which is the thing the allowlist exists to prevent someone doing by accident with a broad glob.
const MetaForgeHost = "forge_host"
MetaForgeHost is the metadata key Mint records a GitHub secret's forge host under. It is derived from the payload; a value a caller supplies is replaced.
const SandboxLeaseRoot = "/run/cloop"
SandboxLeaseRoot is the parent directory a lease's files are delivered under inside an executor that does not share the hub's filesystem.
/run is the conventional place for ephemeral runtime state and is a tmpfs on every systemd host, which is the same property the hub's /dev/shm is chosen for. A driver that cannot write there relocates and rewrites the environment; see executor.Spec.RelocateSecrets.
const SandboxRepoRoot = "/repos"
SandboxRepoRoot is where granted repositories appear inside a sandbox.
A fixed path rather than a configurable one so a harness, a setup script and a README can all name it without knowing which executor they landed on. It is outside /workspace on purpose: the workspace is bind-mounted whole by the container driver, so a target underneath it would be shadowed by that mount on some drivers and not others.
Variables ¶
var ( // ErrInvalidKind: the secret kind is not one of the known kinds. ErrInvalidKind = errors.New("secretbroker: invalid secret kind") // ErrInvalidSecret: the secret failed structural validation. ErrInvalidSecret = errors.New("secretbroker: invalid secret") // ErrInvalidGrant: the grant failed structural validation. ErrInvalidGrant = errors.New("secretbroker: invalid grant") // ErrInvalidSubject: the grant subject could not be parsed or is empty. ErrInvalidSubject = errors.New("secretbroker: invalid subject") // ErrInvalidConstraint: a constraint pattern is malformed, unsafe to // embed, or missing where its kind requires one. ErrInvalidConstraint = errors.New("secretbroker: invalid constraint") // ErrSecretNotFound: no secret with that ID or name exists. ErrSecretNotFound = errors.New("secretbroker: secret not found") // ErrSecretDeleted: the secret a grant points at existed and was // destroyed, and its tombstone says what it was (Task 20400). It wraps // ErrSecretNotFound, so every caller that treats a missing secret as final // treats a deleted one the same way; the difference is that the denial // carrying it can name the credential instead of quoting an id. ErrSecretDeleted = fmt.Errorf("%w: deleted", ErrSecretNotFound) // ErrGrantNotFound: no grant with that ID exists. ErrGrantNotFound = errors.New("secretbroker: grant not found") // ErrLeaseNotFound: the lease ID is unknown or already released. ErrLeaseNotFound = errors.New("secretbroker: lease not found") // ErrDuplicateName: a secret with that name already exists. ErrDuplicateName = errors.New("secretbroker: secret name already in use") // ErrGrantExpired: the grant's TTL elapsed. ErrGrantExpired = errors.New("secretbroker: grant expired") // ErrGrantRevoked: the grant was revoked. ErrGrantRevoked = errors.New("secretbroker: grant revoked") // ErrGrantSuspended: the grant's owner was deactivated at the identity // provider, and the grant is refused until they are reactivated (Task // 20415). ErrGrantSuspended = errors.New("secretbroker: grant suspended") // ErrGrantWithheld: the grant matched, and the requester asked for it not // to be delivered (Requester.Withhold). A denial like the two above, so // the lease's trail says which credential a run did not get, and why. ErrGrantWithheld = errors.New("secretbroker: grant withheld from this lease") // ErrLeaseExpired: the lease's TTL elapsed; Renew or re-Lease. ErrLeaseExpired = errors.New("secretbroker: lease expired") // ErrLeaseMoved: another hub process holds the lease now — it adopted the // run the lease was issued for (Task 20382) — so this one may neither // extend nor release it. ErrLeaseMoved = errors.New("secretbroker: lease is held by another hub process") // ErrLeaseRevoked: an operator, or a cordon, revoked the lease (Task // 20419). The revocation is recorded, so no hub process may take the // lease over or keep it alive afterwards — including one that adopts its // run after the holder that heard of it died. ErrLeaseRevoked = errors.New("secretbroker: lease revoked") // ErrRepoDenied: the repository is outside the grant's allowlist, or // could not be normalised into owner/repo form. ErrRepoDenied = errors.New("secretbroker: repository denied") // ErrNamespaceDenied: the Kubernetes namespace is outside the allowlist. ErrNamespaceDenied = errors.New("secretbroker: namespace denied") // ErrHostDenied: the host is outside the egress allowlist, or could not // be normalised. ErrHostDenied = errors.New("secretbroker: host denied") // ErrMinimizedEmpty: minimising the payload against the grant's // constraints left nothing to deliver. This is a denial, not an empty // success — delivering an empty kubeconfig would look like a working // credential that mysteriously cannot reach anything. ErrMinimizedEmpty = errors.New("secretbroker: nothing remains after minimization") // ErrMalformedPayload: the stored payload is not in the shape its kind // requires (e.g. a kubeconfig that is not valid YAML). ErrMalformedPayload = errors.New("secretbroker: malformed secret payload") // ErrGitHubAppMint: a GitHub App installation token could not be minted. // The grant is denied — the private key is never delivered as a fallback. ErrGitHubAppMint = errors.New("secretbroker: github app token mint failed") // ErrGitHubAppRevoke: a minted installation token could not be destroyed // at GitHub. It still lapses on GitHub's own expiry, but until then it is // a credential nothing here can take back. ErrGitHubAppRevoke = errors.New("secretbroker: github app token revoke failed") // ErrGitHubAppRefused: GitHub answered a mint with a refusal that asking // again will not change — the installation is suspended or gone, the // App's key was revoked, or a repository the token is scoped to is no // longer in the installation (Task 20375). Always wrapped together with // ErrGitHubAppMint, so a caller that only knows the older sentinel still // matches. A network failure, a 5xx or a rate limit is not one: those are // retried. ErrGitHubAppRefused = errors.New("secretbroker: github refused the installation token") // ErrRefreshRefused: a GitHub App token a lease holds could not be // re-minted, and will not be on a later attempt either — the grant was // revoked or expired, the lease was released, or GitHub refused // (ErrGitHubAppRefused). Access ends as it does on revocation: the tokens // are destroyed at GitHub and a proxy session presenting them is closed. // Wrapped together with the sentinel that names the cause. ErrRefreshRefused = errors.New("secretbroker: github app token refresh refused") // ErrNoKey: CLOOP_SECRET_KEY is unset, so payloads can be neither // sealed nor opened. ErrNoKey = errors.New("secretbroker: CLOOP_SECRET_KEY is not set") // ErrSealFailed: encryption or decryption failed (wrong key, or a // corrupt/tampered envelope — AES-GCM cannot tell you which). ErrSealFailed = errors.New("secretbroker: seal/unseal failed") // ErrKeyRetired: the material names a KEK whose salt was destroyed. The // material is unrecoverable and the credential must be re-minted. ErrKeyRetired = errors.New("secretbroker: sealing key retired") // this process holds — CLOOP_SECRET_KEY, or CLOOP_SECRET_KEY_PREVIOUS // while a change of root is in progress. The material is intact; the key // is wrong, or the key belongs to a root this hub member was not given. ErrKeyUnavailable = errors.New("secretbroker: sealing key unavailable") // ErrKeyUnknown: the material names a KEK this hub has no record of — // a database from another hub, or a partially restored backup. ErrKeyUnknown = errors.New("secretbroker: unknown sealing key") // ErrKeyInUse: the KEK cannot be retired because sealed material still // references it, or because it is the primary. ErrKeyInUse = errors.New("secretbroker: sealing key still in use") // ErrRotationFailed: a rotation could not rewrap every row. Rotation is // resumable, so this means "run it again", not "start over". ErrRotationFailed = errors.New("secretbroker: key rotation incomplete") // ErrRootConfig: a source of the root key is configured and refused — a // key file with the wrong owner or mode, two sources that disagree // (pkg/rootkey, Task 20425). Never a degraded mode to carry on in, the // way ErrNoKey is: the operator meant to give the hub a key, and what the // hub found is not one it may use. ErrRootConfig = errors.New("secretbroker: the root key's configuration is refused") // ErrNoPreviousRoot: retiring the previous root was asked of a process // that holds none (CLOOP_SECRET_KEY_PREVIOUS). ErrNoPreviousRoot = errors.New("secretbroker: no previous root key is configured") // ErrRequestsUnsupported: this broker's store does not keep requests, so // the request path is unavailable. Reported rather than silently degrading // to "no requests", which would make an empty queue indistinguishable from // a queue nobody can read. ErrRequestsUnsupported = errors.New("secretbroker: this store does not keep access requests") // ErrInvalidRequest: the access request failed structural validation. ErrInvalidRequest = errors.New("secretbroker: invalid access request") // ErrRequestNotFound: no access request with that ID exists. ErrRequestNotFound = errors.New("secretbroker: access request not found") // ErrRequestNotPending: the request was already decided, withdrawn or // expired, so it cannot be decided again. ErrRequestNotPending = errors.New("secretbroker: access request is already decided") // ErrRequestExpired: the request lapsed before anyone decided it. ErrRequestExpired = errors.New("secretbroker: access request expired") // ErrNotRequester: only the identity that filed a request may withdraw it. ErrNotRequester = errors.New("secretbroker: only the requester may withdraw a request") // ErrSelfApproval: the decider filed the request. Refused explicitly rather // than left to the role check, because on a small team the requester very // often holds the approving permission too. ErrSelfApproval = errors.New("secretbroker: a request cannot be approved by the person who filed it") // ErrDelegationExceeded: the approval would hand out more than this // approver is entitled to delegate. ErrDelegationExceeded = errors.New("secretbroker: approval exceeds what this approver may delegate") // ErrNotOwner: the caller tried to spend, re-scope or destroy a personal // secret belonging to somebody else. A denial, not a validation error: // reaching for another person's credential is precisely the event the // ownership model exists to record. // // Paths that must not reveal the secret exists at all report // ErrSecretNotFound instead; this sentinel is for the paths where the // caller has already legitimately been shown the secret and is asking to // act on it. ErrNotOwner = errors.New("secretbroker: this secret belongs to another user") // ErrOwnerRequired: a personal secret was requested without an identity to // own it. An anonymously-owned secret would be indistinguishable from a // shared one at every later check, so it is refused at the point of // minting rather than silently widened into the organisation's. ErrOwnerRequired = errors.New("secretbroker: a personal secret needs an owner identity") // ErrPersonalWildcard: a personal secret was granted to a wildcard or // "any" subject. A personal credential exists to be spent on its owner's // own work, and a subject matching every project or every executor hands // it to whoever runs next — the one outcome personal ownership is meant // to prevent. ErrPersonalWildcard = errors.New("secretbroker: a personal secret cannot be granted to a wildcard subject") // ErrBranchesUnenforced: a grant limits its pushes to a branch allowlist, // no git proxy guards it on this hub, and its credential cannot be // narrowed to read-only instead (Task 20340). A denial rather than a // fallback: delivering the credential would hand over a push to every // branch it can reach, which is exactly what the allowlist was written to // rule out. ErrBranchesUnenforced = errors.New("secretbroker: branch allowlist cannot be enforced without the git proxy") // ErrKubeConstraintsUnenforced: a kubeconfig grant claims something only // the Kubernetes access monitor enforces — a namespace allowlist, or verbs // short of every one, read-only above all — and this delivery has no // monitor (Task 20420). Without it the namespaces are only kubectl's // default and the verbs are not checked at all, so delivering the // minimised kubeconfig would hand the workload whatever the kubeconfig's // identity may do. A denial, not a fallback, unless an administrator has // recorded an acknowledgement on the grant (Grant.UnguardedAck). ErrKubeConstraintsUnenforced = errors.New("secretbroker: kubeconfig grant constraints cannot be enforced without the Kubernetes access monitor") // ErrRenewalStopped: a credential the hub renews by itself was not // renewed, because the hub refuses to keep it alive — its owner is denied // on the hub (Task 20420). ErrRenewalStopped = errors.New("secretbroker: renewal stopped") // ErrUnguardedAckUnsupported: this broker's store keeps no unguarded // acknowledgements, so none can be recorded. Reported rather than // silently dropped: an acknowledgement nobody stored is a delivery that // keeps failing while the administrator believes it was allowed. ErrUnguardedAckUnsupported = errors.New("secretbroker: this store does not keep unguarded acknowledgements") )
Sentinel errors. Callers match with errors.Is; every constructor in this package wraps one of these with %w and adds detail.
The denial errors (ErrRepoDenied, ErrNamespaceDenied, ErrHostDenied, ErrGrantExpired, ErrGrantRevoked) are deliberately distinct from the validation errors: a denial is a security event that must reach the audit log, while a validation error is a malformed request that never became one.
ErrGuardUnavailable reports that a guard was configured but could not produce a session.
It is a distinct sentinel because the choice it forces is not a detail. A guarded delivery that quietly fell back to writing the PAT into the sandbox would hand over the broad credential at precisely the moment the boundary meant to narrow it was broken — the one circumstance in which it must not. Callers fail the lease instead. See githubMaterial.
The Kubernetes monitor's refusals wrap it too (kubeMaterial), which is why its text names neither guard: since Task 20413 it reaches the project whose run went without the credential, and "git guard" on a kubeconfig misleads.
var ErrSecretChanged = errors.New("secretbroker: the secret changed while it was being renewed")
ErrSecretChanged is a renewal that lost a race: the secret's payload changed between being read and being replaced, and the newer one stands.
Functions ¶
func AADFor ¶
AADFor builds the associated data for one row of one sealed set. Every seal, open, and rewrap of that row must use the identical value.
func ExecutorBindings ¶ added in v0.0.2
func ExecutorBindings(leaseID string, expiresAt time.Time, raw []LeaseBinding) []executor.SecretBinding
ExecutorBindings projects a lease's per-grant attribution onto the driver-facing executor.SecretBinding.
The bindings are what let a driver take *one* credential back mid-run instead of killing the workload: they name the environment variables and file paths a given grant contributed, and nothing else. No material crosses this boundary — see SecretBinding's own documentation for why that is a hard rule rather than a convention.
expiresAt rides along on every binding so a driver that has lost contact with the control plane can expire the material locally. Without it a lease TTL binds only the hub, which is the half of the system least likely to be the one holding the credential when it matters.
func ExecutorSecretFiles ¶ added in v0.0.2
func ExecutorSecretFiles(leaseID string, raw []DeliveredFile) []executor.SecretFile
ExecutorSecretFiles projects the credential files a driver has to place itself onto executor.SecretFile.
Only a delivered (not materialised) lease has these. When the hub wrote the files to its own filesystem, the workload reads them from there and the bindings already name the paths; handing the contents over as well would put plaintext into a second place for no benefit.
func GitHubAppPermissions ¶ added in v0.0.2
func GitHubAppPermissions(c Constraints) (map[string]string, error)
GitHubAppPermissions maps a grant's permission set onto the permissions object of an installation token request.
The default is contents:read, because a github credential in cloop exists to clone, and a grant that says nothing about permissions has authorised nothing more than reading. A grant that allows a write to contents gets contents:write, so a push works and nothing else widens.
A "*" entry returns nil, which asks GitHub for the installation's full permission set. That is the same reading allowsAllRepos gives the repository allowlist: the wildcard has to be written out, and when it is, it means it.
The error is the reason this is exported to Constraints.ValidateFor as well as used at mint time: "pull_requests:maybe" is a typo that should be refused in front of the operator who wrote it, not eight hours later inside a lease.
func GitHubPATPayload ¶ added in v0.0.6
GitHubPATPayload renders token, issued by the GitHub host host, as a github_pat payload. An empty host, or github.com, renders the bare token: the form every PAT was stored in before hosts were recorded.
func KubeconfigSummary ¶
KubeconfigSummary describes a minimized kubeconfig for audit and CLI output: which contexts and namespaces survived. It never includes cluster endpoints' credentials, only names.
func LegacyKeyOpens ¶ added in v0.0.5
LegacyKeyOpens reports whether one of roots opens sample, a ciphertext the pre-envelope construction sealed under the store-wide salt saltHex.
That construction has no check value, so the only way to answer is to open something it sealed. The plaintext is wiped before this returns and never leaves it; the answer is all a caller gets.
func MarshalAppSlotScope ¶ added in v0.0.5
func MarshalAppSlotScope(r AppSlotRecord) (string, error)
MarshalAppSlotScope renders a record's scope for storage.
func MinimizeKubeconfig ¶
func MinimizeKubeconfig(raw []byte, c Constraints) ([]byte, error)
MinimizeKubeconfig rewrites a kubeconfig to contain only what the grant's constraints permit, and returns the reduced document.
This is the strongest enforcement in the package, because it is subtractive: the executor receives a kubeconfig that has no credentials for the clusters it may not reach and no context naming a namespace it may not touch. Nothing it can do with that document widens it back out. That is a different and better guarantee than handing over the full kubeconfig alongside a policy note.
Rules:
- a context survives only if Constraints.AllowsContext says so;
- when the grant lists namespaces, each surviving context is pinned to an allowed namespace — its own if that is allowed, otherwise the first concrete (non-glob) namespace in the allowlist;
- a context whose namespace is disallowed and which cannot be pinned (the allowlist is globs only) is dropped rather than guessed at;
- clusters and users not referenced by a surviving context are removed, so their certificates and tokens never leave the control plane;
- current-context is repointed at a surviving context;
- if nothing survives, the result is ErrMinimizedEmpty — a denial, not an empty file that would look like a working credential.
func NewLeaseDirPath ¶
NewLeaseDirPath returns an unused lease-directory path under baseDir — without creating it, and without writing anything.
It exists so a caller can record its intent to materialise *before* any plaintext lands on disk. Materialize used to pick the directory itself with MkdirTemp, which left no way to write a durable "a lease directory is about to exist at this path" row first: a hub killed between the mkdir and the row came back up with a credential directory nothing knew about, and the startup sweep had nothing to reconcile it against. /dev/shm is a tmpfs and clears on reboot, but a hub *process* restart is the common case and clears nothing.
Splitting the naming from the creation makes the ordering possible:
dir, _ := NewLeaseDirPath("") // nothing on disk yet
db.PutSecretLeaseDir(...) // intent recorded
lease.MaterializeAt(dir) // plaintext appears
A crash after the row exists leaves a trace the sweep can act on; a crash before it leaves nothing at all. There is no window in between.
baseDir may be empty to use a tmpfs. The 96-bit suffix matches lease IDs, so two hubs sharing one /dev/shm cannot collide.
func NormalizeHost ¶
NormalizeHost reduces a host, host:port, or URL authority to a bare lowercase hostname. Anything carrying a path, credentials, or a wildcard is rejected: those are the shapes an attacker uses to make "evil.com" look like "api.example.com" to a naive matcher.
func NormalizeOwner ¶ added in v0.0.2
NormalizeOwner trims an owner identity into the form stored on a Secret.
Exported because the hub needs to compare a request's identity against a stored owner, and a second normaliser written at the call site is how the two come to disagree about whether "Alice@Corp" owns "alice@corp".
Lowercased because OwnerKey already is for the email case, and storing a mixed-case copy would make the SQL index disagree with OwnedBy's case-insensitive comparison — two answers to "is this yours" is one too many. The "sub:" form is left alone beyond the prefix: an issuer subject is opaque and case-sensitive, so lowercasing it could merge two distinct people.
func NormalizeProjectID ¶
NormalizeProjectID canonicalises a project identifier so that a grant written as "/srv/app/" and a lease requested for "/srv/app" agree. Absolute paths are cleaned; anything else (an opaque project ID, or the "*" wildcard) is passed through trimmed. Matching is exact after this — no prefix or basename fuzziness, which would let /srv/app-staging pick up /srv/app's credentials.
func NormalizeRepo ¶
NormalizeRepo canonicalises the many ways a repository gets named into the single "owner/repo" form the allowlist is written in, so that "https://github.com/Org/Repo.git" and "org/repo" are the same subject.
Anything that does not reduce to exactly one owner and one repo is an error, not a best guess: a caller that hands us something unrecognisable must be denied, not matched against a partially-parsed string.
func OnGrantRevoked ¶ added in v0.0.5
func OnGrantRevoked(fn func(context.Context, GrantRevocation)) (cancel func())
OnGrantRevoked registers fn to run after every grant revocation any Broker in this process makes — Revoke, a secret's deletion, a supersession — and returns the function that unregisters it.
fn runs synchronously, on the revoking goroutine, after the grant is stamped in the store: a caller that wants the cascade's outcome passes it back through ctx, and a caller that does not is still not finished revoking until the material is taken back. It is not called for a grant that was already revoked: a revocation is idempotent, and one that cascaded once must not cascade again. A panicking fn is recovered, so one subscriber cannot leave a grant stamped and the rest of the process uninformed.
func ParseLocalRepoRoot ¶
ParseLocalRepoRoot validates a local_repo payload.
Exported because storing one of these secrets should fail at the API with a usable message — "that path does not exist on the hub" is a typo the operator can fix in the dialog, and finding out at lease time instead means finding out during someone else's run.
func RedactString ¶
RedactString removes credential-shaped substrings from s.
The shapes are pkg/redact's registry — GitHub tokens in both forms, cloop's own tokens, cloud and model-provider keys, JWTs, PEM private keys, kubeconfig credentials, Authorization values and URL userinfo — so an audit reason is scrubbed of exactly what cloop audit would report in it. This package keeps only its replacement: every credential becomes "[redacted]", with whatever identified it ("Bearer ", a URL's host) left readable around the marker.
It used to carry its own prefix list, scrubbing from a prefix to the next delimiter. That knew none of cloop's own prefixes, turned "risk-free" into "ri[redacted]" on the bare "sk-", and removed only the "-----BEGIN" of a private key while the key itself went into the log.
func SafeRef ¶
SafeRef renders a secret reference for an error message or an audit record without republishing it.
A "no such secret" error is one of the few places a user-supplied string is echoed verbatim, and the single most common way to reach it is to paste the credential where its *name* belongs. That mistake is silent — the operator sees a not-found error, retries correctly, and never learns that the first attempt wrote the live token into the audit log, which is then shipped off-box and retained for a year.
Redaction alone is not enough: it recognises known token prefixes, and a kubeconfig token or a registry auth blob has no prefix to recognise. So the reference is also truncated, which is the part that generalises to credential shapes nobody has enumerated yet.
func SandboxLeaseDir ¶
SandboxLeaseDir returns the directory a lease's credential files take inside a sandbox. The final element carries leaseDirPrefix so the agent's confinement rule recognises it as lease-owned.
func SecretForgeHost ¶ added in v0.0.6
SecretForgeHost reports the forge host a GitHub secret's credential is good against, from what Mint recorded, without opening the payload; known is false when that cannot be said.
A github_pat minted before hosts were recorded is a bare token, which is a github.com token, so it is known. A github_app minted then is not: where it belongs is in its sealed base_url. A caller that chooses by this treats an unknown host as "decided when the payload is opened", which a lease does — it never treats it as a match.
func SelfRenewingKubeconfig ¶ added in v0.0.5
SelfRenewingKubeconfig reports whether s is a kubeconfig share the hub renews, whose ServiceAccount can therefore create tokens for itself.
func UnmarshalAppSlotScope ¶ added in v0.0.5
func UnmarshalAppSlotScope(raw string, r *AppSlotRecord) error
UnmarshalAppSlotScope reads a stored scope back into r.
func ValidateName ¶
nameCharset restricts secret names to characters that are safe to embed in audit payloads, CLI output, env var derivations, and file names without escaping. Rejecting at the door is cheaper than escaping at every sink.
Types ¶
type AccessRequest ¶ added in v0.0.2
type AccessRequest struct {
ID string `json:"id"`
// RequestedBy is the identity that asked — an OIDC subject, a service
// account, or "cli:<user>". It is the value ApproveRequest compares against
// the decider, so it must identify a person and not a role.
RequestedBy string `json:"requested_by"`
SecretID string `json:"secret_id"`
SecretName string `json:"secret_name,omitempty"`
Kind Kind `json:"kind,omitempty"`
// Subject and Constraints are the grant this request is asking to have
// created. They are replayed verbatim on approval.
Subject Subject `json:"subject"`
Constraints Constraints `json:"constraints"`
Scope string `json:"scope,omitempty"`
// TTL is the grant lifetime asked for. What was actually issued is
// GrantExpiresAt minus DecidedAt, which will be shorter whenever the
// approver or their delegation clamped it — and a reviewer comparing the
// two is exactly how "did anyone actually narrow this" gets answered.
TTL time.Duration `json:"ttl"`
// Justification is why. Required, because a request with no stated reason
// cannot be evaluated, only rubber-stamped.
Justification string `json:"justification"`
State RequestState `json:"state"`
CreatedAt time.Time `json:"created_at"`
// ExpiresAt is when this *request* lapses undecided. Never the credential's
// expiry — that is GrantExpiresAt.
ExpiresAt time.Time `json:"expires_at"`
DecidedBy string `json:"decided_by,omitempty"`
DecidedAt time.Time `json:"decided_at,omitempty"`
DecisionNote string `json:"decision_note,omitempty"`
// GrantID is the grant an approval minted, and empty in every other state.
GrantID string `json:"grant_id,omitempty"`
GrantExpiresAt time.Time `json:"grant_expires_at,omitempty"`
}
AccessRequest is one developer's ask, and its decision.
There is no payload field and nowhere one could go: a request names a secret that already exists. The credential itself never touches this type, which is what makes an AccessRequest safe to render into a review panel, a CLI table and an audit payload without any of them having to redact it.
func (AccessRequest) Pending ¶ added in v0.0.2
func (r AccessRequest) Pending(now time.Time) bool
Pending reports whether the request is still awaiting a decision at now, treating a lapsed deadline as not pending even before a sweep has moved it.
The clock is consulted rather than the state alone so that a decision path cannot act on a request the next sweep is about to expire. Without it, whether a stale request could still be approved would depend on how recently the janitor happened to run.
type AccessRequestInput ¶ added in v0.0.2
type AccessRequestInput struct {
// SecretRef is a secret ID or name. The secret must already exist: a
// request asks for access to a credential an operator stored, never for one
// to be created, because creating one means supplying material and a
// requester by definition does not have it.
SecretRef string
Subject Subject
Constraints Constraints
Scope string
TTL time.Duration
Justification string
// RequestTTL is how long to wait for a decision. Zero means
// DefaultRequestTTL.
RequestTTL time.Duration
// Actor is the requesting identity. Required: an anonymous request cannot
// be approved, because the self-approval check has nothing to compare.
Actor string
}
AccessRequestInput is what a requester supplies.
type Action ¶
type Action string
Action names the brokered operation an audit event describes.
const ( ActionMint Action = "secret.mint" ActionDeleteSec Action = "secret.delete" ActionGrant Action = "secret.grant" ActionRevoke Action = "secret.revoke" ActionLease Action = "secret.lease" ActionRenew Action = "secret.renew" ActionRelease Action = "secret.release" ActionAccessCheck Action = "secret.access_check" // Lease revocation actions (Task 20178). These are separate from // ActionRelease because releasing a lease and revoking one answer // different questions. A release is the ordinary end of a workload's // credential lifetime; a revocation is somebody deciding, mid-run, that // an executor may no longer hold something it already has. // // All three exist because a revocation is not one event. It is sent, // and then it either lands or it does not, possibly minutes later when // an offline device reconnects. Collapsing them into one row would make // the trail claim a credential was withdrawn at a moment when it // demonstrably still worked. ActionLeaseRevokeSent Action = "lease.revoke_sent" ActionLeaseRevokeAcked Action = "lease.revoke_acked" ActionLeaseRevokeFailed Action = "lease.revoke_failed" // ActionLeaseRefresh records a re-minted GitHub App token being delivered // to an executor holding the lease's token file — or failing to be // (Task 20375). Separate from ActionRenew, which records the re-mint: a // token minted and never delivered is a credential that exists at GitHub // while the workload still holds the old one, and the trail has to be able // to say so. ActionLeaseRefresh Action = "lease.refresh" // ActionAppTokenDestroy records a GitHub App installation token being // destroyed at GitHub (Task 20254). // // It is deliberately not ActionRevoke. A github_app token is destroyed on // every ordinary lease release, so folding these rows into `secret.revoke` // would make "how many grants did an operator withdraw" a number dominated // by routine task teardown — and the one question the revoke series exists // to answer would stop having an answer. // // It is also not ActionRelease, for the reason the lease.revoke_* family is // separate from it: a release says a workload finished with a credential, // while this says a credential stopped existing. When the DELETE fails the // first is still true and the second is not, and an incident response needs // to be able to tell those apart. ActionAppTokenDestroy Action = "github_app.token_destroy" // Self-service request actions (Task 20271). // // Separate from the secret.grant family rather than folded into it, because // a request and a grant are different facts and a reviewer needs to tell // them apart. secret.grant answers "what authority exists"; these answer // "who asked, who decided, and how long it took" — and the gap between a // request and its approval is the number that tells an operator whether the // request path is actually being used or routed around. // // secret.request_expire is its own action, not a flavour of deny, for the // reason RequestExpired is its own state: denied is an answer and expired is // the absence of one, and a trail that conflated them would hide a queue // nobody is working. // Unguarded acknowledgements (Task 20420): an administrator deciding // that a kubeconfig grant whose constraints only the Kubernetes access // monitor enforces may be delivered on a hub without one — and taking // that decision back. Their own actions, because they are the rows that // answer "who let this grant reach a sandbox unenforced, and why". ActionUnguardedAck Action = "secret.unguarded_ack" ActionUnguardedWithdraw Action = "secret.unguarded_withdraw" ActionRequestOpen Action = "secret.request" ActionRequestApprove Action = "secret.request_approve" ActionRequestDeny Action = "secret.request_deny" ActionRequestWithdraw Action = "secret.request_withdraw" ActionRequestExpire Action = "secret.request_expire" // Egress actions come from pkg/egressbroker, which brokers the hub's // Internet connection as a fourth grantable resource alongside GitHub // repositories, PATs, and Kubernetes clusters. // // They share this audit trail rather than opening a second one on // purpose: "what did this executor reach, and with whose authority" is // one question, and answering it from two hash chains that can disagree // about ordering would be strictly worse than answering it from one. ActionEgressGrant Action = "egress.grant" ActionEgressRevoke Action = "egress.revoke" ActionEgressRedeem Action = "egress.redeem" ActionEgressConnect Action = "egress.connect" ActionEgressRequest Action = "egress.request" ActionEgressClose Action = "egress.close" // ActionEgressRenew is a live proxy session renewed for a run that is // still going (Task 20378), after its grant was re-read and found active. ActionEgressRenew Action = "egress.renew" // ActionEgressRestore is a proxy session restored by the hub process that // adopted its run after the one serving it stopped (Task 20383) — or // refused, because its grant was revoked or expired meanwhile. ActionEgressRestore Action = "egress.restore" )
type AppCredential ¶ added in v0.0.2
type AppCredential struct {
AppID int64
InstallationID int64
// BaseURL is the API root, defaulted to github.com.
BaseURL string
// contains filtered or unexported fields
}
AppCredential is a parsed, validated github_app payload.
The private key is unexported and there is no accessor: the only thing any caller may do with an AppCredential is ask it to sign a JWT, which is what keeps "the key never leaves the hub" a property of the type rather than a convention.
func ParseGitHubApp ¶ added in v0.0.2
func ParseGitHubApp(payload []byte) (*AppCredential, error)
ParseGitHubApp parses and validates a github_app payload.
It is deliberately strict. Before Task 20254 the payload was free-form — the broker never looked at it, so a typo, a PEM with a mangled line ending, or a PAT pasted into the wrong kind was stored happily and failed at lease time, during someone else's run. Now the only accepted shape is
{"app_id": 123, "installation_id": 456, "private_key": "-----BEGIN ..."}
with an optional "base_url" for GitHub Enterprise Server. Both IDs must be integers and the key must parse as RSA, because RS256 is the only algorithm GitHub accepts for an app JWT.
func ParseGitHubAppConnection ¶ added in v0.0.3
func ParseGitHubAppConnection(payload []byte) (*AppCredential, error)
ParseGitHubAppConnection parses the half of a github_app payload that exists before the App's installation is known: the App ID and the signing key.
It is what discovery runs on. An operator configuring cloop has just come from GitHub's App settings page, which shows an App ID and offers a key download, and has no installation ID because that is a property of the *install*, not of the App. The credential this returns can sign an app JWT — enough to ask GitHub where the App is installed — and nothing else: its InstallationID is zero, so mint refuses it (see mint's guard) and it can never be stored as a working secret by mistake.
Everything else is validated exactly as ParseGitHubApp validates it, so a malformed key is reported in the connect dialog rather than at the next step.
type AppInstallation ¶ added in v0.0.3
type AppInstallation struct {
// ID is the installation_id a github_app payload needs.
ID int64 `json:"id"`
// AppID is the app the installation belongs to. Echoed back so a caller
// holding several keys can tell which one produced this list.
AppID int64 `json:"app_id"`
// Account is the org or user the App is installed on ("bb-selforg").
Account string `json:"account"`
// AccountType is "Organization" or "User".
AccountType string `json:"account_type"`
// RepositorySelection is "all" or "selected" — whether the installation
// covers every repository the account owns or an explicitly chosen subset.
// An operator who granted "all" and still cannot reach a repository has a
// different problem from one who granted "selected" and forgot to tick it.
RepositorySelection string `json:"repository_selection"`
}
AppInstallation is one place an App has been installed: an organisation or a user account.
It exists because an installation ID is the one field of a github_app payload that the person configuring it does not have. GitHub hands out an App ID and a private key on the App's own settings page; the installation is created afterwards, by whoever installs the App on their org, and its ID appears only in the URL of a settings page that person may never visit. Asking an operator to go and find it — as this package did before Task 20306 — is asking them to read a number out of a browser address bar, which is exactly the kind of step that gets guessed wrong and surfaces as a 404 at lease time.
So the hub asks GitHub instead. Nothing here is a secret: an App ID, an installation ID and an account name are all public identifiers, which is what makes it safe to show the whole list in a dialog.
type AppSlotRecord ¶ added in v0.0.5
type AppSlotRecord struct {
LeaseID string
GrantID string
Holder string
SecretID string
SecretName string
BaseURL string
InstallationID int64
// RepositoryIDs is nil for an installation-wide token.
RepositoryIDs []int64
// Permissions is what the first mint asked for; nil asked for every
// permission the installation has.
Permissions map[string]string
// Granted is what GitHub answered the first mint with.
Granted map[string]string
// Summary names what the token reaches, audit-safe.
Summary string
// Guarded: a git proxy session presents the token, named by SessionID.
Guarded bool
SessionID string
EnvExported bool
// FileName is the lease file an unguarded token is delivered in.
FileName string
// TokenExpiresAt is when the token the workload or session holds stops
// working at GitHub.
TokenExpiresAt time.Time
}
AppSlotRecord is the durable account of one GitHub App token a lease holds. It names what the token was minted for and never carries the token.
type AppSlotStore ¶ added in v0.0.5
type AppSlotStore interface {
PutAppSlot(r AppSlotRecord) error
ListAppSlots(leaseID string) ([]AppSlotRecord, error)
// UpdateAppSlot rewrites a slot's guarded flag, session, export flag and
// held-token expiry if r.Holder holds it.
UpdateAppSlot(r AppSlotRecord) (bool, error)
TakeAppSlots(leaseID, from, holder string) (int, error)
DeleteAppSlots(leaseID, holder string) (int, error)
DeleteAppSlot(leaseID, grantID, holder string) (bool, error)
}
AppSlotStore persists AppSlotRecords. Optional, like LeaseStore: a broker whose store does not implement it keeps its slots in memory only. Every write after the insert is fenced on the holder.
type AppTokenRefresh ¶ added in v0.0.5
type AppTokenRefresh struct {
// Token is the credential, for the hub's own use: a proxy session presents
// it upstream. Never logged, never delivered to a sandbox by this type.
Token string
// ExpiresAt is when GitHub stops honouring Token.
ExpiresAt time.Time
// Retire destroys the token this one superseded, at GitHub. Nil when no
// new token was minted — another caller had already refreshed, and Token is
// the one it minted. Idempotent, safe after the lease was released, and
// never blocks longer than appRevokeTimeout.
Retire func()
}
AppTokenRefresh is a re-minted installation token.
func (AppTokenRefresh) GoString ¶ added in v0.0.5
func (r AppTokenRefresh) GoString() string
GoString mirrors String.
func (AppTokenRefresh) String ¶ added in v0.0.5
func (r AppTokenRefresh) String() string
String redacts the token so a %v on a refresh cannot log a credential.
type Auditor ¶
type Auditor interface {
Audit(ev Event)
}
Auditor receives brokered-operation events. Implementations must not block the broker: an audit sink that is slow or down must not stop a workload from starting, so the production adapter writes best-effort and swallows its own errors, exactly as pkg/statedb's audit emitters do.
type AuditorFunc ¶
type AuditorFunc func(Event)
AuditorFunc adapts a function to the Auditor interface.
type Broker ¶
type Broker struct {
// GitGuard, when set, takes custody of a GitHub PAT so the sandbox
// receives a git-proxy session instead of the token. It turns the
// repository allowlist from something the workload is asked to respect
// into something it cannot exceed. See gitguard.go.
//
// Exported and settable after construction because the proxy it fronts is
// started by pkg/ui alongside the hub, later than the broker and from a
// layer this package must not import.
GitGuard GitGuard
// KubeGuard, when set, takes custody of a kubeconfig so the sandbox
// receives a monitor session instead of the cluster credential. It turns
// the namespace allowlist from a client-side default into a bound, and it
// is the only way a kubeconfig grant's verbs mean anything at all. See
// kubeguard.go.
//
// Exported and settable after construction for the same reason GitGuard
// is: the monitor it fronts is started by pkg/ui alongside the hub, later
// than the broker and from a layer this package must not import.
KubeGuard KubeGuard
// contains filtered or unexported fields
}
Broker issues scoped, expiring credential leases against a Store.
Safe for concurrent use.
func New ¶
New builds a Broker over store. Unless WithCipher is supplied, the payload key is derived from CLOOP_SECRET_KEY, and New fails if it is unset — a broker that cannot open payloads would otherwise fail later, at lease time, in the middle of somebody's run.
func (*Broker) AcknowledgeUnguarded ¶ added in v0.0.5
AcknowledgeUnguarded records req's acknowledgement on its grant and returns the grant as stored.
It is refused for a grant that is not a kubeconfig grant, is revoked or expired, claims nothing only the monitor enforces (so has nothing to acknowledge), or stands on a Kubernetes share the hub renews — whose ServiceAccount may mint its own tokens, which no acknowledgement can make safe to hand a workload.
func (*Broker) AppTokensDue ¶ added in v0.0.5
AppTokensDue reports whether RefreshLeaseFiles has anything to do for leaseID: an App token delivered as a file is within AppTokenRefreshWindow of its end, or was refreshed and has not reached the workload yet. It mints nothing, so the keepalive can ask on every tick — and ask where the new file would go before a token is minted for a workload that cannot receive it.
func (*Broker) ApproveRequest ¶ added in v0.0.2
func (b *Broker) ApproveRequest(ctx context.Context, in DecideInput) (AccessRequest, Grant, error)
ApproveRequest mints the grant a request asked for.
The grant is created by calling Broker.Grant — the same entry point the CLI and the REST panel use — rather than by writing a Grant struct here. That is the single most important line in this file: Grant is where a grant's constraints are validated against its secret's kind, where the audit row for a creation is emitted, and where any future check will be added. A second minting path would be a second place for that logic to be missing.
What this function adds on top is the two-person rule, the delegation clamp, and the record linking the grant back to the ask.
func (*Broker) CheckRepoAccess ¶
CheckRepoAccess is the in-process enforcement point for github grants: it reports whether this requester may act on repo, and audits the decision.
cloop's own GitHub call sites (pkg/github, pkg/githubsync) route through here so that a repository outside the allowlist is refused before a request is made, rather than relying on the credential helper alone.
func (*Broker) DeleteSecret ¶
DeleteSecret removes a secret and revokes every grant that pointed at it. Leaving grants behind would leave rows that resolve to nothing and read, in a grant listing, as still-live access.
func (*Broker) DeleteSecretBecause ¶ added in v0.0.5
func (b *Broker) DeleteSecretBecause(ctx context.Context, ref, actor string, cause DeletionCause, reason string) error
DeleteSecretBecause is DeleteSecret with a cause and a reason (Task 20400).
The reason lands on the secret.delete audit row and on the tombstone; the cause is what a later refusal tells the project that lost the credential — "deleted", or "destroyed when its owner was offboarded". The store scrubs the sealed columns before it deletes the row (statedb.deleteBrokerSecret), so the material is overwritten rather than merely unlinked.
func (*Broker) DeleteSecretFor ¶ added in v0.0.2
DeleteSecretFor removes a secret on behalf of v, revoking its grants.
Deleting is the one act where an admin may reach a personal secret that is not theirs, because the alternative is a hub that cannot complete an offboarding: when someone leaves, their credentials must go with them, and an account that no longer exists cannot come back to press the button. The audit event Broker.DeleteSecret emits names the actor, so an admin doing this is recorded doing it.
func (*Broker) DenyRequest ¶ added in v0.0.2
func (b *Broker) DenyRequest(ctx context.Context, in DecideInput) (AccessRequest, error)
DenyRequest refuses a request, on the record.
func (*Broker) DescribeSecret ¶
DescribeSecret resolves a secret by ID or name. The payload stays sealed: this answers "does this reference name something, and of what kind", which is what a caller validating a grant before creating it needs.
It exists so that path does not have to re-implement reference resolution against ListSecrets, where a subtly different name match would mean a grant validated against one secret and then created against another.
func (*Broker) DescribeSecretFor ¶ added in v0.0.2
DescribeSecretFor resolves a secret by ID or name, for a viewer.
A secret v may not see reports ErrSecretNotFound rather than ErrNotOwner, and the distinction matters: names are chosen by people and are guessable ("alice-github-pat"), so an error that separates "no such secret" from "not yours" would turn this endpoint into an oracle for who keeps what. The hub makes the same choice for projects a caller cannot reach, where require() answers 404 rather than 403.
func (*Broker) DiscoverGitHubAppInstallations ¶ added in v0.0.3
func (b *Broker) DiscoverGitHubAppInstallations(ctx context.Context, payload []byte) ([]AppInstallation, error)
DiscoverGitHubAppInstallations reports where an App is installed.
The payload is a github_app payload with the installation_id omitted: an app_id, a private_key, and optionally a base_url for GitHub Enterprise Server. It is the operator's own input, arriving from a dialog, and is never stored — the caller decides which installation to keep and mints the secret afterwards with that ID filled in.
The returned list is public information: account names and numeric IDs. It is sorted by account so a hub with several installations renders the same order on every call rather than GitHub's.
func (*Broker) DropLeaseGrant ¶ added in v0.0.5
DropLeaseGrant takes grantID out of the live lease leaseID once its material has been taken back from the workload (Task 20403): the lease keeps the grants nobody revoked, is extended on those alone, and is recorded so for any process that takes it over. Any App token minted for the grant in this lease is destroyed at GitHub. It reports how many grants the lease still carries; dropping a grant the lease does not carry changes nothing.
func (*Broker) EnvKeyNamesFor ¶ added in v0.0.5
EnvKeyNamesFor returns the variable names an env secret defines, for a viewer who may see that secret.
A secret v may not see reports ErrSecretNotFound, as DescribeSecretFor does, so the names of somebody else's personal secret are not an oracle for its existence. A secret of another kind reports ErrInvalidKind.
func (*Broker) EnvOffers ¶ added in v0.0.5
EnvOffers previews the environment a lease for r would carry, grant by grant.
Every grant whose subject matches r and which has not been revoked is reported, whatever its expiry; a revoked grant is a decision someone made and is left out. Only grants over env secrets are considered: no other kind puts a credential the hub controls into a variable by name.
It mirrors LeaseFor's matching — the requester's project is normalised the same way and Subject.Matches decides — and deliberately ignores r.Withhold, because deciding what to withhold is the caller's use for this preview.
It writes no audit row and mints nothing. What it returns is names, so the preview discloses to its caller exactly what a lease's own summary row would have recorded ("env keys: …"), and nothing a lease would have kept back.
func (*Broker) ExpireRequests ¶ added in v0.0.2
func (b *Broker) ExpireRequests(ctx context.Context) ([]AccessRequest, error)
ExpireRequests moves pending requests past their deadline to expired and emits one audit event for each.
An event per expiry rather than a summary count, because a lapsed request is an outcome the requester is owed: from their side, an expiry and an approval nobody told them about look identical until they try to use the credential.
func (*Broker) Extend ¶ added in v0.0.5
Extend keeps a live lease valid for another lease period, in place: same ID, same materials, a later deadline.
It is the renewal a dispatched workload can actually use (Task 20349). Renew re-issues — a new lease ID, a fresh GitHub App installation token, a fresh git-proxy session — and destroys the old lease's tokens, which is right for a caller that can hand the workload the new material and wrong for one that cannot: a sandbox holding a token in its environment or in a file the hub cannot rewrite would lose it the moment Renew returned. Nothing on the dispatch path could re-deliver, so nothing renewed, and every run's lease lapsed fifteen minutes in — its files scrubbed, its git proxy sessions closed, its App token destroyed at GitHub — however long the run was.
What Extend keeps from Renew is the property the short TTL exists for: every grant the lease carries is re-read from the store and re-checked, so a grant revoked or expired since the lease was issued refuses the extension, the lease lapses on its own schedule, and the janitor takes the material back — a revocation still lands within one lease period. The new deadline is clamped to the earliest grant expiry exactly as at issue.
It does not re-render anything. A secret whose value an operator re-minted reaches the next dispatch, not this one. An App token keeps the hour GitHub gave it, and is replaced before that hour runs out by a refresh at the same scope (apprefresh.go, Task 20375) — the one credential a lease carries that the hub can renew without re-issuing the lease.
func (*Broker) GetRequest ¶ added in v0.0.2
func (b *Broker) GetRequest(id string) (AccessRequest, error)
GetRequest returns one request by ID.
func (*Broker) GitHubAppRepositories ¶ added in v0.0.3
GitHubAppRepositories lists the repositories a stored github_app secret's installation covers.
This is what turns assigning repositories to a project into picking from a list instead of typing patterns blind. A name typed by hand is a name that can be wrong in a way nothing detects until a clone fails; a name chosen from the installation's own inventory is one GitHub has already confirmed.
The secret's private key is opened, used, and dropped inside this call. The discovery token minted to read the list is metadata:read and is destroyed before the function returns. It is unscoped, for the CLI and the hub host. Anything serving a browser request must call GitHubAppRepositoriesFor instead.
func (*Broker) GitHubAppRepositoriesFor ¶ added in v0.0.3
func (b *Broker) GitHubAppRepositoriesFor(ctx context.Context, ref string, v Viewer) ([]InstallationRepo, error)
GitHubAppRepositoriesFor is GitHubAppRepositories on behalf of one identity.
Gated on SpendableBy rather than VisibleTo, which is stricter than it first looks: enumerating signs a JWT with the stored key and presents it to GitHub, so it *uses* the credential. An admin may see that a colleague keeps a personal App here and may destroy it, but no permission in the ladder lets them spend one — and a route that enumerates on their behalf would be exactly that, with the assertion already sent by the time anything refused (Task 20323).
func (*Broker) GitHubGrantHost ¶ added in v0.0.6
GitHubGrantHost reports the forge host the credential behind grantID of leaseID is good against, after the checks GitHubUpstream makes, without minting anything: what a process restoring a git proxy session asks before it spends a GitHub App mint on one it may then refuse (Task 20423).
A PAT's host is read from its sealed payload. An App's is the web host of the API base its lease's token slot was minted against — what every token the slot re-mints is good for — and the App's current payload must still name that base: a slot whose secret has since moved to another host is refused here as remint would refuse it, before any token exists.
func (*Broker) GitHubUpstream ¶ added in v0.0.5
func (b *Broker) GitHubUpstream(ctx context.Context, leaseID, grantID, sessionID string) (UpstreamGitHub, error)
GitHubUpstream re-derives the credential a git proxy session standing on grantID of leaseID presents upstream, for the hub process that took the lease over (Restore) and is restoring the session. sessionID names it, for the audit row of a re-minted App token.
The grant is re-checked first. A PAT is opened from the grant's secret; an App token is minted at once at the slot's recorded scope — the token the stopped process minted is out of reach — and kept alive by the returned refresher, as a minted session's is.
func (*Broker) HeldAppTokenExpiry ¶ added in v0.0.5
HeldAppTokenExpiry is when the first GitHub App token file leaseID's workload holds stops working at GitHub. After a refresh that has not reached the workload yet, that is the token it still reads, not the one waiting to replace it. Zero when the lease holds none, or only tokens already past their hour.
func (*Broker) HeldElsewhere ¶ added in v0.0.5
HeldElsewhere reports whether leaseID is recorded as held by a hub process other than this broker's: taken over with its run. False when this broker keeps no records or the record cannot be read — the answer that leaves a lease with the process that issued it.
func (*Broker) HeldGrant ¶ added in v0.0.5
HeldGrant returns a grant of a lease this broker holds, after the checks Extend makes: not revoked or expired, still issued to the lease's requester, its secret still there. A refusal is a denied secret.renew row. It is what a process restoring a proxy session reads the grant's current constraints from, to hold the session's recorded scope to them (Task 20383).
func (*Broker) HeldGrantIDs ¶ added in v0.0.5
HeldGrantIDs returns the grants the live lease leaseID carries, sorted. Nil for a lease this broker does not hold.
func (*Broker) HeldLease ¶ added in v0.0.5
HeldLease describes a lease this broker holds — its ids, its current deadline and the grants it carries, by name — without opening any of its materials. It is for a caller that keeps a lease alive past the call that issued it and must not keep the material that call returned: the workspace lease a git proxy session stands on (Task 20390). False for a lease this broker does not hold.
func (*Broker) HeldOn ¶ added in v0.0.5
HeldOn returns the grants the live lease leaseID carries on grantID's authority: grantID itself when the lease carries it, and every superseded grant whose successors lead to it. A revocation of grantID withdraws those; a supersession of it leaves them standing on its successor.
func (*Broker) HoldsFileAppTokens ¶ added in v0.0.5
HoldsFileAppTokens reports whether leaseID carries a GitHub App token the workload holds as a file — the only kind the keepalive refreshes.
func (*Broker) HubCredential ¶ added in v0.0.6
HubCredential opens a shared env secret the hub presents as itself.
ref is "name" for a secret holding one value — a bare value, or an env object with one key — or "name#KEY" for one key of an object holding several. The caller owns the returned bytes and should zero them once the credential is in use.
func (*Broker) KeepsLeaseRevocations ¶ added in v0.0.5
KeepsLeaseRevocations reports whether this broker records revocations.
func (*Broker) Keyring ¶
Keyring returns the broker's key registry, or nil when it was built over a store with no registry. Callers that rotate must handle nil rather than assume every broker can.
func (*Broker) KubeconfigUpstream ¶ added in v0.0.5
KubeconfigUpstream re-derives the cluster credential a Kubernetes monitor session standing on grantID of leaseID spends: the grant's kubeconfig, minimised to its constraints exactly as the guard was given it at mint. The grant is re-checked first. The caller zeroes the returned document once it has parsed it.
func (*Broker) Lease ¶
Lease issues the credentials this executor may hold for this project.
It returns only grants whose subject matches the requester and which are neither expired nor revoked, each minimized against its own constraints. An executor never sees the store, another project's grants, or any part of a payload that its constraints excluded.
A requester with no matching grants gets an empty lease, not an error: "this project has no secrets" is a normal state and must not fail a run.
func (*Broker) LeaseFor ¶
LeaseFor is Lease with executor labels (for SubjectLabel grants) and an explicit actor for the audit trail.
func (*Broker) LeaseHolder ¶ added in v0.0.5
LeaseHolder returns the process this broker holds leases for, or "".
func (*Broker) LeaseHorizon ¶ added in v0.0.5
LeaseHorizon reports the grant whose expiry would end a lease for r: of the grants a lease would consider — matching r, neither revoked nor expired, not withheld by r.Withhold — the one that expires first. ok is false when none of them expires at all.
A lease is issued only until its earliest grant ends (leaseDeadline), and Extend refuses it once any of its grants lapses, so every credential in it shares that end, whatever its own grant says. A preview that judged each credential by its own grant alone would promise a run a Claude login that a five-minute GitHub grant beside it was about to take away.
Like EnvOffers it writes no audit row and opens no payload. It is a bound rather than a forecast: a grant the lease would go on to deny (a GitHub App GitHub refuses, a kubeconfig that minimises to nothing) is counted here and would not shorten the real lease.
func (*Broker) LeaseRecordFor ¶ added in v0.0.5
func (b *Broker) LeaseRecordFor(leaseID string) (LeaseRecord, error)
LeaseRecordFor returns the record of leaseID, or a wrapped ErrLeaseNotFound.
func (*Broker) LeaseRecords ¶ added in v0.0.5
func (b *Broker) LeaseRecords() ([]LeaseRecord, error)
LeaseRecords returns every recorded lease. Nil when this broker keeps none.
func (*Broker) LeaseRevocationsFor ¶ added in v0.0.5
func (b *Broker) LeaseRevocationsFor(leaseID string) ([]LeaseRevocation, error)
LeaseRevocationsFor returns the revocations recorded for leaseID, nil when this broker keeps none.
func (*Broker) ListGrants ¶
func (b *Broker) ListGrants(f GrantFilter) ([]Grant, error)
ListGrants returns grants matching the filter, newest first.
func (*Broker) ListGrantsFor ¶ added in v0.0.2
func (b *Broker) ListGrantsFor(f GrantFilter, v Viewer) ([]Grant, error)
ListGrantsFor returns the grants v may see.
Grants over personal secrets follow their secret: the grant row carries the owner precisely so this filter needs no second read.
func (*Broker) ListRequests ¶ added in v0.0.2
func (b *Broker) ListRequests(f RequestFilter) ([]AccessRequest, error)
ListRequests returns requests matching the filter, newest first.
func (*Broker) ListSecrets ¶
ListSecrets returns stored secrets. Payloads stay sealed.
func (*Broker) ListSecretsFor ¶ added in v0.0.2
ListSecretsFor returns the secrets v may see, sorted by name.
The scoped counterpart to ListSecrets, which stays unscoped because rotation, offboarding and the hub-host CLI all legitimately need every row. Request handlers must use this one; tests/security enforces that they do.
func (*Broker) LookupGrant ¶ added in v0.0.5
LookupGrant returns a grant as stored: who it is issued to, its constraints, when and why it was revoked. Metadata only — a grant carries no material.
func (*Broker) Mint ¶
Mint seals a payload and stores it as a new secret.
The payload is zeroed in the caller's slice on return so a mint site does not leave plaintext in a buffer the garbage collector may not touch for a while. Callers that still need the bytes must pass a copy.
func (*Broker) NoteLeaseRevocation ¶ added in v0.0.5
NoteLeaseRevocation records why a revocation is still pending.
func (*Broker) PendingLeaseRevocations ¶ added in v0.0.5
func (b *Broker) PendingLeaseRevocations() ([]LeaseRevocation, error)
PendingLeaseRevocations returns every revocation not yet enforced, nil when this broker keeps none.
func (*Broker) ReconfirmSupersededSlots ¶ added in v0.0.5
func (b *Broker) ReconfirmSupersededSlots()
ReconfirmSupersededSlots marks every GitHub App token slot this broker holds whose grant was superseded since its scope was last held to it, so its next refresh holds the scope to the successor's constraints — and, for a token in the workload's files, so that refresh is the keepalive's next tick (Task 20403). An edit that narrowed an assignment narrows the running workload's token this way; one that widened it cannot widen it.
func (*Broker) RecordHeldOn ¶ added in v0.0.5
func (b *Broker) RecordHeldOn(rec LeaseRecord, grantID string) []string
HeldOn returns the grants rec carries on grantID's authority, as Broker.HeldOn does for a lease this broker holds.
func (*Broker) RecordLeaseRevocation ¶ added in v0.0.5
func (b *Broker) RecordLeaseRevocation(rev LeaseRevocation) (LeaseRevocation, bool, error)
RecordLeaseRevocation writes rev down, filling in who the lease was issued to and who holds it from the lease's record where the caller did not, and returns what was recorded. recorded is false with a nil error when this broker keeps no revocations.
It records nothing about the lease's material and asks no holder anything: that is the caller's fan-out, which this record outlives.
func (*Broker) RedactionValues ¶ added in v0.0.5
RedactionValues returns the plaintext a held lease's stored credentials carry — the delivered keys of its env secrets, the token of a personal access token — for a hub process that must scrub its workload's output without having issued it: the one that took the lease over with its run (Task 20382). The issuing process scrubbed with what it delivered; a rehydrated handle has nothing, because a dispatched Spec's secrets are never persisted.
It renders nothing and mints nothing: a GitHub App token was minted for the lease by the process that issued it and is not in the store, and it is the shape the pattern-based scrubbers already recognise. Nil for a lease this broker does not hold.
func (*Broker) RefreshAppToken ¶ added in v0.0.5
func (b *Broker) RefreshAppToken(ctx context.Context, leaseID, grantID, held string) (AppTokenRefresh, error)
RefreshAppToken re-mints the installation token leaseID holds for grantID and returns it. held is the token the caller presents now; when the slot's token is no longer that one, another caller refreshed first and its token comes back with no Retire, unminted.
It is the workspace path's refresh (pkg/executor/gitcreds): a pinned git proxy session presents the token its inner lease carries. A lease path session is handed its refresher by the guard instead (GitGuardRequest), and a sandbox's token file is refreshed by RefreshLeaseFiles.
func (*Broker) RefreshLeaseFiles ¶ added in v0.0.5
RefreshLeaseFiles re-mints every App token lease delivered as a file that is within AppTokenRefreshWindow of its end, and returns the files to rewrite — together with any token refreshed earlier whose file has not reached the workload yet. Nil when there is nothing to do, which is the common case and costs one map lookup.
Guarded tokens are skipped: their proxy session refreshes them. So is a lease with no App material.
func (*Broker) RefuseRenewal ¶ added in v0.0.5
RefuseRenewal records, as a secret.renew deny row, that a credential was not renewed and why — for a renewal refused before any attempt was made, such as a Kubernetes share whose owner the hub now denies (Task 20420).
func (*Broker) Release ¶
Release drops a lease's server-side record and destroys any credential the hub minted for it. Callers should Close the Mount as well; wiping the lease directory is what removes the copy on disk, and this is what removes the one at GitHub.
It takes no context because every caller is an executor cleanup path that has already finished its work. The revocation gets its own bounded deadline instead (appRevokeTimeout), so a hub cannot block on api.github.com while a task teardown waits behind it.
func (*Broker) Renew ¶
Renew re-issues a lease for the same requester.
It deliberately re-evaluates every grant from the store rather than extending the existing materials. That is what makes revocation effective within one lease period: a grant revoked a minute ago is gone from the renewed lease even though the executor and project are unchanged.
func (*Broker) RenewSecret ¶ added in v0.0.5
RenewSecret opens a secret, has req.Renew compute its new payload, and stores that under the same id, grants and owner. It returns the stored secret.
It authorises nothing: the HTTP layer decides who may renew whose secret, as it decides who may delete one.
func (*Broker) RequestAccess ¶ added in v0.0.2
func (b *Broker) RequestAccess(ctx context.Context, in AccessRequestInput) (AccessRequest, error)
RequestAccess files a request for a grant that does not exist yet.
The request is validated against the secret's kind at filing time, using the same Constraints.ValidateFor that Grant uses. That is deliberate: an ask that could never be approved — a github request with no repository allowlist — should fail in front of the person who can fix it, not days later in front of the approver, who would then have to guess what was meant.
func (*Broker) RequestUses ¶ added in v0.0.2
func (b *Broker) RequestUses(requestID string) ([]RequestUse, error)
RequestUses returns what an approval actually produced: every lease that redeemed the grant, and the task that held it where one is known.
func (*Broker) Restore ¶ added in v0.0.5
Restore takes over a lease another hub process issued, for the run this one adopted: from here on this broker extends it and releases it.
It refuses what Extend would refuse — a lease that lapsed while nobody held it, a grant revoked or expired since, a grant no longer issued to the requester, a secret that is gone — because taking a lease over is extending its life past the process that issued it, and nothing may keep a credential alive that its grants no longer allow. A lease another process took over first is refused with ErrLeaseMoved.
A lease some of whose grants were revoked while nobody held it, and some not, is taken over rather than refused (Task 20403): the returned Lease names the revoked ones in Withdrawn, for the adopting process to take back from the workload and drop (DropLeaseGrant), and the run keeps the grants nobody revoked. Only a lease every grant of which was withdrawn is refused whole.
The returned Lease names the materials the lease carries — grant, secret, kind, constraints — and holds none of their values: the workload already has them, and this process has no business re-reading them to hold a lease.
func (*Broker) RetireRecord ¶ added in v0.0.5
func (b *Broker) RetireRecord(rec LeaseRecord, reason string) (bool, error)
RetireRecord ends a recorded lease that no hub process will take over: its run ended while nobody held it, or it lapsed and has been swept from the devices that held it. The record goes — conditionally on it still being held by rec.Holder, so a lease a process took over meanwhile is left alone — and the lease's trail gets the release row its holder never wrote. It reports whether the record was retired.
func (*Broker) RetireSuperseded ¶ added in v0.0.5
RetireSuperseded destroys the superseded tokens of leaseID whose grace has passed: replaced in the sandbox's token file at least appTokenRetireGrace ago. It is the no-proxy half of retirement, called by the hub's lease keepalive; a proxy session retires its own when its requests drain.
A superseded token that has simply expired is forgotten without a call to GitHub, and so is one whose grant exported it into the environment (see envExported) once it lapses.
func (*Broker) Revoke ¶
Revoke marks a grant unusable: the next Lease or Renew leaves it out, and the subscribers this process registered with OnGrantRevoked — the hub — take its material back from the workloads already holding it (Task 20403). In a process with no subscriber, a CLI, nothing reaches those workloads from here: the caller announces the revocation to the running hub, and failing that the lease lapses within its period, as before.
func (*Broker) RevokeBecause ¶ added in v0.0.5
RevokeBecause is Revoke with a reason on its secret.revoke row (Task 20400): an offboarding revokes the grants over the departed person's secrets, and "revoked" alone does not say that is why.
func (*Broker) RevokeGrant ¶ added in v0.0.5
func (b *Broker) RevokeGrant(ctx context.Context, req RevokeGrantRequest) (GrantRevocation, bool, error)
RevokeGrant marks a grant unusable and announces it to this process's subscribers (OnGrantRevoked), which take its material back from the workloads holding it. It reports what was revoked and whether this call revoked it: false, with a nil error, for a grant that was already revoked — a retry is not an error, and announces nothing.
func (*Broker) RevokeWithCause ¶ added in v0.0.5
func (b *Broker) RevokeWithCause(ctx context.Context, grantID, actor string, cause RevocationCause, reason string) error
RevokeWithCause is RevokeBecause recording on the grant why it was revoked (Task 20400). Offboarding revokes with RevokedOwnerOffboarded, which is what lets the project's next lease tell it the grant went with its owner — even when the secret is kept under a legal hold, or destroyed days later.
func (*Broker) SettleLeaseRevocation ¶ added in v0.0.5
SettleLeaseRevocation records that a revocation's material was taken back from every holder, by the hub process by.
func (*Broker) Supersede ¶ added in v0.0.5
func (b *Broker) Supersede(ctx context.Context, grantID, successorID, actor string) (GrantRevocation, bool, error)
Supersede revokes grantID because successorID replaces it — an edited assignment, a credential granted again for longer — and records the successor on the grant, so the leases carrying grantID stand on successorID instead of giving the material back, and announces it as a supersession. The successor must be live and issued to the same subject: a grant replaced by one that reaches somebody else, or by nothing, is withdrawn, which is Revoke.
func (*Broker) SuspendLeaseTokens ¶ added in v0.0.5
SuspendLeaseTokens lets go of the App tokens a lease holds without ending the lease, for a process that stops serving it: shutting down gracefully, or handing it to the process that adopted its run. A token a proxy session presented is destroyed at GitHub — the session is suspended with it, and whoever restores the session mints its own. A token in a workload's file is left alone: the workload is still using it, and the new holder replaces it before it expires. So is a file token a refresh superseded whose successor the workload has not confirmed receiving: its file may still hold it, so it lapses at its hour rather than being destroyed under the workload. The slots' records stay, for the new holder.
func (*Broker) SweepExpired ¶
SweepExpired drops lease records whose TTL has passed and reports what is left, tallied by credential kind.
It exists for two reasons that happen to have the same implementation.
The metrics reason: cloop_secret_leases_live has to mean "leases that would be honoured right now", and the only way to know that is to apply the same expiry test the broker applies. Counting map entries would report leases that expired hours ago as live.
The correctness reason: before this, a lease record was removed only by Renew or Release. A lease that simply expired — the ordinary end of a task that was killed, or an executor that went away without releasing — left its leaseState in the map for the lifetime of the process. On a busy hub that is an unbounded map keyed by lease ID, which is to say a slow leak that a tenant can drive. Expiry now collects them.
The returned maps are (expired, live) counts by kind.
func (*Broker) Tombstone ¶ added in v0.0.5
Tombstone returns what a deleted secret was, and whether this broker's store remembers it.
func (*Broker) UpdateSecretMetadata ¶ added in v0.0.5
UpdateSecretMetadata rewrites a secret's metadata, leaving its payload as it is: bookkeeping a renewal keeps about itself, such as why the last attempt failed. It is not audited; what it records is.
func (*Broker) WithdrawRequest ¶ added in v0.0.2
WithdrawRequest lets the requester take back a pending ask.
Only the requester may withdraw, and the check is here rather than at the route: "withdraw" and "deny" are different words for different acts, and an approver who wants a request gone says no to it, on the record, with a note. Letting an approver withdraw would give them a way to make a request they did not want to answer disappear without ever appearing as a refusal.
func (*Broker) WithdrawRequestOnOffboard ¶ added in v0.0.5
func (b *Broker) WithdrawRequestOnOffboard(ctx context.Context, id, actor, reason string) (AccessRequest, error)
WithdrawRequestOnOffboard withdraws a pending request because the person who filed it has been offboarded (Task 20400).
The one withdrawal the requester does not make, and the exception is narrow on purpose. WithdrawRequest refuses everyone but the requester so that an approver who does not want a request says no on the record — a rule about people who are still here. A departed requester cannot withdraw anything, and leaving the ask open would let a later approval mint a grant for a person the hub no longer admits. So the offboarding withdraws it, under the offboarding actor's name, with the reason in the decision note — a different act from a denial, because nobody judged the request.
A request that is no longer pending reports ErrRequestNotPending, which the offboarding treats as already settled.
func (*Broker) WithdrawUnguardedAck ¶ added in v0.0.5
WithdrawUnguardedAck clears req's grant's acknowledgement, so the grant is refused again on a hub with no monitor. Withdrawing one that is not in force is refused, so the trail never records a withdrawal of nothing.
func (*Broker) WithdrawnGrants ¶ added in v0.0.5
func (b *Broker) WithdrawnGrants(leaseID string) []WithdrawnGrant
WithdrawnGrants returns the grants the live lease leaseID carries that no longer authorise it: revoked, superseded with no successor it stands on, or gone with their secret. An expired grant is not among them — the lease's deadline is clamped to its grants' expiry, so the lease lapses with it — and neither is a grant the store could not be asked about just now. Nil for a lease this broker does not hold.
type CausedRevoker ¶ added in v0.0.5
type CausedRevoker interface {
// RevokeGrantWithCause is RevokeGrant recording the cause. Revoking an
// already-revoked grant changes neither its time nor its cause.
RevokeGrantWithCause(id string, at time.Time, cause RevocationCause) error
}
CausedRevoker is an optional Store extension that records why a grant was revoked. A store without it revokes as before and its revocations have no cause, which costs only the refusal a project would otherwise be told.
type Cipher ¶
type Cipher struct {
// contains filtered or unexported fields
}
Cipher seals and opens secret payloads with AES-256-GCM.
Unlike pkg/secret, which re-derives its key from a fresh salt on every save, the broker derives once from a salt persisted in the store and caches the key. That matters here: pkg/secret rewrites one file wholesale, while the broker opens individual payloads on every lease. At 200 000 SHA-256 rounds, per-payload derivation would put roughly a fifth of a second of CPU between an executor and each of its credentials. Per-payload randomness comes from the GCM nonce instead, which is what it is for.
func NewCipher ¶
NewCipher derives the payload key from the hub's current root (pkg/rootkey) and the store-wide salt. The salt is created on first use and persisted, so the same passphrase yields the same key across restarts.
A single-key Cipher is what a store with no key registry gets — the test doubles; it has no previous root to open with, because it has no rotation to finish.
func NewCipherWithKey ¶
NewCipherWithKey builds a Cipher from a raw 32-byte key. Used by tests and by callers that manage key material themselves.
func NewLegacyCipher ¶
NewLegacyCipher rebuilds the pre-envelope cipher from a passphrase and the store-wide salt.
It is exported for exactly two callers: the tests that verify pre-envelope rows still open after migration 0019, and any recovery tooling that has to read a database whose broker_meta salt survived but whose registry did not. Nothing on the serving path uses it — the Keyring loads the legacy cipher itself, and only when a salt is already present.
func (*Cipher) OpenEnvelope ¶
OpenEnvelope lets a plain Cipher satisfy sealer.
func (*Cipher) SealFor ¶
SealFor lets a plain Cipher satisfy sealer. It produces legacy-shaped envelopes: no wrapped DEK, ciphertext directly under the one key.
Associated data is ignored, because the legacy construction has nowhere to put it. That is a real (small) difference in the guarantees a Cipher-backed broker offers versus a Keyring-backed one, and it is the reason production paths use a Keyring: pkg/secretstore implements KeyStore, so every hub gets envelope encryption and the row binding that comes with it.
type Constraints ¶
type Constraints struct {
// Repos is an owner/repo glob allowlist for github_pat and github_app.
// Patterns match case-insensitively; "*" alone allows every repository.
Repos []string `json:"repos,omitempty"`
// Permissions is the permission set a github credential may exercise
// ("contents:read", "pull_requests:write"). Enforced at cloop's own
// GitHub call sites via AllowsPermission; GitHub cannot narrow an
// already-issued PAT server-side.
Permissions []string `json:"permissions,omitempty"`
// Branches narrows where a github_pat or github_app grant may push:
// branch-name globs such as "cloop/*", "feature/**" or "develop". Empty
// means the grant adds no branch restriction of its own, and the hub's git
// proxy policy (executors.git_proxy.allowed_refs) is the only limit.
//
// Only a grant that authorises a push may carry one — a branch list on a
// read-only grant restricts nothing and would read as though it did — and
// it restricts branches only: a push of a tag, or of any other ref, is
// outside every branch list.
//
// Nothing inside a sandbox can enforce this, because git's credential
// protocol never says which ref a push is for. The git proxy can, since it
// reads every pushed command before forwarding it; see pkg/gitproxy
// Policy.RestrictRefs. Where no proxy guards the grant, the broker withholds
// the grant's write authority rather than deliver a credential that would
// ignore the list: see RestrictsBranches.
Branches []string `json:"branches,omitempty"`
// Namespaces is the Kubernetes namespace allowlist for kubeconfig.
Namespaces []string `json:"namespaces,omitempty"`
// Contexts is the kubeconfig context allowlist. The delivered
// kubeconfig contains only these contexts.
Contexts []string `json:"contexts,omitempty"`
// Verbs is the RBAC verb allowlist for a kubeconfig grant: what the
// holder may *do* to the cluster, as opposed to which parts of it the
// other two fields let them see.
//
// Empty means read-only — get, list and watch — which is the answer to
// "an operator granted a cluster and said nothing about writes". It is
// the one place in this struct where an empty list is neither "allow all"
// nor a validation error, and the asymmetry is deliberate: the safe
// reading of silence about a cluster credential is that nobody asked for
// the ability to change anything.
//
// Unlike Namespaces and Contexts, this cannot be enforced by rewriting
// the kubeconfig — a kubeconfig has no field for it. It is enforced by
// pkg/kubeguard, the monitor the hub runs outside the sandbox, and it
// therefore only takes effect when executors.kube_guard is enabled.
// KubeMonitorOnly names what a grant claims that only the monitor
// enforces, and a grant claiming anything is not delivered without one
// (ErrKubeConstraintsUnenforced) unless an administrator acknowledged it.
Verbs []string `json:"verbs,omitempty"`
// Hosts is the allowed-host list for egress_proxy. A leading "*."
// matches subdomains only, not the bare domain.
Hosts []string `json:"hosts,omitempty"`
// Registries is the container-registry allowlist for registry secrets.
Registries []string `json:"registries,omitempty"`
// EnvKeys restricts which keys of an env secret are delivered. Empty
// means every key in the secret, which is safe because an env secret's
// keys *are* its scope — there is nothing wider to fall open to.
EnvKeys []string `json:"env_keys,omitempty"`
// Devices is the device-name allowlist for host_device. Patterns match
// case-sensitively against the inventory's handles; "*" alone allows
// every device in the inventory.
//
// Case-sensitive for the reason the local_repo matcher is: these names
// are chosen by an operator and matched exactly, and folding would make a
// grant on "gpu0" also open "GPU0" — which, if both existed, would be two
// different pieces of hardware nobody named.
Devices []string `json:"devices,omitempty"`
// Interfaces is the interface-name allowlist for host_interface. Patterns
// match case-sensitively against the inventory's handles, exactly as
// Devices does and for the same reason; "*" alone allows every interface
// in the inventory.
Interfaces []string `json:"interfaces,omitempty"`
// Writable makes a local_repo grant read-write. It is the one constraint
// that widens rather than narrows, so it is a bool that defaults to the
// safe reading: a grant that says nothing delivers a read-only mount.
//
// Read-only is the useful default rather than a cautious one. The common
// case is a sandbox that needs to *read* a developer's checkout — build
// against it, grep it, copy from it — and a read-only bind means a
// runaway harness cannot rewrite the history of a repository that exists
// nowhere else. A project that genuinely needs to commit asks for it.
Writable bool `json:"writable,omitempty"`
}
Constraints narrow what a granted secret may be used for. Which fields apply depends on the secret's Kind; ValidateFor rejects a grant whose constraints do not gate its kind, so an under-specified grant is a creation-time error rather than a delivery-time surprise.
The wildcard "*" is always spelled out. An empty list on a gating dimension is rejected by ValidateFor rather than treated as "allow all" — the single most common way allowlists fail open is an empty slice reading as "no restrictions", and that reading is unavailable here.
func (Constraints) AllowsContext ¶
func (c Constraints) AllowsContext(name string) bool
AllowsContext reports whether a kubeconfig context name is permitted. A grant that lists namespaces but no contexts permits every context (the namespace pin is then the constraint); a grant that lists contexts restricts to exactly those.
func (Constraints) AllowsEnvKey ¶
func (c Constraints) AllowsEnvKey(key string) bool
AllowsEnvKey reports whether an env secret's key may be delivered. An empty EnvKeys list allows every key the secret contains — see the field comment for why that is not a fail-open.
func (Constraints) AllowsHost ¶
func (c Constraints) AllowsHost(host string) bool
AllowsHost reports whether host is in the egress allowlist.
func (Constraints) AllowsNamespace ¶
func (c Constraints) AllowsNamespace(ns string) bool
AllowsNamespace reports whether ns is permitted. An empty allowlist denies: ValidateFor guarantees a kubeconfig grant has namespaces or contexts, and where it has only contexts, namespace checks are not the gate.
func (Constraints) AllowsPermission ¶
func (c Constraints) AllowsPermission(perm string) bool
AllowsPermission reports whether a github permission is within the grant's permission set. An empty set permits nothing beyond read: an operator who does not enumerate permissions has not authorised any write.
func (Constraints) AllowsRegistry ¶
func (c Constraints) AllowsRegistry(reg string) bool
AllowsRegistry reports whether a container registry host is permitted. Registry names are host-shaped, so they reuse the host matcher's subdomain semantics after normalisation.
func (Constraints) AllowsRepo ¶
func (c Constraints) AllowsRepo(repo string) bool
AllowsRepo reports whether repo is covered by the allowlist.
Matching is per-segment: path.Match's "*" does not cross "/", so "org/*" covers org/tool but not a three-segment path, and a bare "*" is special- cased to mean every repository. Comparison is case-insensitive because GitHub owner and repo names are.
func (Constraints) BranchNames ¶ added in v0.0.3
func (c Constraints) BranchNames() []string
BranchNames is the allowlist without the refs/heads/ prefix, the form a person types and reads.
func (Constraints) BranchRefPatterns ¶ added in v0.0.3
func (c Constraints) BranchRefPatterns() []string
BranchRefPatterns renders the branch allowlist as full ref patterns, the form pkg/gitproxy matches pushed refs against: "cloop/*" becomes "refs/heads/cloop/*", and an entry already written with the prefix is kept as it is.
func (Constraints) CheckHost ¶
func (c Constraints) CheckHost(host string) error
CheckHost is AllowsHost with a reason attached.
"*.example.com" matches subdomains only — api.example.com yes, example.com no. Requiring the bare domain to be listed separately keeps "allow the API subdomains" from silently also allowing the apex, which is often a different service on a different box.
func (Constraints) CheckRepo ¶
func (c Constraints) CheckRepo(repo string) error
CheckRepo is AllowsRepo with the denial reason attached, so callers can put a cause in the audit log instead of a bare false.
func (Constraints) Covers ¶ added in v0.0.5
func (c Constraints) Covers(old Constraints) (bool, string)
Covers reports whether c allows at least everything old allows and, when it does not, names the first dimension that narrows.
It decides whether a grant replacing old may stand for the material a running workload already holds under old (Task 20403). Keeping that material on c's authority is safe only if c would have delivered it too — otherwise an edit that narrows a grant would narrow every run but the ones running. So the proof is literal: every pattern old lists, c lists as well, or c lifts the restriction altogether. It never compares what two different patterns match, which makes it conservative in one direction only: an edit that widens by rewriting a pattern ("cloop/a" into "cloop/*") reads as one that narrows, and the old grant is withdrawn rather than kept. Never the reverse.
func (Constraints) KubeMonitorOnly ¶ added in v0.0.5
func (c Constraints) KubeMonitorOnly() []string
KubeMonitorOnly lists what a kubeconfig grant claims that nothing but the Kubernetes access monitor enforces, as phrases for a person (Task 20420). Empty means the minimised kubeconfig itself carries everything the grant says, so delivering it without the monitor claims nothing false.
Two dimensions can claim such a thing:
- Namespaces, when they confine: without the monitor a namespace is only the default kubectl uses when -n is not given. A list holding "*" does not confine.
- Verbs, when they are short of all eight: a kubeconfig has no field for verbs, so without the monitor every verb the kubeconfig's identity holds works. The empty list is read-only, and so claims the most.
Contexts are not here: minimisation removes the others from the document, which needs no monitor.
func (Constraints) KubeReadOnly ¶ added in v0.0.2
func (c Constraints) KubeReadOnly() bool
KubeReadOnly reports whether the grant's effective verbs only read. It is what the UI badges and the audit summary render.
func (Constraints) KubeVerbs ¶ added in v0.0.2
func (c Constraints) KubeVerbs() []string
KubeVerbs returns the effective verb allowlist for a kubeconfig grant, resolving the empty list to read-only.
Callers must use this rather than reading Verbs directly, because the two differ in exactly the case that matters: a grant nobody configured.
func (Constraints) RestrictsBranches ¶ added in v0.0.3
func (c Constraints) RestrictsBranches() bool
RestrictsBranches reports whether this grant's pushes are limited to a branch allowlist.
It is the question every delivery path has to answer before handing over a credential that can write, because the answer decides whether the credential may leave the hub at all. A restricted grant is only ever honoured through the git proxy; where there is none, the broker withholds the write rather than deliver a credential that would ignore the list.
func (Constraints) Summary ¶
func (c Constraints) Summary() string
Summary renders the constraints as a short, sorted, human-readable string for CLI listings and audit payloads. It contains only allowlist patterns, never payload material.
func (Constraints) ValidateFor ¶
func (c Constraints) ValidateFor(kind Kind) error
ValidateFor checks that these constraints are well-formed *and* that they actually gate the given kind. A github_pat grant with no repo allowlist is rejected here: there is no safe default for "which repositories may this token touch", so the operator has to say, even if what they say is "*".
type DecideInput ¶ added in v0.0.2
type DecideInput struct {
RequestID string
// Actor is the deciding identity, compared against the requester to refuse
// self-approval.
Actor string
// Note is the approver's comment. Required on a denial — a refusal with no
// reason is one the requester can only respond to by asking again — and
// optional on an approval.
Note string
// TTL, when positive, shortens the minted grant. It can only ever narrow:
// a value above what was requested, or above the delegation ceiling, is
// clamped rather than honoured.
TTL time.Duration
// Delegation is the ceiling this approver may hand out.
Delegation Delegation
}
DecideInput is what an approver supplies. Shared by ApproveRequest and DenyRequest so the two cannot drift apart on identity or note handling.
type Delegation ¶ added in v0.0.2
type Delegation struct {
// MaxTTL caps the minted grant's lifetime. Zero means
// DefaultDelegationMaxTTL.
MaxTTL time.Duration
// AllowWildcardSubject permits approving a request aimed at every project,
// every executor, or every requester.
//
// Its own flag rather than a TTL question because the two failure modes are
// not comparable: a grant that lives too long is bounded by its expiry,
// while a grant aimed at project:* reaches every tenant on the hub for as
// long as it lasts. An approver entitled to the first is not automatically
// entitled to the second.
AllowWildcardSubject bool
}
Delegation bounds what one approver may hand out.
It is a parameter rather than something this package derives, because the answer lives in pkg/authz and in the caller's session: the broker knows what a grant *is*, not who the person clicking approve is allowed to be. Passing it in keeps the authorisation model in one place and still lets the clamp be applied at the point the grant is minted, where it cannot be bypassed by a caller that forgot to check.
The zero value is the safe one: no wildcard subjects, and the default TTL ceiling. A caller that forgets to populate it gets the narrow answer.
type DeletionCause ¶ added in v0.0.5
type DeletionCause string
DeletionCause says why a secret was destroyed, in the terms a refusal shows to the people who lost it.
const ( // CauseDeleted: somebody deleted the secret — its owner, or an operator. CauseDeleted DeletionCause = "deleted" // CauseOffboarded: the secret was personal, and was destroyed because its // owner was offboarded. CauseOffboarded DeletionCause = "offboarded" )
type DeliveredFile ¶
type DeliveredFile struct {
GrantID string
SecretName string
Kind Kind
// Dir is the directory on the executor, mirroring Delivery.Dir so a
// caller can build one flat list from several leases.
Dir string
Name string
Mode os.FileMode
// Content is the plaintext.
Content []byte
}
DeliveredFile is one credential file a Delivery is handing to a driver.
func (DeliveredFile) GoString ¶
func (f DeliveredFile) GoString() string
GoString mirrors String so %#v is redacted too.
func (DeliveredFile) String ¶
func (f DeliveredFile) String() string
String redacts the content, so a `%v` on a delivery cannot log a credential.
type Delivery ¶
type Delivery struct {
// Dir is where the workload will find the files. It is a path on the
// executor, not on this host.
Dir string
// contains filtered or unexported fields
}
Delivery is a lease rendered for someone else's filesystem: the environment to hand the workload, the per-grant attribution, and the file bytes the driver must place at Dir.
It is the Mount's counterpart, with the same accessors and one difference that is the point of the type: nothing here has touched a disk, so Close zeroes buffers rather than unlinking files.
func (*Delivery) Bindings ¶
func (d *Delivery) Bindings() []LeaseBinding
Bindings returns the per-grant attribution: names and paths, never values.
func (*Delivery) Close ¶
Close zeroes the buffers holding the plaintext and drops the rest. It is idempotent, so it is safe in a defer beside an explicit call.
Zeroing is not a guarantee — Go's garbage collector may already have copied the bytes during a slice growth, and nothing here can reach that copy — but it bounds the window in which a heap dump of a long-lived control plane contains a credential it relayed hours ago.
func (*Delivery) Devices ¶ added in v0.0.2
func (d *Delivery) Devices() []GrantedDevice
Devices returns the host devices this lease opens.
func (*Delivery) Files ¶
func (d *Delivery) Files() []DeliveredFile
Files returns the credential files for the driver to place, with their contents. Nil after Close.
The content is copied per call rather than aliased: a driver that base64s it into a frame must not be handed the buffer Close will zero underneath it.
func (*Delivery) Interfaces ¶ added in v0.0.3
func (d *Delivery) Interfaces() []GrantedInterface
Interfaces returns the host network interfaces this lease moves into the sandbox.
func (*Delivery) Withdraw ¶ added in v0.0.5
Withdraw zeroes and drops the file contents grantID contributed, and reports how many files there were: one grant of a lease taken back while the others stay (Task 20403). A refresh never re-delivers them, and a heap dump taken later does not contain them. A file several grants went into goes with any of its owners (kubemulti.go).
type EnvOffer ¶ added in v0.0.5
type EnvOffer struct {
// Grant is the grant, revoked or not; Active says whether a lease issued
// now would use it. Expired grants are included so a caller can say which
// credential lapsed rather than only that none is left.
Grant Grant
// Secret is the env secret the grant points at. It carries no plaintext,
// by construction (see Secret).
Secret Secret
// Keys are the variable names the grant's material would hold, sorted:
// the payload's own keys, narrowed by the grant's env_keys allowlist and
// by what can be encoded as K=V. The same rule envMaterial applies.
Keys []string
// Err is why the keys could not be read — the payload would not open, or
// the secret is gone. A lease would deny this grant for the same reason.
Err error
}
EnvOffer is what one env grant would contribute, by name, to the environment of a lease for some requester.
type Envelope ¶
Envelope is one piece of envelope-encrypted material.
A row stores all three parts. KeyID names the KEK that WrappedDEK is sealed under; Ciphertext is sealed under the DEK inside WrappedDEK. Rotation rewrites the first two and leaves the third byte-identical.
KeyID == LegacyKeyID (or "") means the pre-envelope construction: WrappedDEK is empty and Ciphertext is sealed directly under the passphrase-derived key. Those rows keep working until a rotation upgrades them.
func LegacyEnvelope ¶
LegacyEnvelope wraps a pre-envelope ciphertext for the open path.
type Event ¶
type Event struct {
Time time.Time `json:"time"`
Action Action `json:"action"`
Decision Decision `json:"decision"`
// Actor is who asked: an OIDC subject, "cli", "ui", or an executor ID.
Actor string `json:"actor,omitempty"`
// Subject is the grant subject the operation concerned, rendered in
// the CLI's "--to" syntax.
Subject string `json:"subject,omitempty"`
// SecretID and SecretName identify the credential. The name is a
// handle chosen by an operator, never the value.
SecretID string `json:"secret_id,omitempty"`
SecretName string `json:"secret_name,omitempty"`
Kind Kind `json:"kind,omitempty"`
GrantID string `json:"grant_id,omitempty"`
// RequestID is the access request an event concerns (Task 20271). Present
// on the request family above, and on nothing else: a grant minted directly
// has no request behind it, and leaving the field empty is what makes
// "which grants came through review" answerable from the trail.
RequestID string `json:"request_id,omitempty"`
LeaseID string `json:"lease_id,omitempty"`
ExecutorID string `json:"executor_id,omitempty"`
ProjectID string `json:"project_id,omitempty"`
// RunID names the execution a lease was issued to (Task 20282). It is what
// makes "which credentials did this task hold" answerable exactly: a task
// id may run many times and unions every attempt's leases, whereas a run id
// names the one execution that spent them.
//
// Populated for dispatch-time leases. Empty for grant and mint events,
// which are operator actions with no execution behind them.
RunID string `json:"run_id,omitempty"`
// Constraints is the allowlist summary that applied to the decision.
Constraints string `json:"constraints,omitempty"`
// ExpiresAt is the TTL stamp of the grant or lease involved.
ExpiresAt time.Time `json:"expires_at,omitempty"`
// Reason explains a denial, or annotates an allow ("2 grants matched").
Reason string `json:"reason,omitempty"`
// TaskID is the unit of work the decision was made for. Populated by
// the egress broker, which knows it; the secret broker's leases are
// per-executor rather than per-task and leave it empty.
TaskID string `json:"task_id,omitempty"`
// Host and Port name an egress destination. They are a hostname and a
// port and nothing else — deliberately not a URL, because a URL has a
// path and a query, and those are request contents rather than access
// metadata.
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
// BytesUp and BytesDown are the session's transfer totals at the time of
// the decision, from the workload's point of view.
BytesUp int64 `json:"bytes_up,omitempty"`
BytesDown int64 `json:"bytes_down,omitempty"`
}
Event is one audit record. Every field on it is metadata about a credential: which one, for whom, under what constraints, and what was decided. None of them is, or may become, the credential itself.
This is enforced rather than asserted: Event has no field capable of carrying a payload, and Redact scrubs the free-text Reason before the event leaves the package, because Reason is built from error strings and an error string is the one place a payload could plausibly get spliced in.
func Redact ¶
Redact returns a copy of ev safe to persist.
It scrubs the Reason of anything that pattern-matches a credential. That may look like belt-and-braces given that no code path deliberately puts a payload in a Reason — and it is. The reason to do it anyway is that Reason is assembled from wrapped errors, and error wrapping is exactly how a payload leaks: someone one day writes fmt.Errorf("bad token %q: %w", tok, err) three packages down, and the audit log quietly becomes the place the credential is stored in plaintext forever. Scrubbing at the boundary means that mistake degrades to an unhelpful log line instead of a breach.
Auditors must call this (the broker calls it on every emission), and the storage adapter calls it again on the way in.
type File ¶
type File struct {
// Name is a bare file name — no directory separators. Materialize
// re-checks this before writing, so a crafted name cannot escape the
// lease directory.
Name string
Content []byte
Mode os.FileMode
// EnvVar, when set, receives the file's absolute path after
// materialisation (KUBECONFIG, GIT_CONFIG_GLOBAL, DOCKER_CONFIG).
EnvVar string
// EnvIsDir points EnvVar at the containing directory rather than the
// file, which is what DOCKER_CONFIG expects.
EnvIsDir bool
// contains filtered or unexported fields
}
File is one credential file to place in the lease directory.
type GitGuard ¶ added in v0.0.2
type GitGuard interface {
// GuardGitHub takes a GitHub token and the constraints it was granted
// under, and returns the credentials a sandbox should use in its place.
//
// Returning a zero GitGuardResult with a nil error means "not guarding
// this one" — the delivery falls back to the in-sandbox helper. That is
// how a hub with the proxy switched off keeps working, and it is a
// decision the implementation makes deliberately rather than one this
// package infers from an error it cannot interpret.
GuardGitHub(ctx context.Context, req GitGuardRequest) (GitGuardResult, error)
}
GitGuard interposes an out-of-sandbox proxy between a workload and GitHub.
An implementation takes custody of the token and returns credentials for a proxy that holds it. It must not return a result that embeds the token.
type GitGuardRequest ¶ added in v0.0.2
type GitGuardRequest struct {
// Token is the credential the guard takes custody of. It must not be
// copied into the result, into a log, or into anything the sandbox reads.
Token string
// Host is the forge host Token is good against, in forgehost's canonical
// form: github.com, or the GitHub Enterprise Server host the grant's
// secret names (Task 20423). The guard presents Token there and nowhere
// else, and routes the sandbox's https://<Host>/ to a proxy URL of that
// host's own (GitGuardResult.ForgeURL). Empty is github.com, the only
// host a request named before hosts were recorded.
Host string
// TokenExpiresAt is when GitHub stops honouring Token: a GitHub App
// installation token's hour. Zero for a PAT, whose expiry the hub does not
// know.
TokenExpiresAt time.Time
// Refresh re-mints Token before it expires, at the scope it was first
// minted at (Task 20375): the same installation, repositories and
// permissions, and only while the grant behind it holds. Set for a
// github_app grant, nil for a PAT. held is the token the caller presents
// now; see Broker.RefreshAppToken.
//
// A guard that keeps Token for longer than an hour — a proxy session —
// calls it from its own request path when Token is within
// AppTokenRefreshWindow of TokenExpiresAt, presents the result from then
// on, and calls its Retire once nothing presents the old one. An error
// wrapping ErrRefreshRefused is final: the grant's access has ended, the
// tokens are destroyed, and the guard should close what it minted.
Refresh func(ctx context.Context, held string) (AppTokenRefresh, error)
// Repos is the grant's owner/name glob allowlist. The guard enforces it.
Repos []string
// Permissions is the grant's permission set ("contents:read"). The guard
// translates it into what the proxy will allow — most importantly whether
// the sandbox may push at all.
Permissions []string
// Branches is the grant's branch allowlist, as the grant stores it. The
// guard narrows the session's ref policy to it, within the hub's own
// allowlist. Empty means the grant adds no branch restriction.
Branches []string
// Labels. None is secret; all are audit bookkeeping.
SecretName string
SecretID string
GrantID string
// LeaseID is the lease the token is being issued in, and RunID the run
// it is for (Task 20383): a guard records both on the session it mints,
// so the process that takes the lease over with the run can restore it.
LeaseID string
RunID string
ProjectID string
ExecutorID string
Actor string
// Owner is the identity a personal secret belongs to, or "" for a shared
// one. It rides along so the proxy's audit rows name the person whose
// credential is being spent, which for a personal PAT is the whole point.
Owner string
}
GitGuardRequest is one token and the authority it was granted.
type GitGuardResult ¶ added in v0.0.2
type GitGuardResult struct {
// BaseURL is the proxy's https base, with no path. The sandbox's git
// reaches the proxy here, and its credential helper answers for this
// host only.
BaseURL string
// ForgeURL is what the sandbox's https://<Host>/ is rewritten to: the
// proxy's route for the forge host the session presents its credential
// to, beneath BaseURL (Task 20423). Empty means BaseURL itself, the route
// for github.com. A lease holding grants for two hosts gets one rewrite
// per host, each to that host's own route, so neither can reach the
// other's session.
ForgeURL string
// Username and Password authenticate to the proxy and to nothing else.
Username string
Password string
// ExpiresAt is when the proxy stops honouring them.
ExpiresAt time.Time
// SessionID identifies the proxy session, for audit joins and revocation.
SessionID string
// Summary is an audit-safe description of what the proxy will enforce
// ("read-only, acme/*"). It carries no credential.
Summary string
// ReadOnly records whether the grant's permissions left the session
// unable to push. Surfaced so the lease's own summary can say so without
// re-deriving the rule the guard already applied.
ReadOnly bool
// PushRefs is the hub's own ref allowlist for pushes — the ceiling the
// grant's Branches narrow, as full ref patterns. Announced to the workload
// so it can choose a branch the proxy will accept, rather than learn the
// policy one refused push at a time. Empty when the session cannot push.
PushRefs []string
}
GitGuardResult is the substitute credential a sandbox receives.
func (GitGuardResult) Guarded ¶ added in v0.0.2
func (r GitGuardResult) Guarded() bool
Guarded reports whether the result actually carries a substitute credential. The zero value means the guard declined, which is not an error.
type GitHubAppAPI ¶ added in v0.0.2
type GitHubAppAPI interface {
// CreateInstallationToken mints an installation access token.
CreateInstallationToken(ctx context.Context, req InstallationTokenRequest) (InstallationToken, error)
// ListInstallationRepos enumerates the repositories the *token* reaches.
// It is authenticated with an installation token, not the app JWT —
// GitHub offers no app-JWT route to this list.
ListInstallationRepos(ctx context.Context, baseURL, installationToken string) ([]InstallationRepo, error)
// RevokeInstallationToken destroys the token used to authenticate the
// call. After it returns nil the credential is dead at GitHub, not merely
// deleted from a sandbox's disk.
RevokeInstallationToken(ctx context.Context, baseURL, installationToken string) error
// ListAppInstallations enumerates where the App is installed. It is
// authenticated with the app JWT rather than an installation token —
// it is the one call that runs before any installation is known, which is
// the whole reason it exists.
ListAppInstallations(ctx context.Context, baseURL, appJWT string) ([]AppInstallation, error)
}
GitHubAppAPI is every call this package makes to GitHub.
It exists so the mint/refresh/revoke logic can be tested without a network, and so that a hub operator reading this file can see the complete list of what cloop asks GitHub for on their behalf: mint a scoped token, enumerate the installation, destroy a token.
func NewHTTPGitHubAppAPI ¶ added in v0.0.6
func NewHTTPGitHubAppAPI(client *http.Client) GitHubAppAPI
NewHTTPGitHubAppAPI returns the production GitHub App API client over client: for a caller whose GitHub — a GitHub Enterprise Server behind a private CA, a test's stand-in — needs a transport of its own. Pass it to WithGitHubApp. A nil client gets the default's per-request timeout.
type Grant ¶
type Grant struct {
ID string `json:"id"`
SecretID string `json:"secret_id"`
// Scope is a free-form operator-facing label ("ci", "deploy") used for
// grouping and filtering. It carries no authorisation weight — that is
// entirely Subject plus Constraints — so an operator cannot widen a
// grant by editing it.
Scope string `json:"scope,omitempty"`
Subject Subject `json:"subject"`
Constraints Constraints `json:"constraints"`
// ExpiresAt is when the grant stops being usable. Zero means no expiry,
// which Validate permits only because the legacy import needs it; the
// CLI always sets a TTL.
ExpiresAt time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
RevokedAt time.Time `json:"revoked_at,omitempty"`
// Owner is copied from the secret this grant points at when that secret
// is personal, and is empty otherwise.
//
// Denormalised on purpose. A grant listing is read far more often than a
// secret is minted, and the alternative — resolving every grant's secret
// to find out whether the row should be shown — is the N+1 read that the
// listing handlers already go out of their way to avoid. More importantly
// it keeps the row self-describing: when the secret is deleted, the
// revoked grants it leaves behind still say whose credential they spent,
// which is exactly the question an offboarding or incident review asks.
Owner string `json:"owner,omitempty"`
// RevokedCause is why a revoked grant was revoked, when a revocation said
// (Task 20400): its owner was offboarded, or its secret was deleted. Empty
// for a grant revoked by hand. It decides whether a lease that meets the
// grant hands the refusal back for the project to be told.
RevokedCause RevocationCause `json:"revoked_cause,omitempty"`
// SupersededBy names the grant that replaced this one, for a grant revoked
// with RevokedSuperseded (Task 20403). A lease still carrying this grant
// stands on the successor's authority — and on its successor's, should it
// be superseded in turn.
SupersededBy string `json:"superseded_by,omitempty"`
// SuspendedAt is when the grant was suspended because its owner was
// deactivated at the identity provider, and SuspendedBy which SCIM
// credential did it (Task 20415). A suspended grant is refused like a
// revoked one until the provider reactivates the owner, which lifts the
// suspension — unlike a revocation, which nothing lifts. Zero while the
// grant is not suspended.
SuspendedAt time.Time `json:"suspended_at,omitempty"`
SuspendedBy string `json:"suspended_by,omitempty"`
// UnguardedAck is an administrator's recorded decision that this
// kubeconfig grant may be delivered without the Kubernetes access monitor,
// though it claims something only the monitor enforces (Task 20420). Zero
// unless recorded; see Broker.AcknowledgeUnguarded.
UnguardedAck UnguardedAck `json:"unguarded_ack,omitzero"`
}
Grant authorises one subject to use one secret, under Constraints, until ExpiresAt.
func (Grant) DenyReason ¶
DenyReason returns the audit-friendly reason this grant cannot be used at now, or "" when it is usable. Returning the reason rather than a bool is what lets every denial land in the audit log with a cause attached.
func (Grant) Personal ¶ added in v0.0.2
Personal reports whether this grant hands out a personally-owned credential.
func (Grant) Standing ¶ added in v0.0.5
Standing reports whether the grant is neither revoked nor expired at now — usable, or suspended and able to be again (Task 20415). It is what severing a grant has to find: an offboarding that read only the active ones would leave a suspended grant standing, ready for nothing but a lift.
func (Grant) Validate ¶
Validate checks a Grant's structural invariants, including that its constraints make sense for the kind of secret it points at.
type GrantFilter ¶
type GrantFilter struct {
// Subject, when set, keeps only grants whose subject renders to this
// string (exact match on the "--to" syntax).
Subject string
// SecretRef, when set, keeps only grants for that secret.
SecretRef string
// ActiveOnly drops expired and revoked grants.
ActiveOnly bool
}
GrantFilter narrows ListGrants.
type GrantRequest ¶
type GrantRequest struct {
// SecretRef is a secret ID or name.
SecretRef string
Subject Subject
Constraints Constraints
Scope string
// TTL is the grant's lifetime. Zero means DefaultGrantTTL unless
// NoExpiry is set.
TTL time.Duration
// NoExpiry creates a grant that never expires. Reserved for the legacy
// import, where imposing a TTL on secrets that previously had none
// would break running projects at an unpredictable moment.
NoExpiry bool
Actor string
// Viewer is the identity the grant is created on behalf of. It gates
// personal secrets: only their owner may hand one to an executor
// (Task 20275). The zero value sees no personal secret at all, so a
// caller that forgets it is refused rather than privileged.
//
// Shared secrets ignore it entirely, which is what keeps every existing
// call site — the CLI, the legacy import, the approval path — correct
// without change.
Viewer Viewer
}
GrantRequest describes a grant to create.
type GrantRevocation ¶ added in v0.0.5
type GrantRevocation struct {
GrantID string
SecretID string
SecretName string
Kind Kind
Subject Subject
Actor string
Cause RevocationCause
Reason string
At time.Time
// SupersededBy names the grant that replaced this one, for a
// supersession (Cause RevokedSuperseded).
SupersededBy string
// ControlPlane is where the store keeps its data — the database file —
// when the store can say (Locator). A process serving more than one
// control plane uses it to tell whose revocation this is.
ControlPlane string
}
GrantRevocation is one grant a Broker in this process has just stamped revoked.
func (GrantRevocation) Superseded ¶ added in v0.0.5
func (r GrantRevocation) Superseded() bool
Superseded reports whether the grant was replaced rather than withdrawn.
type GrantedDevice ¶ added in v0.0.2
type GrantedDevice struct {
// Name is the handle from the inventory, and what a grant's allowlist and
// a sandbox spec's `devices:` list match against.
Name string `json:"name"`
// Source is the absolute device path on the executor's host.
Source string `json:"source"`
// Target is where it appears inside the sandbox. Equal to Source unless
// the inventory remapped it.
Target string `json:"target"`
// Permissions is the rwm access string ("r", "rw", "rwm").
Permissions string `json:"permissions"`
}
GrantedDevice is one device node a grant opens to a workload.
It mirrors executor.HostDevice and is deliberately a separate type, for the reason RepoMount is: this package does not import pkg/executor, so that the broker stays a statement about authority and the executor package stays a statement about execution. The caller that holds both — pkg/ui — converts.
func ParseDeviceInventory ¶ added in v0.0.2
func ParseDeviceInventory(payload []byte) ([]GrantedDevice, error)
ParseDeviceInventory validates a host_device payload.
Exported so that storing one of these fails in front of the person who typed it, with a message they can act on. It is called by `cloop secret mint` and by the dashboard's mint route before either stores anything.
Validating early matters because the late failure is *quiet*. Broker.LeaseFor emits a deny row for a grant whose material will not build and continues with the rest (see broker.go), so a malformed inventory yields a lease that is simply missing its devices — and a sandbox that starts, runs, and reports on hardware it never had. The audit row is there for anyone who goes looking; the run does not fail, and nobody is looking.
type GrantedInterface ¶ added in v0.0.3
type GrantedInterface struct {
// Name is the handle from the inventory, and what a grant's allowlist and
// a sandbox spec's `interfaces:` list match against.
Name string `json:"name"`
// Source is the interface's name on the executor's host.
Source string `json:"source"`
// Target is the name it takes inside the sandbox. Equal to Source unless
// the inventory renamed it.
Target string `json:"target"`
// Address is the optional CIDR to configure inside the sandbox.
Address string `json:"address,omitempty"`
// Gateway is the optional default route to install through it.
Gateway string `json:"gateway,omitempty"`
// MTU optionally overrides the interface's MTU. Zero means leave it.
MTU int `json:"mtu,omitempty"`
}
GrantedInterface is one network interface a grant moves into a sandbox.
It mirrors executor.HostInterface and is deliberately a separate type, for the reason GrantedDevice mirrors HostDevice: this package does not import pkg/executor, so the broker stays a statement about authority and the executor package stays a statement about execution. The caller that holds both — pkg/ui — converts.
func ParseInterfaceInventory ¶ added in v0.0.3
func ParseInterfaceInventory(payload []byte) ([]GrantedInterface, error)
ParseInterfaceInventory validates a host_interface payload.
Exported so that storing one fails in front of the person who typed it, with a message they can act on — `cloop secret mint` and the dashboard's mint route both call it before anything is stored.
Validating early matters more here than anywhere else, because the late failure is both quiet and destructive. Broker.LeaseFor emits a deny row for a grant whose material will not build and continues with the rest, so a malformed inventory yields a sandbox that starts with no wire in it; and an inventory naming the *wrong* interface is not caught by any amount of retrying, because by then the host has already handed it over.
type InstallationRepo ¶ added in v0.0.2
type InstallationRepo struct {
ID int64 `json:"id"`
FullName string `json:"full_name"`
Private bool `json:"private"`
}
InstallationRepo is one repository an installation covers.
type InstallationToken ¶ added in v0.0.2
type InstallationToken struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
Permissions map[string]string `json:"permissions,omitempty"`
RepositorySelection string `json:"repository_selection,omitempty"`
}
InstallationToken is what GitHub minted.
func (InstallationToken) GoString ¶ added in v0.0.2
func (t InstallationToken) GoString() string
GoString mirrors String so %#v is redacted too.
func (InstallationToken) String ¶ added in v0.0.2
func (t InstallationToken) String() string
String redacts the token so a %v on a mint result cannot log a credential.
type InstallationTokenRequest ¶ added in v0.0.2
type InstallationTokenRequest struct {
// BaseURL and AppJWT authenticate the request as the app.
BaseURL string
AppJWT string
// InstallationID names the installation to mint for.
InstallationID int64
// RepositoryIDs narrows the token to these repositories. IDs rather than
// names: a name is ambiguous across the owners an installation may span,
// and an ID cannot be made to mean a different repository by a rename.
RepositoryIDs []int64
// Permissions narrows what the token may do. Nil asks for the
// installation's full permission set.
Permissions map[string]string
}
InstallationTokenRequest is a mint request. Zero-valued scoping fields mean "the whole installation", which is why every caller here sets one unless the grant's allowlist is explicitly "*".
type KEKRecord ¶
type KEKRecord struct {
ID string
Salt string // hex-encoded KDF salt; emptied on retirement
State string // primary | active | retired
// CheckValue is kekCheckPlaintext sealed under this KEK.
CheckValue []byte
CreatedAt time.Time
CreatedBy string
RetiredAt time.Time
// Root is the fingerprint of the root secret the key derives from
// (pkg/rootkey); "" for a key nobody has attributed yet. A record, not an
// authority: which root opens a key is decided by deriving it.
Root string
}
KEKRecord is one key-encryption key as stored. It never contains key material: `Salt` plus the passphrase derives the key, and the salt alone is useless.
type KeyAttributor ¶ added in v0.0.6
type KeyAttributor interface {
// AttributeKEK records root for a key no one has attributed yet, and
// reports whether it did. A recorded root is never rewritten.
AttributeKEK(id, root string) (bool, error)
}
KeyAttributor is the optional part of a KeyStore that records which root a key derives from (Task 20425). A store without it — the in-memory test doubles — still opens, mints and rotates; its old keys stay unattributed.
type KeyCheck ¶ added in v0.0.5
type KeyCheck struct {
ID string `json:"id"`
State string `json:"state"`
// Opens: one of the roots derives the key and it passes its check value.
Opens bool `json:"opens"`
// Root is the index in the roots checked of the one that derives it — 0
// for the first, the current root — or -1 when none does.
Root int `json:"root"`
// Retired: the key's salt was destroyed, so nothing derives it — whatever
// is still sealed under it was lost before anybody asked.
Retired bool `json:"retired,omitempty"`
}
KeyCheck is one sealing key judged against a set of roots.
func CheckKeys ¶ added in v0.0.5
CheckKeys reports, for every record, whether one of roots derives it: the check-value test OpenKeyring runs before it will serve, asked of every root a hub could hold — the current one and, during a change of root, the previous (Task 20425). Every live record is derived — about 60 ms per root tried — because a registry with one corrupt row is exactly the case where "the first one opened" says nothing about the rest. Results are ordered by ID. Empty roots are skipped; no roots open nothing.
type KeyInfo ¶
type KeyInfo struct {
ID string `json:"id"`
State string `json:"state"`
Primary bool `json:"primary"`
Openable bool `json:"openable"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
RetiredAt time.Time `json:"retired_at,omitempty"`
// Root is the fingerprint the registry records for the root the key
// derives from; "" when nobody has attributed it.
Root string `json:"root,omitempty"`
// DerivedBy says which of this process's roots derives it, RootCurrent
// or RootPrevious; "" when neither does.
DerivedBy string `json:"derived_by,omitempty"`
}
KeyInfo is a KEK as reported to an operator. It carries no key material and is safe to render in a CLI table or an API response.
type KeyStore ¶
type KeyStore interface {
// PutKEK inserts or replaces a KEK record.
PutKEK(k KEKRecord) error
// ListKEKs returns every KEK, including retired ones — retired rows are
// kept so `cloop hub key list` can still explain what a row referencing
// a dead key is referencing.
ListKEKs() ([]KEKRecord, error)
// PrimaryKEK returns the current primary, if there is one. It exists
// separately from ListKEKs because it is on the seal path: every seal
// re-reads it, so it must be a single indexed lookup rather than a scan.
PrimaryKEK() (KEKRecord, bool, error)
// PromoteKEK makes id the sole primary, demoting any incumbent, in one
// transaction. Atomicity matters: two hubs promoting concurrently must
// not both succeed and leave rows sealed under a key neither considers
// current.
PromoteKEK(id string, at time.Time) error
// RetireKEK marks a KEK retired and destroys its salt. It must refuse
// while any sealed row still references the key.
RetireKEK(id string, at time.Time) error
}
KeyStore is the persistence a Keyring needs on top of the broker's Store.
It is separate from Store on purpose. Store has several in-memory implementations in tests whose value is that they are twenty lines long; folding key management into it would force every one of them to grow a KEK registry to keep compiling, for no gain. A store that implements both gets envelope encryption and rotation; a store that implements only Store keeps the single-key behaviour, which is exactly right for a test double.
type KeyUsage ¶
type KeyUsage struct {
KeyID string `json:"key_id"`
Total int `json:"total"`
BySet map[string]int `json:"by_set"`
}
KeyUsage is how many rows of one set are sealed under one key.
type Keyring ¶
type Keyring struct {
// contains filtered or unexported fields
}
Keyring holds every derivable KEK and performs envelope seal/open.
Safe for concurrent use: leases and session reads both go through it from many goroutines, and a rotation mutates the primary underneath them.
func OpenKeyring ¶
func OpenKeyring(store KeyStore, opts ...KeyringOption) (*Keyring, error)
OpenKeyring loads the KEK registry, derives every live KEK from the roots it was given — by default the hub's, resolved through pkg/rootkey — and ensures a primary exists.
Derivation is eager, and goes through a cache that lives as long as the process (keycache.go). A derivation costs ~60ms at 200 000 KDF rounds, and pkg/ui opens a keyring for every request that touches a secret, so without the cache a hub that has rotated a few times would pay a few hundred milliseconds a request; with it, a key is derived once per process. Eager also means an unusable key is an open failure rather than an intermittent one: the rule below is checked against every live key, not the first.
func (*Keyring) AddKey ¶
AddKey mints a new KEK, promotes it to primary, and returns it. Existing material stays readable under its old KEK — that overlap is what lets a rotation run while the hub serves traffic.
func (*Keyring) HasLegacy ¶
HasLegacy reports whether pre-envelope material can still be opened on this hub. False with legacy rows present means those rows are unreadable, which `cloop hub key status` reports rather than leaving to be discovered.
func (*Keyring) HasPrevious ¶ added in v0.0.6
HasPrevious reports whether this keyring holds a previous root: a change of root is in progress on this process.
func (*Keyring) OpenEnvelope ¶
OpenEnvelope unwraps the DEK and decrypts the payload.
The error cases are deliberately distinguishable, because they call for completely different operator responses: a retired key means the material is gone and the credential must be re-minted; an underivable key means the passphrase is wrong and the material is probably fine; a tag failure means tampering or corruption. Collapsing all three into "decryption failed" turns a five-minute diagnosis into an afternoon.
func (*Keyring) PreviousRootKeys ¶ added in v0.0.6
PreviousRootKeys returns the live keys the previous root derives, sorted: what retiring the previous root retires. Every live key is derived first, so the answer does not depend on which ones happen to have been read.
func (*Keyring) PrimaryFromCurrent ¶ added in v0.0.6
PrimaryFromCurrent reports whether the primary derives from the current root. False during a change of root until `cloop hub key rotate --retire-previous` has minted a key of the new one, and on a keyring with no primary.
func (*Keyring) Reload ¶
Reload re-reads the KEK registry, adopting keys another process minted and dropping ones it retired.
It never mints and never demotes to nothing: a registry that has become unreadable leaves the in-memory state exactly as it was, because a hub that is currently serving reads is better off with a stale key set than with an empty one.
func (*Keyring) RetireKey ¶
RetireKey destroys a KEK's salt, making it permanently underivable.
The store refuses while any row still references the key, so this is safe to call optimistically; the refusal is the second step the design requires, and it is enforced in SQL rather than by a check here that a concurrent write could race past.
func (*Keyring) Rewrap ¶
Rewrap moves an envelope onto the primary KEK without decrypting its payload.
This is the operation rotation is made of, and the reason it is cheap: the payload ciphertext is returned byte-identical, so a rotation over a hundred megabytes of kubeconfigs still only encrypts a hundred DEKs. It also means a rotation never materialises a credential, so a rotation running on a serving hub does not widen the plaintext exposure window at all.
func (*Keyring) RootFingerprints ¶ added in v0.0.6
RootFingerprints names the roots this keyring derives from; previous is "" when there is none. Each costs a key derivation the first time in a process.
func (*Keyring) SealFor ¶
SealFor seals plaintext under a fresh DEK wrapped by the primary KEK.
aad identifies the row this envelope belongs to — build it with AADFor so it carries the sealed set as well as the row id. The identical value must be supplied to OpenEnvelope; a mismatch is an authentication failure, which is how a row-swap attack surfaces.
func (*Keyring) Underivable ¶ added in v0.0.6
Underivable names the live keys no configured root derives. Only a read-only keyring can have any (WithoutKeyCreation); one opened to serve is refused instead.
type KeyringOption ¶
type KeyringOption func(*keyringConfig)
KeyringOption configures a Keyring.
func WithKeyringActor ¶
func WithKeyringActor(a string) KeyringOption
WithKeyringActor records who caused a KEK to be created.
func WithKeyringClock ¶
func WithKeyringClock(fn func() time.Time) KeyringOption
WithKeyringClock overrides the time source.
func WithKeyringPassphrase ¶
func WithKeyringPassphrase(p string) KeyringOption
WithKeyringPassphrase gives the keyring one root, p, instead of resolving the hub's (pkg/rootkey). Tests use it; so does any caller that holds the key some other way — a backup's preflight, an escrow being checked.
func WithKeyringRoots ¶ added in v0.0.6
func WithKeyringRoots(current, previous string) KeyringOption
WithKeyringRoots gives the keyring a current root and a previous one ("" for none) instead of resolving them.
func WithoutKeyCreation ¶
func WithoutKeyCreation() KeyringOption
WithoutKeyCreation opens the keyring read-only with respect to the registry: if no primary KEK exists, OpenKeyring fails rather than minting one.
Read-only diagnostics (`cloop hub key list` against someone else's hub, a health check) should not have the side effect of creating key material, and a diagnostic that silently mints a KEK would report a healthy registry it had just fabricated.
type Kind ¶
type Kind string
Kind classifies what a secret *is*, which determines both the constraints that apply to it and how it is minimized and delivered.
const ( // KindGitHubPAT is a GitHub personal access token. Constrained by a // repository allowlist and a permission set. KindGitHubPAT Kind = "github_pat" // KindGitHubApp is a GitHub App installation credential (JSON with // app_id/installation_id/private_key). Constrained like a PAT. KindGitHubApp Kind = "github_app" // KindKubeconfig is a kubeconfig YAML document. Constrained by allowed // contexts and namespaces, and rewritten to match before delivery. KindKubeconfig Kind = "kubeconfig" // KindRegistry is a container-registry credential (docker config JSON, // or "user:password"). Constrained by an allowed registry list. KindRegistry Kind = "registry" // KindEnv is an opaque environment variable or set of them. Constrained // by an allowed key list. KindEnv Kind = "env" // KindEgressProxy is an outbound proxy endpoint, possibly with embedded // credentials. Constrained by an allowed-host list. KindEgressProxy Kind = "egress_proxy" // KindLocalRepo is a directory on the hub host that holds git // repositories. Constrained by a repository allowlist, and delivered as // a bind mount rather than as bytes. // // It is the odd one out here and deliberately so. The payload is a path, // not a credential, and the thing being granted is reach into the control // plane's own filesystem — which is exactly why it belongs in this // package rather than beside SpecMount. A path that a sandbox may read is // an authority, and every property this broker already provides for // authorities is one it needs: a named subject so the grant reaches one // project and not the fleet, a TTL so it lapses, an audit row recording // who opened it, and revocation that lands within a lease period. // // The alternative — a host path in .cloop/sandbox.yaml — is the reason // SpecMount sources are workspace-relative: that file is repo-committed, // so it is whatever a pull request says it is. This kind is the same // capability with the trust inverted. A human with secret.grant names the // path, out of band, and the project receives it. KindLocalRepo Kind = "local_repo" // KindHostDevice is an inventory of device nodes on the executor's host, // opened to one project. Constrained by a device-name allowlist, and // delivered as a runtime --device rather than as bytes. // // It is the widest authority this broker issues, and it is here for the // reason KindLocalRepo is: the alternative is a device path in // .cloop/sandbox.yaml, which is whatever a pull request says it is. See // hostdevice.go. KindHostDevice Kind = "host_device" // KindHostInterface is an inventory of network interfaces on the // executor's host, moved into one project's sandbox. Constrained by an // interface-name allowlist, and delivered as a namespace move rather // than as bytes. // // It is wider still than KindHostDevice, and differs from every other // kind in one way: honouring it takes the interface *away* from the // host for the life of the workload. See hostinterface.go. KindHostInterface Kind = "host_interface" )
func Kinds ¶
func Kinds() []Kind
Kinds returns every valid Kind, sorted, for CLI help and validation messages.
type KubeGrantDescription ¶ added in v0.0.5
type KubeGrantDescription struct {
// Contexts are the grant's contexts in the merged kubeconfig, its own
// current-context first.
Contexts []string `json:"contexts"`
// Secret names the secret the grant spends.
Secret string `json:"secret"`
// Namespace is the namespace the grant pins its contexts to, as
// CLOOP_K8S_NAMESPACE says for a single grant. Absent when the allowlist
// names none outright.
Namespace string `json:"namespace,omitempty"`
// AllowedNamespaces is the grant's namespace allowlist. Absent when the
// grant is not narrowed by namespace.
AllowedNamespaces []string `json:"allowed_namespaces,omitempty"`
// Verbs are the verbs the grant permits.
Verbs []string `json:"verbs"`
// Monitored reports whether the grant's contexts reach their cluster
// through the Kubernetes monitor, which enforces AllowedNamespaces and
// Verbs; unmonitored, the cluster's own RBAC is all that bounds them.
Monitored bool `json:"monitored"`
// Session is a monitored grant's session id.
Session string `json:"session,omitempty"`
}
KubeGrantDescription is one grant's entry in CLOOP_K8S_GRANTS. Exported so a harness written in Go, and this package's tests, decode the variable with the type that wrote it.
type KubeGuard ¶ added in v0.0.2
type KubeGuard interface {
// GuardKubeconfig takes custody of a kubeconfig and the constraints it
// was granted under, and returns the document to deliver instead.
//
// Returning a zero result with a nil error means "declined": there is no
// monitor on this hub, and the caller should deliver the minimised
// kubeconfig as before. Returning an error means a monitor was required
// and could not be provided, which must fail the lease rather than fall
// back — a hub whose config promises enforcement and silently delivers
// the raw credential is worse than one that never promised.
GuardKubeconfig(ctx context.Context, req KubeGuardRequest) (KubeGuardResult, error)
}
KubeGuard interposes an out-of-sandbox monitor between a workload and a Kubernetes cluster.
type KubeGuardRequest ¶ added in v0.0.2
type KubeGuardRequest struct {
// Kubeconfig is the minimised document, already reduced to the contexts
// the grant allows. The monitor takes custody of it; it must not be
// copied into the result, into a log, or into anything the sandbox reads.
//
// Minimised rather than raw on purpose: the two narrowings compose, so a
// monitor that was somehow pointed at the wrong context still cannot
// reach a cluster the grant excluded.
Kubeconfig []byte
// Verbs is the grant's effective verb allowlist, already resolved from
// Constraints.KubeVerbs — so the monitor never has to re-derive that an
// empty list means read-only.
Verbs []string
// Namespaces is the grant's namespace allowlist. The monitor enforces it
// on every request, which is the thing the delivered document could not.
Namespaces []string
// Labels. None is secret; all are audit bookkeeping.
SecretName string
SecretID string
GrantID string
// LeaseID is the lease the kubeconfig is being issued in, and RunID the
// run it is for: recorded on the session so it can be restored with the
// lease (Task 20383).
LeaseID string
RunID string
ProjectID string
ExecutorID string
Actor string
// Owner is the identity a personal kubeconfig belongs to, or "" for a
// shared one. It rides along so the monitor's audit rows name the person
// whose cluster credential is being spent.
Owner string
}
KubeGuardRequest is what the broker hands the monitor.
type KubeGuardResult ¶ added in v0.0.2
type KubeGuardResult struct {
// Kubeconfig is the document to deliver into the sandbox. It points at
// the monitor and carries a session token, and it contains no cluster
// credential — that is the whole point, and kubeconfig_test.go in
// pkg/kubeguard asserts it against a known plaintext.
Kubeconfig []byte
// SessionID identifies the monitor session, for audit joins and
// revocation.
SessionID string
// ExpiresAt is when the monitor stops honouring the session.
ExpiresAt time.Time
// ReadOnly records whether the resulting session can only read. Surfaced
// so the lease summary can say so without re-deriving the rule.
ReadOnly bool
// Summary is an audit-safe description of what the monitor will enforce
// ("read-only, ns app|app-staging"). It carries no credential.
Summary string
}
KubeGuardResult is the monitor's answer.
func (KubeGuardResult) Guarded ¶ added in v0.0.2
func (r KubeGuardResult) Guarded() bool
Guarded reports whether the monitor actually took custody. A zero result is a decline.
type Lease ¶
type Lease struct {
ID string `json:"id"`
ExecutorID string `json:"executor_id,omitempty"`
ProjectID string `json:"project_id,omitempty"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
// RunID is the execution this lease was issued to (Task 20282). Carried on
// the lease so the renew and release rows correlate to the same run as the
// issuance — a lease's *end* is as audit-relevant as its start, and a
// release row that could not name its run would break the join at exactly
// the point an auditor is checking that a credential stopped being held.
RunID string `json:"run_id,omitempty"`
// Materials carries the credentials. json:"-" on the sensitive fields
// of Material keeps a marshalled Lease audit-safe.
Materials []Material `json:"materials"`
// Refused names the grants this lease was aimed at whose secret no longer
// exists (Task 20400), each with the reason its audit row records. Metadata
// only. The dispatcher shows them to the project, which cannot otherwise
// see that a credential it was granted — often a departed colleague's —
// was destroyed.
Refused []RefusedCredential `json:"refused,omitempty"`
// Withdrawn names grants a lease taken over from another hub process
// still carried that were revoked while nobody held it (Task 20403). The
// process that took it over takes their material back from the workload
// and drops them from the lease. Metadata only.
Withdrawn []WithdrawnGrant `json:"withdrawn,omitempty"`
}
Lease is a time-boxed bundle of materials issued to one requester.
TTL is the point of the type. A grant may last 24 hours, but the credentials an executor holds should not: a lease expires in minutes and is renewed while the work continues, so a compromised executor's window is bounded by the lease rather than by the grant.
func (*Lease) Deliver ¶
Deliver renders the lease for a workload that will find its credentials at dir on a filesystem this process does not own, without writing anything.
The Delivery holds the plaintext in memory for the driver to place — staged into a container's own tmpfs, projected as a Kubernetes Secret, or sent to an edge agent that writes it through its confined vault path. That is the whole difference from Materialize: the control plane never has a file to leak for a credential it is only relaying.
func (*Lease) Empty ¶
Empty reports whether the lease carries no credentials. An empty lease is a legitimate outcome — a project with no grants gets one — and callers should treat it as "run with no secrets", not as an error.
func (*Lease) Kinds ¶
Kinds returns the distinct credential kinds the lease carries, sorted.
Deduplicated because a lease holding two GitHub PATs is one lease that involves PATs, not two: leaving the duplicate in would make the lease event counters a function of how many grants happened to match rather than of how many leases were issued.
func (*Lease) Materialize ¶
Materialize writes the lease's credentials into a private directory and returns the Mount describing it. baseDir may be empty to use a tmpfs.
Call it only for a workload that will read this host's filesystem — see executor.Capabilities.SecretFilesFromHostPath. For anything isolated, use Deliver: writing plaintext here for a sandbox that cannot open it creates a credential file on the control plane that nothing ever reads.
The caller must Close the Mount when the workload exits. Failure partway through cleans up what was already written rather than leaving credential files behind.
func (*Lease) MaterializeAt ¶
MaterializeAt writes the lease's credentials into dir, which it creates.
dir must be absolute and its final element must carry leaseDirPrefix: the prefix is what every wipe path in this system uses to recognise a directory as lease-owned, and one created without it could never be swept. Creation is exclusive — a directory that already exists is refused rather than reused, so two leases cannot land in one directory and have the first Close take the second's files.
func (*Lease) SecretNames ¶
SecretNames lists the delivered secrets by name, for logging.
type LeaseBinding ¶
type LeaseBinding struct {
// GrantID is the grant this material came from.
GrantID string
// SecretID and SecretName identify the stored secret, for diagnostics.
SecretID string
SecretName string
// Kind is the credential kind.
Kind Kind
// EnvKeys are the variables this grant contributed, by name.
EnvKeys []string
// Files are the absolute paths written for this grant: the files whose
// removal withdrawing it requires. A file several grants went into is
// named by each grant it belongs to (kubemulti.go), which need not include
// the grant whose material carried it.
Files []string
// Dir is the lease directory holding Files.
Dir string
// Mounts are the host paths this grant opened, recorded for the audit
// trail rather than for revocation.
//
// The distinction matters and is a real limitation: a revoked lease's
// files are wiped and its variables scrubbed, but a bind already in a
// running sandbox's mount namespace cannot be taken back from outside it.
// A local_repo grant therefore lapses when the workload exits, not when
// the grant is revoked. Narrowing that window means stopping the workload.
Mounts []RepoMount
// Devices are the host device nodes this grant opened, recorded for the
// audit trail rather than for revocation.
//
// The same limitation as Mounts applies, and harder: a device already in a
// running sandbox's cgroup and mount namespace cannot be taken back from
// outside it. A host_device grant therefore lapses when the workload exits,
// not when the grant is revoked, and narrowing that window means stopping
// the workload.
Devices []GrantedDevice
// Interfaces are the host network interfaces this grant moved into the
// sandbox, recorded for the audit trail rather than for revocation.
//
// The same limitation as Devices applies: an interface inside a running
// sandbox's network namespace cannot be reached from outside it, so a
// host_interface grant lapses when the workload exits rather than when
// the grant is revoked.
Interfaces []GrantedInterface
}
LeaseBinding attributes part of a materialised lease to the grant that produced it: which environment variables it set, and which files it wrote.
It carries names and paths, never values. The point is revocation — an executor holding GITHUB_TOKEN cannot otherwise tell that the variable came from a grant an operator has just withdrawn, so "revoke this lease" could only be honoured by killing the whole workload. With the attribution, a driver scrubs exactly what the lease delivered and leaves the rest alone.
See pkg/executor.SecretBinding, the driver-facing shape this maps onto, and pkg/executor/remote/revoke.go for what a driver does with it.
type LeaseFileRefresh ¶ added in v0.0.5
type LeaseFileRefresh struct {
LeaseID string
// Files are the token files to rewrite in place, named as the lease
// rendered them. Dir is empty: the caller knows where the lease landed.
Files []DeliveredFile
// Refused names the grants whose refresh was refused. Their tokens are
// already destroyed at GitHub, so the files the workload holds name dead
// tokens, as after a grant revocation; the caller reports why.
Refused []RefusedRefresh
// Pending reports grants whose refresh failed for a reason that may pass.
// Their current token still works; the next call tries again.
Pending []string
// Summary is audit-safe: which grants, which files, until when.
Summary string
// contains filtered or unexported fields
}
LeaseFileRefresh is new content for token files a lease already delivered: what the hub's keepalive hands the executor holding the lease.
func (*LeaseFileRefresh) Close ¶ added in v0.0.5
func (r *LeaseFileRefresh) Close()
Close zeroes the file contents.
func (*LeaseFileRefresh) Delivered ¶ added in v0.0.5
func (r *LeaseFileRefresh) Delivered(eventual bool)
Delivered records that every file reached the workload. The tokens they replaced may be destroyed appTokenRetireGrace from now — unless eventual, when the files went to something that delivers them later (the kubelet syncing a Secret volume): the workload may still read the old token after any grace this side could choose, so the superseded token is left to lapse at its own expiry instead.
func (*LeaseFileRefresh) Empty ¶ added in v0.0.5
func (r *LeaseFileRefresh) Empty() bool
Empty reports whether there is nothing to deliver or report.
type LeaseGrantStore ¶ added in v0.0.5
type LeaseGrantStore interface {
SetLeaseGrants(id, holder string, grantIDs []string) (bool, error)
}
LeaseGrantStore is an optional LeaseStore extension that rewrites the grants a held lease carries if holder still holds it (Task 20403). A store without it keeps the grants a record was written with: a process taking such a lease over meets a withdrawn grant as revoked, and withdraws it again — idempotently — rather than carrying it.
type LeaseRecord ¶ added in v0.0.5
type LeaseRecord struct {
ID string
// Holder is the hub process that keeps the lease alive and owes its
// release.
Holder string
Requester Requester
Actor string
IssuedAt time.Time
ExpiresAt time.Time
Kinds []Kind
GrantIDs []string
}
LeaseRecord is the durable account of a live lease.
type LeaseRevocation ¶ added in v0.0.5
type LeaseRevocation struct {
LeaseID string
// GrantID narrows the revocation to one grant of the lease; "" is the
// whole lease.
GrantID string
// Action is how the material is taken back, "scrub" or "kill".
Action string
Reason string
Actor string
// ExecutorID and ProjectID name who the lease was issued to.
ExecutorID string
ProjectID string
// Holder is the hub process that held the lease when it was revoked, ""
// when none did.
Holder string
RevokedAt time.Time
// EnforcedAt is when a process found the material taken back from every
// holder; zero while the revocation is pending.
EnforcedAt time.Time
EnforcedBy string
// Detail is the last account of why a pending revocation is pending.
Detail string
}
LeaseRevocation is the durable account of a lease, or one grant of it, revoked.
func RevocationOf ¶ added in v0.0.5
func RevocationOf(err error) (LeaseRevocation, bool)
RevocationOf extracts the revocation an error refusing a revoked lease carries.
func (LeaseRevocation) Pending ¶ added in v0.0.5
func (r LeaseRevocation) Pending() bool
Pending reports whether the revocation is not yet enforced.
func (LeaseRevocation) Whole ¶ added in v0.0.5
func (r LeaseRevocation) Whole() bool
Whole reports whether the revocation takes the whole lease.
type LeaseRevocationStore ¶ added in v0.0.5
type LeaseRevocationStore interface {
// PutLeaseRevocation records a revocation; recording one already there
// re-opens it (see statedb.PutLeaseRevocation).
PutLeaseRevocation(r LeaseRevocation) error
// LeaseRevocations returns every revocation of one lease.
LeaseRevocations(leaseID string) ([]LeaseRevocation, error)
// ListLeaseRevocations returns every revocation, or only the pending.
ListLeaseRevocations(pendingOnly bool) ([]LeaseRevocation, error)
// SettleLeaseRevocation marks a pending revocation enforced, and reports
// whether it was pending.
SettleLeaseRevocation(leaseID, grantID, by, detail string, at time.Time) (bool, error)
// NoteLeaseRevocation records why a pending revocation is still pending.
NoteLeaseRevocation(leaseID, grantID, detail string) error
}
LeaseRevocationStore persists lease revocations. Optional: a store without it revokes as every broker did before Task 20419, by message alone.
type LeaseStore ¶ added in v0.0.5
type LeaseStore interface {
// PutLease inserts or replaces a record.
PutLease(r LeaseRecord) error
// GetLease returns one record, or a wrapped ErrLeaseNotFound.
GetLease(id string) (LeaseRecord, error)
// ListLeases returns every record.
ListLeases() ([]LeaseRecord, error)
// ExtendLease moves a record's deadline if holder holds it, and reports
// whether it did.
ExtendLease(id, holder string, expiresAt time.Time) (bool, error)
// TakeLease moves a record from one holder to another if from still holds
// it, and reports whether it did.
TakeLease(id, from, holder string) (bool, error)
// DeleteLease removes a record if holder holds it, and reports whether it
// did.
DeleteLease(id, holder string) (bool, error)
}
LeaseStore persists LeaseRecords. Optional: a Store that does not implement it keeps leases in memory only, as every broker did before Task 20382.
The conditional writes are what make a lease safe to hand over: each one names the holder it expects, so of two processes racing to take the same lease over only one succeeds, and the one that lost it cannot write over the winner's record.
type LegacyImportResult ¶
type LegacyImportResult struct {
// Imported names the secrets newly minted into the broker.
Imported []string
// Skipped names entries already present (the import is idempotent).
Skipped []string
}
LegacyImportResult reports what a legacy import did.
func ImportLegacySecrets ¶
func ImportLegacySecrets(ctx context.Context, b *Broker, workDir, actor string) (LegacyImportResult, error)
ImportLegacySecrets copies every entry from the flat pkg/secret store into the broker as an unscoped `env` secret with a matching wildcard grant.
"Unscoped" is the whole point and is deliberate. The entries being imported were, until this moment, delivered to every workload with no scoping at all; narrowing them during a migration would break running projects at a moment nobody chose, and would do it silently — the workload would simply stop seeing a variable it depends on. So the import preserves existing reach exactly, with SubjectAny and no expiry, and leaves tightening to an operator who can then do it one grant at a time and watch what breaks. Every imported grant is audited, so "which of my secrets are still wide open" is a query rather than an archaeology exercise.
It is idempotent: an entry whose name already exists in the broker is skipped, so this can run on every broker construction.
A missing or unreadable legacy store is not an error — most installs will not have one.
type Locator ¶ added in v0.0.5
type Locator interface {
Location() string
}
Locator is an optional Store extension that names where the store keeps its data. pkg/secretstore answers with its database file.
type Material ¶
type Material struct {
GrantID string `json:"grant_id"`
SecretID string `json:"secret_id"`
SecretName string `json:"secret_name"`
Kind Kind `json:"kind"`
Constraints Constraints `json:"constraints"`
// Env are variables to inject, already narrowed. Values are
// credentials; this field is json:"-" so a Material cannot be
// serialised into an API response by accident.
Env map[string]string `json:"-"`
// SensitiveEnv names the Env keys whose values are credentials rather
// than the constraints those credentials were narrowed to.
//
// The distinction only exists because output redaction needs it. A
// material delivers GITHUB_TOKEN *and* CLOOP_GITHUB_REPO_ALLOWLIST;
// scrubbing the first out of a task transcript protects a credential,
// and scrubbing the second would replace every mention of a repository
// name with a marker. Nothing about the two values says which is which,
// so the builder that knows says so here.
//
// Names only — a value here would be a second copy of the secret in a
// struct whose whole point is to keep track of the first.
SensitiveEnv []string `json:"-"`
// Files are written into the lease's tmpfs directory.
Files []File `json:"-"`
// Mounts are host paths the grant opens to the workload. Unlike Files
// they are not written anywhere: the path already exists and the grant
// is permission to reach it, so materialisation validates it and the
// driver binds it.
//
// This one is not json:"-", because a mount carries no secret — a source
// path and a read-only flag are exactly what an operator inspecting a
// lease needs to see, and hiding them would make the one material kind
// with no credential in it the hardest to audit.
Mounts []RepoMount `json:"mounts,omitempty"`
// Devices are host device nodes the grant opens to the workload. Like
// Mounts they are not written anywhere — the node already exists and the
// grant is permission to reach it — and like Mounts they are not
// json:"-", because a device name, path and access mode are exactly what
// an operator inspecting a lease needs to see and carry no credential.
Devices []GrantedDevice `json:"devices,omitempty"`
// Interfaces are host network interfaces the grant moves into the
// sandbox. Not written anywhere either, and not json:"-" for the reason
// Devices is not: a handle, a host name and an address are what an
// operator inspecting a lease needs to see and carry no credential.
Interfaces []GrantedInterface `json:"interfaces,omitempty"`
// Summary is the audit-safe description of what was delivered
// (surviving kubeconfig contexts, allowed registries, env key names).
Summary string `json:"summary,omitempty"`
// contains filtered or unexported fields
}
Material is one grant's credentials, already minimized against that grant's constraints and ready to hand to an executor.
The plaintext lives here and only here. A Material is short-lived by construction: it exists between Lease and Materialize, and the Mount that results is wiped when the workload exits.
func (Material) CheckForgeHost ¶ added in v0.0.6
CheckForgeHost reports why a reference to a repository on originHost may not be authenticated with this material, or nil when it may: a *forgehost.Mismatch naming both hosts when they differ.
func (Material) GitHubHost ¶ added in v0.0.6
GitHubHost returns the forge host a GitHub material's token is good against, in forgehost's canonical form — github.com, or a GitHub Enterprise Server host — and "" for any other kind. A material is presented to this host and to no other.
func (Material) GitHubToken ¶
GitHubToken returns the raw token a GitHub material carries, and whether it carries one at all.
It exists because workspace provisioning (pkg/executor/gitcreds) needs the token as an HTTP basic password, and the two paths that already deliver it — the credential-helper script and the bare GITHUB_TOKEN env var — are both wrong for that use. The helper needs a shell and a materialised lease directory, which is exactly the disk write a sandbox fetch must avoid; and GITHUB_TOKEN is only exported for an unrestricted "*" grant, so relying on it would make narrow grants silently unable to clone.
The returned string is the caller's to handle carefully — it is a credential, and nothing here can stop it being logged.
The dedicated field is consulted first and the Files scan is the fallback. The two used to be one thing, and separating them is what lets a guarded delivery withhold the token from the sandbox without also withholding it from the hub: under GitGuard there is no token file to find, but the hub's own workspace provisioning still needs the credential to hand to the proxy. The fallback keeps Materials built by older paths — and by tests that construct one directly — answering as they did.
func (Material) GitHubTokenExpiresAt ¶ added in v0.0.5
GitHubTokenExpiresAt reports when the GitHub token this material carries stops working, for a token the hub minted (github_app). Zero for a PAT.
It is what tells the workspace path that the credential it hands the git proxy needs a refresh source (Task 20375): a pinned session presenting it would otherwise fail an hour after dispatch.
type MintRequest ¶
type MintRequest struct {
Name string
Kind Kind
Payload []byte
Metadata map[string]string
Actor string
// Owner, when set, mints a *personal* secret belonging to that identity
// rather than a shared one belonging to the organisation (Task 20275).
// See ownership.go for everything that changes as a result.
Owner string
// Personal asserts that this mint is meant to produce a personal secret.
//
// Redundant with a non-empty Owner in the success case, and that is the
// point: it separates "mint this for alice@corp" from "mint this for
// whatever identity the caller happened to resolve", so a handler whose
// identity lookup silently returned "" produces ErrOwnerRequired instead
// of quietly minting the user's private credential as an organisation-wide
// one that every maintainer can then spend.
Personal bool
}
MintRequest describes a secret to store.
type Mount ¶
type Mount struct {
// Dir is the lease directory. Mode 0700, on a tmpfs where one is
// available.
Dir string
// contains filtered or unexported fields
}
Mount is a materialised lease on a filesystem: a private directory holding the credential files, plus the environment to hand the workload.
Close wipes it. The zero value is not usable; get one from Lease.Materialize.
func (*Mount) Bindings ¶
func (m *Mount) Bindings() []LeaseBinding
Bindings returns the per-grant attribution for this mount: which variables and files each grant contributed. Names and paths only — no values.
A driver uses it to build executor.SecretBinding, which is what makes a mid-run revocation targetable at one credential instead of at the whole workload.
func (*Mount) Close ¶
Close wipes and removes the mount. It is idempotent and safe to call from a defer alongside an explicit call.
Each file is overwritten with zeros before removal. On a tmpfs that is belt-and-braces, since the pages are freed anyway; on the os.TempDir() fallback it is the only thing standing between an unlinked credential and whoever reads the underlying blocks next. It is not a guarantee against a copy-on-write or log-structured filesystem, which is why tmpfs is preferred rather than merely convenient.
func (*Mount) Devices ¶ added in v0.0.2
func (m *Mount) Devices() []GrantedDevice
Devices returns the host devices this lease opens, in the order the materials were processed. The caller exposes them to the sandbox; there is nothing on disk here to clean up, so Close does not touch them.
func (*Mount) Env ¶
Env returns the environment additions in "K=V" form, sorted, ready to append to an executor.Spec's Env.
func (*Mount) Interfaces ¶ added in v0.0.3
func (m *Mount) Interfaces() []GrantedInterface
Interfaces returns the host network interfaces this lease moves into the sandbox. Like Devices there is nothing on disk to clean up: the netdev already exists and the grant is permission to claim it.
func (*Mount) Mounts ¶
Mounts returns the host repositories this lease opens, in the order the materials were processed. The caller binds them into the sandbox; there is nothing on disk here to clean up, so Close does not touch them.
func (*Mount) Withdraw ¶ added in v0.0.5
Withdraw wipes the files grantID wrote into the mount and forgets them, and reports how many there were: one grant of a lease taken back while the others stay (Task 20403). The directory stays, holding the other grants' files; Close removes it with them. A file shared with another grant — the gitconfig several GitHub grants install their helpers through — belongs to the first grant's binding and goes with it, which errs toward less access; the kubeconfig several kubeconfig grants are merged into belongs to every grant whose cluster credential it carries, and goes with any of them (kubemulti.go).
type Option ¶
type Option func(*Broker)
Option configures a Broker.
func WithAuditor ¶
WithAuditor sets the audit sink. Without one, events are dropped — which is acceptable for tests and for read-only CLI paths, and never for the control plane.
func WithCipher ¶
WithCipher supplies a pre-built Cipher, bypassing CLOOP_SECRET_KEY.
A Cipher has no key registry, so a broker built this way seals in the legacy single-key shape and cannot rotate. That is right for a test double and wrong for a hub; prefer WithKeyring where rotation matters.
func WithClock ¶
WithClock overrides the time source. Tests use it to exercise TTL expiry without sleeping.
func WithGitHubApp ¶ added in v0.0.2
func WithGitHubApp(api GitHubAppAPI) Option
WithGitHubApp supplies the GitHub API client used to mint, enumerate and destroy App installation tokens.
Without it a broker talks to api.github.com. Tests substitute a fake so the suite is hermetic, and a hub with no outbound access can substitute one that refuses — which denies github_app grants rather than falling back to delivering the private key, because there is no safe fallback.
func WithKeyring ¶
WithKeyring supplies a pre-opened Keyring, so a caller that already built one (the hub, which shares it with the session store) does not pay the KDF cost a second time.
func WithLeaseRecords ¶ added in v0.0.5
WithLeaseRecords makes every non-empty lease this broker issues durable, held by holder — the hub process's cluster member id. An empty holder records nothing: a lease that names no process is one nothing could take over from it.
func WithMaxLeaseTTL ¶
WithMaxLeaseTTL overrides DefaultMaxLeaseTTL. Values above the grant's own remaining lifetime are still clamped to it.
func WithoutKey ¶ added in v0.0.5
func WithoutKey() Option
WithoutKey builds a broker that can administer the store — list, revoke, delete, withdraw — and can neither seal nor open a payload (Task 20400).
It exists for offboarding from a shell. Destroying a departed user's personal secrets needs no key: it removes rows, it never reads them. A broker that insisted on CLOOP_SECRET_KEY anyway would make the command fail in exactly the emergency it is for, on a host whose operator shell does not carry the hub's sealing passphrase. Every operation that would touch material — Mint, Lease, anything that opens a payload — fails with ErrNoKey, so a keyless broker cannot be mistaken for a working one.
type RefusedCredential ¶ added in v0.0.5
type RefusedCredential struct {
GrantID string `json:"grant_id"`
SecretID string `json:"secret_id"`
SecretName string `json:"secret_name,omitempty"`
Kind Kind `json:"kind,omitempty"`
Owner string `json:"owner,omitempty"`
DeletedAt time.Time `json:"deleted_at,omitempty"`
// Reason is the refusal as the lease's audit row records it.
Reason string `json:"reason"`
// Delivery marks a refusal made while the material was being built: the
// grant and its secret stand, so what refused it may pass — a monitor
// started, a payload replaced — and every run it affects is told, where a
// destroyed secret is told once (Task 20413).
Delivery bool `json:"delivery,omitempty"`
}
RefusedCredential is a grant a lease was aimed at and could not honour because of something done elsewhere (Task 20400): the secret behind it was destroyed, or the grant was withdrawn when the secret's owner was offboarded. Or, since Task 20413, a grant whose secret and grant both stand and whose material the broker could not build (Delivery): a Kubernetes monitor or git proxy the hub requires and is not running, a payload or constraint that no longer parses, an exec-plugin kubeconfig the monitor will not hold.
A lease refuses grants for other reasons too — expiry, an operator's revocation, a withholding — and every one of those lands in the audit trail. These are also handed back to the caller, because they are the refusals the people running the project cannot see the cause of from inside it: often the person who owned the credential left, or the hub is misconfigured. Either way the run starts without a credential it was granted, and the failure it meets — git refusing to authenticate, a cluster answering 401 — names neither. The dispatcher puts the refusal in front of them.
Metadata only, like every field on Event.
type RefusedRefresh ¶ added in v0.0.5
RefusedRefresh is one grant a refresh ended.
type RenewSecretRequest ¶ added in v0.0.5
type RenewSecretRequest struct {
// Ref names the secret: an id or a name.
Ref string
// Kind, when set, is the kind the secret must be.
Kind Kind
// Actor is who renews: the hub, or the owner pushing a new credential.
Actor string
// Renew computes the new payload from the stored secret and its
// plaintext. The plaintext is wiped when it returns, so it must not keep
// a reference to it.
Renew func(ctx context.Context, s Secret, plaintext []byte) (RenewedPayload, error)
// AuditFailure decides whether a failed renewal is written to the audit
// trail; nil writes every one. A caller retrying on a timer uses it to
// write each distinct cause once rather than once a minute.
AuditFailure func(error) bool
}
RenewSecretRequest asks the broker to renew one secret's payload in place.
type RenewedPayload ¶ added in v0.0.5
type RenewedPayload struct {
// Payload is the new plaintext. Wiped once sealed.
Payload []byte
// Metadata is the secret's complete new metadata; nil keeps the old.
Metadata map[string]string
// ExpiresAt is when the new credential stops working, for the audit row.
ExpiresAt time.Time
// Reason describes the renewal for the audit row. No credential.
Reason string
}
RenewedPayload is what a renewal produced.
type RepoMount ¶
type RepoMount struct {
// Name is the repository's directory name under the granted root, and
// the handle a workload refers to it by.
Name string `json:"name"`
// Source is the absolute host path. Always inside the granted root:
// selectRepos resolves symlinks before accepting one, so a symlink
// planted under the root cannot redirect the bind out of it.
Source string `json:"source"`
// Target is the absolute path the repository appears at in the sandbox.
Target string `json:"target"`
// ReadOnly mirrors the grant's Writable constraint, inverted, because
// read-only is the default and a zero value should mean the safe thing.
ReadOnly bool `json:"read_only,omitempty"`
}
RepoMount is one local git repository a grant opens to a workload.
It names paths on two different machines' terms: Source is on the host the executor runs on, Target is inside the sandbox. They are equal only for a driver that shares the host filesystem, which is why the distinction is in the type rather than left to the driver to remember.
type RequestFilter ¶ added in v0.0.2
type RequestFilter struct {
// State, when set, keeps only requests in that state.
State RequestState
// RequestedBy, when set, keeps only that identity's requests. This is what
// scopes the list for a caller who may file requests but not decide them.
RequestedBy string
// SecretRef, when set, keeps only requests for that secret.
SecretRef string
// PendingOnly keeps only requests still awaiting a decision, including
// dropping ones whose deadline has passed but which no sweep has moved yet.
PendingOnly bool
}
RequestFilter narrows ListRequests.
type RequestState ¶ added in v0.0.2
type RequestState string
RequestState is where an access request sits in its lifecycle.
const ( // RequestPending: filed, not yet decided. The only state from which any // transition is possible. RequestPending RequestState = "pending" // RequestApproved: a grant was minted. GrantID names it. RequestApproved RequestState = "approved" // RequestDenied: an approver refused, with a note. RequestDenied RequestState = "denied" // RequestWithdrawn: the requester took it back. RequestWithdrawn RequestState = "withdrawn" // RequestExpired: nobody decided it in time. // // A terminal state of its own rather than a flavour of denied, because the // two mean opposite things to the person who asked: denied is an answer, // expired is the absence of one. An operator whose requests keep expiring // has a queue problem, and collapsing the states would hide it. RequestExpired RequestState = "expired" )
func (RequestState) Terminal ¶ added in v0.0.2
func (s RequestState) Terminal() bool
Terminal reports whether no further transition is possible from s.
func (RequestState) Valid ¶ added in v0.0.2
func (s RequestState) Valid() bool
Valid reports whether s is a known state.
type RequestStore ¶ added in v0.0.2
type RequestStore interface {
// PutAccessRequest inserts or replaces a request by ID.
PutAccessRequest(r AccessRequest) error
// GetAccessRequest returns one request, or a wrapped ErrRequestNotFound.
GetAccessRequest(id string) (AccessRequest, error)
// ListAccessRequests returns every request, including decided ones — the
// broker filters, so a review UI can still see history.
ListAccessRequests() ([]AccessRequest, error)
// ExpireAccessRequests moves pending requests past their deadline to
// expired and returns the ones it changed.
ExpireAccessRequests(now time.Time) ([]AccessRequest, error)
// RecordRequestUse notes that a lease redeemed an approved request's
// grant, upserting on (request, lease).
RecordRequestUse(u RequestUse) error
// ListRequestUses returns the uses recorded against one request, or every
// use when requestID is empty.
ListRequestUses(requestID string) ([]RequestUse, error)
}
RequestStore is the persistence a broker needs to serve requests.
Optional, and detected by type assertion in the same shape as KeyStore: the core Store contract is implemented by test doubles and by anything embedding the broker, and widening it would break every one of them to add a feature most do not need. A broker over a store that does not implement it refuses request operations with ErrRequestsUnsupported rather than silently pretending to record them.
type RequestUse ¶ added in v0.0.2
type RequestUse struct {
RequestID string `json:"request_id"`
LeaseID string `json:"lease_id"`
GrantID string `json:"grant_id,omitempty"`
ExecutorID string `json:"executor_id,omitempty"`
ProjectID string `json:"project_id,omitempty"`
// TaskID is the unit of work that held the lease, or 0 when the lease was
// issued for a whole project run rather than one task.
//
// Zero at the moment the lease is issued and filled in afterwards: the hub
// leases credentials *before* it dispatches the workload that will hold
// them, so at lease time there is genuinely no task to name. See
// statedb.AttributeRequestUseTasks.
TaskID int `json:"task_id,omitempty"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
}
RequestUse is one lease that redeemed the grant an approval produced.
It answers the question an approver actually has a week later — not "does this grant exist", which they know, but "did anything use it, and for what". A grant nothing ever redeemed and a grant feeding a workload around the clock are indistinguishable from broker_grants alone.
type Requester ¶
type Requester struct {
ExecutorID string
ProjectID string
Labels map[string]string
// RunID names the execution the credentials are for (Task 20282). It is
// recorded on the audit rows this request produces and is deliberately
// never matched against: a grant is issued to a project or an executor, not
// to one run, so letting a run id participate in Subject.Matches would
// invent an authority scope that operators cannot see, list or revoke.
//
// Empty for every non-dispatch caller — a CLI lease, a UI inspection — and
// an empty run id simply leaves the field off the row.
RunID string
// GitHubProxied says the caller will hand any GitHub credential in this
// lease to cloop's git proxy itself, and never to a workload — which is
// what workspace provisioning does when the hub runs a proxy
// (pkg/executor/gitproxycreds). Like RunID it is never matched against.
//
// It matters to exactly one decision: whether a grant limited to certain
// branches may be delivered with its push intact. Without a proxy in the
// path the broker withholds that push (Task 20340), because nothing else
// can hold a push to the list; with one, the proxy does, and withholding
// would break the very pushes the grant authorises.
//
// Set it only when it is true of every material the lease will carry. A
// caller that set it and then materialised the lease into a sandbox would
// be delivering a push the grant restricted, unrestricted.
GitHubProxied bool
// KubeInProcess says no kubeconfig in this lease leaves the hub process:
// the caller drives the cluster with it itself — the kubernetes
// executor's own credential, the one it creates Pods with
// (pkg/executor/kubernetes) — or discards it, as workspace provisioning
// does (pkg/executor/gitcreds). Like RunID it is never matched against.
//
// It matters to exactly one decision: whether a kubeconfig whose grant
// claims namespaces or verbs only the Kubernetes access monitor enforces
// may be delivered with no monitor in the path (Task 20420). In a
// workload nothing else would enforce them, so it is refused; in the hub
// the credential is the hub acting as itself, which the monitor does not
// govern either way.
//
// Set it only when it is true of every material the lease will carry. A
// caller that set it and then materialised the lease into a sandbox would
// be delivering exactly the kubeconfig the refusal exists to stop.
KubeInProcess bool
// Withhold names grants this lease must not deliver even though their
// subject matches, each with the reason its denial row records (Task
// 20379). The hub fills it for a sandboxed run whose Claude credential
// follows the person who started it: another user's personal Claude token,
// granted to a project they share, is not spent on this run.
//
// It only ever narrows. Like RunID it plays no part in Subject.Matches, so
// no grant can be reached through it, and Renew re-evaluates under the same
// map, so a withheld grant stays withheld for the life of the lease.
Withhold map[string]string
}
Requester is the identity a lease is issued to: one executor running work for one project. Labels are the executor's own labels, used by SubjectLabel grants.
type RetirePreviousOptions ¶ added in v0.0.6
type RetirePreviousOptions struct {
// Actor is recorded on a minted key and the rotation record.
Actor string
// DryRun counts what would move and names what would be retired, and
// writes nothing.
DryRun bool
}
RetirePreviousOptions configures RetirePrevious.
type RetirePreviousReport ¶ added in v0.0.6
type RetirePreviousReport struct {
// Minted is the key of the current root the run minted and promoted; ""
// when the primary already derived from it.
Minted string `json:"minted,omitempty"`
// Rotation is the rewrap of every sealed row onto the primary.
Rotation RotationReport `json:"rotation"`
// Keys are the keys of the previous root: retired, or on a dry run, the
// ones that would be. Remaining are those that could not be retired.
Keys []string `json:"keys"`
Remaining []string `json:"remaining,omitempty"`
Errors []string `json:"errors,omitempty"`
// Complete: every row is under a key of the current root and every key of
// the previous root is retired. The previous root opens nothing any more
// and can be removed from every member.
Complete bool `json:"complete"`
DryRun bool `json:"dry_run,omitempty"`
}
RetirePreviousReport is the outcome of RetirePrevious.
type RevocationCause ¶ added in v0.0.5
type RevocationCause string
RevocationCause says why a grant was revoked, when the revocation said (Task 20400). It is a fixed word rather than the revoker's text, which goes to the audit trail: this is what decides whether a lease that meets the grant hands its refusal back for the project to be told, and what it says.
const ( // RevokedOwnerOffboarded: the grant spent a personal secret whose owner // was offboarded. The secret is then destroyed, or kept under a legal // hold; either way the grant is gone and the project should hear why. RevokedOwnerOffboarded RevocationCause = "owner_offboarded" // RevokedSecretDeleted: the secret the grant spent was deleted. RevokedSecretDeleted RevocationCause = "secret_deleted" // RevokedSuperseded: another grant replaced this one — an edited // repository assignment, a credential granted again for longer (Task // 20403). No new lease carries it; a running lease that does keeps it, // standing on the successor the grant names (Grant.SupersededBy), because // the successor still authorises the work the run is doing. RevokedSuperseded RevocationCause = "superseded" )
type RevokeGrantRequest ¶ added in v0.0.5
type RevokeGrantRequest struct {
GrantID string
Actor string
// Cause is recorded on the grant; see RevocationCause.
Cause RevocationCause
// Reason goes on the secret.revoke row.
Reason string
// SupersededBy names the replacing grant, with Cause RevokedSuperseded.
SupersededBy string
}
RevokeGrantRequest describes a revocation.
type RevokedLeaseError ¶ added in v0.0.5
type RevokedLeaseError struct {
Revocation LeaseRevocation
// contains filtered or unexported fields
}
RevokedLeaseError is the refusal of a revoked lease, carrying the revocation so a caller enforcing it can act as the operator asked.
func (*RevokedLeaseError) Error ¶ added in v0.0.5
func (e *RevokedLeaseError) Error() string
func (*RevokedLeaseError) Unwrap ¶ added in v0.0.5
func (e *RevokedLeaseError) Unwrap() error
Unwrap lets errors.Is(err, ErrLeaseRevoked) match.
type RotateOptions ¶
type RotateOptions struct {
// NewKey mints a fresh KEK and promotes it before rewrapping. This is
// the normal case: rotating onto the *current* primary is an upgrade of
// stragglers, not a rotation.
NewKey bool
// Actor is recorded on the new key and the rotation record.
Actor string
// DryRun reports what would be rewrapped without writing anything.
DryRun bool
}
RotateOptions configures one run.
type RotationRecord ¶
type RotationRecord struct {
ID string `json:"id"`
ToKeyID string `json:"to_key_id"`
State string `json:"state"`
StartedAt time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_at"`
FinishedAt time.Time `json:"finished_at,omitempty"`
StartedBy string `json:"started_by,omitempty"`
Total int `json:"total"`
Rewrapped int `json:"rewrapped"`
Skipped int `json:"skipped"`
Failed int `json:"failed"`
LastError string `json:"last_error,omitempty"`
}
RotationRecord is the operator-facing history of a rotation run.
type RotationReport ¶
type RotationReport struct {
RotationID string `json:"rotation_id,omitempty"`
TargetKeyID string `json:"target_key_id"`
Sets []SetReport `json:"sets"`
Rewrapped int `json:"rewrapped"`
Skipped int `json:"skipped"`
Failed int `json:"failed"`
Complete bool `json:"complete"`
DryRun bool `json:"dry_run,omitempty"`
}
RotationReport is the outcome of one Rotate call.
type RotationStatus ¶
type RotationStatus struct {
PrimaryKeyID string `json:"primary_key_id"`
Keys []KeyInfo `json:"keys"`
Usage []KeyUsage `json:"usage"`
Legacy int `json:"legacy_rows"`
LegacyOpen bool `json:"legacy_openable"`
Unrotated int `json:"unrotated_rows"`
Rotations []RotationRecord `json:"rotations,omitempty"`
// ChangingRoot reports a previous root configured on this process
// (Task 20425); PreviousRootRows counts the rows still sealed under keys
// it derives, which `cloop hub key rotate --retire-previous` moves.
ChangingRoot bool `json:"changing_root,omitempty"`
PreviousRootRows int `json:"previous_root_rows,omitempty"`
}
RotationStatus is the whole picture: which keys exist, what each is sealing, and how the last rotation went.
type RotationStore ¶
type RotationStore interface {
PutRotation(r RotationRecord) error
ListRotations(limit int) ([]RotationRecord, error)
}
RotationStore persists rotation history. Optional: a rotator without one still rotates, it just cannot show progress after the process exits.
type Rotator ¶
type Rotator struct {
// contains filtered or unexported fields
}
Rotator rewraps every sealed row onto the keyring's primary KEK.
func NewRotator ¶
NewRotator builds a rotator over a keyring and one or more sealed sets.
func (*Rotator) RetireKey ¶
RetireKey retires a KEK after confirming nothing references it.
The check here is advisory and the store's is authoritative — a row could be written between the two — but running it first turns the common case into a message naming the sets and counts that are blocking, instead of a bare constraint violation from SQLite.
func (*Rotator) RetirePrevious ¶ added in v0.0.6
func (r *Rotator) RetirePrevious(ctx context.Context, opts RetirePreviousOptions) (RetirePreviousReport, error)
RetirePrevious finishes a change of root: it makes the primary a key of the current root — minting one if the primary still derives from the previous root — rewraps every sealed row onto it with the compare-and-swap rotation Rotate always uses, and retires every key of the previous root.
It is resumable exactly as Rotate is. A run that is interrupted, or finds a row it cannot rewrap, retires nothing and returns the rotation's error: re-running continues, and mints nothing the second time, because the primary then derives from the current root already.
What has to be true before it runs is the caller's to check, because it is about the deployment rather than the registry: every live hub member holds the new root as its current one — a member that does not would be left unable to read anything once the old keys are gone — and an escrow of the new root is on record. `cloop hub key rotate --retire-previous` checks both.
func (*Rotator) Rotate ¶
func (r *Rotator) Rotate(ctx context.Context, opts RotateOptions) (RotationReport, error)
Rotate rewraps every row onto the primary KEK.
Safe to call repeatedly, concurrently with hub traffic, and after an interruption — see the file comment. The returned report is complete only when every set drained; otherwise ErrRotationFailed is returned alongside a report that says precisely how far it got, and re-running continues.
func (*Rotator) Status ¶
func (r *Rotator) Status(historyLimit int) (RotationStatus, error)
Status assembles the report behind `cloop hub key status`.
func (*Rotator) WithHistory ¶
func (r *Rotator) WithHistory(h RotationStore) *Rotator
WithHistory attaches a store for rotation records.
func (*Rotator) WithProgress ¶
func (r *Rotator) WithProgress(fn func(RotationRecord)) *Rotator
WithProgress installs a callback invoked after each batch, for a CLI progress line.
type SealedRow ¶
SealedRow is one piece of sealed material as seen by the rotator.
It carries no plaintext and the rotator never produces any: rewrapping operates on the DEK, not the payload. AAD is the row's associated data — its ID — which must round-trip exactly or the rewrap fails authentication.
type SealedSet ¶
type SealedSet interface {
// SealedSetName identifies the set in reports ("secrets", "sessions").
SealedSetName() string
// CountSealedByKey returns row counts keyed by key ID, including
// LegacyKeyID for pre-envelope rows.
CountSealedByKey() (map[string]int, error)
// ListSealedNotUnder returns up to limit rows whose key ID differs from
// keyID. Order is unspecified; the rotator does not depend on it.
ListSealedNotUnder(keyID string, limit int) ([]SealedRow, error)
// ReplaceSealed swaps a row's envelope if and only if the stored one
// still matches expect exactly. It reports whether the swap happened;
// false is a normal outcome (a concurrent writer got there first), not
// an error.
ReplaceSealed(id string, expect, next Envelope) (bool, error)
}
SealedSet is one population of rows that participates in rotation.
Implemented by pkg/secretstore (brokered secrets) and pkg/sessionstore (session refresh tokens). Anything else that seals long-lived material under the hub key implements this and is rotated for free — which is the point of the interface: the alternative is a rotation command that knows the names of the tables, and therefore silently misses the next one.
type Secret ¶
type Secret struct {
ID string `json:"id"`
Kind Kind `json:"kind"`
// Name is a human-facing unique handle ("prod-deploy-pat"). Grants are
// created against it from the CLI.
Name string `json:"name"`
// Sealed is the AES-256-GCM ciphertext of the payload, encrypted under
// this secret's own data key (see WrappedDEK). Never logged.
Sealed []byte `json:"-"`
// KeyID names the KEK that WrappedDEK is sealed under. LegacyKeyID means
// the pre-envelope construction: WrappedDEK is empty and Sealed is
// encrypted directly under the passphrase-derived key (Task 20181).
//
// It is exported because storage must persist it, and json:"-" because a
// key ID in an API response tells a caller nothing they can act on while
// telling an attacker which rows a stolen key still opens.
KeyID string `json:"-"`
// WrappedDEK is this secret's data key, sealed under KeyID. Rotation
// rewrites this field and leaves Sealed untouched.
WrappedDEK []byte `json:"-"`
// Metadata is non-sensitive descriptive data (rotation date, ticket).
// It is included in audit payloads, so callers must not put credential
// material here.
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
// Owner is the identity this secret belongs to personally, in the
// OwnerKey namespace the rest of the hub uses for people (a lowercased
// email, or "sub:<issuer subject>"). Empty means a shared secret: the
// organisation's credential, provisioned by a maintainer, which is what
// every secret was before Task 20275.
//
// It is deliberately not a Metadata entry. Metadata is operator free text
// that lands in audit payloads and that nothing enforces; Owner is an
// authorisation input read by VisibleTo and by every mutation path in the
// broker. A rule enforced against a map any caller may overwrite is not a
// rule, and the two have opposite handling: metadata is for humans to
// read, Owner is for this package to obey.
Owner string `json:"owner,omitempty"`
}
Secret is a stored credential. The payload is sealed (AES-256-GCM) at all times outside the broker: there is deliberately no plaintext field on this struct, so a Secret that escapes into a log, an API response, or the storage layer cannot carry the credential with it.
func (Secret) DeletableBy ¶ added in v0.0.2
DeletableBy reports whether v may destroy this secret.
Wider than SpendableBy by exactly one case: an admin may delete a personal secret they do not own, because otherwise a hub could never complete an offboarding — an account that no longer exists cannot come back to press the button. The deletion is audited with the actor's name attached.
func (Secret) Envelope ¶
Envelope returns the secret's sealed material in the form the keyring understands.
func (Secret) OwnedBy ¶ added in v0.0.2
OwnedBy reports whether identity owns this secret.
Both sides go through NormalizeOwner and are then compared exactly, so that "is this yours" has one definition rather than two that can drift. The normaliser is where the asymmetry lives, and it is deliberate on both sides:
email folded to lowercase. IdPs are inconsistent about the case they
release an address in, and "Alice@corp" locked out of the secret
"alice@corp" minted is a lockout, not a security win.
sub: left exactly as issued. An issuer subject is an opaque identifier,
case-sensitive by specification, so folding it could silently merge
two distinct people into one owner.
func (Secret) Personal ¶ added in v0.0.2
Personal reports whether the secret belongs to one person rather than to the organisation.
func (Secret) SpendableBy ¶ added in v0.0.2
SpendableBy reports whether v may grant this secret to an executor — the act that puts the credential to work.
Strictly narrower than VisibleTo, and the one place where Admin buys nothing. An admin may see a colleague's personal secret and may delete it; handing it to a workload is neither of those, and allowing it would make ownership advisory.
func (Secret) VisibleTo ¶ added in v0.0.2
VisibleTo reports whether v may see that this secret exists.
func (Secret) WithEnvelope ¶
WithEnvelope returns a copy of the secret carrying env's sealed material.
type SetReport ¶
type SetReport struct {
Name string `json:"name"`
Rewrapped int `json:"rewrapped"`
Skipped int `json:"skipped"`
Failed int `json:"failed"`
Errors []string `json:"errors,omitempty"`
}
SetReport is per-set rotation detail.
type Store ¶
type Store interface {
// PutSecret inserts or replaces a secret by ID.
PutSecret(s Secret) error
// GetSecret returns the secret with the given ID, or a wrapped
// ErrSecretNotFound.
GetSecret(id string) (Secret, error)
// ListSecrets returns every stored secret.
ListSecrets() ([]Secret, error)
// DeleteSecret removes a secret. Removing a secret that does not exist
// returns a wrapped ErrSecretNotFound.
DeleteSecret(id string) error
// PutGrant inserts or replaces a grant by ID.
PutGrant(g Grant) error
// GetGrant returns the grant with the given ID, or a wrapped
// ErrGrantNotFound.
GetGrant(id string) (Grant, error)
// ListGrants returns every stored grant, including expired and revoked
// ones — the broker filters, so that callers listing for an audit UI
// can still see history.
ListGrants() ([]Grant, error)
// RevokeGrant stamps a grant revoked at the given time. Revoking an
// already-revoked grant is a no-op, not an error: revocation is
// idempotent by design, because a caller racing to revoke twice must
// not be told the second attempt failed.
RevokeGrant(id string, at time.Time) error
// Meta reads a broker-scoped metadata value.
Meta(key string) (value string, ok bool, err error)
// SetMeta writes a broker-scoped metadata value.
SetMeta(key, value string) error
}
Store is the persistence contract the broker needs.
It is an interface, and it lives here rather than in pkg/statedb, for the same reason pkg/executor/remote defines its own Store: the broker's policy logic is the part worth testing exhaustively, and it should not need a SQLite file to test. pkg/secretstore is the production implementation.
The payload crosses this boundary sealed and only sealed. Store implementations therefore cannot leak plaintext credentials even if they log every argument they receive — which is the point, since a storage layer is exactly the kind of code that grows debug logging.
type Subject ¶
type Subject struct {
Type SubjectType `json:"type"`
// Value is the project path or executor ID for those types, "*" for a
// wildcard within the type, and unused for label/any.
Value string `json:"value,omitempty"`
// Labels is the selector for SubjectLabel: every pair must be present
// on the requester for the grant to match.
Labels map[string]string `json:"labels,omitempty"`
}
Subject is the "who" of a grant.
func ParseSubject ¶
ParseSubject parses the CLI's "--to" syntax:
project:/srv/app one project, by path project:* every project executor:edge-01 one executor label:region=eu,gpu=true every executor carrying both labels any / * every requester (used by the legacy import)
An unrecognised or empty spec is an error rather than a permissive default: a typo in a grant target must not silently widen it.
func (Subject) Matches ¶
Matches reports whether this subject selects r.
A wildcard "*" is honoured within project/executor types, but only when written explicitly — an empty Value never matches, so a half-constructed Subject fails closed instead of matching everything.
func (Subject) Wildcard ¶ added in v0.0.2
Wildcard reports whether this subject selects an open-ended set of requesters rather than one named project or executor.
"any" and an explicit "*" both qualify. A label selector does not: it names a property of a machine, and an operator who labels two executors has said which two — whereas "*" says "and every one added later", which is a different promise and the one a personal credential cannot make.
type SubjectType ¶
type SubjectType string
SubjectType is the dimension a Grant's subject selects on.
const ( // SubjectProject scopes a grant to one project (or "*" for all). SubjectProject SubjectType = "project" // SubjectExecutor scopes a grant to one executor ID (or "*"). SubjectExecutor SubjectType = "executor" // SubjectLabel scopes a grant to executors carrying every label in the // selector — the way a fleet of edge devices is addressed without // enumerating device IDs. SubjectLabel SubjectType = "label" // SubjectAny matches every requester. Reserved for the legacy import // path, where narrowing an existing flat secret store would silently // break running projects. SubjectAny SubjectType = "any" )
type SupersedingRevoker ¶ added in v0.0.5
type SupersedingRevoker interface {
RevokeGrantSuperseded(id string, at time.Time, successorID string) error
}
SupersedingRevoker is an optional Store extension that revokes a grant as superseded, naming its successor (Task 20403). A store without it records the supersession as a revocation with cause RevokedSuperseded and no successor, so a lease carrying the grant is withdrawn rather than kept on the successor's authority: less access, never more.
type Tombstone ¶ added in v0.0.5
type Tombstone struct {
SecretID string `json:"secret_id"`
Name string `json:"name"`
Kind Kind `json:"kind,omitempty"`
Owner string `json:"owner,omitempty"`
DeletedAt time.Time `json:"deleted_at"`
DeletedBy string `json:"deleted_by,omitempty"`
Cause DeletionCause `json:"cause,omitempty"`
// Reason is what the deleter wrote. It is kept for the audit trail and
// for admins; Describe leaves it out.
Reason string `json:"reason,omitempty"`
}
Tombstone is what is left of a deleted secret.
type TombstoneStore ¶ added in v0.0.5
type TombstoneStore interface {
// DeleteSecretTombstoned removes a secret and records its tombstone in
// one step, so nothing can observe the row gone and the tombstone absent.
// Removing a secret that does not exist returns a wrapped
// ErrSecretNotFound and records nothing.
DeleteSecretTombstoned(id string, t Tombstone) error
// GetTombstone returns the tombstone of a deleted secret, or a wrapped
// ErrSecretNotFound when none was recorded.
GetTombstone(secretID string) (Tombstone, error)
}
TombstoneStore is an optional Store extension that remembers what a deleted secret was.
Optional, and detected by type assertion, for the reason RequestStore is: the core Store contract is implemented by test doubles and by anything embedding the broker, and a store without tombstones keeps working exactly as before — its refusals just cannot name a deleted secret.
type UnavailableGitGuard ¶ added in v0.0.2
type UnavailableGitGuard struct {
// is not in effect, not merely say that something is missing.
Reason string
}
UnavailableGitGuard refuses every request, for a caller that must not deliver a GitHub token unguarded but has no proxy to guard it with.
It exists because "the proxy is required and is not here" arises in two unrelated places — the hub whose proxy failed to start, and a short-lived CLI process that never had one (cloop hub doctor --smoke brokers a real lease and materialises it into a real sandbox). Both must fail closed, and a second hand-rolled copy of that decision is how one of them ends up not doing so.
func (UnavailableGitGuard) GuardGitHub ¶ added in v0.0.2
func (g UnavailableGitGuard) GuardGitHub(context.Context, GitGuardRequest) (GitGuardResult, error)
GuardGitHub implements GitGuard by always failing.
type UnavailableKubeGuard ¶ added in v0.0.2
type UnavailableKubeGuard struct {
// is not in effect, not merely say that something is missing.
Reason string
}
UnavailableKubeGuard refuses every request, for a hub whose configuration asked for a monitor and has not got one.
It refuses rather than being left nil because the two are read differently: nil means "this deployment does not intercept Kubernetes traffic", which is a valid configuration, while this means "it was supposed to and is not".
func (UnavailableKubeGuard) GuardKubeconfig ¶ added in v0.0.2
func (g UnavailableKubeGuard) GuardKubeconfig(context.Context, KubeGuardRequest) (KubeGuardResult, error)
GuardKubeconfig implements KubeGuard by always failing.
type UnguardedAck ¶ added in v0.0.5
type UnguardedAck struct {
At time.Time `json:"at"`
By string `json:"by"`
Reason string `json:"reason"`
}
UnguardedAck is the acknowledgement on a kubeconfig grant that lets it be delivered without the Kubernetes access monitor — knowingly, by name and with a reason, rather than because nobody noticed the monitor was off.
func (UnguardedAck) Recorded ¶ added in v0.0.5
func (a UnguardedAck) Recorded() bool
Recorded reports whether an acknowledgement is in force.
type UnguardedAckRequest ¶ added in v0.0.5
type UnguardedAckRequest struct {
GrantID string
// Actor is who decided, recorded on the grant and the audit row.
Actor string
// Reason is why delivering the grant unenforced is acceptable. Required
// to acknowledge, so the decision carries its own justification.
Reason string
}
UnguardedAckRequest acknowledges, or withdraws the acknowledgement of, a kubeconfig grant delivered without the Kubernetes access monitor.
type UnguardedAcknowledger ¶ added in v0.0.5
type UnguardedAcknowledger interface {
// SetUnguardedAck stores ack on grant id; a zero ack clears it. A grant
// that does not exist is a wrapped ErrGrantNotFound.
SetUnguardedAck(id string, ack UnguardedAck) error
}
UnguardedAcknowledger is an optional Store extension that records, or clears, an administrator's unguarded acknowledgement on a grant (Task 20420). A store without it cannot record one, and a broker over it refuses to (ErrUnguardedAckUnsupported) rather than pretend it did.
type UpstreamGitHub ¶ added in v0.0.5
type UpstreamGitHub struct {
// Token is the credential, for the proxy's own use. Never logged, never
// delivered to a sandbox by this type.
Token string
// Host is the forge host Token is good against, canonical (Task 20423).
// A restored session presents Token there and nowhere else.
Host string
// ExpiresAt is when GitHub stops honouring an App token; zero for a PAT.
ExpiresAt time.Time
// Refresh re-mints an App token at its recorded scope; nil for a PAT.
Refresh func(ctx context.Context, held string) (AppTokenRefresh, error)
}
UpstreamGitHub is the credential a restored git proxy session presents upstream: a PAT, or an App token minted for it now.
func (UpstreamGitHub) GoString ¶ added in v0.0.5
func (u UpstreamGitHub) GoString() string
GoString mirrors String.
func (UpstreamGitHub) String ¶ added in v0.0.5
func (u UpstreamGitHub) String() string
String redacts the token.
type Viewer ¶ added in v0.0.2
type Viewer struct {
// Identity is the viewer's OwnerKey, matched against Secret.Owner. It
// authorises everything about the viewer's own personal secrets and
// nothing about anybody else's.
Identity string
// Admin marks a caller entitled to administer *other people's* personal
// secrets: to see that alice@corp keeps one here, and to destroy it when
// she leaves.
//
// It confers visibility and deletion, never use. Offboarding requires both,
// and pretending an admin cannot see the row would be theatre — the
// material is sealed under a key the hub operator already holds. Spending
// somebody else's credential is a different act, it is the one thing the
// ownership model exists to prevent, and no flag here enables it.
Admin bool
}
Viewer is the identity a read or a mutation is performed on behalf of.
It exists so the broker can answer "may this person see, spend or destroy this secret" without importing pkg/authz, which would invert the dependency: authz is a policy over HTTP routes and OIDC claims, and the broker must stay usable from the CLI and from tests with neither present.
A Viewer answers one question and deliberately not two: *ownership*. Whether the caller holds organisation-level authority over the shared, unowned secrets that were the only kind before Task 20275 is not asked here — that is authz.PermSecretGrant and authz.PermSecretRevoke, enforced by the hub's route table, which already does it correctly and is already tested. Answering it twice, in two packages, with two notions of who counts, is how the two answers come to disagree.
So: shared secrets pass through this type untouched, exactly as they did before, and every existing call site stays correct without change. What the zero value cannot do is see, spend or delete a *personal* secret — which is the whole of the new capability, and which therefore fails closed for any caller that forgets to say who they are.
func PrivilegedViewer ¶ added in v0.0.2
PrivilegedViewer is the viewer for callers that already hold full authority over the secret store: the CLI running on the hub host, key rotation, and offboarding sweeps.
Named rather than written as a literal at each site so that the places which deliberately see every user's secrets are greppable, and so that reviewing "what can see everything" is a search for one identifier.
type WithdrawnGrant ¶ added in v0.0.5
type WithdrawnGrant struct {
GrantID string `json:"grant_id"`
// Reason says why, in the denial's words.
Reason string `json:"reason"`
}
WithdrawnGrant is a grant a live lease carries that no longer authorises it.
Source Files
¶
- apprefresh.go
- appslots.go
- audit.go
- broker.go
- cipher.go
- constraints.go
- envelope.go
- envoffer.go
- errors.go
- executorbind.go
- forgehost.go
- gitguard.go
- githubapp.go
- githubapp_discover.go
- githubguarded.go
- githubmulti.go
- githubpat.go
- grantrevoke.go
- hostdevice.go
- hostinterface.go
- hubcredential.go
- keycache.go
- keycheck.go
- keyring.go
- kubeconfig.go
- kubeguard.go
- kubemulti.go
- kubeshare.go
- lease.go
- leaserecord.go
- leaserevocation.go
- legacy.go
- localrepo.go
- material.go
- model.go
- ownership.go
- renewsecret.go
- request.go
- rotate.go
- store.go
- tombstone.go
- unguarded.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package secretbrokertest provides fakes for testing code that leases GitHub App credentials through pkg/secretbroker: an in-memory store, a GitHub App API whose clock a test drives, a git forge that honours only the tokens that API minted and has not expired or revoked, and an HTTPS CONNECT proxy that lets a workload reach that forge under the name github.com.
|
Package secretbrokertest provides fakes for testing code that leases GitHub App credentials through pkg/secretbroker: an in-memory store, a GitHub App API whose clock a test drives, a git forge that honours only the tokens that API minted and has not expired or revoked, and an HTTPS CONNECT proxy that lets a workload reach that forge under the name github.com. |