mirror of
https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git
synced 2026-11-03 01:13:24 +00:00
fix: ship bubblewrap for DSH's command sandbox, keep agents usable without it (#35)
* fix(example): keep agents usable when DSH's inner sandbox has no backend DSH confines bash commands itself (bubblewrap, else Landlock) and fails closed with SANDBOX_UNAVAILABLE when no runner is usable. In this hardened pod - non-root, capabilities dropped, seccomp RuntimeDefault - bubblewrap cannot start (it needs CAP_SYS_ADMIN) and the pod exposes no Landlock runner, so the shipped workspace-write default made every command fail: sandbox mode "workspace-write" is requested but no sandbox backend is usable The DSH base bundle at the pinned ref documents DSH_PERMISSION_MODE as the explicit deployment override, so set it to danger-full-access in both sandbox templates. The per-user pod is the isolation boundary in this example (its own pod, PVC and Service; non-root; all capabilities dropped), so a working agent is worth more than an inner file policy that cannot be enforced. Note that danger-full-access also turns approval prompts off. Trade-off documented rather than hidden: an agent command can now read its own DSH_HOME (including .credentials.yaml) and write anywhere in the container. Hosts where a runner works should keep workspace-write or read-only. - main-image docs: DSH_PERMISSION_MODE row plus a note on the runners and the fail-closed behavior - example README: limits bullet and a troubleshooting row with the in-pod backend check and how to restore a confined mode - architecture: tenant-isolation bullet extended - skill runbook/troubleshooting: operation guidance; never grant CAP_SYS_ADMIN or seccompProfile: Unconfined to "fix" this without an explicit decision Verified: make check (14 manifests), make lint, YAML assertions on both templates, and the override expression at the pinned dsh-v0.2.1-rc.5 base bundle (mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'). * fix(image): install bubblewrap for DSH's command sandbox DSH's Linux sandbox runner chain is bubblewrap, then Landlock, and it fails closed with SANDBOX_UNAVAILABLE when neither works. The image shipped neither, so a sandboxed agent had no way to execute confined commands. Add bubblewrap to the runtime stage. Debian ships it unsetuid (0.12.0), so it still needs unprivileged user namespaces plus the namespace/mount syscalls: in a hardened pod (all capabilities dropped, RuntimeDefault seccomp) it usually cannot create one, and DSH then falls back to Landlock or, failing that, to the DSH_PERMISSION_MODE=danger-full-access override the templates already set. Where the host allows an unprivileged user namespace, the first-choice runner now exists, so the confined modes can be restored. Docs corrected to match: bubblewrap is no longer "absent"; the blocker is the unsetuid binary's namespace requirement, not a missing CAP_SYS_ADMIN grant. Verified: make check, make lint; the package exists in the pinned base (node:24-trixie-slim, bubblewrap 0.12.0-1~deb13u1); a throwaway container with the pod's flags (--user 1000 --cap-drop ALL --security-opt no-new-privileges) still cannot create a namespace, which is why the override stays. No local image build was run. * ci(image): build and smoke-test the main image on PRs that touch it The Dockerfile and the pins in versions.env could only be validated by a merge to main or by running `make image` by hand; neither gives a PR-time signal. Add ci-image.yaml: on pull requests that change Dockerfile, .dockerignore or versions.env (and on manual dispatch) it builds linux/amd64 natively with the pins sourced from versions.env - no registry login, no push - loads the image locally and smoke-tests it: - the packaged CLI runs and reports the pinned version - the container is the non-root node user (1000) and owns /workspace + DSH_HOME - the bubblewrap command-sandbox runner is present - the OCI version label matches versions.env - a container serves /favicon.svg (the same unauthenticated probe the shipped manifests use), with logs filtered so the launch token is never printed Path-filtered like the other on-demand checks, so it is not a required status check; the published multi-arch build stays in build-main.yaml and release.yaml. Docs updated in the same change: AGENTS.md repository map and testing table, docs/gitflow.md checks section, docs/main-image.md build section. Verified: actionlint 1.7.12 and yamllint 1.38.0 (CI versions) clean over the whole repository, plus make check and make lint. --------- Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
This commit is contained in:
committed by
GitHub
parent
367294b230
commit
a09d4ac340
@@ -49,16 +49,25 @@ branch; the PR is squash-merged, so individual branch commits are never part of
|
||||
| `CodeQL` | `codeql.yml` | Static analysis for Python and GitHub Actions. |
|
||||
| `dependency-review` | `ci.yaml` | Dependency changes introduced by the PR (job `dependency-review`; runs on pull requests only). |
|
||||
|
||||
`python` and `manifests` use native `on.pull_request.paths` filtering, so PRs that
|
||||
do not touch the gateway or the manifests skip them. The gateway image build and
|
||||
`/healthz` are no longer smoke-tested in a dedicated job: `/healthz` is covered by
|
||||
the gateway unit tests, the Dockerfile is linted by `lint` (hadolint), and the
|
||||
multi-arch image build is published by `build-gateway.yaml` on merge.
|
||||
`python`, `manifests` and `build-amd64` use native `on.pull_request.paths`
|
||||
filtering, so PRs that do not touch the gateway, the manifests or the image
|
||||
definition skip them. The gateway image build and `/healthz` are no longer
|
||||
smoke-tested in a dedicated job: `/healthz` is covered by the gateway unit tests,
|
||||
the Dockerfile is linted by `lint` (hadolint), and the multi-arch image build is
|
||||
published by `build-gateway.yaml` on merge.
|
||||
|
||||
`ci-image.yaml` (`build-amd64`) is the PR-side check for the main image: when
|
||||
`Dockerfile`, `.dockerignore` or `versions.env` change it builds `linux/amd64`
|
||||
with the pins from `versions.env` — no registry login, no push — and smoke-tests
|
||||
the result (`--version`, non-root UID, `/workspace` + `DSH_HOME`, the
|
||||
`bubblewrap` runner, the version label, and `/favicon.svg` served from a running
|
||||
container). The published multi-arch build still happens only in
|
||||
`build-main.yaml` on merge.
|
||||
|
||||
Because a skipped path-filtered check is not a passing check, only the always-on
|
||||
checks (`lint`, `pr-title`, `CodeQL`, `dependency-review`) should be enforced as
|
||||
required status checks; `python` and `manifests` are on-demand checks that skip
|
||||
when unrelated to the change.
|
||||
required status checks; `python`, `manifests` and `build-amd64` are on-demand
|
||||
checks that skip when unrelated to the change.
|
||||
|
||||
The image build workflows (`_build-image.yaml`, `build-main.yaml`,
|
||||
`build-gateway.yaml`) and `e2e-kind.yaml` are **not** required on every PR: the
|
||||
|
||||
@@ -18,7 +18,8 @@ The Dockerfile is a two-stage build:
|
||||
CLI with `pnpm pack`, and installs its production dependencies with npm.
|
||||
2. **Runtime stage** (`node:24-trixie-slim`) copies `/opt/dsh` to `/app`, adds the
|
||||
CLI tools DSH agents commonly use (`git`, `ripgrep`, `jq`, `yq`, `curl`, `wget`,
|
||||
`gh`, `tini`, `uv`), and starts `dsh web` through a small entrypoint.
|
||||
`gh`, `tini`, `uv`), installs `bubblewrap` as DSH's preferred command-sandbox
|
||||
runner, and starts `dsh web` through a small entrypoint.
|
||||
|
||||
## Build it
|
||||
|
||||
@@ -27,6 +28,9 @@ make image # canonical: builds the DSH Web image, no push
|
||||
```
|
||||
|
||||
`make image` reads [versions.env](../versions.env) and passes the pins as build args.
|
||||
Pull requests that touch `Dockerfile`, `.dockerignore` or `versions.env` get the
|
||||
same build for `linux/amd64` from the [`ci-image.yaml`](../.github/workflows/ci-image.yaml)
|
||||
workflow — no registry login, no push — followed by a container smoke test.
|
||||
To reproduce the equivalent by hand:
|
||||
|
||||
```bash
|
||||
@@ -76,10 +80,24 @@ The entrypoint is `tini -- /usr/local/bin/dsh-entrypoint`:
|
||||
| `DSH_TRUSTED_HOSTS` | empty | Extra `Host`/`Origin` allow-list entries, comma- or space-separated. `localhost`/`127.0.0.1` are always trusted. Required when a proxy forwards the browser's `Host`. |
|
||||
| `DEEPSEEK_API_KEY` | empty | Provider credential read by DSH. Inject it with `-e`/`envFrom`; never bake it into the image. Only model calls fail when it is missing. |
|
||||
| `DSH_HOME` | `/home/node/.dsh` | DSH state directory (conversations, sessions, config, credentials). Mount a volume here to persist it. |
|
||||
| `DSH_PERMISSION_MODE` | `workspace-write` | Deployment default for DSH's file policy: `read-only`, `workspace-write`, or `danger-full-access` (which also turns approval prompts off). The confined modes need `bubblewrap` or Landlock inside the container; where neither is usable every bash command fails closed with `SANDBOX_UNAVAILABLE`. See the note below. |
|
||||
|
||||
The image also sets `NODE_ENV=production`, `HOME=/home/node`, and a `PATH` that
|
||||
includes `/app/node_modules/.bin`.
|
||||
|
||||
DSH confines bash commands itself (a second layer inside whatever container
|
||||
sandbox you already run). Its Linux runners are `bubblewrap` and then Landlock,
|
||||
and it fails closed rather than running unconfined. The image ships
|
||||
`bubblewrap`, but Debian's binary is unsetuid: it needs **unprivileged user
|
||||
namespaces** plus the namespace/mount syscalls. A hardened pod — non-root,
|
||||
`capabilities.drop: ["ALL"]`, `seccompProfile: RuntimeDefault` — usually cannot
|
||||
create one, and it may expose no Landlock runner either, in which case every
|
||||
command fails with `SANDBOX_UNAVAILABLE`. Set
|
||||
`DSH_PERMISSION_MODE=danger-full-access` only when the surrounding pod or VM is
|
||||
the isolation boundary you rely on; keep the confined modes when the host
|
||||
provides a working runner (check `grep -o landlock /sys/kernel/security/lsm` and
|
||||
`bwrap --ro-bind / / true` inside the container first).
|
||||
|
||||
### Provider credentials
|
||||
|
||||
DSH resolves provider API keys from the environment when no stored credential
|
||||
|
||||
Reference in New Issue
Block a user