mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 21:54:56 +03:00
341 lines
12 KiB
Python
341 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tailscale Zero Trust VPN Management and Monitoring.
|
|
|
|
Manages Tailscale deployment, ACL generation, network health monitoring,
|
|
and compliance reporting for zero trust mesh VPN infrastructure.
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import datetime
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class TailscaleNode:
|
|
hostname: str
|
|
ip4: str
|
|
ip6: str = ""
|
|
os: str = ""
|
|
online: bool = False
|
|
tags: list = field(default_factory=list)
|
|
key_expiry: str = ""
|
|
last_seen: str = ""
|
|
exit_node: bool = False
|
|
subnet_routes: list = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class ACLRule:
|
|
action: str # "accept" or "deny"
|
|
src: list
|
|
dst: list
|
|
description: str = ""
|
|
|
|
|
|
@dataclass
|
|
class ACLGroup:
|
|
name: str
|
|
members: list
|
|
|
|
|
|
class TailscaleACLGenerator:
|
|
"""Generate and validate Tailscale ACL configurations."""
|
|
|
|
def __init__(self):
|
|
self.groups: dict[str, list] = {}
|
|
self.tag_owners: dict[str, list] = {}
|
|
self.acls: list[dict] = []
|
|
self.ssh_rules: list[dict] = []
|
|
self.auto_approvers: dict = {"routes": {}, "exitNode": []}
|
|
self.node_attrs: list[dict] = []
|
|
|
|
def add_group(self, name: str, members: list):
|
|
if not name.startswith("group:"):
|
|
name = f"group:{name}"
|
|
self.groups[name] = members
|
|
|
|
def add_tag(self, tag: str, owners: list):
|
|
if not tag.startswith("tag:"):
|
|
tag = f"tag:{tag}"
|
|
self.tag_owners[tag] = owners
|
|
|
|
def add_acl_rule(self, src: list, dst: list, action: str = "accept"):
|
|
self.acls.append({"action": action, "src": src, "dst": dst})
|
|
|
|
def add_ssh_rule(self, src: list, dst: list, users: list, action: str = "check"):
|
|
self.ssh_rules.append({
|
|
"action": action,
|
|
"src": src,
|
|
"dst": dst,
|
|
"users": users,
|
|
})
|
|
|
|
def add_auto_approver_route(self, cidr: str, approvers: list):
|
|
self.auto_approvers["routes"][cidr] = approvers
|
|
|
|
def add_auto_approver_exit_node(self, approvers: list):
|
|
self.auto_approvers["exitNode"] = approvers
|
|
|
|
def generate_policy(self) -> dict:
|
|
policy = {}
|
|
if self.groups:
|
|
policy["groups"] = self.groups
|
|
if self.tag_owners:
|
|
policy["tagOwners"] = self.tag_owners
|
|
if self.acls:
|
|
policy["acls"] = self.acls
|
|
if self.ssh_rules:
|
|
policy["ssh"] = self.ssh_rules
|
|
if self.auto_approvers["routes"] or self.auto_approvers["exitNode"]:
|
|
policy["autoApprovers"] = self.auto_approvers
|
|
if self.node_attrs:
|
|
policy["nodeAttrs"] = self.node_attrs
|
|
return policy
|
|
|
|
def export_policy(self, output_path: str):
|
|
policy = self.generate_policy()
|
|
path = Path(output_path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, "w") as f:
|
|
json.dump(policy, f, indent=2)
|
|
return policy
|
|
|
|
def validate_policy(self) -> list:
|
|
"""Validate ACL policy for common issues."""
|
|
issues = []
|
|
policy = self.generate_policy()
|
|
|
|
# Check for empty ACLs
|
|
if not policy.get("acls"):
|
|
issues.append("WARNING: No ACL rules defined - all traffic will be denied")
|
|
|
|
# Check for overly permissive rules
|
|
for i, rule in enumerate(self.acls):
|
|
if "*" in rule.get("src", []) and "*" in rule.get("dst", []):
|
|
issues.append(f"CRITICAL: Rule {i} allows all-to-all access")
|
|
for dst in rule.get("dst", []):
|
|
if dst.endswith(":*"):
|
|
issues.append(f"WARNING: Rule {i} allows all ports to {dst}")
|
|
|
|
# Check groups reference valid members
|
|
for group_name, members in self.groups.items():
|
|
if not members:
|
|
issues.append(f"WARNING: Group {group_name} has no members")
|
|
|
|
# Check tag owners exist
|
|
for tag, owners in self.tag_owners.items():
|
|
for owner in owners:
|
|
if owner.startswith("group:") and owner not in self.groups:
|
|
issues.append(f"ERROR: Tag {tag} references undefined group {owner}")
|
|
|
|
# Check SSH rules
|
|
for i, rule in enumerate(self.ssh_rules):
|
|
if "root" in rule.get("users", []) and rule.get("action") != "check":
|
|
issues.append(f"WARNING: SSH rule {i} allows root access without re-auth check")
|
|
|
|
return issues
|
|
|
|
|
|
class TailscaleMonitor:
|
|
"""Monitor Tailscale network health and compliance."""
|
|
|
|
def __init__(self):
|
|
self.nodes: list[TailscaleNode] = []
|
|
|
|
def get_status(self) -> dict:
|
|
"""Get current Tailscale status via CLI."""
|
|
try:
|
|
result = subprocess.run(
|
|
["tailscale", "status", "--json"],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
if result.returncode == 0:
|
|
return json.loads(result.stdout)
|
|
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
|
|
pass
|
|
return {}
|
|
|
|
def parse_nodes(self, status: dict) -> list[TailscaleNode]:
|
|
"""Parse Tailscale status into node objects."""
|
|
self.nodes = []
|
|
peers = status.get("Peer", {})
|
|
for peer_id, peer_data in peers.items():
|
|
node = TailscaleNode(
|
|
hostname=peer_data.get("HostName", "unknown"),
|
|
ip4=peer_data.get("TailscaleIPs", [""])[0] if peer_data.get("TailscaleIPs") else "",
|
|
ip6=peer_data.get("TailscaleIPs", ["", ""])[1] if len(peer_data.get("TailscaleIPs", [])) > 1 else "",
|
|
os=peer_data.get("OS", ""),
|
|
online=peer_data.get("Online", False),
|
|
tags=peer_data.get("Tags", []),
|
|
key_expiry=peer_data.get("KeyExpiry", ""),
|
|
last_seen=peer_data.get("LastSeen", ""),
|
|
exit_node=peer_data.get("ExitNode", False),
|
|
)
|
|
self.nodes.append(node)
|
|
return self.nodes
|
|
|
|
def check_health(self) -> dict:
|
|
"""Run health checks on the tailnet."""
|
|
report = {
|
|
"timestamp": datetime.datetime.now().isoformat(),
|
|
"total_nodes": len(self.nodes),
|
|
"online_nodes": sum(1 for n in self.nodes if n.online),
|
|
"offline_nodes": sum(1 for n in self.nodes if not n.online),
|
|
"expiring_keys": [],
|
|
"untagged_nodes": [],
|
|
"exit_nodes": [],
|
|
"issues": [],
|
|
}
|
|
|
|
for node in self.nodes:
|
|
# Check for expiring keys
|
|
if node.key_expiry:
|
|
try:
|
|
expiry = datetime.datetime.fromisoformat(node.key_expiry.replace("Z", "+00:00"))
|
|
days_until = (expiry - datetime.datetime.now(datetime.timezone.utc)).days
|
|
if days_until < 30:
|
|
report["expiring_keys"].append({
|
|
"hostname": node.hostname,
|
|
"expires_in_days": days_until,
|
|
})
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Check for untagged nodes
|
|
if not node.tags:
|
|
report["untagged_nodes"].append(node.hostname)
|
|
|
|
# Track exit nodes
|
|
if node.exit_node:
|
|
report["exit_nodes"].append(node.hostname)
|
|
|
|
# Generate issues
|
|
if report["offline_nodes"] > 0:
|
|
report["issues"].append(
|
|
f"{report['offline_nodes']} nodes offline"
|
|
)
|
|
if report["expiring_keys"]:
|
|
report["issues"].append(
|
|
f"{len(report['expiring_keys'])} nodes with keys expiring within 30 days"
|
|
)
|
|
if report["untagged_nodes"]:
|
|
report["issues"].append(
|
|
f"{len(report['untagged_nodes'])} nodes without tags (ungoverned by ACLs)"
|
|
)
|
|
|
|
return report
|
|
|
|
def generate_compliance_report(self) -> dict:
|
|
"""Generate zero trust compliance report for the tailnet."""
|
|
report = {
|
|
"report_date": datetime.datetime.now().isoformat(),
|
|
"zero_trust_checks": {
|
|
"encryption": {
|
|
"status": "PASS",
|
|
"detail": "All connections use WireGuard end-to-end encryption",
|
|
},
|
|
"identity_based_access": {
|
|
"status": "PASS" if all(n.tags for n in self.nodes) else "FAIL",
|
|
"detail": "All nodes should have identity tags for ACL enforcement",
|
|
},
|
|
"least_privilege": {
|
|
"status": "REVIEW",
|
|
"detail": "ACL policy review required - validate minimum necessary access",
|
|
},
|
|
"continuous_verification": {
|
|
"status": "PASS" if not any(
|
|
n.key_expiry == "" for n in self.nodes
|
|
) else "WARNING",
|
|
"detail": "Key expiry should be enabled for all non-server nodes",
|
|
},
|
|
"device_trust": {
|
|
"status": "REVIEW",
|
|
"detail": "Verify device authorization and Network Lock status",
|
|
},
|
|
},
|
|
}
|
|
return report
|
|
|
|
|
|
def generate_example_policy():
|
|
"""Generate an example zero trust ACL policy for Tailscale."""
|
|
gen = TailscaleACLGenerator()
|
|
|
|
# Define groups
|
|
gen.add_group("engineering", ["eng1@company.com", "eng2@company.com"])
|
|
gen.add_group("sre", ["sre1@company.com", "sre2@company.com"])
|
|
gen.add_group("security", ["sec1@company.com"])
|
|
gen.add_group("management", ["mgr1@company.com"])
|
|
|
|
# Define tags
|
|
gen.add_tag("production", ["group:sre"])
|
|
gen.add_tag("staging", ["group:engineering", "group:sre"])
|
|
gen.add_tag("development", ["group:engineering"])
|
|
gen.add_tag("database", ["group:sre"])
|
|
gen.add_tag("monitoring", ["group:sre", "group:security"])
|
|
gen.add_tag("ci-runner", ["group:sre"])
|
|
|
|
# ACL rules - zero trust, least privilege
|
|
gen.add_acl_rule(
|
|
src=["group:engineering"],
|
|
dst=["tag:development:*", "tag:staging:443,8080"]
|
|
)
|
|
gen.add_acl_rule(
|
|
src=["group:sre"],
|
|
dst=["tag:production:22,443,8080", "tag:staging:*", "tag:database:5432,3306"]
|
|
)
|
|
gen.add_acl_rule(
|
|
src=["group:security"],
|
|
dst=["tag:monitoring:443,9090", "tag:production:443"]
|
|
)
|
|
gen.add_acl_rule(
|
|
src=["tag:ci-runner"],
|
|
dst=["tag:staging:443,8080", "tag:production:443"]
|
|
)
|
|
|
|
# SSH rules with re-authentication
|
|
gen.add_ssh_rule(
|
|
src=["group:sre"],
|
|
dst=["tag:production"],
|
|
users=["admin"],
|
|
action="check" # Requires re-auth, records session
|
|
)
|
|
gen.add_ssh_rule(
|
|
src=["group:engineering"],
|
|
dst=["tag:development"],
|
|
users=["autogroup:nonroot"],
|
|
action="accept"
|
|
)
|
|
|
|
# Auto-approvers for subnet routes
|
|
gen.add_auto_approver_route("10.0.0.0/16", ["group:sre"])
|
|
gen.add_auto_approver_exit_node(["group:sre"])
|
|
|
|
# Validate
|
|
issues = gen.validate_policy()
|
|
if issues:
|
|
print("Policy Validation Issues:")
|
|
for issue in issues:
|
|
print(f" - {issue}")
|
|
else:
|
|
print("Policy validation: PASSED")
|
|
|
|
# Export
|
|
policy = gen.export_policy("tailscale_acl_policy.json")
|
|
print(f"\nGenerated ACL policy with:")
|
|
print(f" Groups: {len(gen.groups)}")
|
|
print(f" Tags: {len(gen.tag_owners)}")
|
|
print(f" ACL Rules: {len(gen.acls)}")
|
|
print(f" SSH Rules: {len(gen.ssh_rules)}")
|
|
print(f"\nPolicy saved to: tailscale_acl_policy.json")
|
|
print(f"\nPolicy preview:")
|
|
print(json.dumps(policy, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
generate_example_policy()
|