Container Peer Authentication Unilateral Validation

August 17, 2026 · View on GitHub

中文版

This is the tester-run plan for the SkillFS side of issue #2439. It validates the container HMAC contract in a real Linux or Kubernetes environment without waiting for an independently released agent-sec-core image.

The independent standard-library Python probe at scripts/container-peer-auth-probe.py acts only as a control client or notify server. Passing this plan proves the SkillFS implementation and cross-language wire contract. It does not prove agent-sec-core configuration, reconciliation, activation decisions, or release readiness; those still require bilateral integration and ACK evidence.

1. Freeze the test inputs

Create an evidence directory outside the repository and record the following before changing the environment:

export C7_EVIDENCE=/var/tmp/skillfs-c7-evidence
install -d -m 0700 "$C7_EVIDENCE"
git rev-parse HEAD | tee "$C7_EVIDENCE/git-commit.txt"
rustc --version | tee "$C7_EVIDENCE/rustc.txt"
python3 --version | tee "$C7_EVIDENCE/python.txt"
uname -a | tee "$C7_EVIDENCE/uname.txt"

For Kubernetes, also record immutable cluster and image inputs:

kubectl version -o yaml >"$C7_EVIDENCE/kubernetes-version.yaml"
kubectl get nodes -o wide >"$C7_EVIDENCE/nodes.txt"
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.containerRuntimeVersion}{"\n"}{end}' \
  >"$C7_EVIDENCE/container-runtime.txt"
kubectl -n "$C7_NAMESPACE" get pod "$C7_POD" -o yaml \
  >"$C7_EVIDENCE/pod.yaml"
kubectl -n "$C7_NAMESPACE" get pod "$C7_POD" \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.imageID}{"\n"}{end}' \
  >"$C7_EVIDENCE/image-ids.txt"

Do not save the Secret object, raw secret, proof, or reusable credential in the evidence directory.

2. Required topology

Use three containers with shareProcessNamespace absent or false:

ContainerEffective UIDSourceFUSE viewRuntime socketsAuth file
SkillFS0 for this testread-writemount creatorread-writeread-only
trusted probe0 for this testsame physical source at the same absolute pathoptionalread-writeread-only
workload / attackerdifferent UID for normal I/O; repeat negative socket tests with UID 0absentread-write, propagatedabsent normally; read-only for negative testsabsent

Use separate volumes for:

  • the physical source;
  • the propagated FUSE view;
  • private runtime sockets; and
  • the staged authentication file.

Kubernetes Secret projections use symlinked entries, while the production loader deliberately opens the final component with O_NOFOLLOW. Therefore do not pass a projected Secret path directly to SkillFS. Use an init container to copy the projected key into a trusted emptyDir, set mode 0400 or 0600, and mount that staged file only into SkillFS and the trusted probe. This is an acceptance requirement, not a relaxation of the no-follow rule.

The initial profile requires SkillFS and sec-core to use the same effective UID for authenticated notify because SkillFS validates the sec-core-owned endpoint and its parent against its own effective UID. Each auth file must also be owned by its reader's effective UID. A future different-UID profile requires an explicit endpoint ownership policy as well as separate private key copies that contain the same raw bytes.

Mount or copy the probe script into the trusted probe container. Evidence can use a private mounted directory at $C7_EVIDENCE; otherwise write inside the probe container and retrieve each file with kubectl cp. Neither the probe nor the evidence volume belongs in the workload container.

3. Prepare the fixture

Use these paths consistently in the trusted containers:

export C7_SOURCE=/var/lib/skillfs/source
export C7_MOUNT=/var/lib/skillfs/shared/mount
export C7_RUNTIME=/run/anolisa
export C7_SECRET=/run/anolisa/auth/skillfs.key
export C7_CONTROL=/run/anolisa/skillfs/control.sock
export C7_NOTIFY=/run/anolisa/probe/notify.sock
export C7_PROBE=/workspace/src/skillfs/scripts/container-peer-auth-probe.py

Generate one raw random key before creating the Kubernetes Secret:

umask 077
head -c 32 /dev/urandom >skillfs.key
test "$(wc -c <skillfs.key)" -eq 32
kubectl -n "$C7_NAMESPACE" create secret generic skillfs-c7-auth \
  --from-file=skillfs.key=skillfs.key

Seed a Flat-layout Skill with a visible snapshot. The live directory must also contain SKILL.md so skill.resolveLiveSource can verify it:

install -d -m 0755 \
  "$C7_SOURCE/weather/.skill-meta/versions/v000001.snapshot"
printf '%s\n' '---' 'name: weather' 'description: C7 validation' '---' \
  >"$C7_SOURCE/weather/SKILL.md"
cp "$C7_SOURCE/weather/SKILL.md" \
  "$C7_SOURCE/weather/.skill-meta/versions/v000001.snapshot/SKILL.md"
printf '%s\n' '{"schemaVersion":1,"target":".skill-meta/versions/v000001.snapshot"}' \
  >"$C7_SOURCE/weather/.skill-meta/activation.json"

Start SkillFS in the foreground with the explicit container profile:

skillfs mount "$C7_SOURCE" "$C7_MOUNT" \
  --foreground --allow-other \
  --security --activation-mode file \
  --notify-socket "$C7_NOTIFY" \
  --notify-auth-key-file "$C7_SECRET" \
  --control-socket "$C7_CONTROL" \
  --trusted-peer-key-file "$C7_SECRET"

Before proceeding, require all of the following:

  • the SkillFS process remains running;
  • the mount appears in /proc/self/mountinfo;
  • skills/weather/SKILL.md is readable through the workload's propagated view; and
  • the control socket is mode 0600 in a private runtime directory; and
  • the notify listener uses an owner-matched socket with no group/other bits under an owner-matched directory with no group/other bits (0700 is the recommended default).

4. Positive control tests

Run these commands from the trusted probe container:

python3 "$C7_PROBE" control \
  --socket "$C7_CONTROL" --secret "$C7_SECRET" \
  --request '{"schemaVersion":"1","method":"ping"}' \
  | tee "$C7_EVIDENCE/control-ping.json"

python3 "$C7_PROBE" control \
  --socket "$C7_CONTROL" --secret "$C7_SECRET" \
  --request '{"schemaVersion":"1","method":"status"}' \
  | tee "$C7_EVIDENCE/control-status.json"

python3 "$C7_PROBE" control \
  --socket "$C7_CONTROL" --secret "$C7_SECRET" \
  --request "{\"schemaVersion\":\"1\",\"method\":\"skill.resolveLiveSource\",\"canonicalSkillDir\":\"$C7_SOURCE/weather\"}" \
  | tee "$C7_EVIDENCE/control-resolve.json"

Expected results:

  • each process exits zero after mutual authentication;
  • ping returns ok=true and pong=true;
  • status retains the existing schema v1 business response;
  • resolve returns managed=true, skillId=weather, liveSkillDir=$C7_SOURCE/weather, and transport=shared_path; and
  • the printed nonce differs for every connection.

5. Negative control tests

5.1 Plain and wrong-key peers

Intentionally mount the runtime socket, but not the real auth file, into the attacker container. Run it first as the normal workload UID and repeat as UID 0, matching SkillFS's effective UID:

python3 "$C7_PROBE" plain --socket "$C7_CONTROL"
umask 077
head -c 32 /dev/urandom >/tmp/wrong-skillfs.key
if python3 "$C7_PROBE" control \
  --socket "$C7_CONTROL" --secret /tmp/wrong-skillfs.key \
  --request '{"schemaVersion":"1","method":"ping"}'; then
  echo 'FAIL: wrong-key peer was accepted' >&2
  exit 1
else
  echo 'PASS: wrong-key peer was rejected'
fi

The plain probe must print PASS. The wrong-key command must fail without a business response. SkillFS must remain mounted and a subsequent trusted ping must still succeed.

5.2 Replay, business tampering, and slow partial frames

python3 "$C7_PROBE" replay \
  --socket "$C7_CONTROL" --secret "$C7_SECRET" --timeout 6
python3 "$C7_PROBE" tamper \
  --socket "$C7_CONTROL" --secret "$C7_SECRET" --timeout 6
python3 "$C7_PROBE" slow \
  --socket "$C7_CONTROL" --interval 1 --bound 7
python3 "$C7_PROBE" control \
  --socket "$C7_CONTROL" --secret "$C7_SECRET" \
  --request '{"schemaVersion":"1","method":"ping"}'

The replay must observe a new nonce and receive no auth.ok. The tamper probe must complete a valid handshake but receive no response after changing the raw request without a matching tag. The slow sender must be disconnected within seven seconds. The final trusted request must pass, proving the single-request listener recovered.

5.3 Secret and configuration startup gates

For each row, start a fresh SkillFS process and assert non-zero exit before a FUSE mount or socket is left behind:

CaseSetupExpected diagnostic
missingconfigured file is absentcannot open authentication key
projected symlinkdirect Kubernetes Secret entryno-follow open failure
FIFOmode 0600, no writerregular-file rejection without blocking
short31 raw bytesmust contain 32–4096 bytes
oversized4097 raw bytesmust contain 32–4096 bytes
permissivemode 0640 or 0644group or other permissions rejected
wrong ownerowner differs from effective UIDeffective-user ownership rejected
half-hardenedHMAC control plus notify socket, but no notify keynotify auth key required
mixed modesboth trusted peer executable and keymutually exclusive

After every failure, capture stderr, confirm no residual mount with findmnt, and confirm that neither control nor notify socket remains.

6. Positive notify test

Start the independent notify server in the trusted probe container before causing the mutation:

python3 "$C7_PROBE" notify-server \
  --socket "$C7_NOTIFY" --secret "$C7_SECRET" \
  --output "$C7_EVIDENCE/notify-request.json" \
  >"$C7_EVIDENCE/notify-probe.log" 2>&1 &
C7_NOTIFY_PID=$!

Wait for READY, then write through the workload-visible FUSE view:

grep -q '^READY:' "$C7_EVIDENCE/notify-probe.log"
date -u +%FT%TZ >"$C7_MOUNT/skills/weather/validation.txt"
wait "$C7_NOTIFY_PID"
python3 -m json.tool "$C7_EVIDENCE/notify-request.json"

Expected results:

  • the probe prints PASS: authenticated notify v2 recorded;
  • the request method is skill_ledger.skillfs_notify_change;
  • params.schemaVersion is 2;
  • params.canonicalSkillDir is $C7_SOURCE/weather;
  • params.skillId is weather and the changed path is relative; and
  • normal FUSE I/O completes even if notification delivery later fails.

Startup reconcile may produce the first notify. If so, save it as separate evidence, restart the one-shot probe, then perform the workload mutation.

7. Negative notify server proof

Start the probe with an intentionally invalid server proof, then trigger another FUSE mutation:

python3 "$C7_PROBE" notify-server \
  --socket "$C7_NOTIFY" --secret "$C7_SECRET" \
  --output "$C7_EVIDENCE/notify-must-not-exist.json" \
  --wrong-server-proof \
  >"$C7_EVIDENCE/notify-wrong-server.log" 2>&1 &
C7_BAD_NOTIFY_PID=$!
date -u +%FT%TZ >"$C7_MOUNT/skills/weather/wrong-server-proof.txt"
wait "$C7_BAD_NOTIFY_PID"
test ! -e "$C7_EVIDENCE/notify-must-not-exist.json"

The probe must report that no business frame followed the wrong proof. SkillFS must log an authentication failure, keep serving FUSE I/O, and leave activation mapping unchanged.

8. Namespace and volume isolation

From the workload container, record and assert:

test ! -e "$C7_SOURCE"
test ! -e "$C7_SECRET"
test ! -e "$C7_CONTROL"
test ! -e "$C7_NOTIFY"
cat "$C7_MOUNT/skills/weather/SKILL.md" >/dev/null

Record PID and mount namespace identifiers from each container and require the SkillFS and trusted probe values to differ. Do not enable a shared PID namespace to make executable authentication work.

For the deliberate attacker case, mount only the runtime volume and repeat the plain and wrong-key tests. Seeing the socket must not grant authority.

9. Restart and shutdown

  1. Record the SkillFS container restart count and a successful nonce.
  2. Terminate PID 1 in the SkillFS sidecar and wait for kubelet to restart it.
  3. Require the mount probe to pass again and issue another authenticated ping; its nonce must be fresh.
  4. Restart the trusted notify probe and require a new mutation to authenticate and deliver.
  5. Start the slow control probe, terminate SkillFS, and require shutdown and unmount to finish within the configured handshake bound plus the normal container termination grace period.
  6. Delete the Pod normally, wait for termination, and recreate it on the same node. Startup must not report a residual FUSE mount or stale socket.

Save pre/post restart counts, Pod events, SkillFS logs, probe output, and mount probe output.

10. Credential disclosure check

Collect SkillFS and probe logs, then run a comparison inside a trusted container without printing the credential encodings:

python3 - "$C7_SECRET" "$C7_EVIDENCE/combined.log" <<'PY'
import base64
import pathlib
import sys

secret = pathlib.Path(sys.argv[1]).read_bytes()
logs = pathlib.Path(sys.argv[2]).read_bytes()
markers = (secret, secret.hex().encode(), base64.b64encode(secret))
if any(marker in logs for marker in markers):
    raise SystemExit("FAIL: credential material appears in logs")
print("PASS: raw, hex, and Base64 credential material absent from logs")
PY

Also inspect JSON responses and protocol-event output. They may identify the authentication mode, but must contain no secret, proof, or reusable credential.

11. Acceptance record

Mark each item PASS, FAIL, or NOT RUN with an evidence filename:

IDRequired result
C7-U01real FUSE mount and propagated workload read pass
C7-U02authenticated ping, status, and shared-path resolve pass
C7-U03nonce differs; replay and modified business frames are rejected
C7-U04plain, wrong-key, and same-UID/no-secret peers are rejected
C7-U05slow partial authentication is bounded and listener recovers
C7-U06all secret/config startup gates fail before mounting
C7-U07authenticated notify v2 is received with unchanged business schema
C7-U08invalid notify server proof blocks the business request
C7-U09workload cannot see source, Secret, or private sockets
C7-U10SkillFS and probe restart paths reauthenticate with fresh challenges
C7-U11graceful deletion leaves no residual FUSE mount or socket
C7-U12logs, responses, and events disclose no credential material

Any failed required row blocks unilateral acceptance. NOT RUN is allowed only when the environment cannot supply the required FUSE, privilege, propagation, or namespace feature, and the exact blocker is recorded.

12. Interpretation

A full pass is sufficient to ask sec-core maintainers to implement or review against the frozen contract in container-peer-authentication.md. It is not sufficient to close #2439. Closure additionally requires real agent-sec-core resolver, notify, activation, failure, and restart integration in separate containers, followed by the focused ACK record.