Skip to content

Commit 8d5caaa

Browse files
committed
Initial phase 1-3 of a per-node Kata config delivery
Closes: #77 Signed-off-by: Erick Bourgeois <erick@jeb.ca>
1 parent ad7b3ea commit 8d5caaa

27 files changed

Lines changed: 3237 additions & 4 deletions

.claude/CHANGELOG.md

Lines changed: 366 additions & 0 deletions
Large diffs are not rendered by default.

.trivyignore

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ AVD-KSV-0048
4141
# which is impractical for a cluster-wide controller.
4242
AVD-KSV-0041
4343

44+
# NOTE: KSV-0049 (create/update/patch/delete on 'configmaps') is intentionally
45+
# NOT suppressed. The controller ClusterRole grants `configmaps: ["get"]` only
46+
# (read-only kata source-object probe, ADR 0002); the reclaim-agent ConfigMap
47+
# projection writes ride the resolved child-cluster kubeconfig identity, not the
48+
# controller token. There is no configmap-write finding to suppress.
49+
4450
# ─────────────────────────────────────────────────────────────────────────────
4551
# Deployment — deploy/deployment/deployment.yaml
4652
# ─────────────────────────────────────────────────────────────────────────────
@@ -190,6 +196,61 @@ AVD-KSV-0116
190196
# armed.
191197
AVD-KSV-0022
192198

199+
# ─────────────────────────────────────────────────────────────────────────────
200+
# DaemonSet — deploy/kata-config-agent/daemonset.yaml
201+
# ─────────────────────────────────────────────────────────────────────────────
202+
#
203+
# The kata-config-agent shares most of its hardened-but-rooted posture with the
204+
# reclaim agent, so the hostPath (KSV-0023/0121), hostPID (KSV-0010), and
205+
# root/runAsNonRoot/runAsGroup findings (KSV-0012/0118/0105/0020/0021/0116) are
206+
# already suppressed above by the same global rule IDs. The two additional
207+
# findings are privileged (KSV-0017) and its capability corollary (KSV-0106):
208+
209+
# KSV-0017 — Container 'agent' should not run as privileged.
210+
# Architecturally required (ADR 0003). The agent's job is to write a Kata
211+
# containerd drop-in to the HOST filesystem and bounce the host k0s service via
212+
# `nsenter -t 1 -m -u -i -n -p -- systemctl restart <restartService>` so
213+
# containerd reloads it. setns() into the host mount/PID namespaces requires
214+
# `privileged: true` (hostPID alone is insufficient for the mount-namespace
215+
# entry). The escalation is bounded by: opt-in nodeSelector
216+
# (5spot.finos.org/kata-config=enabled — lands only on nodes the controller
217+
# labelled, which only happens when an SM with spec.kata resolves a source on
218+
# this cluster), readOnlyRootFilesystem, seccomp RuntimeDefault, a single-purpose
219+
# binary with no shell, and a narrow node-scoped ServiceAccount (nodes get/patch
220+
# on its own Node + read-only configmaps/secrets in its target namespace).
221+
AVD-KSV-0017
222+
223+
# KSV-0106 — Container 'agent' should drop all capabilities (and only add
224+
# NET_BIND_SERVICE). Moot under privileged: true (KSV-0017 above, ADR 0003): a
225+
# privileged container holds the full Linux capability set regardless of any
226+
# securityContext.capabilities.drop list, so `capabilities.drop: [ALL]` would be
227+
# a no-op rather than a real hardening — adding it would misrepresent the
228+
# container's effective privileges in an audit. The agent needs no
229+
# NET_BIND_SERVICE (it binds no privileged ports; it writes the host drop-in and
230+
# bounces the k0s service via nsenter). The escalation is bounded by the same
231+
# controls justified for KSV-0017: opt-in nodeSelector, readOnlyRootFilesystem,
232+
# seccomp RuntimeDefault, a shell-less single-purpose binary, and a narrow
233+
# node-scoped ServiceAccount.
234+
AVD-KSV-0106
235+
236+
# ─────────────────────────────────────────────────────────────────────────────
237+
# RBAC — deploy/kata-config-agent/rbac.yaml
238+
# ─────────────────────────────────────────────────────────────────────────────
239+
240+
# KSV-0113 — Role '5spot-kata-config-agent' shouldn't view/manage secrets in
241+
# namespace '5spot-system'.
242+
# Access is READ-ONLY and narrowed to `get` (no list/watch, no create/update/
243+
# patch/delete) — the "manage" wording is misleading; the rule fires on any
244+
# namespace-scoped secret read. The grant is architecturally required: a Kata
245+
# drop-in source object may be a Secret (spec.kata referencing a Secret, ADR
246+
# 0002), and the agent resolves it with a direct GET by the exact name carried
247+
# in its Node's kata-config-ref annotation — it never enumerates. Same posture
248+
# as the controller's KSV-0041 read-only secrets suppression. Tightening
249+
# further (e.g. dropping `get`) would remove the Secret-backed drop-in source
250+
# capability entirely; scope is already minimal (single namespace, get-only,
251+
# opt-in nodeSelector).
252+
AVD-KSV-0113
253+
193254
# ─────────────────────────────────────────────────────────────────────────────
194255
# Dockerfile — Dockerfile, Dockerfile.chainguard
195256
# ─────────────────────────────────────────────────────────────────────────────

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ lazy_static = "1.5"
6868
# enabled explicitly.
6969
warp = { version = "0.4", default-features = false, features = ["server"] }
7070
toml = "1.1"
71+
# SHA-256 for the kata-config agent's content/drift hashing and (Phase 4)
72+
# the applied-hash node annotation. RustCrypto, pure-Rust, no system deps.
73+
sha2 = "0.10"
7174

7275
# Linux-only — used by the reclaim-agent's rung 2 netlink proc connector
7376
# subscriber (`src/netlink_proc.rs`). The portable byte parsers in that
@@ -81,6 +84,10 @@ nix = { version = "0.31", default-features = false, features = ["socket", "net",
8184
name = "5spot-reclaim-agent"
8285
path = "src/bin/reclaim_agent.rs"
8386

87+
[[bin]]
88+
name = "5spot-kata-config-agent"
89+
path = "src/bin/kata_config_agent.rs"
90+
8491
[dev-dependencies]
8592
tokio-test = "0.4"
8693
mockall = "0.14"

deploy/crds/scheduledmachine.yaml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,89 @@ spec:
134134
- kind
135135
- spec
136136
type: object
137+
kata:
138+
description: |-
139+
Optional reference to a `Secret` or `ConfigMap` on the **workload
140+
cluster** holding a Kata containerd drop-in to deliver to the node(s) this
141+
resource owns.
142+
143+
When set, the controller resolves the referenced object on the workload
144+
cluster (via the `kubeconfig-<clusterName>` Secret) in `kata.namespace`
145+
(default `5spot-system`). If it is present, the controller stamps the
146+
`5spot.finos.org/kata-config=enabled` opt-in label **and** a reference
147+
annotation on the backing Node; the `5spot-kata-config-agent` DaemonSet —
148+
scheduled onto labelled nodes — reads the object from the workload API,
149+
writes the drop-in to `destPath`, and restarts `restartService` so
150+
containerd reloads it. If the object (or its namespace) is absent, the
151+
controller does **not** label the Node and reports a fail-fast status
152+
condition — 5-Spot never creates the object (it must pre-exist,
153+
Flux-delivered).
154+
155+
This is config *delivery*, not a Kata install: the `/opt/kata` binaries
156+
remain `kata-deploy`'s responsibility, and the existing
157+
`katacontainers.io/kata-runtime` opt-in label is unaffected. See ADR 0002
158+
and ADR 0003.
159+
nullable: true
160+
properties:
161+
destPath:
162+
default: /etc/k0s/container.d/kata-containers.toml
163+
description: |-
164+
Absolute host path the agent writes the drop-in to. Defaults to
165+
`/etc/k0s/container.d/kata-containers.toml` (the k0s containerd drop-in
166+
directory). Configurable because layouts vary (k0s-in-docker, kairos,
167+
vanilla containerd).
168+
maxLength: 4096
169+
minLength: 1
170+
pattern: ^/[^\u0000]*$
171+
type: string
172+
key:
173+
default: kata-containers.toml
174+
description: |-
175+
Key within the source's `data` map whose value is the drop-in content.
176+
Defaults to `kata-containers.toml`.
177+
maxLength: 253
178+
minLength: 1
179+
pattern: ^[A-Za-z0-9._-]+$
180+
type: string
181+
kind:
182+
description: Whether the source is a `ConfigMap` or a `Secret`. Required (no default).
183+
enum:
184+
- ConfigMap
185+
- Secret
186+
type: string
187+
name:
188+
description: |-
189+
Name of the source object on the workload cluster. RFC-1123 DNS subdomain
190+
(max 253 chars).
191+
maxLength: 253
192+
minLength: 1
193+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
194+
type: string
195+
namespace:
196+
default: 5spot-system
197+
description: |-
198+
Workload-cluster namespace the agent reads the object from. Defaults to
199+
`5spot-system` (the agent's own namespace, so no cross-namespace agent
200+
RBAC is needed). Override to place config in a per-tenant namespace.
201+
maxLength: 63
202+
minLength: 1
203+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
204+
type: string
205+
restartService:
206+
default: k0sworker.service
207+
description: |-
208+
systemd unit the node agent restarts (via `nsenter`) so containerd
209+
reloads the drop-in. Defaults to `k0sworker.service`; override with
210+
`k0scontroller.service` on single-node / controller-runs-workloads
211+
layouts.
212+
maxLength: 255
213+
minLength: 1
214+
pattern: ^[A-Za-z0-9@._-]+\.service$
215+
type: string
216+
required:
217+
- kind
218+
- name
219+
type: object
137220
killIfCommands:
138221
description: |-
139222
Optional list of process patterns that trigger an emergency node

deploy/deployment/rbac/clusterrole.yaml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,43 @@ rules:
6161
resources: ["events"]
6262
verbs: ["create", "patch"]
6363

64-
# Secrets for SSH keys and bootstrap data
64+
# Secrets for SSH keys, bootstrap data, and child-cluster kubeconfigs.
65+
#
66+
# Read-only. Also covers reading a `kata` source Secret on the workload
67+
# cluster in the degenerate co-located case (ADR 0002); in true multi-cluster
68+
# that read rides the kubeconfig identity. The controller never writes
69+
# Secrets, so no create/patch/delete here.
6570
- apiGroups: [""]
6671
resources: ["secrets"]
6772
verbs: ["get", "list", "watch"]
6873

74+
# ConfigMaps — read-only.
75+
#
76+
# `get` is the kata source-object existence probe (ADR 0002): the controller
77+
# checks that the referenced source ConfigMap exists, then stamps a Node label
78+
# + reference annotation. The node-side agent — not the controller — reads the
79+
# source and writes the host drop-in, so the controller never writes a kata
80+
# ConfigMap.
81+
#
82+
# The reclaim-agent per-node ConfigMap projection (`reclaim-agent-<node>`) does
83+
# write ConfigMaps, but on the workload cluster via the resolved
84+
# `kubeconfig-<clusterName>` child-cluster identity — not this controller
85+
# token. This matches the posture that shipped with reclaim: before the kata
86+
# branch this ClusterRole had no `configmaps` rule at all. Keeping write verbs
87+
# off the controller ServiceAccount preserves least privilege and avoids the
88+
# Trivy KSV-0049 finding entirely (no suppression needed).
89+
- apiGroups: [""]
90+
resources: ["configmaps"]
91+
verbs: ["get"]
92+
93+
# Namespaces — read-only. The `kata` fail-fast probe distinguishes a missing
94+
# source object from a missing target namespace (ADR 0002). In the degenerate
95+
# co-located case this uses the controller SA; in multi-cluster it rides the
96+
# kubeconfig identity.
97+
- apiGroups: [""]
98+
resources: ["namespaces"]
99+
verbs: ["get"]
100+
69101
# Nodes for drain operations and reclaim-agent label / taint management.
70102
#
71103
# `update` was removed in Phase 5 of the 2026-04-25 security audit
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# 5-Spot Kata Config Agent DaemonSet (workload cluster)
2+
#
3+
# Opt-in node-side delivery of a Kata containerd drop-in. Lands only on nodes
4+
# the controller has labelled `5spot.finos.org/kata-config=enabled` (which only
5+
# happens when an SM with `spec.kata` resolves its source object on this cluster).
6+
#
7+
# The agent reads its Node's `5spot.finos.org/kata-config-ref` annotation, GETs
8+
# the referenced ConfigMap/Secret from the workload API (NOT a mounted volume — a
9+
# DaemonSet cannot template a configMap.name per replica, ADR 0002 §3), and
10+
# atomically writes the drop-in to the configured destPath under the hostPath
11+
# /host mount.
12+
#
13+
# The pod is `privileged: true` + `hostPID: true` (ADR 0003): writing the host
14+
# drop-in and bouncing the host k0s service via `nsenter -t 1 -m -u -i -n -p --
15+
# systemctl restart <restartService>` require setns into the host namespaces and
16+
# resolving host PID 1. `privileged` is the architecturally-required escalation,
17+
# bounded by opt-in label-gating (lands only on nodes the controller labelled),
18+
# a read-only root filesystem, and seccomp RuntimeDefault. Justification is in
19+
# `.trivyignore` (kata-config-agent banner) and docs/src/security/threat-model.md.
20+
apiVersion: apps/v1
21+
kind: DaemonSet
22+
metadata:
23+
name: 5spot-kata-config-agent
24+
namespace: 5spot-system
25+
labels:
26+
app: 5spot-kata-config-agent
27+
app.kubernetes.io/name: 5spot
28+
app.kubernetes.io/component: kata-config-agent
29+
spec:
30+
selector:
31+
matchLabels:
32+
app: 5spot-kata-config-agent
33+
updateStrategy:
34+
type: RollingUpdate
35+
rollingUpdate:
36+
maxUnavailable: 10%
37+
template:
38+
metadata:
39+
labels:
40+
app: 5spot-kata-config-agent
41+
app.kubernetes.io/name: 5spot
42+
app.kubernetes.io/component: kata-config-agent
43+
spec:
44+
serviceAccountName: 5spot-kata-config-agent
45+
# Opt-in: only land on nodes the controller has labelled.
46+
nodeSelector:
47+
5spot.finos.org/kata-config: enabled
48+
# Reach cordoned / NotReady nodes too — delivery is infra-level.
49+
tolerations:
50+
- operator: Exists
51+
priorityClassName: system-node-critical
52+
terminationGracePeriodSeconds: 5
53+
# hostPID so `nsenter -t 1` resolves host PID 1 (systemd) for the k0s
54+
# service restart (ADR 0003).
55+
hostPID: true
56+
securityContext:
57+
# Root is required to write the drop-in under /etc/k0s on the host.
58+
# nosemgrep: yaml.kubernetes.security.run-as-non-root-unsafe-value.run-as-non-root-unsafe-value
59+
runAsNonRoot: false
60+
runAsUser: 0
61+
runAsGroup: 0
62+
seccompProfile:
63+
type: RuntimeDefault
64+
containers:
65+
- name: agent
66+
# Pin a released tag — never :latest in production (KSV-0013).
67+
image: ghcr.io/finos/5-spot-kata-config-agent:v0.1.0
68+
imagePullPolicy: IfNotPresent
69+
env:
70+
- name: RUST_LOG
71+
value: "info"
72+
- name: RUST_LOG_FORMAT
73+
value: "json"
74+
- name: NODE_NAME
75+
valueFrom:
76+
fieldRef:
77+
fieldPath: spec.nodeName
78+
- name: HOST_ROOT
79+
value: "/host"
80+
securityContext:
81+
# privileged is required for setns into host mount/PID namespaces
82+
# (the `nsenter` k0s-service restart, ADR 0003). With privileged,
83+
# allowPrivilegeEscalation must be true; the root filesystem stays
84+
# read-only and seccomp stays RuntimeDefault to keep the escalation
85+
# as narrow as the feature allows.
86+
privileged: true
87+
runAsUser: 0
88+
runAsGroup: 0
89+
# nosemgrep: yaml.kubernetes.security.run-as-non-root-unsafe-value.run-as-non-root-unsafe-value
90+
runAsNonRoot: false
91+
allowPrivilegeEscalation: true
92+
readOnlyRootFilesystem: true
93+
seccompProfile:
94+
type: RuntimeDefault
95+
resources:
96+
limits:
97+
cpu: 50m
98+
memory: 64Mi
99+
requests:
100+
cpu: 10m
101+
memory: 16Mi
102+
volumeMounts:
103+
# Host root mounted writable so the agent can write the configurable
104+
# destPath (default /etc/k0s/container.d/). The dest path is
105+
# operator-configurable per SM, so a narrower subPath cannot be
106+
# pinned in a shared DaemonSet spec (ADR 0003).
107+
- name: host-root
108+
mountPath: /host
109+
volumes:
110+
- name: host-root
111+
hostPath:
112+
path: /
113+
type: Directory

0 commit comments

Comments
 (0)