Help us shape the future of Go! Take the 2026 Go Developer Survey

secretaccess

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package secretaccess derives OpenBao JWT auth mounts, roles and ACL policies from the set of secret names each Kubernetes namespace reads from the store.

It is pure: no I/O, no clock, no randomness. The same Request always yields the same Document, byte for byte, so the two programs that apply it (forge's OpenBao converge Job for a self-run store, and a hosted control plane for its own store) cannot disagree about who may read what. Applying the Document to a live OpenBao is the caller's job; Mount, Role and Policy each carry the API path and payload to write.

Model

One JWT auth mount per cluster (auth/jwt-<cluster id>), trusting that cluster's own service-account issuer. One role per (env, cluster, namespace), bound to the namespace's reliant-secrets service account and to the cluster's audience, issuing 10-minute batch tokens carrying exactly one policy. That policy lists, by exact path, the names the namespace's ExternalSecrets reference:

path "secret/data/tenants/<org>/<env>/STRIPE_SECRET_KEY" {
  capabilities = ["read"]
}

No globs, no list, no metadata: a compromised namespace can read the values it already receives and nothing else. The one escape is a WildcardRead, which needs a stated reason and is confined to a single env's directory.

Changing the name set safely

Apply Widen (the union of the old and new shapes) before the workloads that need the new names roll out, then Derive the new shape alone once they converge, and delete whatever Removed reports. Narrowing never races a rollout that still needs the old name.

Consuming it

Callers declare their own narrow interface for the thing that writes to OpenBao (typically one method, put-policy, and one, put-role). Nothing here performs a network call, so there is no client to mock.

Index

Constants

View Source
const (
	// DefaultKVMount is the KV-v2 mount the component creates.
	DefaultKVMount = "secret"
	// DefaultRoot is the first path segment under the mount for values that
	// are delivered to a cluster. Host-process local envs use "local".
	DefaultRoot = "tenants"
	// DefaultServiceAccount is the service account in each namespace whose
	// projected token logs in. Role subjects are pinned to it.
	DefaultServiceAccount = "reliant-secrets"
	// TokenTTL is the lifetime of every issued token. It is also the maximum:
	// batch tokens are not renewable, ESO logs in again.
	TokenTTL = 10 * time.Minute
	// TokenType is the OpenBao token type roles issue.
	TokenType = "batch"
)
View Source
const MountPrefix = "jwt-"

MountPrefix starts every JWT auth mount name; the cluster id follows.

Variables

This section is empty.

Functions

func MountName

func MountName(clusterID string) string

MountName is the auth mount name of a cluster: "jwt-<cluster id>".

func ValidName

func ValidName(name string) bool

ValidName reports whether name is a legal secret name: an env-var style identifier. It is the rule the stores enforce on write, restated here so a bad shape fails before anything is applied.

Types

type Binding

type Binding struct {
	Cluster   string
	Namespace string
	// Names are the secret names the namespace's ExternalSecrets reference,
	// each granted by exact path.
	Names []string
	// Wildcards are the escape from exact paths; each needs a reason.
	Wildcards []WildcardRead
}

Binding is the set of names one namespace of one cluster reads.

type Cluster

type Cluster struct {
	// ID names the cluster and its mount (auth/jwt-<ID>).
	ID string
	// Audience is the token audience the cluster's SecretStores request and
	// the roles require. It pins a registration: a token minted for another
	// audience is refused.
	Audience string
	// Trust says how OpenBao verifies the cluster's tokens. Leave it zero when
	// the applier fills the mount's config itself (for instance a converge
	// Job that fetches the cluster's JWKS); a non-zero Trust is emitted as
	// the mount's config.
	Trust Trust
}

Cluster is one registered cluster: the identity of its auth mount and the issuer that vouches for its tokens.

type Document

type Document struct {
	Mounts   []Mount
	Policies []Policy
	Roles    []Role
}

Document is the full set of objects a Request needs in OpenBao, sorted so the same Request always yields an identical Document.

func Derive

func Derive(req Request) (Document, error)

Derive maps a Request to the mounts, roles and policies it needs. It reports every problem it finds rather than the first.

func Removed

func Removed(old, next Document) Document

Removed returns what next no longer contains: the mounts, roles and policies present in old and absent from next. After narrowing, deleting them is what takes access away from a namespace or cluster that left the shape.

type Mount

type Mount struct {
	Cluster string
	Trust   Trust
}

Mount is one cluster's JWT auth method.

func (Mount) ConfigPath

func (m Mount) ConfigPath() string

ConfigPath is the API path of the mount's verification config (PUT; payload Mount.ConfigPayload).

func (Mount) ConfigPayload

func (m Mount) ConfigPayload() map[string]any

ConfigPayload is the verification config, or nil when the cluster declared no Trust and the applier supplies the mount's config itself.

func (Mount) EnablePath

func (m Mount) EnablePath() string

EnablePath is the API path that enables the mount (PUT; payload Mount.EnablePayload). OpenBao answers 400 "path is already in use" when the mount exists, so an applier that reruns lists sys/auth first and enables only the mounts that are missing; the config, role and policy writes are idempotent overwrites.

func (Mount) EnablePayload

func (m Mount) EnablePayload() map[string]any

EnablePayload is the body of the enable call.

func (Mount) Name

func (m Mount) Name() string

Name is the mount's name under auth/, as `bao auth enable -path=<Name>` takes it.

type Policy

type Policy struct {
	Name  string
	Rules []Rule
}

Policy is one ACL policy.

func (Policy) HCL

func (p Policy) HCL() string

HCL renders the policy. Rules are sorted by path, so the text is stable.

func (Policy) Path

func (p Policy) Path() string

Path is the API path of the policy (PUT; payload Policy.Payload).

func (Policy) Payload

func (p Policy) Payload() map[string]any

Payload is the policy's body.

type Request

type Request struct {
	Scope Scope
	// ServiceAccount is the account roles bind to. Default
	// [DefaultServiceAccount].
	ServiceAccount string
	Clusters       []Cluster
	Bindings       []Binding
}

Request is everything Derive needs: one env, the clusters that deliver it, and what each namespace reads.

func Widen

func Widen(old, next Request) (Request, error)

Widen returns the union of two requests for the same env: every cluster and every namespace of either, with the names of a namespace present in both merged. Deriving the union before a promotion and the new request alone after it keeps a rollout that still needs an old name from losing access.

type Role

type Role struct {
	// Mount is the auth mount name ("jwt-<cluster>").
	Mount          string
	Name           string
	BoundAudiences []string
	// BoundSubject is the exact service-account subject:
	// system:serviceaccount:<namespace>:<service account>.
	BoundSubject string
	Policies     []string
}

Role is one namespace's login role on a cluster's mount. It carries the same name as its policy.

func (Role) Path

func (r Role) Path() string

Path is the API path of the role (PUT; payload Role.Payload).

func (Role) Payload

func (r Role) Payload() map[string]any

Payload is the role's body: a JWT role bound to one audience and one subject that issues short-lived batch tokens (verified against OpenBao 2.6.2). The token also carries OpenBao's built-in default policy, which only lets it look itself up; ESO's Vault provider relies on that, so it is kept.

type Rule

type Rule struct {
	Path         string
	Capabilities []string
	// WildcardReadReason is set on a rule whose Path ends in "*". It is
	// written into the policy as a comment.
	WildcardReadReason string
}

Rule grants capabilities on one path.

type Scope

type Scope struct {
	// Mount is the KV-v2 mount. Default [DefaultKVMount].
	Mount string
	// Root is "tenants" (delivered to a cluster, the default) or "local".
	Root string
	Org  string
	Env  string
}

Scope addresses one env's directory in the KV mount: <Mount>/data/<Root>/<Org>/<Env>/<NAME>. For a self-run store Org is the project and Env the env name.

func (Scope) PolicyName

func (s Scope) PolicyName(clusterID, namespace string) string

PolicyName is the name of the policy, and of the role that carries it, for one namespace of one cluster:

env-<env>-<cluster>-<hash>

env and cluster are short readable labels (see label); hash is eight hex digits of a digest of the whole (org, env, cluster, namespace) tuple, so two bindings that share both labels still get distinct names. Policies are global in OpenBao, which is why the env is part of the name.

func (Scope) SecretPath

func (s Scope) SecretPath(name string) string

SecretPath is the KV-v2 API path of one secret's current version, the path a policy grants and a reader requests. name is not validated.

func (Scope) Validate

func (s Scope) Validate() error

Validate reports why the scope cannot address a directory. Every segment must be a plain path segment: a slash, glob character or ".." would let one env's policy name another's values.

type Trust

type Trust struct {
	// OIDCDiscoveryURL is the issuer's discovery base (GKE publishes it).
	OIDCDiscoveryURL string
	// BoundIssuer is the required "iss" claim. Defaults to OIDCDiscoveryURL.
	BoundIssuer string
	// PublicKeys are PEM keys, for clusters whose issuer is not reachable from
	// OpenBao.
	PublicKeys []string
}

Trust is the mount's verification config. At most one of OIDCDiscoveryURL and PublicKeys may be set.

type WildcardRead

type WildcardRead struct {
	// Pattern is a name prefix ending in a single "*" ("STRIPE_*", or "*" for
	// the whole env). It cannot leave the env's directory.
	Pattern string
	// Reason is why exact names are not enough.
	Reason string
}

WildcardRead grants read on every name matching Pattern within the env's own directory. It exists for the rare consumer whose names are not known ahead of time; every use is a standing exception, so Reason is mandatory and is written into the policy as a comment.

Jump to

Keyboard shortcuts

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