# The headless browser Jarvis is allowed to use, and the policy that keeps it away from everything # else. # # WHY IT IS ITS OWN POD. A browser's entire purpose is to fetch code written by strangers and run it. # The api pod holds VAULT_MASTER_KEY, the database URL and a live connection to Postgres and Redis; # putting Chromium in it would let any page the assistant is pointed at reach all three — and reach # the node's metadata endpoint on 169.254.169.254, which is how instance credentials get read out # loud. PowerShell is in the api image and this is not, and the difference is exactly that: the # Exchange module talks to Microsoft, and a browser talks to whoever the model was told to look at. # # THE NETWORK POLICY BELOW IS THE REAL CONTROL. `browser.client.ts` refuses private addresses before # it asks, which is the half that can EXPLAIN itself to the model — but it cannot survive a redirect # and it cannot survive DNS rebinding, because by then the browser is already connecting. The policy # can, because it is enforced on the packet. # # DELETE THIS FILE (and its line in kustomization.yaml, and BROWSER_URL from 02-config.yaml) if you # would rather Jarvis had no web access. Nothing else stops working: the two web tools report # themselves unavailable and every other tool is unaffected. apiVersion: apps/v1 kind: Deployment metadata: name: jarvis-browser labels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: browser spec: # One is enough and more is not better: each replica is a Chromium, and a render is short. Scale # this only if the assistant is genuinely browsing concurrently across many conversations. replicas: 1 selector: matchLabels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: browser template: metadata: labels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: browser spec: # No service account token. This pod has no business talking to the Kubernetes API, and a # mounted token is the first thing anything escaping a renderer would look for. automountServiceAccountToken: false containers: - name: browser image: git.luxit.be/luxit/jarvis-browser:stable ports: - containerPort: 3000 name: http # No envFrom, no secretRef, no volume. Deliberately: this container should hold nothing # worth stealing, so that escaping the renderer is not the same as reaching anything. resources: requests: cpu: 100m memory: 256Mi limits: # Chromium will use whatever it is given on a heavy page. The limit is what stops one # bad page evicting the api from the same node. memory: 1Gi securityContext: allowPrivilegeEscalation: false runAsNonRoot: true # `pwuser` in the Playwright image. runAsUser: 1000 capabilities: drop: ["ALL"] seccompProfile: type: RuntimeDefault # NOT read-only: Chromium writes its profile, its cache and its shared memory. The # writable surface is an emptyDir that dies with the pod rather than the image. volumeMounts: - name: tmp mountPath: /tmp - name: home mountPath: /home/pwuser livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 20 periodSeconds: 30 readinessProbe: httpGet: path: /health port: http initialDelaySeconds: 5 periodSeconds: 10 volumes: - name: tmp emptyDir: # /dev/shm is small by default and Chromium is the classic victim of it; the server # passes --disable-dev-shm-usage and writes here instead. sizeLimit: 512Mi - name: home emptyDir: sizeLimit: 512Mi --- apiVersion: v1 kind: Service metadata: name: jarvis-browser spec: selector: app.kubernetes.io/name: jarvis app.kubernetes.io/component: browser ports: - port: 3000 targetPort: http name: http --- # WHAT THE BROWSER MAY TALK TO, and it is the file's reason for existing. # # Ingress: the api pods, and nothing else. Nothing in this namespace should be calling a browser. # # Egress: DNS, and then the internet MINUS every private range. The exception list is the whole # point — 10/8, 172.16/12 and 192.168/16 are where the cluster and the customer's estate live, and # 169.254.0.0/16 is the metadata endpoint. Without these `except` clauses a page could ask the # browser to fetch http://169.254.169.254/ and read the node's cloud credentials back as page text. # # NOTE THIS NEEDS A CNI THAT ENFORCES NetworkPolicy. Calico, Cilium and most managed clusters do; # plain kube-proxy with flannel does NOT, and on such a cluster this file is documentation rather # than a control. Check before relying on it — `browser.client.ts` still refuses private addresses # on the near side either way, but that half cannot survive a redirect. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: jarvis-browser spec: podSelector: matchLabels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: browser policyTypes: - Ingress - Egress ingress: - from: - podSelector: matchLabels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: api ports: - protocol: TCP port: 3000 egress: # DNS, to the cluster resolver. Without this nothing resolves and every page fails. - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53 # The public internet, and nothing that is not. - 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 - 127.0.0.0/8 - 100.64.0.0/10 ports: - protocol: TCP port: 80 - protocol: TCP port: 443