···55 control-plane: controller-manager
66 app.kubernetes.io/name: loom
77 app.kubernetes.io/managed-by: kustomize
88+ # Privileged policy allows Unconfined seccomp for buildah user namespaces.
99+ # The spindle jobs themselves are still hardened (non-root, no caps, no privilege escalation).
1010+ pod-security.kubernetes.io/enforce: privileged
1111+ pod-security.kubernetes.io/audit: privileged
1212+ pod-security.kubernetes.io/warn: privileged
813 name: system
914---
1015apiVersion: apps/v1
···2227 control-plane: controller-manager
2328 app.kubernetes.io/name: loom
2429 replicas: 1
3030+ # Use Recreate strategy because we use RWO PVCs that can only attach to one pod
3131+ strategy:
3232+ type: Recreate
2533 template:
2634 metadata:
2735 annotations:
···9494// InitWorkflow parses the workflow YAML and initializes a Workflow model.
9595// Pipeline environment variables (TANGLED_*) are injected into workflow.Environment
9696// by the framework after this method returns.
9797-func (e *KubernetesEngine) InitWorkflow(twf tangled.Pipeline_Workflow, cloneStep models.CloneStep) (*models.Workflow, error) {
9797+func (e *KubernetesEngine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) {
9898 // Parse the Raw YAML into the unified WorkflowSpec type
9999 var spec loomv1alpha1.WorkflowSpec
100100 if err := yaml.Unmarshal([]byte(twf.Raw), &spec); err != nil {
···122122 StepCommand: stepSpec.Command,
123123 StepKind: models.StepKindUser,
124124 })
125125+ }
126126+127127+ // Build clone step (uses upstream models.BuildCloneStep which is self-contained)
128128+ var cloneStep models.CloneStep
129129+ devMode := false // TODO: Make this configurable
130130+131131+ if twf.Clone == nil || !twf.Clone.Skip {
132132+ cloneStep = models.BuildCloneStep(twf, *tpl.TriggerMetadata, devMode)
125133 }
126134127135 // Store pre-computed workflow data
+73-19
internal/jobbuilder/job_template.go
···194194 RunAsNonRoot: &[]bool{true}[0],
195195 RunAsUser: &[]int64{1000}[0],
196196 FSGroup: &[]int64{1000}[0],
197197- // Note: User namespaces (hostUsers: false) for enhanced buildah rootless
198198- // operation requires Kubernetes 1.33+ and is not yet available in the
199199- // current API version. Buildah will still work in rootless mode without it.
197197+ // Unconfined seccomp is required for buildah to create user namespaces
198198+ // via unshare(CLONE_NEWUSER). The container is still hardened with:
199199+ // - RunAsNonRoot, RunAsUser 1000
200200+ // - AllowPrivilegeEscalation: false
201201+ // - Capabilities: Drop ALL
200202 SeccompProfile: &corev1.SeccompProfile{
201201- Type: corev1.SeccompProfileTypeRuntimeDefault,
203203+ Type: corev1.SeccompProfileTypeUnconfined,
202204 },
203205 },
204206 // Disable ServiceAccount token mounting for security
···223225nobody:x:65534:
224226runner:x:1000:
225227EOF
226226-mkdir -p /home-override/runner
228228+# subuid/subgid mappings for rootless buildah user namespaces
229229+cat > /etc-override/subuid <<'EOF'
230230+runner:100000:65536
231231+EOF
232232+cat > /etc-override/subgid <<'EOF'
233233+runner:100000:65536
234234+EOF
235235+# Create home directory structure then fix ownership
236236+mkdir -p /home-override/runner/.config/containers
237237+chmod 700 /home-override/runner/.config
238238+chown -R 1000:1000 /home-override/runner
227239echo "User setup complete"
228240`},
229241 SecurityContext: &corev1.SecurityContext{
···234246 RunAsUser: &[]int64{0}[0],
235247 Capabilities: &corev1.Capabilities{
236248 Drop: []corev1.Capability{"ALL"},
249249+ // CAP_CHOWN is needed to set ownership of home directory for UID 1000
250250+ Add: []corev1.Capability{"CHOWN"},
237251 },
238252 },
239253 VolumeMounts: []corev1.VolumeMount{
···272286 Image: "quay.io/buildah/stable:latest",
273287 Command: []string{"/bin/sh", "-c"},
274288 Args: []string{`
275275-# Configure buildah for rootless operation
289289+# Configure buildah storage - native overlay (kernel 6.12+ supports in user namespaces)
276290mkdir -p /var/lib/containers/storage
277291cat > /var/lib/containers/storage.conf <<'EOF'
278292[storage]
279293driver = "overlay"
280294runroot = "/var/lib/containers/runroot"
281295graphroot = "/var/lib/containers/storage"
282282-283283-[storage.options]
284284-additionalimagestores = []
285285-286286-[storage.options.overlay]
287287-mount_program = "/usr/bin/fuse-overlayfs"
288288-mountopt = "nodev,metacopy=on"
289296EOF
290297291298# Copy buildah binary to shared location
···293300294301echo "Buildah configured successfully"
295302`},
303303+ Env: []corev1.EnvVar{
304304+ {Name: "HOME", Value: "/home/runner"},
305305+ },
296306 SecurityContext: &corev1.SecurityContext{
297307 AllowPrivilegeEscalation: &[]bool{false}[0],
298308 RunAsNonRoot: &[]bool{true}[0],
···314324 Name: "tmp",
315325 MountPath: "/tmp",
316326 },
327327+ // Mount passwd/group/subuid/subgid and home directory so buildah
328328+ // sees consistent user identity and can write to ~/.config
329329+ {
330330+ Name: "etc-override",
331331+ MountPath: "/etc/passwd",
332332+ SubPath: "passwd",
333333+ },
334334+ {
335335+ Name: "etc-override",
336336+ MountPath: "/etc/group",
337337+ SubPath: "group",
338338+ },
339339+ {
340340+ Name: "etc-override",
341341+ MountPath: "/etc/subuid",
342342+ SubPath: "subuid",
343343+ },
344344+ {
345345+ Name: "etc-override",
346346+ MountPath: "/etc/subgid",
347347+ SubPath: "subgid",
348348+ },
349349+ {
350350+ Name: "home-override",
351351+ MountPath: "/home/runner",
352352+ SubPath: "runner",
353353+ },
317354 },
318355 },
319356 buildCloneInitContainer(config),
···329366 WorkingDir: "/tangled/workspace",
330367331368 SecurityContext: &corev1.SecurityContext{
332332- AllowPrivilegeEscalation: &[]bool{false}[0],
369369+ // AllowPrivilegeEscalation is required for newuidmap/newgidmap
370370+ // to set up user namespace mappings for rootless buildah.
371371+ AllowPrivilegeEscalation: &[]bool{true}[0],
333372 RunAsNonRoot: &[]bool{true}[0],
334373 RunAsUser: &[]int64{1000}[0],
335374 // Note: ReadOnlyRootFilesystem is NOT set for the runner container
···337376 // (e.g., /go/pkg, ~/.cache, /var/tmp) that we can't predict or mount
338377 Capabilities: &corev1.Capabilities{
339378 Drop: []corev1.Capability{"ALL"},
379379+ // SETUID/SETGID are needed for newuidmap/newgidmap file capabilities
380380+ // to work when setting up user namespace mappings for buildah
381381+ Add: []corev1.Capability{"SETUID", "SETGID"},
340382 },
341383 },
342384···344386345387 VolumeMounts: buildRunnerVolumeMounts(config),
346388347347- Env: append(buildEnvironmentVariables(config), corev1.EnvVar{
348348- Name: "LOOM_WORKFLOW_SPEC",
349349- Value: string(workflowSpecJSON),
350350- }),
389389+ Env: append(buildEnvironmentVariables(config),
390390+ corev1.EnvVar{
391391+ Name: "LOOM_WORKFLOW_SPEC",
392392+ Value: string(workflowSpecJSON),
393393+ },
394394+ ),
351395352396 // Inject repository secrets via envFrom if available
353397 EnvFrom: buildEnvFromSources(config),
···477521 Name: "buildah-storage",
478522 MountPath: "/var/lib/containers",
479523 },
480480- // Mount passwd/group files created by setup-user init container
524524+ // Mount passwd/group/subuid/subgid files created by setup-user init container
481525 // This ensures UID 1000 is recognized by tools like buildah
482526 {
483527 Name: "etc-override",
···488532 Name: "etc-override",
489533 MountPath: "/etc/group",
490534 SubPath: "group",
535535+ },
536536+ {
537537+ Name: "etc-override",
538538+ MountPath: "/etc/subuid",
539539+ SubPath: "subuid",
540540+ },
541541+ {
542542+ Name: "etc-override",
543543+ MountPath: "/etc/subgid",
544544+ SubPath: "subgid",
491545 },
492546 {
493547 Name: "home-override",