# ============================================================================= # 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"'