From 5637dea10b698ed96fbf2acb70d5287a030d35bb Mon Sep 17 00:00:00 2001 From: jasonwitty Date: Mon, 24 Aug 2026 09:46:27 -0700 Subject: [PATCH] Gate image builds on socktop CLI compatibility; fix agent probe; caps for entrypoint - CI now builds the image locally on the (arm64) runner and runs scripts/verify-image-socktop-flags.sh before pushing: every --flag the restricted/session shells pass must be documented by the socktop binary actually installed in the image. Catches the 0.3.9 failure class (cached apt layer shipping a pre-flag socktop) at build time. - Manifest adds CHOWN/DAC_OVERRIDE/FOWNER alongside SETUID/SETGID: with ALL dropped, uid 0 has no implicit file privilege and prepare_demo_home crash-looped on the demo-owned 700 home dir. Sessions still run with zero capabilities via setpriv. - Agent liveness probe uses /proc instead of kill -0: without CAP_KILL even root gets EPERM signalling the socktop-user agent, so the old check false-alarmed in the pod logs. - 0.3.11 Co-Authored-By: Claude Fable 5 --- .gitea/workflows/build-and-deploy.yaml | 19 +++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- docker/entrypoint.sh | 6 ++- kubernetes/03-deployment.yaml | 11 +++++- scripts/verify-image-socktop-flags.sh | 54 ++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 5 deletions(-) create mode 100755 scripts/verify-image-socktop-flags.sh diff --git a/.gitea/workflows/build-and-deploy.yaml b/.gitea/workflows/build-and-deploy.yaml index e9602c3..80e217e 100644 --- a/.gitea/workflows/build-and-deploy.yaml +++ b/.gitea/workflows/build-and-deploy.yaml @@ -84,6 +84,25 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} + # Build into the runner's docker first (runner is arm64, so the image + # runs natively), gate on the CLI-compatibility check, and only then + # push. The layer cache makes the second build a no-op. + - name: Build Docker image (local, for verification) + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/arm64 + push: false + load: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:candidate + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + + - name: Verify installed socktop understands the shells' flags + run: | + chmod +x scripts/verify-image-socktop-flags.sh + scripts/verify-image-socktop-flags.sh ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:candidate + - name: Build and push Docker image uses: docker/build-push-action@v5 with: diff --git a/Cargo.lock b/Cargo.lock index 64886f8..040222b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2609,7 +2609,7 @@ dependencies = [ [[package]] name = "webterm" -version = "0.3.10" +version = "0.3.11" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 6a95f08..e831583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ documentation = "https://docs.rs/webterm" readme = "README.md" categories = ["web-programming", "web-programming::websocket", "web-programming::http-server", "command-line-utilities"] keywords = ["terminal", "xterm", "websocket", "terminus", "console"] -version = "0.3.10" +version = "0.3.11" authors = ["fabian.freyer@physik.tu-berlin.de","jasonpwitty+socktop@proton.me"] edition = "2021" license = "BSD-3-Clause" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index e0aebd2..309dcec 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -96,8 +96,10 @@ start_socktop_agent() { # Give it a moment to start sleep 1 - # Check if it's running - if kill -0 $AGENT_PID 2>/dev/null; then + # Check if it's running. /proc, not kill -0: the agent runs as another UID + # and the pod's capability set strips CAP_KILL, so even root gets EPERM + # from a probe signal and the check would false-alarm. + if [ -d "/proc/$AGENT_PID" ]; then echo " ✓ socktop-agent is running on port 3001" else echo " ⚠ socktop-agent may have failed to start (check /tmp/socktop-agent.log)" diff --git a/kubernetes/03-deployment.yaml b/kubernetes/03-deployment.yaml index f416fa8..0864843 100644 --- a/kubernetes/03-deployment.yaml +++ b/kubernetes/03-deployment.yaml @@ -85,7 +85,7 @@ spec: containers: - name: webterm - image: gt.wittyoneoff.com/jason/socktop-webterm:0.3.10 + image: gt.wittyoneoff.com/jason/socktop-webterm:0.3.11 imagePullPolicy: Always command: ["/docker-entrypoint.sh"] @@ -180,6 +180,15 @@ spec: add: - SETUID - SETGID + # prepare_demo_home (entrypoint.sh) writes into and re-owns + # /home/demo, which the image ships as demo-owned 700. With + # ALL dropped, uid 0 has no implicit file privilege, so the + # three file caps must come back or the entrypoint crashes on + # mkdir/chown/chmod. Sessions still get zero caps — session- + # shell.sh drops them all via setpriv --inh-caps -all. + - CHOWN + - DAC_OVERRIDE + - FOWNER readOnlyRootFilesystem: false runAsUser: 0 runAsGroup: 0 diff --git a/scripts/verify-image-socktop-flags.sh b/scripts/verify-image-socktop-flags.sh new file mode 100755 index 0000000..ae00d72 --- /dev/null +++ b/scripts/verify-image-socktop-flags.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Verify that the socktop binary baked into a built image understands every +# --flag the restricted/session shells pass to it. +# +# Why this exists: image 0.3.9 shipped with a cached apt layer holding socktop +# 1.60.1, while restricted-shell.sh had started passing --no-kill (added in +# 1.60.2). 1.60.1 parsed the unknown flag as the positional websocket URL, +# prompted to overwrite the 'local' profile with url "--no-kill", and broke the +# demo. This check fails the build whenever the shells and the installed +# binary disagree about the CLI. +# +# Usage: verify-image-socktop-flags.sh IMAGE +set -euo pipefail + +IMAGE="${1:?usage: verify-image-socktop-flags.sh IMAGE}" +cd "$(dirname "$0")/.." + +# Every --flag appearing on a socktop invocation line in the shells. +FLAGS=$(grep -hE '/usr/bin/socktop' docker/restricted-shell.sh docker/session-shell.sh 2>/dev/null | + grep -oE -- '--[a-z][a-z-]*' | sort -u) +if [ -z "$FLAGS" ]; then + echo "ERROR: found no socktop flags to verify — did the shells move?" >&2 + exit 1 +fi + +# The image is arm64-only; pin the platform so the check behaves the same on +# the arm64 CI runner and on an amd64 box with qemu binfmt. +run_socktop() { + docker run --rm --platform linux/arm64 --entrypoint /usr/bin/socktop "$IMAGE" "$@" 2>&1 +} + +if ! VERSION=$(run_socktop --version); then + echo "ERROR: could not run socktop from ${IMAGE}:" >&2 + echo "$VERSION" >&2 + exit 1 +fi +HELP=$(run_socktop --help || true) +echo "image socktop: ${VERSION}" + +rc=0 +for flag in $FLAGS; do + if printf '%s' "$HELP" | grep -q -- "$flag"; then + echo " ok: $flag" + else + echo " MISSING: installed socktop does not document $flag" >&2 + rc=1 + fi +done + +if [ "$rc" -ne 0 ]; then + echo "FAIL: the image's socktop predates flags the shells pass." >&2 + echo "Bump SOCKTOP_VERSION in the Dockerfile to a release that has them." >&2 +fi +exit $rc