Skip to content

Commit 7e5b87a

Browse files
stubbiclaude
andauthored
fix: apply CRD security context override to all Paperclip containers (#46)
fix: apply CRD security context override to onboard and bootstrap containers The onboard init container and bootstrap job had hardcoded security contexts that ignored the CRD's security.containerSecurityContext override. This meant users could not customize security settings for these containers, which is needed on clusters where the Paperclip image entrypoint requires different privileges. Extracts a shared paperclipContainerSecurityContext() helper used by all three Paperclip-image containers (main, onboard, bootstrap) so the CRD override applies consistently. Ref #45 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 94fc4a0 commit 7e5b87a

4 files changed

Lines changed: 124 additions & 34 deletions

File tree

internal/resources/bootstrap.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,7 @@ echo "Admin bootstrap finished successfully."
188188
ImagePullPolicy: imagePullPolicy(instance),
189189
Command: []string{"/bin/sh", "-c"},
190190
Args: []string{script},
191-
SecurityContext: &corev1.SecurityContext{
192-
AllowPrivilegeEscalation: Ptr(false),
193-
RunAsNonRoot: Ptr(true),
194-
SeccompProfile: &corev1.SeccompProfile{
195-
Type: corev1.SeccompProfileTypeRuntimeDefault,
196-
},
197-
Capabilities: &corev1.Capabilities{
198-
Drop: []corev1.Capability{"ALL"},
199-
},
200-
},
191+
SecurityContext: paperclipContainerSecurityContext(instance),
201192
Env: append(buildEnvVars(instance),
202193
corev1.EnvVar{
203194
Name: "ADMIN_EMAIL",

internal/resources/common.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package resources
22

33
import (
4+
corev1 "k8s.io/api/core/v1"
45
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
56

67
paperclipv1alpha1 "github.com/paperclipinc/paperclip-operator/api/v1alpha1"
@@ -199,3 +200,22 @@ func DatabaseSecretName(instance *paperclipv1alpha1.Instance) string {
199200
func SecretsMasterKeySecretName(instance *paperclipv1alpha1.Instance) string {
200201
return instance.Name + "-secrets-master-key"
201202
}
203+
204+
// paperclipContainerSecurityContext returns the security context for containers
205+
// running the Paperclip image. If the user has provided a custom security context
206+
// via the CRD, it is used; otherwise the restricted-PSS-compliant default is returned.
207+
func paperclipContainerSecurityContext(instance *paperclipv1alpha1.Instance) *corev1.SecurityContext {
208+
if instance.Spec.Security.ContainerSecurityContext != nil {
209+
return instance.Spec.Security.ContainerSecurityContext
210+
}
211+
return &corev1.SecurityContext{
212+
AllowPrivilegeEscalation: Ptr(false),
213+
RunAsNonRoot: Ptr(true),
214+
SeccompProfile: &corev1.SeccompProfile{
215+
Type: corev1.SeccompProfileTypeRuntimeDefault,
216+
},
217+
Capabilities: &corev1.Capabilities{
218+
Drop: []corev1.Capability{"ALL"},
219+
},
220+
}
221+
}

internal/resources/resources_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,3 +1387,101 @@ func TestNamingConventions(t *testing.T) {
13871387
})
13881388
}
13891389
}
1390+
1391+
func TestContainerSecurityContextOverrideAppliesToAllPaperclipContainers(t *testing.T) {
1392+
instance := newTestInstance("my-paperclip")
1393+
instance.Spec.Security.ContainerSecurityContext = &corev1.SecurityContext{
1394+
AllowPrivilegeEscalation: Ptr(false),
1395+
RunAsNonRoot: Ptr(false),
1396+
RunAsUser: Ptr(int64(0)),
1397+
SeccompProfile: &corev1.SeccompProfile{
1398+
Type: corev1.SeccompProfileTypeRuntimeDefault,
1399+
},
1400+
Capabilities: &corev1.Capabilities{
1401+
Drop: []corev1.Capability{"ALL"},
1402+
Add: []corev1.Capability{"SETUID", "SETGID"},
1403+
},
1404+
}
1405+
1406+
// Main container
1407+
sts := BuildStatefulSet(instance, nil)
1408+
mainContainer := sts.Spec.Template.Spec.Containers[0]
1409+
if *mainContainer.SecurityContext.RunAsNonRoot != false {
1410+
t.Error("main container: expected RunAsNonRoot=false from CRD override")
1411+
}
1412+
if *mainContainer.SecurityContext.RunAsUser != 0 {
1413+
t.Error("main container: expected RunAsUser=0 from CRD override")
1414+
}
1415+
1416+
// Onboard init container - should also use the CRD override
1417+
var onboard *corev1.Container
1418+
for i := range sts.Spec.Template.Spec.InitContainers {
1419+
if sts.Spec.Template.Spec.InitContainers[i].Name == "onboard" {
1420+
onboard = &sts.Spec.Template.Spec.InitContainers[i]
1421+
break
1422+
}
1423+
}
1424+
if onboard == nil {
1425+
t.Fatal("expected onboard init container")
1426+
}
1427+
if *onboard.SecurityContext.RunAsNonRoot != false {
1428+
t.Error("onboard: expected RunAsNonRoot=false from CRD override")
1429+
}
1430+
1431+
// Bootstrap job
1432+
instance.Spec.Auth.AdminUser = &paperclipv1alpha1.AdminUserSpec{
1433+
Email: "admin@test.com",
1434+
PasswordSecretRef: corev1.SecretKeySelector{
1435+
LocalObjectReference: corev1.LocalObjectReference{Name: "admin-secret"},
1436+
Key: "password",
1437+
},
1438+
}
1439+
job := BuildBootstrapJob(instance)
1440+
bootstrapContainer := job.Spec.Template.Spec.Containers[0]
1441+
if *bootstrapContainer.SecurityContext.RunAsNonRoot != false {
1442+
t.Error("bootstrap: expected RunAsNonRoot=false from CRD override")
1443+
}
1444+
}
1445+
1446+
func TestDefaultSecurityContextOnOnboardAndBootstrap(t *testing.T) {
1447+
instance := newTestInstance("my-paperclip")
1448+
// No CRD override - should get restricted PSS defaults
1449+
1450+
sts := BuildStatefulSet(instance, nil)
1451+
var onboard *corev1.Container
1452+
for i := range sts.Spec.Template.Spec.InitContainers {
1453+
if sts.Spec.Template.Spec.InitContainers[i].Name == "onboard" {
1454+
onboard = &sts.Spec.Template.Spec.InitContainers[i]
1455+
break
1456+
}
1457+
}
1458+
if onboard == nil {
1459+
t.Fatal("expected onboard init container")
1460+
}
1461+
sc := onboard.SecurityContext
1462+
if *sc.RunAsNonRoot != true {
1463+
t.Error("onboard: expected default RunAsNonRoot=true")
1464+
}
1465+
if *sc.AllowPrivilegeEscalation != false {
1466+
t.Error("onboard: expected default AllowPrivilegeEscalation=false")
1467+
}
1468+
if sc.Capabilities == nil || sc.Capabilities.Drop[0] != "ALL" {
1469+
t.Error("onboard: expected default drop ALL capabilities")
1470+
}
1471+
1472+
instance.Spec.Auth.AdminUser = &paperclipv1alpha1.AdminUserSpec{
1473+
Email: "admin@test.com",
1474+
PasswordSecretRef: corev1.SecretKeySelector{
1475+
LocalObjectReference: corev1.LocalObjectReference{Name: "admin-secret"},
1476+
Key: "password",
1477+
},
1478+
}
1479+
job := BuildBootstrapJob(instance)
1480+
bsc := job.Spec.Template.Spec.Containers[0].SecurityContext
1481+
if *bsc.RunAsNonRoot != true {
1482+
t.Error("bootstrap: expected default RunAsNonRoot=true")
1483+
}
1484+
if *bsc.AllowPrivilegeEscalation != false {
1485+
t.Error("bootstrap: expected default AllowPrivilegeEscalation=false")
1486+
}
1487+
}

internal/resources/statefulset.go

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -165,20 +165,10 @@ func buildMainContainer(instance *paperclipv1alpha1.Instance) corev1.Container {
165165
}
166166

167167
// Container security context
168-
if instance.Spec.Security.ContainerSecurityContext != nil {
169-
container.SecurityContext = instance.Spec.Security.ContainerSecurityContext
170-
} else {
171-
container.SecurityContext = &corev1.SecurityContext{
172-
AllowPrivilegeEscalation: Ptr(false),
173-
ReadOnlyRootFilesystem: Ptr(false), // Paperclip needs writable filesystem for node_modules, etc.
174-
RunAsNonRoot: Ptr(true),
175-
SeccompProfile: &corev1.SeccompProfile{
176-
Type: corev1.SeccompProfileTypeRuntimeDefault,
177-
},
178-
Capabilities: &corev1.Capabilities{
179-
Drop: []corev1.Capability{"ALL"},
180-
},
181-
}
168+
container.SecurityContext = paperclipContainerSecurityContext(instance)
169+
if container.SecurityContext.ReadOnlyRootFilesystem == nil {
170+
container.SecurityContext = container.SecurityContext.DeepCopy()
171+
container.SecurityContext.ReadOnlyRootFilesystem = Ptr(false) // Paperclip needs writable filesystem for node_modules, etc.
182172
}
183173

184174
// Multi-replica heartbeat gating: only pod-0 runs the scheduler.
@@ -845,16 +835,7 @@ exit 1
845835
Env: buildEnvVars(instance),
846836
EnvFrom: instance.Spec.EnvFrom,
847837
VolumeMounts: buildVolumeMounts(instance),
848-
SecurityContext: &corev1.SecurityContext{
849-
AllowPrivilegeEscalation: Ptr(false),
850-
RunAsNonRoot: Ptr(true),
851-
SeccompProfile: &corev1.SeccompProfile{
852-
Type: corev1.SeccompProfileTypeRuntimeDefault,
853-
},
854-
Capabilities: &corev1.Capabilities{
855-
Drop: []corev1.Capability{"ALL"},
856-
},
857-
},
838+
SecurityContext: paperclipContainerSecurityContext(instance),
858839
}
859840
}
860841

0 commit comments

Comments
 (0)