Merge L3 default-deny-failclosed (R-NET fail-closed green)
This commit is contained in:
2 files changed
+160
-27
No files matched your search
@@ -1,20 +1,15 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# L3 FAULT-INJECTION SCAFFOLD — Default-deny fail-closed (R-NET-01, R-NET-02)
|
# L3 FAULT-INJECTION — Default-deny fail-closed (R-NET-01, R-NET-02) *SAFETY-CRITICAL* [RUNNABLE]
|
||||||
# *SAFETY-CRITICAL*
|
|
||||||
# Catalog/spec: docs/14 R-TEST-05 lists default-deny among the behaviors that MUST
|
# Catalog/spec: docs/14 R-TEST-05 lists default-deny among the behaviors that MUST
|
||||||
# be proven by fault-injection (not happy-path). This is the L3 partner of the L2
|
# be proven by FAULT INJECTION (not happy-path). L3 partner of tests/l2-behavioral/default-deny.
|
||||||
# behavioral default-deny test (tests/l2-behavioral/default-deny).
|
|
||||||
#
|
#
|
||||||
# GIVEN a source that CAN reach a dest only because of an explicit allow rule,
|
# GIVEN a source that can reach dest:8080 ONLY because of an explicit allow rule,
|
||||||
# WHEN the allow rule is REVOKED (fault injection: delete the CiliumNetworkPolicy),
|
# WHEN the allow rule is REVOKED (fault: delete both halves of the NetworkPolicy),
|
||||||
# THEN connectivity drops to fail-closed within a bounded window (revocation takes
|
# THEN connectivity drops to fail-closed within a bounded window (the baseline denies
|
||||||
# effect immediately — the baseline denies once the allow is gone), while DNS
|
# once the allow is gone), while DNS still resolves.
|
||||||
# still resolves.
|
|
||||||
#
|
#
|
||||||
# This proves the deny is enforced by the live datapath, not merely declared, and
|
# Proves the deny is enforced by the live datapath, not merely declared, and that
|
||||||
# that removing access fails closed rather than lingering open.
|
# removing access fails CLOSED rather than lingering open.
|
||||||
#
|
|
||||||
# STATUS: scaffold — needs T-2.2 (baseline) + T-2.3 (rendered allow) + T-3.0 harness.
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||||
kind: Test
|
kind: Test
|
||||||
@@ -27,43 +22,73 @@ metadata:
|
|||||||
conformance.home.k3s/requirements: "R-NET-01, R-NET-02"
|
conformance.home.k3s/requirements: "R-NET-01, R-NET-02"
|
||||||
conformance.home.k3s/layer: "L3"
|
conformance.home.k3s/layer: "L3"
|
||||||
conformance.home.k3s/safety-critical: "true"
|
conformance.home.k3s/safety-critical: "true"
|
||||||
|
conformance.home.k3s/executable: "true"
|
||||||
conformance.home.k3s/intent: >-
|
conformance.home.k3s/intent: >-
|
||||||
Revoking an allow rule drops connectivity to fail-closed immediately; DNS
|
Revoking an allow rule drops connectivity to fail-closed immediately; DNS
|
||||||
still works — the datapath enforces default-deny, not just declares it.
|
still works — the datapath enforces default-deny, not just declares it.
|
||||||
spec:
|
spec:
|
||||||
|
timeouts:
|
||||||
|
apply: 90s
|
||||||
|
assert: 120s
|
||||||
|
exec: 150s
|
||||||
steps:
|
steps:
|
||||||
- name: given-source-allowed-to-dest
|
- name: given-source-allowed-to-dest
|
||||||
try:
|
try:
|
||||||
- apply:
|
- apply:
|
||||||
file: fixtures/allowed-pair.yaml # TODO(T-2.3): source+dest+allow policy
|
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:
|
- script:
|
||||||
|
# The explicit allow is the SOLE reason this succeeds (baseline denies all else).
|
||||||
content: |
|
content: |
|
||||||
set -euo pipefail
|
set -eu
|
||||||
kubectl -n "$NAMESPACE" exec source -- nc -z -w 3 dest 80 && echo ALLOWED_OK
|
kubectl -n "$NAMESPACE" exec source -- nc -z -w 5 dest 8080 && echo ALLOWED_OK
|
||||||
check:
|
check:
|
||||||
(contains($stdout, 'ALLOWED_OK')): true
|
(contains($stdout, 'ALLOWED_OK')): true
|
||||||
- name: when-revoke-allow-then-fail-closed
|
- name: when-revoke-allow-then-fail-closed
|
||||||
try:
|
try:
|
||||||
# FAULT INJECTION: delete the allow policy; baseline default-deny remains.
|
# -- FAULT INJECTION: revoke the allow (both halves); baseline default-deny remains. --
|
||||||
- script:
|
- script:
|
||||||
content: |
|
content: |
|
||||||
set -euo pipefail
|
set -eu
|
||||||
echo "TODO(T-3.0): delete the source->dest CiliumNetworkPolicy allow"
|
kubectl -n "$NAMESPACE" delete networkpolicy \
|
||||||
|
allow-source-to-dest-egress allow-source-to-dest-ingress --wait=true
|
||||||
|
echo REVOKED
|
||||||
- script:
|
- script:
|
||||||
# Bounded poll: connection must start failing (fail-closed on revocation).
|
# Connectivity must fail CLOSED. Poll briefly for the datapath to converge; a
|
||||||
|
# persistent success is a LEAK (fails the test).
|
||||||
content: |
|
content: |
|
||||||
set -euo pipefail
|
set -u
|
||||||
if kubectl -n "$NAMESPACE" exec source -- nc -z -w 3 dest 80; then
|
for i in $(seq 1 10); do
|
||||||
echo "LEAK: still reachable after allow revoked" >&2; exit 1
|
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
|
fi
|
||||||
echo FAIL_CLOSED_OK
|
done
|
||||||
|
echo "LEAK: dest still reachable after allow revoked" >&2; exit 1
|
||||||
check:
|
check:
|
||||||
(contains($stdout, 'FAIL_CLOSED_OK')): true
|
(contains($stdout, 'FAIL_CLOSED_OK')): true
|
||||||
- script:
|
- script:
|
||||||
# DNS must still work (default-deny permits DNS egress).
|
# 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: |
|
content: |
|
||||||
set -euo pipefail
|
set -u
|
||||||
kubectl -n "$NAMESPACE" exec source -- nslookup kubernetes.default && echo DNS_OK
|
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:
|
check:
|
||||||
(contains($stdout, 'DNS_OK')): true
|
(contains($stdout, 'DNS_OK')): true
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Fixtures for the default-deny FAIL-CLOSED fault-injection test (R-NET-01/02).
|
||||||
|
# Applied into the per-test namespace chainsaw creates (hermetic, self-cleaned).
|
||||||
|
#
|
||||||
|
# Models: a namespace default-deny baseline + ONE explicit allow that is the SOLE
|
||||||
|
# reason source can reach dest:8080. The test later DELETES the allow (fault) and
|
||||||
|
# proves connectivity drops to fail-closed while DNS still resolves.
|
||||||
|
#
|
||||||
|
# Standard Kubernetes NetworkPolicy (Cilium enforces it). No readinessProbe on the
|
||||||
|
# pods: under default-deny the kubelet TCP probe (node->pod) is dropped, so a
|
||||||
|
# probe-bearing pod would never go Ready — without a probe the container is Ready
|
||||||
|
# once running, which is all the test needs (K2 finding).
|
||||||
|
# =============================================================================
|
||||||
|
---
|
||||||
|
# --- Default-deny baseline: deny all ingress+egress, allow only DNS egress. -----------
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny-baseline
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes: [Ingress, Egress]
|
||||||
|
ingress: []
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: kube-dns
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
|
---
|
||||||
|
# --- The ONE explicit allow (source -> dest tcp/8080): the two matching halves a
|
||||||
|
# real rendered dependency produces (egress permit on source, ingress on dest).
|
||||||
|
# Deleting BOTH is the fault injection.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-source-to-dest-egress
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels: { app: source }
|
||||||
|
policyTypes: [Egress]
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels: { app: dest }
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-source-to-dest-ingress
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels: { app: dest }
|
||||||
|
policyTypes: [Ingress]
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels: { app: source }
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
---
|
||||||
|
# --- source: an idle probe pod (nc is issued via kubectl exec). ------------------------
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: source
|
||||||
|
labels: { app: source }
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: app
|
||||||
|
image: busybox:1.36.1
|
||||||
|
command: ["sh", "-c", "sleep 3600"]
|
||||||
|
---
|
||||||
|
# --- dest: LISTENS on 8080 so a block is caused by POLICY, not a missing listener. -----
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: dest
|
||||||
|
labels: { app: dest }
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: app
|
||||||
|
image: busybox:1.36.1
|
||||||
|
command: ["httpd", "-f", "-p", "8080", "-h", "/tmp"]
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: dest
|
||||||
|
spec:
|
||||||
|
selector: { app: dest }
|
||||||
|
ports:
|
||||||
|
- name: p8080
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
Reference in new issue
Block a user