commit e9b9de2558f81bd59154023780a5e3bdd20bd498 Author: Lauri Võsandi Date: Tue Jun 15 23:42:50 2021 +0300 Initial commit diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..ff6c948 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +inline-quotes = " +multiline-quotes = """ +indent-size = 4 +max-line-length = 160 +ignore = Q003 E128 E704 E731 diff --git a/.gitlint b/.gitlint new file mode 100644 index 0000000..e3f0c82 --- /dev/null +++ b/.gitlint @@ -0,0 +1,9 @@ +[general] +ignore=body-is-missing,T3 +ignore-stdin=true + +[title-match-regex] +regex=[A-Z] + +[author-valid-email] +regex=[^@]+@pinecrypt.com diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..23d9eb4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: +- repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: [flake8-typing-imports==1.10.0,flake8-quotes==3.2.0] + +- repo: https://github.com/jorisroovers/gitlint + rev: v0.15.1 + hooks: + - id: gitlint + +- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs + rev: v1.1.1 + hooks: + - id: dockerfile_lint diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..17c7dc9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3-alpine +ENV PYTHONUNBUFFERED=1 +LABEL name="pinecrypt/firewall" \ + version="rc" \ + maintainer="Pinecrypt Labs " +RUN apk add iptables ip6tables ipset +RUN pip install motor +ADD firewall.py /firewall.py +CMD /firewall.py diff --git a/firewall.py b/firewall.py new file mode 100755 index 0000000..2cc12a2 --- /dev/null +++ b/firewall.py @@ -0,0 +1,147 @@ +#!/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())