Documentation
¶
Overview ¶
Package openbao renders forge.OpenBao, the secret store an env runs itself.
kcl/lib/openbao.k validates the declaration and emits ONE forge.dev/v1alpha1 record of kind OpenBao whose spec is Spec; Render expands it into the Kubernetes objects, the same way pkg/deploy expands a ManagedDatabase. Both executors (`forge env deploy` and a Flux bundle build) call it, so it is PURE and DETERMINISTIC: same spec, byte-identical objects.
What it renders ¶
- the server: a StatefulSet (Raft) or Deployment (Postgres), its Services, ServiceAccount, config, PodDisruptionBudget, NetworkPolicy and, for Postgres on Cloud SQL, the cloud-sql-proxy sidecar;
- TLS: a cert-manager self-signed CA Issuer and Certificate, unless the spec brings its own Secret;
- two content-addressed Jobs that apply the access document (AccessDoc, derived with pkg/secretaccess) by running the embedded converge.sh: a pre-rollout WIDEN Job and a post-rollout CONVERGE Job.
Seams other components fill in ¶
- SF7 delivers the Secret [BootstrapSecretName] (the static seal key, and the Postgres password) and may mount an escrow hook at EscrowHookPath;
- SF8 is Spec.Public (public.go): the Gateway-side objects, the proxy NetworkPolicy rule and the X-Forwarded-For stanza. It needs neither Spec.IngressFrom nor Spec.ExtraDNSNames: the Gateway re-encrypts to the Service's own name, so the certificate never carries the public host;
- SF5 reads the server URL, CA ConfigMap, mount and audience from the resolved `openbao` block KCL emits beside the record.
Index ¶
- Constants
- Variables
- func ConvergeServiceAccount(s Spec) string
- func EscrowConfigMap(name string) string
- func Render(s Spec) ([]*unstructured.Unstructured, error)
- type Access
- type AccessCluster
- type AccessDoc
- type Audit
- type Binding
- type Bootstrap
- type Consumer
- type Grant
- type HA
- type Hooks
- type KV
- type Listener
- type Mount
- type Peer
- type Policy
- type Postgres
- type Public
- type PublicMatch
- type Raft
- type RetiredAuthMount
- type Role
- type Rule
- type Seal
- type Snapshot
- type Spec
- func (s Spec) BootstrapStore() string
- func (s Spec) EscrowLedger() string
- func (s Spec) Host() string
- func (s Spec) PublicAllowlist() []PublicMatch
- func (s Spec) ReaderServiceAccount() string
- func (s Spec) ServerURL() string
- func (s Spec) StorageSecret() string
- func (s Spec) TLSSecret() string
- func (s Spec) UnsealSecret() string
- func (s Spec) Validate() error
- func (s Spec) WithDefaults() Spec
- type Storage
- type TLS
- type TransitMount
- type Wildcard
- type Writer
Constants ¶
const ( // PolicyAccessAdmin is what `delegated` grants: author JWT mounts, roles // and env-* policies, and nothing under the KV mount. PolicyAccessAdmin = "access-admin" // PolicyConverge is the converge Job's own: access-admin plus the KV // mount, and policies of any name (consumers'). PolicyConverge = "converge-admin" // PolicyWriter is the write-only policy of the secret writer. PolicyWriter = "forge-secret-writer" // RoleConverge is the converge Job's login role on the self mount. RoleConverge = "openbao-converge" // ConvergeAudience is the audience of the Job's projected token. ConvergeAudience = "openbao-converge" // RoleWriter is the writer's role. RoleWriter = "forge-secret-writer" // ConsumerPrefix names a consumer's role and policy: consumer-<name>. ConsumerPrefix = "consumer-" )
The fixed principals and policies every store gets.
const ( // AnnotationGCPServiceAccount binds a Kubernetes service account to a // Google service account under GKE Workload Identity. AnnotationGCPServiceAccount = "iam.gke.io/gcp-service-account" // DefaultESONamespace and ESOServiceAccount are the chart defaults the // External Secrets Operator runs as when the Bundle does not declare it. DefaultESONamespace = "external-secrets" ESOServiceAccount = "external-secrets" // SealSecretVersion is the Secret Manager version the seal key is read at. // The key is written once and never rotated in place (a store is sealed // under the key it was initialised with), so reading version 1 and never // "latest" makes an extra version, from a lost generation race or a stray // `versions add`, unable to change what the store unseals with. SealSecretVersion = "1" // BootstrapRefresh is how often ESO re-reads the bootstrap values. BootstrapRefresh = "1h" )
What the bootstrap source hands the component, and takes from it.
const ( // StaticKeyKey is the key, in the Secret [Spec.UnsealSecret], holding the // static seal key (hex or base64 of 32 bytes). StaticKeyKey = "static-key" // ConnectionURLKey is the key, in [Spec.StorageSecret], holding the // Postgres DSN. ConnectionURLKey = "connection-url" )
Names of what the component expects from, and hands to, its neighbours.
const ( PhaseWiden = "widen" PhaseConverge = "converge" )
Phases of converge.sh.
const ( APIVersion = "forge.dev/v1alpha1" Kind = "OpenBao" )
Record identity.
const ( LabelName = "app.kubernetes.io/name" LabelManagedBy = "app.kubernetes.io/managed-by" LabelPartOf = "app.kubernetes.io/part-of" LabelEnv = "forge.dev/env" LabelJobName = "forge.dev/job-name" LabelSpecHash = "forge.dev/spec-hash" // LabelOwner marks the env that owns the component. A second owner on the // same cluster is refused (by kind and label, never by name). LabelOwner = "forge.dev/component-owner" // LabelComponent is "openbao" on every object of this component. LabelComponent = "forge.dev/component" // AnnotationDeployPhase places a Job relative to the workloads // (pkg/release.AnnotationDeployPhase). AnnotationDeployPhase = "forge.dev/deploy-phase" )
Labels forge stamps on what it renders.
const ( // EscrowHookPath is where a component may mount an executable that is run // with the init output file as $1, after the in-cluster recovery Secret is // written and before anything is converged. A non-zero exit fails the run. EscrowHookPath = "/escrow/escrow.sh" // GenerateHookPath is run after the access document is applied, when // present. It is the seam for minting generated store values inside the // cluster. GenerateHookPath = "/generate/generate.sh" )
Paths inside the pods and the converge Job.
const ( NodeSpreadRequired = "required" NodeSpreadPreferred = "preferred" )
Node spread values.
const (
// ClusterPort is the Raft / HA forwarding port.
ClusterPort = 8201
)
Ports.
const DefaultCloudSQLProxyImage = "gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1"
DefaultCloudSQLProxyImage is the Cloud SQL Auth Proxy sidecar.
const DefaultKubectlImage = stsresize.DefaultImage
DefaultKubectlImage runs the converge Jobs: kubectl, curl and jq, pinned by digest (the image forge's StatefulSet resize Job already runs).
Variables ¶
var ConvergeScript string
ConvergeScript is the script the converge Jobs run. It is embedded so the Job's content address covers it: a change to the script re-runs the converge.
var EscrowScript string
EscrowScript is the escrow sink the converge Job runs when the bootstrap source is Google Secret Manager. It is embedded so the hook ConfigMap's name (and with it the Job's content address) covers it.
Functions ¶
func ConvergeServiceAccount ¶
ConvergeServiceAccount is the converge Jobs' service account.
func EscrowConfigMap ¶
EscrowConfigMap is the content-addressed ConfigMap holding the escrow hook.
func Render ¶
func Render(s Spec) ([]*unstructured.Unstructured, error)
Render expands the spec into the objects that run it. The objects carry no forge.dev/cluster or forge.dev/env stamp: the caller copies the record's.
Types ¶
type Access ¶
type Access struct {
Org string `json:"org"`
Env string `json:"env"`
Root string `json:"root,omitempty"`
ServiceAccount string `json:"serviceAccount,omitempty"`
SelfCluster string `json:"selfCluster"`
Clusters []AccessCluster `json:"clusters"`
Bindings []Binding `json:"bindings,omitempty"`
}
Access is everything pkg/secretaccess needs, plus the self-cluster identity.
type AccessCluster ¶
type AccessCluster struct {
ID string `json:"id"`
Audience string `json:"audience"`
// Exactly one of OIDCDiscoveryURL / PublicKeys, or neither for the self
// cluster, whose keys the converge Job reads from its own API server.
OIDCDiscoveryURL string `json:"oidcDiscoveryURL,omitempty"`
BoundIssuer string `json:"boundIssuer,omitempty"`
PublicKeys []string `json:"publicKeys,omitempty"`
}
AccessCluster is a cluster whose service-account tokens may log in.
type AccessDoc ¶
type AccessDoc struct {
// Self is the id of the cluster OpenBao runs on; its mount (jwt-<Self>)
// holds the converge, consumer and writer roles and is never removed.
Self string `json:"self"`
KV KV `json:"kv"`
// Transit are the transit mounts to enable; never disabled.
Transit []TransitMount `json:"transit,omitempty"`
Mounts []Mount `json:"mounts"`
Policies []Policy `json:"policies"`
Roles []Role `json:"roles"`
// Retired are auth mounts the store must not carry; converge disables
// each that is present (retired.go). Widen never applies them.
Retired []RetiredAuthMount `json:"retired,omitempty"`
}
AccessDoc is everything the converge Job applies to a running OpenBao: the KV mount, the JWT auth mounts, the ACL policies and the login roles. It is derived from the Spec alone (the consumers, the writer, and the bindings pkg/secretaccess turns into exact-path policies), serialised into a ConfigMap, and executed by converge.sh. Its JSON is canonical: the same spec always yields the same bytes, which is what makes the Job content-addressed.
func BuildAccess ¶
BuildAccess derives the access document from a spec.
func Removed ¶
Removed is 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. The self mount is never removed.
func Widen ¶
Widen is the union of the document applied before a rollout and the new one: every mount, policy and role of either, with a policy present in both holding the union of its rules. Applying it before the workloads that need a new name roll out, and the new document alone after they converge, narrows without racing a rollout that still needs an old name. converge.sh does this with jq against the document it stored; this is the reference it is tested against.
type Audit ¶
type Audit struct {
Stdout bool `json:"stdout"`
File bool `json:"file"`
FilePath string `json:"filePath"`
}
Audit is the audit devices.
type Binding ¶
type Binding struct {
Cluster string `json:"cluster"`
Namespace string `json:"namespace"`
Names []string `json:"names"`
Wildcards []Wildcard `json:"wildcards,omitempty"`
}
Binding is the set of names one namespace of one cluster reads.
type Bootstrap ¶
type Bootstrap struct {
// Project is the Google project holding the secrets.
Project string `json:"project"`
// ReaderServiceAccount is the Kubernetes service account, in the store's
// namespace, that the ESO `gcpsm` SecretStore logs in as; ReaderGSA is the
// Google service account it is federated to (secretAccessor on the seal
// key and the Postgres password only).
ReaderServiceAccount string `json:"readerServiceAccount"`
ReaderGSA string `json:"readerGSA"`
// EscrowGSA is the Google service account the converge Job's service
// account is federated to: versions.add on the recovery escrow only.
EscrowGSA string `json:"escrowGSA"`
// ClusterProject, ClusterLocation and ClusterName say which GKE cluster the
// store runs on, for Workload Identity. All empty = ESO asks the GKE
// metadata server, which a NetworkPolicy may block; the project can differ
// from Project, where the secrets are.
ClusterProject string `json:"clusterProject,omitempty"`
ClusterLocation string `json:"clusterLocation,omitempty"`
ClusterName string `json:"clusterName,omitempty"`
// ESONamespace is where the External Secrets Operator runs; it is granted
// a token for ReaderServiceAccount and nothing else.
ESONamespace string `json:"esoNamespace"`
// SealSecretID, PasswordSecretID and EscrowSecretID are the secret ids of
// the seal key, the Postgres password (Postgres storage only) and the
// recovery escrow.
SealSecretID string `json:"sealSecretID"`
PasswordSecretID string `json:"passwordSecretID,omitempty"`
EscrowSecretID string `json:"escrowSecretID"`
}
Bootstrap is the bootstrap source when it is Google Secret Manager. See bootstrap.go for what it renders.
type Consumer ¶
type Consumer struct {
Name string `json:"name"`
Workload string `json:"workload"`
Namespace string `json:"namespace"`
Grants []Grant `json:"grants,omitempty"`
Delegated bool `json:"delegated,omitempty"`
}
Consumer is a static principal with exact grants.
type Grant ¶
type Grant struct {
Path string `json:"path"`
Capabilities []string `json:"capabilities"`
WildcardReadReason string `json:"wildcardReadReason,omitempty"`
}
Grant is one ACL rule.
type HA ¶
type HA struct {
PDB bool `json:"pdb"`
ZoneSpread bool `json:"zoneSpread"`
// NodeSpread is "required" (the default; empty means required) or
// "preferred". Required refuses to co-locate two replicas of an HA store
// on one node (a node loss would take quorum or every standby with it);
// preferred is the scheduler's best effort and states NodeSpreadReason.
NodeSpread string `json:"nodeSpread,omitempty"`
NodeSpreadReason string `json:"nodeSpreadReason,omitempty"`
PriorityClass string `json:"priorityClass,omitempty"`
}
HA is availability.
type Hooks ¶
type Hooks struct {
Escrow string `json:"escrow,omitempty"`
Generate string `json:"generate,omitempty"`
}
Hooks names the ConfigMaps holding the converge Job's optional hooks. ConfigMap keys are the files: escrow.sh and generate.sh.
type Listener ¶
type Listener struct {
Port int `json:"port"`
TLS bool `json:"tls"`
TransitionalReason string `json:"transitionalReason,omitempty"`
}
Listener is one API listener.
type Mount ¶
type Mount struct {
Name string `json:"name"`
Cluster string `json:"cluster"`
// Trust says how OpenBao verifies the cluster's tokens: "self" (the Job
// reads the cluster's own JWKS), or "config" (Config is written as is).
Trust string `json:"trust"`
Config map[string]any `json:"config,omitempty"`
}
Mount is one cluster's JWT auth method.
type Peer ¶
type Peer struct {
NamespaceLabels map[string]string `json:"namespaceLabels,omitempty"`
PodLabels map[string]string `json:"podLabels,omitempty"`
}
Peer is one NetworkPolicy ingress peer, as the NetworkPolicy API spells it.
type Postgres ¶
type Postgres struct {
CloudSQLInstance string `json:"cloudSQLInstance,omitempty"`
Host string `json:"host,omitempty"`
Port int `json:"port"`
Database string `json:"database"`
Role string `json:"role"`
GCPServiceAccount string `json:"gcpServiceAccount,omitempty"`
// PrivateIP makes the proxy dial the instance's private address (an
// instance with no public IP).
PrivateIP bool `json:"privateIP,omitempty"`
MaxParallel int `json:"maxParallel"`
}
Postgres is PostgreSQL storage, on Cloud SQL (via the proxy) or on a Host.
type Public ¶
type Public struct {
// Host is the DNS name clients dial.
Host string `json:"host"`
// Gateway, Listener and GatewayNamespace say where the route attaches. The
// listener is the HTTPS one forge adds to the Gateway for Host.
Gateway string `json:"gateway"`
GatewayNamespace string `json:"gatewayNamespace"`
Listener string `json:"listener"`
// RequestsPerSecond is the ceiling on all sources together,
// PerSourceRequestsPerSecond on one source address, MaxConnections on the
// listener. All are per Envoy replica.
RequestsPerSecond int `json:"requestsPerSecond"`
PerSourceRequestsPerSecond int `json:"perSourceRequestsPerSecond"`
MaxConnections int `json:"maxConnections"`
// RequestTimeout is a Gateway API duration (30s, 1m30s).
RequestTimeout string `json:"requestTimeout"`
// Port is the TLS port OpenBao opens for the Gateway, beside its
// Listeners. Only the Gateway's proxy pods can reach it.
Port int `json:"port"`
// ProxyNamespace is where Envoy Gateway runs the proxy pods; the
// NetworkPolicy admits the pods of Gateway there and nothing else.
ProxyNamespace string `json:"proxyNamespace"`
// TrustedProxyCIDRs are the peer ranges OpenBao trusts to set
// X-Forwarded-For on Port. A peer outside them is refused (HTTP 400), as is
// a request with no header: nothing but Envoy should be talking to Port.
TrustedProxyCIDRs []string `json:"trustedProxyCIDRs"`
}
Public is Spec.Public. The KCL lowering fills it from forge.OpenBaoPublic and from the Gateway that field names.
type PublicMatch ¶
type PublicMatch struct {
Method string
// Type is a Gateway API path match type: Exact or RegularExpression. The
// allowlist never uses PathPrefix.
Type string
Value string
}
PublicMatch is one entry of the route's allowlist, as an HTTPRoute match.
type Raft ¶
type Raft struct {
SizeGiB int `json:"sizeGiB"`
StorageClass string `json:"storageClass,omitempty"`
Snapshot *Snapshot `json:"snapshot,omitempty"`
}
Raft is integrated storage on PersistentVolumeClaims.
type RetiredAuthMount ¶
type RetiredAuthMount struct {
Name string `json:"name"`
Reason string `json:"reason"`
Force bool `json:"force,omitempty"`
}
RetiredAuthMount is an auth mount the store must no longer carry: one left by an install that predates this component (a bespoke `kubernetes` or `approle` mount). The component converges only the `jwt-*` mounts it writes, so nothing else would ever remove it; declaring it here does.
The converge Job disables the mount if it is present (a mount that is already gone is a no-op), after checking it holds no roles. Disabling a mount revokes every token it issued and deletes its roles, so a mount that still has roles is refused unless Force says the roles are known to be dead. Reason is required and records why that is safe (the evidence the mount is unused); it is written to the Job's log with the disable.
type Role ¶
type Role struct {
Mount string `json:"mount"`
Name string `json:"name"`
Payload map[string]any `json:"payload"`
}
Role is one login role; Payload is the body written to auth/<Mount>/role/<Name>.
type Rule ¶
type Rule struct {
Path string `json:"path"`
Capabilities []string `json:"capabilities"`
Reason string `json:"reason,omitempty"`
}
Rule grants capabilities on one path.
type Snapshot ¶
type Snapshot struct {
Bucket string `json:"bucket"`
Schedule string `json:"schedule"`
Keep int `json:"keep"`
}
Snapshot is a Raft snapshot CronJob to a bucket.
type Spec ¶
type Spec struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Image string `json:"image"`
// ReleaseOwner is the native provider's explicit compiled snapshot consumer.
ReleaseOwner string `json:"releaseOwner,omitempty"`
// KubectlImage runs the converge Jobs: kubectl, curl and jq. Empty =
// [DefaultKubectlImage].
KubectlImage string `json:"kubectlImage,omitempty"`
// CloudSQLProxyImage is the sidecar for Postgres storage on Cloud SQL.
// Empty = [DefaultCloudSQLProxyImage].
CloudSQLProxyImage string `json:"cloudSQLProxyImage,omitempty"`
Replicas int `json:"replicas"`
// Owner is "<project>.<env>", stamped as [LabelOwner].
Owner string `json:"owner"`
Storage Storage `json:"storage"`
Listeners []Listener `json:"listeners"`
TLS TLS `json:"tls"`
Seal Seal `json:"seal"`
// RecoverySecret is the in-cluster Secret the converge Job writes the
// `operator init` output to, before it converges anything.
RecoverySecret string `json:"recoverySecret"`
KV KV `json:"kv"`
// Transit are the transit secret-engine mounts the converge Job enables
// (keys are created by the consumers whose grants allow it). Empty = none.
Transit []TransitMount `json:"transit,omitempty"`
HA HA `json:"ha"`
Audit Audit `json:"audit"`
Consumers []Consumer `json:"consumers,omitempty"`
Writer Writer `json:"writer"`
Access Access `json:"access"`
// RetiredAuthMounts are auth mounts a previous install left behind and the
// converge Job disables (retired.go). Empty = none.
RetiredAuthMounts []RetiredAuthMount `json:"retiredAuthMounts,omitempty"`
// Hooks are ConfigMaps of scripts the converge Job runs at two points:
// the escrow hook right after init, the generate hook after the document
// is applied. Empty = none. The names should be content-addressed so a
// changed hook changes the Job.
Hooks Hooks `json:"hooks,omitempty"`
// Bootstrap delivers the values the store needs before it can serve a read
// (the seal key, the Postgres password) from Google Secret Manager through
// ESO, and escrows the `operator init` output back to it. Nil = the Secrets
// are provided by something else (a prerequisite, or the env's file store).
Bootstrap *Bootstrap `json:"bootstrap,omitempty"`
// IngressNamespaces are namespaces whose pods may reach the API port:
// ESO's, and every namespace that holds a SecretStore for this store.
IngressNamespaces []string `json:"ingressNamespaces,omitempty"`
// CANamespaces are the namespaces (beside OpenBao's own) that get the CA
// published as a ConfigMap, so a SecretStore there can name it. They must
// exist when the converge Job runs.
CANamespaces []string `json:"caNamespaces,omitempty"`
// IngressFrom are extra NetworkPolicy peers (the Gateway, for `public`).
IngressFrom []Peer `json:"ingressFrom,omitempty"`
// ExtraDNSNames are added to the server certificate (the public host).
ExtraDNSNames []string `json:"extraDNSNames,omitempty"`
// Public publishes the store on a Gateway (public.go). Nil = not published.
Public *Public `json:"public,omitempty"`
}
Spec is the `spec` of the OpenBao record kcl/lib/openbao.k emits.
func (Spec) BootstrapStore ¶
BootstrapStore is the name of the namespaced SecretStore over Secret Manager.
func (Spec) EscrowLedger ¶
EscrowLedger is the ConfigMap that records which recovery Secret has been escrowed. The escrow identity can add versions but not read them, so "is the escrow empty" is answered here, in the cluster.
func (Spec) PublicAllowlist ¶
func (s Spec) PublicAllowlist() []PublicMatch
PublicAllowlist is the whole of what the public host routes. Anything else, whatever the method, matches no route and Envoy answers 404.
- Login is PUT and POST: ESO's Vault client writes with PUT (the design said POST; OpenBao treats them alike).
- GET auth/token/lookup-self is what ESO calls to validate a SecretStore and to decide whether to log in again. Without it every SecretStore on a remote cluster stays NotReady. It reveals nothing but the caller's own token, which the caller holds.
- The read is one KV value of one env. The patterns are the ones pkg/secretaccess enforces on write (a test pins them to it), so the edge never routes a path no policy could grant.
Query strings are not part of the match, so `?version=N` (a pinned read) passes.
func (Spec) ReaderServiceAccount ¶
ReaderServiceAccount is the service account, in the store's namespace, that the ESO `gcpsm` SecretStore logs in as. It is bound by Workload Identity to a Google service account that can read the seal key and the Postgres password and nothing else.
func (Spec) StorageSecret ¶
StorageSecret is the in-cluster Secret the Postgres DSN is read from.
func (Spec) UnsealSecret ¶
UnsealSecret is the in-cluster Secret the static seal key is read from.
func (Spec) Validate ¶
Validate refuses a spec the renderer could not render safely. Every message names the escape. It re-states the rules KCL enforces, because a spec also reaches this package from callers that never ran KCL.
func (Spec) WithDefaults ¶
WithDefaults fills the images a record may leave empty.
type Storage ¶
type Storage struct {
Type string `json:"type"`
Raft *Raft `json:"raft,omitempty"`
Postgres *Postgres `json:"postgres,omitempty"`
}
Storage is a union: exactly one of Raft or Postgres is set, named by Type.
type TLS ¶
type TLS struct {
// Secret is a BYO kubernetes.io/tls Secret. Empty = cert-manager issues
// one from a namespaced self-signed CA.
Secret string `json:"secret,omitempty"`
// CAConfigMap names the ConfigMap holding the CA that signed the
// certificate. For the self-signed default the converge Job publishes it.
CAConfigMap string `json:"caConfigMap"`
Duration string `json:"duration"`
}
TLS says where the listeners' certificate comes from.
type TransitMount ¶ added in v1.0.14
type TransitMount struct {
Mount string `json:"mount"`
}
TransitMount is one transit secret engine: OpenBao signs and encrypts with keys it never releases. A mount is enabled and never disabled by the Job: disabling it would destroy every key on it.
type Writer ¶
type Writer struct {
Type string `json:"type"`
// ServiceAccount is the SA forge renders for Type "service_account".
ServiceAccount string `json:"serviceAccount,omitempty"`
// Role is the ManagedIdentity role for Type "oidc" (recorded; the OIDC
// wiring is not rendered here).
Role string `json:"role,omitempty"`
}
Writer is how a person writes values.