fix(build): allow kaniko to unpack base-image layers — drop-ALL removed the caps the tar apply needs (#71)
This commit is contained in:
2 files changed
+71
-5
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user