Fixed
Full Changelog: https://github.com/giantswarm/klaus/compare/v0.1.20...v0.1.21
Full Changelog: https://github.com/giantswarm/klaus/compare/v0.1.20...v0.1.21
LLMInferenceService: the llm-d workload’s network policy admits the workload’s own port, 8000 (giantswarm/agent-platform#525; proof 1 of giantswarm/giantswarm#37639). The connectivity chart’s -model-serving-llmisvc-workload CiliumNetworkPolicy admitted ingress on modelServing.networkPolicy.predictor.port — 8080, the classic InferenceService predictor’s, the one port every shape’s policy shared since #506 — while the workload pod the llm-d controller creates is reached on 8000 — the port KServe’s <name>-kserve-workload-svc targets and the rendered HTTPRoute’s backendRef names: the template’s llm-d routing sidecar (--port=8000 --vllm-port=8001), vLLM’s main on 8001 behind it. On gazelle (4.28.18, 2026-09-17 05:39Z, the first hands-free run of the proof to reach a Ready predictor) hubble showed the gateway’s SYNs to the pod on 8000 policy-denied, and every POST …/v1/chat/completions with a person’s id_token answered 503 upstream call failed: Connect: deadline has elapsed (without a token 401: the JWT policy worked). Each pod shape now carries its port in the chart’s one shape list (agent-platform.modelServing.podShapes): the classic predictor predictor.port (8080, unchanged), the llm-d workload the new modelServing.networkPolicy.llmisvcWorkload.port (8000), declared in both charts — the meta chart forwards its whole values tree to the connectivity release, so a connectivity-only default would have been shadowed by the meta chart’s tree exactly as the #522 lists were (#524). Every rule that names the port renders per shape: the ingress from the callers and from the kubelet (cilium), the kubernetes-flavour ingress, and the kagent agents’ egress, now one rule per shape on that shape’s port instead of both shapes on 8080. make verify-model-serving-policies ties each shape’s admitted port — in every one of those rules, as rendered from the connectivity defaults and from the values the meta chart forwards — to the port of the container the shape’s fixture pod is reached on (tests/fixtures/model-serving-llmisvc-workload-pod.yaml gains the routing sidecar the pod is reached through on 8000; main keeps its 8001), refuses two fixtures on one port, and fails a render whose llm-d value is the classic port (the 4.28.18 shape) naming the shape; make verify-meta (tests/verify-mirrored-values.py) holds every networkPolicy port equal between the two charts next to the fqdns / cidrs lists, both model-serving ports required. Every installation with model serving on gets two policy changes and no pod roll: the llm-d workload’s ingress moves to 8000, and the agents’ egress splits into one rule per shape. The live half — POST …/v1/chat/completions with a person’s id_token → 200 from the served model, without a token → 401 — is the proof’s.Full Changelog: https://github.com/giantswarm/klaus/compare/v0.1.19...v0.1.20
*.*.*.hf.co to the connectivity chart’s defaults only. The meta chart forwards its whole values tree to the connectivity release (templates/components.yaml, forwardAllValues), and a forwarded copy of a default shadows the child’s own, so an installation’s connectivity release still rendered the four old patterns from modelServing.networkPolicy.huggingFace.fqdns and modelManager.networkPolicy.huggingFace.fqdns of helm/agent-platform/values.yaml (gazelle 2026-09-17 04:30Z: Helm storage’s supplied .config with four entries beside the chart’s five; the three serving CNPs unchanged, the download would have been dropped exactly as before; caught before a model was loaded). Both meta lists gain *.*.*.hf.co, and the meta chart’s huggingFace and policies.env comments are the connectivity chart’s (the #520 rationale corrected in #523 was still the meta copy’s). Two checks hold the two charts together: make verify-meta (tests/verify-mirrored-values.py) asserts the meta chart’s copy of every networkPolicy fqdns / cidrs list equals the connectivity chart’s default, naming the path that differs, with a negative control (a list short of one entry fails, naming modelServing.networkPolicy.huggingFace.fqdns); make verify-model-serving-policies renders the connectivity chart a second time from the values the meta chart forwards to its release (the meta chart’s defaults, the model serving switch on) and matches the HUB_HOSTS fixture against those three CNPs too — the render an installation gets, not only the child’s defaults; against the previous meta values it fails naming us.aws.cdn.hf.co. The live half — the weights on the claim, the predictor Ready, /v1/chat/completions answering — is the proof’s.us.aws.cdn.hf.co, three labels under hf.co (giantswarm/agent-platform#522; proof 1 of giantswarm/giantswarm#37639). The connectivity chart’s -model-serving-predictor, -model-serving-llmisvc-workload and -model-serving-download CiliumNetworkPolicies admitted huggingface.co, *.huggingface.co, *.hf.co and *.*.hf.co; Cilium’s * matches one label and never a dot, so the download CDN the Hub redirects every shard request of a Xet-backed repository to (Xet client or not — the LFS path lands on the same host) matched nothing and the download’s SYNs were dropped one second after the DNS proxy had resolved it: with Xet off (#520) the storage-initializer crash-looped on us.aws.cdn.hf.co, with Xet on it hung. modelServing.networkPolicy.huggingFace.fqdns and modelManager.networkPolicy.huggingFace.fqdns gain *.*.*.hf.co; the lists stay a toFQDNs allow-list (no world, no CIDR). tests/verify-model-serving-policies.py (make verify-model-serving-policies) matches the names of the download path — the Hub, the LFS fronts, the Xet fronts, the CDN and a regional sibling — against the rendered patterns of all three policies with Cilium’s rule and keeps out a deeper name and a look-alike domain, so a new host shape fails the render, not a download on an installation. The modelServing.policies.env comment of #520 is corrected: Xet off keeps the initializer on one host path that fails fast on a policy drop; it is not what reaches the CDN.huggingface.co and then hung forever — no error, no restart, 0 CPU, eight sleeping threads (gazelle 2026-09-17 02:14Z and 02:30Z, chart 4.28.15, huggingface_hub 0.32.4 + hf_xet 1.5.1 + hf_transfer 0.1.9 in the initializer image, HF_HUB_ENABLE_HF_TRANSFER=1, HF_XET_HIGH_PERFORMANCE=1, HF_XET_NUM_CONCURRENT_RANGE_GETS=8). hubble observe --verdict DROPPED on the pod showed every SYN to the Xet CDN (15.236.85.177:443, world) policy-denied, and the endpoint’s cilium fqdn cache held addresses for cas-server.xethub.hf.co, us.aws.cdn.hf.co and huggingface.co — not that one. The connectivity chart’s serving-namespace CiliumNetworkPolicies are a toFQDNs allow-list (huggingface.co, *.huggingface.co, *.hf.co, *.*.hf.co on 443): Cilium admits exactly the addresses its DNS proxy saw resolved for an allowed name, for the answer’s TTL; the Xet CDN (CloudFront) answers with ~30 s TTLs and rotating address sets, and hf_xet — a Rust client with its own resolver cache and one connection per shard range-GET — keeps connecting to addresses the proxy has forgotten or never saw, so the shards’ SYNs are dropped and the client waits on its connect timeouts without reporting. The allow-list stays as it is — a model download is no reason to open world on 443 to the serving pods, and a CIDR block for a CDN is a list that rots — and the client comes off the Xet path: the pods ClusterPolicy gains one strategic-merge rule per pod shape, model-pod-env-<shape>, that merges modelServing.policies.env (default HF_HUB_DISABLE_XET=1) by name onto the storage-initializer’s and the shape’s runtime container’s env (kserve-container / main; every model pod with a storage-initializer, named model or not; the containers’ own entries stay, so HF_HUB_ENABLE_HF_TRANSFER=1 is kept). With Xet off, huggingface_hub downloads the shards with hf_transfer from the LFS fronts (cdn-lfs*.hf.co, cdn-lfs.huggingface.co — within the patterns) through the system resolver: every connection is a DNS query the proxy sees, and its address is admitted. The values document the mechanism, that widening the egress is not the fix, and the cluster-level alternative the chart does not own — a tofqdns-min-ttl on the Cilium agent long enough for the client’s cache, after which an installation may set env: [] to have Xet back; a mirror’s HF_ENDPOINT belongs in the same list. make verify-model-serving-policies (kyverno apply of the rendered policy over a fixture pod of each shape, the initializer now carrying KServe’s three HF variables and the classic runtime the ClusterServingRuntime’s two) holds the entry next to the containers’ own env on both containers of both shapes, added where a container has none, on a pod without a model name too, a no-op over the policy’s own output (webhook reinvocation), the download Job’s pod and a pod without an initializer untouched, no env rule with the list empty, and the mutated pod still passing the fleet’s restricted PSS with the chart’s exception. The live half — the 8 GB of qwen3-4b-instruct on the claim, the predictor Ready and /v1/chat/completions answering, a second load downloading nothing — is the proof’s. model-manager’s own download Job takes the same path on its side.model-serving pods policy injected hf-cache-init into every model pod — root, CHOWN/DAC_OVERRIDE/FOWNER added, chmod 0777 of the cache subdirectory — and the fleet’s Kyverno set denied the pod on two rules the chart’s PolicyException does not name (disallow-capabilities-strict/adding-capabilities-strict, require-run-as-non-root-user/run-as-non-root-user): on gazelle (4.28.14) the LLMInferenceService’s ReplicaSet was refused on every attempt (validate.kyverno.svc-fail, ReplicaFailure=True), no pod, no node, nothing served — the first time the container reached admission at all (#506 made the rule match these pods, #514 made it idempotent). The init container is removed, neither excepted nor made rootless: the redirect-model-storage-* rules give the pod the claim’s fsGroup next to the claim mount (modelServing.cache.fsGroup, default 1000 — the uid and fsGroup model-manager’s cache pods use; a pod that declared another fsGroup gets the claim’s, one claim, one group), and the kubelet does the rest — it makes the claim’s contents group-owned, group-writable and setgid on every mount (the CSI driver’s fsGroupPolicy permitting: EBS gp3 and Azure Disk apply it) and creates a missing subPath directory with the claim root’s mode and group, so <claim>/<model> is root:1000 2775 and the storage-initializer (uid 1000), the runtime and model-manager’s download Job (uid 1000, fsGroup 1000) write it, mkdir -m 0777 -p it and rm -rf it without root or a capability (the kubelet’s exact sequence replayed in a container; without the fsGroup uid 1000 is denied, which is what the init container had papered over). modelServing.policies.cacheInit (image, resources) is gone. The chart’s PolicyException is unchanged: it names the four rules the vLLM image and the storage-initializer violate, and nothing the chart adds needs one. make verify-model-serving-policies applies the fleet’s five restricted-PSS ClusterPolicies (tests/fixtures/restricted-pss-clusterpolicies.yaml: kyverno-policies 3.7.2 as installed on a management cluster, every rule Enforce) with the chart’s exception over the mutated pod of both shapes and holds every rule to pass or skip (the four excepted rules skip, adding-capabilities-strict and run-as-non-root-user pass), the bare pod to fail exactly the four excepted rules, and a pod with the 4.28.14 init container to fail exactly the two that denied it on gazelle; it holds the mutated pod’s fsGroup and that the mutation adds no container. The live half — the workload pod admitted on the first ReplicaSet attempt and the predictor Ready — is the proof’s.muster call – and every other command that connects to an aggregator – fails fast with auth_required instead of opening a browser, and a rejected token is no longer removed from the store. Without a usable token a command now exits with status 2 at once; the error starts with auth_required, names muster auth login --context <ctx> (or --endpoint <url>) and the new --login flag, which opens the browser from the command itself for a person at a terminal (--auth auto and MUSTER_AUTH_MODE=auto still do the same; the default --auth mode is now none, and a stored token is used and refreshed in every mode). A script, an agent or a CI job can never complete a browser flow, and inside a tool call the old behaviour hung until killed. The 401 handling of the commands, of auth status and of the agent’s re-authentication no longer deletes the token file first: several muster processes share ~/.config/muster/tokens/, the ID token in the file serves other clients, and a sign-in replaces the file only when it completes. The store writes each token file atomically (temporary file and rename), so a process reading an endpoint’s file while another refreshes it sees the old or the new token and never a truncated file it would take for a missing one; every write and removal is logged at debug with the pid and the command words of the process. auth logout removes the current context’s token only, --all every token. (#1273)add-cache-init pod mutation is idempotent under Kyverno webhook reinvocation (giantswarm/agent-platform#514; proof 1 of giantswarm/giantswarm#37639). The connectivity chart’s model-serving pods policy added hf-cache-init with a JSON patch (add /spec/initContainers/0) and no guard against being applied twice. Kyverno’s pod mutation webhook is registered with reinvocationPolicy: IfNeeded, and the API server reinvokes it whenever another mutating webhook changed the pod after Kyverno ran — the case for every LLMInferenceService workload pod on a Giant Swarm cluster — so the rule inserted the container a second time and the pod was refused by validation: on gazelle (4.28.11) the ReplicaSet of the served preset failed nine times in a row with spec.initContainers[2].name: Duplicate value: "hf-cache-init" while the GPU node ran empty (the two redirect-model-storage-* rules, strategic merges, were idempotent; before #506 the rule never reached these pods). add-cache-init is now a strategic merge keyed by the container’s name: the container is added the first time and found on a reinvocation, and Kyverno’s strategic merge leads a list with the entries it patched, so hf-cache-init stays ahead of the storage-initializer the way the insert at index 0 put it — the rule stays last in the policy, since a later merge on the storage-initializer would move that one ahead again. Audit of the chart’s other mutation rules: agent-sandbox-pod-security’s add-controller-security-context merges by (name) and +(securityContext) anchors, and no rule of either chart appends to a list with a JSON patch; nothing else to change. make verify-model-serving-policies now applies the rendered policy to its own output for both pod shapes and asserts a no-op (one hf-cache-init, one hf-cache volume) — the check failed on the previous rule with exactly the duplicate seen on gazelle. The live half — the workload pod admitted on the first ReplicaSet attempt with hf-cache-init, the hf-cache mount and the 4Gi initializer limit — is the proof’s.make live-serving-slice reads the controller’s jwks-store in the shape the controller writes it (giantswarm/agent-platform#515; proof 1 of giantswarm/giantswarm#37639): tests/verify-serving-slice-live.py asserted a positive nkeys on the jwks-store-<hash> entry, a field the store has never had — the entry is {requestKey, url, fetchedAt, jwks} as the controller’s jwks package persists it, jwks the fetched JWKS document as a JSON string — so against a healthy controller (gazelle 2026-09-16 23:41Z: models-jwt Accepted=Valid, two keys for the in-cluster Dex including the token’s kid) the one live signal #505 introduced failed. The check now parses jwks and counts its keys, printing their kids (a 401 token uses the unknown key "<kid>" names the one to compare with), fails naming the URL on an empty set as before, and fails naming the ConfigMap when jwks is not a JWKS document; the policy’s Accepted.reason stays the primary signal. make verify-serving-slice-store (tests/verify-serving-slice-store.py) holds the reader over tests/fixtures/jwks-store-configmaps.json, a copied store in the writer’s shape (names and request keys derived as the controller derives them: the issuer with two keys, an issuer with an empty set, a labelled ConfigMap without the entry) with kubectl stubbed — pass, empty set, unknown URL, malformed document; verify-all runs it.