In its own cluster, the agent (plumb-agent ClusterRole, generated from the code) has these permissions:
| Resource | Verbs | Why |
|---|---|---|
adaptivepolicies |
get, list, watch | Read policies |
adaptivepolicies/status |
get, update, patch | Reports, intents, fleet status |
deployments, pods, nodes |
get, list, watch | Replicas and the placement simulation |
horizontalpodautoscalers |
get, list, watch | Whether the workload's HPA holds replicas for its scale-down window |
events (core) |
get, list, watch | Karpenter launch-failure events |
events (events.k8s.io) |
create, patch | Decision Events |
nodepools (karpenter.sh) |
get, list, watch | Dynamic room (limits minus usage) |
nodeclaims (karpenter.sh) |
get, list, watch | Launch failures, and nodes being launched |
leases |
get, list, watch, create, update, patch, delete | Per-cluster and fleet leader election |
clusterprofiles |
get, list, watch | Fleet membership |
httproutes |
get, update, patch | Traffic weights |
secrets (Plumb's namespace only, Role) |
get | For kubeconfig-secretreader-style access providers |
The scaler reads adaptivepolicies and the plumb-hub Lease, and nothing else.
In other members, the identity each access provider yields needs only the subset the hub uses: policies (read, status patch), leases in Plumb's namespace, and the HTTPRoutes it steers. Binding it to plumb-agent is simplest. A narrower ClusterRole with only those rules is tighter.
Plumb never creates or deletes nodes or pods, and never edits NodePools.
- Member credentials come from ClusterProfile access providers (KEP-5339 exec plugins), resolved per request. Prefer short-lived tokens from your cloud's identity (e.g. workload identity) over long-lived kubeconfigs in Secrets.
- The model API key is read from
PLUMB_MODEL_API_KEY(else the provider's own variable). Set it from a Secret withmodel.apiKeySecret; it is never logged. - The planner (experimental) uses its host's own credentials: for Bedrock, the AWS SDK's default chain, i.e. IRSA (
agent.serviceAccountAnnotations) or EKS Pod Identity. Grant the rolebedrock:InvokeModelon the one model it uses. For an OpenAI-compatible host, Plumb requires an explicit endpoint and optionally sendsPLUMB_PLANNER_API_KEYas a Bearer token fromplanner.apiKeySecret; it never logs the token.
Nothing, by default. Data leaves only through the experimental models, and only when you configure them. Both are called only while there is something to decide (a member short, or the fleet not Steady), in shadow mode too.
| Call | When | What it carries |
|---|---|---|
| Ranking model (rules path) | model.provider and a key are set, and two or more clusters are candidates |
Per candidate: cluster name, ready replicas, static and dynamic room, recent launch-failure count, cost rank, max replicas |
| Planner and Jev (adaptive path) | A policy has experimental.adaptive, and planner.provider is set, with model.provider for chooser: jev (with chooser: planner only the planner is called) |
The policy's intent text and placement preference; per cluster: name, replicas, room, launch failures, floor, traffic share, pressure, latency, error rate, demand, safe pressure, and the signals.metrics values with their names, units and meanings; how those went over the last 10 minutes; recent decisions and their outcomes; the candidate plans |
Workload names, namespaces, pod specs and node names are not sent. The intent and metric meanings are sent as written: keep secrets out of them.
- Hosted Jev (
https://api.typesafe.ai) is called only when an API key is set. Withmodel.provider=cloudflareorvercelthe summary goes to Cloudflare Workers AI (listed as zero data retention for this model in Cloudflare's model catalog) or Vercel AI Gateway instead, again only with a key. - Plain
httpmodel URLs are refused unless the host islocalhost, a loopback address, or an in-cluster*.svcname. - Keeping it in-house. Rules only is the default (
model.provider=""); a compatible model can also be served in-cluster.
- Kubernetes API calls use each cluster's TLS and credentials.
- The scaler's gRPC endpoint is plaintext by default and reachable in-cluster only (ClusterIP). Set
scaler.tlsSecretNameto serve TLS, and restrict access with a NetworkPolicy that admits only KEDA's operator. - Metrics (
:8080) are plain HTTP. Scrape them in-cluster, or front them with your usual metrics authentication.
- Hub election needs a majority of members, with compare-and-swap writes on each Lease. See architecture.
- Fencing. Floors are served only while their writer holds the member's copy of the hub Lease, and only until they expire (5 minutes). Route weights are written only after the same check.
- Model output is untrusted input. Probabilities for unknown clusters or outside [0, 1] are rejected. On the rules path the model can reorder candidates, but never change how much moves or put dynamic capacity ahead of idle static capacity. On the adaptive path every plan, whoever proposed it, is validated against
maxReplicas,step,stepPercent, weight bounds, the cooldown and other policies' reservations before Jev may pick it, and Jev can only pick a plan it was offered.
- Release images are multi-arch, distroless (
nonroot), and published with SBOM and provenance attestations. - Pods run non-root with a read-only root filesystem, no privilege escalation, all capabilities dropped, and the
RuntimeDefaultseccomp profile.
See SECURITY.md.