1
0
mirror of https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git synced 2026-11-03 09:23:25 +00:00
Files
deepseek-kubernetes-sandbox/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml
Nicolás Georger 8e9bd478b4 chore: improve package installation and add Python depend… (#42)
* chore(dockerfile): improve package installation and add Python dependencies

* chore(dockerfile): update package installation commands for consistency

* chore: update release version to dsh-v0.2.1-rc.5-r6 across documentation and configuration files
2026-10-31 06:25:39 -03:00

220 lines
12 KiB
YAML

# The reusable DSH Web blueprint. Warm pools stamp pod env BEFORE any user
# exists, so per-user secrets cannot live in pod env; the shared provider keys
# are injected via envFrom, and claims that would bypass the warm pool are
# rejected outright (see 40-claim-rejected.yaml).
#
# A stripped-down single-tenant variant of this template (no warm pool, no
# policy enforcement) lives in examples/minimal-single-tenant/.
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxTemplate
metadata:
name: dsh-web
namespace: dsh-demo
spec:
# Claims must not inject env or volumeClaimTemplates: with Disallowed, such
# claims are REJECTED instead of silently taking the cold (non-warm-pool)
# provisioning path. Demonstrated in step 6.
envVarsInjectionPolicy: Disallowed
volumeClaimTemplatesPolicy: Disallowed
# Per-sandbox headless Service -> stable DNS (sandbox.status.serviceFQDN).
# The gateway routes each user to their DSH Web instance by this name.
service: true
# The example leaves NetworkPolicy to an optional, opt-in manifest
# (manifests/optional/network-policy/): the base walkthrough should not
# require a policy-capable CNI, and the controller's Managed secure default
# only allows the sandbox-router, which this example does not deploy. If you
# switch to Managed, define spec.networkPolicy explicitly or the gateway will
# be blocked. See docs/optional-hardening.md.
networkPolicyManagement: Unmanaged
# OPTIONAL: replace a Failed sandbox Pod instead of parking the sandbox.
# The default, Retain, leaves the Failed Pod in place and surfaces
# Finished=True on the Sandbox until a human intervenes. Recreate deletes the
# Failed Pod and starts a replacement with the same Sandbox identity and
# Sandbox-owned PVCs, backing off (5s doubling to 5m) on repeated failures.
# Requires agent-sandbox v1.0.8+; existing Sandboxes keep their own stamped
# policy, so patch them separately. See docs/optional-hardening.md.
# failedPodPolicy: Recreate
podTemplate:
spec:
# DSH Web is multi-arch (amd64 + arm64); no arch nodeSelector is needed.
#
# OPTIONAL: run the sandbox under gVisor. Requires a RuntimeClass named
# "gvisor" and nodes that can run it; gVisor breaks `kubectl port-forward`
# into the sandbox (use the gateway). Existing Sandboxes snapshot their
# pod template, so enabling it later requires patching/recreating them.
# See docs/optional-hardening.md.
# runtimeClassName: gvisor
#
# OPTIONAL: pin sandboxes to a dedicated node pool (for example a gVisor
# pool). Replace this placeholder with a label your nodes actually carry;
# add a matching toleration if the pool is tainted. Active
# cluster-specific scheduling belongs in a local patch, not here.
# nodeSelector:
# dsh.example.com/node-pool: dsh
restartPolicy: "OnFailure"
securityContext:
runAsUser: 1000 # the 'node' user in the DSH image
runAsGroup: 1000
fsGroup: 1000
runAsNonRoot: true
# DSH's credentials plugin refuses to start when .credentials.yaml has
# any group/other permission bit (it creates the file 0600). The
# default kubelet fsGroup policy ("Always") recursively ORs 0660 into
# every file each time the pod starts, so a resumed sandbox comes back
# with 0660 and crash-loops before the gateway can proxy to it.
# OnRootMismatch skips that walk once the volume root already carries
# the fsGroup ownership/mode applied on the first mount.
# Keep in sync with the minimal variant:
# examples/minimal-single-tenant/manifests/10-sandboxtemplate.yaml.
fsGroupChangePolicy: OnRootMismatch
containers:
- name: dsh-web
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
imagePullPolicy: IfNotPresent
ports:
- name: web
containerPort: 3080
envFrom:
- secretRef:
name: dsh-provider-keys
# Optional extra provider keys (OpenRouter, Gemini, NVIDIA, ...).
# The Secret may be absent; every key in it becomes an env var.
# Create it before the warm pool, because pod env is stamped at
# pod creation. See docs/optional-hardening.md.
- secretRef:
name: dsh-provider-keys-extra
optional: true
env:
# Point the OS home at the persisted agent workspace. DSH's
# directory picker (the folder explorer used to choose a workspace)
# has no start-path setting: when it opens it lists $HOME, and
# os.homedir() follows this variable. The image default is
# /home/node, which is not on a volume, so the picker would open on
# a throwaway directory; /workspace is the PVC-backed WORKDIR. `~`
# in agent commands resolves here too. Trade-off: tools that keep
# caches/config under $HOME (npm/pnpm, uv, git, gh, and DSH's global
# ~/.agents instructions and skills) now write dotfiles onto the PVC
# inside the agent workspace, so they are visible to the agent and
# in the file explorer. DSH_HOME is explicit (/home/node/.dsh), so
# sessions, config and credentials are unaffected. Existing
# sandboxes stamp pod env at creation and keep the old HOME until
# recreated. Keep in sync with the minimal variant:
# examples/minimal-single-tenant/manifests/10-sandboxtemplate.yaml.
- name: HOME
value: /workspace
# Empty => DSH derives links from the request Host, which the
# gateway forwards (plus X-Forwarded-*). Point this at the
# gateway base URL if you want hard-coded absolute links instead.
- name: DSH_PUBLIC_URL
value: ""
# DSH Host/Origin-fences /api. The gateway forwards the browser's
# Host, so DSH must trust the public gateway host. Empty is right
# for port-forward/localhost; for either exposure variant set this
# to the public host (e.g. dsh.example.com for
# manifests/exposure/ingress/60-ingress.yaml, or
# dsh-demo.example.com for the gateway-api-sso/ HTTPRoute).
- name: DSH_TRUSTED_HOSTS
value: ""
# DSH confines every bash command with its own file sandbox
# (bubblewrap, else Landlock). Neither backend is usable inside this
# pod: bubblewrap ships in the image but is unsetuid, so it needs an
# unprivileged user namespace that the pod's RuntimeDefault seccomp
# and dropped capabilities do not grant, and the runtime/kernel
# surface here exposes no Landlock runner. The executor therefore
# fails closed with SANDBOX_UNAVAILABLE and every command fails. The
# pod is the isolation boundary in this example (one pod
# per user, non-root, capabilities dropped, seccomp RuntimeDefault,
# a private PVC), so trade DSH's inner file policy for a working
# agent. Set this to workspace-write (or read-only) on hosts where a
# backend is available; see the README troubleshooting row.
# SECURITY: the pod is the isolation boundary in this example.
# danger-full-access is set only because DSH's own command sandbox
# has no usable backend in this pod (see above), which means an
# agent command can read its own DSH_HOME (including
# .credentials.yaml) and write anywhere in the container. What
# still contains it is the pod itself: one pod per user, non-root,
# all capabilities dropped, RuntimeDefault seccomp, its own PVC and
# Service. That is NOT a hardened multi-tenant boundary on a shared
# kernel. For production/multi-tenant use, additionally run the
# sandbox under gVisor (`runtimeClassName: gvisor` above) so a
# container escape is trapped in a userspace kernel, and apply the
# sandbox NetworkPolicy (manifests/optional/network-policy/) so
# tenants cannot reach each other or cluster-internal endpoints.
# The base manifests without those two controls are a demo. Treat
# danger-full-access as a fallback, not a target: on hosts where
# DSH's command-sandbox backend works, prefer workspace-write (or
# read-only), and recreate the pod because the value is stamped at
# creation.
- name: DSH_PERMISSION_MODE
value: danger-full-access
# Probes target /favicon.svg: DSH gates / behind its launch-token
# cookie and answers 401 to an unauthenticated GET, which would
# otherwise fail readiness and then get the pod killed by liveness.
# /favicon.svg is served without auth and is the cheapest liveness
# signal that proves the HTTP server is actually serving requests.
#
# Gate liveness/readiness behind a startup probe: a cold start on a
# small node takes tens of seconds to load plugins and start serving
# (a resumed sandbox starts cold too). Without it the kubelet can kill
# DSH mid-startup, and a SIGTERM'd DSH exits 0, which the sandbox
# controller records as finished. 24 x 5s = 120s, matching the
# gateway's WAKE_TIMEOUT.
startupProbe:
httpGet:
path: /favicon.svg
port: 3080
periodSeconds: 5
failureThreshold: 24
readinessProbe:
httpGet:
path: /favicon.svg
port: 3080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /favicon.svg
port: 3080
initialDelaySeconds: 15
periodSeconds: 15
failureThreshold: 3
# The container only needs to serve HTTP; drop every capability and
# disallow privilege escalation. DSH writes only to its PVC-backed
# directories (DSH_HOME and the agent workspace).
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
resources:
requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "1", memory: 1Gi}
volumeMounts:
- name: dsh-home
mountPath: /home/node/.dsh # DSH_HOME: sessions, config, state
# The agent workspace (the image's WORKDIR) is PVC-backed too, but
# under its own subPath so DSH state - including .credentials.yaml -
# never shows up as workspace data. Kubelet creates workspace/ on
# first mount with the volume's fsGroup permissions (0775, setgid
# inherited from the volume root), so UID 1000 can write; no init
# container is needed. If the kubelet is killed mid-creation the
# directory stays 0755: see Troubleshooting in the README.
- name: dsh-home
mountPath: /workspace
subPath: workspace
volumeClaimTemplates:
- metadata:
name: dsh-home
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
# storageClassName omitted => cluster default (kind & GKE both work)