Initial commit
This commit is contained in:
		
							
								
								
									
										6
									
								
								.flake8
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.flake8
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| [flake8] | ||||
| inline-quotes = " | ||||
| multiline-quotes = """ | ||||
| indent-size = 4 | ||||
| max-line-length = 160 | ||||
| ignore = Q003 E128 E704 E731 | ||||
							
								
								
									
										9
									
								
								.gitlint
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.gitlint
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										16
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										9
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| FROM python:3-alpine | ||||
| ENV PYTHONUNBUFFERED=1 | ||||
| LABEL name="pinecrypt/firewall" \ | ||||
|       version="rc" \ | ||||
|       maintainer="Pinecrypt Labs <info@pinecrypt.com>" | ||||
| RUN apk add iptables ip6tables ipset | ||||
| RUN pip install motor | ||||
| ADD firewall.py /firewall.py | ||||
| CMD /firewall.py | ||||
							
								
								
									
										147
									
								
								firewall.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										147
									
								
								firewall.py
									
									
									
									
									
										Executable file
									
								
							| @@ -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()) | ||||
		Reference in New Issue
	
	Block a user