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