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


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

# 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
}

# -----------------------------------------------------------------------
# 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..."
info "Create the dynamic nftables firewall file..."
touch /etc/nftables-dynamic-ports.conf

# 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.0.0/16 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

        # --- Dynamic port rules (auto-managed) ---
        include "/etc/nftables-dynamic-ports.conf"

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

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

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

# Generate a new SSH Key - required due to inconsistence in LXC creation
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..."
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 /etc/ssh/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 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.


LXC Configuration - Docker

The result of further testing. Added as an option

#!/bin/bash

# ============================================================
# Proxmox LXC Docker + Docker Compose
# ============================================================
# Purpose:
#   - This script uses a Debian 13 LXC container
#   - Restructure the entire network 
#        - Docker deployment
#        - Proxy on dedicated host
#   - Completely build the firewall - switch to nftables
#       - 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: 1.0.4
# Created: 04-01-2026
# Updated: 04-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; }


# 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
}



# -----------------------------------------------------------------------
# 1. Base Packages
# -----------------------------------------------------------------------
info "Installing base packages - excluding docker respositories..."
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

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

# 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, Docker-aware)
# -----------------------------------------------------------------------

info "Configuring nftables firewall..."
info "Create the dynamic nftables firewall file..."
touch /etc/nftables-dynamic-ports.conf

# Host firewall rules (Docker-aware; does not interfere with iptables-nft rules)
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
        ip saddr {
            192.168.1.1,
            192.168.1.10,
            192.168.1.15,
            192.168.1.166,
            192.168.1.252
        } accept

        # Allow Docker container subnets
        ip saddr 172.16.0.0/12 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

        # SSH brute-force protection (Proxmox only)
        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 others
        tcp dport 22 log prefix "SSH-DENY: " level info
        tcp dport 22 drop

        # --- Dynamic port rules (auto-managed) ---
        include "/etc/nftables-dynamic-ports.conf"

        # Generic rate-limited logging
        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 Docker/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..."

# 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. Docker Engine + Compose Installation & Validation
# -----------------------------------------------------------------------
info "Installing Docker Engine and Docker Compose..."

# Install prerequisites
apt-get update -y
apt-get install -y ca-certificates curl gnupg lsb-release

# Create keyring directory
install -m 0755 -d /etc/apt/keyrings

# Install Docker GPG key
if curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg \
    | gpg --dearmor -o /etc/apt/keyrings/docker.gpg; then
    info "Docker GPG key installed."
else
    error "Failed to install Docker GPG key."
    exit 1
fi

chmod a+r /etc/apt/keyrings/docker.gpg

# Add Docker repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
  $(lsb_release -cs) stable" \
  > /etc/apt/sources.list.d/docker.list

apt-get update -y

# Install Docker Engine + CLI + Compose plugin
if apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then
    info "Docker Engine and Docker Compose installed."
else
    error "Docker installation failed."
    exit 1
fi

# -----------------------------------------------------------------------
# Docker Diagnostics

info "Running Docker diagnostics..."

DOCKER_OK=true

if ! systemctl enable --now docker; then
    warn "Docker service failed to start."
    DOCKER_OK=false
fi

if ! docker info >/dev/null 2>&1; then
    warn "Docker daemon not responding to 'docker info'."
    DOCKER_OK=false
else
    info "Docker daemon is responding."
fi

if ! docker run --rm hello-world >/dev/null 2>&1; then
    warn "Docker test container failed to run."
    DOCKER_OK=false
else
    info "Docker test container executed successfully."
fi

if [ "$DOCKER_OK" = false ]; then
    error "Docker diagnostics reported issues. Review logs before continuing."
else
    info "Docker diagnostics passed."
fi

info "Create the base docker network bridge ..."
docker network create \
  --driver bridge \
  --subnet 172.20.0.0/16 \
  --gateway 172.20.0.1 \
  --ipv6=false \
  app-net


# -----------------------------------------------------------------------
# Portainer Agent Deployment (Docker)

info "Deploying Portainer Agent..."

# Remove any existing container
docker rm -f portainer-agent >/dev/null 2>&1 || true

# Create Portainer Agent container
if docker create \
    --name portainer-agent \
    --restart=always \
    --privileged \
    -p 9001:9001 \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /:/host \
    -v portainer_agent_data:/var/lib/docker/volumes \
    portainer/agent:latest; then
    info "Portainer Agent container created."
else
    error "Failed to create Portainer Agent container."
    exit 1
fi

# Generate systemd unit
docker run --rm \
    -v /etc/systemd/system:/systemd \
    portainer/agent:latest \
    >/dev/null 2>&1 || true

# Manual systemd creation (Docker does not auto-generate like Podman)
cat <<EOF >/etc/systemd/system/portainer-agent.service
[Unit]
Description=Portainer Agent
After=docker.service
Requires=docker.service

[Service]
Restart=always
ExecStart=/usr/bin/docker start -a portainer-agent
ExecStop=/usr/bin/docker stop -t 10 portainer-agent

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now portainer-agent.service

# Firewall rule (function assumed to exist)
configure_firewall_ports 9001

info "Portainer Agent deployed successfully."


# -----------------------------------------------------------------------
# 7. 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 "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..."
cat <<'EOF' > "${SSHD_CFG}"
# Managed by setup-lxc-v6.sh

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

HostKey /etc/ssh/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


# -----------------------------------------------------------------------
# 8. Final Notes
# -----------------------------------------------------------------------
info "  Deployment complete."
info "  Docker Engine and compose deployed..."

warn "  Please run the LXC-GPU code post reboot after this script has completed."

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

I do not use Docker, so I am not spending any more time on this script.
If you do use Docker your attention is drawn to the following

Docker with nftables
How Docker works with nftables

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