Last updated: April 2026
Overview
CleanStart images are hardened by default: they run as a non-root user with a read-only root filesystem. This eliminates entire classes of container escape and privilege escalation attacks. This guide explains what that means for your deployments and how to configure writable paths for your application.
Non-Root Execution
Default User
All CleanStart production images run as:
Field | Value |
|---|---|
UID | 65532 |
GID | 65532 |
Username |
|
Group |
|
This is enforced at both the image level (OCI User config) and in generated Kubernetes manifests:
securityContext: runAsUser: 65532 runAsGroup: 65532 fsGroup: 65532 runAsNonRoot: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"]Custom Users
If your application requires a specific UID/GID, define it in the spec's accounts section:
accounts: - username: postgres uid: 999 gid: 999The generated image and Kubernetes manifests will use the specified user.
Migrating from Root-Based Images
If you are migrating from images that run as root (UID 0) or from Bitnami (UID 1001), you may need to fix permissions on existing persistent volumes:
initContainers: - name: fix-permissions image: gcr.io/clean-image-build/base:latest securityContext: runAsUser: 0 # initContainer runs as root to fix ownership command: ["chown", "-R", "65532:65532", "/data"] volumeMounts: - name: data mountPath: /dataNote: chown is not a cleanimg-init builtin. This initContainer uses the base image's chown binary directly, running as root to change ownership before the main container starts as UID 65532.
--- ## Read-Only Root Filesystem ### What It Means The container's root filesystem is mounted read-only. Your application cannot write to `/etc`, `/usr`, `/var`, or any other path unless you explicitly configure a writable volume. This prevents attackers from modifying binaries, injecting configuration, or writing malware to the filesystem even if they compromise the application process. ### How It's Enforced CleanStart sets the OCI image label:dev.cleanstart.kubernetes.readOnlyRootFilesystem: "true"
Generated Kubernetes manifests include: ```yamlsecurityContext: readOnlyRootFilesystem: trueConfiguring Writable Paths
Applications need to write to certain directories (logs, temp files, caches, data). CleanStart supports two approaches:
Image-Level Writable Paths
Declare writable paths in your spec file. These directories are created during the build with correct ownership (matching the configured UID/GID):
deployment: profile: production writable_paths: - /data - /var/log - /tmpAt runtime, these directories exist in the image but still require volume mounts to be writable under readOnlyRootFilesystem: true.
Kubernetes emptyDir Volumes
Generated Helm charts and manifests automatically mount emptyDir volumes for declared writable paths:
volumes: - name: data emptyDir: sizeLimit: "1Gi" - name: tmp emptyDir: sizeLimit: "64Mi" medium: "Memory" # tmpfs for /tmp volumeMounts: - name: data mountPath: /data - name: tmp mountPath: /tmpFor applications that need persistent storage, replace emptyDir with a PersistentVolumeClaim. Generated StatefulSet manifests do this automatically when persistence patterns are detected.
Common Writable Paths by Application
Application Type | Typical Writable Paths |
|---|---|
Web servers |
|
Databases |
|
Message brokers |
|
Application servers |
|
Pod Security Standards
CleanStart images are compatible with Kubernetes Pod Security Standards at the Restricted level:
Requirement | CleanStart Default |
|---|---|
Running as non-root | Yes (UID 65532) |
| Yes |
Privilege escalation disabled | Yes |
All capabilities dropped | Yes |
Read-only root filesystem | Yes |
Seccomp profile | RuntimeDefault |
| true (enables |
You can deploy CleanStart images in namespaces with the strictest Pod Security Admission policies without modification.
Debugging with Read-Only Filesystem
Since the production filesystem is read-only and contains no shell, use kubectl debug with the debug variant image:
kubectl debug -it <pod-name> \ --image=gcr.io/clean-image-build/redis:8.2.2-debugThe debug container shares the pod's process namespace (via shareProcessNamespace: true) so you can inspect the production process, capture dumps, and run diagnostics. See Debug Guide for details.
Troubleshooting
"Permission denied" writing files -- The path is either on the read-only root filesystem or owned by a different UID. Add an emptyDir volume mount for that path, or verify the volume's fsGroup matches the container's GID.
Persistent volume has wrong ownership -- Use an initContainer running as root to chown the volume before the main container starts. See the migration example above.
Application expects to run as root -- CleanStart images enforce runAsNonRoot: true. If your application genuinely requires root (e.g., binding to port 80), either reconfigure it to use a high port or use NET_BIND_SERVICE capability (contact CleanStart support for guidance).
Pod rejected by admission controller -- CleanStart images comply with Restricted Pod Security Standards by default. If your admission controller is rejecting pods, check that you haven't added custom settings that violate the policy (e.g., adding capabilities back).
