Securing Your FS PBX Server
If you’ve been following Nerd Vittles for the past couple decades, then you know that we are sticklers when it comes to security. So bear with us while we add a few extra layers of security to FS PBX. Log back into your server as root using SSH. First, we need to change the default port for future SSH access. Edit /etc/ssh/sshd_config. Uncomment the Port line and enter a new port number. A good choice might be the year you were born. Then save the file. Don’t restart the SSH service just yet, or you will lock yourself out of your server.
Next, edit /etc/iptables/rules.v4. Scroll down to the line containing –dport 22 and duplicate the line. Change the number in the duplicated line to the port number you assigned for future SSH access. Then SAVE the file and reboot your server.
Log back in: ssh -p 1234 root@fspbx.yourdomain.com where 1234 is the port you assigned for SSH access. Assuming you got back in, edit /etc/iptables/rules.v4 again and comment out or delete the –dport 22 line. Save the file and restart IPtables: systemctl restart iptables.
Now we’re ready to add a few layers of protection for the FS PBX web interface. We’ll be using two free services: VoIP Blacklist and APIban. These two services implement a VoIP blacklist of over 100,000 bad guys, and we have found the listings block virtually all attacks on FS PBX which, by design, exposes its SIP ports on the public internet.
You will need an APIkey for APIban which you can obtain here. Once you have your APIkey, issue the following commands:
cd /
apt install ipset -y
wget https://filedn.com/lBgbGypMOdDm8PWOoOiBR7j/FusionPBX/ipset-additions.tar.gz
tar zxvf ipset-additions.tar.gz
# insert your APIkey when the text editor opens below & save the file
nano -w /usr/local/sbin/apiban-init
chmod +x /etc/rc.local
echo "2 0 * * * root rm -f /var/log/*.gz >/dev/null 2>&1" >> /etc/crontab
echo "2 5 * * * root rm -f /var/log/*.1 >/dev/null 2>&1" >> /etc/crontab
echo "9 5 * * * root rm -f /var/log/freeswitch/*.1 >/dev/null 2>&1" >> /etc/crontab
echo "3 */6 * * * root /usr/local/sbin/iptables-restart >/dev/null 2>&1" >> /etc/crontab
reboot
Depending upon the amount of RAM available in your new server, IPtables may or may not restart after you reboot. You can check it by logging in as root and issuing the command: iptables -nL. If you see the following entries as the first five in your INPUT chain, all is well and you can move on to the next section:
ACCEPT 0 -- 0.0.0.0/0 0.0.0.0/0
ACCEPT 0 -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
ACCEPT 0 -- 10.8.0.0/24 0.0.0.0/0
DROP 0 -- 0.0.0.0/0 0.0.0.0/0 match-set voipbl src
DROP 0 -- 0.0.0.0/0 0.0.0.0/0 match-set apiban src
If you see a bunch of DROP entries in the iptables listing, then the likely culprit is that /etc/rc.local didn’t get executed when you rebooted. You can fix this with the following commands:
cd /etc/systemd/system
rm -f rc-local.service
ln -s /lib/systemd/system/rc-local.service rc-local.service
reboot
After rebooting, issue the iptables -nL command again and check for the five entries shown above. If they’re there, you’re done. If the series of DROP commands are also missing, then we need to address the memory constraints of your server. Edit /etc/rc.local and replace the existing contents with the following:
#!/bin/sh -e
/usr/local/sbin/apiban-init
#/usr/local/sbin/voipbl-init
systemctl restart iptables
systemctl restart ip6tables
systemctl restart fail2ban
/usr/sbin/iptables -I INPUT -m set --match-set apiban src -j DROP
#/usr/sbin/iptables -I INPUT -m set --match-set voipbl src -j DROP
/usr/sbin/iptables -I INPUT -s 10.8.0.0/24 -j ACCEPT
/usr/sbin/iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
/usr/sbin/iptables -I INPUT -i lo -j ACCEPT
exit 0
When you issue the iptables -nL command now, you should see only four entries at the top of the INPUT chain:
ACCEPT 0 -- 0.0.0.0/0 0.0.0.0/0
ACCEPT 0 -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
ACCEPT 0 -- 10.8.0.0/24 0.0.0.0/0
DROP 0 -- 0.0.0.0/0 0.0.0.0/0 match-set apiban src
The VoIP Blacklist IPset will be run as a cron job during the day to add the necessary protection for FS PBX.
Instructions:
1. Create the update script
Create a script at /usr/local/bin/update-dynamic-whitelist.sh:
2. Insert this code into the script:
#!/bin/bash
# /usr/local/bin/update-dynamic-whitelist.sh
# Dynamic full-access whitelist for multiple FQDNs (nftables fixed)
set -euo pipefail
# ================== CONFIGURATION ==================
FQDNS=(
"ddns1.ddns.net"
"ddns2.ddns.net"
)
# ====^ SET Your Dynamic DNS FQDN's above ^. You can add as many as needed ====
COMMENT="Dynamic FQDN full access"
STATE_DIR="/var/cache/dynamic-whitelist"
LOGFILE="/var/log/dynamic-whitelist.log"
# ===================================================
mkdir -p "$STATE_DIR"
# ================== HELPER FUNCTIONS ==================
init_nftables() {
local nft_cmd="${NFT_CMD:-$(command -v /usr/sbin/nft || command -v nft)}"
if sudo "$nft_cmd" list table inet dynamic_whitelist >/dev/null 2>&1; then
return 0
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') Initializing nftables dynamic whitelist table..." | tee -a "$LOGFILE"
sudo "$nft_cmd" -f - <<EOF
table inet dynamic_whitelist {
chain input {
type filter hook input priority 10; policy accept;
# Dynamic rules will be added here
}
}
EOF
}
update_fqdn() {
local fqdn="$1"
local state_file="${STATE_DIR}/last-ip-$(echo "$fqdn" | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]-.' '_').txt"
local current_ip
current_ip=$(dig +short A "$fqdn" | tail -n1)
if [[ -z "$current_ip" ]] || ! [[ $current_ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: Failed to resolve $fqdn" | tee -a "$LOGFILE"
return 1
fi
local prev_ip=""
if [[ -f "$state_file" ]]; then
prev_ip=$(cat "$state_file")
fi
if [[ "$current_ip" == "$prev_ip" ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') No change: $fqdn is still $current_ip" | tee -a "$LOGFILE"
return 0
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') IP changed for $fqdn: ${prev_ip:-none} → $current_ip" | tee -a "$LOGFILE"
# Detect firewall (improved for Debian 13 + nftables)
if [[ -z "${FIREWALL:-}" ]]; then
if command -v ufw >/dev/null 2>&1; then
FIREWALL="ufw"
elif command -v /usr/sbin/nft >/dev/null 2>&1 || command -v nft >/dev/null 2>&1; then
FIREWALL="nft"
NFT_CMD=$(command -v /usr/sbin/nft || command -v nft)
init_nftables
elif command -v /usr/sbin/iptables >/dev/null 2>&1 || command -v iptables >/dev/null 2>&1; then
FIREWALL="iptables"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: No supported firewall found" | tee -a "$LOGFILE"
echo " Hint: Run 'sudo apt install nftables' on Debian 13" | tee -a "$LOGFILE"
return 1
fi
fi
# Remove old rule if IP changed
if [[ -n "$prev_ip" && "$prev_ip" != "$current_ip" ]]; then
case $FIREWALL in
ufw)
sudo ufw delete allow from "$prev_ip" to any comment "$COMMENT" 2>/dev/null || true
;;
nft)
# Delete any existing rule for the old IP
sudo "$NFT_CMD" delete rule inet dynamic_whitelist input ip saddr "$prev_ip" accept 2>/dev/null || true
;;
iptables)
sudo iptables -D INPUT -s "$prev_ip" -j ACCEPT 2>/dev/null || true
;;
esac
fi
# Add new full-access rule
case $FIREWALL in
ufw)
sudo ufw allow from "$current_ip" to any comment "$COMMENT"
;;
nft)
sudo "$NFT_CMD" add rule inet dynamic_whitelist input ip saddr "$current_ip" accept comment "$COMMENT"
;;
iptables)
sudo iptables -I INPUT -s "$current_ip" -j ACCEPT -m comment --comment "$COMMENT"
;;
esac
echo "$current_ip" > "$state_file"
echo "$(date '+%Y-%m-%d %H:%M:%S') Success: Full access granted to $current_ip ($fqdn)" | tee -a "$LOGFILE"
}
# Main
for fqdn in "${FQDNS[@]}"; do
update_fqdn "$fqdn"
done
echo "$(date '+%Y-%m-%d %H:%M:%S') Completed check for all FQDNs" | tee -a "$LOGFILE"
3. Set the file executable:
chmod +x /usr/local/bin/update-dynamic-whitelist.sh
4. Create a file to store IP's addresses
mkdir -p /var/cache/dynamic-whitelist
5. test running the script:
/usr/local/bin/update-dynamic-whitelist.sh
6. Set up crontab to run it every 5 minutes
crontab -e
*/5 * * * * /usr/local/bin/update-dynamic-whitelist.sh >> /var/log/dynamic-whitelist.log 2>&1
7. Check the log at /var/log/dynamic-whitelist.log to see the results. The DDNS FQDN IP’s are in the /var/cache/dynamic-whitelist directory