# Run a root-only agent without root on the node, with user namespaces

> A legacy agent must run as root, and on the node it does. Give it a user namespace, stable in Kubernetes 1.36, so its root maps to an unprivileged user.

Level: Advanced · Topic: Security · About 15 minutes
Try it in the browser: https://k8wiz.com/playground/run-root-containers-in-a-user-namespace

The playground is a simulated Kubernetes cluster that runs in the browser. kubectl output and errors follow the real ones.

## The situation

The agent DaemonSet runs a legacy monitoring agent on every worker. It has to start as root, because it chowns its data directory, and it was set up with the node's network. Seen from the node, its process runs as root: anyone who escapes the container is root on the node.

User namespaces are stable since Kubernetes 1.36. With `hostUsers: false`, root inside the Pod maps to an unprivileged range of IDs on the node. Look at the agent from the node, move it into a user namespace, and look again.

## Objectives

1. See from the node that the agent runs as root (`kubectl debug node/worker-1 -it --image=busybox:1.37`, then `ps`)
2. Run the agent in a user namespace on every worker
3. Read the ID mapping from inside an agent Pod (`cat /proc/self/uid_map` in the container)
4. Check from the node that no agent process runs as root

## Hints

1. `kubectl debug node/worker-1 -it --image=busybox:1.37` opens a shell that shares the node's processes. `ps` there lists everything on the node, and `chroot /host ps aux` uses the node's own ps. `exit` leaves.
2. Set `hostUsers: false` in the DaemonSet's Pod template; agent.yaml in the Editor has it. The API server refuses it while `hostNetwork: true` is set, because a Pod in its own user namespace can't share the node's namespaces.
3. Don't reach for `runAsUser: 1000`: the agent can't start without root. In a user namespace it's still root, just not on the node.
4. `kubectl exec <agent Pod> -- cat /proc/self/uid_map` shows the mapping: ID 0 inside starts at a high ID on the node, for 65536 IDs. The debug Pods stay behind; delete them when you're done.

## Solution

```bash
kubectl get pods -o wide -l app=agent
```
```bash
kubectl debug node/worker-1 --image=busybox:1.37 -- ps
```
```bash
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: agent
spec:
  selector:
    matchLabels:
      app: agent
  template:
    metadata:
      labels:
        app: agent
    spec:
      hostUsers: false
      containers:
      - name: agent
        image: ghcr.io/k8wiz/legacy-agent:1.0
        ports:
        - containerPort: 80
EOF
```
```bash
kubectl rollout status daemonset agent --timeout=120s
```
```bash
kubectl exec $(kubectl get pods -l app=agent -o name | head -1) -- cat /proc/self/uid_map
```
```bash
kubectl debug node/worker-1 --image=busybox:1.37 -- ps
```

## What it teaches

- Without a user namespace, root in a container is root on the node: only the other isolation layers stand between a container escape and the host.
- `hostUsers: false` gives the Pod its own user namespace. Root inside maps to an unprivileged range on the node, 65536 IDs per Pod, while the process inside still sees itself as root.
- A Pod in its own user namespace can't use `hostNetwork`, `hostPID` or `hostIPC`; the API server rejects the combination.
- `kubectl debug node/NAME` starts a Pod that shares the node's process namespace, with the node's filesystem at /host: a quick way to see what really runs there, and as whom.
