11 KiB
title, summary
| title | summary |
|---|---|
| Kubernetes Execution — Security Model | Isolation primitives applied to every tenant namespace — NetworkPolicy, PodSecurity, RBAC, ResourceQuota, and compliance posture |
Every company that runs agents on a Kubernetes cluster gets an isolated namespace with a layered set of controls. This document describes each layer, why it exists, and how to verify it. The spec section that defines these primitives is §2 of the design spec.
M1 scope: These controls are provisioned by cluster ensure-tenant. Agent pods that enforce them (PodSecurity Restricted, NetworkPolicy enforcement during actual runs) are an M2 deliverable. The namespace is fully hardened at provision time; the hardening is exercised at run time.
Tenancy boundary: one Namespace per company
Every Paperclip company maps to exactly one namespace per cluster connection, named paperclip-{companySlug}. This is the primary isolation boundary: Kubernetes RBAC, ResourceQuota, NetworkPolicy, and PodSecurityAdmission all attach at the namespace level.
Naming rules (from spec §2.1):
- Primary:
paperclip-{companySlug}(companySlug truncated to 53 chars so the total stays ≤ 63) - Fallback:
paperclip-{companySlug}-{base36(blake3(companyId))[:8]}on DNS-1123 overflow or slug collision
The immutable machine identifier is the paperclip.ai/company-id=<uuid> label on the Namespace object, not the namespace name.
The driver refuses to manage any namespace that does not carry paperclip.ai/managed-by=paperclip. This prevents accidental mutation of pre-existing namespaces.
Pod identity: zero-trust by default
Each tenant namespace contains a ServiceAccount named paperclip-agent with:
automountServiceAccountToken: false
This ServiceAccount has no RBAC bindings by default. A pod running as paperclip-agent cannot call the Kubernetes API at all. The driver's own identity (the paperclip-driver ServiceAccount in paperclip-system) is separate and holds the ClusterRole described in cluster-rbac.md.
Disabling automountServiceAccountToken removes the projected token from the pod filesystem, eliminating a common privilege escalation path even if the service account later gains bindings.
NetworkPolicy: default-deny + allowlist
Three NetworkPolicy objects are applied to every tenant namespace.
default-deny-ingress
podSelector: {}
policyTypes: [Ingress]
Blocks all ingress to every pod in the namespace. Agent pods do not accept inbound connections.
default-deny-egress
podSelector: {}
policyTypes: [Egress]
Blocks all egress by default. Only pods that match the paperclip-agent-egress allowlist policy can make outbound connections.
paperclip-agent-egress
Applied only to pods labeled paperclip.ai/role: agent-runtime. Three egress rules, in order:
1. Cluster DNS
to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports: [{ port: 53, protocol: UDP }, { port: 53, protocol: TCP }]
Agents need name resolution to reach the Paperclip server and external APIs.
2. In-cluster Paperclip server (in-cluster topology only)
to:
- namespaceSelector:
matchLabels: { paperclip.ai/role: control-plane }
podSelector:
matchLabels: { app.kubernetes.io/name: paperclip-server }
ports: [{ port: 443, protocol: TCP }, { port: 3102, protocol: TCP }]
Agents call back to the Paperclip server to exchange the bootstrap token for a run JWT. This rule is omitted for cross-cluster topologies where the server is not in the same cluster.
3. Internet egress with internal ranges denied
to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 169.254.0.0/16 # link-local — cloud metadata service
- 100.64.0.0/10 # CGNAT
- ipBlock:
cidr: ::/0
except:
- fd00::/8 # IPv6 ULA
ports: [{ port: 443, protocol: TCP }]
Why each block matters:
| CIDR | What it blocks | Why it matters |
|---|---|---|
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 |
RFC 1918 private ranges | Cluster-internal databases, caches, other services |
169.254.0.0/16 |
Link-local (incl. 169.254.169.254) |
Cloud metadata service — this is the primary SSRF target on AWS/GCP/Azure. A compromised pod that can reach the metadata service can obtain instance credentials and escape the tenant boundary |
100.64.0.0/10 |
CGNAT / shared address space | Often used by cloud providers for internal routing; blocks a secondary metadata path on some platforms |
fd00::/8 |
IPv6 ULA | Internal IPv6 ranges; same threat model as RFC 1918. Must be in a separate ipBlock entry because the Kubernetes API rejects IPv6 ranges inside an IPv4 cidr |
The IPv4 and IPv6 deny blocks are intentionally separate ipBlock entries. The Kubernetes NetworkPolicy API requires each except entry to be a strict subset of its parent cidr; mixing IPv6 ranges inside 0.0.0.0/0 is rejected with a 422 error.
Cilium variant: FQDN allowlist (auto-detected)
When probeClusterCapabilities detects the cilium.io/v2 API group, the driver also applies a CiliumNetworkPolicy alongside the vanilla NetworkPolicy. The vanilla policy stays as defense-in-depth.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: paperclip-agent-egress-l7
spec:
endpointSelector:
matchLabels: { paperclip.ai/role: agent-runtime }
egress:
- toFQDNs:
- matchPattern: "*.anthropic.com"
- matchPattern: "api.openai.com"
- matchPattern: "*.googleapis.com"
- matchPattern: "github.com"
- matchPattern: "*.github.com"
- matchPattern: "gitlab.com"
toPorts:
- ports: [{ port: "443", protocol: TCP }]
- toEndpoints:
- matchLabels: { paperclip.ai/role: control-plane }
toPorts:
- ports: [{ port: "443", protocol: TCP }]
The FQDN list is composed from two sources:
adapter.networkRequirements.allowFqdns(declared per adapter type inServerAdapterModule)cluster_tenant_policies.network_json.additionalAllowFqdns(per-tenant override, described in multi-tenant-onboarding.md)
Cilium detection happens automatically at cluster add time. Re-run cluster doctor <id> after installing Cilium to update the stored capabilities; then re-run cluster ensure-tenant to apply the CNP.
PodSecurity Restricted
The namespace is labeled at provision time:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Any pod admitted to the namespace must conform to the restricted PodSecurity Standard. Agent pods (M2) are built to satisfy it:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile: { type: RuntimeDefault }
containers:
- securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
readOnlyRootFilesystem: true is enforced. Writable paths are:
/workspace— agentPersistentVolumeClaim(M2)/tmp—emptyDirvolume capped at 1 Gi (M2)
An attempt to kubectl run a privileged pod into a Paperclip namespace will be rejected by the admission controller — this is the expected behavior and confirms PSS is working. See multi-tenant-onboarding.md for how to verify this.
ResourceQuota and LimitRange
Default values (can be overridden per tenant via cluster_tenant_policies — see multi-tenant-onboarding.md):
ResourceQuota paperclip-tenant-quota
hard:
requests.cpu: "16"
requests.memory: "64Gi"
limits.cpu: "64"
limits.memory: "256Gi"
requests.storage: "200Gi"
count/jobs.batch: "100"
count/persistentvolumeclaims: "50"
count/secrets: "200"
count/configmaps: "200"
LimitRange paperclip-tenant-limits
limits:
- type: Container
default: { cpu: "1", memory: "2Gi" }
defaultRequest: { cpu: "250m", memory: "512Mi" }
max: { cpu: "8", memory: "32Gi" }
- type: PersistentVolumeClaim
max: { storage: "20Gi" }
The LimitRange ensures that pods without explicit resource requests/limits still get sensible defaults and cannot consume unbounded resources. Without it, a misconfigured pod could exhaust node capacity and starve other tenants.
Image pull secret model
If the cluster connection has an associated image registry and the imagePullDockerConfigJson is resolved at provision time, the driver creates a Secret of type kubernetes.io/dockerconfigjson in the tenant namespace:
Secret name: paperclip-image-pull
Secret type: kubernetes.io/dockerconfigjson
This secret is referenced by agent pod specs (M2) as an imagePullSecret. Credentials are per-namespace and are not shared across tenants. The secret value is resolved from the Paperclip secret provider at ensure-tenant time.
Secret-resolver M1 gap
The secret provider abstraction supports aws_secrets, gcp_secret, vault, and env in M1. The local_encrypted provider (Paperclip-managed encrypted store) is planned for M2.
Operators running a self-hosted Paperclip instance without access to a cloud secret manager should use env:<VAR_NAME> for the kubeconfig secret during M1.
Compliance bookkeeping
The M1 provisioning pipeline is aligned with:
- NSA/CISA Kubernetes Hardening Guidance — Restricted PSS, NetworkPolicy default-deny, no privilege escalation, no host network/PID/IPC, drop ALL capabilities, RuntimeDefault seccomp
- CIS Kubernetes Benchmark — namespace isolation, ResourceQuota enforcement, no automounted ServiceAccount tokens
The CI release pipeline runs kube-audit-kit and polaris against a freshly provisioned tenant namespace on every build. PSS Restricted violations or NSA Hardening regressions block the release.
Related
- Quickstart — set up a cluster connection and run
ensure-tenant - Cluster RBAC — the driver ClusterRole and binding templates
- Multi-tenant onboarding — playbook for provisioning multiple companies and handling edge cases
- Design spec §2