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,
},
)