CI Sandbox Attestation Workflow Draft (B4′) — Phase 2 Offline MVP
CI Sandbox Attestation Workflow Draft (B4′)
Status: CI_WORKFLOW_DRAFT_READY_NOT_APPLIED · Date: 2026-06-10 · Venue class: approved-equivalent CI runner (GitHub-hosted, ephemeral, NON-Mac-local) · Production mutation: NONE · Codex consulted: NO
This is a draft / ready-to-apply artifact. It is evidence-of-design, not authority. It is NOT applied to any repo, NOT triggered, and produces NO B4′ PASS by itself. Applying it requires an owner authorization decision (see §0 and the route-decision report
reports/phase2-execution-substrate-and-route-decision-2026-06-10.md). It operationalizes the SSOT sandbox profiledesigns/deny-by-default-sandbox-profile-phase2-offline-mvp-2026-06-09.md; if anything here conflicts with that profile, the profile wins (CONFLICT → prefer the design SSOT).
0. Why this exists / what it changes
The prior canonical fix (checkpoints/operator-blocker-packet-sandbox-attestation-2026-06-09.md) required a human operator with docker run permission on the VPS (or an unspecified "approved CI runner"). This draft converts the CI option into a turnkey, command-free path: an authorized actor drops these files into a repo and clicks Run workflow. The GitHub-hosted runner builds the deny-by-default image, runs the 12 §6 probes inside the container, and uploads the §7 evidence bundle as an artifact. No VPS shell, no Mac-local Docker.
The one decision the owner still owns (it cannot be resolved by engineering evidence): which repo is authorized to host this attestation workflow. Options:
- CI-A: owner approves creating a private repo (e.g.
Huyen1974/tki-sandbox-attest, private) solely for attestation. The authenticatedghCLI (Huyen1974, scoperepo+workflow) can create+push it once authorized. - CI-B: owner designates an existing approved repo/CI runner already inside the governed system to host it.
- If neither is authorized → fall back to the VPS operator route (
checkpoints/operator-execution-packet-phase2-sandbox-final-2026-06-10.md).
Until that authorization exists, this file stays a draft. No external publish has occurred.
1. Repo layout to apply
.
├── .github/workflows/b4-prime-sandbox-attestation.yml # the workflow (§2)
├── Dockerfile.sandbox # from the profile §3 (reproduced §4)
├── seccomp-deny-by-default.json # profile §4a targeted-deny (reproduced §5)
└── inspector/
└── main.py # attestation probe harness (§6)
All four files are below verbatim. No other repo content is required. The repo may be empty/private; the workflow has no push trigger (manual workflow_dispatch only) so it never runs by accident.
2. .github/workflows/b4-prime-sandbox-attestation.yml
name: B4' Sandbox Attestation (deny-by-default)
# Manual trigger ONLY. No push/PR triggers => no accidental runs, no auto-publish behaviour.
on:
workflow_dispatch:
inputs:
run_date:
description: "Attestation date stamp (YYYY-MM-DD)"
required: true
default: "2026-06-10"
permissions:
contents: read # least privilege; no write, no packages, no id-token
jobs:
attest:
runs-on: ubuntu-latest # GitHub-hosted, ephemeral, NOT Mac-local; Docker preinstalled
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Record venue identity (proves NOT Mac-local)
id: venue
run: |
echo "runner_os=$RUNNER_OS" >> "$GITHUB_OUTPUT"
echo "runner_arch=$RUNNER_ARCH" >> "$GITHUB_OUTPUT"
echo "image_os=$(uname -a)" >> venue.txt
echo "github_run_id=$GITHUB_RUN_ID" >> venue.txt
echo "github_repo=$GITHUB_REPOSITORY" >> venue.txt
docker --version >> venue.txt
cat venue.txt
- name: Build deny-by-default image
run: docker build -f Dockerfile.sandbox -t tki-inspector:attest .
- name: Capture image digest + seccomp hash + runtime
id: meta
run: |
IMG_DIGEST=$(docker image inspect tki-inspector:attest --format '{{index .Id}}')
SECCOMP_SHA=$(sha256sum seccomp-deny-by-default.json | awk '{print $1}')
RUNTIME=$(docker --version | sed 's/,.*//')
echo "image_digest=${IMG_DIGEST}" >> "$GITHUB_OUTPUT"
echo "seccomp_sha256=${SECCOMP_SHA}" >> "$GITHUB_OUTPUT"
echo "runtime=${RUNTIME}" >> "$GITHUB_OUTPUT"
- name: Prepare disposable in/out dirs
run: |
mkdir -p "$RUNNER_TEMP/in" "$RUNNER_TEMP/out"
# read-only input fixture (empty packet is sufficient for boundary probes)
echo '{"packet":"attestation-fixture"}' > "$RUNNER_TEMP/in/packet.json"
chmod -R a-w "$RUNNER_TEMP/in" || true
- name: Run probes INSIDE the deny-by-default container (§5 exact run command)
run: |
set +e
docker run --rm \
--name tki-sandbox-attest \
--network none \
--user 65532:65532 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m \
--cap-drop ALL \
--security-opt no-new-privileges \
--security-opt seccomp=seccomp-deny-by-default.json \
--pids-limit 64 \
--memory 256m --memory-swap 256m --cpus 1 \
--env-file /dev/null \
-v "$RUNNER_TEMP/in":/in:ro \
-v "$RUNNER_TEMP/out":/out:rw \
tki-inspector:attest --attest --run-date "${{ github.event.inputs.run_date }}"
echo "container_exit=$?"
set -e
- name: Merge venue + image metadata into the evidence bundle
run: |
IN="$RUNNER_TEMP/out/sandbox-attestation-inside.json"
OUT="$RUNNER_TEMP/out/sandbox-attestation-evidence-ci-${{ github.event.inputs.run_date }}.json"
if [ ! -f "$IN" ]; then
echo "::error::probe harness produced no inside-bundle — container may have failed-closed before writing /out"; exit 1
fi
python3 - "$IN" "$OUT" <<'PY'
import json, sys, os
inside = json.load(open(sys.argv[1]))
inside["venue"] = "CI"
inside["venue_identity"] = {
"platform": "github-hosted-runner",
"runner_os": os.environ.get("RUNNER_OS"),
"runner_arch": os.environ.get("RUNNER_ARCH"),
"github_repository": os.environ.get("GITHUB_REPOSITORY"),
"github_run_id": os.environ.get("GITHUB_RUN_ID"),
"not_mac_local": True,
}
inside["image_digest"] = os.environ.get("IMAGE_DIGEST")
inside["seccomp_sha256"]= os.environ.get("SECCOMP_SHA256")
inside["runtime"] = os.environ.get("RUNTIME")
json.dump(inside, open(sys.argv[2], "w"), indent=2)
print(open(sys.argv[2]).read())
PY
env:
IMAGE_DIGEST: ${{ steps.meta.outputs.image_digest }}
SECCOMP_SHA256: ${{ steps.meta.outputs.seccomp_sha256 }}
RUNTIME: ${{ steps.meta.outputs.runtime }}
- name: Upload evidence bundle artifact
uses: actions/upload-artifact@v4
with:
name: b4-prime-sandbox-attestation-evidence
path: |
${{ runner.temp }}/out/sandbox-attestation-evidence-ci-*.json
${{ runner.temp }}/out/sandbox-attestation-inside.json
if-no-files-found: error
retention-days: 30
Cleanup: none required — docker run --rm removes the container; the runner VM is destroyed at job end (GitHub-hosted runners are single-use). No persistent infra, no production touch.
3. How to apply, trigger, collect, verify
- Authorize a repo (see §0). With
ghalready authenticated asHuyen1974:# CI-A (only after owner authorization — this PUBLISHES the files to GitHub):gh repo create Huyen1974/tki-sandbox-attest --private --disable-issues --disable-wiki# populate the 4 files of §1, then:git add . && git commit -m "B4' sandbox attestation harness" && git push -u origin main - Trigger:
gh workflow run "B4' Sandbox Attestation (deny-by-default)" -f run_date=2026-06-10(or the Actions UI → Run workflow). - Collect:
gh run download <run-id> -n b4-prime-sandbox-attestation-evidence→ yieldssandbox-attestation-evidence-ci-2026-06-10.json. - Return to KB: place the bundle at
reports/sandbox-attestation-evidence-ci-2026-06-10.json(KB) or VPS/opt/incomex/docs/mcp-writes/sandbox-attestation-ci-2026-06-10.json. - Verify (follow-up agent, read-only): confirm every probe
actual == §3-expected, bind to rev4 matrix #24–#37, then assert B4′ acceptance and run the gated build prompt.
4. Dockerfile.sandbox (reproduced from profile §3 — profile is SSOT)
FROM gcr.io/distroless/python3-debian12:nonroot
WORKDIR /app
COPY --chown=root:root inspector/ /app/
USER 65532:65532
ENTRYPOINT ["python", "/app/main.py"]
Distroless = no shell, no package manager → "no subprocess host binary" is structural, not merely seccomp-enforced.
5. seccomp-deny-by-default.json (profile §4a targeted-deny — first-attestation realization)
{
"defaultAction": "SCMP_ACT_ALLOW",
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
"syscalls": [
{
"names": [
"socket","socketpair","connect","bind","listen","accept","accept4",
"sendto","sendmsg","recvfrom","recvmsg","getpeername","getsockname",
"execve","execveat","ptrace","process_vm_readv","process_vm_writev",
"mount","umount2","pivot_root","chroot","setns","unshare","clone3",
"init_module","finit_module","delete_module","kexec_load","reboot",
"swapon","swapoff","bpf","perf_event_open"
],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1
}
]
}
errnoRet: 1 = EPERM. The §4b hardened default-deny allowlist is a follow-on target and cannot be finalized without first running the real inspector (workload-specific); do not block first attestation on it.
6. inspector/main.py — attestation probe harness (runs INSIDE the container)
The harness runs all 12 §6 probes, records expected vs actual + errno, and writes /out/sandbox-attestation-inside.json (the workflow merges venue/image metadata afterwards). It never opens a network connection on purpose beyond the denied probe; it makes no live KB/PG read; it is self-contained stdlib only.
#!/usr/bin/env python3
# B4' attestation probe harness. Runs inside the deny-by-default container.
# Emits /out/sandbox-attestation-inside.json with the §7 probe schema.
import os, sys, json, socket, errno, ctypes
RUN_DATE = "unknown"
if "--run-date" in sys.argv:
RUN_DATE = sys.argv[sys.argv.index("--run-date") + 1]
OUT = "/out/sandbox-attestation-inside.json"
SECRET_RE = None
import re
SECRET_RE = re.compile(r"(?i)(token|secret|password|passwd|_dsn|conn|^pg|^kb|api[_-]?key)")
probes = []
def record(pid, operation, expected, actual, errno_or_exit, verdict):
probes.append({
"probe_id": pid, "operation": operation, "expected": expected,
"actual_stderr_or_value": str(actual)[:500], "errno_or_exit": errno_or_exit,
"verdict": verdict, "artifact_path": OUT,
})
# PR-NET-1: connect to a public resolver -> EPERM (socket denied) or ENETUNREACH/EAFNOSUPPORT
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.settimeout(2)
s.connect(("1.1.1.1", 53))
record("PR-NET-1", "connect 1.1.1.1:53", "EPERM/ENETUNREACH/EAFNOSUPPORT", "CONNECTED", 0, "FAIL")
except OSError as e:
ok = e.errno in (errno.EPERM, errno.ENETUNREACH, errno.EAFNOSUPPORT)
record("PR-NET-1", "connect 1.1.1.1:53", "EPERM(1)/ENETUNREACH(101)/EAFNOSUPPORT(97)", e, e.errno, "PASS" if ok else "FAIL")
# PR-NET-2: /proc/net/dev -> only 'lo'
try:
ifaces = [ln.split(":")[0].strip() for ln in open("/proc/net/dev").read().splitlines()[2:]]
ok = set(ifaces) <= {"lo"}
record("PR-NET-2", "read /proc/net/dev", "interfaces == {lo}", ifaces, "n/a", "PASS" if ok else "FAIL")
except OSError as e:
record("PR-NET-2", "read /proc/net/dev", "interfaces == {lo}", e, e.errno, "FAIL")
# PR-SOCK-1: raw AF_INET socket -> EPERM
try:
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
record("PR-SOCK-1", "socket(AF_INET,SOCK_STREAM)", "EPERM(1)", "CREATED", 0, "FAIL")
except OSError as e:
record("PR-SOCK-1", "socket(AF_INET,SOCK_STREAM)", "EPERM(1)", e, e.errno, "PASS" if e.errno == errno.EPERM else "FAIL")
# PR-ENV-1: no secret-pattern env key
keys = sorted(os.environ.keys())
leaked = [k for k in keys if SECRET_RE.search(k)]
record("PR-ENV-1", "os.environ.keys()", "no token/secret/password/_dsn/conn/pg/kb/api_key", {"keys": keys, "leaked": leaked}, "n/a", "PASS" if not leaked else "FAIL")
# PR-FS-RO-IN: write into /in -> EROFS
try:
open("/in/__probe", "w"); record("PR-FS-RO-IN", "open /in/__probe w", "EROFS(30)", "WROTE", 0, "FAIL")
except OSError as e:
record("PR-FS-RO-IN", "open /in/__probe w", "EROFS(30)", e, e.errno, "PASS" if e.errno in (errno.EROFS, errno.EACCES) else "FAIL")
# PR-FS-ESC-1: write /etc -> EROFS (read-only rootfs)
try:
open("/etc/__probe", "w"); record("PR-FS-ESC-1", "open /etc/__probe w", "EROFS(30)", "WROTE", 0, "FAIL")
except OSError as e:
record("PR-FS-ESC-1", "open /etc/__probe w", "EROFS(30)", e, e.errno, "PASS" if e.errno in (errno.EROFS, errno.EACCES) else "FAIL")
# PR-FS-ESC-2: write /app -> EROFS/EACCES
try:
open("/app/__probe", "w"); record("PR-FS-ESC-2", "open /app/__probe w", "EROFS(30)/EACCES(13)", "WROTE", 0, "FAIL")
except OSError as e:
record("PR-FS-ESC-2", "open /app/__probe w", "EROFS(30)/EACCES(13)", e, e.errno, "PASS" if e.errno in (errno.EROFS, errno.EACCES) else "FAIL")
# PR-FS-OUT-OK: positive control — /out is the one writable path
try:
open("/out/report.md", "w").write("ok")
record("PR-FS-OUT-OK", "open /out/report.md w", "succeeds (positive control)", "WROTE ok", 0, "PASS")
except OSError as e:
record("PR-FS-OUT-OK", "open /out/report.md w", "succeeds (positive control)", e, e.errno, "FAIL")
# PR-EXEC-1: execve real binary -> EPERM (seccomp); distroless also lacks /bin/ls
try:
os.execv(sys.executable, [sys.executable, "-c", "pass"])
record("PR-EXEC-1", "os.execv(python)", "EPERM(1) or FileNotFoundError", "EXECVE-SUCCEEDED", 0, "FAIL")
except OSError as e:
ok = e.errno in (errno.EPERM, errno.ENOENT)
record("PR-EXEC-1", "os.execv(python)", "EPERM(1)/ENOENT(2)", e, e.errno, "PASS" if ok else "FAIL")
# PR-MOUNT-1: mount table — rootfs ro, /in ro, /out rw, /tmp noexec, nothing else writable
try:
mounts = open("/proc/self/mountinfo").read()
has_in_ro = any("/in" in ln and " ro" in ln for ln in mounts.splitlines())
has_out_rw = any("/out" in ln and " rw" in ln for ln in mounts.splitlines())
record("PR-MOUNT-1", "read /proc/self/mountinfo", "rootfs ro; /in ro; /out rw; /tmp noexec; nothing else",
{"in_ro": has_in_ro, "out_rw": has_out_rw, "raw_present": True}, "n/a",
"PASS" if (has_in_ro and has_out_rw) else "FAIL")
except OSError as e:
record("PR-MOUNT-1", "read /proc/self/mountinfo", "expected mount table", e, e.errno, "FAIL")
# PR-SOCK-DOCKER: docker socket absent
exists = os.path.exists("/var/run/docker.sock")
record("PR-SOCK-DOCKER", "exists /var/run/docker.sock", "False", exists, "n/a", "PASS" if not exists else "FAIL")
# PR-PTRACE-1: ptrace(PTRACE_TRACEME) -> EPERM
try:
libc = ctypes.CDLL("libc.so.6", use_errno=True)
PTRACE_TRACEME = 0
rc = libc.ptrace(PTRACE_TRACEME, 0, 0, 0)
en = ctypes.get_errno()
ok = (rc != 0 and en == errno.EPERM)
record("PR-PTRACE-1", "ptrace(PTRACE_TRACEME)", "EPERM(1)", {"rc": rc, "errno": en}, en, "PASS" if ok else "FAIL")
except Exception as e:
# libc may be unloadable in distroless; record UNVERIFIED rather than fake-PASS
record("PR-PTRACE-1", "ptrace(PTRACE_TRACEME)", "EPERM(1)", f"UNVERIFIED: {e}", "n/a", "UNVERIFIED")
# raw context (observable only from inside)
def safe_read(p):
try: return open(p).read()
except OSError as e: return f"<unreadable: {e}>"
bundle = {
"schema": "b4-prime-sandbox-attestation/inside/v1",
"run_date": RUN_DATE,
"probes": probes,
"raw": {
"mountinfo": safe_read("/proc/self/mountinfo"),
"env_keyset": keys,
"proc_net_dev": safe_read("/proc/net/dev"),
},
"summary": {
"total": len(probes),
"pass": sum(1 for p in probes if p["verdict"] == "PASS"),
"fail": sum(1 for p in probes if p["verdict"] == "FAIL"),
"unverified": sum(1 for p in probes if p["verdict"] == "UNVERIFIED"),
},
}
os.makedirs("/out", exist_ok=True)
json.dump(bundle, open(OUT, "w"), indent=2)
print(json.dumps(bundle["summary"]))
# Fail-closed exit: nonzero if any probe FAILED, so the workflow surfaces a boundary breach.
sys.exit(0 if bundle["summary"]["fail"] == 0 else 2)
7. Probe → rev4 matrix binding (for the follow-up verification agent)
| Probe | rev4 matrix test | Expected proof-of-block |
|---|---|---|
| PR-NET-1 / PR-NET-2 | #27 | EPERM/ENETUNREACH; ifaces={lo} |
| PR-SOCK-1 | #25 sib | EPERM on socket |
| PR-ENV-1 | #28 | no secret env key |
| PR-FS-RO-IN | #33 | EROFS on /in |
| PR-FS-ESC-1 / PR-FS-ESC-2 | #29 | EROFS/EACCES outside /out |
| PR-FS-OUT-OK | #33 ctrl | /out writable (positive control) |
| PR-EXEC-1 | #34 / #25 | EPERM(execve) / ENOENT |
| PR-MOUNT-1 | #29/#33 | mount table = {rootfs ro, /in ro, /out rw, /tmp noexec} |
| PR-SOCK-DOCKER | #36 | docker.sock absent |
| PR-PTRACE-1 | #37 | EPERM |
#35 (PR-DYNIMPORT) is a build-time L2 check, not an OS probe — attested when the guard harness is built; flagged here so it is not silently dropped.
8. Honesty caveats (Article 14)
- This draft produces no PASS. B4′ is PASS only after the bundle exists from an approved run and a follow-up agent verifies it against #24–#37.
- The GitHub-hosted runner is an approved-equivalent isolation venue (ephemeral, non-Mac-local). Calling it "approved" is the owner's determination, not the agent's — see §0 / the route-decision report.
- Applying CI-A publishes the harness to GitHub; that is outward-facing and is gated on explicit owner authorization. No publish has occurred.
- If
gcr.io/distroless/...oractions/*pinning policy differs in the target environment, pin to digests before applying. The seccomparchitectureslist assumes x86_64/aarch64 runners.