Unverified Commit 6e477305 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(build): allow kaniko to unpack base-image layers — drop-ALL removed the...

fix(build): allow kaniko to unpack base-image layers — drop-ALL removed the caps the tar apply needs (#71)
parent ac403b9c
Loading
Loading
Loading
Loading
+19 −5
Changes for internal/build/jobspec.go: 19 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -128,15 +128,29 @@ func BuildJob(p JobParams) (*batchv1.Job, error) {
		deadline = int64(defaultDeadline / time.Second)
	}

	// Hardened container security context shared by both build containers: no
	// privilege, no privilege escalation, drop all capabilities. Kaniko needs a
	// writable root filesystem to unpack layers, so we do not force read-only
	// root here, but it gains no privilege.
	// Hardened container security context baseline: no privilege, no privilege
	// escalation, drop all capabilities. Kaniko needs a writable root filesystem
	// to unpack layers, so we do not force read-only root here; it also needs a
	// minimal capability subset added back (kanikoSec below), while fetch and
	// trivy run with exactly this baseline.
	sec := &corev1.SecurityContext{
		Privileged:               boolPtr(false),
		AllowPrivilegeEscalation: boolPtr(false),
		Capabilities:             &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}},
	}
	// Kaniko unpacks base-image layers as root, and the tar apply must chown/chmod
	// files to the owners the layer recorded — impossible under drop-ALL (live:
	// "failed to get filesystem from image: chown /etc/gshadow: operation not
	// permitted" for any FROM <base image>; scratch builds masked this because
	// COPY only ever creates files kaniko itself owns). Add back exactly the caps
	// the unpack needs and nothing else: CHOWN/FOWNER for the ownership and mode
	// restore, DAC_OVERRIDE to write entries whose bits would otherwise exclude
	// even root once the capability-based exemption is gone.
	kanikoSec := sec.DeepCopy()
	kanikoSec.Capabilities = &corev1.Capabilities{
		Drop: []corev1.Capability{"ALL"},
		Add:  []corev1.Capability{"CHOWN", "DAC_OVERRIDE", "FOWNER"},
	}

	// The context Kaniko reads. An http(s) ref (the submit lane's derived ref: the
	// API streams the blob on its internal face, because the build Pod can neither
@@ -224,7 +238,7 @@ func BuildJob(p JobParams) (*batchv1.Job, error) {
		},
		VolumeMounts:    kanikoMounts,
		Resources:       corev1.ResourceRequirements{Limits: limits, Requests: buildRequests(limits)},
		SecurityContext: sec,
		SecurityContext: kanikoSec,
	}
	initContainers = append(initContainers, kaniko)

+52 −0
Changes for internal/build/jobspec_test.go: 52 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -210,6 +210,58 @@ func TestBuildJobKanikoPushesAndTrivyGates(t *testing.T) {
	}
}

// Kaniko (and only kaniko) must carry back exactly the unpacking capability
// subset: drop-ALL broke every FROM <base image> build live ("failed to get
// filesystem from image: chown /etc/gshadow: operation not permitted"), while a
// scratch COPY masked it. The pod stays unprivileged and every other container
// keeps the pure baseline.
func TestBuildJobKanikoGetsOnlyUnpackCapabilities(t *testing.T) {
	job, err := BuildJob(sampleJobParams())
	if err != nil {
		t.Fatalf("BuildJob: %v", err)
	}
	var kaniko *corev1.Container
	for i := range job.Spec.Template.Spec.InitContainers {
		if job.Spec.Template.Spec.InitContainers[i].Name == "kaniko" {
			kaniko = &job.Spec.Template.Spec.InitContainers[i]
		}
	}
	if kaniko == nil {
		t.Fatal("no kaniko initContainer")
	}
	sc := kaniko.SecurityContext
	if sc == nil || sc.Capabilities == nil {
		t.Fatal("kaniko lost its security context")
	}
	if sc.Privileged == nil || *sc.Privileged ||
		sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation {
		t.Errorf("kaniko must stay unprivileged with no escalation, got %+v", sc)
	}
	if len(sc.Capabilities.Drop) != 1 || string(sc.Capabilities.Drop[0]) != "ALL" {
		t.Errorf("kaniko must still drop ALL, got %v", sc.Capabilities.Drop)
	}
	want := map[corev1.Capability]bool{"CHOWN": true, "DAC_OVERRIDE": true, "FOWNER": true}
	if len(sc.Capabilities.Add) != len(want) {
		t.Fatalf("kaniko capabilities.add = %v, want exactly the unpack trio", sc.Capabilities.Add)
	}
	for _, c := range sc.Capabilities.Add {
		if !want[c] {
			t.Errorf("kaniko must not gain %q — only the unpack trio is justified", c)
		}
	}
	// Every other container keeps the pure baseline: nothing re-added.
	all := append([]corev1.Container{}, job.Spec.Template.Spec.InitContainers...)
	all = append(all, job.Spec.Template.Spec.Containers...)
	for _, c := range all {
		if c.Name == "kaniko" {
			continue
		}
		if c.SecurityContext != nil && c.SecurityContext.Capabilities != nil && len(c.SecurityContext.Capabilities.Add) > 0 {
			t.Errorf("container %q must not add capabilities, got %v", c.Name, c.SecurityContext.Capabilities.Add)
		}
	}
}

// A configured DB repository (the internal mirror) must reach Trivy as
// --db-repository: without it the scan tries the internet, which the build egress
// lock denies, and every build fails closed at the scan gate.