Skip to content

Commit d91ccb0

Browse files
stubbiclaude
andauthored
fix: add spec.security.seLinuxRelabel opt-out for relabel init container (#98)
* fix: add spec.security.seLinuxRelabel opt-out for relabel init container The operator unconditionally adds a privileged "selinux-relabel" init container whenever persistence is enabled. On clusters where SELinux relabeling does not apply (NFS-backed storage, or nodes that are not SELinux-enforcing such as Ubuntu with AppArmor), the container's chcon fails permanently with "Operation not supported", leaving the pod stuck in Init:CrashLoopBackOff and the Service returning 503. There was no way to disable it via the Instance CRD. Add a backward-compatible opt-out field, spec.security.seLinuxRelabel (*bool, default true). The init container is now gated on both PersistenceEnabled and SELinuxRelabelEnabled. Unset preserves today's behavior so existing SELinux-enforcing clusters are unaffected; an explicit false skips the init container entirely. Closes #96 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: regenerate api-reference for seLinuxRelabel field Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0528728 commit d91ccb0

10 files changed

Lines changed: 176 additions & 1 deletion

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,28 @@ spec:
973973
drop: [ALL]
974974
```
975975

976+
### SELinux Relabel
977+
978+
When persistence is enabled, the operator adds a privileged `selinux-relabel`
979+
init container that runs `chcon` on the data volume so its MCS categories match
980+
the pod's SELinux level. On SELinux-enforcing nodes this is required for the pod
981+
to read its own persistent data.
982+
983+
On clusters where the relabel does not apply -- NFS-backed storage, or nodes
984+
that are not SELinux-enforcing (for example Ubuntu with AppArmor) -- `chcon`
985+
fails permanently with `Operation not supported`, leaving the pod stuck in
986+
`Init:CrashLoopBackOff`. Opt out by setting `seLinuxRelabel: false`:
987+
988+
```yaml
989+
spec:
990+
security:
991+
seLinuxRelabel: false # default: true (init container is added)
992+
```
993+
994+
The field defaults to `true`, preserving the legacy behavior, so existing
995+
SELinux-enforcing clusters are unaffected. Setting it to `false` skips the init
996+
container entirely.
997+
976998
### RBAC and ServiceAccount
977999

9781000
```yaml

api/v1alpha1/paperclipinstance_types.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,27 @@ type SecuritySpec struct {
934934
// +optional
935935
ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"`
936936

937+
// SELinuxRelabel controls whether the operator adds a privileged
938+
// "selinux-relabel" init container that runs chcon on the data volume when
939+
// persistence is enabled. This relabel is required on SELinux-enforcing
940+
// nodes so the data directory's MCS categories match the pod's SELinux
941+
// level; without it the pod cannot read its own persistent data.
942+
//
943+
// It defaults to true to preserve the legacy behavior (the init container is
944+
// always added when persistence is enabled). Set it to false on clusters
945+
// where the relabel does not apply and would fail permanently, e.g. NFS-
946+
// backed storage or nodes that are not SELinux-enforcing (Ubuntu with
947+
// AppArmor). On those clusters chcon returns "Operation not supported",
948+
// leaving the pod stuck in Init:CrashLoopBackOff.
949+
//
950+
// A pointer is required so an explicit `false` survives marshaling: a plain
951+
// bool with omitempty is dropped on marshal and the API server re-defaults
952+
// it to true on every controller update (same bug class as
953+
// PersistenceSpec.Enabled and NetworkPolicySpec.Enabled).
954+
// +kubebuilder:default=true
955+
// +optional
956+
SELinuxRelabel *bool `json:"seLinuxRelabel,omitempty"`
957+
937958
// NetworkPolicy configures network isolation.
938959
// +optional
939960
NetworkPolicy NetworkPolicySpec `json:"networkPolicy,omitempty"`

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/paperclip-operator/templates/crds/paperclip.inc_instances.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6939,6 +6939,27 @@ spec:
69396939
annotations for the ServiceAccount.
69406940
type: object
69416941
type: object
6942+
seLinuxRelabel:
6943+
default: true
6944+
description: |-
6945+
SELinuxRelabel controls whether the operator adds a privileged
6946+
"selinux-relabel" init container that runs chcon on the data volume when
6947+
persistence is enabled. This relabel is required on SELinux-enforcing
6948+
nodes so the data directory's MCS categories match the pod's SELinux
6949+
level; without it the pod cannot read its own persistent data.
6950+
6951+
It defaults to true to preserve the legacy behavior (the init container is
6952+
always added when persistence is enabled). Set it to false on clusters
6953+
where the relabel does not apply and would fail permanently, e.g. NFS-
6954+
backed storage or nodes that are not SELinux-enforcing (Ubuntu with
6955+
AppArmor). On those clusters chcon returns "Operation not supported",
6956+
leaving the pod stuck in Init:CrashLoopBackOff.
6957+
6958+
A pointer is required so an explicit `false` survives marshaling: a plain
6959+
bool with omitempty is dropped on marshal and the API server re-defaults
6960+
it to true on every controller update (same bug class as
6961+
PersistenceSpec.Enabled and NetworkPolicySpec.Enabled).
6962+
type: boolean
69426963
type: object
69436964
selfConfigure:
69446965
description: |-

config/crd/bases/paperclip.inc_instances.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6933,6 +6933,27 @@ spec:
69336933
annotations for the ServiceAccount.
69346934
type: object
69356935
type: object
6936+
seLinuxRelabel:
6937+
default: true
6938+
description: |-
6939+
SELinuxRelabel controls whether the operator adds a privileged
6940+
"selinux-relabel" init container that runs chcon on the data volume when
6941+
persistence is enabled. This relabel is required on SELinux-enforcing
6942+
nodes so the data directory's MCS categories match the pod's SELinux
6943+
level; without it the pod cannot read its own persistent data.
6944+
6945+
It defaults to true to preserve the legacy behavior (the init container is
6946+
always added when persistence is enabled). Set it to false on clusters
6947+
where the relabel does not apply and would fail permanently, e.g. NFS-
6948+
backed storage or nodes that are not SELinux-enforcing (Ubuntu with
6949+
AppArmor). On those clusters chcon returns "Operation not supported",
6950+
leaving the pod stuck in Init:CrashLoopBackOff.
6951+
6952+
A pointer is required so an explicit `false` survives marshaling: a plain
6953+
bool with omitempty is dropped on marshal and the API server re-defaults
6954+
it to true on every controller update (same bug class as
6955+
PersistenceSpec.Enabled and NetworkPolicySpec.Enabled).
6956+
type: boolean
69366957
type: object
69376958
selfConfigure:
69386959
description: |-

docs/api-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@ _Appears in:_
10901090
| --- | --- | --- | --- |
10911091
| `podSecurityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#podsecuritycontext-v1-core)_ | PodSecurityContext specifies security settings for the pod. | | Optional: \{\} <br /> |
10921092
| `containerSecurityContext` _[SecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#securitycontext-v1-core)_ | ContainerSecurityContext specifies security settings for the Paperclip container. | | Optional: \{\} <br /> |
1093+
| `seLinuxRelabel` _boolean_ | SELinuxRelabel controls whether the operator adds a privileged<br />"selinux-relabel" init container that runs chcon on the data volume when<br />persistence is enabled. This relabel is required on SELinux-enforcing<br />nodes so the data directory's MCS categories match the pod's SELinux<br />level; without it the pod cannot read its own persistent data.<br />It defaults to true to preserve the legacy behavior (the init container is<br />always added when persistence is enabled). Set it to false on clusters<br />where the relabel does not apply and would fail permanently, e.g. NFS-<br />backed storage or nodes that are not SELinux-enforcing (Ubuntu with<br />AppArmor). On those clusters chcon returns "Operation not supported",<br />leaving the pod stuck in Init:CrashLoopBackOff.<br />A pointer is required so an explicit `false` survives marshaling: a plain<br />bool with omitempty is dropped on marshal and the API server re-defaults<br />it to true on every controller update (same bug class as<br />PersistenceSpec.Enabled and NetworkPolicySpec.Enabled). | true | Optional: \{\} <br /> |
10931094
| `networkPolicy` _[NetworkPolicySpec](#networkpolicyspec)_ | NetworkPolicy configures network isolation. | | Optional: \{\} <br /> |
10941095
| `rbac` _[RBACSpec](#rbacspec)_ | RBAC configures ServiceAccount and RBAC settings. | | Optional: \{\} <br /> |
10951096

internal/resources/common.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ func PersistenceEnabled(instance *paperclipv1alpha1.Instance) bool {
120120
return *instance.Spec.Storage.Persistence.Enabled
121121
}
122122

123+
// SELinuxRelabelEnabled resolves spec.security.seLinuxRelabel (*bool, nil =
124+
// default true). When true (or unset) and persistence is enabled the operator
125+
// adds the privileged selinux-relabel init container. An explicit false lets
126+
// operators opt out on clusters where chcon fails permanently (NFS storage or
127+
// non-SELinux-enforcing nodes).
128+
func SELinuxRelabelEnabled(instance *paperclipv1alpha1.Instance) bool {
129+
if instance.Spec.Security.SELinuxRelabel == nil {
130+
return true
131+
}
132+
return *instance.Spec.Security.SELinuxRelabel
133+
}
134+
123135
// SchedulerGatingMode resolves spec.heartbeat.schedulerGating to the mode the
124136
// operator actually applies: "ordinal" or "lease". "auto" currently resolves
125137
// to "ordinal" (it will flip to "lease" once the minimum supported app version

internal/resources/podtemplate.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,12 @@ func BuildServerPodTemplate(instance *paperclipv1alpha1.Instance, extraPodAnnota
7676
// the pod's SELinux context. Required because Kubernetes may assign MCS
7777
// categories to the volume that differ from the pod's level, making
7878
// the data inaccessible. Runs as privileged to perform chcon.
79-
if PersistenceEnabled(instance) {
79+
//
80+
// Gated on spec.security.seLinuxRelabel (default true) so operators can opt
81+
// out on clusters where chcon fails permanently, e.g. NFS-backed storage or
82+
// non-SELinux-enforcing nodes. Leaving it unset preserves the legacy
83+
// behavior for existing SELinux-enforcing clusters.
84+
if PersistenceEnabled(instance) && SELinuxRelabelEnabled(instance) {
8085
seLevel := "s0"
8186
if instance.Spec.Security.PodSecurityContext != nil &&
8287
instance.Spec.Security.PodSecurityContext.SELinuxOptions != nil &&

internal/resources/resources_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,61 @@ func findInitContainer(sts *appsv1.StatefulSet, name string) (*corev1.Container,
12271227
return nil, -1
12281228
}
12291229

1230+
func TestBuildStatefulSetSELinuxRelabelOptOut(t *testing.T) {
1231+
tests := []struct {
1232+
name string
1233+
persistence *bool
1234+
seLinuxLabel *bool
1235+
wantContainer bool
1236+
}{
1237+
{
1238+
// Legacy behavior preserved: persistence on, field unset => present.
1239+
name: "persistence enabled, seLinuxRelabel unset",
1240+
persistence: Ptr(true),
1241+
seLinuxLabel: nil,
1242+
wantContainer: true,
1243+
},
1244+
{
1245+
// Explicit opt-out: persistence on, field false => absent.
1246+
name: "persistence enabled, seLinuxRelabel false",
1247+
persistence: Ptr(true),
1248+
seLinuxLabel: Ptr(false),
1249+
wantContainer: false,
1250+
},
1251+
{
1252+
// Explicit opt-in: persistence on, field true => present.
1253+
name: "persistence enabled, seLinuxRelabel true",
1254+
persistence: Ptr(true),
1255+
seLinuxLabel: Ptr(true),
1256+
wantContainer: true,
1257+
},
1258+
{
1259+
// No PVC to relabel: persistence off => absent regardless of field.
1260+
name: "persistence disabled, seLinuxRelabel unset",
1261+
persistence: Ptr(false),
1262+
seLinuxLabel: nil,
1263+
wantContainer: false,
1264+
},
1265+
}
1266+
1267+
for _, tt := range tests {
1268+
t.Run(tt.name, func(t *testing.T) {
1269+
instance := newTestInstance("my-paperclip")
1270+
instance.Spec.Storage.Persistence.Enabled = tt.persistence
1271+
instance.Spec.Security.SELinuxRelabel = tt.seLinuxLabel
1272+
1273+
sts := BuildStatefulSet(instance, nil)
1274+
c, _ := findInitContainer(sts, "selinux-relabel")
1275+
if tt.wantContainer && c == nil {
1276+
t.Fatal("expected selinux-relabel init container to be present")
1277+
}
1278+
if !tt.wantContainer && c != nil {
1279+
t.Fatal("expected selinux-relabel init container to be absent")
1280+
}
1281+
})
1282+
}
1283+
}
1284+
12301285
func TestBuildStatefulSetNoSeedInstanceAdminWhenNil(t *testing.T) {
12311286
instance := newTestInstance("my-paperclip")
12321287
// PlatformAdmin defaults to nil.

test/e2e/e2e_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ spec:
356356
Expect(env).NotTo(ContainSubstring("PAPERCLIP_RATE_LIMIT_REDIS_URL"))
357357
Expect(env).NotTo(ContainSubstring("PAPERCLIP_MANAGED_"))
358358

359+
By("asserting the selinux-relabel init container is present (persistence on, seLinuxRelabel unset => legacy behavior)")
360+
Expect(stsInitContainerNames(instNS, "e2e-boot")).To(ContainSubstring("selinux-relabel"))
361+
359362
By("asserting no managed Redis resources were created")
360363
_, err := utils.Run(exec.Command("kubectl", "get", "statefulset", "e2e-boot-redis", "-n", instNS))
361364
Expect(err).To(HaveOccurred(), "no Redis StatefulSet should exist")
@@ -692,6 +695,15 @@ func stsEnvNames(ns, name string) string {
692695
return out
693696
}
694697

698+
// stsInitContainerNames returns the space-separated names of the StatefulSet's
699+
// init containers, used to assert presence/absence of the selinux-relabel init
700+
// container.
701+
func stsInitContainerNames(ns, name string) string {
702+
out, _ := utils.Run(exec.Command("kubectl", "get", "statefulset", name, "-n", ns,
703+
"-o", "jsonpath={.spec.template.spec.initContainers[*].name}"))
704+
return out
705+
}
706+
695707
// serviceAccountToken returns a token for the specified service account in the given namespace.
696708
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
697709
// and parsing the resulting token from the API response.

0 commit comments

Comments
 (0)