Proxmox VE 9.1 - Configuration

Proxmox VE 9.1 - Configuration
Proxmox VE 9.1

As previously stated, I have been reading a lot of Brandon Lee and realized that I have been configuring my LXC's and Proxmox server with obsolete iptables.

This has resulted in a complete rewrite of the associated configuration scripts


Brandon Lee is located here

Complete Guide to Proxmox Containers in 2025: Docker VMs, LXC, and New OCI Support
Learn how to run Proxmox containers in 2025 using Docker VMs, LXC, and new OCI support with tips for performance, updates, and home lab.
Proxmox VE 9.1 Launches with OCI Image Support, vTPM Snapshots, and Big SDN Upgrades
See what is new in Proxmox VE 9.1, from OCI containers to vTPM and networking upgrades, and follow easy upgrade steps to get your cluster ready.
12 Proxmox Host Tweaks Worth Doing This Weekend
These Proxmox host tweaks focus on real-world performance, reliability, and cleanup tasks you can knock out in a weekend home lab maintenance window.
Proxmox Defaults I Leave Alone (And the Ones I Always Change)
Review Proxmox default settings for home labs, including VM hardware, ZFS tuning, backup retention, and which defaults to keep or change for stability.

Build Recommendations

Brandon Lee yet again but very good place to start

My Top 3 Proxmox Server Builds for Performance and Efficiency
Take a look at my top Proxmox server builds featuring Minisforum boards for performance and efficiency in home lab setups.

Background

The following changes were made

  • Previously rebuilt the server to a clean install of Proxmox VE 9.1
  • Rebuild all the LXC Debian 13 containers based on Proxmox Community Scripts
  • Review networking especially the sysctl commands
  • Implemented a lot of Brandon Lees code and ideas - he is very good.
  • Move all firewalls to nftables and ditch iptables - you can't on the Proxmox server
  • Test, test and test.

At time of writing and multiple reboots I have not found a problem with the server, the reverse proxy LXC or the Podman application servers since I have ditched Docker due to the November fiasco that created havoc for most enthusiasts.


Network Configuration

The following basically gives you an idea of the network

  • All IPs are issued by the router - ipv4 and ipv6 - some are fixed
  • The Proxmox server hosts all applications whether they are local or WAN enabled
  • The Proxmox server runs LXC's due to dated hardware, has GPU enabled via LXC pass through which is very different from VM pass through
  • I run a dedicated proxy on one LXC
  • I then run one primary application LXC and a couple of developmental types

Proxmox Server Code

Find below the primary installation script

  • Additional scripts are - gpu, postfix, fail2ban
  • I won't share these as they are a work in progress and not needed to get running
#!/bin/bash

# ========================================================================
# Proxmox VE 9.1 (Debian 13.2) PXE / Utility Node Setup Script
# ========================================================================
# This script performs a full initial configuration of a Proxmox host used
# as a PXE server, utility node, or general-purpose hypervisor in a home lab
# or small office environment.
#
# Major functions performed:
#   • Rewrites /etc/network/interfaces with corrected vmbr0 bridge config
#   • Properly configures eth0 as bridge slave with explicit up/down commands
#   • Enables IPv4/IPv6 forwarding and applies sysctl hardening (before ifup)
#   • Installs and configures resolvconf for managed DNS resolution
#   • Deploys /etc/network/if-up.d/ipv6-dns hook for stable IPv6 DNS
#   • Removes dhcpcd5 and disables the service to prevent conflicts
#   • Uses ONLY standard Debian networking (no dual network managers)
#   • Regenerates SSH host keys and applies a hardened sshd_config
#   • Installs required packages and removes unsafe defaults
#   • Configures nftables as the authoritative firewall (Proxmox firewall disabled)
#   • Deploys a hardened nftables ruleset with trusted-host access only
#   • Sets up nftables logging and log rotation
#   • Applies kernel, memory, and system tuning parameters
#   • Performs final diagnostics for networking and SSH
#
# Requirements:
#   • Designed for Proxmox VE 9.1 running Debian 13.2
#   • Safe for single-node or non-clustered Proxmox deployments
#   • Assumes primary NIC is "eth0" and main bridge is "vmbr0"
#   • Script is intended for fresh installations — it overwrites key configs
#   • iptables is NOT removed (required by Proxmox), but is not used
#   • nftables becomes the sole active firewall
#
# Expected Behavior After Running:
#   ✓ LXC containers can restart without breaking host connectivity
#   ✓ SSH remains accessible during network reconfigurations
#   ✓ Proxmox GUI remains accessible during network reconfigurations
#   ✓ No more need to reboot after LXC restarts
#   ✓ Single network manager eliminates race conditions
#   ✓ Stable bridge configuration survives network events
#   ✓ IPv6 DNS resolves correctly using UDM SE link-local address
#   ✓ IPv6 DNS survives UDM SE reboots (link-local never rotates)
#   ✓ search domain braedach.com applied for both IPv4 and IPv6
#
# Testing Recommendations:
#   1. After running script, verify: systemctl status networking
#   2. Confirm dhcpcd is gone: systemctl status dhcpcd (should be "not found")
#   3. Test LXC restart: pct restart <container-id>
#   4. Verify SSH still works after LXC restart
#   5. Check logs: journalctl -u networking -n 50
#   6. Verify IPv6 DNS: cat /etc/resolv.conf (should show fe80::x%vmbr0)
#   7. Test IPv6 resolution: dig -6 @<fe80_addr>%vmbr0 braedach.com AAAA
#
# Backup and Rollback:
#   • All original configs are backed up with timestamps
#   • Network config: /etc/network/interfaces.bak.<timestamp>
#   • SSH config: /etc/ssh/backup-<timestamp>/
#   • Rollback: restore backups and run: systemctl restart networking && reboot
#
# Version: 4.1.0
# Code review date: 24-03-2026
# ========================================================================

set -euo pipefail

# ------------------------------------------------------------------------
# Logging
info()  { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn()  { echo -e "\033[0;33m[WARN]\033[0m $*" >&2; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }

LOGFILE="/var/log/proxmox-setup-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$LOGFILE") 2>&1

BACKUP_TS="$(date +%Y%m%d-%H%M%S)"

# ------------------------------------------------------------------------
# Configuration - adjust these for your environment
PRIMARY_NIC="eth0"
VM_BRIDGE="vmbr0"
DNS_SEARCH_DOMAIN="braedach.com"
IPV4_DNS="192.168.1.1"

# UDM SE link-local IPv6 address.
# The link-local is derived from the UDM SE MAC address and NEVER changes
# across reboots, unlike the global GUA which rotates. This is why we use
# the link-local rather than the global address for DNS.
#
# To find your UDM SE link-local BEFORE running this script:
#   ip -6 neigh show dev vmbr0 | grep fe80
# Or on the UDM SE itself: ip -6 addr show | grep fe80
#
# Set this variable to your actual UDM SE link-local address:
UDM_SE_LINK_LOCAL="fe80::f6e2:c6ff:feee:63e3"

# ------------------------------------------------------------------------
# Timezone configuration
info "Setting timezone to Australia/Perth..."
timedatectl set-timezone Australia/Perth

# ------------------------------------------------------------------------
# Install necessary packages
# NOTE: resolvconf is included here - required for the IPv6 DNS hook
info "Installing required packages..."
apt-get update -qq || warn "apt-get update failed - check DNS/network"
apt-get install -y -qq \
    rsyslog \
    mtr \
    git \
    curl \
    wget \
    build-essential \
    dkms \
    resolvconf \
    || error "Package installation failed"

# ------------------------------------------------------------------------
# Remove conflicting packages
# dhcpcd5 is explicitly removed - it conflicts with networking service and
# causes race conditions during LXC container restarts
info "Removing conflicting packages..."
for pkg in ufw inetutils-telnet dhcpcd5; do
    if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
        apt-get purge --auto-remove -y -qq "$pkg" || warn "$pkg failed to uninstall"
    fi
done

systemctl stop dhcpcd 2>/dev/null || true
systemctl disable dhcpcd 2>/dev/null || true

# ------------------------------------------------------------------------
# Backup existing configs
cp -a /etc/sysctl.d /etc/sysctl.d.bak.${BACKUP_TS}
cp -a /etc/apt/apt.conf.d /etc/apt/apt.conf.d.bak.${BACKUP_TS}
cp -a /etc/logrotate.conf /etc/logrotate.conf.bak.${BACKUP_TS}

# ------------------------------------------------------------------------
# APT configuration
# NOTE: ForceIPv4 has been deliberately removed. It prevented APT from
# using IPv6 and contradicted the IPv6 DNS setup. Remove from any existing
# config files if present from a previous run.
info "Configuring APT..."
echo 'Acquire::Languages "none";' | tee /etc/apt/apt.conf.d/99-disable-languages
rm -f /etc/apt/apt.conf.d/99force-ipv4

# ------------------------------------------------------------------------
# Kernel panic auto-reboot
info "Configuring kernel panic auto-reboot..."
echo "kernel.panic = 10"    | tee    /etc/sysctl.d/99-kernelpanic.conf
echo "kernel.panic_on_oops = 1" | tee -a /etc/sysctl.d/99-kernelpanic.conf
sysctl -p /etc/sysctl.d/99-kernelpanic.conf || warn "Failed to apply kernel panic sysctl"

# ------------------------------------------------------------------------
# System limits
info "Configuring system limits..."
echo "fs.inotify.max_user_watches = 1048576" | tee /etc/sysctl.d/99-maxwatches.conf
echo "* soft nofile 1048576" | tee /etc/security/limits.d/99-limits.conf

# ------------------------------------------------------------------------
# 1. Network sysctl tuning
# IMPORTANT: These are written and applied BEFORE the networking restart so
# that accept_ra=2 is active when the interface first comes up, ensuring
# Router Advertisements are correctly accepted from the first bring-up.
info "Writing and applying IPv4 sysctl settings..."
tee /etc/sysctl.d/99-ipv4-${VM_BRIDGE}.conf >/dev/null <<EOF
net.ipv4.ip_forward=1
EOF

info "Writing and applying IPv6 sysctl settings..."
tee /etc/sysctl.d/99-ipv6-${VM_BRIDGE}.conf >/dev/null <<EOF
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.default.forwarding=1
net.ipv6.conf.${VM_BRIDGE}.accept_ra=2
net.ipv6.conf.${VM_BRIDGE}.autoconf=1
EOF

# Apply all sysctl settings now, before networking restart
sysctl --system || warn "Failed to apply sysctl settings"

# ------------------------------------------------------------------------
# 2. Backup and write /etc/network/interfaces
info "Backing up and writing /etc/network/interfaces..."
cp /etc/network/interfaces /etc/network/interfaces.bak.${BACKUP_TS} || true

# NOTE on inet6 stanza: dns-search and dns-nameservers are silently ignored
# by ifupdown for inet6 stanzas. IPv6 DNS is injected by the if-up.d hook
# script deployed in the next section. The inet4 stanza dns-* lines ARE
# processed by resolvconf via the resolvconf integration in ifupdown.
tee /etc/network/interfaces >/dev/null <<EOF
# Managed by setup-pxe-script.sh (${BACKUP_TS})
# CORRECTED VERSION - dhcpcd removed to prevent conflicts
# IPv6 DNS is handled by /etc/network/if-up.d/ipv6-dns hook script

auto lo
iface lo inet loopback

# Physical interface - bring up without IP configuration
# Critical: This ensures the bridge always has its physical port available
auto ${PRIMARY_NIC}
iface ${PRIMARY_NIC} inet manual
    up ip link set ${PRIMARY_NIC} up
    down ip link set ${PRIMARY_NIC} down

# Primary bridge - VMs and LXC containers use this
auto ${VM_BRIDGE}
iface ${VM_BRIDGE} inet dhcp
    bridge-ports ${PRIMARY_NIC}
    bridge-stp off
    bridge-fd 0
    bridge-maxwait 0
    dns-nameservers ${IPV4_DNS}
    dns-search ${DNS_SEARCH_DOMAIN}

# IPv6 auto-configuration via Router Advertisement
# DNS for IPv6 is handled by /etc/network/if-up.d/ipv6-dns (not here -
# dns-* options are ignored by ifupdown in inet6 stanzas)
iface ${VM_BRIDGE} inet6 auto
    accept_ra 2
    autoconf 1
    privext 0
EOF

info "Interfaces file written."

# ------------------------------------------------------------------------
# 3. Deploy IPv6 DNS hook script
#
# Why this approach:
#   The UDM SE's global IPv6 address (GUA) changes on reboot because the
#   ISP prefix rotates. The link-local address (fe80::) is derived from
#   the MAC address and is stable forever. By using the link-local as the
#   DNS server, IPv6 name resolution survives UDM SE reboots without any
#   reconfiguration. DHCPv6 prefix delegation options cannot be configured
#   on the UDM SE to inject DNS options, so this hook is the correct fix.
#
# The hook is triggered each time vmbr0 inet6 comes up and injects the
# link-local DNS server and search domain into resolvconf.

info "Deploying IPv6 DNS if-up hook..."

mkdir -p /etc/network/if-up.d

tee /etc/network/if-up.d/ipv6-dns >/dev/null <<EOF
#!/bin/sh
# /etc/network/if-up.d/ipv6-dns
# Inject IPv6 DNS server using the UDM SE link-local address.
#
# The link-local (fe80::) address is derived from the UDM SE MAC address
# and never changes across reboots - unlike the global IPv6 address which
# rotates when the ISP reassigns the prefix. This makes it the correct
# choice for a stable DNS server reference.
#
# Managed by setup-pxe-script.sh (${BACKUP_TS})

[ "\$IFACE" = "${VM_BRIDGE}" ] || exit 0

# Trigger on inet6 address family, or when called without ADDRFAM set
# (some ifupdown versions omit ADDRFAM for auto stanzas)
if [ -n "\${ADDRFAM:-}" ] && [ "\$ADDRFAM" != "inet6" ]; then
    exit 0
fi

# Verify the link-local gateway is reachable before injecting
# (avoids injecting a stale entry if IPv6 hasn't come up yet)
GATEWAY_LL="${UDM_SE_LINK_LOCAL}"
SCOPED_ADDR="\${GATEWAY_LL}%${VM_BRIDGE}"

resolvconf -a "\${IFACE}.inet6" <<RESOLVEOF
nameserver \${SCOPED_ADDR}
search ${DNS_SEARCH_DOMAIN}
RESOLVEOF

logger -t ipv6-dns "Injected IPv6 DNS: \${SCOPED_ADDR} search ${DNS_SEARCH_DOMAIN}"
EOF

chmod +x /etc/network/if-up.d/ipv6-dns
info "IPv6 DNS hook deployed: /etc/network/if-up.d/ipv6-dns"

# Also create corresponding if-down.d to clean up on interface down
mkdir -p /etc/network/if-down.d
tee /etc/network/if-down.d/ipv6-dns >/dev/null <<'EOF'
#!/bin/sh
# /etc/network/if-down.d/ipv6-dns
# Remove IPv6 DNS entry when interface goes down

[ "$IFACE" = "vmbr0" ] || exit 0
resolvconf -d "${IFACE}.inet6" 2>/dev/null || true
logger -t ipv6-dns "Removed IPv6 DNS entry for ${IFACE}.inet6"
EOF

chmod +x /etc/network/if-down.d/ipv6-dns

# ------------------------------------------------------------------------
# 4. Configure resolvconf and verify /etc/resolv.conf symlink
info "Configuring resolvconf..."
systemctl enable resolvconf 2>/dev/null || true
systemctl start resolvconf 2>/dev/null || true

# Ensure /etc/resolv.conf is a resolvconf-managed symlink, not a plain file.
# A plain file means resolvconf cannot update DNS - hook injections are lost.
RESOLV_TARGET=$(readlink /etc/resolv.conf 2>/dev/null || true)
if echo "$RESOLV_TARGET" | grep -q "resolvconf\|run/resolvconf"; then
    info "/etc/resolv.conf is correctly managed by resolvconf."
else
    warn "/etc/resolv.conf is not managed by resolvconf - correcting..."
    cp /etc/resolv.conf /etc/resolv.conf.bak.${BACKUP_TS} || true
    rm -f /etc/resolv.conf
    # Debian's resolvconf package creates this symlink target
    if [ -e /run/resolvconf/resolv.conf ]; then
        ln -s /run/resolvconf/resolv.conf /etc/resolv.conf
    else
        # Pre-run: the runtime directory may not exist until resolvconf starts
        # Create the directory and a placeholder; resolvconf will populate it
        mkdir -p /run/resolvconf
        ln -s /run/resolvconf/resolv.conf /etc/resolv.conf
    fi
    info "/etc/resolv.conf symlink corrected."
fi

# ------------------------------------------------------------------------
# 5. Harden bash history
info "Configuring hardened bash history..."
cat >/etc/profile.d/hardened-history.sh <<'EOF'
export HISTSIZE=10000
export HISTFILESIZE=100000
shopt -s histappend
export HISTTIMEFORMAT='%F %T '
PROMPT_COMMAND='history -a'
history -r
EOF

# ------------------------------------------------------------------------
# 6. Configure cron jobs
info "Configuring cron jobs for updates and maintenance..."
( crontab -l 2>/dev/null || true; \
  echo "0 1 * * * apt-get update -qq && apt-get -y -qq upgrade && apt-get -y -qq autoremove && apt-get -y -qq autoclean >> /var/log/apt-cron.log 2>&1" \
) | sort -u | crontab -

if systemctl restart cron.service 2>/dev/null; then
    info "Cron restarted (cron.service)"
elif systemctl restart crond.service 2>/dev/null; then
    info "Cron restarted (crond.service)"
else
    error "Cron restart failed"
    exit 1
fi

# ------------------------------------------------------------------------
# 7. MOTD configuration
info "Configuring MOTD..."
rm -f /etc/update-motd.d/*

cat >/etc/update-motd.d/80-sysinfo <<'EOF'
#!/bin/bash
echo "=== System Information ==="
echo ""
echo "  🖥️ Operating System : $( lsb_release -ds 2>/dev/null || grep -oP '(?<=PRETTY_NAME=\").*(?=\")' /etc/os-release )" 
echo "  🌐 IPv4 Address : $(hostname -I | awk '{print $1}')" 
echo "  🔗 IPv6 Global : $(ip -6 addr show scope global | grep inet6 | awk '{print $2}' | cut -d'/' -f1 | head -n1)" 
echo "  🪢 IPv6 Link-local : $(ip -6 addr show scope link | grep inet6 | awk '{print $2}' | cut -d'/' -f1 | head -n1)" 
echo "  ⏱️ System Uptime : $(uptime -p)"
EOF

chmod 755 /etc/update-motd.d/80-sysinfo
chown root:root /etc/update-motd.d/80-sysinfo

# ------------------------------------------------------------------------
# 8. Restart networking
# Sysctl settings were applied before this point so accept_ra=2 is active
# before the interface first comes up.
info "Restarting networking service..."
ip link set "${VM_BRIDGE}" down 2>/dev/null || true

if systemctl restart networking; then
    info "Networking service restarted successfully."
else
    warn "Networking restart failed - check configuration manually."
    warn "You may need to reboot to apply network changes."
fi

sleep 3

if ip link show "${VM_BRIDGE}" | grep -q "state UP"; then
    info "${VM_BRIDGE} is UP."
else
    warn "${VM_BRIDGE} may not be up yet:"
    ip link show "${VM_BRIDGE}" || true
fi

# ------------------------------------------------------------------------
# 9. SSH regeneration and hardening
info "Hardening SSH configuration..."

SSHD_DIR="/etc/ssh"
SSHD_CFG="${SSHD_DIR}/sshd_config"
SSHD_CUSTOM_DIR="${SSHD_DIR}/sshd_config.d"
SSHD_CUSTOM_CFG="${SSHD_CUSTOM_DIR}/99-hardened-custom.conf"
BACKUP_DIR="${SSHD_DIR}/backup-${BACKUP_TS}"

mkdir -p "${BACKUP_DIR}"
mkdir -p "${SSHD_CUSTOM_DIR}"

info "Backing up sshd_config and host keys..."
cp -a "${SSHD_CFG}" "${BACKUP_DIR}/sshd_config.bak" || true
for key in ssh_host_ed25519_key ssh_host_rsa_key ssh_host_ecdsa_key; do
    [[ -f "${SSHD_DIR}/${key}" ]]     && mv "${SSHD_DIR}/${key}"     "${BACKUP_DIR}/" || true
    [[ -f "${SSHD_DIR}/${key}.pub" ]] && mv "${SSHD_DIR}/${key}.pub" "${BACKUP_DIR}/" || true
done

info "Generating new SSH host keys..."
rm -f /etc/ssh/ssh_host_*
ssh-keygen -t ed25519 -f "${SSHD_DIR}/ssh_host_ed25519_key" -N "" -o -a 100
ssh-keygen -t rsa -b 4096 -f "${SSHD_DIR}/ssh_host_rsa_key" -N "" -o

chmod 600 "${SSHD_DIR}/ssh_host_ed25519_key" "${SSHD_DIR}/ssh_host_rsa_key"
chmod 644 "${SSHD_DIR}/ssh_host_ed25519_key.pub" "${SSHD_DIR}/ssh_host_rsa_key.pub"

info "Ensuring sshd_config includes drop-in directory..."
if ! grep -q "^Include ${SSHD_CUSTOM_DIR}/\*.conf" "${SSHD_CFG}"; then
    sed -i "1i Include ${SSHD_CUSTOM_DIR}/*.conf" "${SSHD_CFG}"
    info "Added Include directive to ${SSHD_CFG}"
fi

info "Writing hardened SSH configuration..."
tee "${SSHD_CUSTOM_CFG}" >/dev/null <<EOF
# Hardened SSH Configuration
# Managed by setup-pxe-script.sh (${BACKUP_TS})

Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
HostbasedAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
LoginGraceTime 30
MaxAuthTries 4
MaxSessions 10

HostKey ${SSHD_DIR}/ssh_host_ed25519_key
HostKey ${SSHD_DIR}/ssh_host_rsa_key

AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
UseDNS no
PrintMotd no
PrintLastLog yes
AcceptEnv LANG LC_*

# Permit password auth from RFC1918, ULA, and link-local IPv6
Match Address 10.0.0.0/8,192.168.0.0/16,fe80::/10,fc00::/7
  PermitRootLogin yes
  PasswordAuthentication yes
EOF

chmod 644 "${SSHD_CUSTOM_CFG}"

info "Validating sshd_config..."
if sshd -t; then
    info "sshd_config syntax OK, restarting..."
    if systemctl restart ssh || systemctl restart sshd; then
        info "sshd restarted successfully."
    else
        error "systemctl restart ssh failed."
    fi
else
    error "sshd_config test failed - not restarting ssh."
    error "Backup available at: ${BACKUP_DIR}/sshd_config.bak"
fi

# ------------------------------------------------------------------------
# 10. Configure firewall - nftables only (Proxmox-safe)
info "Configuring nftables firewall..."

apt-get install -y nftables
systemctl enable --now nftables

pve-firewall stop 2>/dev/null || true
systemctl disable pve-firewall 2>/dev/null || true
systemctl disable --now netfilter-persistent 2>/dev/null || true

cat >/etc/nftables.conf <<'EOF'
#!/usr/sbin/nft -f
include "/etc/nftables-host.conf"
EOF

# NOTE on IPv6 trusted hosts:
# Trusted LAN IPv4 hosts are listed explicitly. IPv6 equivalents are covered
# by the fc00::/7 (ULA) and fe80::/10 (link-local) rules which permit full
# access from the local network segment. Individual IPv6 GUAs are NOT listed
# because they may change. Link-local and ULA are stable.
cat >/etc/nftables-host.conf <<'EOF'
#!/usr/sbin/nft -f

add table inet filter
flush table inet filter

table inet filter {

    chain input {
        type filter hook input priority filter; policy drop;

        # Loopback
        iifname "lo" accept

        # Established and related connections - must be near the top
        ct state established,related accept
        ct state invalid log prefix "INVALID-STATE: " drop

        # Trusted LAN hosts (IPv4) - full access
        ip saddr {
            192.168.1.1,      # Router / UDM SE
            192.168.1.15,     # Alex (reverse proxy)
            192.168.1.166,    # Management workstation 1
            192.168.1.252     # Management workstation 2
        } accept

        # Trusted LAN (IPv6) - ULA and link-local get full access
        # This covers the IPv6 equivalents of the trusted hosts above
        # without needing to hardcode GUAs that may rotate
        ip6 saddr fc00::/7  accept
        ip6 saddr fe80::/10 accept

        # SMTP relay (Mailgun) - rate limited
        tcp dport 25 ct state new limit rate 100/minute burst 200 packets accept
        tcp dport 25 log prefix "SMTP-RATELIMIT: " drop

        # Proxmox GUI (8006) - LAN only (IPv6 covered by trusted block above)
        tcp dport 8006 ip saddr 192.168.0.0/16 accept
        tcp dport 8006 log prefix "PROXMOX-DENY: " drop

        # DNS (both IPv4 and IPv6, both UDP and TCP)
        meta l4proto { udp, tcp } th dport 53 accept

        # DHCP client (receiving from DHCP server)
        meta l4proto udp udp sport 67 udp dport 68 accept

        # DHCPv6 client
        meta l4proto udp udp dport 546 accept

        # mDNS
        meta l4proto udp udp dport 5353 accept

        # ICMPv4 - essential types only
        ip protocol icmp icmp type {
            echo-request,
            echo-reply,
            destination-unreachable,
            time-exceeded,
            parameter-problem
        } accept

        # ICMPv6 - all types accepted; required for IPv6 operation
        # (neighbor discovery, router advertisement, MLD, etc.)
        meta l4proto ipv6-icmp accept

        # SSH - trusted management workstations only, with brute-force protection
        tcp dport 22 ip saddr { 192.168.1.166, 192.168.1.252 } \
            ct state new limit rate 3/minute burst 5 packets accept
        tcp dport 22 ip saddr { 192.168.1.166, 192.168.1.252 } \
            ct state new log prefix "SSH-BRUTE: " drop
        tcp dport 22 log prefix "SSH-DENY: " drop

        # Generic rate-limited drop logging
        limit rate 5/minute burst 20 packets log prefix "DROP: "
    }

    chain forward {
        type filter hook forward priority filter; policy accept;
        # Forward policy ACCEPT is required for VM/LXC traffic.
        # Add specific forward rules here if inter-VM filtering is needed.
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}
EOF

info "Validating nftables configuration..."
if ! nft -c -f /etc/nftables-host.conf; then
    error "nftables-host.conf is invalid - investigate before proceeding."
    exit 1
fi

if ! nft -c -f /etc/nftables.conf; then
    error "nftables.conf is invalid - investigate before proceeding."
    exit 1
fi

systemctl enable nftables
systemctl restart nftables

if systemctl is-active --quiet nftables; then
    info "nftables firewall applied successfully."
else
    error "nftables service failed to start!"
    journalctl -u nftables -n 20 --no-pager
    exit 1
fi

# nftables logging via rsyslog
touch /var/log/nftables.log
tee /etc/rsyslog.d/30-nftables.conf >/dev/null <<'EOF'
:msg, contains, "DROP:" -/var/log/nftables.log
:msg, contains, "SSH-DENY:" -/var/log/nftables.log
:msg, contains, "SSH-BRUTE:" -/var/log/nftables.log
:msg, contains, "SMTP-RATELIMIT:" -/var/log/nftables.log
:msg, contains, "PROXMOX-DENY:" -/var/log/nftables.log
:msg, contains, "INVALID-STATE:" -/var/log/nftables.log
& stop
EOF

tee /etc/logrotate.d/nftables >/dev/null <<'EOF'
/var/log/nftables.log {
    daily
    missingok
    rotate 7
    compress
    delaycompress
    notifempty
    create 0640 root adm
    postrotate
        systemctl reload rsyslog >/dev/null 2>&1 || true
    endscript
}
EOF

systemctl restart rsyslog
info "nftables logging configured at /var/log/nftables.log"

# ------------------------------------------------------------------------
# 11. Final diagnostics
info "========================================="
info "Final Network Diagnostics:"
info "========================================="

info "Bridge status:"
ip addr show "${VM_BRIDGE}" || warn "Could not show ${VM_BRIDGE}"

info "IPv4 routes:"
ip route show || warn "Could not show IPv4 routes"

info "IPv6 routes:"
ip -6 route show || warn "Could not show IPv6 routes"

info "Current /etc/resolv.conf:"
cat /etc/resolv.conf || warn "Could not read /etc/resolv.conf"

info "IPv6 neighbour table (check for UDM SE link-local):"
ip -6 neigh show dev "${VM_BRIDGE}" | grep fe80 || warn "No fe80 neighbours found yet - may appear after first RA"

info "Listening services (SSH):"
ss -lntup | grep -E ':(22)\b' || warn "SSH not listening"

info "nftables ruleset (first 30 lines):"
nft list ruleset | head -n 30 || warn "Could not list nftables ruleset"

info "========================================="
info "Setup complete. Version 4.1.0"
info "Log saved to ${LOGFILE}"
info "========================================="
info ""
info "POST-INSTALL: Verify UDM SE link-local DNS address"
info "----------------------------------------------------"
info "Run the following to confirm the link-local address in use:"
info "  ip -6 neigh show dev ${VM_BRIDGE} | grep fe80"
info ""
info "If the address differs from '${UDM_SE_LINK_LOCAL}', update:"
info "  /etc/network/if-up.d/ipv6-dns  (GATEWAY_LL variable)"
info "Then re-trigger: ifdown ${VM_BRIDGE} && ifup ${VM_BRIDGE}"
info ""
info "To verify DNS injection worked:"
info "  cat /etc/resolv.conf"
info "  dig -6 @${UDM_SE_LINK_LOCAL}%${VM_BRIDGE} ${DNS_SEARCH_DOMAIN} AAAA"
info ""
info "Backups:"
info "  /etc/network/interfaces.bak.${BACKUP_TS}"
info "  ${SSHD_DIR}/backup-${BACKUP_TS}/"
info "========================================="

#Endscript

I have had no problem with this code and in relation to the names of the ethernet NIC's I adjust them on installation, I believe it's under advanced. Note you will need to adjust sections of this code to suit your hardware and needs.


LXC Configuration

My LXC's are based on Podman (the native repository version). You will definitely need to adjust this code to your needs as most people use docker. I do not for the following reasons

  • Podman does not use runc but crun and cgroups version 2
  • November crashed my systems, and I am not going down that road again
  • Podman does not use a daemon
  • It forced me to learn a completely new container system
  • Limited hardware - I cannot run up multiple LXCs even with Proxmox's new features
  • The base LXC is a Debian 13 container optimized for containers from Community scripts
  • You set it up when running the script on your server and can clone it as required
  • Additional scripts include - postfix, gpu and fail2ban
#!/bin/bash

# ============================================================
# Proxmox LXC Podman + Conditional Portainer CE Setup Script
# ============================================================
# Purpose:
#   - This script uses a Debian 13 LXC container
#   - Restructure the entire network 
#        - Change the proxy, flatten the Podman networks, reduce production servers
#        - Proxy on dedicated host - add the tunnel daemon later
#   - Completely build the firewall - switch to nftables - breaking change
#       - Switch to the trusted host model using nftables
#       - Requires the removal and purging of all iptables components and
#         carefully resetting nftables without touching podman created filters
#    - Manually creating the LXC out side community scripts broke the code
#       - Retest and fix the broken pieces.
#   - Remove all the unnecessary code
#
#   Version: 9.1.6
#
# Created: 14-11-2025
# Updated: 31-01-2026
# ============================================================

set -euo pipefail

# -----------------------------------------------------------------------
# Utility Functions
# -----------------------------------------------------------------------

info()  { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn()  { echo -e "\033[0;33m[WARN]\033[0m $*" >&2; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }

# Master host server - hosts Portainer CE - all other host agents
MASTER_HOST=baden

# Dynamic firewall ports configuration
configure_firewall_ports() {
    local ports=("$@")

    info "Opening firewall ports: ${ports[*]}"

    {
        echo ""
        echo "# Dynamically added ports"
        for port in "${ports[@]}"; do
            echo "tcp dport $port accept"
        done
    } >> /etc/nftables-dynamic-ports.conf

    if nft -f /etc/nftables.conf; then
        info "Firewall updated successfully for ports: ${ports[*]}"
    else
        error "Failed to reload nftables after adding ports."
        return 1
    fi
}

# Check to ensure the system is set to nftables only
set_alt() {
    local name="$1"
    local target="$2"

    if sudo update-alternatives --set "$name" "$target"; then
        info "Set $name → $target"
    else
        warn "Failed to set $name to $target — investigate alternatives state"
    fi
}

# -----------------------------------------------------------------------
# 1. Base Packages
# -----------------------------------------------------------------------
info "Installing base packages..."
apt-get update -qq || warn "apt-get update failed, check DNS/network"
apt-get install -y -qq \
    bind9-dnsutils rsyslog sudo curl gpg net-tools apt-transport-https cron mtr git openssh-server openssh-client \
    podman podman-docker podman-compose

info "Removing undesired packages..."
for pkg in ufw inetutils-telnet iptables iptables-persistent netfilter-persistent; do
    if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
        apt-get purge --auto-remove -y -qq "$pkg"
    fi
done


# -----------------------------------------------------------------------
# 2. Miscellaneous Configurations
# -----------------------------------------------------------------------
info "Configuring automatic updates via cron..."

# Ensure the cron job exists
( crontab -l 2>/dev/null || true; \
  echo "0 1 * * * apt-get update -qq && apt-get -y -qq upgrade && apt-get -y -qq autoremove && apt-get -y -qq autoclean" \
) | sort -u | crontab -

# Restart cron service depending on distro
if systemctl restart cron.service 2>/dev/null; then
  echo "Cron restarted (cron.service)"
elif systemctl restart crond.service 2>/dev/null; then
  echo "Cron restarted (crond.service)"
else
  echo "Cron restart failed" >&2
  exit 1
fi

# Harden the bash history
info "Harden the bash history - ensure nothing is lost..."
cat >/etc/profile.d/hardened-history.sh <<'EOF'
# Hardened Bash History
export HISTSIZE=10000
export HISTFILESIZE=100000
shopt -s histappend
export HISTTIMEFORMAT='%F %T '
PROMPT_COMMAND='history -a'
history -r
EOF

# Timezone configuration
info "Set the timezone manually to ensure consistency..."
timedatectl set-timezone Australia/Perth

# Ensure timezone is set in /etc/containers/containers.conf
CONF_FILE="/etc/containers/containers.conf"

# Create file if missing
if [ ! -f "$CONF_FILE" ]; then
    echo "[engine]" > "$CONF_FILE"
    echo 'tz = "Australia/Perth"' >> "$CONF_FILE"
else
    # Ensure [engine] section exists
    if ! grep -q "^

\[engine\]

" "$CONF_FILE"; then
        echo "[engine]" >> "$CONF_FILE"
    fi
    # Update or append tz line idempotently
    if grep -q "^tz" "$CONF_FILE"; then
        sed -i 's|^tz.*|tz = "Australia/Perth"|' "$CONF_FILE"
    else
        sed -i '/^

\[engine\]

/a tz = "Australia/Perth"' "$CONF_FILE"
    fi
fi


# Skip downloading extra APT languages
info "Configuring APT to skip downloading extra language files..."
echo 'Acquire::Languages "none";' | tee /etc/apt/apt.conf.d/99-disable-languages

# Force APT to use IPv4 to avoid potential issues
info "Configuring APT to use IPv4..."
echo 'Acquire::ForceIPv4 "true";' | tee /etc/apt/apt.conf.d/99force-ipv4

# -----------------------------------------------------------------------
# 3. Firewall configuration (nftables-native, Podman-safe)
# -----------------------------------------------------------------------

info "Configuring nftables firewall..."

# Create the nftables service file if missing
info "Create the dynamic nftables firewall file..."
touch /etc/nftables-dynamic-ports.conf || error "Failed to create /etc/nftables-dynamic-ports.conf"
chmod 644 /etc/nftables-dynamic-ports.conf || error "Failed to set permissions on /etc/nftables-dynamic-ports.conf"

info "Forcing the system to nftables - everything else to nft shm...."
set_alt iptables   /usr/sbin/iptables-nft
set_alt ip6tables  /usr/sbin/ip6tables-nft
set_alt arptables  /usr/sbin/arptables-nft
set_alt ebtables   /usr/sbin/ebtables-nft

# Host firewall rules (do NOT flush ruleset — Podman owns inet netavark)
cat <<'EOF' > /etc/nftables-host.conf
#!/usr/sbin/nft -f

# Create the table if it doesn't exist, then flush it
add table inet filter
flush table inet filter

table inet filter {

    chain input {
        type filter hook input priority filter; policy drop;

        # Loopback - use iifname for LXC compatibility
        iifname "lo" accept

        # Established / related connections (MUST be first)
        ct state established,related accept
        ct state invalid log prefix "INVALID-STATE: " drop

        # Trusted LAN hosts (only these can reach Baden)
        ip saddr {
            192.168.1.1,      # Router
            192.168.1.10,     # Proxmox
            192.168.1.15,     # Alex (reverse proxy)
            192.168.1.166,    # Management workstation 1
            192.168.1.252     # Management workstation 2
        } accept

        # Allow Podman container subnets to reach host services
        ip saddr 10.88.0.0/16 accept
        ip saddr 10.89.0.0/16 accept

        # DNS
        meta l4proto udp udp dport 53 accept
        meta l4proto tcp tcp dport 53 accept

        # DHCP client (LXC receiving from DHCP server)
        meta l4proto udp udp sport 67 udp dport 68 accept

        # mDNS
        meta l4proto udp udp dport 5353 accept

        # ICMP (v4 + v6)
        ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded } \
            limit rate 10/second accept
        meta l4proto ipv6-icmp accept

        # SSH from Proxmox only, with brute-force protection
        tcp dport 22 ip saddr 192.168.1.10 ct state new \
            limit rate 3/minute burst 5 packets accept
        tcp dport 22 ip saddr 192.168.1.10 ct state new \
            log prefix "SSH-BRUTE: " drop

        # Deny SSH from all other sources
        tcp dport 22 log prefix "SSH-DENY: " drop

        # --- Dynamic port rules (auto-managed) ---
        # Only include if file exists to prevent startup failure
        include "/etc/nftables-dynamic-ports.conf"

        # Generic rate-limited logging for other drops
        limit rate 5/minute burst 20 packets log prefix "DROP: "
    }

    chain forward {
        type filter hook forward priority filter; policy accept;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}
EOF

# Loader file — safe for Podman (NO flush ruleset)
cat <<'EOF' > /etc/nftables.conf
#!/usr/sbin/nft -f
include "/etc/nftables-host.conf"
EOF

info "Loading nftables ruleset..."
if ! nft -f /etc/nftables.conf; then
    error "Failed to load nftables ruleset; firewall not applied."
    exit 1
fi

systemctl enable --now nftables || error "nftables did not enable - please fix.."


# -----------------------------------------------------------------------
# 4. Firewall logging (rsyslog + logrotate)
# -----------------------------------------------------------------------

info "Configuring nftables logging..."

# Rsyslog routing for nftables log prefixes
cat <<'EOF' > /etc/rsyslog.d/nftables.conf
:msg, contains, "SSH-BRUTE"   -/var/log/nftables-dropped.log
:msg, contains, "SSH-DENY"    -/var/log/nftables-dropped.log
:msg, contains, "DROP:"       -/var/log/nftables-dropped.log
& stop
EOF

# Ensure log file exists with correct permissions
touch /var/log/nftables-dropped.log
chmod 0640 /var/log/nftables-dropped.log
chown root:adm /var/log/nftables-dropped.log

# Logrotate policy for nftables logs
cat <<'EOF' > /etc/logrotate.d/nftables
/var/log/nftables-dropped.log {
    daily
    missingok
    rotate 7
    compress
    delaycompress
    notifempty
    create 0640 root adm
    postrotate
        systemctl reload rsyslog >/dev/null 2>&1 || true
    endscript
}
EOF

# Restart rsyslog to apply new rules
systemctl restart rsyslog || warn "Failed to restart rsyslog"


# -----------------------------------------------------------------------
# 5. Sysctl configuration
# -----------------------------------------------------------------------
info "Configuring sysctl settings..."


# Enable IPv4 forwarding and RA acceptance on bridge
info "Applying IPv4 sysctl tuning..."
tee /etc/sysctl.d/99-ipv4-lxc.conf >/dev/null <<EOF
net.ipv4.ip_forward=0
EOF

# Enable IPv6 forwarding and RA acceptance on bridge
info "Applying IPv6 sysctl tuning..."
tee /etc/sysctl.d/99-ipv6-lxc.conf >/dev/null <<EOF
net.ipv6.conf.all.forwarding=0
net.ipv6.conf.eth0.accept_ra=2
EOF

sysctl --system || warn "Failed to apply IPv4/IPv6 sysctl settings"

warn "Some warning are normal due to the fact its a LXC...."

# -----------------------------------------------------------------------
# 6. Podman Registry Config & Network Setup
# -----------------------------------------------------------------------
info "Configuring Podman registries..."
mkdir -p /etc/containers/registries.conf.d
cat <<'EOF' > /etc/containers/registries.conf.d/99-unqualified.conf
# Managed by setup-lxc-script.sh (${BACKUP_TS})
unqualified-search-registries = ["docker.io", "quay.io", "ghcr.io", "registry.fedoraproject.org"]

[aliases]
# Map unqualified image names to fully qualified ones
"portainer/agent"      = "docker.io/portainer/agent"
"portainer/portainer-ce" = "docker.io/portainer/portainer-ce"
EOF

# Create Podman networks - modified.
info "Creating Podman network -"
podman network create app-net

# Restart the following services
systemctl start podman && systemctl enable podman
systemctl restart podman.socket  && systemctl enable podman.socket
# Enable Podman to restart all containers With Restart Policy Set To Always.
systemctl start podman-restart.service && systemctl enable podman-restart.service
systemctl restart podman.socket  && systemctl enable podman.socket
# Enable the auto update function that requires an additional label in the service stacks
systemctl enable podman-auto-update.service
systemctl enable podman-auto-update.timer
systemctl start podman-auto-update.timer
# Enable the clean function so that it runs on boot - there is no timer service
systemctl enable --now podman-clean-transient.service

# Ensure the cron job exists (deduplicated)
( crontab -l 2>/dev/null || true; \
  echo "0 21 * * * /usr/bin/systemctl start podman-clean-transient.service" \
) | sort -u | crontab -

# Restart cron service depending on distro
if systemctl restart cron.service 2>/dev/null; then
  echo "Cron restarted (cron.service)"
elif systemctl restart crond.service 2>/dev/null; then
  echo "Cron restarted (crond.service)"
else
  echo "Cron restart failed" >&2
  exit 1
fi


# -----------------------------------------------------------------------
# 7. Conditional Portainer Deployment
# -----------------------------------------------------------------------
HOST_SHORT=$(hostname -s || echo "unknown")
info "Detected short hostname: $HOST_SHORT"

if [[ "$HOST_SHORT" == "$MASTER_HOST" ]]; then
  info "Primary host — deploying Portainer CE..."

  # Ensure Podman does not reuse CE name
  podman rm -f portainer-ce >/dev/null 2>&1 || true

  # Build base create command as an array (safer than eval)
  podman create --name portainer-ce \
       --restart=always \
       --privileged \
       --no-healthcheck \
       -p 9000:9000 -p 9443:9443 \
       -v /run/podman/podman.sock:/var/run/docker.sock \
       -v portainer_data:/data \
       docker.io/portainer/portainer-ce:latest

  # Generate systemd unit
  podman generate systemd --name portainer-ce --files --new
  mv container-portainer-ce.service /etc/systemd/system/portainer-ce.service

  # Enable and start
  systemctl daemon-reexec && systemctl daemon-reload
  systemctl start portainer-ce.service && systemctl enable --now portainer-ce.service
  configure_firewall_ports 9000 9443
  info "Portainer CE deployed."

else
  info "Secondary host — deploying Portainer Agent (requires privileged)..."

  # Ensure Podman does not reuse CE name
  podman rm -f portainer-agent >/dev/null 2>&1 || true

  # Agent requires access to the container runtime socket and listens on 9001 - requires privileged
  podman create --name portainer-agent \
  --restart=always \
  --privileged \
  --no-healthcheck \
  -p 9001:9001 \
  -v /run/podman/podman.sock:/var/run/docker.sock \
  -v portainer_agent_data:/var/lib/docker/volumes \
  -v /:/host \
  docker.io/portainer/agent:latest


  # Generate systemd unit
  podman generate systemd --name portainer-agent --files --new
  mv container-portainer-agent.service /etc/systemd/system/portainer-agent.service

  # Enable and start
  systemctl daemon-reexec && systemctl daemon-reload
  systemctl start portainer-agent.service && systemctl enable --now portainer-agent.service
  configure_firewall_ports 9001
  info "Portainer Agent deployed."
fi


# -----------------------------------------------------------------------
# 8. SSH Configuration Hardening
# -----------------------------------------------------------------------

info "Hardening SSH configuration..."

BACKUP_TS=$(date +"%Y%m%d-%H%M%S")
SSHD_DIR="/etc/ssh"
SSHD_CFG="${SSHD_DIR}/sshd_config"
SSHD_CUSTOM_DIR="${SSHD_DIR}/sshd_config.d"
SSHD_CUSTOM_CFG="${SSHD_CUSTOM_DIR}/99-hardened-custom.conf"
BACKUP_DIR="${SSHD_DIR}/backup-${BACKUP_TS}"

mkdir -p "${BACKUP_DIR}"
mkdir -p "${SSHD_CUSTOM_DIR}"

info "Backing up sshd_config and host keys..."
cp -a "${SSHD_CFG}" "${BACKUP_DIR}/sshd_config.bak" || true
for key in ssh_host_ed25519_key ssh_host_rsa_key ssh_host_ecdsa_key; do
    [[ -f "${SSHD_DIR}/${key}" ]] && mv "${SSHD_DIR}/${key}" "${BACKUP_DIR}/" || true
    [[ -f "${SSHD_DIR}/${key}.pub" ]] && mv "${SSHD_DIR}/${key}.pub" "${BACKUP_DIR}/" || true
done

info "Deleting old SSH host keys..."
rm -f /etc/ssh/ssh_host_*

info "Generating new ED25519 host key..."
ssh-keygen -t ed25519 -f "${SSHD_DIR}/ssh_host_ed25519_key" -N "" -o -a 100

info "Generating new RSA host key..."
ssh-keygen -t rsa -b 4096 -f "${SSHD_DIR}/ssh_host_rsa_key" -N "" -o

chmod 600 "${SSHD_DIR}/ssh_host_ed25519_key" "${SSHD_DIR}/ssh_host_rsa_key"
chmod 644 "${SSHD_DIR}/ssh_host_ed25519_key.pub" "${SSHD_DIR}/ssh_host_rsa_key.pub"

info "Ensuring main sshd_config includes custom configs..."

# Check if Include directive exists and add if missing
if ! grep -q "^Include ${SSHD_CUSTOM_DIR}/\*.conf" "${SSHD_CFG}"; then
    # Add Include directive at the top of the config file
    sed -i "1i Include ${SSHD_CUSTOM_DIR}/*.conf" "${SSHD_CFG}"
    info "Added Include directive to ${SSHD_CFG}"
fi

info "Writing hardened SSH configuration to ${SSHD_CUSTOM_CFG}..."

tee "${SSHD_CUSTOM_CFG}" >/dev/null <<EOF
# Hardened SSH Configuration
# Managed by base-setup script (${BACKUP_TS})
# This file is in sshd_config.d/ and will survive system updates

Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
HostbasedAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
LoginGraceTime 30
MaxAuthTries 4
MaxSessions 10

HostKey ${SSHD_DIR}/ssh_host_ed25519_key
HostKey ${SSHD_DIR}/ssh_host_rsa_key

AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
UseDNS no
PrintMotd no
PrintLastLog yes
AcceptEnv LANG LC_*

# Permit root password auth only from RFC1918 and link-local IPv6
Match Address 10.0.0.0/8,192.168.0.0/16,fe80::/10
  PermitRootLogin yes
  PasswordAuthentication yes
EOF

# Ensure the custom config has appropriate permissions
chmod 644 "${SSHD_CUSTOM_CFG}"

info "Validating sshd_config..."
if sshd -t; then
    info "sshd_config syntax OK, restarting ssh..."
    if systemctl restart ssh || systemctl restart sshd; then
        info "sshd restarted successfully."
    else
        error "systemctl restart ssh failed - check service status."
    fi
else
    error "sshd_config test failed - not restarting ssh."
    error "Backup available at: ${BACKUP_DIR}/sshd_config.bak"
fi

# -----------------------------------------------------------------------
# 9. Final Notes
# -----------------------------------------------------------------------
info "  Deployment complete."
info "  Portainer stacks still need watchtower"
info "  Portainer and agents will auto update"

warn "  Please run the LXC-GPU code post reboot after this script has completed."
warn "  Please ensure that the LXC container configuration file meets prototype kept on file"

warn "  Please reboot the system to ensure all settings take full effect."

This should get you up and running but as above you will definitely need to edit this script depending on your container system, the SSH rules, the trusted hosts and so forth.

I have proofread this multiple times - and found additional errors - fixed
I have rebooted multiple times and found no major issues
I have port scanned multiple times.


The proxy configuration has been shared under another post located here

Zoraxy Reverse Proxy Setup
This script has been created to automate the process of creating the proxy Learnings: * Debian 13 uses nftables by default so I made a mess of it using iptables * Backup the LXC * Undo all the incorrect work and rebuild it using nftables * Test the code again * Put a reworking note

Any how sharing is caring and this might help someone

#enoughsaid