> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Ensure Use Of Google-Managed SSL Certificates

### More Info:

Use Google-managed SSL certificates on Ingress resources so TLS certificates are automatically provisioned and rotated. This avoids expired or mismanaged manual certificates and exposed LoadBalancer services.

### Risk Level

Medium

### Address

Security

### Compliance Standards

* CIS GKE

### Triage and Remediation

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. On any machine with `gcloud` and `kubectl` access, list external LoadBalancer Services and candidate apps to migrate to Ingress:
           ```bash theme={null}
           kubectl get svc -A -o wide | grep LoadBalancer || echo "No LoadBalancer services"
           ```

        2. For each HTTP/HTTPS LoadBalancer Service you want to protect with managed certs, create or reuse a DNS name that points to the GKE Ingress IP (will be known after step 5). Ensure you control the DNS zone in Cloud DNS or another DNS provider.

        3. On any machine with `gcloud`/`kubectl` access, create a `ManagedCertificate` for each domain you want Google to manage (replace values with your real domain and namespace):
           ```bash theme={null}
           cat > managed-cert-example.yaml << 'EOF'
           apiVersion: networking.gke.io/v1
           kind: ManagedCertificate
           metadata:
             name: example-com-cert
             namespace: default
           spec:
             domains:
               - example.com
               - www.example.com
           EOF

           kubectl apply -f managed-cert-example.yaml
           ```

        4. Migrate the existing external Service to Ingress by:
           * Changing the Service to `ClusterIP` (so it is only internal to the cluster), for example:
             ```bash theme={null}
             kubectl -n default patch service my-app-service \
               -p '{"spec":{"type":"ClusterIP","externalIPs":null,"loadBalancerIP":null}}'
             ```
           * Creating an Ingress that:
             * Routes to that Service
             * Uses the Google HTTP(S) Load Balancer `gce` controller
             * References the `ManagedCertificate` via annotation:
             ```bash theme={null}
             cat > ingress-my-app.yaml << 'EOF'
             apiVersion: networking.k8s.io/v1
             kind: Ingress
             metadata:
               name: my-app-ingress
               namespace: default
               annotations:
                 kubernetes.io/ingress.class: "gce"
                 networking.gke.io/managed-certificates: "example-com-cert"
             spec:
               rules:
                 - host: example.com
                   http:
                     paths:
                       - path: /
                         pathType: Prefix
                         backend:
                           service:
                             name: my-app-service
                             port:
                               number: 80
               # Optional HTTPS redirect via BackendConfig can be added separately
             EOF

             kubectl apply -f ingress-my-app.yaml
             ```

        5. After the Ingress is created, obtain its external IP and point your DNS records at it (run on any machine with `kubectl`):
           ```bash theme={null}
           kubectl -n default get ingress my-app-ingress -o wide
           ```
           Create/modify A (and/or CNAME) records for `example.com` and `www.example.com` to target the Ingress IP. Wait for DNS to propagate and for the ManagedCertificate to become `Active`:
           ```bash theme={null}
           kubectl -n default get managedcertificate example-com-cert -o yaml | grep -A3 status:
           ```

        6. Verify overall compliance from any machine with `kubectl` and `jq`:
           ```bash theme={null}
           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_ANDNO_PUBLIC_LB_SERVICES"
                 end
             end
           '
           ```
      </Accordion>

      <Accordion title="Using kubectl">
        kubectl cannot remediate this finding because it requires configuring Google-managed SSL certificates and Ingress behavior at the GKE / cloud provider configuration and IaC layer. Make the changes through the Google Cloud Console, gcloud, or your IaC (per the Manual Steps section), then re-run the audit command from any machine with kubectl access to confirm the issue is resolved.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        #
        # automate-gke-managed-certs.sh
        #
        # Idempotently:
        # - Detects public Services of type LoadBalancer
        # - Creates corresponding Ingress objects that use Google-managed SSL certificates
        # - Creates ManagedCertificate resources per service
        # - Leaves existing Ingresses/ManagedCertificates untouched
        # - Verifies using the benchmark audit logic
        #
        # REQUIREMENTS (run on any machine with kubectl and jq configured for the cluster):
        # - kubectl
        # - jq
        #
        # LIMITATIONS:
        # - This cannot change GKE control-plane settings; it only applies Kubernetes
        #   resources via the API.
        # - It assumes HTTP applications on port 80 behind the Service. You may need to
        #   adjust hostnames and paths for production use.

        set -euo pipefail

        # ------------- CONFIGURABLE SETTINGS ----------------

        # Default DNS suffix to use when fabricating hostnames for demo purposes.
        # In real environments, set this to a domain you control, or replace logic
        # below to read proper hostnames from an inventory.
        DEFAULT_DOMAIN="example.com"

        # Label selector to filter which public LoadBalancer Services to convert.
        # Leave empty ("") to target all.
        SVC_LABEL_SELECTOR=""

        # ------------- HELPER FUNCTIONS ---------------------

        log() {
          printf '[%s] %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" >&2
        }

        kubectl_json() {
          # Wrapper to avoid repetitive 2>/dev/null.
          kubectl "$@" -o json 2>/dev/null
        }

        # Idempotently apply a manifest from stdin
        kapply() {
          kubectl apply -f -
        }

        # ------------- DISCOVERY ----------------------------

        log "Discovering Services, Ingresses and ManagedCertificates..."

        svc_json="$(kubectl_json get svc -A || echo '{"items":[],"__err":"SVC_FORBIDDEN"}')"
        ing_json="$(kubectl_json get ingress -A || echo '{"items":[],"__err":"INGRESS_FORBIDDEN"}')"
        mc_json="$(kubectl_json get managedcertificates -A || echo '{"items":[],"__err":"MC_FORBIDDEN"}')"

        svcs_err="$(echo "$svc_json" | jq -r '.__err // empty')"
        ings_err="$(echo "$ing_json" | jq -r '.__err // empty')"
        mcs_err="$(echo "$mc_json" | jq -r '.__err // empty')"

        if [[ -n "$svcs_err" || -n "$ings_err" || -n "$mcs_err" ]]; then
          log "ERROR: Missing permissions for kubectl list: svc_err='$svcs_err' ing_err='$ings_err' mc_err='$mcs_err'"
          log "Grant list access to services, ingresses, and managedcertificates and re-run."
          exit 1
        fi

        # ------------- SELECT PUBLIC LOADBALANCER SERVICES ----------------

        log "Selecting candidate Services of type LoadBalancer..."

        svc_filter_args=()
        if [[ -n "$SVC_LABEL_SELECTOR" ]]; then
          svc_filter_args=( -l "$SVC_LABEL_SELECTOR" )
        fi

        svc_lb_json="$(kubectl get svc -A "${svc_filter_args[@]}" -o json \
          | jq '.items // [] | map(select(.spec.type=="LoadBalancer"))')"

        svc_count="$(echo "$svc_lb_json" | jq 'length')"

        if [[ "$svc_count" -eq 0 ]]; then
          log "No public LoadBalancer Services found (after label filtering). Nothing to convert."
        else
          log "Found $svc_count public LoadBalancer Service(s) to inspect."
        fi

        # ------------- CREATE MANAGEDCERT + INGRESS PER SERVICE -----------

        log "Ensuring Google-managed SSL certificates and Ingress resources exist..."

        for idx in $(seq 0 $((svc_count - 1))); do
          svc="$(echo "$svc_lb_json" | jq ".[$idx]")"
          ns="$(echo "$svc" | jq -r '.metadata.namespace')"
          name="$(echo "$svc" | jq -r '.metadata.name')"

          # Derive resource names
          mc_name="${name}-managed-cert"
          ing_name="${name}-ing"

          # Choose a hostname. Adjust to real DNS in production.
          host="${name}.${ns}.${DEFAULT_DOMAIN}"

          log "Processing Service: ${ns}/${name} -> Ingress: ${ns}/${ing_name}, ManagedCertificate: ${ns}/${mc_name}, host: ${host}"

          # Check for existing ManagedCertificate
          mc_exists="false"
          if kubectl get managedcertificate "$mc_name" -n "$ns" >/dev/null 2>&1; then
            mc_exists="true"
            log "  ManagedCertificate ${ns}/${mc_name} already exists; leaving as-is."
          fi

          # Check for Ingress already annotated to use this ManagedCertificate
          ing_exists="false"
          if kubectl get ingress "$ing_name" -n "$ns" >/dev/null 2>&1; then
            ing_exists="true"
            log "  Ingress ${ns}/${ing_name} already exists; leaving as-is (idempotent apply will reconcile)."
          fi

          # Create/Update ManagedCertificate (idempotent)
          if [[ "$mc_exists" == "false" ]]; then
            log "  Creating ManagedCertificate ${ns}/${mc_name}..."
          else
            log "  Updating ManagedCertificate ${ns}/${mc_name} (idempotent)..."
          fi

          cat <<EOF | kapply
        apiVersion: networking.gke.io/v1
        kind: ManagedCertificate
        metadata:
          name: ${mc_name}
          namespace: ${ns}
        spec:
          domains:
            - ${host}
        EOF

          # Build basic HTTP Ingress referencing the Service and ManagedCertificate
          # NOTE: This assumes Service exposes port 80. Adjust as needed.
          log "  Creating/Updating Ingress ${ns}/${ing_name} with managed certificate annotation..."

          cat <<EOF | kapply
        apiVersion: networking.k8s.io/v1
        kind: Ingress
        metadata:
          name: ${ing_name}
          namespace: ${ns}
          annotations:
            kubernetes.io/ingress.class: "gce"
            networking.gke.io/managed-certificates: "${mc_name}"
        spec:
          rules:
            - host: ${host}
              http:
                paths:
                  - path: /
                    pathType: Prefix
                    backend:
                      service:
                        name: ${name}
                        port:
                          number: 80
          tls:
            - hosts:
                - ${host}
        EOF

        done

        # ------------- VERIFICATION (BENCHMARK-AWARE) ---------------------

        log "Running verification using the benchmark audit logic..."

        verify_svc_json="$(kubectl get svc -A -o json 2>/dev/null || echo '{"items":[],"__err":"SVC_FORBIDDEN"}')"
        verify_ing_json="$(kubectl get ingress -A -o json 2>/dev/null || echo '{"items":[],"__err":"INGRESS_FORBIDDEN"}')"
        verify_mc_json="$(kubectl get managedcertificates -A -o json 2>/dev/null || echo '{"items":[],"__err":"MC_FORBIDDEN"}')"

        printf '%s\n%s\n%s\n' "$verify_svc_json" "$verify_ing_json" "$verify_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
        ' | while read -r line; do
          log "VERIFY: $line"
        done

        log "Script completed. Review verification output above. For any remaining FOUND_PUBLIC_LB_SERVICE entries, decide case-by-case whether they should remain as LoadBalancer or be refactored into Ingress + ManagedCertificate."
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
