Files
PaperClipAI/docs/k8s-execution/security-model.md
T
2026-05-12 10:03:32 -05:00

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 in ServerAdapterModule)
  • 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 — agent PersistentVolumeClaim (M2)
  • /tmp — emptyDir volume 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.