A TLS compliance scanner for OpenShift/Kubernetes clusters. Uses testssl.sh to enumerate TLS versions, cipher suites, and key exchange groups across cluster pods, with post-quantum (ML-KEM) readiness checks.
- Go environment - For building the scanner binary.
- Container tool - Docker or Podman for building and pushing the scanner image.
- OpenShift/Kubernetes cluster access -
ocorkubectlconfigured to point to your target cluster. - Sufficient privileges - Permissions to create Jobs, and grant
cluster-readerandprivilegedSCC to a ServiceAccount.
The scanner is designed to be run from within the cluster it is scanning. This is the most reliable way to ensure network access to all pods. The included deploy.sh script automates the build and deployment process.
This is the recommended approach for automated scanning in an ephemeral test environment.
Set these environment variables in your CI job:
SCANNER_IMAGE: The full tag of the image to build and push (e.g.,quay.io/my-org/tls-scanner:latest).NAMESPACE: The OpenShift/Kubernetes namespace where the scanner Job will be deployed (e.g.,scanner-project). Note: This does NOT restrict what gets scanned - it only determines where the scanner runs.NAMESPACE_FILTER: (Optional) Comma-separated list of namespaces to scan. If not set, the scanner will scan all pods in the entire cluster. Example:production,stagingto scan only those namespaces.KUBECONFIG: Path to the kubeconfig file for the ephemeral test cluster.JOB_TEMPLATE_FILE: (Optional) Path to the Job manifest template used bydeploy.sh deploy(default:scanner-job.yaml.template). For host-based scanning, use a template that runs with host network access (e.g.scanner-job-microshift.yaml.template).SCAN_MODE: (Optional)pod(default) orhost. Pod mode discovers pods in the cluster and scans their TLS ports. Host mode is for environments where core components (API server, etcd, kubelet) run on the host rather than in pods—e.g. single-node or edge setups such as MicroShift. Use a job template withhostNetwork: trueand setSCAN_MODE=hostso the scanner runs on the host and can reach those services.
Your CI pipeline needs to be authenticated with your container registry.
# Build the binary and container image
./deploy.sh build
# Push the image to your registry
./deploy.sh pushThis step creates the necessary RBAC permissions and deploys a Kubernetes Job into the ephemeral cluster.
./deploy.sh deployThe CI job must wait for the Kubernetes Job to complete and then copy the artifacts out.
# Wait for the job to finish (adjust timeout as needed)
kubectl wait --for=condition=complete job/tls-scanner-job -n "$NAMESPACE" --timeout=15m
# Get the name of the pod created by the job
POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l job-name=tls-scanner-job -o jsonpath='{.items[0].metadata.name}')
# Create a local directory for artifacts
mkdir -p ./artifacts
# Copy all result files from the pod
kubectl cp "${NAMESPACE}/${POD_NAME}:/artifacts/." "./artifacts/"Your ./artifacts directory will now contain results.json, results.csv, and scan.log.
In some environments, core cluster components (API server, etcd, kubelet) run on the host instead of as pods. Examples include single-node and edge deployments such as MicroShift. For those setups, use host scan mode so the scanner runs with host network access and can discover and scan TLS services bound to the host:
JOB_TEMPLATE_FILE=scanner-job-microshift.yaml.template SCAN_MODE=host ./deploy.sh deployUse JOB_TEMPLATE_FILE to point at a job template that sets spec.hostNetwork: true (and any required security context). The default scanner-job.yaml.template is for pod-based scanning only.
The scanner produces a CSV file with detailed information about each scanned port. Key columns include:
| Column | Description |
|---|---|
| IP | Pod IP address that was scanned |
| Port | TCP port number |
| Protocol | Protocol (typically "tcp") |
| Service | Detected service (e.g., "https", "http") |
| Pod Name | Kubernetes pod name |
| Namespace | Kubernetes namespace |
| Component Name | OpenShift component name extracted from image |
| Process | Process name listening on the port (from lsof) |
| TLS Ciphers | Detected TLS cipher suites |
| TLS Version | Detected TLS versions (e.g., TLSv1.2, TLSv1.3) |
| Status | Categorized scan result (see below) |
| Reason | Detailed explanation of the status |
| Listen Address | Address the port is bound to (e.g., 127.0.0.1, *, 0.0.0.0) |
The Status column categorizes why a port couldn't be scanned or its TLS configuration:
| Status | Description |
|---|---|
OK |
TLS scan successful - cipher and version information available |
NO_TLS |
Port is open but not using TLS (plain HTTP/TCP service) |
LOCALHOST_ONLY |
Port is bound to 127.0.0.1, not accessible from pod IP |
FILTERED |
Port blocked by network policy or firewall |
CLOSED |
Port not listening on the scanned IP address |
MTLS_REQUIRED |
TLS handshake failed - likely requires client certificate |
TIMEOUT |
Connection timed out |
NO_PORTS |
Pod declares no TCP ports in its spec |
ERROR |
Scan error occurred (see Reason for details) |
IP,Port,Protocol,Service,Pod Name,Namespace,...,Status,Reason,Listen Address
10.128.0.15,8443,tcp,https,kube-apiserver-pod,openshift-kube-apiserver,...,OK,TLS scan successful,*
10.0.53.147,9257,tcp,N/A,controller-manager-pod,openshift-cloud-controller,...,LOCALHOST_ONLY,Bound to 127.0.0.1 not accessible from pod IP,127.0.0.1
10.0.87.193,10258,tcp,N/A,aws-ccm-pod,openshift-cloud-controller,...,FILTERED,Network policy or firewall blocking access,N/A
10.128.0.20,8080,tcp,http,metrics-pod,openshift-monitoring,...,NO_TLS,Port open but no TLS detected (plain HTTP/TCP),*-
LOCALHOST_ONLY ports: These are internal-only ports bound to 127.0.0.1. They're only accessible from within the pod itself and don't pose external TLS compliance concerns. Common for metrics endpoints that are scraped via sidecar containers.
-
FILTERED ports: The scanner couldn't reach these ports due to network policies, firewall rules, or the service not listening on the pod IP. Review network policies if you need to scan these.
-
NO_TLS ports: These ports are open but don't use TLS. This may be expected (e.g., health check endpoints, plaintext metrics) or may indicate a security concern depending on the data transmitted.
-
MTLS_REQUIRED ports: Common for etcd (ports 2379/2380) and other services requiring mutual TLS. The scanner can't complete the handshake without a client certificate.
Remove the scanner Job and associated RBAC permissions from the cluster.
./deploy.sh cleanupYou can also run the steps manually.
- Build the image:
export SCANNER_IMAGE="your-registry/image:tag"and run./deploy.sh build. - Push the image:
./deploy.sh push. - Deploy the job:
export NAMESPACE="scanner-project" NAMESPACE_FILTER="production"and run./deploy.sh deploy - Monitor and retrieve results as shown in the CI workflow.
- Clean up with
./deploy.sh cleanup.
build: Builds thetls-scannerbinary and container image.push: Pushes the container image to the registry specified by$SCANNER_IMAGE.deploy: Deploys the scanner Kubernetes Job to the cluster specified by$KUBECONFIGand$NAMESPACE.cleanup: Removes the scanner Job and RBAC resources.full-deploy(or no action): Runsbuild,push, anddeploy.
The scanner binary accepts the following command-line options. These are configured in the scanner-job.yaml.template file.
./tls-scanner [OPTIONS]Options:
-host <ip>- Target host/IP to scan (default: 127.0.0.1)-port <port>- Target port to scan (default: 443)-targets <host:port,...>- Comma-separated list of host:port targets to scan-all-pods- Scan all pods in the cluster (requires cluster access)-component-filter <names>- Filter pods by component name (comma-separated, used with -all-pods)-namespace-filter <names>- Filter pods by namespace (comma-separated, used with -all-pods)-limit-ips <num>- Cap number of IPs to scan, for testing (0 = no limit)-pqc-check- Check for TLS 1.3 + ML-KEM (post-quantum) support; exits non-zero on failure-j <num>- Number of concurrent scans; 0 = runtime.NumCPU() (default: 0)-artifact-dir <dir>- Directory for output files (default: /tmp)-json-file <file>- Output results in JSON format to specified file-csv-file <file>- Output results in CSV format to specified file-junit-file <file>- Output results in JUnit XML format to specified file-log-file <file>- Redirect all log output to the specified file-timing-file <file>- Write timing report to specified file in artifact-dir-version- Print version and exit