AnalysisType: rule
Filename: anonymous_api_access.py
RuleID: "Amazon.EKS.AnonymousAPIAccess"
DisplayName: "EKS Anonymous API Access Detected"
Enabled: true
LogTypes:
- Amazon.EKS.Audit
Severity: Low
Reports:
MITRE ATT&CK:
- "TA0001:T1190" # Initial Access: Exploit Public-Facing Application
Description: >
This rule detects anonymous API requests made to the Kubernetes API server.
In production environments, anonymous access should be disabled to prevent
unauthorized access to the API server.
DedupPeriodMinutes: 60
Reference:
https://raesene.github.io/blog/2023/03/18/lets-talk-about-anonymous-access-to-Kubernetes/
Runbook: |
Check the EKS cluster configuration and ensure that anonymous access
to the Kubernetes API server is disabled. This can be done by verifying the API
server arguments and authentication webhook configuration.
SummaryAttributes:
- user:username
- p_any_ip_addresses
- p_source_label
Tags:
- EKS
- Security Control
- API
- Initial Access:Exploit Public-Facing Application
Tests:
- Name: Anonymous API Access
ExpectedResult: true
Log:
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer"
},
"apiVersion": "audit.k8s.io/v1",
"auditID": "abcde12345",
"kind": "Event",
"level": "Request",
"objectRef": {
"apiVersion": "v1",
"name": "test-pod",
"namespace": "default",
"resource": "pods"
},
"p_any_aws_account_ids": [
"123412341234"
],
"p_any_aws_arns": [
"arn:aws:iam::123412341234:role/DevAdministrator"
],
"p_any_ip_addresses": [
"8.8.8.8"
],
"p_any_usernames": [
"system:anonymous"
],
"p_event_time": "2022-11-29 00:09:04.38",
"p_log_type": "Amazon.EKS.Audit",
"p_parse_time": "2022-11-29 00:10:25.067",
"p_row_id": "2e4ab474b0f0f7a4a8fff4f014a9b32a",
"p_source_id": "4c859cd4-9406-469b-9e0e-c2dc1bee24fa",
"p_source_label": "example-cluster-eks-logs",
"requestReceivedTimestamp": "2022-11-29 00:09:04.38",
"requestURI": "/api/v1/namespaces/default/pods/test-pod",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"8.8.8.8"
],
"stage": "ResponseComplete",
"user": {
"username": "system:anonymous"
},
"userAgent": "kubectl/v1.25.4"
}
- Name: Non-Anonymous API Access
ExpectedResult: false
Log:
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer"
},
"apiVersion": "audit.k8s.io/v1",
"auditID": "abcde12345",
"kind": "Event",
"level": "Request",
"objectRef": {
"apiVersion": "v1",
"name": "test-pod",
"namespace": "default",
"resource": "pods"
},
"p_any_aws_account_ids": [
"123412341234"
],
"p_any_aws_arns": [
"arn:aws:iam::123412341234:role/DevAdministrator"
],
"p_any_ip_addresses": [
"8.8.8.8"
],
"p_any_usernames": [
"kubernetes-admin"
],
"p_event_time": "2022-11-29 00:09:04.38",
"p_log_type": "Amazon.EKS.Audit",
"p_parse_time": "2022-11-29 00:10:25.067",
"p_row_id": "2e4ab474b0f0f7a4a8fff4f014a9b32a",
"p_source_id": "4c859cd4-9406-469b-9e0e-c2dc1bee24fa",
"p_source_label": "example-cluster-eks-logs",
"requestReceivedTimestamp": "2022-11-29 00:09:04.38",
"requestURI": "/api/v1/namespaces/default/pods/test-pod",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"8.8.8.8"
],
"stage": "ResponseComplete",
"user": {
"username": "kubernetes-admin"
},
"userAgent": "kubectl/v1.25.4"
}
- Name: Anonymous API Access Web Scanner Allowed
ExpectedResult: true
Log:
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding \"system:public-info-viewer\" of ClusterRole \"system:public-info-viewer\" to Group \"system:unauthenticated\""
},
"apiVersion": "audit.k8s.io/v1",
"auditID": "d976bfc6-a2bc-49d5-bdeb-074441e0b875",
"kind": "Event",
"level": "Metadata",
"requestReceivedTimestamp": "2024-11-13 18:34:10.595141000",
"requestURI": "/version",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"44.238.138.237"
],
"stage": "ResponseComplete",
"stageTimestamp": "2024-11-13 18:34:10.595494000",
"user": {
"groups": [
"system:unauthenticated"
],
"username": "system:anonymous"
},
"userAgent": "python-requests/2.31.0",
"verb": "get"
}
- Name: ELB Health Check from 10.0.0.0/8
ExpectedResult: false
Log:
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer"
},
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"level": "Metadata",
"requestURI": "/healthz",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"10.128.5.42"
],
"stage": "ResponseComplete",
"user": {
"username": "system:anonymous"
},
"userAgent": "ELB-HealthChecker/2.0"
}
- Name: ELB Health Check from 172.16.0.0/12
ExpectedResult: false
Log:
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer"
},
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"level": "Metadata",
"requestURI": "/healthz",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"172.20.1.15"
],
"stage": "ResponseComplete",
"user": {
"username": "system:anonymous"
},
"userAgent": "ELB-HealthChecker/2.0"
}
- Name: ELB Health Check from 192.168.0.0/16
ExpectedResult: false
Log:
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer"
},
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"level": "Metadata",
"requestURI": "/healthz",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"192.168.10.20"
],
"stage": "ResponseComplete",
"user": {
"username": "system:anonymous"
},
"userAgent": "ELB-HealthChecker/2.0"
}
- Name: ELB Health Check from Public IP
ExpectedResult: true
Log:
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer"
},
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"level": "Metadata",
"requestURI": "/healthz",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"54.10.20.30"
],
"stage": "ResponseComplete",
"user": {
"username": "system:anonymous"
},
"userAgent": "ELB-HealthChecker/2.0"
}
- Name: Anonymous API Access Web Scanner Denied
ExpectedResult: true
Log:
{
"annotations": {
"authorization.k8s.io/decision": "forbid",
"authorization.k8s.io/reason": ""
},
"apiVersion": "audit.k8s.io/v1",
"auditID": "edf35e8d-92c3-4507-9bc6-4dd9cf068bcf",
"kind": "Event",
"level": "Metadata",
"requestReceivedTimestamp": "2024-11-13 23:50:32.672347000",
"requestURI": "/vendor/phpunit/src/Util/PHP/eval-stdin.php",
"responseStatus": {
"code": 403,
"message": "forbidden: User \"system:anonymous\" cannot get path \"/vendor/phpunit/src/Util/PHP/eval-stdin.php\"",
"reason": "Forbidden",
"status": "Failure"
},
"sourceIPs": [
"8.216.81.10"
],
"stage": "ResponseComplete",
"stageTimestamp": "2024-11-13 23:50:32.673504000",
"user": {
"groups": [
"system:unauthenticated"
],
"username": "system:anonymous"
},
"userAgent": "Custom-AsyncHttpClient",
"verb": "get"
}
# ------ paired body: anonymous_api_access.py ------
from ipaddress import ip_address
from panther_aws_helpers import eks_panther_obj_ref
def rule(event):
src_ip = event.get("sourceIPs", ["0.0.0.0"]) # nosec
if src_ip == ["127.0.0.1"]:
return False
if event.get("userAgent", "") == "ELB-HealthChecker/2.0":
try:
if ip_address(src_ip[0]).is_private:
return False
except ValueError:
pass
# Check if the username is set to "system:anonymous", which indicates anonymous access
if event.deep_get("user", "username") == "system:anonymous":
return True
return False
def title(event):
# For INFO-level events, just group them all together since they're not that interesting
if event.deep_get("annotations", "authorization.k8s.io/decision") != "allow":
return "Failed Anonymous EKS Access Attempt(s) Detected"
if event.get("requestURI") == "/version":
return "Anonymous EKS Access to /version Endpoint Detected"
p_eks = eks_panther_obj_ref(event)
return (
f"Anonymous API access detected on Kubernetes API server "
f"from [{p_eks.get('sourceIPs')[0]}] to [{event.get('requestURI', 'NO_URI')}] "
f"on [{p_eks.get('p_source_label')}]"
)
def severity(event):
if event.deep_get("annotations", "authorization.k8s.io/decision") != "allow":
return "INFO"
if event.get("requestURI") == "/version":
return "INFO"
return "DEFAULT"
def dedup(event):
# For INFO-level events, just group them all together since they're not that interesting
if severity(event) == "INFO":
return "no dedup"
p_eks = eks_panther_obj_ref(event)
return f"anonymous_access_{p_eks.get('p_source_label')}_{event.get('userAgent')}"
def alert_context(event):
p_eks = eks_panther_obj_ref(event)
mutable_event = event.to_dict()
mutable_event["p_eks"] = p_eks
return dict(mutable_event)