Kubernetes Ingress Created Without TLS


Description

This detection monitors for Ingress objects being created without TLS certificates configured. Ingresses without TLS expose services over unencrypted HTTP, allowing sensitive data like passwords, tokens, and PII to be transmitted in cleartext. This violates security best practices and compliance requirements like PCI-DSS and HIPAA, and enables man-in-the-middle attacks.

Query · python

from panther_base_helpers import deep_get
from panther_kubernetes_helpers import (
    is_failed_request,
    is_system_namespace,
    is_system_principal,
    k8s_alert_context,
)


def rule(event):
    verb = event.udm("verb")
    resource = event.udm("resource")
    namespace = event.udm("namespace")
    username = event.udm("username")
    response_status = event.udm("responseStatus")

    # Only check ingress creation events
    if verb != "create" or resource != "ingresses":
        return False

    # Skip failed requests
    if is_failed_request(response_status):
        return False

    # Exclude system namespaces and system principals to reduce false positives
    if is_system_namespace(namespace) or is_system_principal(username):
        return False

    # Check if ingress has TLS configuration
    tls = deep_get(event.udm("requestObject"), "spec", "tls")

    # Alert if TLS is not configured (missing or empty)
    if not tls:
        return True

    return False


def title(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_INGRESS>"

    return f"[{username}] created Ingress [{namespace}/{name}] without TLS certificate"


def dedup(event):
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_INGRESS>"
    return f"k8s_ingress_no_tls_{namespace}_{name}"


def severity(event):
    """Increase severity based on ingress annotations and rules."""
    request_object = event.udm("requestObject") or {}
    metadata = request_object.get("metadata", {})
    annotations = metadata.get("annotations", {})

    # Check if this is an external-facing ingress (has external annotations)
    external_annotations = [
        "kubernetes.io/ingress.class",
        "cert-manager.io/cluster-issuer",
        "external-dns.alpha.kubernetes.io/hostname",
    ]

    if any(key in annotations for key in external_annotations):
        return "MEDIUM"

    return "DEFAULT"


def alert_context(event):
    request_object = event.udm("requestObject") or {}
    spec = request_object.get("spec", {})
    rules = spec.get("rules", [])
    metadata = request_object.get("metadata", {})
    annotations = metadata.get("annotations", {})

    # Extract hosts from ingress rules
    hosts = []
    for rule_entry in rules:
        host = rule_entry.get("host")
        if host:
            hosts.append(host)

    return k8s_alert_context(
        event,
        extra_fields={
            "ingress_name": event.udm("name"),
            "ingress_hosts": hosts,
            "annotations": annotations,
            "has_tls": False,
        },
    )

Analyst notes

  1. Review the ingress specification to determine if this is an internal-only service or if TLS is terminated at an external load balancer
  2. If TLS is required, work with the team to configure TLS certificates using cert-manager or manual certificate creation
  3. Search for other ingresses without TLS in the past 7 days to identify if this is a systemic configuration issue
Raw source Kubernetes Ingress Created Without TLS · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
RuleID: "Kubernetes.Ingress.NoTLS"
DisplayName: "Kubernetes Ingress Created Without TLS"
Enabled: true
Filename: k8s_ingress_without_tls.py
LogTypes:
  - Amazon.EKS.Audit
  - Azure.MonitorActivity
  - GCP.AuditLog
Tags:
  - Kubernetes
  - Network Security
  - Encryption
  - Compliance
  - Unified Detection
Severity: Medium
Description: >
  This detection monitors for Ingress objects being created without TLS certificates configured. Ingresses
  without TLS expose services over unencrypted HTTP, allowing sensitive data like passwords, tokens, and PII
  to be transmitted in cleartext. This violates security best practices and compliance requirements like
  PCI-DSS and HIPAA, and enables man-in-the-middle attacks.
Runbook: |
  1. Review the ingress specification to determine if this is an internal-only service or if TLS is terminated at an external load balancer
  2. If TLS is required, work with the team to configure TLS certificates using cert-manager or manual certificate creation
  3. Search for other ingresses without TLS in the past 7 days to identify if this is a systemic configuration issue
Reports:
  MITRE ATT&CK:
    - TA0009:T1040 # Collection: Network Sniffing
    - TA0006:T1552.004 # Credential Access: Unsecured Credentials - Private Keys
Reference: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
DedupPeriodMinutes: 60
SummaryAttributes:
  - username
  - namespace
  - name
  - p_source_label
Tests:
  - Name: EKS Ingress without TLS
    ExpectedResult: true
    Log:
      {
        "kind": "Event",
        "apiVersion": "audit.k8s.io/v1",
        "verb": "create",
        "user": {"username": "developer@example.com"},
        "sourceIPs": ["203.0.113.42"],
        "userAgent": "kubectl/v1.28.0",
        "objectRef": {
          "resource": "ingresses",
          "namespace": "production",
          "name": "api-ingress",
          "apiGroup": "networking.k8s.io",
          "apiVersion": "v1"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "Ingress",
          "metadata": {
            "name": "api-ingress",
            "namespace": "production",
            "annotations": {
              "kubernetes.io/ingress.class": "nginx"
            }
          },
          "spec": {
            "rules": [
              {
                "host": "api.example.com",
                "http": {
                  "paths": [
                    {
                      "path": "/",
                      "backend": {
                        "serviceName": "api-service",
                        "servicePort": 80
                      }
                    }
                  ]
                }
              }
            ]
          }
        },
        "p_log_type": "Amazon.EKS.Audit",
        "p_source_label": "eks-cluster"
      }
  - Name: AKS Ingress without TLS
    ExpectedResult: true
    Log:
      {
        "p_log_type": "Azure.MonitorActivity",
        "category": "kube-audit",
        "operationName": "Microsoft.ContainerService/managedClusters/diagnosticLogs/Read",
        "properties": {
          "log": "{\"kind\":\"Event\",\"apiVersion\":\"audit.k8s.io/v1\",\"verb\":\"create\",\"user\":{\"username\":\"admin@example.com\"},\"sourceIPs\":[\"1.2.3.4\"],\"objectRef\":{\"resource\":\"ingresses\",\"namespace\":\"default\",\"name\":\"test-ingress\",\"apiGroup\":\"networking.k8s.io\"},\"responseStatus\":{\"code\":201},\"requestObject\":{\"kind\":\"Ingress\",\"metadata\":{\"name\":\"test-ingress\"},\"spec\":{\"rules\":[{\"http\":{\"paths\":[{\"path\":\"/testpath\",\"backend\":{\"serviceName\":\"test\",\"servicePort\":80}}]}}]}}}"
        },
        "p_source_label": "aks-cluster"
      }
  - Name: GKE Ingress without TLS with external DNS annotation
    ExpectedResult: true
    Log:
      {
        "protoPayload": {
          "authenticationInfo": {"principalEmail": "user@company.com"},
          "authorizationInfo": [{
            "granted": true,
            "permission": "io.k8s.networking.v1.ingresses.create",
            "resource": "networking.k8s.io/v1/namespaces/production/ingresses/web-ingress"
          }],
          "methodName": "io.k8s.networking.v1.ingresses.create",
          "requestMetadata": {"callerIP": "8.8.8.8"},
          "resourceName": "networking.k8s.io/v1/namespaces/production/ingresses/web-ingress",
          "serviceName": "k8s.io",
          "request": {
            "kind": "Ingress",
            "metadata": {
              "name": "web-ingress",
              "annotations": {
                "external-dns.alpha.kubernetes.io/hostname": "web.example.com"
              }
            },
            "spec": {
              "rules": [
                {
                  "host": "web.example.com",
                  "http": {
                    "paths": [{"path": "/", "backend": {"serviceName": "web", "servicePort": 8080}}]
                  }
                }
              ]
            }
          }
        },
        "resource": {
          "type": "k8s_cluster",
          "labels": {"project_id": "test-project"}
        },
        "p_log_type": "GCP.AuditLog",
        "p_source_label": "gke-cluster"
      }
  - Name: EKS Ingress with TLS configured
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "developer@example.com"},
        "objectRef": {
          "resource": "ingresses",
          "namespace": "production",
          "name": "secure-ingress"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "Ingress",
          "spec": {
            "tls": [
              {
                "hosts": ["api.example.com"],
                "secretName": "api-tls-cert"
              }
            ],
            "rules": [
              {
                "host": "api.example.com",
                "http": {"paths": [{"path": "/", "backend": {"serviceName": "api", "servicePort": 443}}]}
              }
            ]
          }
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: System namespace (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "admin@example.com"},
        "objectRef": {
          "resource": "ingresses",
          "namespace": "kube-system",
          "name": "system-ingress"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "Ingress",
          "spec": {
            "rules": [{"http": {"paths": [{"path": "/"}]}}]
          }
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: Failed request (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "developer@example.com"},
        "objectRef": {
          "resource": "ingresses",
          "namespace": "production",
          "name": "test-ingress"
        },
        "responseStatus": {"code": 403},
        "requestObject": {
          "kind": "Ingress",
          "spec": {
            "rules": [{"http": {"paths": [{"path": "/"}]}}]
          }
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: Updating existing ingress (not creating)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "update",
        "user": {"username": "admin@example.com"},
        "objectRef": {
          "resource": "ingresses",
          "namespace": "production",
          "name": "api-ingress"
        },
        "responseStatus": {"code": 200},
        "requestObject": {
          "kind": "Ingress",
          "spec": {
            "rules": [{"http": {"paths": [{"path": "/"}]}}]
          }
        },
        "p_log_type": "Amazon.EKS.Audit"
      }


# ------ paired body: k8s_ingress_without_tls.py ------

from panther_base_helpers import deep_get
from panther_kubernetes_helpers import (
    is_failed_request,
    is_system_namespace,
    is_system_principal,
    k8s_alert_context,
)


def rule(event):
    verb = event.udm("verb")
    resource = event.udm("resource")
    namespace = event.udm("namespace")
    username = event.udm("username")
    response_status = event.udm("responseStatus")

    # Only check ingress creation events
    if verb != "create" or resource != "ingresses":
        return False

    # Skip failed requests
    if is_failed_request(response_status):
        return False

    # Exclude system namespaces and system principals to reduce false positives
    if is_system_namespace(namespace) or is_system_principal(username):
        return False

    # Check if ingress has TLS configuration
    tls = deep_get(event.udm("requestObject"), "spec", "tls")

    # Alert if TLS is not configured (missing or empty)
    if not tls:
        return True

    return False


def title(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_INGRESS>"

    return f"[{username}] created Ingress [{namespace}/{name}] without TLS certificate"


def dedup(event):
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_INGRESS>"
    return f"k8s_ingress_no_tls_{namespace}_{name}"


def severity(event):
    """Increase severity based on ingress annotations and rules."""
    request_object = event.udm("requestObject") or {}
    metadata = request_object.get("metadata", {})
    annotations = metadata.get("annotations", {})

    # Check if this is an external-facing ingress (has external annotations)
    external_annotations = [
        "kubernetes.io/ingress.class",
        "cert-manager.io/cluster-issuer",
        "external-dns.alpha.kubernetes.io/hostname",
    ]

    if any(key in annotations for key in external_annotations):
        return "MEDIUM"

    return "DEFAULT"


def alert_context(event):
    request_object = event.udm("requestObject") or {}
    spec = request_object.get("spec", {})
    rules = spec.get("rules", [])
    metadata = request_object.get("metadata", {})
    annotations = metadata.get("annotations", {})

    # Extract hosts from ingress rules
    hosts = []
    for rule_entry in rules:
        host = rule_entry.get("host")
        if host:
            hosts.append(host)

    return k8s_alert_context(
        event,
        extra_fields={
            "ingress_name": event.udm("name"),
            "ingress_hosts": hosts,
            "annotations": annotations,
            "has_tls": False,
        },
    )

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.