Allow UPF to run without privileged/root - #17
Merged
Conversation
Add an opt-in spec.upf.unprivileged field, default false with no change in behavior unless set. When enabled, an init container (root, not privileged, NET_ADMIN only) creates a persistent TUN device owned by the main container's UID, sets its address, and adds the NAT rule. The main container runs fully non-root with no added capabilities and mounts the same device via a hostPath volume, both containers sharing the pod's network namespace. Kubernetes does not set ambient or inheritable capabilities for non-root containers, so a plain capabilities.add on a non-root container is not effective - only root gets its capability set applied automatically via the bounding set. Root is therefore kept, but only for the short-lived init step, and never combined with privileged: true. net.ipv4.ip_forward is set via the pod's declarative securityContext.sysctls field rather than an in-container sysctl -w, since /proc/sys stays read-only inside a container regardless of capabilities or UID - this requires the cluster's kubelet and the bound SCC to both allow the sysctl.
The image's default entrypoint (invoked whenever no Command is set, since Args just carries "open5gs-upfd") redoes the same TUN/NAT/sysctl setup unconditionally on every start, in addition to what the init container already did. That only works today because the main container already runs privileged; it defeats the point of the non-root, zero-capability main container the unprivileged mode is meant to give. Set Command to invoke /opt/open5gs/bin/open5gs-upfd directly with its config path, skipping that wrapper. The main container then does nothing beyond opening the already-configured TUN device, which needs no capability at all - only creating a TUN device does. Also leave seLinuxOptions unset in the pod/container spec for the unprivileged mode. SELinux-enforcing nodes (RHCOS/OpenShift) need type spc_t to access /dev/net/tun - the default container_t domain has no policy allowing it - but the matching level is namespace-specific and can't be hardcoded here; setting only type without it fails SCC validation. This is left entirely to the bound SCC's own seLinuxContext default instead.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in unprivileged mode for the UPF component so it can run without privileged: true and without a root main container, aligning better with restricted platforms (e.g., OpenShift) while preserving the current default behavior.
Changes:
- Introduces
spec.*.unprivileged(intended forspec.upf.unprivileged) and wires it into UPF reconciliation/defaulting. - Updates UPF rendered resources to support an unprivileged mode (root +
NET_ADMINinit container, non-root main container, explicit/dev/net/tunhostPath, pod sysctl fornet.ipv4.ip_forward). - Adds unit tests for privileged vs unprivileged UPF rendering and entrypoint script differences.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
internal/controller/open5gs_resources.go |
Adds conditional UPF entrypoint script rendering and conditional UPF Deployment security/volume/sysctl behavior for unprivileged mode. |
internal/controller/open5gs_resources_upf_test.go |
Adds unit tests covering unprivileged rendering and validating key default-path invariants. |
internal/controller/open5gs_controller.go |
Wires unprivileged flag from the CR into UPF resource rendering and sets a default value. |
api/v1/open5gs_types.go |
Adds Unprivileged field to the API type used by component specs. |
api/v1/zz_generated.deepcopy.go |
Updates deepcopy generation for the new Unprivileged field. |
config/crd/bases/net.gradiant.org_open5gses.yaml |
Extends CRD schema with unprivileged (currently replicated across all component blocks). |
charts/open5gs-operator/templates/open5gs-crd.yaml |
Mirrors the CRD schema updates in the Helm chart template. |
Files not reviewed (1)
- api/v1/zz_generated.deepcopy.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+56
to
+57
| // Unprivileged runs the UPF without privileged:true/root (UPF only). | ||
| Unprivileged *bool `json:"unprivileged,omitempty" default:"false"` |
Comment on lines
+67
to
+70
| unprivileged: | ||
| description: Unprivileged runs the UPF without privileged:true/root | ||
| (UPF only). | ||
| type: boolean |
Comment on lines
+68
to
+71
| unprivileged: | ||
| description: Unprivileged runs the UPF without privileged:true/root | ||
| (UPF only). | ||
| type: boolean |
| corev1 "k8s.io/api/core/v1" | ||
| ) | ||
|
|
||
| func TestCreateUPFDeploymentDefaultUnchanged(t *testing.T) { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The UPF Deployment and its init container currently hardcode privileged: true + runAsUser: 0 with no way to override. This blocks running the operator on clusters/platforms that don't grant the privileged SCC by default — notably OpenShift, where that's a much bigger ask than the anyuid-style SCC the other Open5GS components already work under. This MR adds an opt-in
spec.upf.unprivilegedboolean field, default false — no behavior change for existing users unless they set it.Design
/dev/net/tunvia an explicit hostPath volume (type: CharDevice) — privileged: true's only real effect here was auto-mounting the whole host /dev./proc/sysstays read-only inside a container regardless of capabilities or UID. This requires the cluster's kubelet to allow it (--allowed-unsafe-sysctls) and, on OpenShift, the bound SCC too — a real platform prerequisite, not something this change can work around.spc_tfor/dev/net/tunaccess (the default container_t domain has no policy for it), but the matching level is namespace-specific and can't be hardcoded — this is left to the bound SCC's own default instead.Kubernetes does not set ambient/inheritable capabilities for non-root containers, so a plain capabilities.add on a non-root container isn't effective — only root gets its capability set applied automatically via the bounding set. That's why the init container stays root, but only for that short-lived step, never combined with privileged: true.
Checked against upstream open5gs's own
docker/docker-compose.yml: it runs the exact same open5gs-upfd binary with only cap_add: [NET_ADMIN] and a/dev/net/tundevice mount — no privileged anywhere. This brings the operator in line with that.Testing
internal/controller/open5gs_resources_upf_test.go) covering both the unprivileged rendering and that the default (unset) path is byte-for-byte unchanged from before this change.Compatibility
Fully additive/opt-in. No behavior change for anyone not setting spec.upf.unprivileged: true.