#!/usr/bin/env python import asyncio import os import socket import sys from motor.motor_asyncio import AsyncIOMotorClient FQDN = socket.getfqdn() DEBUG = os.getenv("DEBUG") DISABLE_MASQUERADE = os.getenv("DISABLE_MASQUERADE") REPLICAS = [j for j in os.getenv("REPLICAS", "").split(",") if j] MONGO_URI = os.getenv("MONGO_URI") def generate_firewall_rules(disabled=False): default_policy = "REJECT" if DEBUG else "DROP" yield "*filter" yield ":INBOUND_BLOCKED - [0:0]" yield "-A INBOUND_BLOCKED -j %s -m comment --comment \"Default policy\"" % default_policy yield ":OUTBOUND_CLIENT - [0:0]" yield "-A OUTBOUND_CLIENT -m set ! --match-set ipset4-client-ingress dst -j SET --add-set ipset4-client-ingress dst" yield "-A OUTBOUND_CLIENT -j ACCEPT" yield ":INBOUND_CLIENT - [0:0]" yield "-A INBOUND_CLIENT -m set ! --match-set ipset4-client-ingress src -j SET --add-set ipset4-client-ingress src" yield "-A INBOUND_CLIENT -j ACCEPT" yield ":INPUT DROP [0:0]" yield "-A INPUT -i lo -j ACCEPT -m comment --comment \"Allow loopback\"" yield "-A INPUT -p icmp -j ACCEPT -m comment --comment \"Allow ping\"" yield "-A INPUT -p esp -j ACCEPT -m comment --comment \"Allow ESP traffic\"" yield "-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT -m comment --comment \"Allow returning packets\"" yield "-A INPUT -p tcp --dport 22 -j ACCEPT -m comment --comment \"Allow SSH\"" yield "-A INPUT -p udp --dport 53 -j ACCEPT -m comment --comment \"Allow GoreDNS over UDP\"" yield "-A INPUT -p tcp --dport 53 -j ACCEPT -m comment --comment \"Allow GoreDNS over TCP\"" yield "-A INPUT -p tcp --dport 80 -j ACCEPT -m comment --comment \"Allow insecure HTTP\"" yield "-A INPUT -p tcp --dport 8443 -j ACCEPT -m comment --comment \"Allow mutually authenticated HTTPS\"" if disabled: # 443 redirect handled in PREROUTING yield "-A INPUT -p udp --dport 1194 -j DROP -m comment --comment \"Drop OpenVPN UDP\"" yield "-A INPUT -p udp --dport 500 -j DROP -m comment --comment \"Drop IPsec IKE\"" yield "-A INPUT -p udp --dport 4500 -j DROP -m comment --comment \"Drop IPsec NAT traversal\"" else: yield "-A INPUT -p tcp --dport 443 -j ACCEPT -m comment --comment \"Allow HTTPS / OpenVPN TCP\"" yield "-A INPUT -p udp --dport 1194 -j ACCEPT -m comment --comment \"Allow OpenVPN UDP\"" yield "-A INPUT -p udp --dport 500 -j ACCEPT -m comment --comment \"Allow IPsec IKE\"" yield "-A INPUT -p udp --dport 4500 -j ACCEPT -m comment --comment \"Allow IPsec NAT traversal\"" if REPLICAS: yield "-A INPUT -p tcp --dport 27017 -j ACCEPT -m set --match-set ipset4-mongo-replicas src -m comment --comment \"Allow MongoDB internode\"" yield "-A INPUT -j INBOUND_BLOCKED" yield ":FORWARD DROP [0:0]" yield "-A FORWARD -i tun0 -j INBOUND_CLIENT -m comment --comment \"Inbound traffic from OpenVPN UDP clients\"" yield "-A FORWARD -i tun1 -j INBOUND_CLIENT -m comment --comment \"Inbound traffic from OpenVPN TCP clients\"" yield "-A FORWARD -m policy --dir in --pol ipsec -j INBOUND_CLIENT -m comment --comment \"Inbound traffic from IPSec clients\"" yield "-A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j OUTBOUND_CLIENT -m comment --comment \"Outbound traffic to clients\"" yield "-A FORWARD -j %s -m comment --comment \"Default policy\"" % default_policy yield ":OUTPUT DROP [0:0]" yield "-A OUTPUT -j ACCEPT" yield "COMMIT" yield "*nat" yield ":PREROUTING ACCEPT [0:0]" if disabled: # Bypass OpenVPN when replica is disabled yield "-A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 1443" yield ":INPUT ACCEPT [0:0]" yield ":OUTPUT ACCEPT [0:0]" yield ":POSTROUTING ACCEPT [0:0]" if not DISABLE_MASQUERADE: yield "-A POSTROUTING -j MASQUERADE" yield "COMMIT" def apply_firewall_rules(**kwargs): with open("/tmp/rules4", "w") as fh: for line in generate_firewall_rules(**kwargs): fh.write(line) fh.write("\n") os.system("iptables-restore < /tmp/rules4") os.system("sed -e 's/ipset4/ipset6/g' -e 's/p icmp/p ipv6-icmp/g' /tmp/rules4 > /tmp/rules6") os.system("ip6tables-restore < /tmp/rules6") os.system("sysctl -w net.ipv6.conf.all.forwarding=1") os.system("sysctl -w net.ipv6.conf.default.forwarding=1") os.system("sysctl -w net.ipv4.ip_forward=1") async def update_firewall_rules(): print("Setting up firewall rules") if REPLICAS: # TODO: atomic update with `ipset restore` for replica in REPLICAS: for fam, _, _, _, addrs in socket.getaddrinfo(replica, None): if fam == 10: os.system("ipset add ipset6-mongo-replicas %s" % addrs[0]) elif fam == 2: os.system("ipset add ipset4-mongo-replicas %s" % addrs[0]) os.system("ipset create -exist -quiet ipset4-client-ingress hash:ip timeout 3600 counters") os.system("ipset create -exist -quiet ipset6-client-ingress hash:ip family inet6 timeout 3600 counters") os.system("ipset create -exist -quiet ipset4-client-egress hash:ip timeout 3600 counters") os.system("ipset create -exist -quiet ipset6-client-egress hash:ip family inet6 timeout 3600 counters") os.system("ipset create -exist -quiet ipset4-mongo-replicas hash:ip") os.system("ipset create -exist -quiet ipset6-mongo-replicas hash:ip family inet6") db = AsyncIOMotorClient(MONGO_URI).get_default_database() q = { "common_name": FQDN, "status": "signed" } doc = await db.certidude_certificates.find_one(q) if not doc: print("Unable to lookup signed certificate for %s" % FQDN) sys.exit(1) apply_firewall_rules(disabled=doc["disabled"]) flt = [{ "$match": { "fullDocument.common_name": FQDN, "fullDocument.status": "signed", "$and": [{ "updateDescription.updatedFields.disabled": {"$exists": True}, "operationType": "update" }] } }] print("Waiting for updates...") async with db.certidude_certificates.watch(flt, full_document="updateLookup") as stream: async for event in stream: apply_firewall_rules( disabled=event["updateDescription"]["updatedFields"]["disabled"]) print("Starting main loop") loop = asyncio.get_event_loop() loop.run_until_complete(update_firewall_rules())