Fix restore-wait init container ignoring pod-level securityContext (#10047)

restore-wait's securityContext fallback chain checked the fs-restore
ConfigMap, then the first container's SecurityContext, then hardcoded
runAsUser 1000. It never consulted pod.Spec.SecurityContext, so pods
that set identity only at the pod level got a helper running as uid
1000 regardless of the workload's actual uid. On volumes where restored
content is owner-only-visible to a non-1000 uid, the helper's stat on
the done-file returns EACCES forever and the pod deadlocks at Init:0/1.

Add pod-level spec.securityContext.runAsUser/runAsGroup as a fallback
between the container-level check and the hardcoded default, since the
workload's own identity is the one that can read what it restored.
Defer to the pod's own RunAsNonRoot setting when runAsUser is 0, since
the hardcoded RunAsNonRoot: true would otherwise contradict a root uid.

Also add a test case covering both container-level and pod-level
SecurityContext set together, confirming container-level still wins.

Fixes #10046

Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
This commit is contained in:
Tiger Kaovilai
2026-08-10 15:36:14 -04:00
committed by GitHub
parent fb86290def
commit ced051b72f
3 changed files with 225 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
Fix restore-wait init container ignoring pod-level securityContext, falling back to hardcoded runAsUser 1000 instead of the workload's own uid/gid, causing fs-backup restores to deadlock at Init:0/1 on owner-restricted volumes
@@ -198,6 +198,25 @@ func (a *PodVolumeRestoreAction) Execute(input *velero.RestoreItemActionExecuteI
securityContext = *pod.Spec.Containers[0].SecurityContext.DeepCopy()
securityContextSet = true
}
// if no configmap or container-level securityContext is set, fall back to the pod-level
// spec.securityContext runAsUser/runAsGroup: the workload's own identity is the one that
// wrote the restored files, so it's the one that can read them back
if !securityContextSet && pod.Spec.SecurityContext != nil &&
(pod.Spec.SecurityContext.RunAsUser != nil || pod.Spec.SecurityContext.RunAsGroup != nil) {
securityContext = defaultSecurityCtx()
if pod.Spec.SecurityContext.RunAsUser != nil {
securityContext.RunAsUser = pod.Spec.SecurityContext.RunAsUser
// defaultSecurityCtx() hardcodes RunAsNonRoot: true, which contradicts a pod-level
// RunAsUser of 0 (root); defer to the pod's own RunAsNonRoot setting in that case
if *pod.Spec.SecurityContext.RunAsUser == 0 {
securityContext.RunAsNonRoot = pod.Spec.SecurityContext.RunAsNonRoot
}
}
if pod.Spec.SecurityContext.RunAsGroup != nil {
securityContext.RunAsGroup = pod.Spec.SecurityContext.RunAsGroup
}
securityContextSet = true
}
if !securityContextSet {
securityContext = defaultSecurityCtx()
}
@@ -156,6 +156,155 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) {
defaultRestoreHelperImage := "velero/velero:v1.0"
podLevelUID := int64(999)
podLevelGID := int64(999)
podLevelSecurityContext := corev1api.SecurityContext{
AllowPrivilegeEscalation: boolptr.False(),
Capabilities: &corev1api.Capabilities{
Drop: []corev1api.Capability{"ALL"},
},
SeccompProfile: &corev1api.SeccompProfile{
Type: corev1api.SeccompProfileTypeRuntimeDefault,
},
RunAsUser: &podLevelUID,
RunAsGroup: &podLevelGID,
RunAsNonRoot: boolptr.True(),
}
podWithPodLevelSecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
Result()
podWithPodLevelSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelUID, RunAsGroup: &podLevelGID}
wantPodWithPodLevelSecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
InitContainers(
newRestoreInitContainerBuilder(defaultRestoreHelperImage, "").
Resources(&resourceReqs).
SecurityContext(&podLevelSecurityContext).
VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()).
Command([]string{"/velero-restore-helper"}).Result()).
Result()
wantPodWithPodLevelSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelUID, RunAsGroup: &podLevelGID}
podLevelRootUID := int64(0)
podLevelRootSecurityContext := corev1api.SecurityContext{
AllowPrivilegeEscalation: boolptr.False(),
Capabilities: &corev1api.Capabilities{
Drop: []corev1api.Capability{"ALL"},
},
SeccompProfile: &corev1api.SeccompProfile{
Type: corev1api.SeccompProfileTypeRuntimeDefault,
},
RunAsUser: &podLevelRootUID,
}
podWithPodLevelRootSecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
Result()
podWithPodLevelRootSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelRootUID}
wantPodWithPodLevelRootSecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
InitContainers(
newRestoreInitContainerBuilder(defaultRestoreHelperImage, "").
Resources(&resourceReqs).
SecurityContext(&podLevelRootSecurityContext).
VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()).
Command([]string{"/velero-restore-helper"}).Result()).
Result()
wantPodWithPodLevelRootSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelRootUID}
podLevelGroupOnlyGID := int64(777)
podLevelGroupOnlySecurityContext := corev1api.SecurityContext{
AllowPrivilegeEscalation: boolptr.False(),
Capabilities: &corev1api.Capabilities{
Drop: []corev1api.Capability{"ALL"},
},
SeccompProfile: &corev1api.SeccompProfile{
Type: corev1api.SeccompProfileTypeRuntimeDefault,
},
RunAsUser: &id,
RunAsGroup: &podLevelGroupOnlyGID,
RunAsNonRoot: boolptr.True(),
}
podWithPodLevelGroupOnlySecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
Result()
podWithPodLevelGroupOnlySecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsGroup: &podLevelGroupOnlyGID}
wantPodWithPodLevelGroupOnlySecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
InitContainers(
newRestoreInitContainerBuilder(defaultRestoreHelperImage, "").
Resources(&resourceReqs).
SecurityContext(&podLevelGroupOnlySecurityContext).
VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()).
Command([]string{"/velero-restore-helper"}).Result()).
Result()
wantPodWithPodLevelGroupOnlySecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsGroup: &podLevelGroupOnlyGID}
bothLevelsPodUID := int64(500)
bothLevelsContainerUID := int64(999)
bothLevelsContainerSecurityContext := corev1api.SecurityContext{
AllowPrivilegeEscalation: boolptr.False(),
Capabilities: &corev1api.Capabilities{
Drop: []corev1api.Capability{"ALL"},
},
SeccompProfile: &corev1api.SeccompProfile{
Type: corev1api.SeccompProfileTypeRuntimeDefault,
},
RunAsUser: &bothLevelsContainerUID,
RunAsNonRoot: boolptr.True(),
}
podWithBothLevelsSecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
Containers(
builder.ForContainer("app-container", "app-image").
SecurityContext(&bothLevelsContainerSecurityContext).Result()).
Result()
podWithBothLevelsSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &bothLevelsPodUID}
wantPodWithBothLevelsSecurityContext := builder.ForPod("ns-1", "my-pod").
ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Volumes(
builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(),
).
Containers(
builder.ForContainer("app-container", "app-image").
SecurityContext(&bothLevelsContainerSecurityContext).Result()).
InitContainers(
newRestoreInitContainerBuilder(defaultRestoreHelperImage, "").
Resources(&resourceReqs).
SecurityContext(&bothLevelsContainerSecurityContext).
VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()).
Command([]string{"/velero-restore-helper"}).Result()).
Result()
wantPodWithBothLevelsSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &bothLevelsPodUID}
tests := []struct {
name string
pod *corev1api.Pod
@@ -350,6 +499,62 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) {
VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()).
Command([]string{"/velero-restore-helper"}).Result()).Result(),
},
{
name: "Restoring pod with pod-level securityContext (no container-level SecurityContext) uses pod-level runAsUser/runAsGroup for the restore initContainer",
pod: podWithPodLevelSecurityContext,
podVolumeBackups: []runtime.Object{
builder.ForPodVolumeBackup(veleroNs, "pvb-1").
PodName("my-pod").
PodNamespace("ns-1").
Volume("myvol").
ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)).
SnapshotID("foo").
Result(),
},
want: wantPodWithPodLevelSecurityContext,
},
{
name: "Restoring pod with pod-level securityContext.runAsUser=0 does not force RunAsNonRoot on the restore initContainer",
pod: podWithPodLevelRootSecurityContext,
podVolumeBackups: []runtime.Object{
builder.ForPodVolumeBackup(veleroNs, "pvb-1").
PodName("my-pod").
PodNamespace("ns-1").
Volume("myvol").
ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)).
SnapshotID("foo").
Result(),
},
want: wantPodWithPodLevelRootSecurityContext,
},
{
name: "Restoring pod with pod-level securityContext.runAsGroup only (no runAsUser) still applies the group to the restore initContainer",
pod: podWithPodLevelGroupOnlySecurityContext,
podVolumeBackups: []runtime.Object{
builder.ForPodVolumeBackup(veleroNs, "pvb-1").
PodName("my-pod").
PodNamespace("ns-1").
Volume("myvol").
ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)).
SnapshotID("foo").
Result(),
},
want: wantPodWithPodLevelGroupOnlySecurityContext,
},
{
name: "Restoring pod with both container-level and pod-level SecurityContext set uses the container-level SecurityContext for the restore initContainer (container-level takes priority)",
pod: podWithBothLevelsSecurityContext,
podVolumeBackups: []runtime.Object{
builder.ForPodVolumeBackup(veleroNs, "pvb-1").
PodName("my-pod").
PodNamespace("ns-1").
Volume("myvol").
ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)).
SnapshotID("foo").
Result(),
},
want: wantPodWithBothLevelsSecurityContext,
},
{
name: "pod volume backups in a different namespace are ignored when looking for matches due to namespace scoping",
pod: builder.ForPod("ns-1", "my-pod").