Files
T

254 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""Kubernetes RBAC hardening audit agent.
Audits Kubernetes Role-Based Access Control configuration for security
weaknesses including overly permissive ClusterRoles, wildcard permissions,
privilege escalation paths, and service account misconfigurations.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
def run_kubectl(args_list, timeout=60):
"""Execute kubectl and return parsed JSON."""
cmd = ["kubectl"] + args_list + ["-o", "json"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
if result.returncode != 0:
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
return None
def audit_cluster_roles():
"""Audit ClusterRoles for dangerous permissions."""
findings = []
data = run_kubectl(["get", "clusterroles"])
if not data:
return findings
dangerous_verbs = {"*", "create", "update", "patch", "delete"}
dangerous_resources = {"secrets", "pods/exec", "clusterroles", "clusterrolebindings",
"roles", "rolebindings", "serviceaccounts", "nodes"}
escalation_resources = {"clusterroles", "clusterrolebindings", "roles", "rolebindings"}
for role in data.get("items", []):
name = role.get("metadata", {}).get("name", "")
if name.startswith("system:"):
continue # Skip system roles
for rule in role.get("rules", []):
verbs = set(rule.get("verbs", []))
resources = set(rule.get("resources", []))
api_groups = rule.get("apiGroups", [])
# Wildcard everything
if "*" in verbs and "*" in resources:
findings.append({
"type": "wildcard_all", "role": name, "kind": "ClusterRole",
"severity": "CRITICAL",
"detail": "Full wildcard access (verbs: *, resources: *)",
"recommendation": "Replace with specific verbs and resources",
})
# Secrets access
if resources & {"secrets", "*"} and verbs & {"get", "list", "watch", "*"}:
findings.append({
"type": "secrets_access", "role": name, "kind": "ClusterRole",
"severity": "HIGH",
"detail": f"Can read secrets (verbs: {verbs & {'get', 'list', 'watch', '*'}})",
})
# Pod exec
if "pods/exec" in resources or ("pods" in resources and "create" in verbs):
findings.append({
"type": "pod_exec", "role": name, "kind": "ClusterRole",
"severity": "HIGH",
"detail": "Can exec into pods (container escape risk)",
})
# Privilege escalation via RBAC modification
if resources & escalation_resources and verbs & {"create", "update", "patch", "*"}:
findings.append({
"type": "rbac_escalation", "role": name, "kind": "ClusterRole",
"severity": "CRITICAL",
"detail": f"Can modify RBAC resources: {resources & escalation_resources}",
})
# Node access
if "nodes" in resources and verbs & {"get", "list", "proxy", "*"}:
findings.append({
"type": "node_access", "role": name, "kind": "ClusterRole",
"severity": "MEDIUM",
"detail": "Can access node resources",
})
return findings
def audit_cluster_role_bindings():
"""Audit ClusterRoleBindings for overly broad subject assignments."""
findings = []
data = run_kubectl(["get", "clusterrolebindings"])
if not data:
return findings
for binding in data.get("items", []):
name = binding.get("metadata", {}).get("name", "")
if name.startswith("system:"):
continue
role_ref = binding.get("roleRef", {})
role_name = role_ref.get("name", "")
subjects = binding.get("subjects", [])
for subject in subjects:
kind = subject.get("kind", "")
subj_name = subject.get("name", "")
namespace = subject.get("namespace", "")
# Cluster-admin binding
if role_name == "cluster-admin":
findings.append({
"type": "cluster_admin_binding",
"binding": name, "subject": f"{kind}/{subj_name}",
"severity": "CRITICAL",
"detail": f"cluster-admin bound to {kind} '{subj_name}'",
})
# Group bindings to all authenticated/unauthenticated
if kind == "Group" and subj_name in ("system:authenticated", "system:unauthenticated"):
findings.append({
"type": "broad_group_binding",
"binding": name, "subject": subj_name,
"severity": "CRITICAL" if subj_name == "system:unauthenticated" else "HIGH",
"detail": f"Role '{role_name}' bound to group '{subj_name}'",
})
# Default service account bindings
if kind == "ServiceAccount" and subj_name == "default":
findings.append({
"type": "default_sa_binding",
"binding": name, "subject": f"default SA in {namespace}",
"severity": "MEDIUM",
"detail": f"Role '{role_name}' bound to default service account",
})
return findings
def audit_service_accounts(namespace=None):
"""Audit service accounts for misconfigurations."""
findings = []
cmd = ["get", "serviceaccounts", "--all-namespaces"] if not namespace else ["get", "serviceaccounts", "-n", namespace]
data = run_kubectl(cmd)
if not data:
return findings
for sa in data.get("items", []):
name = sa.get("metadata", {}).get("name", "")
ns = sa.get("metadata", {}).get("namespace", "")
automount = sa.get("automountServiceAccountToken", None)
if name == "default" and automount is not False:
findings.append({
"type": "default_sa_automount",
"namespace": ns, "service_account": name,
"severity": "MEDIUM",
"detail": f"Default SA in '{ns}' has automountServiceAccountToken enabled",
"recommendation": "Set automountServiceAccountToken: false on default SA",
})
secrets = sa.get("secrets", [])
if len(secrets) > 1:
findings.append({
"type": "sa_multiple_secrets",
"namespace": ns, "service_account": name,
"severity": "LOW",
"detail": f"SA has {len(secrets)} token secrets",
})
return findings
def format_summary(role_findings, binding_findings, sa_findings):
"""Print RBAC audit summary."""
all_findings = role_findings + binding_findings + sa_findings
print(f"\n{'='*60}")
print(f" Kubernetes RBAC Hardening Audit")
print(f"{'='*60}")
print(f" ClusterRole Issues : {len(role_findings)}")
print(f" Binding Issues : {len(binding_findings)}")
print(f" ServiceAccount Issues : {len(sa_findings)}")
print(f" Total Findings : {len(all_findings)}")
severity_counts = {}
for f in all_findings:
sev = f.get("severity", "INFO")
severity_counts[sev] = severity_counts.get(sev, 0) + 1
print(f"\n By Severity:")
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
count = severity_counts.get(sev, 0)
if count:
print(f" {sev:10s}: {count}")
if all_findings:
print(f"\n Top Findings:")
for f in sorted(all_findings, key=lambda x: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2}.get(x["severity"], 9))[:15]:
print(f" [{f['severity']:8s}] {f['type']:25s} | {f.get('detail', '')[:50]}")
return severity_counts
def main():
parser = argparse.ArgumentParser(description="Kubernetes RBAC hardening audit agent")
parser.add_argument("--namespace", "-n", help="Specific namespace to audit")
parser.add_argument("--kubeconfig", help="Path to kubeconfig")
parser.add_argument("--skip-roles", action="store_true")
parser.add_argument("--skip-bindings", action="store_true")
parser.add_argument("--skip-sa", action="store_true")
parser.add_argument("--output", "-o", help="Output JSON report")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
if args.kubeconfig:
os.environ["KUBECONFIG"] = args.kubeconfig
role_findings = [] if args.skip_roles else audit_cluster_roles()
binding_findings = [] if args.skip_bindings else audit_cluster_role_bindings()
sa_findings = [] if args.skip_sa else audit_service_accounts(args.namespace)
severity_counts = format_summary(role_findings, binding_findings, sa_findings)
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tool": "K8s RBAC Audit",
"role_findings": role_findings,
"binding_findings": binding_findings,
"sa_findings": sa_findings,
"severity_counts": severity_counts,
"risk_level": (
"CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
else "HIGH" if severity_counts.get("HIGH", 0) > 0
else "MEDIUM" if severity_counts.get("MEDIUM", 0) > 0
else "LOW"
),
}
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"\n[+] Report saved to {args.output}")
elif args.verbose:
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()