Zoraxy Reverse Proxy Setup

Zoraxy Reverse Proxy Setup
Zoraxy Reverse Proxy

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 in my Todo list to change all code to use nftables
  • Port scan from mobile device passed

Here is the code for your learning. Hopefully it's a little better than previous work

#!/bin/bash
# ====================================================================
# Proxmox LXC Zoraxy Proxy Setup (nftables-native)
# ====================================================================
# Purpose:
#    - Configure a Debian 13 LXC as a Zoraxy reverse proxy
#    - Use nftables as the ONLY firewall mechanism (no iptables)
#    - Enforce inbound policy with:
#         * Drop-by-default on INPUT
#         * SSH restricted to a single admin IP with brute-force protection
#         * Basic HTTP(S)/admin DoS protection
#         * Centralised firewall logging via rsyslog + logrotate
#
# Notes:
#    - Assumes this LXC is a reverse proxy, not a router
#    - No Podman/LXC container chaining on this host (pure proxy role)
#
# Version: 1.1.6
# Created: 26-12-2025
# Updated: 12-12-2026
# ====================================================================

set -euo pipefail

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

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

# -----------------------------------------------------------------------
# 2. 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 \
    openssh-server openssh-client smartmontools nftables

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

# Ensure nftables service is enabled
info "Enabling nftables service..."
systemctl enable --now nftables.service || warn "Failed to enable/start nftables.service"

# -----------------------------------------------------------------------
# 3. Miscellaneous configurations
# -----------------------------------------------------------------------
info "Configuring automatic updates via cron..."

( 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 -

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

# 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

# Harden bash records - survive across sessions - crashes
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


# -----------------------------------------------------------------------
# 4. Firewall configuration (nftables-native)
# -----------------------------------------------------------------------

info "Configuring nftables firewall..."
info "Create the dynamic nftables firewall file..."
touch /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

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

# Only flush your own filter table, not the entire ruleset.
add table inet filter
flush table inet filter

table inet filter {

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

        # Loopback
        iif "lo" accept

        # Established / related
        ct state established,related 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 from trusted admin IP 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
        tcp dport 22 log prefix "SSH-DENY: " level info
        tcp dport 22 drop

        # HTTP/HTTPS/Zoraxy admin with basic DoS protection
        tcp dport {80, 81, 443, 8890} ct state new limit rate 50/second burst 200 packets accept
        tcp dport {80, 81, 443, 8890} log prefix "DOS-DROP: " level info
        tcp dport {80, 81, 443, 8890} 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 drop;
    }

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

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

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


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

info "Configuring nftables logging..."

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, "DOS-DROP"  -/var/log/nftables-dropped.log
:msg, contains, "DROP:"     -/var/log/nftables-dropped.log
& stop
EOF

touch /var/log/nftables-dropped.log

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

systemctl restart rsyslog || warn "Failed to restart rsyslog"

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

# -----------------------------------------------------------------------
# 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..."
cp -a "${SSHD_CFG}" "${BACKUP_DIR}/sshd_config.bak" || true

info "Writing hardened sshd_config..."
cat <<EOF > "${SSHD_CFG}"
# Managed by setup-proxy-lxc.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_*

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. Install the Zoraxy proxy
# -----------------------------------------------------------------------

INSTALL_DIR="/srv/zoraxy"
BIN_PATH="$INSTALL_DIR/zoraxy"
SERVICE_FILE="/etc/systemd/system/zoraxy.service"
ZORAXY_URL="https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64"

info "Creating install directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"

info "Downloading Zoraxy binary..."
curl -L "$ZORAXY_URL" -o "$BIN_PATH"
chmod +x "$BIN_PATH"

info "Creating Zoraxy start script..."
cat <<EOF > "$INSTALL_DIR/start.sh"
#!/bin/bash
"$BIN_PATH" -port=:81
EOF

chmod +x "$INSTALL_DIR/start.sh"

info "Creating Zoraxy systemd service..."
cat <<EOF > "$SERVICE_FILE"
[Unit]
Description=Zoraxy Reverse Proxy
After=network.target

[Service]
Type=simple
User=root
Group=root
ExecStart=$INSTALL_DIR/start.sh
WorkingDirectory=$INSTALL_DIR
Restart=on-failure
StandardOutput=journal
StandardError=journal
SyslogIdentifier=zoraxy

[Install]
WantedBy=multi-user.target
EOF

info "Enabling and starting Zoraxy service..."
systemctl daemon-reload  || error "systemd failed to reload"
systemctl enable zoraxy || warn "Zoraxy service failed to enable"
systemctl start zoraxy  || warn "Zoraxy service failed to start"

info "Zoraxy installation complete."
info "Manage the service with: systemctl [status|start|stop|restart] zoraxy"


# -----------------------------------------------------------------------
# 9. Install Cloudflared Tunnel
# -----------------------------------------------------------------------

info " installing Cloudflared Tunnel..."

mkdir -p /usr/local/bin
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
  -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared || warn "Failed to install Cloudflared tunnel..."

cloudflared --version

warn " You need to run the initialisation command ...."


# -----------------------------------------------------------------------
# 10. Final notes
# -----------------------------------------------------------------------
info "Zoraxy proxy deployed on host."
info "Verify nftables ruleset with: nft list ruleset"
info "Review /var/log/iptables-dropped.log for firewall events."
info "CLoudflared tunnel installed..."

Hope this helps someone

Significant headway has been made on the nftables "trusted host model" nftables rules for the Podman servers. It will take time to test them.

Basically the idea is that only the router, Proxmox server, proxy and management devices need to talk to the application LXC's.

Everyone else has to go through the proxy.

Tunnel code is missing as I am testing it, and finalizing my thoughts. I will use Cloudflares tunnel and run it as a systemd. Initial testing has worked.

Probably going to pull other posts and start a clean one on my systems.

Live and learn

#enoughsaid