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.0
# Created: 26-12-2025
# Updated: 29-12-2025
# ====================================================================

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

# -----------------------------------------------------------------------
# 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 git \
    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

info "Setting timezone..."
timedatectl set-timezone Australia/Perth

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

info "Configuring nftables firewall..."

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

flush ruleset

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

# -----------------------------------------------------------------------
# 5. 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, "DOS-DROP"  -/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"

# -----------------------------------------------------------------------
# 6. Sysctl configuration
# -----------------------------------------------------------------------

info "Configuring sysctl settings..."

cat <<'EOF' >/etc/sysctl.d/99-lxc-ipv6.conf
# Global forwarding (disabled for reverse proxy LXC)
net.ipv4.ip_forward=0
net.ipv6.conf.all.forwarding=0

# External NIC (eth0): allow RA if needed
net.ipv6.conf.eth0.accept_ra=2
EOF

sysctl --system || warn "sysctl --system reported an error"

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

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