fix(build): allow kaniko to unpack base-image layers — drop-ALL removed the caps the tar apply needs (#71)

This commit is contained in:
Lemon-miaow committed 2026-09-24 03:05:07 +08:00
1 parent ac403b9cd3
commit 6e47730501
2 files changed
+71 -5

No files matched your search

+19 -5
View File
@@ -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
View File
@@ -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.