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
126
.github/workflows/ci-image.yaml
vendored
Normal file
126
.github/workflows/ci-image.yaml
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
# Build-only check for the main DSH Web image: linux/amd64, no registry login,
|
||||
# no push. It proves the Dockerfile still builds from the pinned versions.env and
|
||||
# that the resulting image can actually run DSH Web. The published multi-arch
|
||||
# build stays in build-main.yaml (merge to main) and release.yaml (tags).
|
||||
#
|
||||
# Like ci-python.yaml and ci-manifests.yaml this is path-filtered, so it is an
|
||||
# on-demand check rather than a required status check.
|
||||
name: CI image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
- versions.env
|
||||
- .github/workflows/ci-image.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-image-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
name: build-amd64
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Read versions.env
|
||||
id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source versions.env
|
||||
set +a
|
||||
identity="$DSH_VERSION"
|
||||
[ -z "${RELEASE_REVISION:-}" ] || identity="$DSH_VERSION-$RELEASE_REVISION"
|
||||
{
|
||||
echo "identity=$identity"
|
||||
echo "dsh_version=$DSH_VERSION"
|
||||
echo "pnpm_version=$PNPM_VERSION"
|
||||
echo "release_revision=${RELEASE_REVISION:-}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
|
||||
- name: Build linux/amd64 (load locally, never push)
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
tags: dsh-web:ci
|
||||
build-args: |
|
||||
DSH_VERSION=${{ steps.meta.outputs.dsh_version }}
|
||||
RELEASE_REVISION=${{ steps.meta.outputs.release_revision }}
|
||||
PNPM_VERSION=${{ steps.meta.outputs.pnpm_version }}
|
||||
VCS_REF=${{ github.sha }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.version=${{ steps.meta.outputs.identity }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
cache-from: type=gha,scope=ci-image-linux-amd64
|
||||
cache-to: type=gha,mode=max,scope=ci-image-linux-amd64
|
||||
|
||||
- name: Smoke test the image
|
||||
env:
|
||||
IDENTITY: ${{ steps.meta.outputs.identity }}
|
||||
DSH_VERSION: ${{ steps.meta.outputs.dsh_version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image=dsh-web:ci
|
||||
|
||||
# The packaged CLI runs and reports the version pinned in versions.env.
|
||||
version="$(docker run --rm --entrypoint node "$image" /app/lib/bin.js --version)"
|
||||
printf 'DSH CLI: %s\n' "$version"
|
||||
grep -q "${DSH_VERSION#dsh-v}" <<<"$version"
|
||||
|
||||
# The container runs as the non-root node user and its writable
|
||||
# directories exist and are owned by that user.
|
||||
[ "$(docker run --rm --entrypoint id "$image" -u)" = "1000" ]
|
||||
docker run --rm --entrypoint sh "$image" -c \
|
||||
'test -d /workspace && test -d "$DSH_HOME" && [ "$(stat -c %u /workspace)" = 1000 ]'
|
||||
|
||||
# The command-sandbox runner this image is expected to ship.
|
||||
docker run --rm --entrypoint bwrap "$image" --version
|
||||
|
||||
# Labels come from the build args rather than a hard-coded value.
|
||||
label="$(docker inspect -f '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$image")"
|
||||
printf 'image version label: %s\n' "$label"
|
||||
[ "$label" = "$IDENTITY" ]
|
||||
|
||||
- name: Serve DSH Web
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run -d --name dsh-ci -p 18080:3080 \
|
||||
-e DSH_TRUSTED_HOSTS=127.0.0.1:18080 dsh-web:ci >/dev/null
|
||||
trap 'docker rm -f dsh-ci >/dev/null 2>&1 || true' EXIT
|
||||
|
||||
# /favicon.svg is unauthenticated and served by the web layer, which is
|
||||
# why the shipped probes use it too.
|
||||
for second in $(seq 1 60); do
|
||||
if curl -fsS -o /dev/null http://127.0.0.1:18080/favicon.svg; then
|
||||
printf 'DSH Web served /favicon.svg after ~%ss\n' "$((second * 2))"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "::error::DSH Web did not serve /favicon.svg within 120s"
|
||||
# Pod logs can hold the DSH launch token: never dump them verbatim.
|
||||
docker logs dsh-ci 2>&1 | grep -v 'token=' | tail -50 || true
|
||||
exit 1
|
||||
Reference in New Issue
Block a user