Files

193 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""Agent for implementing container network policies with Calico.
Audits Kubernetes network policies, identifies unprotected
namespaces, validates Calico policy enforcement, and generates
default-deny baseline policy manifests.
"""
import argparse
import json
import os
import subprocess
from datetime import datetime
from pathlib import Path
try:
from kubernetes import client, config
except ImportError:
client = None
config = None
DEFAULT_DENY_INGRESS = {
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {"name": "default-deny-ingress"},
"spec": {"podSelector": {}, "policyTypes": ["Ingress"]},
}
DEFAULT_DENY_EGRESS = {
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {"name": "default-deny-egress"},
"spec": {"podSelector": {}, "policyTypes": ["Egress"]},
}
class CalicoNetworkPolicyAgent:
"""Audits and manages Calico network policies on Kubernetes."""
def __init__(self, kubeconfig=None, output_dir="./calico_policy_audit"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.findings = []
self.v1 = None
self.net_v1 = None
if client and config:
try:
if kubeconfig:
config.load_kube_config(config_file=kubeconfig)
else:
config.load_kube_config()
self.v1 = client.CoreV1Api()
self.net_v1 = client.NetworkingV1Api()
except Exception:
pass
def _run_calicoctl(self, args):
"""Run calicoctl command and return parsed output."""
try:
result = subprocess.run(
["calicoctl"] + args, capture_output=True, text=True, timeout=30
)
return result.stdout, result.returncode
except FileNotFoundError:
return "", 1
except subprocess.TimeoutExpired:
return "", 1
def list_k8s_network_policies(self):
"""List all Kubernetes NetworkPolicy resources across namespaces."""
if not self.net_v1:
return []
policies = self.net_v1.list_network_policy_for_all_namespaces()
return [{"name": p.metadata.name, "namespace": p.metadata.namespace,
"pod_selector": p.spec.pod_selector.match_labels or {},
"policy_types": p.spec.policy_types or [],
"ingress_rules": len(p.spec.ingress or []),
"egress_rules": len(p.spec.egress or [])}
for p in policies.items]
def list_calico_policies(self):
"""List Calico-specific NetworkPolicy and GlobalNetworkPolicy resources."""
policies = []
stdout, rc = self._run_calicoctl(["get", "networkpolicy", "-o", "json", "--all-namespaces"])
if rc == 0 and stdout.strip():
try:
data = json.loads(stdout)
items = data.get("items", [data]) if "items" in data else [data]
for item in items:
meta = item.get("metadata", {})
policies.append({"kind": "NetworkPolicy", "name": meta.get("name"),
"namespace": meta.get("namespace")})
except json.JSONDecodeError:
pass
stdout, rc = self._run_calicoctl(["get", "globalnetworkpolicy", "-o", "json"])
if rc == 0 and stdout.strip():
try:
data = json.loads(stdout)
items = data.get("items", [data]) if "items" in data else [data]
for item in items:
policies.append({"kind": "GlobalNetworkPolicy",
"name": item.get("metadata", {}).get("name")})
except json.JSONDecodeError:
pass
return policies
def find_unprotected_namespaces(self):
"""Identify namespaces without any network policies."""
if not self.v1 or not self.net_v1:
return []
namespaces = [ns.metadata.name for ns in self.v1.list_namespace().items]
policies = self.net_v1.list_network_policy_for_all_namespaces()
protected = {p.metadata.namespace for p in policies.items}
system_ns = {"kube-system", "kube-public", "kube-node-lease", "calico-system"}
unprotected = [ns for ns in namespaces if ns not in protected and ns not in system_ns]
for ns in unprotected:
self.findings.append({"severity": "high", "type": "Unprotected Namespace",
"detail": f"Namespace '{ns}' has no network policies"})
return unprotected
def generate_default_deny(self, namespace):
"""Generate default-deny policy manifests for a namespace."""
ingress = {**DEFAULT_DENY_INGRESS, "metadata": {**DEFAULT_DENY_INGRESS["metadata"],
"namespace": namespace}}
egress = {**DEFAULT_DENY_EGRESS, "metadata": {**DEFAULT_DENY_EGRESS["metadata"],
"namespace": namespace}}
return {"namespace": namespace, "ingress_policy": ingress, "egress_policy": egress}
def check_calico_node_status(self):
"""Check Calico node status using calicoctl."""
stdout, rc = self._run_calicoctl(["node", "status"])
if rc == 0:
return {"status": "healthy", "output": stdout[:500]}
return {"status": "unavailable"}
def test_connectivity(self, src_pod, dst_pod, namespace, port=80):
"""Test pod-to-pod connectivity using kubectl exec."""
try:
result = subprocess.run(
["kubectl", "exec", src_pod, "-n", namespace, "--",
"wget", "--spider", "--timeout=3", f"http://{dst_pod}:{port}"],
capture_output=True, text=True, timeout=10,
)
return {"src": src_pod, "dst": dst_pod, "port": port,
"connected": result.returncode == 0}
except (FileNotFoundError, subprocess.TimeoutExpired):
return {"src": src_pod, "dst": dst_pod, "connected": None, "error": "test failed"}
def generate_report(self):
k8s_policies = self.list_k8s_network_policies()
calico_policies = self.list_calico_policies()
unprotected = self.find_unprotected_namespaces()
node_status = self.check_calico_node_status()
deny_manifests = [self.generate_default_deny(ns) for ns in unprotected[:5]]
report = {
"report_date": datetime.utcnow().isoformat(),
"k8s_network_policies": {"count": len(k8s_policies), "policies": k8s_policies},
"calico_policies": {"count": len(calico_policies), "policies": calico_policies},
"unprotected_namespaces": unprotected,
"calico_node_status": node_status,
"recommended_deny_policies": deny_manifests,
"findings": self.findings,
"total_findings": len(self.findings),
}
out = self.output_dir / "calico_policy_report.json"
with open(out, "w") as f:
json.dump(report, f, indent=2)
print(json.dumps(report, indent=2))
return report
def main():
parser = argparse.ArgumentParser(
description="Audit and manage Calico network policies on Kubernetes"
)
parser.add_argument("--kubeconfig", default=None,
help="Path to kubeconfig file")
parser.add_argument("--output-dir", default="./calico_policy_audit",
help="Output directory for report")
args = parser.parse_args()
os.makedirs(args.output_dir, exist_ok=True)
agent = CalicoNetworkPolicyAgent(kubeconfig=args.kubeconfig,
output_dir=args.output_dir)
agent.generate_report()
if __name__ == "__main__":
main()