diff --git a/bin/fw-apply b/bin/fw-apply deleted file mode 100755 index 964fefc..0000000 --- a/bin/fw-apply +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr//bin/env bash -set -euo pipefail - -######################## -# Args -######################## -DO_APPLY=true -if [[ "${1:-}" == "--check" ]]; then - DO_APPLY=false -elif [[ "${1:-}" != "" ]]; then - echo "Usage: $0 [--check]" >&2 - exit 2 -fi - -######################## -# Defaults -######################## -EXT_IF="eth0" -PRIV_IF="enp7s0" -PRIV_NET="172.20.0.0/21" - -ALLOW_SSH_PUBLIC_IN=false -ALLOW_APT_PUBLIC_OUT=false - -ALLOW_ICMP4_PUBLIC=true -ALLOW_ICMP6_PUBLIC=true - -######################## -# Optional config file -######################## -CFG="/etc/default/nft-fw" -if [[ -r "$CFG" ]]; then - # shellcheck disable=SC1090 - source "$CFG" -fi - -######################## -# Normalize booleans -######################## -normalize_bool() { - # trim whitespace + remove CR (Windows line endings) - local v - v="$(printf '%s' "${1:-}" | tr -d '\r' | xargs)" - case "${v,,}" in - true|yes|1) echo true ;; - false|no|0|"") echo false ;; - *) echo false ;; - esac -} - -ALLOW_ICMP4_PUBLIC="$(normalize_bool "${ALLOW_ICMP4_PUBLIC:-true}")" -ALLOW_ICMP6_PUBLIC="$(normalize_bool "${ALLOW_ICMP6_PUBLIC:-true}")" -ALLOW_SSH_PUBLIC_IN="$(normalize_bool "${ALLOW_SSH_PUBLIC_IN:-false}")" -ALLOW_APT_PUBLIC_OUT="$(normalize_bool "${ALLOW_APT_PUBLIC_OUT:-false}")" - - -NEED_EXT=false -if [[ "$ALLOW_SSH_PUBLIC_IN" == "true" || "$ALLOW_APT_PUBLIC_OUT" == "true" || "$ALLOW_ICMP4_PUBLIC" == "true" || "$ALLOW_ICMP6_PUBLIC" == "true" ]]; then - NEED_EXT=true -fi - -EFFECTIVE_ALLOW_ICMP6_PUBLIC="$ALLOW_ICMP6_PUBLIC" -if [[ "$NEED_EXT" == "true" ]]; then - EFFECTIVE_ALLOW_ICMP6_PUBLIC=true -fi - -######################## -# Build optional rule blocks -######################## -ICMP_PUBLIC_IN_RULES="" -ICMP_PUBLIC_OUT_RULES="" -SSH_PUBLIC_IN_RULE="" -APT_PUBLIC_OUT_RULES="" - - -# ICMPv4 optional -if [[ "$ALLOW_ICMP4_PUBLIC" == "true" ]]; then - ICMP_PUBLIC_IN_RULES="$(cat < "$TMP" - -######################## -# Remove only our table (do NOT touch fail2ban) -######################## -nft delete table inet fw_static 2>/dev/null || true - -######################## -# Validate + apply -######################## -nft -c -f "$TMP" - -if [[ "$DO_APPLY" == "true" ]]; then - nft -f "$TMP" - install -m 600 "$TMP" /etc/nftables.conf -else - echo "" - echo " fw-apply: check mode" - [[ -r "$CFG" ]] && echo " - config loaded: $CFG" || echo " - config loaded: defaults only" - echo " - rendered nftables ruleset: OK" - echo " - syntax check (nft -c): OK" - echo " - no changes applied" - echo "" -fi diff --git a/bin/fw-stop b/bin/fw-stop deleted file mode 100755 index 0794fe9..0000000 --- a/bin/fw-stop +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Enstferne -nft delete table inet fw_static 2>/dev/null || true diff --git a/etc-default/nft-fw b/etc-default/nft-fw deleted file mode 100644 index d3dca4a..0000000 --- a/etc-default/nft-fw +++ /dev/null @@ -1,16 +0,0 @@ -# Öffentliches Interface -EXT_IF=eth0 - -# Privates Interface -PRIV_IF=enp7s0 - -# Privates Netz -PRIV_NET=172.20.0.0/21 - -# Public access toggles -ALLOW_SSH_PUBLIC_IN=true -ALLOW_APT_PUBLIC_OUT=true - -# ICMP toggles (separat) -ALLOW_ICMP4_PUBLIC=true -ALLOW_ICMP6_PUBLIC=true diff --git a/etc-nftables.conf.d/nft-fw.conf b/etc-nftables.conf.d/nft-fw.conf new file mode 100644 index 0000000..1d85ece --- /dev/null +++ b/etc-nftables.conf.d/nft-fw.conf @@ -0,0 +1,22 @@ +# Host-specific configuration for nft-fw. +# This file is read by /usr/local/sbin/fw-apply. +# +# Syntax: shell KEY=VALUE +# Values "true/false" are parsed case-insensitively. + +# Interfaces / networks +EXT_IF=eth0 +PRIV_IF=enp7s0 +PRIV_NET=172.20.0.0/21 + +# Feature toggles +ALLOW_SSH_PUBLIC_IN=true +ALLOW_APT_PUBLIC_OUT=true + +# ICMP toggles +ALLOW_ICMP4_PUBLIC=true +ALLOW_ICMP6_PUBLIC=true + +# Force ICMPv6 essential types when EXT_IF is "in use" (SSH or APT enabled) +FORCE_ICMP6_ESSENTIAL=true + diff --git a/install.sh b/install.sh index 8b8e459..b7137d4 100755 --- a/install.sh +++ b/install.sh @@ -7,18 +7,19 @@ say(){ echo "[nft-fw-nd-priv] $*"; } say "Creating directories..." install -d -m 0755 /usr/local/sbin +install -d -m 0755 /etc/nftables.conf.d say "Installing template..." install -m 0644 "$REPO_DIR/templates/nftables.conf.in" /etc/nftables.conf.in say "Installing scripts..." -install -m 0755 "$REPO_DIR/bin/fw-apply" /usr/local/sbin/fw-apply -install -m 0755 "$REPO_DIR/bin/fw-stop" /usr/local/sbin/fw-stop +install -m 0755 "$REPO_DIR/sbin/fw-apply" /usr/local/sbin/fw-apply +install -m 0755 "$REPO_DIR/sbin/fw-stop" /usr/local/sbin/fw-stop say "Installing default config (won't overwrite existing)..." -if [[ ! -f /etc/default/nft-fw ]]; then - install -m 0644 "$REPO_DIR/etc-default/nft-fw" /etc/default/nft-fw +if [[ ! -f /etc/nftables.conf.d/nft-fw.conf ]]; then + install -m 0644 "$REPO_DIR/etc-nftables.conf.d/nft-fw.conf" /etc/nftables.conf.d/nft-fw.conf else say "Config already exists at /etc/default/nft-fw (leaving as-is)." fi @@ -66,6 +67,6 @@ fi say "Applying firewall now..." /usr/local/sbin/fw-apply -say "Done. Edit /etc/default/nft-fw-nd-priv and re-run: fw-apply" +say "Done. Edit /etc/nftables.conf.d/nft-fw.conf and re-run: fw-apply" diff --git a/remove.sh b/remove.sh index c8b476e..152a83a 100755 --- a/remove.sh +++ b/remove.sh @@ -73,8 +73,13 @@ backup_then_remove /usr/local/sbin/fw-stop say "Removing template..." backup_then_remove /etc/nftables.conf.in -say "Removing default config..." -backup_then_remove /etc/default/nft-fw +say "Removing config..." +backup_then_remove /etc/nftables.conf.d/nft-fw.conf +backup_then_remove /etc/nftables.conf + +if ! rmdir /etc/nftables.conf.d 2>/dev/null; then + say "Directory '/etc/nftables.conf.d' could not be deleted because it is not empty." +fi say "Removing systemd unit file..." backup_then_remove /etc/systemd/system/nft-fw.service diff --git a/sbin/fw-apply b/sbin/fw-apply new file mode 100755 index 0000000..727850e --- /dev/null +++ b/sbin/fw-apply @@ -0,0 +1,193 @@ +#!/bin/bash +# *** [ Ansible managed file: DO NOT EDIT DIRECTLY ] *** + +# +# fw-apply +# +# Generic firewall loader (Ansible-independent): +# - Reads config from /etc/nftables.conf.d/nft-fw.conf (optional) +# - Renders /etc/nftables.conf.in using envsubst +# - Validates with: nft -c -f +# - Applies by deleting ONLY table inet fw_static and loading the new definition +# (keeps fail2ban tables intact) +# +# Usage: +# fw-apply -> validate + apply +# fw-apply --check -> validate only (no changes) +# + +set -euo pipefail + +############################ +# Argument handling +############################ +DO_APPLY=true +if [[ "${1:-}" == "--check" ]]; then + DO_APPLY=false +elif [[ "${1:-}" != "" ]]; then + echo "Usage: $0 [--check]" >&2 + exit 2 +fi + +############################ +# Config location +############################ +CFG_DIR="/etc/nftables.conf.d" +CFG_FILE="$CFG_DIR/nft-fw.conf" + +############################ +# Defaults (used if config missing/partial) +############################ +EXT_IF="eth0" +PRIV_IF="enp7s0" +PRIV_NET="172.20.0.0/21" + +ALLOW_SSH_PUBLIC_IN="true" +ALLOW_APT_PUBLIC_OUT="true" + +ALLOW_ICMP4_PUBLIC="true" +ALLOW_ICMP6_PUBLIC="true" +FORCE_ICMP6_ESSENTIAL="true" + +############################ +# Load config if present +############################ +if [[ -r "$CFG_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CFG_FILE" +fi + +############################ +# Helpers +############################ +normalize_bool() { + # Robust against CRLF and leading/trailing whitespace. + local v + v="$(printf '%s' "${1:-}" | tr -d '\r' | xargs)" + case "${v,,}" in + true|yes|1) echo true ;; + false|no|0|"") echo false ;; + *) echo false ;; + esac +} + +ALLOW_SSH_PUBLIC_IN="$(normalize_bool "$ALLOW_SSH_PUBLIC_IN")" +ALLOW_APT_PUBLIC_OUT="$(normalize_bool "$ALLOW_APT_PUBLIC_OUT")" +ALLOW_ICMP4_PUBLIC="$(normalize_bool "$ALLOW_ICMP4_PUBLIC")" +ALLOW_ICMP6_PUBLIC="$(normalize_bool "$ALLOW_ICMP6_PUBLIC")" +FORCE_ICMP6_ESSENTIAL="$(normalize_bool "$FORCE_ICMP6_ESSENTIAL")" + +############################ +# Determine external usage +############################ +# EXT_IF is considered "in use" if any externally-relevant feature is enabled. +NEED_EXT=false +if [[ "$ALLOW_SSH_PUBLIC_IN" == "true" || "$ALLOW_APT_PUBLIC_OUT" == "true" ]]; then + NEED_EXT=true +fi + +############################ +# Build envsubst blocks (must contain REAL newlines) +############################ +SSH_PUBLIC_IN_RULE="" +APT_PUBLIC_OUT_RULES="" +ICMP_PUBLIC_IN_RULES="" +ICMP_PUBLIC_OUT_RULES="" + +# SSH IN (public) +if [[ "$ALLOW_SSH_PUBLIC_IN" == "true" ]]; then + SSH_PUBLIC_IN_RULE="$(cat < "$TMP" + +# Replace only our table (keep fail2ban intact) +nft delete table inet fw_static 2>/dev/null || true + +# Validate syntax +nft -c -f "$TMP" + +if [[ "$DO_APPLY" == "true" ]]; then + nft -f "$TMP" + install -m 600 "$TMP" /etc/nftables.conf +else + echo "fw-apply: check mode" + [[ -r "$CFG_FILE" ]] && echo "- config loaded: $CFG_FILE" || echo "- config loaded: defaults only" + echo "- rendered nftables ruleset: OK" + echo "- syntax check (nft -c): OK" + echo "- no changes applied" +fi + diff --git a/sbin/fw-stop b/sbin/fw-stop new file mode 100755 index 0000000..0bb2550 --- /dev/null +++ b/sbin/fw-stop @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +# *** [ Ansible managed file: DO NOT EDIT DIRECTLY ] *** + + +nft delete table inet fw_static 2>/dev/null || true + diff --git a/systemd/nft-fw.service b/systemd/nft-fw.service index 56331a5..85c028c 100644 --- a/systemd/nft-fw.service +++ b/systemd/nft-fw.service @@ -2,7 +2,6 @@ Description=Apply nftables firewall (parameterized) Documentation=man:nft(8) After=local-fs.target -Wants=network-pre.target Before=network-pre.target [Service] @@ -14,3 +13,4 @@ PrivateTmp=yes [Install] WantedBy=multi-user.target + diff --git a/templates/nftables.conf.in b/templates/nftables.conf.in index 0dfda16..55cf1b5 100644 --- a/templates/nftables.conf.in +++ b/templates/nftables.conf.in @@ -1,5 +1,13 @@ #!/usr/sbin/nft -f +# +# Static firewall template. +# This file contains shell-style placeholders ($VARS) that are replaced by fw-apply via envsubst. +# +# Important: +# - We only manage table inet fw_static. +# - We do NOT flush the entire ruleset (fail2ban rules remain intact). + table inet fw_static { chain input { @@ -11,10 +19,8 @@ table inet fw_static { # Public: ICMP (optional) $ICMP_PUBLIC_IN_RULES - # Public: SSH IN (optional) $SSH_PUBLIC_IN_RULE - # Private network (in) iif "$PRIV_IF" ip saddr $PRIV_NET accept } @@ -26,12 +32,10 @@ $SSH_PUBLIC_IN_RULE oif "lo" accept ct state established,related accept - # Public: ICMP (optional) + # Public: ICMP (optional) $ICMP_PUBLIC_OUT_RULES - # Public: APT OUT (optional) - includes DNS + HTTP/HTTPS $APT_PUBLIC_OUT_RULES - # Private network (out) oif "$PRIV_IF" ip daddr $PRIV_NET accept } @@ -41,3 +45,4 @@ $APT_PUBLIC_OUT_RULES policy drop; } } +