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.

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

#!/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 a clean vmbr0 bridge configuration
#   • Enables IPv4/IPv6 forwarding and applies sysctl hardening
#   • Installs and configures dhcpcd for stable IPv6 RA handling
#   • 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
#
# Notes:
#   • 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
#
# Version 9.2.0
#
# Code review date: 2025-12-30
# ========================================================================

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)"

# ------------------------------------------------------------------------
# Miscellaneous Configuration

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

# Install necessary packages prior to network changes
info "Installing additional packages - adjust as required..."
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 || error "Package installation failed"

# Remove packages as necessary prior to network changes
info "Removing packages - adjust as required..."
for pkg in ufw inetutils-telnet; 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

# Create backups of sysctl
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}

# 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

# Configure 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 settings"

# Increase 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
sysctl --system || warn "Failed to apply sysctl settings"

# Harden bash records - survive across sessions - crashes
info "Reconfiguring bash records..."
export HISTSIZE=10000
export HISTFILESIZE=100000
shopt -s histappend
export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"


# ------------------------------------------------------------------------
# Define primary NIC explicitly
PRIMARY_NIC="eth0"
VM_BRIDGE="vmbr0"

# ------------------------------------------------------------------------
# 1. 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


tee /etc/network/interfaces >/dev/null <<EOF
# Managed by setup-pxe-script.sh (${BACKUP_TS})

auto lo
iface lo inet loopback

iface ${PRIMARY_NIC} inet manual

auto ${VM_BRIDGE}
iface ${VM_BRIDGE} inet dhcp
    bridge-ports ${PRIMARY_NIC}
    bridge-stp on
    bridge-fd 0
    dns-nameservers 192.168.1.1
    dns-search braedach.com

iface ${VM_BRIDGE} inet6 auto
    accept_ra 2
    autoconf 1
    privext 0
    dns-search braedach.com
EOF

info "Interfaces file written."

# ------------------------------------------------------------------------
# 2. dhcpcd setup
info "Ensuring dhcpcd installed..."
apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y dhcpcd5 || true

info "Writing /etc/dhcpcd.conf..."
tee /etc/dhcpcd.conf >/dev/null <<EOF
# Managed by setup-pxe-script.sh (${BACKUP_TS})
denyinterfaces ${PRIMARY_NIC}
interface ${VM_BRIDGE}
ipv6rs
hostname $(hostname)
clientid
# Required to forward the DNS settings to LXC containers
static domain_name_servers=192.168.1.1 
static domain_name_servers=fe80::f6e2:c6ff:feee:63e3
static domain_search=braedach.com
EOF

systemctl enable dhcpcd
systemctl restart dhcpcd

# ------------------------------------------------------------------------
# 3. Network sysctl tuning

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

# Enable IPv6 forwarding and RA acceptance on bridge
info "Applying IPv6 sysctl tuning..."
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

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

# ------------------------------------------------------------------------
# 4. Bring up bridge
info "Bringing up ${VM_BRIDGE}..."
if ip link set "${VM_BRIDGE}" up; then
  info "${VM_BRIDGE} brought up successfully."
else
  warn "Failed to bring up ${VM_BRIDGE} — check interface definition."
fi

info "Restarting networking service..."
if systemctl restart networking; then
  info "Networking service restarted."
else
  warn "Networking restart failed — continuing, but verify manually."
fi


# ------------------------------------------------------------------------
# 5. SSH regeneration and hardening
SSHD_DIR="/etc/ssh"
SSHD_CFG="${SSHD_DIR}/sshd_config"
BACKUP_DIR="${SSHD_DIR}/backup-${BACKUP_TS}"
mkdir -p "${BACKUP_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; do
  [[ -f "${SSHD_DIR}/${key}" ]] && mv "${SSHD_DIR}/${key}" "${BACKUP_DIR}/"
  [[ -f "${SSHD_DIR}/${key}.pub" ]] && mv "${SSHD_DIR}/${key}.pub" "${BACKUP_DIR}/"
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-4096 host key - not used - can be deleted..."
ssh-keygen -t rsa -b 4096 -f "${SSHD_DIR}/ssh_host_rsa_key" -N "" -o -a 100

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 "Writing hardened sshd_config..."

tee ${SSHD_CFG} >/dev/null <<EOF
# Managed by setup-pxe-script.sh (${BACKUP_TS})

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

HostKey ${SSHD_DIR}/ssh_host_ed25519_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
  AuthenticationMethods any
EOF

info "Validating sshd_config..."
if sshd -t; then
  info "sshd_config syntax OK, restarting ssh..."
  if systemctl restart ssh; then
    info "sshd restarted successfully."
  else
    error "systemctl restart ssh failed — check service status."
  fi
else
  error "sshd_config test failed — not restarting ssh."
fi


# ------------------------------------------------------------------------
# 6. Configure firewall - nftables only (Proxmox-safe)

info "Configuring nftables firewall..."

# Ensure nftables is installed and enabled
apt-get update -y
apt-get install -y nftables
systemctl enable --now nftables

# Disable Proxmox firewall (we use nftables instead)
pve-firewall stop || true
systemctl disable pve-firewall || true

# Ensure no iptables persistence is active
systemctl disable --now netfilter-persistent 2>/dev/null || true

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

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

table inet filter {

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

        # Loopback
        iif "lo" accept

        # Established / related
        ct state established,related accept

        # SMTP relay (Mailgun)
        tcp dport 25 accept

        # Proxmox GUI (8006)
        tcp dport 8006 ip saddr 192.168.0.0/16 accept
        tcp dport 8006 ip6 saddr fe80::/10 accept
        tcp dport 8006 ip6 saddr fc00::/7 accept

        # Trusted LAN hosts (full access)
        ip saddr {
            192.168.1.1,
            192.168.1.15,
            192.168.1.166,
            192.168.1.252
        } accept

        # DNS
        udp dport 53 accept
        tcp dport 53 accept

        # DHCP (IPv4 + IPv6)
        udp sport 67 udp dport 68 accept
        udp sport 68 udp dport 67 accept
        udp dport 546 accept

        # mDNS
        udp dport 5353 accept

        # ICMP (v4 + v6)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # SSH (management only)
        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 } \
            log prefix "SSH-BRUTE: " level info
        tcp dport 22 ip saddr { 192.168.1.166, 192.168.1.252 } drop

        # Deny SSH from all others
        tcp dport 22 log prefix "SSH-DENY: " level info
        tcp dport 22 drop

        # Generic logging for other drops
        limit rate 5/minute burst 20 packets log prefix "DROP: " level info

        drop
    }

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

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

# Reload nftables
nft -f /etc/nftables.conf || error " nftables code block invalid - investigate..."
systemctl restart nftables

info "nftables firewall applied."

# Configure nftables logging
touch /var/log/nftables.log
cat <<'EOF' >/etc/rsyslog.d/30-nftables.conf
:msg, contains, "DROP:" -/var/log/nftables.log
& stop
EOF

# Log rotation
cat <<'EOF' >/etc/logrotate.d/nftables
/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."


# ------------------------------------------------------------------------
# 8. Final diagnostics
info "Diagnostics:"
ip addr show "${VM_BRIDGE}" || true
ip route show || true
ip -6 route show || true
ss -lntup | grep -E ':(22)\b' || true

info "✅ Network + SSH setup complete. Log saved to ${LOGFILE}"

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
#   - Remove all the unnecessary code
#
#   Version: 9.0.1
#
# Created: 14-11-2025
# Updated: 29-12-2025
# ============================================================

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

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-host.conf

    # Reload nftables safely (NO flush, NO interference with Podman)
    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
}


# -----------------------------------------------------------------------
# 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 \
    smartmontools 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..."
export HISTSIZE=50000
export HISTFILESIZE=100000
shopt -s histappend
export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"


# 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

# Harden bash records - survive across sessions - crashes
info "Reconfiguring bash records..."
export HISTSIZE=10000
export HISTFILESIZE=100000
shopt -s histappend
export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"


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

info "Configuring nftables firewall..."

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

table inet filter {

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

        # Loopback
        iif "lo" accept

        # Established / related
        ct state established,related accept

        # 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

        # DNS
        udp dport 53 accept
        tcp dport 53 accept

        # DHCP client
        udp sport 67 udp dport 68 accept
        udp sport 68 udp dport 67 accept

        # mDNS
        udp dport 5353 accept

        # ICMP (v4 + v6)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Allow Podman container subnets to reach host
        ip saddr 10.88.0.0/16 accept
        ip saddr 10.89.1.0/24 accept
        ip saddr 10.89.2.0/24 accept
        ip saddr 10.89.3.0/24 accept
        ip saddr 10.89.4.0/24 accept
        ip saddr 10.89.5.0/24 accept
        ip saddr 10.89.6.0/24 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 log prefix "SSH-BRUTE: " level info
        tcp dport 22 ip saddr 192.168.1.10 drop

        # Deny SSH from all other sources (should be redundant due to trusted hosts rule, but explicit)
        tcp dport 22 log prefix "SSH-DENY: " level info
        tcp dport 22 drop

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

        drop
    }

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

    chain output {
        type filter hook output priority 0; 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


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

info "Configuring nftables logging..."

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

touch /var/log/iptables-dropped.log

cat <<'EOF' > /etc/logrotate.d/nftables
/var/log/iptables-dropped.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 || 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..."

SSHD_DIR="/etc/ssh"
SSHD_CFG="${SSHD_DIR}/sshd_config"
BACKUP_TS="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="${SSHD_DIR}/backup-${BACKUP_TS}"
mkdir -p "${BACKUP_DIR}"

info "Backing up sshd_config and host keys..."
cp -a "${SSHD_CFG}" "${BACKUP_DIR}/sshd_config.bak" || true

info "Writing hardened sshd_config..."
cat <<'EOF' > "${SSHD_CFG}"
# Managed by setup-lxc-v6.sh (${BACKUP_TS})

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

HostKey ${SSHD_DIR}/ssh_host_ed25519_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
  AuthenticationMethods any
EOF

info "Validating sshd_config..."
if sshd -t; then
  info "sshd_config syntax OK, restarting ssh..."
  if systemctl restart ssh; then
    info "sshd restarted successfully."
  else
    error "systemctl restart ssh failed — check service status."
  fi
else
  error "sshd_config test failed — not restarting ssh."
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 proof read this multiple times, but omissions may still occur
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