Skip to main content

Encrypt Traffic To HTTPS Load Balancers With TLS

More Info:

Terminate load balancer traffic over HTTPS using TLS certificates so data in transit to and from the cluster is encrypted.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS AKS

Triage and Remediation

Remediation

Manual Steps
  1. Inventory all AKS-facing load balancers and ingresses

    • On any machine with Azure CLI access:
      az aks list -o table
      For each cluster:
      # Get resource group and node RG
      az aks show -n <AKS_CLUSTER_NAME> -g <AKS_RG> \
      --query "{nodeResourceGroup:nodeResourceGroup}" -o tsv

      # List public IPs and load balancers in node RG
      az network public-ip list -g <NODE_RESOURCE_GROUP> -o table
      az network lb list -g <NODE_RESOURCE_GROUP> -o table
      If using an ingress controller, review its Service and Ingress definitions via IaC or repo to identify all external entrypoints.
  2. Confirm only HTTPS (TCP/443) is exposed externally

    • For each public IP used by a load balancer/ingress:
      az network lb show -g <NODE_RESOURCE_GROUP> -n <LB_NAME> \
      --query "frontendIpConfigurations[].inboundNatRules"
      az network lb rule list -g <NODE_RESOURCE_GROUP> --lb-name <LB_NAME> -o table
    • Verify that:
      • Frontend rules expose port 443 (or custom TLS ports) to clients.
      • Port 80 (HTTP) is either disabled externally or only used for internal HTTP→HTTPS redirects at the application/ingress level.
  3. Verify TLS termination and certificate configuration

    • If using an Azure Application Gateway / AGIC:
      az network application-gateway list -o table
      az network application-gateway show -g <APPGW_RG> -n <APPGW_NAME> \
      --query "{frontendPorts:frontendPorts, httpsListeners:sslCertificates}" -o json
      Ensure HTTPS listeners are configured and mapped to the relevant frontend IPs, and HTTP listeners are either disabled or only used for redirects.
    • If using a cloud load balancer with TLS passthrough to ingress: check the ingress controller configuration (Helm values, Terraform, etc.) to confirm TLS termination is enabled and bound to the HTTPS listener.
  4. Review TLS certificate sources, validity, and key management

    • For certificates stored in Azure Key Vault:
      az keyvault certificate list --vault-name <KEYVAULT_NAME> -o table
      az keyvault certificate show --vault-name <KEYVAULT_NAME> -n <CERT_NAME> \
      --query "{name:name, enabled:attributes.enabled, \
      notBefore:attributes.notBefore, expires:attributes.expires}" -o json
    • Confirm:
      • Certificates are valid (not expired, correct CN/SAN for hostnames).
      • Private keys are protected (Key Vault or secure secret store).
      • Automated renewal and reload are configured where possible.
  5. Harden TLS policy and disable weak protocols/ciphers

    • For Application Gateway:
      az network application-gateway show -g <APPGW_RG> -n <APPGW_NAME> \
      --query "sslPolicy" -o json
    • Decide whether to:
      • Use a predefined strong policy (e.g., AppGwSslPolicy20170401S) or
      • A custom policy that disables TLS 1.0/1.1 and weak ciphers.
    • Update via console or IaC to enforce the chosen policy across all HTTPS listeners.
  6. Document and, if necessary, implement corrections via console/CLI/IaC; then re-verify

    • Apply required changes in your chosen IaC (ARM/Bicep/Terraform) or Azure portal:
      • Ensure all external listeners use HTTPS with valid certificates.
      • Remove or redirect plain HTTP endpoints.
      • Align all entrypoints with the selected TLS policy.
    • Re-run the evidence-gathering commands from steps 2–5 to confirm that:
      • Only HTTPS endpoints are exposed externally.
      • Certificates and TLS policies match your security requirements.
Using kubectl

kubectl cannot configure HTTPS load balancer TLS termination, because this setting is managed in your cloud provider’s load balancer and/or IaC configuration, not via Kubernetes API objects. Make the necessary changes in your cloud console/CLI or IaC definitions, and refer to the Manual Steps section for detailed guidance.

Automation
#!/usr/bin/env bash
# Report Kubernetes Services exposed via cloud load balancers and whether they
# are configured to use HTTPS/TLS on the external interface.
#
# Run on: any machine with kubectl access and correct context.
# Requires: kubectl, jq

set -euo pipefail

# Confirm kubectl works
kubectl version --short >/dev/null

echo "Collecting LoadBalancer Services and ingress-related annotations..."
echo

# Header
printf "%-30s %-20s %-20s %-10s %-60s\n" \
"NAMESPACE/NAME" "TYPE" "EXTERNAL_IP/INGRESS" "PORTS" "TLS / HTTPS INDICATORS"
echo "------------------------------------------------------------------------------------------------------------------------------------------"

# 1) Services of type LoadBalancer
kubectl get svc --all-namespaces -o json | jq -r '
.items[] |
select(.spec.type == "LoadBalancer") |
# Basic info
. as $svc |
(
(.metadata.namespace + "/" + .metadata.name) as $id |
"Service " + $id,
(.status.loadBalancer.ingress // [] | map(.ip // .hostname) | join(",")) as $ing,
(.spec.ports // [] | map((.port|tostring)+"/"+(.protocol//"")) | join(",")) as $ports,
# Heuristics for HTTPS/TLS:
# - Any port 443 or named "https"
# - Common cloud-controller annotations showing HTTPS listeners or certificates
(
[
(if any(.spec.ports[]?; .port==443 or .name=="https") then "PORT_443_OR_HTTPS" else empty end),
(.metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-ssl-cert"] // empty | select(length>0) | "AWS_SSL_CERT"),
(.metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-backend-protocol"] // empty | select(test("https|ssl";"i")) | "AWS_BACKEND_HTTPS"),
(.metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-ssl-ports"] // empty | "AWS_SSL_PORTS=" + .),
(.metadata.annotations["service.beta.kubernetes.io/azure-load-balancer-internal"] // empty | "AZURE_INTERNAL_LB"),
(.metadata.annotations["service.beta.kubernetes.io/azure-load-balancer-tls-cert"] // empty | "AZURE_TLS_CERT"),
(.metadata.annotations["cloud.google.com/app-protocols"] // empty | "GKE_APP_PROTOCOLS=" + .),
(.metadata.annotations["networking.gke.io/app-protocols"] // empty | "GKE_NET_APP_PROTOCOLS=" + .),
(.metadata.annotations["networking.gke.io/managed-certificates"] // empty | "GKE_MANAGED_CERTS=" + .),
(.metadata.annotations["service.beta.kubernetes.io/do-loadbalancer-tls-passthrough"] // empty | "DO_TLS_PASSTHROUGH=" + .),
(.metadata.annotations["service.beta.kubernetes.io/do-loadbalancer-protocol"] // empty | "DO_LB_PROTOCOL=" + .),
(.metadata.annotations["service.beta.kubernetes.io/linode-loadbalancer-ssl-ports"] // empty | "LINODE_SSL_PORTS=" + .),
(.metadata.annotations["service.beta.kubernetes.io/oci-load-balancer-ssl-ports"] // empty | "OCI_SSL_PORTS=" + .)
] | unique | join(",")
) as $tlsHints |
# Emit one line
@tsv "\($id)\tService\t\($ing)\t\($ports)\t\($tlsHints)"
)
' | while IFS=$'\t' read -r id type ing ports tls; do
printf "%-30s %-20s %-20s %-10s %-60s\n" "$id" "$type" "${ing:-<pending>}" "${ports:--}" "${tls:-<none>}"
done

echo
echo "Collecting Ingresses and TLS usage..."
echo

# 2) Ingress resources (may front HTTPS load balancers depending on class/controller)
kubectl get ingress --all-namespaces -o json 2>/dev/null | jq -r '
.items[]? |
. as $ing |
(
(.metadata.namespace + "/" + .metadata.name) as $id |
(.spec.ingressClassName // .metadata.annotations["kubernetes.io/ingress.class"] // "<none>") as $class |
(.status.loadBalancer.ingress // [] | map(.ip // .hostname) | join(",")) as $addr |
((.spec.tls // []) | length | tostring) as $tlsCount |
(
[
(if (.spec.tls // []) | length > 0 then "INGRESS_TLS_SECTIONS="+$tlsCount else empty end),
(.metadata.annotations["nginx.ingress.kubernetes.io/backend-protocol"] // empty | "NGINX_BACKEND_PROTO=" + .),
(.metadata.annotations["nginx.ingress.kubernetes.io/ssl-redirect"] // empty | "NGINX_SSL_REDIRECT=" + .),
(.metadata.annotations["ingress.gcp.kubernetes.io/pre-shared-cert"] // empty | "GCE_PRESHARED_CERT=" + .),
(.metadata.annotations["networking.gke.io/managed-certificates"] // empty | "GKE_MANAGED_CERTS=" + .),
(.metadata.annotations["alb.ingress.kubernetes.io/certificate-arn"] // empty | "AWS_ALB_CERT_ARN=" + .),
(.metadata.annotations["alb.ingress.kubernetes.io/listen-ports"] // empty | "AWS_ALB_LISTEN_PORTS=" + .),
(.metadata.annotations["azure/application-gateway-cert"] // empty | "AZURE_APPGW_CERT=" + .),
(.metadata.annotations["konghq.com/protocols"] // empty | "KONG_PROTOCOLS=" + .)
] | unique | join(",")
) as $tlsHints |
@tsv "\($id)\tIngress(\($class))\t\($addr)\t-\t\($tlsHints)"
)
' 2>/dev/null | while IFS=$'\t' read -r id type addr ports tls; do
printf "%-30s %-20s %-20s %-10s %-60s\n" "$id" "$type" "${addr:-<pending>}" "-" "${tls:-<none>}"
done

cat <<'EOF'

How to interpret this report
----------------------------

Each row represents:
- Service: a Service of type LoadBalancer that will be backed by a cloud load balancer.
- Ingress: an Ingress that typically provisions an external load balancer through its controller.

Columns:
- NAMESPACE/NAME: Resource identifier.
- TYPE: Service or Ingress(+class).
- EXTERNAL_IP/INGRESS: External address of the cloud load balancer if assigned.
- PORTS: Exposed ports (for Services).
- TLS / HTTPS INDICATORS: Heuristics suggesting HTTPS/TLS is configured.

Potential problems (to review manually in cloud console / IaC):
- Service rows where:
- PORTS only show HTTP ports (e.g., 80/TCP) and
- TLS / HTTPS INDICATORS is "<none>" or does not mention provider-specific TLS annotations.
- Ingress rows where:
- TLS / HTTPS INDICATORS is "<none>" and
- You expect HTTPS termination at the load balancer for those hosts.

This script does NOT prove traffic is encrypted:
- It only highlights where TLS-like config appears present or absent in Kubernetes objects.
- You must confirm in your cloud provider console, CLI, or IaC:
- That the external load balancer listeners use HTTPS/TLS.
- That appropriate certificates are attached.
- That HTTP (if present) is redirected to HTTPS or otherwise acceptable by your policy.
EOF