host_firewall: ban firehol level2, bruteforceblocker, vxvault and cybercrime; greensnow is part of level2
This commit is contained in:
@@ -93,17 +93,20 @@ firewall:
|
|||||||
# Lets the kernel log the nft log statements of container network
|
# Lets the kernel log the nft log statements of container network
|
||||||
# namespaces (LXC guests) into the host kernel log.
|
# namespaces (LXC guests) into the host kernel log.
|
||||||
all_netns: false
|
all_netns: false
|
||||||
# Shared by the Spamhaus (fw-spamhaus-in: , fw-spamhaus-out: ), FireHOL (fw-firehol-in: , fw-firehol-out: ), scanner (fw-scanners-in: ) and
|
# Shared by the Spamhaus (fw-spamhaus-in: , fw-spamhaus-out: ), FireHOL (fw-firehol-in: , fw-firehol-out: ), bruteforceblocker (fw-bruteforceblocker-in: ) and scanner (fw-scanners-in: ) chains,
|
||||||
# GreenSnow (fw-greensnow-in: ) chains, one limit each.
|
# one limit each.
|
||||||
spamhaus_rate: "6/minute burst 10 packets"
|
spamhaus_rate: "6/minute burst 10 packets"
|
||||||
# Spamhaus DROP, DROPv6 and ASN-DROP: traffic from the listed networks drops
|
# Spamhaus DROP, DROPv6 and ASN-DROP: traffic from the listed networks drops
|
||||||
# (input, forward), traffic to them is rejected (output, forward). The sets
|
# (input, forward), traffic to them is rejected (output, forward). The sets
|
||||||
# are refreshed daily by nft-host-spamhaus.
|
# are refreshed daily by nft-host-spamhaus.
|
||||||
spamhaus: true
|
spamhaus: true
|
||||||
# FireHOL level1 (IPv4): treated like the Spamhaus lists, with the prefixes
|
# FireHOL lists (IPv4 only, FireHOL has no IPv6 lists), from github.com/firehol/blocklist-ipsets:
|
||||||
# fw-firehol-in: and fw-firehol-out: and the same log limit. Entries in
|
# - level1 and level2 (level2 includes GreenSnow) and the malware set (vxvault and cybercrime): treated like the
|
||||||
# private or reserved space and ones holding an address of the host are left
|
# Spamhaus lists, with the prefixes fw-firehol-in: and fw-firehol-out: and the same log limit.
|
||||||
# out. Refreshed daily by nft-host-firehol.
|
# - bruteforceblocker: inbound new connections from the listed hosts drop, outbound traffic is not filtered. Log
|
||||||
|
# prefix fw-bruteforceblocker-in: with its own limit, which CrowdSec whitelists.
|
||||||
|
# Entries in private or reserved space and ones holding an address of the host are left out. All sets are refreshed
|
||||||
|
# daily by nft-host-firehol.
|
||||||
firehol: true
|
firehol: true
|
||||||
# Internet-wide scanners (Censys, Shodan, ONYPHE, Shadowserver, ...), IPv4
|
# Internet-wide scanners (Censys, Shodan, ONYPHE, Shadowserver, ...), IPv4
|
||||||
# and IPv6: the cidr files of github.com/OpenFilters/internet-scanners
|
# and IPv6: the cidr files of github.com/OpenFilters/internet-scanners
|
||||||
@@ -115,13 +118,6 @@ firewall:
|
|||||||
# of any host, and ones holding an address of the host are left out
|
# of any host, and ones holding an address of the host are left out
|
||||||
# (/etc/nft-host-scanners.allowlist). Refreshed daily by nft-host-scanners.
|
# (/etc/nft-host-scanners.allowlist). Refreshed daily by nft-host-scanners.
|
||||||
scanners: true
|
scanners: true
|
||||||
# GreenSnow (IPv4 single addresses, compromised and dynamic hosts from the
|
|
||||||
# FireHOL blocklist-ipsets): inbound new connections from the listed hosts
|
|
||||||
# drop, outbound traffic is not filtered. Log prefix fw-greensnow-in: with its
|
|
||||||
# own limit, which CrowdSec whitelists. Entries in private or reserved space
|
|
||||||
# and ones holding an address of the host are left out. Refreshed daily by
|
|
||||||
# nft-host-greensnow.
|
|
||||||
greensnow: true
|
|
||||||
# Reject outbound TCP 25; the mail group turns it off.
|
# Reject outbound TCP 25; the mail group turns it off.
|
||||||
block_smtp_out: true
|
block_smtp_out: true
|
||||||
# EU-27 plus CH, NO, GB, IS, LI (ipdeny country codes). Empty leaves the geo
|
# EU-27 plus CH, NO, GB, IS, LI (ipdeny country codes). Empty leaves the geo
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# Managed by Ansible (role host_firewall), do not edit.
|
# Managed by Ansible (role host_firewall), do not edit.
|
||||||
"""Rebuilds the FireHOL level1 set firehol_v4 of table inet host.
|
"""Rebuilds the FireHOL sets of table inet host (IPv4 only, FireHOL has no IPv6 lists).
|
||||||
|
|
||||||
A failed download or sanity check exits non-zero before anything changes, so
|
Every set is built from one or more lists of the FireHOL blocklist-ipsets
|
||||||
the previous set stays in place.
|
(single addresses or CIDR networks, comment lines start with #). All sets go
|
||||||
|
into one file, so one reload replaces them together. A failed download or
|
||||||
|
sanity check of any list exits non-zero before anything changes, so the
|
||||||
|
previous sets stay in place.
|
||||||
"""
|
"""
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
@@ -16,11 +19,18 @@ import time
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
PERSISTED = "/etc/nftables.d/host-firehol.nft"
|
PERSISTED = "/etc/nftables.d/host-firehol.nft"
|
||||||
URL = "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset"
|
BASE_URL = "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/"
|
||||||
USER_AGENT = "ruekov-host-firewall (nft-host-firehol)"
|
USER_AGENT = "ruekov-host-firewall (nft-host-firehol)"
|
||||||
|
|
||||||
# Lower bound that catches an empty or truncated answer.
|
# set name -> (lists merged into it, lower bound for their entries together that
|
||||||
MIN_ENTRIES = 1000
|
# catches an empty or truncated answer; the lists hold about 4700 (level1),
|
||||||
|
# 18700 (level2), 500 (bruteforceblocker) and 70 + 500 (vxvault, cybercrime)).
|
||||||
|
SETS = {
|
||||||
|
"firehol_v4": (["firehol_level1.netset"], 1000),
|
||||||
|
"firehol_level2_v4": (["firehol_level2.netset"], 5000),
|
||||||
|
"bruteforceblocker_v4": (["bruteforceblocker.ipset"], 100),
|
||||||
|
"malware_v4": (["vxvault.ipset", "cybercrime.ipset"], 200),
|
||||||
|
}
|
||||||
# A prefix this wide is a parse error, not a bad network.
|
# A prefix this wide is a parse error, not a bad network.
|
||||||
MIN_PREFIXLEN = 8
|
MIN_PREFIXLEN = 8
|
||||||
# Private, CGNAT (NetBird), loopback, link-local and multicast/reserved space:
|
# Private, CGNAT (NetBird), loopback, link-local and multicast/reserved space:
|
||||||
@@ -36,17 +46,18 @@ def fail(message):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def fetch():
|
def fetch(name):
|
||||||
|
url = BASE_URL + name
|
||||||
error = None
|
error = None
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
try:
|
try:
|
||||||
request = urllib.request.Request(URL, headers={"User-Agent": USER_AGENT})
|
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||||
with urllib.request.urlopen(request, timeout=120) as response:
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
return response.read().decode("utf-8")
|
return response.read().decode("utf-8")
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
error = e
|
error = e
|
||||||
time.sleep(5 * (attempt + 1))
|
time.sleep(5 * (attempt + 1))
|
||||||
fail(f"download of {URL} failed: {error}")
|
fail(f"download of {url} failed: {error}")
|
||||||
|
|
||||||
|
|
||||||
def own_addresses():
|
def own_addresses():
|
||||||
@@ -57,20 +68,20 @@ def own_addresses():
|
|||||||
for a in link.get("addr_info", []) if a.get("family") == "inet"]
|
for a in link.get("addr_info", []) if a.get("family") == "inet"]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def build_set(lists, minimum, own):
|
||||||
networks = []
|
networks = []
|
||||||
for line in fetch().splitlines():
|
for name in lists:
|
||||||
|
for line in fetch(name).splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
networks.append(ipaddress.IPv4Network(line))
|
networks.append(ipaddress.IPv4Network(line))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
fail(f"unparsable line: {line!r}")
|
fail(f"unparsable line in {name}: {line!r}")
|
||||||
if len(networks) < MIN_ENTRIES:
|
if len(networks) < minimum:
|
||||||
fail(f"only {len(networks)} entries")
|
fail(f"only {len(networks)} entries in {', '.join(lists)}")
|
||||||
|
|
||||||
own = own_addresses()
|
|
||||||
kept, excluded, own_skipped = [], 0, 0
|
kept, excluded, own_skipped = [], 0, 0
|
||||||
for network in networks:
|
for network in networks:
|
||||||
if any(network.overlaps(e) for e in EXCLUDED):
|
if any(network.overlaps(e) for e in EXCLUDED):
|
||||||
@@ -82,21 +93,28 @@ def main():
|
|||||||
fail(f"suspiciously wide prefix {network}")
|
fail(f"suspiciously wide prefix {network}")
|
||||||
kept.append(network)
|
kept.append(network)
|
||||||
merged = list(ipaddress.collapse_addresses(kept))
|
merged = list(ipaddress.collapse_addresses(kept))
|
||||||
print(f"firehol_v4: {len(networks)} entries, {excluded} in private or reserved space and "
|
return networks, excluded, own_skipped, merged
|
||||||
f"{own_skipped} holding an own address skipped -> {len(merged)} merged")
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
own = own_addresses()
|
||||||
|
definitions = []
|
||||||
|
for set_name, (lists, minimum) in SETS.items():
|
||||||
|
networks, excluded, own_skipped, merged = build_set(lists, minimum, own)
|
||||||
|
print(f"{set_name}: {len(networks)} entries, {excluded} in private or reserved space and "
|
||||||
|
f"{own_skipped} holding an own address skipped -> {len(merged)} merged")
|
||||||
elements = ",\n\t\t".join(str(n) for n in merged) + ","
|
elements = ",\n\t\t".join(str(n) for n in merged) + ","
|
||||||
|
definitions.append(f"\tset {set_name} {{ type ipv4_addr; flags interval; auto-merge; elements = {{\n"
|
||||||
|
f"\t\t{elements}\n\t}} }}\n")
|
||||||
persisted = ("# Generated by nft-host-firehol, do not edit.\n"
|
persisted = ("# Generated by nft-host-firehol, do not edit.\n"
|
||||||
"table inet host {\n"
|
"table inet host {\n" + "".join(definitions) + "}\n")
|
||||||
"\tset firehol_v4 { type ipv4_addr; flags interval; auto-merge; elements = {\n"
|
|
||||||
f"\t\t{elements}\n\t}} }}\n}}\n")
|
|
||||||
|
|
||||||
# Checked under another table name: a dry run against the live table
|
# Checked under another table name: a dry run against the live table
|
||||||
# name changes the live sets for its duration and unlisted addresses match.
|
# name changes the live sets for its duration and unlisted addresses match.
|
||||||
check_rules = persisted.replace("table inet host {", "table inet host_firehol_check {")
|
check_rules = persisted.replace("table inet host {", "table inet host_firehol_check {")
|
||||||
checked = subprocess.run(["nft", "-c", "-f", "-"], input=check_rules, capture_output=True, text=True)
|
checked = subprocess.run(["nft", "-c", "-f", "-"], input=check_rules, capture_output=True, text=True)
|
||||||
if checked.returncode != 0:
|
if checked.returncode != 0:
|
||||||
fail(f"nft rejects the set: {checked.stderr.strip()}")
|
fail(f"nft rejects the sets: {checked.stderr.strip()}")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
previous = None
|
previous = None
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Refresh the FireHOL set of the host firewall
|
Description=Refresh the FireHOL sets of the host firewall
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Daily refresh of the host firewall FireHOL set
|
Description=Daily refresh of the host firewall FireHOL sets
|
||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
OnCalendar=daily
|
OnCalendar=daily
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# Managed by Ansible (role host_firewall), do not edit.
|
|
||||||
"""Rebuilds the GreenSnow set greensnow_v4 of table inet host.
|
|
||||||
|
|
||||||
Source: the GreenSnow list of the FireHOL blocklist-ipsets (single IPv4
|
|
||||||
addresses, comment lines start with #). A failed download or sanity check
|
|
||||||
exits non-zero before anything changes, so the previous set stays in place.
|
|
||||||
"""
|
|
||||||
import ipaddress
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import time
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
PERSISTED = "/etc/nftables.d/host-greensnow.nft"
|
|
||||||
URL = "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/greensnow.ipset"
|
|
||||||
USER_AGENT = "ruekov-host-firewall (nft-host-greensnow)"
|
|
||||||
|
|
||||||
# Lower bound that catches an empty or truncated answer (the list holds about
|
|
||||||
# 4700 entries).
|
|
||||||
MIN_ENTRIES = 500
|
|
||||||
# A prefix this wide is a parse error, not a bad address.
|
|
||||||
MIN_PREFIXLEN = 8
|
|
||||||
# Private, CGNAT (NetBird), loopback, link-local and multicast/reserved space:
|
|
||||||
# an entry overlapping one of these never lands in the set, so the own
|
|
||||||
# infrastructure cannot block itself.
|
|
||||||
EXCLUDED = [ipaddress.ip_network(n) for n in (
|
|
||||||
"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8",
|
|
||||||
"169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16", "224.0.0.0/3")]
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"nft-host-greensnow: {message}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch():
|
|
||||||
error = None
|
|
||||||
for attempt in range(3):
|
|
||||||
try:
|
|
||||||
request = urllib.request.Request(URL, headers={"User-Agent": USER_AGENT})
|
|
||||||
with urllib.request.urlopen(request, timeout=120) as response:
|
|
||||||
return response.read().decode("utf-8")
|
|
||||||
except OSError as e:
|
|
||||||
error = e
|
|
||||||
time.sleep(5 * (attempt + 1))
|
|
||||||
fail(f"download of {URL} failed: {error}")
|
|
||||||
|
|
||||||
|
|
||||||
def own_addresses():
|
|
||||||
result = subprocess.run(["ip", "-j", "-4", "address", "show"], capture_output=True, text=True)
|
|
||||||
if result.returncode != 0:
|
|
||||||
fail(f"ip address show failed: {result.stderr.strip()}")
|
|
||||||
return [ipaddress.ip_address(a["local"]) for link in json.loads(result.stdout)
|
|
||||||
for a in link.get("addr_info", []) if a.get("family") == "inet"]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
networks = []
|
|
||||||
for line in fetch().splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
networks.append(ipaddress.IPv4Network(line))
|
|
||||||
except ValueError:
|
|
||||||
fail(f"unparsable line: {line!r}")
|
|
||||||
if len(networks) < MIN_ENTRIES:
|
|
||||||
fail(f"only {len(networks)} entries")
|
|
||||||
|
|
||||||
own = own_addresses()
|
|
||||||
kept, excluded, own_skipped = [], 0, 0
|
|
||||||
for network in networks:
|
|
||||||
if network.prefixlen < MIN_PREFIXLEN:
|
|
||||||
fail(f"suspiciously wide prefix {network}")
|
|
||||||
if any(network.overlaps(e) for e in EXCLUDED):
|
|
||||||
excluded += 1
|
|
||||||
elif any(address in network for address in own):
|
|
||||||
own_skipped += 1
|
|
||||||
else:
|
|
||||||
kept.append(network)
|
|
||||||
merged = list(ipaddress.collapse_addresses(kept))
|
|
||||||
print(f"greensnow_v4: {len(networks)} entries, {excluded} in private or reserved space and "
|
|
||||||
f"{own_skipped} holding an own address skipped -> {len(merged)} merged")
|
|
||||||
|
|
||||||
elements = ",\n\t\t".join(str(n) for n in merged) + ","
|
|
||||||
persisted = ("# Generated by nft-host-greensnow, do not edit.\n"
|
|
||||||
"table inet host {\n"
|
|
||||||
"\tset greensnow_v4 { type ipv4_addr; flags interval; auto-merge; elements = {\n"
|
|
||||||
f"\t\t{elements}\n\t}} }}\n}}\n")
|
|
||||||
|
|
||||||
# Checked under another table name: a dry run against the live table
|
|
||||||
# name changes the live sets for its duration and unlisted addresses match.
|
|
||||||
check_rules = persisted.replace("table inet host {", "table inet host_greensnow_check {")
|
|
||||||
checked = subprocess.run(["nft", "-c", "-f", "-"], input=check_rules, capture_output=True, text=True)
|
|
||||||
if checked.returncode != 0:
|
|
||||||
fail(f"nft rejects the set: {checked.stderr.strip()}")
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
previous = None
|
|
||||||
if os.path.exists(PERSISTED):
|
|
||||||
previous = os.path.join(tmp, "previous.nft")
|
|
||||||
shutil.copyfile(PERSISTED, previous)
|
|
||||||
with open(PERSISTED + ".new", "w", encoding="utf-8") as f:
|
|
||||||
f.write(persisted)
|
|
||||||
os.chmod(PERSISTED + ".new", 0o644)
|
|
||||||
os.replace(PERSISTED + ".new", PERSISTED)
|
|
||||||
|
|
||||||
# A reload replaces the whole table, sets included, in one transaction.
|
|
||||||
active = subprocess.run(["systemctl", "-q", "is-active", "nft-host.service"]).returncode == 0
|
|
||||||
if active:
|
|
||||||
reloaded = subprocess.run(["systemctl", "reload", "nft-host.service"], capture_output=True, text=True)
|
|
||||||
if reloaded.returncode != 0:
|
|
||||||
if previous:
|
|
||||||
shutil.copyfile(previous, PERSISTED)
|
|
||||||
fail(f"reloading the rule set failed, kept the previous set: {reloaded.stderr.strip()}")
|
|
||||||
print("reloaded the rule set" if active else "persisted only")
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except SystemExit:
|
|
||||||
raise
|
|
||||||
except Exception as e: # keep the previous set on any surprise
|
|
||||||
fail(f"{type(e).__name__}: {e}")
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Refresh the GreenSnow set of the host firewall
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/sbin/nft-host-greensnow
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Daily refresh of the host firewall GreenSnow set
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=daily
|
|
||||||
RandomizedDelaySec=6h
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
src: "{{ item }}"
|
src: "{{ item }}"
|
||||||
dest: "/etc/systemd/system/{{ item }}"
|
dest: "/etc/systemd/system/{{ item }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
loop: [nft-host.service, nft-host-geo.service, nft-host-geo.timer, nft-host-spamhaus.service, nft-host-spamhaus.timer, nft-host-firehol.service, nft-host-firehol.timer, nft-host-scanners.service, nft-host-scanners.timer, nft-host-greensnow.service, nft-host-greensnow.timer]
|
loop: [nft-host.service, nft-host-geo.service, nft-host-geo.timer, nft-host-spamhaus.service, nft-host-spamhaus.timer, nft-host-firehol.service, nft-host-firehol.timer, nft-host-scanners.service, nft-host-scanners.timer]
|
||||||
register: firewall_units
|
register: firewall_units
|
||||||
|
|
||||||
- name: Install the rollback script
|
- name: Install the rollback script
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
path: /etc/nftables.d/host-spamhaus.nft
|
path: /etc/nftables.d/host-spamhaus.nft
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: FireHOL set
|
- name: FireHOL sets
|
||||||
when: firewall.firehol
|
when: firewall.firehol
|
||||||
block:
|
block:
|
||||||
- name: Install the FireHOL set updater
|
- name: Install the FireHOL set updater
|
||||||
@@ -183,25 +183,31 @@
|
|||||||
dest: /usr/local/sbin/nft-host-firehol
|
dest: /usr/local/sbin/nft-host-firehol
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
# The rule set includes this file, so a missing one would fail the load at boot.
|
# The rule set includes this file and uses all of its sets, so a missing file or set would fail the load at boot.
|
||||||
- name: Build the FireHOL set on first run
|
- name: Look for the FireHOL sets
|
||||||
ansible.builtin.command: /usr/local/sbin/nft-host-firehol
|
ansible.builtin.command: grep -cE 'set (firehol_v4|firehol_level2_v4|bruteforceblocker_v4|malware_v4) ' /etc/nftables.d/host-firehol.nft
|
||||||
args:
|
register: firewall_firehol_sets
|
||||||
creates: /etc/nftables.d/host-firehol.nft
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Read the FireHOL set file
|
- name: Build the FireHOL sets when missing
|
||||||
|
ansible.builtin.command: /usr/local/sbin/nft-host-firehol
|
||||||
|
when: firewall_firehol_sets.stdout != '4'
|
||||||
|
|
||||||
|
- name: Read the FireHOL sets file
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: /etc/nftables.d/host-firehol.nft
|
path: /etc/nftables.d/host-firehol.nft
|
||||||
register: firewall_firehol
|
register: firewall_firehol
|
||||||
|
|
||||||
- name: Require non-empty FireHOL set
|
- name: Require non-empty FireHOL sets
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that: firewall_firehol.stat.exists and firewall_firehol.stat.size > 0
|
that: firewall_firehol.stat.exists and firewall_firehol.stat.size > 0
|
||||||
fail_msg: /etc/nftables.d/host-firehol.nft is missing or empty, refusing to apply the firewall
|
fail_msg: /etc/nftables.d/host-firehol.nft is missing or empty, refusing to apply the firewall
|
||||||
# Check mode neither installs the updater nor builds the sets.
|
# Check mode neither installs the updater nor builds the sets.
|
||||||
when: not ansible_check_mode
|
when: not ansible_check_mode
|
||||||
|
|
||||||
- name: Enable the FireHOL set timer
|
- name: Enable the FireHOL sets timer
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
name: nft-host-firehol.timer
|
name: nft-host-firehol.timer
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -210,10 +216,10 @@
|
|||||||
# Check mode does not install the unit.
|
# Check mode does not install the unit.
|
||||||
ignore_errors: "{{ ansible_check_mode }}"
|
ignore_errors: "{{ ansible_check_mode }}"
|
||||||
|
|
||||||
- name: Stop the FireHOL set refresh
|
- name: Stop the FireHOL sets refresh
|
||||||
when: not firewall.firehol
|
when: not firewall.firehol
|
||||||
block:
|
block:
|
||||||
- name: Disable the FireHOL set timer
|
- name: Disable the FireHOL sets timer
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
name: nft-host-firehol.timer
|
name: nft-host-firehol.timer
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -222,7 +228,7 @@
|
|||||||
# Check mode does not install the unit.
|
# Check mode does not install the unit.
|
||||||
ignore_errors: "{{ ansible_check_mode }}"
|
ignore_errors: "{{ ansible_check_mode }}"
|
||||||
|
|
||||||
- name: Remove the FireHOL set file
|
- name: Remove the FireHOL sets file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /etc/nftables.d/host-firehol.nft
|
path: /etc/nftables.d/host-firehol.nft
|
||||||
state: absent
|
state: absent
|
||||||
@@ -287,59 +293,6 @@
|
|||||||
path: /etc/nftables.d/host-scanners.nft
|
path: /etc/nftables.d/host-scanners.nft
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: GreenSnow set
|
|
||||||
when: firewall.greensnow
|
|
||||||
block:
|
|
||||||
- name: Install the GreenSnow set updater
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: nft-host-greensnow
|
|
||||||
dest: /usr/local/sbin/nft-host-greensnow
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
# The rule set includes this file, so a missing one would fail the load at boot.
|
|
||||||
- name: Build the GreenSnow set on first run
|
|
||||||
ansible.builtin.command: /usr/local/sbin/nft-host-greensnow
|
|
||||||
args:
|
|
||||||
creates: /etc/nftables.d/host-greensnow.nft
|
|
||||||
|
|
||||||
- name: Read the GreenSnow set file
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: /etc/nftables.d/host-greensnow.nft
|
|
||||||
register: firewall_greensnow
|
|
||||||
|
|
||||||
- name: Require non-empty GreenSnow set
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that: firewall_greensnow.stat.exists and firewall_greensnow.stat.size > 0
|
|
||||||
fail_msg: /etc/nftables.d/host-greensnow.nft is missing or empty, refusing to apply the firewall
|
|
||||||
# Check mode neither installs the updater nor builds the set.
|
|
||||||
when: not ansible_check_mode
|
|
||||||
|
|
||||||
- name: Enable the GreenSnow set timer
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: nft-host-greensnow.timer
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: "{{ firewall_units is changed }}"
|
|
||||||
# Check mode does not install the unit.
|
|
||||||
ignore_errors: "{{ ansible_check_mode }}"
|
|
||||||
|
|
||||||
- name: Stop the GreenSnow set refresh
|
|
||||||
when: not firewall.greensnow
|
|
||||||
block:
|
|
||||||
- name: Disable the GreenSnow set timer
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: nft-host-greensnow.timer
|
|
||||||
enabled: false
|
|
||||||
state: stopped
|
|
||||||
daemon_reload: "{{ firewall_units is changed }}"
|
|
||||||
# Check mode does not install the unit.
|
|
||||||
ignore_errors: "{{ ansible_check_mode }}"
|
|
||||||
|
|
||||||
- name: Remove the GreenSnow set file
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /etc/nftables.d/host-greensnow.nft
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Render the rule set
|
- name: Render the rule set
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: host.nft.j2
|
src: host.nft.j2
|
||||||
@@ -414,3 +367,36 @@
|
|||||||
dest: /etc/nftables.d/host.nft.good
|
dest: /etc/nftables.d/host.nft.good
|
||||||
remote_src: true
|
remote_src: true
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
|
# GreenSnow is part of FireHOL level2. The rule file goes last: the last confirmed rule set (host.nft.good) and a
|
||||||
|
# reload by a refresh service still include it until the new rule set is confirmed. The set itself leaves the live
|
||||||
|
# table with the reload of the new rule set.
|
||||||
|
- name: Look for the GreenSnow timer
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /etc/systemd/system/nft-host-greensnow.timer
|
||||||
|
register: firewall_greensnow_timer
|
||||||
|
|
||||||
|
- name: Stop the GreenSnow refresh
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: nft-host-greensnow.timer
|
||||||
|
enabled: false
|
||||||
|
state: stopped
|
||||||
|
when: firewall_greensnow_timer.stat.exists
|
||||||
|
# Check mode does not stop the timer.
|
||||||
|
ignore_errors: "{{ ansible_check_mode }}"
|
||||||
|
|
||||||
|
- name: Remove the GreenSnow files
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: absent
|
||||||
|
loop:
|
||||||
|
- /etc/systemd/system/nft-host-greensnow.service
|
||||||
|
- /etc/systemd/system/nft-host-greensnow.timer
|
||||||
|
- /usr/local/sbin/nft-host-greensnow
|
||||||
|
- /etc/nftables.d/host-greensnow.nft
|
||||||
|
register: firewall_greensnow_files
|
||||||
|
|
||||||
|
- name: Reload systemd after removing the GreenSnow units
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
when: firewall_greensnow_files is changed
|
||||||
|
|||||||
@@ -86,9 +86,6 @@ include "/etc/nftables.d/host-firehol.nft"
|
|||||||
{% if firewall.scanners %}
|
{% if firewall.scanners %}
|
||||||
include "/etc/nftables.d/host-scanners.nft"
|
include "/etc/nftables.d/host-scanners.nft"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if firewall.greensnow %}
|
|
||||||
include "/etc/nftables.d/host-greensnow.nft"
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
table inet host {
|
table inet host {
|
||||||
{{ addr_set('admin_v4', 'ipv4_addr', firewall.admin.v4) }}
|
{{ addr_set('admin_v4', 'ipv4_addr', firewall.admin.v4) }}
|
||||||
@@ -173,6 +170,8 @@ table inet host {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if firewall.firehol %}
|
{% if firewall.firehol %}
|
||||||
ip saddr @firehol_v4 jump firehol_in
|
ip saddr @firehol_v4 jump firehol_in
|
||||||
|
ip saddr @firehol_level2_v4 jump firehol_in
|
||||||
|
ip saddr @malware_v4 jump firehol_in
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -193,10 +192,10 @@ table inet host {
|
|||||||
ip6 saddr @scanners_v6 jump scanners_in
|
ip6 saddr @scanners_v6 jump scanners_in
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if firewall.greensnow %}
|
{% if firewall.firehol %}
|
||||||
|
|
||||||
# After the established rule: only inbound connections of GreenSnow hosts drop.
|
# After the established rule: only inbound connections of bruteforceblocker hosts drop.
|
||||||
ip saddr @greensnow_v4 jump greensnow_in
|
ip saddr @bruteforceblocker_v4 jump bruteforceblocker_in
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
ct state invalid jump drop_log
|
ct state invalid jump drop_log
|
||||||
@@ -361,13 +360,13 @@ table inet host {
|
|||||||
drop
|
drop
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if firewall.greensnow %}
|
{% if firewall.firehol %}
|
||||||
|
|
||||||
# Own limit and prefix, so GreenSnow hits use up neither the fw-in-drop budget
|
# Own limit and prefix, so bruteforceblocker hits use up neither the fw-in-drop budget
|
||||||
# nor the one of the blocklists. CrowdSec whitelists the prefix: the listed
|
# nor the one of the blocklists. CrowdSec whitelists the prefix: the listed
|
||||||
# hosts are known and need no decision.
|
# hosts are known and need no decision.
|
||||||
chain greensnow_in {
|
chain bruteforceblocker_in {
|
||||||
limit rate {{ firewall.log.spamhaus_rate }} log prefix "fw-greensnow-in: "
|
limit rate {{ firewall.log.spamhaus_rate }} log prefix "fw-bruteforceblocker-in: "
|
||||||
drop
|
drop
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -385,6 +384,8 @@ table inet host {
|
|||||||
{% if firewall.firehol %}
|
{% if firewall.firehol %}
|
||||||
|
|
||||||
ip daddr @firehol_v4 jump firehol_out
|
ip daddr @firehol_v4 jump firehol_out
|
||||||
|
ip daddr @firehol_level2_v4 jump firehol_out
|
||||||
|
ip daddr @malware_v4 jump firehol_out
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if firewall.block_smtp_out %}
|
{% if firewall.block_smtp_out %}
|
||||||
|
|
||||||
@@ -448,7 +449,7 @@ table inet host {
|
|||||||
ip6 saddr @geo_v6 accept
|
ip6 saddr @geo_v6 accept
|
||||||
{% endif %}
|
{% endif %}
|
||||||
}
|
}
|
||||||
{% if forward_guard or blocklists or firewall.scanners or firewall.greensnow or rl_forward or firewall.block_smtp_out %}
|
{% if forward_guard or blocklists or firewall.scanners or rl_forward or firewall.block_smtp_out %}
|
||||||
|
|
||||||
chain forward {
|
chain forward {
|
||||||
type filter hook forward priority filter; policy accept;
|
type filter hook forward priority filter; policy accept;
|
||||||
@@ -471,15 +472,19 @@ table inet host {
|
|||||||
|
|
||||||
ip saddr @firehol_v4 jump firehol_in
|
ip saddr @firehol_v4 jump firehol_in
|
||||||
ip daddr @firehol_v4 jump firehol_out
|
ip daddr @firehol_v4 jump firehol_out
|
||||||
|
ip saddr @firehol_level2_v4 jump firehol_in
|
||||||
|
ip daddr @firehol_level2_v4 jump firehol_out
|
||||||
|
ip saddr @malware_v4 jump firehol_in
|
||||||
|
ip daddr @malware_v4 jump firehol_out
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if firewall.scanners %}
|
{% if firewall.scanners %}
|
||||||
|
|
||||||
ct state new ip saddr @scanners_v4 jump scanners_in
|
ct state new ip saddr @scanners_v4 jump scanners_in
|
||||||
ct state new ip6 saddr @scanners_v6 jump scanners_in
|
ct state new ip6 saddr @scanners_v6 jump scanners_in
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if firewall.greensnow %}
|
{% if firewall.firehol %}
|
||||||
|
|
||||||
ct state new ip saddr @greensnow_v4 jump greensnow_in
|
ct state new ip saddr @bruteforceblocker_v4 jump bruteforceblocker_in
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for guard in forward_guard %}
|
{% for guard in forward_guard %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user