Was a scaffold (fixture TODO, fault-injection a no-op echo). Implemented against the live datapath: fixtures/allowed-pair.yaml = default-deny baseline + source + dest(:8080) + the sole explicit allow. The test verifies source->dest works (ALLOWED_OK), then REVOKES both halves of the allow (real fault: kubectl delete networkpolicy), and asserts connectivity fails CLOSED (FAIL_CLOSED_OK) while DNS still resolves (DNS_OK). Proves default-deny is datapath-enforced and fails closed on revocation. DNS check asserts the resolved answer, since busybox nslookup exits non-zero on a successful forward lookup. PASSES (74.8s). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
97 lines
4.2 KiB
YAML
97 lines
4.2 KiB
YAML
# =============================================================================
|
|
# L3 FAULT-INJECTION — Default-deny fail-closed (R-NET-01, R-NET-02) *SAFETY-CRITICAL* [RUNNABLE]
|
|
# Catalog/spec: docs/14 R-TEST-05 lists default-deny among the behaviors that MUST
|
|
# be proven by FAULT INJECTION (not happy-path). L3 partner of tests/l2-behavioral/default-deny.
|
|
#
|
|
# GIVEN a source that can reach dest:8080 ONLY because of an explicit allow rule,
|
|
# WHEN the allow rule is REVOKED (fault: delete both halves of the NetworkPolicy),
|
|
# THEN connectivity drops to fail-closed within a bounded window (the baseline denies
|
|
# once the allow is gone), while DNS still resolves.
|
|
#
|
|
# Proves the deny is enforced by the live datapath, not merely declared, and that
|
|
# removing access fails CLOSED rather than lingering open.
|
|
# =============================================================================
|
|
apiVersion: chainsaw.kyverno.io/v1alpha1
|
|
kind: Test
|
|
metadata:
|
|
name: default-deny-failclosed
|
|
labels:
|
|
conformance.home.k3s/R-NET-01: "true"
|
|
conformance.home.k3s/R-NET-02: "true"
|
|
annotations:
|
|
conformance.home.k3s/requirements: "R-NET-01, R-NET-02"
|
|
conformance.home.k3s/layer: "L3"
|
|
conformance.home.k3s/safety-critical: "true"
|
|
conformance.home.k3s/executable: "true"
|
|
conformance.home.k3s/intent: >-
|
|
Revoking an allow rule drops connectivity to fail-closed immediately; DNS
|
|
still works — the datapath enforces default-deny, not just declares it.
|
|
spec:
|
|
timeouts:
|
|
apply: 90s
|
|
assert: 120s
|
|
exec: 150s
|
|
steps:
|
|
- name: given-source-allowed-to-dest
|
|
try:
|
|
- apply:
|
|
file: fixtures/allowed-pair.yaml
|
|
# No readinessProbe (default-deny drops the kubelet probe) → Ready once Running.
|
|
- assert:
|
|
resource:
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata: { name: source }
|
|
status: { phase: Running }
|
|
- assert:
|
|
resource:
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata: { name: dest }
|
|
status: { phase: Running }
|
|
- script:
|
|
# The explicit allow is the SOLE reason this succeeds (baseline denies all else).
|
|
content: |
|
|
set -eu
|
|
kubectl -n "$NAMESPACE" exec source -- nc -z -w 5 dest 8080 && echo ALLOWED_OK
|
|
check:
|
|
(contains($stdout, 'ALLOWED_OK')): true
|
|
- name: when-revoke-allow-then-fail-closed
|
|
try:
|
|
# -- FAULT INJECTION: revoke the allow (both halves); baseline default-deny remains. --
|
|
- script:
|
|
content: |
|
|
set -eu
|
|
kubectl -n "$NAMESPACE" delete networkpolicy \
|
|
allow-source-to-dest-egress allow-source-to-dest-ingress --wait=true
|
|
echo REVOKED
|
|
- script:
|
|
# Connectivity must fail CLOSED. Poll briefly for the datapath to converge; a
|
|
# persistent success is a LEAK (fails the test).
|
|
content: |
|
|
set -u
|
|
for i in $(seq 1 10); do
|
|
if kubectl -n "$NAMESPACE" exec source -- nc -z -w 5 dest 8080 2>/dev/null; then
|
|
echo "attempt $i: still reachable, waiting for revocation to converge..."; sleep 3
|
|
else
|
|
echo FAIL_CLOSED_OK; exit 0
|
|
fi
|
|
done
|
|
echo "LEAK: dest still reachable after allow revoked" >&2; exit 1
|
|
check:
|
|
(contains($stdout, 'FAIL_CLOSED_OK')): true
|
|
- script:
|
|
# DNS must STILL resolve (baseline permits DNS egress) — the deny is scoped, not total.
|
|
# busybox nslookup can exit non-zero on a successful forward lookup (server-PTR quirk),
|
|
# so assert on the RESOLVED ANSWER in the output, not the exit code.
|
|
content: |
|
|
set -u
|
|
out="$(kubectl -n "$NAMESPACE" exec source -- nslookup kubernetes.default 2>&1 || true)"
|
|
echo "$out"
|
|
echo "$out" | grep -qiE 'kubernetes\.default|10\.43\.' && echo DNS_OK || { echo "DNS_BLOCKED" >&2; exit 1; }
|
|
check:
|
|
(contains($stdout, 'DNS_OK')): true
|
|
finally:
|
|
- script:
|
|
content: 'echo "cleanup via chainsaw namespace teardown"'
|