More Info:
Encrypt traffic to HTTPS load balancers using TLS certificates.Risk Level
LowAddress
SecurityCompliance Standards
- CIS GKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
Manual Steps
-
On any machine with
gcloudandkubectlaccess, list current LoadBalancer Services and Ingresses to decide what to migrate and secure:kubectl get svc -A | grep LoadBalancer || echo "No LoadBalancer Services" kubectl get ingress -A -
For each public
LoadBalancerService you want to front with HTTPS, create or reuse an Ingress and backend Service/ports (GCE HTTP(S) Load Balancer only works with NodePort/NEG backends). Example (edit names/namespaces/ports as needed, run from any machine with kubectl access):cat <<'EOF' > ingress-backend-svc.yaml apiVersion: v1 kind: Service metadata: name: my-app namespace: default spec: type: NodePort selector: app: my-app ports: - port: 80 targetPort: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-app-ing namespace: default annotations: kubernetes.io/ingress.class: "gce" spec: rules: - host: myapp.example.com http: paths: - path: / pathType: Prefix backend: service: name: my-app port: number: 80 EOF kubectl apply -f ingress-backend-svc.yaml -
Create a Google-managed SSL certificate resource in the same namespace as the Ingress (any machine with kubectl access). Use DNS names that point to the load balancer IP you will get:
cat <<'EOF' > managed-cert.yaml apiVersion: networking.gke.io/v1 kind: ManagedCertificate metadata: name: my-app-cert namespace: default spec: domains: - myapp.example.com EOF kubectl apply -f managed-cert.yaml -
Attach the ManagedCertificate to the Ingress via annotation (any machine with kubectl access):
kubectl annotate ingress -n default my-app-ing \ "networking.gke.io/managed-certificates=my-app-cert" \ --overwrite -
Wait for the managed certificate and Ingress to become active, then decommission old public LoadBalancer Services once traffic is confirmed on HTTPS (any machine with kubectl access):
# Watch certificate status kubectl describe managedcertificate -n default my-app-cert | egrep 'Status:|Domain Status' # Watch Ingress IP and provisioning kubectl get ingress -n default my-app-ing -w # When stable and tested over HTTPS, delete obsolete LoadBalancer Services kubectl get svc -A | grep LoadBalancer # Example: kubectl delete svc -n default old-lb-service-name -
Verify compliance from any machine with
kubectlandjqinstalled:svc_json="$(kubectl get svc -A -o json 2>/dev/null || echo '{"items":[],"__err":"SVC_FORBIDDEN"}')" ing_json="$(kubectl get ingress -A -o json 2>/dev/null || echo '{"items":[],"__err":"INGRESS_FORBIDDEN"}')" mc_json="$(kubectl get managedcertificates -A -o json 2>/dev/null || echo '{"items":[],"__err":"MC_FORBIDDEN"}')" printf '%s\n%s\n%s\n' "$svc_json" "$ing_json" "$mc_json" \ | jq -rs ' (.[0] // {}) as $svcsRaw | (.[1] // {}) as $ingsRaw | (.[2] // {}) as $mcsRaw | if ($svcsRaw.__err or $ingsRaw.__err or $mcsRaw.__err) then "ERROR_KUBECTL_LIST:" + ([ ($svcsRaw.__err // empty), ($ingsRaw.__err // empty), ($mcsRaw.__err // empty) ] | join(",")) else ($svcsRaw.items // []) as $svcs | ($ingsRaw.items // []) as $ings | ($mcsRaw.items // []) as $mcs | def trim: gsub("^\\s+|\\s+$";""); def hasmc($ns;$name): any($mcs[]?; .metadata.namespace==$ns and .metadata.name==$name); ([ $svcs[]? | select(.spec.type=="LoadBalancer") | "FOUND_PUBLIC_LB_SERVICE:\(.metadata.namespace // "default"):\(.metadata.name)" ] + [ $ings[]? as $i | ($i.metadata.annotations."networking.gke.io/managed-certificates" // "") as $ann | select($ann=="") | "FOUND_INGRESS_WITHOUT_MANAGED_CERT:\($i.metadata.namespace // "default"):\($i.metadata.name)" ] + [ $ings[]? as $i | ($i.metadata.annotations."networking.gke.io/managed-certificates" // "") as $ann | select($ann!="") | ($i.metadata.namespace // "default") as $ns | ($ann | split(",") | map(trim) | map(select(length>0)) | .[]) as $mc | select(hasmc($ns;$mc) | not) | "FOUND_MISSING_MANAGED_CERT_RESOURCE:\($ns):\($i.metadata.name):cert=\($mc)" ]) as $f | if ($f|length)>0 then $f[] else "ALL_INGRESSES_USE_MANAGED_CERTS_AND_NO_PUBLIC_LB_SERVICES" end end '
Using kubectl
Using kubectl
kubectl cannot configure Google-managed SSL Certificates or convert LoadBalancer Services into Ingress resources; those changes must be made in the GCP console, gcloud CLI, or your IaC (GKE/Cloud Load Balancing configuration). Refer to the Manual Steps section for the exact cloud-side changes to replace public LoadBalancer Services with Ingress objects that use Google-managed certificates.
Automation
Automation
#!/usr/bin/env bash
#
# automate_managed_ssl_gke.sh
#
# Scope: run on any machine with:
# - gcloud CLI configured for the target project
# - kubectl with access to the target GKE cluster/context
#
# What it does (idempotent):
# - Detects Services of type LoadBalancer (does NOT auto-convert them; prints guidance)
# - For each Ingress that:
# * has no managed-certificates annotation, OR
# * has an annotation but some referenced ManagedCertificates are missing
# it:
# * creates/keeps one Google ManagedCertificate per Ingress
# named "<ingress-name>-mc" in the same namespace
# * annotates the Ingress with that single managed-cert reference
# - At the end, re-runs the benchmark audit logic to show remaining findings.
#
# IMPORTANT: You MUST provide a domain for each Ingress.
# This script expects a mapping file to avoid guessing domains.
#
# DOMAIN MAPPING FILE FORMAT (YAML-ish, parsed via grep/sed, keep it simple):
# # lines starting with # are comments
# namespace/ingress-name: example.com
# namespace/another-ing: app.example.org
# default/web: web.example.com
#
# Example usage:
# PROJECT_ID="my-proj" CLUSTER="my-cluster" LOCATION="us-central1" \
# DOMAIN_MAP="ingress_domains.map" ./automate_managed_ssl_gke.sh
#
set -euo pipefail
PROJECT_ID="${PROJECT_ID:-}"
CLUSTER="${CLUSTER:-}"
LOCATION="${LOCATION:-}"
DOMAIN_MAP="${DOMAIN_MAP:-ingress_domains.map}"
if [[ -z "$PROJECT_ID" || -z "$CLUSTER" || -z "$LOCATION" ]]; then
echo "ERROR: Set PROJECT_ID, CLUSTER, and LOCATION environment variables."
exit 1
fi
if [[ ! -f "$DOMAIN_MAP" ]]; then
echo "ERROR: DOMAIN_MAP file '$DOMAIN_MAP' not found."
echo "Create it with lines like: namespace/ingress-name: example.com"
exit 1
fi
# Ensure kubectl context is set to the target GKE cluster
echo "Configuring kubectl for cluster '${CLUSTER}' in '${LOCATION}' (project '${PROJECT_ID}')..."
gcloud container clusters get-credentials "$CLUSTER" --zone "$LOCATION" --project "$PROJECT_ID"
# Helper: trim whitespace
trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; }
# Helper: lookup domain for namespace/ingress from DOMAIN_MAP
lookup_domain() {
local ns="$1"
local ing="$2"
local key="${ns}/${ing}"
# Match "ns/ing:" at line start; ignore comments and blank lines
local line
line="$(grep -E "^[[:space:]]*${key}:" "$DOMAIN_MAP" 2>/dev/null || true)"
if [[ -z "$line" ]]; then
return 1
fi
# Extract substring after first colon
local dom
dom="$(printf '%s\n' "$line" | sed 's/^[^:]*:[[:space:]]*//')"
dom="$(printf '%s\n' "$dom" | trim)"
if [[ -z "$dom" ]]; then
return 1
fi
printf '%s\n' "$dom"
}
# 1) Detect public LoadBalancer Services (read-only; no auto-conversion)
echo "Scanning for Services of type LoadBalancer..."
lb_svcs_json="$(kubectl get svc -A -o json 2>/dev/null || echo '{"items":[]}')"
lb_count="$(printf '%s\n' "$lb_svcs_json" | jq '[.items[]? | select(.spec.type=="LoadBalancer")] | length')"
if [[ "$lb_count" -gt 0 ]]; then
echo "WARNING: Found $lb_count Service(s) of type LoadBalancer."
printf '%s\n' "$lb_svcs_json" \
| jq -r '.items[]? | select(.spec.type=="LoadBalancer") | " - " + (.metadata.namespace//"default") + "/" + .metadata.name'
echo "Per CIS GKE 5.6.8: consider replacing these with Ingress resources."
else
echo "No Services of type LoadBalancer found."
fi
# 2) Process Ingresses and ManagedCertificate resources
echo "Scanning Ingresses and ManagedCertificates..."
ings_json="$(kubectl get ingress -A -o json 2>/dev/null || echo '{"items":[],"__err":"INGRESS_FORBIDDEN"}')"
mcs_json="$(kubectl get managedcertificates -A -o json 2>/dev/null || echo '{"items":[]}')"
if echo "$ings_json" | jq -e 'has("__err")' >/dev/null 2>&1; then
echo "ERROR: Unable to list Ingresses with kubectl. Check permissions."
exit 1
fi
# Get array of Ingresses as lines "ns ing annotations_json"
mapfile -t INGS < <(
printf '%s\n' "$ings_json" | jq -r '
.items[]? |
(.metadata.namespace // "default") + " " +
.metadata.name + " " +
( (.metadata.annotations // {}) | @json )
'
)
# Existing ManagedCertificates: "ns name"
mapfile -t EXISTING_MC < <(
printf '%s\n' "$mcs_json" | jq -r '.items[]? | (.metadata.namespace // "default") + " " + .metadata.name'
)
has_mc() {
local ns="$1" name="$2"
for l in "${EXISTING_MC[@]}"; do
if [[ "$l" == "$ns $name" ]]; then
return 0
fi
done
return 1
}
# For each ingress, ensure it uses a single ManagedCertificate "<ingress-name>-mc"
for line in "${INGS[@]}"; do
ns="$(printf '%s\n' "$line" | awk '{print $1}')"
ing="$(printf '%s\n' "$line" | awk '{print $2}')"
ann_json="$(printf '%s\n' "$line" | cut -d' ' -f3-)"
mc_name="${ing}-mc"
# Desired annotation value: single entry "<ingress-name>-mc"
desired_ann="$mc_name"
# Parse current annotation value (if any)
current_ann="$(printf '%s\n' "$ann_json" | jq -r '."networking.gke.io/managed-certificates" // ""')"
# Determine if current annotation already references our mc_name and mc exists
need_update="true"
if [[ -n "$current_ann" ]]; then
# Split by comma, trim, and check if our mc_name is present and exists
if printf '%s\n' "$current_ann" \
| jq -r --arg mc "$mc_name" '
split(",")
| map(gsub("^\\s+|\\s+$";""))
| map(select(length>0))
| any(. == $mc)
' | grep -qx "true"; then
if has_mc "$ns" "$mc_name"; then
# Already using our desired ManagedCertificate
need_update="false"
fi
fi
fi
# Ensure ManagedCertificate resource exists
if ! has_mc "$ns" "$mc_name"; then
domain="$(lookup_domain "$ns" "$ing" || true)"
if [[ -z "$domain" ]]; then
echo "SKIP: Ingress ${ns}/${ing} has no domain mapping in ${DOMAIN_MAP}; cannot create ManagedCertificate."
continue
fi
echo "Creating/refreshing ManagedCertificate ${ns}/${mc_name} for domain ${domain}..."
cat <<EOF | kubectl apply -n "$ns" -f -
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: ${mc_name}
spec:
domains:
- ${domain}
EOF
# Refresh EXISTING_MC cache entry for idempotence in same run
EXISTING_MC+=("$ns $mc_name")
fi
if [[ "$need_update" == "true" ]]; then
echo "Annotating Ingress ${ns}/${ing} with networking.gke.io/managed-certificates=${desired_ann}..."
kubectl annotate ingress "${ing}" \
-n "${ns}" \
"networking.gke.io/managed-certificates=${desired_ann}" \
--overwrite
else
echo "Ingress ${ns}/${ing} already references existing ManagedCertificate ${mc_name}; no change."
fi
done
echo
echo "Waiting briefly for Kubernetes API to reflect changes..."
sleep 5
# 3) Verification: re-run the benchmark audit logic
echo
echo "=== Verification (CIS GKE 5.6.8 check) ==="
svc_json="$(kubectl get svc -A -o json 2>/dev/null || echo '{"items":[],"__err":"SVC_FORBIDDEN"}')"
ing_json="$(kubectl get ingress -A -o json 2>/dev/null || echo '{"items":[],"__err":"INGRESS_FORBIDDEN"}')"
mc_json="$(kubectl get managedcertificates -A -o json 2>/dev/null || echo '{"items":[],"__err":"MC_FORBIDDEN"}')"
printf '%s\n%s\n%s\n' "$svc_json" "$ing_json" "$mc_json" \
| jq -rs '
(.[0] // {}) as $svcsRaw |
(.[1] // {}) as $ingsRaw |
(.[2] // {}) as $mcsRaw |
if ($svcsRaw.__err or $ingsRaw.__err or $mcsRaw.__err) then
"ERROR_KUBECTL_LIST:" +
([
($svcsRaw.__err // empty),
($ingsRaw.__err // empty),
($mcsRaw.__err // empty)
] | join(","))
else
($svcsRaw.items // []) as $svcs |
($ingsRaw.items // []) as $ings |
($mcsRaw.items // []) as $mcs |
def trim: gsub("^\\s+|\\s+$";"");
def hasmc($ns;$name): any($mcs[]?; .metadata.namespace==$ns and .metadata.name==$name);
([
$svcs[]? | select(.spec.type=="LoadBalancer")
| "FOUND_PUBLIC_LB_SERVICE:\(.metadata.namespace // "default"):\(.metadata.name)"
] + [
$ings[]? as $i
| ($i.metadata.annotations."networking.gke.io/managed-certificates" // "") as $ann
| select($ann=="")
| "FOUND_INGRESS_WITHOUT_MANAGED_CERT:\($i.metadata.namespace // "default"):\($i.metadata.name)"
] + [
$ings[]? as $i
| ($i.metadata.annotations."networking.gke.io/managed-certificates" // "") as $ann
| select($ann!="")
| ($i.metadata.namespace // "default") as $ns
| ($ann | split(",") | map(trim) | map(select(length>0)) | .[]) as $mc
| select(hasmc($ns;$mc) | not)
| "FOUND_MISSING_MANAGED_CERT_RESOURCE:\($ns):\($i.metadata.name):cert=\($mc)"
]) as $f
| if ($f|length)>0
then $f[]
else "ALL_INGRESSES_USE_MANAGED_CERTS_AND_NO_PUBLIC_LB_SERVICES"
end
end
'
echo "=== End of verification ==="

