Define the boundary and acceptance evidence
A security project begins by identifying paths it intends to permit and paths it intends to prevent. In a fictional positions service, the closing client needs the HTTPS endpoint; the processor needs DNS and database access; the supplier needs limited diagnosis. These paths have different identities, protocols and destinations. Draw each connection with its source, destination, port, protocol, expected name and owner. Add the time and configuration revision used in testing. A report can then distinguish operator access from application access, and an earlier trial from the release entering production. Turn the requirement into two observations: necessary behavior works and prohibited behavior is denied within the defined scope. Rejection does not automatically establish security; the service might simply be stopped. A successful connection does not establish correct authentication either. In this module, separate the API declaration, control enforcement, endpoint identity and functional outcome. This separation helps assign the correction to the right team and avoids broad changes before the failure is located.
Read the policy without losing direction
When reviewing NetworkPolicy, start with the selected Pods and the direction in policyTypes. Do not infer direction from the policy name. An empty rule list differs from a list containing an empty rule: no allowances in a list is different from an allowance with no conditions. Write a small test matrix before applying the change. For example, the processor might need UDP 53 to the approved resolver and TCP 5432 to the database, while requiring no administration-endpoint access. A rule omitting protocol uses TCP; do not treat it as permission for both TCP and UDP. Review the applicable policy set and both directions of isolation. An earlier broad allowance can still permit traffic. During testing, confirm actual labels, namespaces, sources and destinations instead of checking only the new manifest. If the application resolves names but cannot reach the database, evidence already separates part of the path. If the resolver does not respond, locate that step before opening all egress to recover service.
Recognize implementation boundaries
An object accepted by the API is not a receipt proving simultaneous enforcement on every node. The plugin processes policy, and a transition can produce different observations across destinations. Record the sequence and repeat checks within a defined time budget; persistent denial still requires diagnosis. Do not turn a possible transition explanation into unlimited justification for every failure. RUN needs to know what to observe, when to escalate and which condition triggers rollback. The path can also change the address observed by policy. Through a load balancer, the evaluated source can differ from the client IP. Confirm implementation before concluding that an ipBlock selects the intended population. Treat hostNetwork as a distinct condition: some plugins distinguish that traffic per Pod, while others handle it as node traffic. An ordinary Pod test does not automatically cover an agent with different networking. Document these differences in the plan, including the plugin, node pool and workload types actually represented by the trial.
Connect Ingress to TLS identity
Ingress declares routing; a controller must implement it. Confirm ingressClassName and the class actually handled, the rule host, referenced Secret and endpoint reached by the client. The TLS format uses tls.crt and tls.key. Those fields do not prove that the key matches the certificate or that the requested name is covered. Separate chain trust, time validity, permitted purpose and name identity. A certificate issued by a trusted CA for report.lab.test does not authenticate funds.lab.test when only the former name appears in its SAN. During rotation, comparing the Secret before and after is necessary but insufficient. Observe the certificate served under the expected name on the actual path. Where multiple hosts or controllers exist, retain SNI and destination in the test record. TLS termination at the edge does not establish encryption of the next segment; separately identify backend transport when required. Do not use disabled client verification as an acceptance criterion. It can hide the very identity error that needs correction.
Run the TLS acceptance lab
The code below requires Python 3.13 or later and the OpenSSL 3.x command-line tool. Save it as run.py and run python3 run.py. It creates two temporary CAs and a temporary server certificate with the funds.lab.test SAN. It installs no system trust and uses no real credentials. Before running, predict the outcomes for a wrong name, wrong CA, unsuitable purpose, IP absent from SAN and times outside validity. The -attime option changes the verification reference time without changing the machine clock. Public-key comparison confirms a matching pair and distinguishes an unrelated key without printing private material. The program then makes real TLS connections over 127.0.0.1. Only the connection with the correct CA and name should return the synthetic response; the other two clients should fail verification. JSON records 12 observations and identifies executed versions. Temporary keys are removed at completion. Explain each rejection before reading the details. These outcomes demonstrate the exercise’s TLS controls without executing DNS, an Ingress controller, CNI or revocation checking. Cluster validation still requires its own trial.
Assess metadata, endpoints and benchmarks
A metadata test should identify its source and configuration. If the actual workload uses another pool or networking mode, retain that coverage gap. Also reduce node-identity permissions: limiting one network path does not justify privileges over unrelated data. Distinguish the cloud identity from the Kubernetes ServiceAccount and assign each review to the appropriate owner. Direct etcd read access can expose sensitive data and enable escalation. Do not classify it as harmless merely because writes are excluded, or assume API RBAC filters content on that direct path. In the hardening report, identify the benchmark and its applicable platform. The CIS catalogue distinguishes Kubernetes and managed-distribution families; do not invent equivalence between reports. If a control lies outside scanner visibility, record who operates it and what evidence is missing. A pending manual control does not become a pass because automated checks passed. After changing exposure or configuration, reassess affected controls and retain the relationship between the result, observed revision and acceptance decision.
Verify the artifact that will be installed
A valid signature satisfies the intended policy only when verification includes the authorized identity and expected issuer. Do not adapt expected values to an unexpected certificate merely to obtain success. For Kubernetes binaries, use release documentation corresponding to the selected artifact and version. Binary and image procedures can identify different publishing identities; read the reference appropriate to the material. Retain the verification result with content identity without turning it into a guarantee that vulnerabilities are absent. Also protect the transition from verification to installation. A script replacing the file at the same path breaks the relationship between verified and used bytes. The filename, size or successful execution does not restore that relationship. Finally, check destination version and architecture: authenticity does not make a binary compatible with every node. For a supplier project, handover should explain artifact selection, origin verification and evidence that the same content entered the environment. This lesson’s TLS lab neither runs Cosign nor validates a Kubernetes release.
Decide RUN handover
In this lesson’s fictional case, the new Secret is correct, but the closing client still receives the previous certificate. The operator tested another host and twenty minutes remain in the change window. Organize investigation around evidence: confirm the client path, identify controller and requested name, observe the presented certificate, and verify the functional response with controls enabled. Retain a fallback with a valid certificate and a known return procedure. Communicate the distinction between prepared material and observed service to the committee. If no validated correction is ready in time, the decision may be to postpone handover or retain the valid previous version with recorded approval. Removing verification to obtain a response does not meet the requirement. The RUN package should contain effective configuration, contacts, test matrix, expected outcomes, trial limits and escalation conditions. Summarize the lesson with four questions: which path was tested, which identity was authenticated, which control was observed, and which gap remains? Use practice questions to identify weak assumptions and revisit the lab when confusing trust, name, time or key correspondence.
"""Original local TLS practice. Creates only disposable keys and loopback sockets.
No Kubernetes cluster, controller, CNI, public DNS, production trust store,
revocation service or official exam environment is exercised.
Python 3.13+ and OpenSSL 3.x CLI required. Output never includes private keys.
"""
import json
import platform
import socket
import ssl
import subprocess
import tempfile
import threading
import time
from pathlib import Path
def command(root, *args, check=True):
p = subprocess.run(['openssl', *args], cwd=root, capture_output=True,
text=True, timeout=15)
if check and p.returncode:
raise RuntimeError('OpenSSL command failed: ' + args[0] + ': ' + p.stderr)
return p
def make_material(root):
for name in ['trusted', 'other']:
command(root, 'req', '-x509', '-newkey', 'ec', '-pkeyopt',
'ec_paramgen_curve:prime256v1', '-noenc', '-days', '10',
'-subj', '/CN=DR temporary ' + name, '-keyout', name + '.key',
'-out', name + '.pem', '-addext', 'basicConstraints=critical,CA:TRUE',
'-addext', 'keyUsage=critical,keyCertSign,cRLSign',
'-addext', 'subjectKeyIdentifier=hash')
command(root, 'req', '-new', '-newkey', 'ec', '-pkeyopt',
'ec_paramgen_curve:prime256v1', '-noenc', '-subj', '/CN=DR lab leaf',
'-keyout', 'leaf.key', '-out', 'leaf.csr')
(root / 'leaf.ext').write_text('basicConstraints=critical,CA:FALSE\n'
'keyUsage=critical,digitalSignature\nextendedKeyUsage=serverAuth\n'
'subjectAltName=DNS:funds.lab.test\nsubjectKeyIdentifier=hash\n'
'authorityKeyIdentifier=keyid,issuer\n')
command(root, 'x509', '-req', '-in', 'leaf.csr', '-CA', 'trusted.pem',
'-CAkey', 'trusted.key', '-set_serial', '101', '-days', '2',
'-extfile', 'leaf.ext', '-out', 'leaf.pem')
for p in root.glob('*.key'):
p.chmod(0o600)
def handshake(root, name, ca):
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(root / 'leaf.pem', root / 'leaf.key')
server_context.minimum_version = ssl.TLSVersion.TLSv1_2
listener = socket.socket
listener.bind(('127.0.0.1', 0))
listener.listen(1)
listener.settimeout(5)
address = listener.getsockname
server = {}
def serve:
try:
with listener:
raw, _ = listener.accept
with raw:
raw.settimeout(5)
with server_context.wrap_socket(raw, server_side=True) as channel:
server['request'] = channel.recv(100).decode('ascii')
channel.sendall(b'DR synthetic response')
server['completed'] = True
except ssl.SSLError as exc:
server['tlsError'] = type(exc).__name__
except Exception as exc:
server['unexpectedError'] = repr(exc)
worker = threading.Thread(target=serve, daemon=True)
worker.start
client = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client.load_verify_locations(cafile=str(root / ca))
client.minimum_version = ssl.TLSVersion.TLSv1_2
result = {'serverName': name, 'trustFile': ca,
'verifyRequired': client.verify_mode == ssl.CERT_REQUIRED,
'hostnameChecked': client.check_hostname}
try:
with socket.create_connection(address, timeout=5) as raw:
with client.wrap_socket(raw, server_hostname=name) as channel:
channel.sendall(b'DR request')
result.update(accepted=True, response=channel.recv(100).decode('ascii'),
protocol=channel.version)
except ssl.SSLCertVerificationError as exc:
result.update(accepted=False, verificationCode=exc.verify_code,
verificationMessage=exc.verify_message)
finally:
worker.join(6)
listener.close
if worker.is_alive or 'unexpectedError' in server:
raise RuntimeError('Local server did not terminate cleanly: ' + repr(server))
result['serverCompleted'] = bool(server.get('completed'))
return result
def run:
with tempfile.TemporaryDirectory(prefix='dr-cks-tls-') as folder:
root = Path(folder)
make_material(root)
now = int(time.time)
checks = []
vectors = [
('valid-server', ['-CAfile', 'trusted.pem', '-purpose', 'sslserver',
'-verify_hostname', 'funds.lab.test'], True, None),
('wrong-name', ['-CAfile', 'trusted.pem', '-verify_hostname', 'other.lab.test'], False, 'hostname mismatch'),
('no-ip-san', ['-CAfile', 'trusted.pem', '-verify_ip', '127.0.0.1'], False, 'IP address mismatch'),
('untrusted-issuer', ['-CAfile', 'other.pem'], False, 'unable to get local issuer'),
('expired-at-reference', ['-CAfile', 'trusted.pem', '-attime', str(now + 4 * 86400)], False, 'has expired'),
('before-validity', ['-CAfile', 'trusted.pem', '-attime', str(now - 86400)], False, 'not yet valid'),
('wrong-purpose', ['-CAfile', 'trusted.pem', '-purpose', 'sslclient'], False, 'unsuitable certificate purpose'),
]
for name, args, expected, message in vectors:
p = command(root, 'verify', '-no-CApath', '-no-CAstore', *args, 'leaf.pem', check=False)
accepted = p.returncode == 0
if accepted!= expected or (message and message not in p.stderr):
raise AssertionError((name, p.returncode, p.stdout, p.stderr))
checks.append({'id': name, 'accepted': accepted, 'expectedAccepted': expected,
'exitCode': p.returncode, 'reasonChecked': message})
certificate_public = command(root, 'x509', '-in', 'leaf.pem', '-pubkey', '-noout').stdout
matching_public = command(root, 'pkey', '-in', 'leaf.key', '-pubout').stdout
other_public = command(root, 'pkey', '-in', 'other.key', '-pubout').stdout
assert certificate_public == matching_public and certificate_public!= other_public
checks.append({'id': 'public-key-correspondence', 'matchingPair': True, 'unrelatedPair': False})
mismatch_rejected = False
try:
ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER).load_cert_chain(root / 'leaf.pem', root / 'other.key')
except ssl.SSLError as exc:
mismatch_rejected = 'KEY_VALUES_MISMATCH' in str(exc)
assert mismatch_rejected
checks.append({'id': 'mismatched-private-key', 'rejected': mismatch_rejected})
trials = [handshake(root, 'funds.lab.test', 'trusted.pem'),
handshake(root, 'other.lab.test', 'trusted.pem'),
handshake(root, 'funds.lab.test', 'other.pem')]
assert [x['accepted'] for x in trials] == [True, False, False]
assert [x['serverCompleted'] for x in trials] == [True, False, False]
assert trials[0]['response'] == 'DR synthetic response'
assert all(x['verifyRequired'] and x['hostnameChecked'] for x in trials)
assert 'hostname mismatch' in trials[1]['verificationMessage'].lower
assert 'issuer' in trials[2]['verificationMessage'].lower
report = {'python': platform.python_version, 'sslLibrary': ssl.OPENSSL_VERSION,
'opensslCLI': command(root, 'version').stdout.strip, 'checks': checks,
'handshakes': trials, 'observations': len(checks) + len(trials),
'systemClockChanged': False, 'systemTrustStoreChanged': False,
'privateKeysPrinted': False, 'clusterExecuted': False,
'scope': 'Actual disposable X.509 verification and loopback TLS; no Kubernetes, CNI, Ingress controller, DNS lookup, revocation check or production claim.'}
report['temporaryMaterialRemoved'] = not Path(folder).exists
assert report['temporaryMaterialRemoved']
return report
if __name__ == '__main__':
print(json.dumps(run, indent=2))
The Secret changed, but the closing client still sees the old certificate: collect endpoint, SNI and served material before accepting handover.
Common pitfalls
Confusing accepted objects with enforced controls; testing another host; disabling verification; passing unobserved controls; installing bytes different from those verified.
Related topics: API identities and authorization · Production auditing and response · Artifacts and provenance
Acceptance binds configuration, path, identity and outcome to the same observed revision while preserving evidence limits.
Reference: CKS certification and domains · Kubernetes v1.35; current six-domain CKS outline