Initial commit

This commit is contained in:
Lauri Võsandi 2021-06-15 23:42:50 +03:00
commit e9b9de2558
5 changed files with 187 additions and 0 deletions

6
.flake8 Normal file
View 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
View 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
View 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
View 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
View 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())