Sync the self-hosting stack

This commit is contained in:
2026-08-31 21:43:38 +02:00
parent 70db57d367
commit fdd35f2d27
5 changed files with 352 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
# 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