Draw the flow before choosing the control
In a fictional funds scenario, a reconciler calls ledger during a processing window. Requirements differ: content must be protected on the inter-node link, only intended clients should reach the service and the application must authorize each operation. Draw source, destination, namespace, node, port and requested name. Identify Service translation, DNS resolution and inter-node transport. This description lets you choose observations matching the requirement. An HTTP 200 response shows that the particular call received a response; alone it establishes neither link confidentiality nor correct business authorization. Likewise, a timeout does not prove policy enforcement: a stopped server or missing route could produce failure. First establish a positive call returning a known synthetic marker. Then change one boundary at a time and retain a control request to interpret differences. Practice uses Cilium with WireGuard and NetworkPolicy. The tunnel protects covered traffic between nodes; policy restricts connections by selectors, directions and ports. This laboratory installs neither Istio nor individual application TLS certificates. The banking case is original and educational, not an internal BNP Paribas procedure. Its APS value lies in separating technical observation from continuity decisions and ownership of each control. Record these distinctions before the change window so a successful network probe is not later reused as approval for an untested business operation.
Choose topology and observation point
Cilium WireGuard establishes peers between node agents. Testing two Pods on the same node does not traverse the inter-node link or establish the property required on that link. In this exercise, the client is on the control plane and the server is on a worker. Observed names and addresses enter the report. This placement serves the exercise; it is not a recommendation for production application placement on a control plane. Capture location also changes meaning. The cilium_wg0 interface can expose the inner packet, including HTTP. This is not equivalent to finding the same text in the representation sent over the outer interface. A lab observer in the node network namespace counts only IPv4 UDP 51871 packets between the two node addresses and checks for the synthetic marker. It retains no payload. Confirm packets were present; an empty capture does not demonstrate confidentiality. Relate results to peers and requests actually completed during the window. Absence of one marker in a limited capture does not certify every cluster flow or the cryptographic implementation. The objective is observable evidence of the exercised path with explained limits. Same-node traffic, hostNetwork, external suppliers and other routing modes need their own assessment. Assign an owner to each additional requirement so a scoped result does not silently become a claim about the entire platform.
Prepare and run the disposable environment
The attached script requires Python 3, Docker, kind 0.33.0, kubectl 1.35.8, Helm 3.19.0 and the official Cilium 1.20.2 chart identified in code. Arguments --kind, --kubectl, --helm, --chart and a new --report specify paths. It creates its own two-node cluster and private kubeconfig. Kind's default CNI is disabled so Cilium enforces the policies. Nodes can temporarily remain NotReady before the CNI becomes operational. Images are obtained by digest and loaded into nodes using exercise-specific aliases to reduce download dependency during startup. The report relates verified origin, alias and image identifier. The observer uses hostNetwork and NET_RAW only inside the disposable cluster. It mounts no personal directories and uses no production credentials. Code removes the cluster, owned aliases and temporary files afterwards; cached image content can remain available. Predict results before running: all three clients initially connect; an ingress policy with no allowances blocks them; a bounded exception permits only the intended reconciler. Two final runs passed 42 observations each, including 12 repeated requests during capture; these are not 42 distinct practical tasks. Initial exercises observed the encrypted path and selector differences, but also identified a DNS-error detail and an installation that did not become ready before its deadline. Keep development failures alongside final evidence so another engineer can distinguish corrected test assumptions from verified application behavior. The validated runner targets ARM64 Docker; image export selects linux/arm64. Another system must adapt and validate that dependency rather than assuming this evidence covers every architecture.
Demonstrate who can connect and who must fail
The exercise matrix has three clients: role=reconciler in dr-funds, another role in the same namespace and role=reconciler in dr-vendor. This combination distinguishes correct selection from a rule accepting equal labels in every namespace. Each client's positive baseline is recorded before restrictions, avoiding attribution of pre-existing unavailability to a policy. To allow only the intended combination, namespaceSelector and podSelector appear in the same from entry. The exercise then separates them into distinct entries and observes broader access for the other-role client inside the permitted namespace. A standalone podSelector remains relative to the policy namespace; it does not automatically admit the equal-label client in dr-vendor. Explain the difference using rule structure and the request matrix rather than YAML appearance alone. The script restores the intersection before continuing. It uses new connections for every observation and waits for the configured result because API acceptance does not give the exact dataplane-adoption time. These tests do not establish termination of older sessions. During an incident, that limit matters: blocking new connections and closing an established session are different claims needing separate observations. Carry the distinction into handover so the response team does not infer session revocation from a fresh connection timing out.
Diagnose both directions and the DNS dependency
Server ingress permission is insufficient when the source is isolated for egress without the required allowance. The lab introduces that state on reconciler and observes IP-based failure. It then adds only egress to the server on port 8080. IP retrieval recovers, while the Service name still depends on resolution. This sequence separates application reachability from DNS problems. The next allowance includes selected CoreDNS Pods in kube-system over UDP and TCP 53. The test returns to ledger.dr-funds.svc.cluster.local and requires the expected marker rather than merely a command without errors. DNS placement is specific to this topology. With NodeLocal DNSCache or another design, confirm actual destination before reusing selectors; copying a rule without that check may leave failure unresolved. The first exercise returned gaierror with text “Try again,” while validation expected other words. The correction uses the structured resolver-error type while preserving the requirement for DNS failure. This is useful APS practice: messages differ across libraries and systems, but error classification should remain tied to the failed layer. Do not automatically respond to a resolution error by broadening business permissions or removing every segmentation control. Present the observed dependency and the smallest justified change to the change owner, then retest required and forbidden paths.
Review accumulated allowances and plugin behavior
After restoring the authorized name-based call, the exercise adds another policy permitting ingress without source restriction. The dr-vendor client connects again. The bounded policy still exists but no longer describes the complete allowed surface. Removing the broad allowance makes the out-of-scope client fail again while reconciler remains functional. Record both results before closing the temporary exception. Union of allowances applies to the ordinary NetworkPolicies used here. Do not turn it into a universal statement about every policy format and tier supported by Cilium. Documentation distinguishes Kubernetes policies, CiliumNetworkPolicy and globally scoped policies with their own semantics. When several coexist, inventory the effective set before attributing unexpected access to one rule. Another example is ipBlock. Cilium 1.20.2 documentation states that it does not match internal Pod or node IPs by default; policyCIDRMatchMode can change that behavior. The laboratory uses selectors and does not test that option. The conceptual exercise asks why widening a CIDR does not replace understanding destination classification. Plugin version, options and topology should accompany any recommendation transferred from another environment. A useful incident note states exactly which policy family was inspected and which were absent or remain to be assessed, preventing a locally correct explanation from becoming an incorrect platform-wide rule.
Investigate tunnel limits without arbitrary control changes
If an inter-node flow fails after a change, start with the changed path. The default WireGuard endpoint uses UDP 51871. Compare node reachability, remote public key, peer-associated IPs and new-endpoint state. A recreated Pod may have a different IP; success for an old Pod does not establish updated state for the new one. Keep this hypothesis separate from application-authorization failures. Additional node-traffic encryption has another scope and a default control-plane-node exclusion to avoid a bootstrap dependency: reaching the API can be necessary to update the public key. This practice does not enable that mode. It also does not test CNI chaining, MTU changes or external suppliers. Those cases define decision boundaries and identify further evidence without being presented as executed. For degradation after encapsulation, compare smaller and larger calls and investigate MTU and fragmentation before changing RBAC without a link to the symptom. For an external-supplier report, draw the egress segment and its protection control. A node's WireGuard key is not an individual TLS certificate for each Pod. When a requirement concerns workload identity or operation authorization, identify and validate its corresponding mechanism. Record this requirement separately from the successful tunnel test so the architecture review can assess both without conflating their evidence.
Prepare resumption and handover
The funds case ends with a matrix-based decision rather than one green health check. Provide permitted calls, refused calls, observed addresses and nodes, effective policies and the Service-name result. Identify the broad exception's effect and confirm its removal. If the application requires reconciliation of earlier results, that validation belongs to the business flow and is not replaced by a synthetic HTTP marker. Handover should retain cluster and plugin versions, image origins and digests, script hash, observation window and limitations. Include failed development exercises and reasons for correction without classifying installation timeout as evidence of policy failure. Repeating the laboratory establishes behavior only within stated scope; it is neither a full practical mock nor independent specialist review. Summary: establish a control request, choose an inter-node path for tunnel assessment, observe the correct capture point and evaluate selectors, egress, ingress and DNS. Confirm both what should work and what must remain blocked. Connect this practice to observability, change, workload identity, incident response and continuity. In questions, justify the next action from the symptom and explain why alternatives do not establish the stated requirement. Keep any unresolved business or architecture condition visible in the resumption decision so the next shift can continue from facts rather than infer completion from connectivity alone.
#!/usr/bin/env python3
"""Original CKS network exercise: disposable two-node kind, Cilium and synthetic HTTP.
Requires Docker,kind0.33.0,kubectl1.35.8,Helm3.19.0 and the official Cilium1.20.2 chart.
Usage: python3 run.py --kind PATH --kubectl PATH --helm PATH --chart PATH --report NEW.json
This validated runner targets an ARM64 Docker engine. Uses its own cluster and kubeconfig. Never applies resources to an existing cluster.
"""
import argparse,datetime,hashlib,json,os,pathlib,shutil,subprocess,tempfile,time,uuid
NODE='kindest/node:v1.35.8@sha256:07b2536e30b803ed61d1677a79df6115f798ce64c80f9e22f6ed45afd09323c0'
PYTHON='python:3.13-alpine@sha256:2d9aefe2fef018a7eb2c13064c89c71929800fd2e5dccdbf52ea5da5bb8d929a'
CILIUM='quay.io/cilium/cilium:v1.20.2@sha256:2939231d0d3e3ebddcd80fffa168b7ddcc78fdf0dc864d1c8c126ff523c54f01'
OPERATOR='quay.io/cilium/operator-generic:v1.20.2@sha256:64d8798350e8569b8e7622563fed6e44dce2625f311e4651b774816516c744fc'
CHART_SHA='b2afd87b7f75f875f92a14559f14f59b7babbb479d968e3fd625a20bf30ec20e'
MARKER='DR_SYNTHETIC_FUNDS_NETWORK_20261007'
SERVER="""from http.server import BaseHTTPRequestHandler,HTTPServer
class H(BaseHTTPRequestHandler):
def do_GET(self):
body=b'DR_SYNTHETIC_FUNDS_NETWORK_20261007'self.send_response(200);self.end_headers;self.wfile.write(body)
def log_message(self,*args):pass
HTTPServer(('0.0.0.0',8080),H).serve_forever
"""
CLIENT="""import json,sys,urllib.request
try:
with urllib.request.urlopen(sys.argv[1],timeout=2) as r:print(json.dumps(dict(ok=r.status==200,body=r.read.decode)))
except Exception as e:print(json.dumps(dict(ok=False,error=type(e).__name__,detail=str(e),reasonType=type(getattr(e,'reason',None)).__name__,reasonErrno=getattr(getattr(e,'reason',None),'errno',None))))
"""
CAPTURE="""import socket,struct,time,json,pathlib,sys
s=socket.socket(socket.AF_PACKET,socket.SOCK_RAW,socket.htons(3));s.bind(('eth0',0));s.settimeout(.25)
peers=set(sys.argv[1:3]);packets=0;marker=False;start=time.monotonic;pathlib.Path('/tmp/ready').write_text('ready')
while time.monotonic-start<18:
try:p=s.recv(65535)
except TimeoutError:continue
if len(p)<42 or p[12:14]!=b'\\x08\\x00' or p[23]!=17:continue
ihl=(p[14]&15)*4;src=socket.inet_ntoa(p[26:30]);dst=socket.inet_ntoa(p[30:34]);off=14+ihl
if {src,dst}!=peers or len(p)<off+8:continue
ports=struct.unpack('!HH',p[off:off+4])
if 51871 not in ports:continue
packets+=1;marker=marker or b'DR_SYNTHETIC_FUNDS_NETWORK_20261007' in p
print(json.dumps(dict(interface='eth0',wireguardPackets=packets,syntheticMarkerObserved=marker,scope='Only IPv4 UDP51871 packets between the two owned node IPs during the observation window. No payload retained.')))
"""
def main:
ap=argparse.ArgumentParser
for n in ['kind','kubectl','helm','chart','report']:ap.add_argument('--'+n,required=True)
a=ap.parse_args;out=pathlib.Path(a.report);assert not out.exists;assert hashlib.sha256(pathlib.Path(a.chart).read_bytes).hexdigest==CHART_SHA
os.umask(0o077);work=pathlib.Path(tempfile.mkdtemp(prefix='dr-cks-network-'));kc=work/'kubeconfig'name='dr-cks-network-'+uuid.uuid4.hex[:10];control=name+'-control-plane'worker=name+'-worker'ns='dr-funds'foreign='dr-vendor'started=False;observations=[];probes=[];ownedAliases=[];appImage=name+'-python:lab'
env={**os.environ,'HELM_CACHE_HOME':str(work/'helm-cache'),'HELM_CONFIG_HOME':str(work/'helm-config'),'HELM_DATA_HOME':str(work/'helm-data')}
report=dict(startedAt=datetime.datetime.now(datetime.timezone.utc).isoformat,cluster=name,scriptSha256=hashlib.sha256(pathlib.Path(__file__).read_bytes).hexdigest,chartSha256=CHART_SHA,nodeImage=NODE,applicationImage=PYTHON,observations=observations,probes=probes,actualCluster=True,actualCNIEnforcement=True,actualCrossNodeTraffic=True,syntheticOnly=True,applicationMTLS=False,serviceMesh=False,fullPracticalMock=False,independentVerification=False)
def run(cmd,data=None,timeout=40,check=True):
r=subprocess.run(cmd,input=data,text=True,capture_output=True,timeout=timeout,env=env)
if check and r.returncode:raise RuntimeError(str(cmd[:3])+' failed: '+r.stderr[-1000:])
return r
def k(*args,obj=None,timeout=40,check=True):return run([a.kubectl,'--kubeconfig',str(kc),'--context','kind-'+name,'--cache-dir',str(work/'cache'),'--request-timeout=20s',*args],json.dumps(obj) if obj is not None else None,timeout,check)
def apply(o):k('apply','-f','-',obj=o)
def observe(n,v,e):observations.append(dict(name=n,observed=v,expected=e,passed=v==e));print(n,flush=True);assert v==e,(n,v,e)
def wait(fn,limit=90):
end=time.monotonic+limit
while time.monotonic<end:
try:
v=fn
if v:return v
except (RuntimeError,subprocess.TimeoutExpired):pass
time.sleep(2)
raise AssertionError('Timed out waiting for observed state')
def pod(n,namespace,node,labels,command,host=False):
c=dict(name='app',image=appImage,imagePullPolicy='IfNotPresent',command=command,resources={'requests':{'cpu':'20m','memory':'32Mi'},'limits':{'memory':'128Mi'}})
if host:c['securityContext']={'capabilities':{'add':['NET_RAW']}}
return dict(apiVersion='v1',kind='Pod',metadata=dict(name=n,namespace=namespace,labels=labels),spec=dict(nodeName=node,automountServiceAccountToken=False,hostNetwork=host,dnsPolicy='ClusterFirstWithHostNet' if host else 'ClusterFirst',containers=[c],tolerations=[{'operator':'Exists'}]))
def request(podname='allowed',namespace=ns,url=None):
d=json.loads(k('-n',namespace,'exec',podname,'--','python','-c',CLIENT,url or target).stdout);probes.append(dict(pod=podname,namespace=namespace,url=url or target,**d));return d
def outcome(label,podname='allowed',namespace=ns,ok=True,url=None):
def check:
d=request(podname,namespace,url)
if ok:return d.get('ok') and d.get('body')==MARKER
return d.get('ok') is False and 'timed out' in d.get('detail','').lower
wait(check);observe(label,True,True)
def policy(n,namespace,selector,types,**rules):return dict(apiVersion='networking.k8s.io/v1',kind='NetworkPolicy',metadata=dict(name=n,namespace=namespace),spec=dict(podSelector={'matchLabels':selector},policyTypes=types,**rules))
try:
config=dict(kind='Cluster',apiVersion='kind.x-k8s.io/v1alpha4',networking=dict(apiServerAddress='127.0.0.1',disableDefaultCNI=True),nodes=[dict(role='control-plane'),dict(role='worker')]);(work/'kind.json').write_text(json.dumps(config));started=True;print('creating-owned-cluster '+name,flush=True)
run([a.kind,'create','cluster','--name',name,'--image',NODE,'--config',str(work/'kind.json'),'--kubeconfig',str(kc),'--wait','120s'],timeout=240)
versions=json.loads(k('version','-o','json').stdout);report['versions']=versions;observe('server-version',versions['serverVersion']['gitVersion'],'v1.35.8');observe('client-version',versions['clientVersion']['gitVersion'],'v1.35.8')
helmversion=run([a.helm,'version','--short']).stdout.strip;report['helmVersion']=helmversion;observe('helm-version',helmversion.startswith('v3.19.0'),True)
print('preloading-pinned-images',flush=True)
observe('docker-architecture',run(['docker','info','--format','{{.Architecture}}']).stdout.strip,'aarch64')
report['preloadedImages']=[]
for reference,alias in [(CILIUM,name+'-cilium:lab'),(OPERATOR,name+'-operator:lab'),(PYTHON,appImage)]:
run(['docker','pull',reference],timeout=300);info=json.loads(run(['docker','image','inspect',reference]).stdout)[0]
run(['docker','tag',reference,alias]);ownedAliases.append(alias);report['preloadedImages'].append(dict(source=reference,alias=alias,imageID=info['Id'],repositoryDigests=info['RepoDigests']))
archive=work/'images.tar'run(['docker','image','save','--platform','linux/arm64','-o',str(archive),*ownedAliases],timeout=180)
run([a.kind,'load','image-archive',str(archive),'--name',name],timeout=240);archive.unlink
print('installing-cilium',flush=True)
values={'image.override':name+'-cilium:lab','operator.image.override':name+'-operator:lab','ipam.mode':'kubernetes','operator.replicas':'1','kubeProxyReplacement':'false','encryption.enabled':'true','encryption.type':'wireguard','hubble.enabled':'false','envoy.enabled':'false','l7Proxy':'false'}
cmd=[a.helm,'install','cilium',a.chart,'--namespace','kube-system','--kubeconfig',str(kc),'--kube-context','kind-'+name,'--wait','--timeout','5m']
for key,value in values.items:cmd+=['--set',key+'='+value]
run(cmd,timeout=330);k('-n','kube-system','rollout','status','ds/cilium','--timeout=180s',timeout=200);k('wait','--for=condition=Ready','nodes','--all','--timeout=120s',timeout=140)
agents=json.loads(k('-n','kube-system','get','pods','-l','k8s-app=cilium','-o','json').stdout)['items'];observe('two-cilium-agents',len(agents),2);report['agentImages']=[x['status']['containerStatuses'] for x in agents]
for agent in agents:
text=k('-n','kube-system','exec',agent['metadata']['name'],'--','cilium-dbg','status').stdout
observe('wireguard-enabled-'+agent['spec']['nodeName'],bool('Encryption:' in text and 'Wireguard' in text and 'Peers: 1' in text),True)
for namespace in [ns,foreign]:apply(dict(apiVersion='v1',kind='Namespace',metadata=dict(name=namespace,labels={'dr-team':'funds' if namespace==ns else 'vendor'})))
apply(pod('server',ns,worker,{'app':'ledger'},['python','-u','-c',SERVER]));apply(pod('allowed',ns,control,{'role':'reconciler'},['python','-c','import time;time.sleep(3600)']));apply(pod('wrong-role',ns,control,{'role':'other'},['python','-c','import time;time.sleep(3600)']));apply(pod('foreign-client',foreign,control,{'role':'reconciler'},['python','-c','import time;time.sleep(3600)']))
apply(dict(apiVersion='v1',kind='Service',metadata=dict(name='ledger',namespace=ns),spec=dict(selector={'app':'ledger'},ports=[dict(port=8080,targetPort=8080)])))
for namespace in [ns,foreign]:k('-n',namespace,'wait','--for=condition=Ready','pods','--all','--timeout=150s',timeout=170)
server=json.loads(k('-n',ns,'get','pod','server','-o','json').stdout);client=json.loads(k('-n',ns,'get','pod','allowed','-o','json').stdout);observe('distinct-source-destination-nodes',client['spec']['nodeName']!=server['spec']['nodeName'],True)
target='http://'+server['status']['podIP']+':8080/'service='http://ledger.dr-funds.svc.cluster.local:8080/'report['topology']={x:{'node':p['spec']['nodeName'],'podIP':p['status']['podIP'],'nodeIP':p['status']['hostIP']} for x,p in [('client',client),('server',server)]}
outcome('baseline-allowed');outcome('baseline-wrong-role','wrong-role');outcome('baseline-foreign','foreign-client',foreign);outcome('baseline-service-dns',url=service)
capture=pod('wire-observer',ns,control,{'app':'observer'},['python','-c',CAPTURE,client['status']['hostIP'],server['status']['hostIP']],True);capture['spec']['restartPolicy']='Never'apply(capture)
wait(lambda:k('-n',ns,'exec','wire-observer','--','test','-e','/tmp/ready',check=False).returncode==0)
for _ in range(12):outcome('captured-request-'+str(_));time.sleep(.15)
# Keep the capture Pod alive long enough to retrieve its synthetic summary.
wait(lambda:json.loads(k('-n',ns,'get','pod','wire-observer','-o','json').stdout)['status']['phase']=='Succeeded',limit=40)
# Terminated containers expose logs; the capture program also prints the summary below.
cap=json.loads(k('-n',ns,'logs','wire-observer').stdout);report['capture']=cap;observe('wireguard-packets-observed',cap['wireguardPackets']>0,True);observe('no-marker-in-selected-wire-packets',cap['syntheticMarkerObserved'],False)
apply(policy('default-deny',ns,{'app':'ledger'},['Ingress'],ingress=[]));outcome('deny-blocks-allowed',ok=False);outcome('deny-blocks-wrong-role','wrong-role',ok=False);outcome('deny-blocks-foreign','foreign-client',foreign,False)
peer={'namespaceSelector':{'matchLabels':{'dr-team':'funds'}},'podSelector':{'matchLabels':{'role':'reconciler'}}};ingress=[{'from':[peer],'ports':[{'protocol':'TCP','port':8080}]}]
apply(policy('ledger-access',ns,{'app':'ledger'},['Ingress'],ingress=ingress));outcome('intersection-allows-client');outcome('intersection-blocks-wrong-role','wrong-role',ok=False);outcome('intersection-blocks-foreign','foreign-client',foreign,False)
loose=[{'from':[{'namespaceSelector':peer['namespaceSelector']},{'podSelector':peer['podSelector']}],'ports':[{'protocol':'TCP','port':8080}]}]
# A bare podSelector is scoped to this policy's namespace. The first OR branch still admits wrong-role there.
apply(policy('ledger-access',ns,{'app':'ledger'},['Ingress'],ingress=loose));outcome('or-broadens-same-namespace','wrong-role');outcome('bare-pod-selector-not-cross-namespace','foreign-client',foreign,False)
apply(policy('ledger-access',ns,{'app':'ledger'},['Ingress'],ingress=ingress));outcome('intersection-restored','wrong-role',ok=False)
apply(policy('client-egress',ns,{'role':'reconciler'},['Egress'],egress=[]));outcome('egress-deny-overrides-ingress-allow',ok=False)
serverpeer={'namespaceSelector':{'matchLabels':{'dr-team':'funds'}},'podSelector':{'matchLabels':{'app':'ledger'}}};toapp={'to':[serverpeer],'ports':[{'protocol':'TCP','port':8080}]}
apply(policy('client-egress',ns,{'role':'reconciler'},['Egress'],egress=[toapp]));outcome('server-egress-restores-ip')
d=request(url=service);observe('dns-still-unavailable',not d['ok'] and d.get('reasonType')=='gaierror',True)
dns={'to':[{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':'kube-system'}},'podSelector':{'matchLabels':{'k8s-app':'kube-dns'}}}],'ports':[{'protocol':'UDP','port':53},{'protocol':'TCP','port':53}]}
apply(policy('client-egress',ns,{'role':'reconciler'},['Egress'],egress=[toapp,dns]));outcome('dns-and-service-restored',url=service)
apply(policy('temporary-broad-allow',ns,{'app':'ledger'},['Ingress'],ingress=[{}]));outcome('additive-policy-admits-foreign','foreign-client',foreign)
k('-n',ns,'delete','networkpolicy','temporary-broad-allow');outcome('remove-broad-policy-restores-boundary','foreign-client',foreign,False);outcome('final-authorized-service',url=service)
report['passed']=True
except BaseException as e:
report['passed']=False;report['error']=str(e)
if kc.exists:
report['diagnostics']=k('-n','kube-system','get','pods','-o','wide',check=False).stdout
report['events']=k('-n','kube-system','get','events','--sort-by=.lastTimestamp',check=False).stdout[-6000:]
raise
finally:
if started:run([a.kind,'delete','cluster','--name',name,'--kubeconfig',str(kc)],timeout=90,check=False)
remaining=run(['docker','ps','-a','--filter','label=io.x-k8s.kind.cluster='+name,'--format','{{.Names}}'],check=False);shutil.rmtree(work)
aliasesRemoved=True
for alias in ownedAliases:
result=run(['docker','image','rm',alias],check=False);aliasesRemoved=aliasesRemoved and result.returncode==0
report['cleanup']=dict(ownedImageAliasesRemoved=aliasesRemoved,ownedClusterRemoved=remaining.returncode==0 and not remaining.stdout.strip,temporaryMaterialRemoved=not work.exists,defaultKubeconfigModified=False,realCredentialsUsed=False);report['finishedAt']=datetime.datetime.now(datetime.timezone.utc).isoformat;out.parent.mkdir(parents=True,exist_ok=True);out.write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(dict(passed=report['passed'],observations=len(observations),report=str(out))))
if __name__=='__main__':main
IP calls recover after allowing server egress, but the job still fails until DNS resolution recovers.
Common pitfalls
Treating empty capture as encryption evidence, confusing local selectors with every namespace and overlooking DNS dependency.
Related topics: NetworkPolicy and DNS · Encryption in transit · Workload identity · Changes and incidents
Establish protected paths and access boundaries with distinct, verifiable observations.
Reference: CKS domains and exam details · Kubernetes v1.35; current six-domain CKS outline