Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,28 @@ spec:
drop: [ALL]
```

### SELinux Relabel

When persistence is enabled, the operator adds a privileged `selinux-relabel`
init container that runs `chcon` on the data volume so its MCS categories match
the pod's SELinux level. On SELinux-enforcing nodes this is required for the pod
to read its own persistent data.

On clusters where the relabel does not apply -- NFS-backed storage, or nodes
that are not SELinux-enforcing (for example Ubuntu with AppArmor) -- `chcon`
fails permanently with `Operation not supported`, leaving the pod stuck in
`Init:CrashLoopBackOff`. Opt out by setting `seLinuxRelabel: false`:

```yaml
spec:
security:
seLinuxRelabel: false # default: true (init container is added)
```

The field defaults to `true`, preserving the legacy behavior, so existing
SELinux-enforcing clusters are unaffected. Setting it to `false` skips the init
container entirely.

### RBAC and ServiceAccount

```yaml
Expand Down
21 changes: 21 additions & 0 deletions api/v1alpha1/paperclipinstance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,27 @@ type SecuritySpec struct {
// +optional
ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"`

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

// NetworkPolicy configures network isolation.
// +optional
NetworkPolicy NetworkPolicySpec `json:"networkPolicy,omitempty"`
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6939,6 +6939,27 @@ spec:
annotations for the ServiceAccount.
type: object
type: object
seLinuxRelabel:
default: true
description: |-
SELinuxRelabel controls whether the operator adds a privileged
"selinux-relabel" init container that runs chcon on the data volume when
persistence is enabled. This relabel is required on SELinux-enforcing
nodes so the data directory's MCS categories match the pod's SELinux
level; without it the pod cannot read its own persistent data.

It defaults to true to preserve the legacy behavior (the init container is
always added when persistence is enabled). Set it to false on clusters
where the relabel does not apply and would fail permanently, e.g. NFS-
backed storage or nodes that are not SELinux-enforcing (Ubuntu with
AppArmor). On those clusters chcon returns "Operation not supported",
leaving the pod stuck in Init:CrashLoopBackOff.

A pointer is required so an explicit `false` survives marshaling: a plain
bool with omitempty is dropped on marshal and the API server re-defaults
it to true on every controller update (same bug class as
PersistenceSpec.Enabled and NetworkPolicySpec.Enabled).
type: boolean
type: object
selfConfigure:
description: |-
Expand Down
21 changes: 21 additions & 0 deletions config/crd/bases/paperclip.inc_instances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6933,6 +6933,27 @@ spec:
annotations for the ServiceAccount.
type: object
type: object
seLinuxRelabel:
default: true
description: |-
SELinuxRelabel controls whether the operator adds a privileged
"selinux-relabel" init container that runs chcon on the data volume when
persistence is enabled. This relabel is required on SELinux-enforcing
nodes so the data directory's MCS categories match the pod's SELinux
level; without it the pod cannot read its own persistent data.

It defaults to true to preserve the legacy behavior (the init container is
always added when persistence is enabled). Set it to false on clusters
where the relabel does not apply and would fail permanently, e.g. NFS-
backed storage or nodes that are not SELinux-enforcing (Ubuntu with
AppArmor). On those clusters chcon returns "Operation not supported",
leaving the pod stuck in Init:CrashLoopBackOff.

A pointer is required so an explicit `false` survives marshaling: a plain
bool with omitempty is dropped on marshal and the API server re-defaults
it to true on every controller update (same bug class as
PersistenceSpec.Enabled and NetworkPolicySpec.Enabled).
type: boolean
type: object
selfConfigure:
description: |-
Expand Down
1 change: 1 addition & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,7 @@ _Appears in:_
| --- | --- | --- | --- |
| `podSecurityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#podsecuritycontext-v1-core)_ | PodSecurityContext specifies security settings for the pod. | | Optional: \{\} <br /> |
| `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 /> |
| `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 /> |
| `networkPolicy` _[NetworkPolicySpec](#networkpolicyspec)_ | NetworkPolicy configures network isolation. | | Optional: \{\} <br /> |
| `rbac` _[RBACSpec](#rbacspec)_ | RBAC configures ServiceAccount and RBAC settings. | | Optional: \{\} <br /> |

Expand Down
12 changes: 12 additions & 0 deletions internal/resources/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ func PersistenceEnabled(instance *paperclipv1alpha1.Instance) bool {
return *instance.Spec.Storage.Persistence.Enabled
}

// SELinuxRelabelEnabled resolves spec.security.seLinuxRelabel (*bool, nil =
// default true). When true (or unset) and persistence is enabled the operator
// adds the privileged selinux-relabel init container. An explicit false lets
// operators opt out on clusters where chcon fails permanently (NFS storage or
// non-SELinux-enforcing nodes).
func SELinuxRelabelEnabled(instance *paperclipv1alpha1.Instance) bool {
if instance.Spec.Security.SELinuxRelabel == nil {
return true
}
return *instance.Spec.Security.SELinuxRelabel
}

// SchedulerGatingMode resolves spec.heartbeat.schedulerGating to the mode the
// operator actually applies: "ordinal" or "lease". "auto" currently resolves
// to "ordinal" (it will flip to "lease" once the minimum supported app version
Expand Down
7 changes: 6 additions & 1 deletion internal/resources/podtemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ func BuildServerPodTemplate(instance *paperclipv1alpha1.Instance, extraPodAnnota
// the pod's SELinux context. Required because Kubernetes may assign MCS
// categories to the volume that differ from the pod's level, making
// the data inaccessible. Runs as privileged to perform chcon.
if PersistenceEnabled(instance) {
//
// Gated on spec.security.seLinuxRelabel (default true) so operators can opt
// out on clusters where chcon fails permanently, e.g. NFS-backed storage or
// non-SELinux-enforcing nodes. Leaving it unset preserves the legacy
// behavior for existing SELinux-enforcing clusters.
if PersistenceEnabled(instance) && SELinuxRelabelEnabled(instance) {
seLevel := "s0"
if instance.Spec.Security.PodSecurityContext != nil &&
instance.Spec.Security.PodSecurityContext.SELinuxOptions != nil &&
Expand Down
55 changes: 55 additions & 0 deletions internal/resources/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,61 @@ func findInitContainer(sts *appsv1.StatefulSet, name string) (*corev1.Container,
return nil, -1
}

func TestBuildStatefulSetSELinuxRelabelOptOut(t *testing.T) {
tests := []struct {
name string
persistence *bool
seLinuxLabel *bool
wantContainer bool
}{
{
// Legacy behavior preserved: persistence on, field unset => present.
name: "persistence enabled, seLinuxRelabel unset",
persistence: Ptr(true),
seLinuxLabel: nil,
wantContainer: true,
},
{
// Explicit opt-out: persistence on, field false => absent.
name: "persistence enabled, seLinuxRelabel false",
persistence: Ptr(true),
seLinuxLabel: Ptr(false),
wantContainer: false,
},
{
// Explicit opt-in: persistence on, field true => present.
name: "persistence enabled, seLinuxRelabel true",
persistence: Ptr(true),
seLinuxLabel: Ptr(true),
wantContainer: true,
},
{
// No PVC to relabel: persistence off => absent regardless of field.
name: "persistence disabled, seLinuxRelabel unset",
persistence: Ptr(false),
seLinuxLabel: nil,
wantContainer: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
instance := newTestInstance("my-paperclip")
instance.Spec.Storage.Persistence.Enabled = tt.persistence
instance.Spec.Security.SELinuxRelabel = tt.seLinuxLabel

sts := BuildStatefulSet(instance, nil)
c, _ := findInitContainer(sts, "selinux-relabel")
if tt.wantContainer && c == nil {
t.Fatal("expected selinux-relabel init container to be present")
}
if !tt.wantContainer && c != nil {
t.Fatal("expected selinux-relabel init container to be absent")
}
})
}
}

func TestBuildStatefulSetNoSeedInstanceAdminWhenNil(t *testing.T) {
instance := newTestInstance("my-paperclip")
// PlatformAdmin defaults to nil.
Expand Down
12 changes: 12 additions & 0 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ spec:
Expect(env).NotTo(ContainSubstring("PAPERCLIP_RATE_LIMIT_REDIS_URL"))
Expect(env).NotTo(ContainSubstring("PAPERCLIP_MANAGED_"))

By("asserting the selinux-relabel init container is present (persistence on, seLinuxRelabel unset => legacy behavior)")
Expect(stsInitContainerNames(instNS, "e2e-boot")).To(ContainSubstring("selinux-relabel"))

By("asserting no managed Redis resources were created")
_, err := utils.Run(exec.Command("kubectl", "get", "statefulset", "e2e-boot-redis", "-n", instNS))
Expect(err).To(HaveOccurred(), "no Redis StatefulSet should exist")
Expand Down Expand Up @@ -692,6 +695,15 @@ func stsEnvNames(ns, name string) string {
return out
}

// stsInitContainerNames returns the space-separated names of the StatefulSet's
// init containers, used to assert presence/absence of the selinux-relabel init
// container.
func stsInitContainerNames(ns, name string) string {
out, _ := utils.Run(exec.Command("kubectl", "get", "statefulset", name, "-n", ns,
"-o", "jsonpath={.spec.template.spec.initContainers[*].name}"))
return out
}

// serviceAccountToken returns a token for the specified service account in the given namespace.
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
// and parsing the resulting token from the API response.
Expand Down
Loading