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