#!/usr/bin/env bash # Dedicated OCI Ubuntu 24.04 only. Run on Oracle, not on your home firewall. # Requires existing iptables, nftables, netfilter-persistent, Python and systemd. set -Eeuo pipefail umask 077 die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die 'Run as root, preserving SSH_CONNECTION when using sudo.' [[ $# -ge 1 && $# -le 2 ]] || die 'Usage: install-firewall.sh ADMIN_IPV4_CIDR [--console]' [[ ${2:-} == '' || ${2:-} == --console ]] || die 'Only --console is supported as the second argument.' for executable in python3 iptables ip6tables iptables-save ip6tables-save iptables-restore ip6tables-restore nft systemctl systemd-run netfilter-persistent; do command -v "$executable" >/dev/null || die "Required command missing: $executable" done [[ -r /etc/os-release ]] || die 'Cannot identify the operating system.' . /etc/os-release [[ ${ID:-} == ubuntu && ${VERSION_ID:-} == 24.04 ]] || die 'This helper requires Ubuntu 24.04.' [[ $(iptables --version) == *nf_tables* && $(ip6tables --version) == *nf_tables* ]] || die 'Expected the stock iptables-nft backend for both families.' [[ -n ${SSH_CONNECTION:-} || ${2:-} == --console ]] || die 'SSH_CONNECTION is absent. Preserve it through sudo, or use --console only from the OCI console.' admin_cidr=$(python3 - "$1" "${SSH_CONNECTION:-}" <<'PY' import ipaddress, sys network = ipaddress.ip_network(sys.argv[1], strict=False) if network.version != 4: raise SystemExit('The administrator CIDR must be IPv4.') if network.prefixlen == 0: raise SystemExit('Refusing unrestricted public SSH; use your administrator /32 or trusted network.') if sys.argv[2]: fields = sys.argv[2].split() if len(fields) != 4: raise SystemExit('Malformed SSH_CONNECTION.') if fields[3] != '22': raise SystemExit('This helper permits SSH port 22 only; current SSH uses another port.') source = ipaddress.ip_address(fields[0]) if source.version != 4 or source not in network: raise SystemExit('Current SSH source is outside the administrator IPv4 CIDR.') print(network) PY ) for manager in ufw firewalld nftables docker; do if systemctl is-active --quiet "$manager.service"; then die "Competing service is active: $manager. Use a fresh dedicated VM." fi done command -v docker >/dev/null && die 'Docker is installed; this helper is for a dedicated VM without Docker.' rules_v4=$(iptables-save 2>&1) rules_v6=$(ip6tables-save 2>&1) rule_inventory="$rules_v4$rules_v6" [[ $rule_inventory != *Warning* && $rule_inventory != *incompatible* ]] || die 'iptables reports incompatible/native or legacy rules; inspect manually.' [[ $rule_inventory != *EDGE_INPUT* && $rule_inventory != *DOCKER* && $rule_inventory != *ufw-* ]] || die 'Existing edge, Docker or UFW chains found; refusing to overwrite them.' nft -j list tables | python3 -c ' import json, sys allowed = {"filter", "nat", "mangle", "raw", "security"} for item in json.load(sys.stdin)["nftables"]: table = item.get("table") if table and (table["family"] not in {"ip", "ip6"} or table["name"] not in allowed): raise SystemExit("Unexpected native nftables table; inspect manually: " + str(table)) ' backup_dir=$(mktemp -d /root/oracle-edge-backup.XXXXXXXX) iptables-save >"$backup_dir/live.v4" ip6tables-save >"$backup_dir/live.v6" for family in v4 v6; do if [[ -e /etc/iptables/rules.$family ]]; then cp /etc/iptables/rules."$family" "$backup_dir/persistent.$family" else touch "$backup_dir/persistent.$family.absent" fi done chmod 600 "$backup_dir"/* unit_name="oracle-edge-rollback-$(date +%s)-$$" printf 'BACKUP_DIR=%q\n' "$backup_dir" >"$backup_dir/rollback.env" cat >"$backup_dir/rollback.sh" <<'ROLLBACK' #!/usr/bin/env bash set -uo pipefail umask 077 . "$(dirname "$0")/rollback.env" result=0 # Restore both complete pre-change rulesets, retaining OCI OUTPUT protections. iptables-restore --wait 10 <"$BACKUP_DIR/live.v4" || result=1 ip6tables-restore --wait 10 <"$BACKUP_DIR/live.v6" || result=1 for family in v4 v6; do if [[ -f $BACKUP_DIR/persistent.$family.absent ]]; then rm -f /etc/iptables/rules."$family" || result=1 else mkdir -p /etc/iptables || result=1 install -m 600 "$BACKUP_DIR/persistent.$family" /etc/iptables/rules."$family" || result=1 fi done exit "$result" ROLLBACK chmod 700 "$backup_dir/rollback.sh" rollback_on_error() { local result=$1 trap - ERR INT TERM printf 'Installation failed; restoring backups from %s\n' "$backup_dir" >&2 if bash "$backup_dir/rollback.sh"; then systemctl stop "$unit_name.timer" 2>/dev/null || true else printf 'Rollback reported an error; use the OCI console. Timer remains armed.\n' >&2 fi exit "$result" } trap 'rollback_on_error $?' ERR trap 'rollback_on_error 130' INT trap 'rollback_on_error 143' TERM # Arm recovery before making any rule changes. Nothing is persisted yet. systemd-run --unit="$unit_name" --on-active=180s --timer-property=AccuracySec=1s \ --property=Type=oneshot /bin/bash "$backup_dir/rollback.sh" systemctl is-active --quiet "$unit_name.timer" # Build unattached chains. Existing INPUT paths remain effective during staging. iptables --wait 10 -N EDGE_INPUT ip6tables --wait 10 -N EDGE_INPUT6 for family in v4 v6; do if [[ $family == v4 ]]; then firewall=iptables chain=EDGE_INPUT else firewall=ip6tables chain=EDGE_INPUT6 fi "$firewall" --wait 10 -A "$chain" -i lo -j ACCEPT "$firewall" --wait 10 -A "$chain" -m conntrack --ctstate INVALID -j DROP "$firewall" --wait 10 -A "$chain" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT if [[ $family == v4 ]]; then "$firewall" --wait 10 -A "$chain" -p icmp -j ACCEPT "$firewall" --wait 10 -A "$chain" -p udp --sport 67 --dport 68 -j ACCEPT "$firewall" --wait 10 -A "$chain" -s "$admin_cidr" -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT "$firewall" --wait 10 -A "$chain" -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT else "$firewall" --wait 10 -A "$chain" -p ipv6-icmp -j ACCEPT "$firewall" --wait 10 -A "$chain" -p udp --sport 547 --dport 546 -j ACCEPT fi "$firewall" --wait 10 -A "$chain" -j DROP done # Within each family the completed chain becomes live with one head insertion. iptables --wait 10 -I INPUT 1 -j EDGE_INPUT ip6tables --wait 10 -I INPUT 1 -j EDGE_INPUT6 trap - ERR INT TERM cat <