Tutorial 36: ViciDial Security Hardening — Complete Guide
The definitive guide to locking down ViciDial, Asterisk, and supporting infrastructure against real-world attacks
| Field | Value |
|---|---|
| Technologies | ViciDial, Asterisk, fail2ban, UFW, iptables, Apache, MariaDB, Let's Encrypt, Loki |
| Difficulty | Advanced |
| Reading Time | ~75 minutes |
| Prerequisites | Root SSH access, running ViciDial installation, basic Linux/Asterisk familiarity |
| Last Updated | 2026-03-14 |
Table of Contents
Part 1 — Core Infrastructure Security
- Introduction — Why VoIP Servers Are Prime Targets
- Threat Model & Attack Surface
- SSH Hardening
- Firewall Configuration
- fail2ban Configuration
- Asterisk Security
Part 2 — Application & Operational Security
- ViciDial Web Security
- MariaDB Security
- Toll Fraud Prevention
- Monitoring & Alerting
- Backup & Recovery
- Security Audit Checklist
- Real Incident Case Studies
1. Introduction — Why VoIP Servers Are Prime Targets
ViciDial servers occupy a unique and dangerous intersection: they are telephony infrastructure with direct access to phone networks, web application servers exposing admin panels to the internet, and database servers holding customer PII. This combination makes them extraordinarily attractive to attackers.
The Four Categories of VoIP Attack
SIP Brute Force & Toll Fraud Every ViciDial server with port 5060 open receives SIP registration attempts within hours of going online. Automated scanners run 24/7, probing for weak SIP credentials. A single compromised SIP peer can generate thousands of dollars in international calls over a weekend — calls to premium-rate numbers in countries you have never heard of, routed through your trunks, billed to your account.
Web Application Exploits
ViciDial's admin panel (/vicidial/) and agent interface (/agc/) are PHP applications with a history of vulnerabilities. In 2024, CVE-2024-8503 and CVE-2024-8504 demonstrated critical SQL injection leading to remote code execution — affecting every unpatched ViciDial installation worldwide. The admin panel often has no IP restrictions, no rate limiting, and runs over plain HTTP.
Asterisk Manager Interface (AMI) Exposure AMI (port 5038) provides complete control over Asterisk: originating calls, redirecting channels, reading configurations. Many installations leave AMI listening on all interfaces with weak passwords. An attacker with AMI access can monitor live calls, inject audio, and pivot to the underlying operating system.
Database Exploitation MariaDB/MySQL frequently listens on all interfaces (port 3306) in ViciDial clusters. Default installations may have users without passwords, overly broad permissions, or no query timeouts — enabling data exfiltration, denial of service via resource exhaustion, or privilege escalation.
What This Guide Covers
This is not a theoretical security guide. Every configuration in this tutorial has been tested on production ViciDial servers handling thousands of calls per day. The fail2ban rules have caught real SIP brute-force attacks. The firewall configurations have blocked real scanning campaigns. The incident case studies at the end document real attacks that were either prevented or (in one case) caused real damage before being fixed.
By the end of this guide, your ViciDial server will have:
- SSH hardened with key-only auth, custom port, and fail2ban protection
- A firewall that allows only necessary traffic with SIP rate limiting
- fail2ban with 5+ jails covering SIP, web, AMI, and ViciDial admin
- Asterisk locked down with ACLs, strong passwords, and module pruning
- Apache hardened with HTTPS, IP restrictions, and header security
- MariaDB secured with per-user permissions and query timeouts
- Toll fraud prevention through dialplan controls and monitoring
- Automated security monitoring and alerting
- Tested backup and recovery procedures
- A monthly security audit checklist with automation
A word on scope: This guide focuses on the ViciDial server itself. Network-level security (VPN, jump boxes, network segmentation) is mentioned where relevant but is not the primary focus.
2. Threat Model & Attack Surface
Before hardening anything, you need to understand what you are defending and against whom. This section maps the complete attack surface of a typical ViciDial server.
Attack Surface Map
| Port/Service | Purpose | Attack Vector | Severity | Mitigation Section |
|---|---|---|---|---|
| 22/9322 (SSH) | Remote administration | Brute force, credential stuffing | Critical | Section 3 |
| 5060-5061 (SIP) | VoIP signaling | Registration brute force, toll fraud | Critical | Section 4, Section 6 |
| 10000-20000 (RTP) | Voice media | Eavesdropping, injection | High | Section 4 |
| 80/443 (HTTP/S) | Agent + admin panels | SQLi, XSS, RCE, brute force | Critical | Section 7 |
| 5038 (AMI) | Asterisk management | Command injection, call hijacking | Critical | Section 6 |
| 3306 (MySQL) | Database | Data exfiltration, DoS, privilege escalation | Critical | Section 8 |
| Cron jobs | Automated tasks | Persistence, privilege escalation | Medium | Section 12 |
| Recording files | Call recordings | Unauthorized access, data theft | High | Section 7 |
Threat Actors
| Actor | Motivation | Typical Attack | Sophistication |
|---|---|---|---|
| SIP scanners (botnets) | Toll fraud revenue | Automated SIP REGISTER brute force | Low — high volume, dumb patterns |
| Web vulnerability scanners | Initial access / data theft | SQLi, path traversal, CVE exploits | Medium — use published exploits |
| Competitors / insiders | Espionage, sabotage | Credential theft, recording access | Medium — know the system |
| Targeted attackers | Persistent access, data | Multi-stage: web exploit → shell → pivot | High — custom tools |
Risk Priority Matrix
Focus your hardening efforts in this order:
- SSH — if they get a shell, everything else is irrelevant
- SIP/Asterisk — toll fraud has immediate, measurable financial impact
- Web panels — the most frequently exploited entry point (CVE-2024-8503)
- Database — often the ultimate target; contains everything
- AMI — critical but less commonly exposed externally
- Monitoring — not an attack vector but essential for detecting everything above
3. SSH Hardening
SSH is the front door to your server. If an attacker gets SSH access, every other security measure is bypassed. This section implements defense in depth: the connection itself is hardened, authentication is restricted, and automated monitoring catches brute-force attempts.
3.1 Move SSH to a Non-Standard Port
Port 22 receives constant automated scanning. Moving to a non-standard port eliminates 99% of automated noise. This is not security through obscurity — it is noise reduction that makes your fail2ban logs actually useful.
# Backup the current config
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d)
# Edit SSH config
vi /etc/ssh/sshd_config
Change or add:
Port 9322
Warning: Before restarting SSH, ensure your firewall allows the new port. Otherwise you will lock yourself out.
# If using UFW
ufw allow 9322/tcp comment "SSH custom port"
# If using iptables directly
iptables -I INPUT -p tcp --dport 9322 -j ACCEPT
# Test the config before restarting
sshd -t
# Restart SSH
systemctl restart sshd
Verify by opening a NEW terminal and connecting on the new port before closing your existing session:
ssh -p 9322 root@YOUR_SERVER_IP
3.2 Key-Only Authentication
Password authentication is the single biggest SSH vulnerability. Disable it completely.
Generate a key pair on your local machine (if you do not already have one):
ssh-keygen -t ed25519 -C "[email protected]"
Copy the public key to the server:
ssh-copy-id -p 9322 root@YOUR_SERVER_IP
Verify key login works in a new terminal before disabling passwords:
ssh -p 9322 -i ~/.ssh/id_ed25519 root@YOUR_SERVER_IP
Disable password authentication:
vi /etc/ssh/sshd_config
# Authentication
PermitRootLogin prohibit-password # Allow root only with key (or use 'yes' if needed)
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes # Keep PAM for session/env handling
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
# Restrict users (optional but recommended)
AllowUsers root admin
# Security tweaks
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
# Validate and restart
sshd -t && systemctl restart sshd
3.3 SSH Connection Multiplexing (ControlMaster)
For operational efficiency, configure SSH connection multiplexing on your management machine. This reuses a single TCP connection for multiple SSH sessions, reducing authentication overhead and connection time.
Add to ~/.ssh/config on your local/management machine:
Host vicidial-prod
HostName YOUR_SERVER_IP
Port 9322
User root
IdentityFile ~/.ssh/id_ed25519
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
ServerAliveInterval 60
ServerAliveCountMax 3
# Wildcard for all ViciDial servers
Host vici-*
Port 9322
User root
IdentityFile ~/.ssh/id_ed25519
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
# Create the sockets directory
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets
3.4 fail2ban SSH Jail
The SSH fail2ban jail catches brute-force attempts that make it past the non-standard port and key-only restrictions (e.g., scanning for the custom port, or if password auth is re-enabled temporarily).
Create /etc/fail2ban/jail.d/ssh.local:
[sshd]
enabled = true
port = 9322
filter = sshd
logpath = /var/log/auth.log
# For CentOS/RHEL:
# logpath = /var/log/secure
maxretry = 3
findtime = 600
bantime = 3600
banaction = iptables-multiport
# Test the filter
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# Restart fail2ban
systemctl restart fail2ban
fail2ban-client status sshd
3.5 Optional: Two-Factor Authentication with Google Authenticator
For environments requiring an additional authentication factor, you can add TOTP (Time-based One-Time Password) to SSH. This means an attacker needs both your private key AND your phone to log in.
Note: This is optional and adds operational complexity. If you are the sole administrator, key-only auth is typically sufficient. For teams, 2FA adds significant value.
# Install on Debian/Ubuntu
apt install libpam-google-authenticator
# Install on CentOS/RHEL
yum install google-authenticator
# Install on openSUSE (ViciBox)
zypper install google-authenticator-libpam
Run the setup as the user who will log in:
google-authenticator
Answer the prompts:
Do you want authentication tokens to be time-based? y
# Scan the QR code with Google Authenticator, Authy, or similar
Do you want me to update your "~/.google_authenticator" file? y
Do you want to disallow multiple uses of the same token? y
Do you want to increase the time window? n
Do you want to enable rate-limiting? y
Configure PAM — edit /etc/pam.d/sshd:
# Add at the TOP of the file (before other auth lines)
auth required pam_google_authenticator.so nullok
The nullok flag allows users who have not set up 2FA to still log in (remove it once all users have configured their tokens).
Configure SSH to require both key AND TOTP:
vi /etc/ssh/sshd_config
# Enable both publickey and keyboard-interactive
AuthenticationMethods publickey,keyboard-interactive
ChallengeResponseAuthentication yes
UsePAM yes
sshd -t && systemctl restart sshd
Test in a new terminal before closing your current session:
ssh -p 9322 root@YOUR_SERVER_IP
# You should be prompted for your TOTP code after key auth
Backup your emergency codes. The
google-authenticatorsetup produces 5 emergency scratch codes. Store these securely — if you lose your phone, these are your only way in.
4. Firewall Configuration
A properly configured firewall is the most effective single security measure. It reduces your attack surface from "every listening port" to "only the ports that need to be reachable." For ViciDial, this means SIP, RTP, HTTP, and SSH — nothing else should be accessible from the internet.
4.1 Understanding ViciDial Port Requirements
| Port | Protocol | Purpose | Who Needs Access |
|---|---|---|---|
| 9322 (custom SSH) | TCP | Server administration | Admin IPs only |
| 80 | TCP | Agent panel (HTTP) | Agent IPs / VPN range |
| 443 | TCP | Agent panel (HTTPS) | Agent IPs / VPN range |
| 5060 | UDP/TCP | SIP signaling | SIP trunk provider IPs only |
| 5061 | TCP | SIP TLS signaling | SIP trunk provider IPs only |
| 10000-20000 | UDP | RTP media (voice) | SIP trunk provider IPs + agent IPs |
| 5038 | TCP | AMI (Asterisk Manager) | localhost only |
| 3306 | TCP | MySQL/MariaDB | localhost or cluster nodes only |
| 4569 | UDP | IAX2 (inter-server) | ViciDial cluster nodes only |
4.2 UFW Configuration (Recommended for Debian/Ubuntu)
UFW (Uncomplicated Firewall) provides a clean interface over iptables. If your ViciDial server is on Debian, Ubuntu, or a system with UFW available, this is the simplest approach.
# Install UFW if not present
apt install ufw # Debian/Ubuntu
# or
zypper install ufw # openSUSE
# Reset to clean state (careful on remote servers!)
# ufw reset
# Default policies: deny incoming, allow outgoing
ufw default deny incoming
ufw default allow outgoing
# SSH (ALWAYS add this FIRST before enabling UFW)
ufw allow 9322/tcp comment "SSH custom port"
# HTTP/HTTPS for agent/admin panels
# Option A: Allow from anywhere (if agents connect from various locations)
ufw allow 80/tcp comment "HTTP - agent panel"
ufw allow 443/tcp comment "HTTPS - agent panel"
# Option B: Restrict to specific IP ranges (more secure)
ufw allow from 10.0.0.0/8 to any port 80 proto tcp comment "HTTP - VPN agents"
ufw allow from 10.0.0.0/8 to any port 443 proto tcp comment "HTTPS - VPN agents"
ufw allow from YOUR_OFFICE_IP to any port 80 proto tcp comment "HTTP - office"
ufw allow from YOUR_OFFICE_IP to any port 443 proto tcp comment "HTTPS - office"
# SIP - restrict to trunk provider IPs ONLY
ufw allow from TRUNK_PROVIDER_IP_1 to any port 5060 proto udp comment "SIP - Provider1"
ufw allow from TRUNK_PROVIDER_IP_2 to any port 5060 proto udp comment "SIP - Provider2"
# Never do: ufw allow 5060/udp (this opens SIP to the world)
# RTP media range
ufw allow 10000:20000/udp comment "RTP media"
# IAX2 for ViciDial cluster (only if multi-server)
ufw allow from CLUSTER_NODE_IP to any port 4569 proto udp comment "IAX2 cluster"
# MySQL cluster replication (only if multi-server)
ufw allow from CLUSTER_NODE_IP to any port 3306 proto tcp comment "MySQL cluster"
# AMI - localhost only (should already be blocked by default deny)
# Explicitly ensure it is not opened:
ufw deny 5038 comment "AMI - localhost only via default deny"
# Enable UFW
ufw enable
# Verify rules
ufw status verbose
ufw status numbered
4.3 iptables Configuration (CentOS/RHEL/ViciBox)
For servers without UFW, or where you need more granular control, use iptables directly. This is common on ViciBox (openSUSE) and CentOS installations.
Create /etc/iptables/rules.v4 (or apply directly):
#!/bin/bash
# ViciDial Server Firewall Rules
# Save existing rules first
iptables-save > /root/iptables-backup-$(date +%Y%m%d).rules
# Flush existing rules
iptables -F
iptables -X
iptables -Z
# Default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Loopback (required for AMI, MySQL localhost, etc.)
iptables -A INPUT -i lo -j ACCEPT
# Established/related connections (critical for return traffic)
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# SSH on custom port
iptables -A INPUT -p tcp --dport 9322 -j ACCEPT
# HTTP/HTTPS
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# SIP - restricted to trunk provider IPs
iptables -A INPUT -p udp --dport 5060 -s TRUNK_PROVIDER_IP_1 -j ACCEPT
iptables -A INPUT -p udp --dport 5060 -s TRUNK_PROVIDER_IP_2 -j ACCEPT
# Block all other SIP
iptables -A INPUT -p udp --dport 5060 -j DROP
# RTP media range
iptables -A INPUT -p udp --dport 10000:20000 -j ACCEPT
# IAX2 cluster (multi-server only)
iptables -A INPUT -p udp --dport 4569 -s CLUSTER_NODE_IP -j ACCEPT
# MySQL cluster (multi-server only)
iptables -A INPUT -p tcp --dport 3306 -s CLUSTER_NODE_IP -j ACCEPT
# ICMP (ping) - optional, useful for monitoring
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Log dropped packets (optional, can be noisy)
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables-dropped: " --log-level 4
# Drop everything else
iptables -A INPUT -j DROP
Save the rules persistently:
# Debian/Ubuntu
apt install iptables-persistent
netfilter-persistent save
# CentOS/RHEL
service iptables save
# openSUSE
iptables-save > /etc/sysconfig/iptables
4.4 SIP Rate Limiting
Even with SIP restricted to known trunk IPs, rate limiting prevents abuse if a trunk provider's network is compromised:
# Limit SIP registrations to 15 per minute per source IP
iptables -A INPUT -p udp --dport 5060 -m recent --name SIP --set
iptables -A INPUT -p udp --dport 5060 -m recent --name SIP \
--update --seconds 60 --hitcount 15 -j DROP
For more sophisticated SIP rate limiting, use the hashlimit module:
# Allow max 20 SIP packets/sec per source IP, burst of 30
iptables -A INPUT -p udp --dport 5060 \
-m hashlimit --hashlimit-name sip_rate \
--hashlimit-above 20/sec --hashlimit-burst 30 \
--hashlimit-mode srcip -j DROP
4.5 GeoIP Blocking
If your SIP trunks and agents are all in specific countries, you can block entire geographic regions. This dramatically reduces scanning noise.
# Install xtables-addons for GeoIP support
# Debian/Ubuntu
apt install xtables-addons-common libtext-csv-xs-perl
# Download GeoIP database
mkdir -p /usr/share/xt_geoip
cd /usr/share/xt_geoip
/usr/lib/xtables-addons/xt_geoip_dl
/usr/lib/xtables-addons/xt_geoip_build -D /usr/share/xt_geoip *.csv
# Block SIP from high-fraud countries (adjust to your needs)
# Common toll fraud sources — only apply to SIP port
iptables -A INPUT -p udp --dport 5060 \
-m geoip --src-cc PH,NG,UA,RU,CN,VN,KP,IR,SY,SD \
-j DROP
# Or: allow SIP ONLY from specific countries
iptables -A INPUT -p udp --dport 5060 \
-m geoip ! --src-cc GB,IT,US,DE,NL \
-j DROP
Note: GeoIP blocking is a supplementary measure. It should never be your only SIP protection — always restrict SIP to specific trunk provider IPs when possible.
4.6 Common Firewall Mistakes
These are real mistakes found on production ViciDial servers:
Mistake 1: The ACCEPT ALL Rule
# WRONG — This rule makes ALL subsequent rules ineffective
iptables -A INPUT -j ACCEPT # Rule 1: Accept everything
iptables -A INPUT -p tcp --dport 3306 -j DROP # Rule 2: Never reached!
iptables processes rules top-to-bottom and stops at the first match. An early ACCEPT ALL rule means nothing below it will ever be evaluated. Check for this with:
# Look for ACCEPT rules with no conditions
iptables -L INPUT -n --line-numbers | grep "ACCEPT all"
If you find one, remove it by rule number:
iptables -D INPUT [RULE_NUMBER]
Mistake 2: Opening SIP to the World
# WRONG — Invites every SIP scanner on the internet
ufw allow 5060/udp
# RIGHT — Restrict to known trunk providers
ufw allow from 203.0.113.10 to any port 5060 proto udp
Mistake 3: Forgetting to Save Rules
iptables rules are lost on reboot unless explicitly saved. After every change:
# Verify rules are persisted
iptables-save | wc -l # Should show your rule count
# Check if persistence mechanism is installed
systemctl status netfilter-persistent 2>/dev/null || echo "Not installed"
Mistake 4: Opening MySQL to All Interfaces
ViciDial's default my.cnf may have bind-address = 0.0.0.0 for cluster support. If you are running a single server:
# In /etc/my.cnf or /etc/mysql/my.cnf
[mysqld]
bind-address = 127.0.0.1
4.7 Verifying Your Firewall
Always verify from an external machine after making changes:
# From another server, scan your ViciDial server
nmap -sS -sU -p 22,80,443,3306,5038,5060,9322 YOUR_SERVER_IP
# Expected results:
# 9322/tcp open (SSH - your custom port)
# 80/tcp open (HTTP - if allowed)
# 443/tcp open (HTTPS - if allowed)
# 5060/udp filtered (SIP - only open to trunk IPs)
# 3306/tcp filtered (MySQL - blocked externally)
# 5038/tcp filtered (AMI - blocked externally)
# 22/tcp filtered (old SSH port - blocked)
5. fail2ban Configuration
fail2ban is your automated intrusion response system. It monitors log files for patterns of malicious activity and temporarily (or permanently) bans offending IP addresses. For a ViciDial server, you need multiple jails covering different attack vectors.
5.1 Installation and Base Configuration
# Debian/Ubuntu
apt install fail2ban
# CentOS/RHEL
yum install epel-release
yum install fail2ban
# openSUSE (ViciBox)
zypper install fail2ban
# Enable and start
systemctl enable fail2ban
systemctl start fail2ban
Create the base override configuration at /etc/fail2ban/jail.local:
[DEFAULT]
# Ban for 1 hour by default
bantime = 3600
# Look at the last 10 minutes of logs
findtime = 600
# Ban after 5 failures
maxretry = 5
# Use iptables for banning
banaction = iptables-multiport
# Whitelist your management IPs (critical — do not lock yourself out)
ignoreip = 127.0.0.1/8 ::1 YOUR_MANAGEMENT_IP YOUR_OFFICE_IP
# Email notifications (optional)
# destemail = [email protected]
# sender = fail2ban@YOUR_SERVER_IP
# action = %(action_mwl)s
# Default action: ban and log
action = %(action_)s
5.2 Jail 1: Asterisk SIP Brute Force
This is the most critical jail. SIP brute-force attacks are constant and can lead to toll fraud within minutes of a successful breach.
Create the filter at /etc/fail2ban/filter.d/asterisk-security.conf:
# fail2ban filter for Asterisk SIP authentication failures
# Works with both chan_sip and PJSIP
# Tested on Asterisk 11.x through 20.x
[INCLUDES]
before = common.conf
[Definition]
_daemon = asterisk
# Pattern 1: chan_sip (legacy) — Registration/auth failures
# Example: NOTICE[12345]: chan_sip.c:12345 handle_request_register: Registration from '"100"<sip:100@YOUR_SERVER_IP>' failed for '192.168.1.100:5060' - Wrong password
# Example: NOTICE[12345]: chan_sip.c:12345 handle_request_register: Registration from '"100"<sip:100@YOUR_SERVER_IP>' failed for '192.168.1.100:5060' - No matching peer found
__chan_sip_fail = Registration from '.*' failed for '<HOST>(:\d+)?' - (?:Wrong password|No matching peer found|Username\/auth name mismatch|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error)
# Pattern 2: PJSIP — Authentication failures
# Example: SECURITY[12345]: res_security_log.c:123 security_event_cb: SecurityEvent="FailedACL" ... RemoteAddress="IPV4/UDP/192.168.1.100/5060"
# Example: SECURITY[12345]: res_security_log.c:123 security_event_cb: SecurityEvent="InvalidAccountID" ... RemoteAddress="IPV4/UDP/192.168.1.100/5060"
__pjsip_fail_acl = SecurityEvent="FailedACL".*RemoteAddress="IPV4/(?:UDP|TCP|TLS)/<HOST>/\d+"
__pjsip_fail_auth = SecurityEvent="(?:InvalidAccountID|ChallengeResponseFailed|InvalidPassword)".*RemoteAddress="IPV4/(?:UDP|TCP|TLS)/<HOST>/\d+"
# Pattern 3: Asterisk security log (unified format)
# Example: NOTICE[12345][C-000001]: chan_sip.c:12345 handle_request_register: Registration from '"test"<sip:[email protected]>' failed for '192.168.1.100:5060' - No matching peer found
__ast_register_fail = Registration from '.*' failed for '<HOST>(:\d+)?'
# Pattern 4: PJSIP specific authentication failures
# Example: WARNING[12345]: res_pjsip/pjsip_distributor.c:660 log_failed_request: Request 'REGISTER' from '"<sip:[email protected]>" <sip:[email protected]>' failed for '192.168.1.100:5060' ... - Failed to authenticate
__pjsip_register_fail = Request '(?:REGISTER|INVITE)' from '.*' failed for '<HOST>(:\d+)?'
# Pattern 5: AMI authentication failures (covered by dedicated jail too)
__ami_fail = (?:failed to authenticate|Manager .* failed|host <HOST> failed to authenticate)
# Combined failregex
failregex = ^%(__prefix_line)s%(__chan_sip_fail)s\s*$
^%(__prefix_line)s%(__pjsip_fail_acl)s\s*$
^%(__prefix_line)s%(__pjsip_fail_auth)s\s*$
^.*%(__ast_register_fail)s\s*$
^.*%(__pjsip_register_fail)s\s*$
ignoreregex =
# Date detection (Asterisk uses various formats)
datepattern = {^LN-BEG}
Create the jail at /etc/fail2ban/jail.d/asterisk.local:
[asterisk-security]
enabled = true
filter = asterisk-security
# Check both log locations — Asterisk versions vary
logpath = /var/log/asterisk/messages
/var/log/asterisk/security
/var/log/asterisk/full
port = 5060,5061
protocol = udp
maxretry = 3
findtime = 300
# Ban for 24 hours — SIP brute force is always malicious
bantime = 86400
# Use allports to block the attacker from everything, not just SIP
banaction = iptables-allports
5.3 Jail 2: Apache/Web Brute Force
Protects the ViciDial admin and agent panels from web-based brute force and scanning.
Create the filter at /etc/fail2ban/filter.d/apache-viciauth.conf:
# fail2ban filter for Apache authentication failures targeting ViciDial
# Catches: admin panel brute force, agent login attempts, API abuse
[INCLUDES]
before = common.conf
[Definition]
# Pattern 1: HTTP 401 Unauthorized (Basic Auth failures)
__http_401 = <HOST> -.*"(GET|POST) /vicidial/.* HTTP/.*" 401
# Pattern 2: Repeated 403 Forbidden (ACL violations)
__http_403 = <HOST> -.*"(GET|POST) /(vicidial|agc)/.* HTTP/.*" 403
# Pattern 3: Known vulnerability scanners (path traversal, etc.)
__scanners = <HOST> -.*"(GET|POST) .*(\.\.\/|etc\/passwd|wp-login|phpmyadmin|\.env|config\.php).*" \d+
# Pattern 4: Excessive 404s from a single IP (directory brute force)
__http_404_burst = <HOST> -.*"(GET|POST|HEAD) .* HTTP/.*" 404
failregex = ^%(__http_401)s$
^%(__http_403)s$
^%(__scanners)s$
ignoreregex = ^<HOST> -.* "(GET|POST) /(agc|vicidial)/(images|css|js|sounds)/
datepattern = {^LN-BEG}%%ExY
^[^\[]*\[({DATE})
{^LN-BEG}
Create the jail at /etc/fail2ban/jail.d/apache-vici.local:
[apache-viciauth]
enabled = true
filter = apache-viciauth
logpath = /var/log/apache2/access_log
/var/log/httpd/access_log
/var/log/apache2/access.log
port = http,https
maxretry = 10
findtime = 300
bantime = 3600
5.4 Jail 3: ViciDial Admin Login
ViciDial's admin login page does not use HTTP Basic Auth — it has its own login form. Failed logins are logged differently, so we need a separate filter.
Create the filter at /etc/fail2ban/filter.d/vicidial-admin.conf:
# fail2ban filter for ViciDial admin panel login failures
# These appear in Apache access logs as POST to admin login with redirect
[INCLUDES]
before = common.conf
[Definition]
# ViciDial admin login failures return a 200 with "Invalid Username or Password"
# in the response, but we can catch them by POST pattern to the login page
# combined with subsequent requests showing no session
# Pattern: Repeated POST requests to the admin login page from same IP
__admin_post = <HOST> -.*"POST /vicidial/admin\.php HTTP/.*"
# Pattern: Failed API authentication
__api_fail = <HOST> -.*"(GET|POST) /vicidial/non_agent_api\.php\?.*" 401
failregex = ^%(__admin_post)s$
^%(__api_fail)s$
ignoreregex =
datepattern = {^LN-BEG}%%ExY
^[^\[]*\[({DATE})
{^LN-BEG}
Create the jail at /etc/fail2ban/jail.d/vicidial-admin.local:
[vicidial-admin]
enabled = true
filter = vicidial-admin
logpath = /var/log/apache2/access_log
/var/log/httpd/access_log
/var/log/apache2/access.log
port = http,https
# Higher threshold — legitimate admins sometimes mistype
maxretry = 5
findtime = 300
bantime = 7200
5.5 Jail 4: AMI Brute Force
The Asterisk Manager Interface should only be accessible from localhost, but if it is misconfigured, this jail provides a safety net.
Create the filter at /etc/fail2ban/filter.d/asterisk-ami.conf:
# fail2ban filter for Asterisk Manager Interface (AMI) auth failures
[INCLUDES]
before = common.conf
[Definition]
_daemon = asterisk
# AMI authentication failures
# Example: NOTICE[12345]: manager.c:3456 authenticate: 192.168.1.100 failed to authenticate as 'admin'
# Example: NOTICE[12345]: manager.c:3456 authenticate: host 192.168.1.100 failed to authenticate
failregex = ^.*\s<HOST> failed to authenticate as '.*'\s*$
^.*\shost <HOST> failed to authenticate\s*$
^.*Manager '.*' logged off from <HOST>\s*$
^.*<HOST> tried to authenticate with nonexistent user '.*'\s*$
ignoreregex =
datepattern = {^LN-BEG}
Create the jail at /etc/fail2ban/jail.d/asterisk-ami.local:
[asterisk-ami]
enabled = true
filter = asterisk-ami
logpath = /var/log/asterisk/messages
/var/log/asterisk/full
port = 5038
maxretry = 2
findtime = 300
# AMI brute force is extremely suspicious — 48 hour ban
bantime = 172800
banaction = iptables-allports
5.6 Jail 5: SSH (Enhanced)
An enhanced SSH jail with progressive banning — repeat offenders get longer bans.
Create /etc/fail2ban/jail.d/ssh-aggressive.local:
[sshd]
enabled = true
port = 9322
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600
# Recidive jail — catches repeat offenders across all jails
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
# If banned 3 times across any jail within 24 hours
maxretry = 3
findtime = 86400
# Ban for 1 week
bantime = 604800
banaction = iptables-allports
5.7 Custom Ban Actions
Email Notification on Ban
Create /etc/fail2ban/action.d/notify-email.conf:
# Custom action: send email notification when an IP is banned
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = printf %%b "Subject: [fail2ban] <name>: banned <ip>
From: fail2ban@YOUR_SERVER_IP
To: [email protected]
The IP <ip> has been banned by fail2ban after <failures> failures
in jail <name>.
Server: YOUR_SERVER_HOSTNAME
Time: $(date)
Whois:
$(whois <ip> 2>/dev/null | head -20)" | sendmail [email protected]
actionunban =
Permanent Ban List
For IPs that are banned repeatedly, add them to a persistent blocklist:
Create /etc/fail2ban/action.d/permanent-ban.conf:
# Custom action: add to permanent ban list after recidive triggers
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = grep -qxF '<ip>' /etc/fail2ban/ip.blocklist 2>/dev/null || \
echo '<ip>' >> /etc/fail2ban/ip.blocklist
iptables -I INPUT -s <ip> -j DROP
actionunban = echo "IP <ip> is on permanent blocklist — not unbanning"
[Init]
Create the blocklist restoration script at /usr/local/bin/fail2ban-restore-blocklist.sh:
#!/bin/bash
# Restore permanent ban list on server reboot
BLOCKLIST="/etc/fail2ban/ip.blocklist"
if [ -f "$BLOCKLIST" ]; then
while IFS= read -r ip; do
[ -z "$ip" ] && continue
[[ "$ip" =~ ^# ]] && continue
iptables -C INPUT -s "$ip" -j DROP 2>/dev/null || \
iptables -I INPUT -s "$ip" -j DROP
done < "$BLOCKLIST"
echo "$(date): Restored $(wc -l < "$BLOCKLIST") permanent bans" >> /var/log/fail2ban-restore.log
fi
chmod +x /usr/local/bin/fail2ban-restore-blocklist.sh
# Add to crontab to restore on reboot
(crontab -l 2>/dev/null; echo "@reboot /usr/local/bin/fail2ban-restore-blocklist.sh") | crontab -
5.8 Testing and Monitoring fail2ban
Test each filter against your logs:
# Test Asterisk SIP filter
fail2ban-regex /var/log/asterisk/messages /etc/fail2ban/filter.d/asterisk-security.conf
# Test Apache filter
fail2ban-regex /var/log/apache2/access_log /etc/fail2ban/filter.d/apache-viciauth.conf
# Test AMI filter
fail2ban-regex /var/log/asterisk/messages /etc/fail2ban/filter.d/asterisk-ami.conf
Monitor active jails:
# Status of all jails
fail2ban-client status
# Detailed status of a specific jail
fail2ban-client status asterisk-security
# Currently banned IPs across all jails
fail2ban-client banned
# Unban a specific IP (if you accidentally ban yourself)
fail2ban-client set asterisk-security unbanip YOUR_IP
Daily ban summary script — save as /usr/local/bin/fail2ban-summary.sh:
#!/bin/bash
# Daily fail2ban summary report
echo "=== fail2ban Summary — $(date '+%Y-%m-%d %H:%M') ==="
echo ""
for jail in $(fail2ban-client status | grep "Jail list" | sed 's/.*://;s/,/ /g'); do
total=$(fail2ban-client status "$jail" | grep "Total banned" | awk '{print $NF}')
current=$(fail2ban-client status "$jail" | grep "Currently banned" | awk '{print $NF}')
echo " $jail: $current currently banned, $total total bans"
done
echo ""
echo "--- Top 10 Banned IPs (last 24h) ---"
grep "Ban " /var/log/fail2ban.log | \
grep "$(date '+%Y-%m-%d')" | \
awk '{print $NF}' | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- Permanent Blocklist ---"
if [ -f /etc/fail2ban/ip.blocklist ]; then
echo "$(wc -l < /etc/fail2ban/ip.blocklist) IPs permanently blocked"
else
echo "No permanent blocklist configured"
fi
chmod +x /usr/local/bin/fail2ban-summary.sh
Whitelist management:
# Add an IP to the whitelist (survives restarts)
# Edit /etc/fail2ban/jail.local and add to ignoreip
# Temporarily whitelist during maintenance
fail2ban-client set asterisk-security addignoreip YOUR_MAINTENANCE_IP
6. Asterisk Security
Asterisk is the core telephony engine in ViciDial. Its security configuration determines whether attackers can make calls through your system, eavesdrop on conversations, or hijack call flows. This section covers SIP peer hardening, module pruning, AMI lockdown, and context separation.
6.1 SIP Peer Hardening
Every SIP peer (extension, trunk, softphone) should be configured with security in mind. The default ViciDial installation creates peers with minimal security settings.
Edit /etc/asterisk/sip.conf (global settings):
[general]
; Security settings
alwaysauthreject = yes ; Always reject with same message (prevents username enumeration)
allowguest = no ; Reject calls from unregistered peers
allow_external_domains = no ; Only accept calls to our domain
srvlookup = no ; Disable DNS SRV (prevents DNS-based attacks)
; TLS settings (if using encrypted SIP)
; tlsenable = yes
; tlscertfile = /etc/asterisk/keys/asterisk.pem
; tlscafile = /etc/asterisk/keys/ca.crt
; tlsdontverifyserver = no
; Registration security
registerattempts = 5 ; Limit registration attempts per peer
registertimeout = 30 ; Registration timeout in seconds
defaultexpiry = 120 ; Default registration expiry
maxexpiry = 300 ; Maximum registration expiry
minexpiry = 60 ; Minimum registration expiry
; Network security
externaddr = YOUR_PUBLIC_IP ; Set your actual public IP
localnet = 10.0.0.0/8 ; Define local networks
localnet = 172.16.0.0/12
localnet = 192.168.0.0/16
; Connection limits
maxcallbitrate = 384 ; Max video bitrate (prevent resource exhaustion)
tcpauthlimit = 100 ; Max unauthenticated TCP connections
tcpauthtimeout = 30 ; Timeout for unauthenticated TCP connections
Individual SIP peer hardening (in sip-vicidial.conf or equivalent):
; Example: Hardened SIP trunk peer
[my_trunk]
type = peer
host = TRUNK_PROVIDER_IP
port = 5060
secret = USE_A_STRONG_32_CHAR_PASSWORD_HERE
context = trunkinbound ; NEVER use 'default' or 'from-internal'
disallow = all
allow = ulaw
allow = alaw
qualify = yes
insecure = port,invite ; Only if required by provider
; ACL restrictions
permit = TRUNK_PROVIDER_IP/32 ; Only allow from provider's IP
deny = 0.0.0.0/0 ; Deny all others
; Example: Hardened agent phone peer
[agent_phone_100]
type = friend
secret = R4nd0m$tr0ngP@ss!2026 ; Minimum 16 characters, mixed case + symbols
context = phones ; Restricted context — not 'default'
host = dynamic
qualify = yes
disallow = all
allow = ulaw
; ACL: only allow from VPN or office network
permit = 10.0.0.0/255.0.0.0
deny = 0.0.0.0/0.0.0.0
; Call limits
call-limit = 2 ; Max concurrent calls per peer
Critical: The
alwaysauthrejectsetting is essential. Without it, Asterisk returns different error messages for "valid username, wrong password" vs. "unknown username" — allowing attackers to enumerate valid SIP accounts before attempting passwords.
6.2 Strong SIP Passwords
ViciDial auto-generates SIP passwords, but they are often short and predictable. Generate strong passwords:
# Generate a strong SIP password (32 characters, no special chars that break SIP)
openssl rand -base64 24 | tr -d '+/=' | head -c 32
# Example output: 7kHm3nP9wQ2xR5vB8jL4cN6tY1uF0aE
# Generate passwords for multiple peers at once
for i in $(seq 100 200); do
pass=$(openssl rand -base64 24 | tr -d '+/=' | head -c 24)
echo "Peer $i: $pass"
done
Update passwords in ViciDial database:
-- Generate new passwords for all phones (ViciDial admin panel is preferred)
-- But for bulk operations:
UPDATE phones SET pass = CONCAT(
SUBSTRING(MD5(RAND()), 1, 8),
SUBSTRING(MD5(RAND()), 1, 8),
SUBSTRING(MD5(RAND()), 1, 8)
) WHERE server_ip = 'YOUR_SERVER_IP';
Note: After changing SIP passwords, all agents will need to reconfigure their softphones. Plan this during a maintenance window.
6.3 Disable Unused Asterisk Modules
Every loaded module is potential attack surface. Disable modules you do not use.
Check currently loaded modules:
asterisk -rx "module show" | wc -l
Create /etc/asterisk/modules.conf (or edit existing):
[modules]
autoload = yes
; Disable modules not needed for ViciDial
; Channel drivers
noload => chan_skinny.so ; Cisco Skinny protocol
noload => chan_mgcp.so ; Media Gateway Control Protocol
noload => chan_unistim.so ; Unistim phones
noload => chan_oss.so ; Console audio
noload => chan_phone.so ; Linux telephony interface
noload => chan_motif.so ; Google Talk/Jingle
noload => chan_ooh323.so ; H.323 protocol
; Resources not needed
noload => res_config_ldap.so ; LDAP (unless using LDAP auth)
noload => res_snmp.so ; SNMP (unless monitoring with SNMP)
noload => res_smdi.so ; Simplified Message Desk Interface
noload => res_calendar.so ; Calendar integration
noload => res_calendar_caldav.so
noload => res_calendar_icalendar.so
noload => res_xmpp.so ; XMPP/Jabber (unless using chat)
; Applications not needed
noload => app_festival.so ; Festival TTS
noload => app_mp3.so ; MP3 playback
noload => app_flash.so ; Flash hook
noload => app_dahdiras.so ; DAHDI RAS
noload => app_ices.so ; ICEcast streaming
noload => app_minivm.so ; Mini voicemail (ViciDial has its own)
noload => app_zapateller.so ; SIT tones
; Format codecs not needed (keep ulaw, alaw, wav, gsm)
noload => format_ogg_vorbis.so
noload => format_mp3.so
; CDR backends not needed (keep cdr_mysql)
noload => cdr_csv.so ; CSV CDR (ViciDial uses MySQL)
noload => cdr_sqlite3_custom.so
noload => cdr_radius.so
noload => cdr_syslog.so
# Reload modules (does not require Asterisk restart)
asterisk -rx "core restart gracefully"
# Or to apply without dropping calls:
asterisk -rx "module reload"
6.4 AMI Security (Asterisk Manager Interface)
AMI provides full control over Asterisk. It must be locked down to localhost only and use strong credentials with minimal permissions.
Edit /etc/asterisk/manager.conf:
[general]
enabled = yes
port = 5038
; CRITICAL: Bind to localhost only
bindaddr = 127.0.0.1
; Reject connections after 3 failed auth attempts
authlimit = 3
authtimeout = 30
; Do not display Asterisk version in AMI responses (information disclosure)
; displayconnects = yes ; Logged but not sent to AMI clients
httpenabled = no ; Disable AMI over HTTP (unless specifically needed)
webenabled = no ; Disable AMI web interface
; ViciDial AMI user — restrict permissions to only what ViciDial needs
[cron]
secret = STRONG_AMI_PASSWORD_32_CHARS_MINIMUM
deny = 0.0.0.0/0.0.0.0
permit = 127.0.0.1/255.255.255.0
; Only grant permissions ViciDial actually needs
read = system,call,log,agent,user,config,dtmf,reporting,cdr,dialplan
write = system,call,agent,user,config,command,reporting,originate
writetimeout = 5000
; Read-only monitoring user (for dashboards/Prometheus)
[monitor]
secret = DIFFERENT_STRONG_PASSWORD_HERE
deny = 0.0.0.0/0.0.0.0
permit = 127.0.0.1/255.255.255.0
; Read-only — cannot originate calls or change configuration
read = system,call,agent,reporting,cdr
write =
writetimeout = 5000
Verify AMI is only listening on localhost:
# Check AMI binding
ss -tlnp | grep 5038
# Should show: 127.0.0.1:5038 (NOT 0.0.0.0:5038)
# Test from another server (should fail)
# From external machine:
telnet YOUR_SERVER_IP 5038
# Should timeout / connection refused
6.5 Context Separation
Asterisk contexts control what callers/peers can do. Poor context design is a common source of toll fraud — an unauthenticated caller landing in a context that can dial external numbers.
Principles:
- Never use the
defaultcontext for anything — create explicit named contexts - Inbound trunks get their own context —
[trunkinbound], not[from-internal] - Agent phones get a restricted context —
[phones]with only what they need - Unauthenticated traffic goes to a dead-end context —
[blackhole]
Example context structure in extensions.conf:
; Dead-end context for unauthenticated traffic
[blackhole]
exten => _X.,1,NoOp(Unauthorized call attempt from ${CALLERID(all)})
same => n,Hangup(21) ; 21 = Call Rejected
; Inbound trunk context — only routes to ViciDial
[trunkinbound]
; Only accept calls to known DIDs
exten => _X.,1,AGI(agi://127.0.0.1:4577/--HVcamd--Aession-server1)
same => n,Hangup()
; Agent phone context — restricted dialing
[phones]
; Internal extensions only
exten => _8XXX,1,Dial(SIP/${EXTEN},30)
same => n,Hangup()
; Emergency numbers (if applicable)
exten => _999,1,Dial(SIP/my_trunk/${EXTEN})
exten => _112,1,Dial(SIP/my_trunk/${EXTEN})
; Block everything else
exten => _X.,1,NoOp(Blocked outbound attempt: ${EXTEN} by ${CALLERID(all)})
same => n,Playback(pbx-invalid)
same => n,Hangup(21)
; ViciDial default context (set in ViciDial admin)
[default]
; Outbound calls from ViciDial campaigns — routed through authorized trunks
exten => _X.,1,AGI(agi://127.0.0.1:4577/--HVcamd--ASession-server1)
same => n,Hangup()
6.6 CDR and Log Permissions
Call Detail Records and Asterisk logs contain sensitive information (phone numbers, call durations, agent IDs). Restrict access:
# Restrict Asterisk log directory
chmod 750 /var/log/asterisk/
chown asterisk:asterisk /var/log/asterisk/
# Restrict CDR files
chmod 640 /var/log/asterisk/cdr-csv/*.csv 2>/dev/null
# Restrict Asterisk config directory
chmod 750 /etc/asterisk/
chown -R asterisk:asterisk /etc/asterisk/
# Restrict recording directory
chmod 750 /var/spool/asterisk/monitor/
chown asterisk:asterisk /var/spool/asterisk/monitor/
6.7 SIP TLS Overview
For environments requiring encrypted SIP signaling, Asterisk supports TLS. This prevents eavesdropping on SIP messages (which contain metadata like caller ID, called numbers, and codec negotiation).
Note: SIP TLS encrypts signaling only. For encrypted media (voice), you also need SRTP. Full TLS+SRTP setup is outside the scope of this guide but the certificate setup below is the foundation.
# Generate a self-signed certificate (or use Let's Encrypt)
mkdir -p /etc/asterisk/keys
cd /etc/asterisk/keys
# Generate private key
openssl genrsa -out asterisk.key 4096
# Generate certificate signing request
openssl req -new -key asterisk.key -out asterisk.csr \
-subj "/CN=YOUR_SERVER_FQDN/O=YourCompany/C=GB"
# Self-sign for 1 year
openssl x509 -req -days 365 -in asterisk.csr \
-signkey asterisk.key -out asterisk.crt
# Combine for Asterisk
cat asterisk.key asterisk.crt > asterisk.pem
# Set permissions
chown asterisk:asterisk /etc/asterisk/keys/*
chmod 600 /etc/asterisk/keys/asterisk.key
chmod 644 /etc/asterisk/keys/asterisk.crt
Enable in sip.conf:
[general]
tlsenable = yes
tlsbindaddr = 0.0.0.0:5061
tlscertfile = /etc/asterisk/keys/asterisk.pem
tlscafile = /etc/asterisk/keys/asterisk.crt
tlsdontverifyserver = no
tlscipher = ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256
For individual peers:
[secure_trunk]
transport = tls
encryption = yes ; Enables SRTP
# Verify TLS is listening
asterisk -rx "sip show settings" | grep TLS
# Should show: TLS Enabled: Yes, TLS Bindaddress: 0.0.0.0:5061
7. ViciDial Web Security
ViciDial's web interfaces are the most commonly exploited attack surface. The admin panel (/vicidial/) and agent interface (/agc/) are PHP applications served by Apache, and they have a documented history of critical vulnerabilities. This section covers Apache hardening, HTTPS deployment, IP restrictions, and — critically — the CVE-2024 vulnerabilities that affected every ViciDial installation worldwide.
7.1 Apache Hardening
Disable directory listing and information disclosure:
# Edit Apache main config
# Debian/Ubuntu: /etc/apache2/apache2.conf
# CentOS/openSUSE: /etc/httpd/conf/httpd.conf or /etc/apache2/httpd.conf
vi /etc/apache2/httpd.conf
# Hide Apache version and OS info
ServerTokens Prod
ServerSignature Off
# Disable TRACE method (prevents Cross-Site Tracing attacks)
TraceEnable Off
# Disable directory listing globally
<Directory />
Options -Indexes -FollowSymLinks
AllowOverride None
Require all denied
</Directory>
# Security headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Remove PHP version header
Header unset X-Powered-By
</IfModule>
# Disable ETag (prevents inode-based information disclosure)
FileETag None
# Limit request body size (prevents DoS via large uploads)
LimitRequestBody 10485760
# Timeout settings (prevents slowloris attacks)
Timeout 60
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
Enable required Apache modules:
# Debian/Ubuntu
a2enmod headers
a2enmod ssl
a2enmod rewrite
# CentOS/openSUSE — modules are typically in httpd.conf
# Ensure these lines are uncommented:
# LoadModule headers_module modules/mod_headers.so
# LoadModule ssl_module modules/mod_ssl.so
# LoadModule rewrite_module modules/mod_rewrite.so
systemctl restart apache2
# or
systemctl restart httpd
7.2 HTTPS with Let's Encrypt
Running ViciDial over plain HTTP means agent credentials, admin passwords, and customer data are transmitted in cleartext. Anyone on the network path can intercept them.
Install Certbot:
# Debian/Ubuntu
apt install certbot python3-certbot-apache
# CentOS/RHEL
yum install epel-release
yum install certbot python3-certbot-apache
# openSUSE
zypper install certbot python3-certbot-apache
Obtain a certificate:
# Ensure your server has a DNS A record pointing to it
# Replace with your actual domain
certbot --apache -d vicidial.yourdomain.com
# If you don't have a domain, use IP-based cert (self-signed)
# Let's Encrypt requires a domain — for IP-only servers, use self-signed:
openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
-keyout /etc/ssl/private/vicidial.key \
-out /etc/ssl/certs/vicidial.crt \
-subj "/CN=YOUR_SERVER_IP/O=ViciDial/C=GB"
Configure Apache SSL VirtualHost:
<VirtualHost *:443>
ServerName vicidial.yourdomain.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/vicidial.yourdomain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/vicidial.yourdomain.com/privkey.pem
# Strong TLS configuration
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on
SSLCompression off
SSLSessionTickets off
# HSTS (only enable after confirming HTTPS works)
Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>
# Redirect HTTP to HTTPS
<VirtualHost *:80>
ServerName vicidial.yourdomain.com
Redirect permanent / https://vicidial.yourdomain.com/
</VirtualHost>
Auto-renewal:
# Test renewal
certbot renew --dry-run
# Certbot automatically installs a renewal cron/timer
# Verify:
systemctl list-timers | grep certbot
# or
cat /etc/cron.d/certbot
7.3 Admin Panel IP Restrictions
The admin panel (/vicidial/) should only be accessible from trusted IP addresses. This single measure prevents the majority of web-based attacks.
Add to your Apache configuration:
# Restrict ViciDial admin panel to specific IPs
<Directory "/var/www/html/vicidial">
# Allow from office and VPN
Require ip YOUR_OFFICE_IP
Require ip 10.0.0.0/8
Require ip 127.0.0.1
# Block everyone else
# (implicit deny with Require directives in Apache 2.4+)
</Directory>
# Agent panel — can be more permissive but still restrict if possible
<Directory "/var/www/html/agc">
# If agents only connect via VPN:
Require ip 10.0.0.0/8
Require ip YOUR_OFFICE_IP
# If agents connect from various locations, leave open
# but ensure HTTPS is enforced
# Require all granted
</Directory>
# ALWAYS restrict the API
<Directory "/var/www/html/vicidial">
<Files "non_agent_api.php">
Require ip YOUR_API_CLIENT_IP
Require ip 127.0.0.1
</Files>
<Files "agent_api.php">
Require ip YOUR_API_CLIENT_IP
Require ip 127.0.0.1
</Files>
</Directory>
# Block access to recordings directory via web
<Directory "/var/spool/asterisk/monitor">
Require all denied
</Directory>
# If recordings are served from a web-accessible path, restrict:
<Directory "/var/www/html/RECORDINGS">
# Only allow from admin IPs
Require ip YOUR_OFFICE_IP
Require ip 10.0.0.0/8
</Directory>
# Test config and restart
apachectl configtest && systemctl restart apache2
7.4 CVE-2024-8503 and CVE-2024-8504 — Critical ViciDial Vulnerabilities
These two CVEs, published in September 2024, represent the most critical ViciDial vulnerabilities in recent history. If your ViciDial installation has not been patched since August 2024, you are almost certainly vulnerable.
CVE-2024-8503: SQL Injection (CVSS 9.8 — Critical)
What it exploits: The vicidial/login.php file (and related authentication endpoints) fail to properly sanitize user input in the login form. An attacker can inject SQL through the username or password field without any authentication.
Impact:
- Full read access to the ViciDial database
- Extraction of all agent credentials, customer phone numbers, and call recordings metadata
- Extraction of admin credentials and AMI passwords
- Used as a stepping stone for CVE-2024-8504 (RCE)
How to check if you are vulnerable:
# Check your ViciDial build
grep -r "VERSION" /var/www/html/vicidial/admin.php 2>/dev/null | head -5
# or check the admin panel: Admin -> System Settings -> version
# The fix was included in ViciDial SVN revision 3848+
# Check your SVN revision
svn info /usr/share/astguiclient 2>/dev/null | grep "Revision"
Patching steps:
# Option 1: Update ViciDial SVN (recommended if you maintain SVN)
cd /usr/share/astguiclient/trunk
svn update
# Then re-install the web files
cp -r /usr/share/astguiclient/trunk/www/vicidial/* /var/www/html/vicidial/
cp -r /usr/share/astguiclient/trunk/www/agc/* /var/www/html/agc/
# Option 2: Manual patch (if SVN is not configured)
# The vulnerability is in the login processing code
# The fix adds proper escaping/parameterization of user inputs
# Backup first!
cp /var/www/html/vicidial/login.php /var/www/html/vicidial/login.php.bak.$(date +%Y%m%d)
# Apply the specific fix (check ViciDial forums for exact patch)
# The core fix replaces direct variable interpolation with mysql_real_escape_string
# or parameterized queries in the authentication SQL statements
CVE-2024-8504: Remote Code Execution (CVSS 9.8 — Critical)
What it exploits: Once authenticated (even with credentials stolen via CVE-2024-8503), an attacker can execute arbitrary operating system commands through the ViciDial admin interface. The vulnerability exists in admin functions that pass user input to system commands without sanitization.
Impact:
- Full remote code execution as the Apache/web server user
- Often leads to root access (many ViciDial installations run Apache as root or with sudo)
- Complete server compromise: install backdoors, pivot to other servers, steal recordings
- Toll fraud via direct Asterisk CLI manipulation
How to check if you are vulnerable:
# Check for the specific vulnerable patterns
# Look for system() or exec() calls with unsanitized input
grep -rn "system\(\$\|exec\(\$\|passthru\(\$\|shell_exec\(\$" \
/var/www/html/vicidial/admin.php 2>/dev/null | head -20
Patching steps:
The fix is included in the same SVN update as CVE-2024-8503. If you cannot update SVN:
# Temporary mitigation: restrict admin panel access by IP (Section 7.3)
# This prevents exploitation even if the code is vulnerable
# In Apache config:
<Directory "/var/www/html/vicidial">
Require ip YOUR_TRUSTED_IPS_ONLY
</Directory>
# Also ensure the admin panel is behind HTTPS
# and fail2ban is monitoring login attempts
Combined Defense Against Both CVEs
The strongest defense is layered:
- Patch your ViciDial to SVN revision 3848+ (fixes the root cause)
- Restrict admin panel by IP (prevents exploitation even if unpatched)
- Run fail2ban on web access logs (detects brute force and SQLi attempts)
- Monitor admin login logs for unusual activity
- Ensure Apache does not run as root (limits blast radius of RCE)
# Verify Apache user
ps aux | grep apache | grep -v grep
# Should show: www-data, apache, or wwwrun — NOT root
# If Apache is running as root, fix it:
# In httpd.conf:
# User wwwrun
# Group www
7.5 API Security
ViciDial's non-agent API (non_agent_api.php) and agent API (agent_api.php) are powerful interfaces that can control campaigns, agents, and calls. They must be secured.
# The API uses IP-based authentication configured in ViciDial admin
# Admin -> System Settings -> Admin Interface
# Set: API Allowed IPs
# Additionally, restrict at the Apache level (defense in depth):
Recommended API configuration in ViciDial admin:
| Setting | Recommended Value |
|---|---|
| API Allowed IPs | Comma-separated list of trusted IPs only |
| API User | Dedicated API user (not admin account) |
| API Permission Level | Minimum required (1-9, where 9 is full access) |
7.6 Session Security Improvements
# Edit PHP configuration to harden sessions
vi /etc/php.ini # or /etc/php/8.x/apache2/php.ini
; Session security
session.cookie_httponly = 1 ; Prevent JavaScript access to session cookies
session.cookie_secure = 1 ; Only send cookies over HTTPS (enable after HTTPS setup)
session.use_strict_mode = 1 ; Reject uninitialized session IDs
session.use_only_cookies = 1 ; Prevent session fixation via URL
session.cookie_samesite = "Strict" ; CSRF protection
; Disable dangerous PHP features
expose_php = Off ; Hide PHP version in headers
allow_url_include = Off ; Prevent remote file inclusion
display_errors = Off ; Never show errors to users in production
log_errors = On ; Log errors to file instead
error_log = /var/log/php_errors.log
; Upload limits (prevent resource exhaustion)
upload_max_filesize = 10M
post_max_size = 10M
max_execution_time = 60
max_input_time = 60
memory_limit = 256M
; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_source
; WARNING: ViciDial uses some of these functions!
; Only disable functions you have verified ViciDial does not use.
; Test thoroughly in a staging environment.
Warning about
disable_functions: ViciDial's admin panel usesexec()andsystem()for legitimate purposes (server status, process management). Disabling these will break admin functionality. Only applydisable_functionsif you have tested which functions ViciDial requires and excluded them from the list. The Apache IP restriction from Section 7.3 is a more practical mitigation.
8. MariaDB Security
MariaDB holds everything: agent credentials, customer phone numbers, call recordings metadata, campaign configurations, and CDRs. A database breach means total compromise of your operation.
8.1 Initial Hardening with mysql_secure_installation
If you have not already run this, do it now:
mysql_secure_installation
Answer the prompts:
Set root password? [Y/n] Y
# Enter a strong password (32+ characters)
Remove anonymous users? [Y/n] Y
Disallow root login remotely? [Y/n] Y
Remove test database and access? [Y/n] Y
Reload privilege tables? [Y/n] Y
8.2 Per-User Permissions
The principle of least privilege is critical for database security. Create separate users for each purpose with only the permissions they need.
-- Connect as root
mysql -u root -p
-- 1. ViciDial application user (used by cron scripts and web interface)
-- Needs full access to the ViciDial database, but NOT to other databases
CREATE USER 'cron'@'localhost' IDENTIFIED BY 'STRONG_CRON_PASSWORD_HERE';
GRANT ALL PRIVILEGES ON asterisk.* TO 'cron'@'localhost';
-- Only if using multi-server:
-- GRANT ALL PRIVILEGES ON asterisk.* TO 'cron'@'CLUSTER_NODE_IP' IDENTIFIED BY 'STRONG_CRON_PASSWORD_HERE';
-- 2. Read-only reporting user (for dashboards, Grafana, external tools)
CREATE USER 'grafana_ro'@'localhost' IDENTIFIED BY 'STRONG_GRAFANA_PASSWORD';
GRANT SELECT ON asterisk.* TO 'grafana_ro'@'localhost';
-- Allow from monitoring server if needed:
GRANT SELECT ON asterisk.* TO 'grafana_ro'@'MONITORING_SERVER_IP' IDENTIFIED BY 'STRONG_GRAFANA_PASSWORD';
-- 3. Replication user (if using replica for reporting)
CREATE USER 'repl_user'@'REPLICA_IP' IDENTIFIED BY 'STRONG_REPL_PASSWORD';
GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'REPLICA_IP';
-- 4. Backup user (for mysqldump)
CREATE USER 'backup'@'localhost' IDENTIFIED BY 'STRONG_BACKUP_PASSWORD';
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON asterisk.* TO 'backup'@'localhost';
GRANT RELOAD ON *.* TO 'backup'@'localhost';
-- 5. Audit: Remove any users with overly broad access
-- Check existing users and their permissions
SELECT user, host, authentication_string FROM mysql.user;
SHOW GRANTS FOR 'root'@'localhost';
-- Remove unused users
-- DROP USER 'old_user'@'%';
-- Apply changes
FLUSH PRIVILEGES;
8.3 Network Binding
Control which network interfaces MariaDB listens on:
vi /etc/my.cnf
# or /etc/mysql/my.cnf or /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
# Single server: localhost only
bind-address = 127.0.0.1
# Multi-server cluster: bind to specific interface (not 0.0.0.0)
# bind-address = YOUR_PRIVATE_IP
# Skip name resolution (prevents DNS-based attacks + improves performance)
skip-name-resolve
# Disable symbolic links (prevents symlink-based attacks)
symbolic-links = 0
# Disable LOCAL INFILE (prevents file reading attacks)
local-infile = 0
# Log slow queries (useful for detecting DoS via heavy queries)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 5
8.4 Query Timeouts
Prevent runaway queries from consuming all database resources. This protects against both accidental bad queries and deliberate DoS attacks.
[mysqld]
# Maximum query execution time in seconds (MariaDB 10.1+)
max_statement_time = 30
# Connection timeout
wait_timeout = 600
interactive_timeout = 600
# Max connections (prevent connection flooding)
max_connections = 500
max_connect_errors = 100
# Per-user resource limits
# Applied when creating/altering users:
-- Apply resource limits to the reporting user
ALTER USER 'grafana_ro'@'localhost' WITH
MAX_QUERIES_PER_HOUR 1000
MAX_CONNECTIONS_PER_HOUR 100
MAX_USER_CONNECTIONS 5;
-- Apply stricter limits to the backup user
ALTER USER 'backup'@'localhost' WITH
MAX_QUERIES_PER_HOUR 100
MAX_CONNECTIONS_PER_HOUR 10
MAX_USER_CONNECTIONS 2;
FLUSH PRIVILEGES;
Test query timeout:
-- This should be killed after max_statement_time seconds:
SET STATEMENT max_statement_time=5 FOR
SELECT SLEEP(30);
-- Should return: ERROR 1969 (70100): Query execution was interrupted (max_statement_time exceeded)
8.5 Audit Logging (Optional)
For environments requiring database audit trails:
[mysqld]
# MariaDB Audit Plugin (built-in since 10.0)
plugin_load_add = server_audit
server_audit_events = CONNECT,QUERY_DDL,QUERY_DCL
server_audit_logging = ON
server_audit_file_path = /var/log/mysql/audit.log
server_audit_file_rotate_size = 104857600
server_audit_file_rotations = 10
# Exclude noisy SELECT queries:
server_audit_excl_users = 'cron'
9. Toll Fraud Prevention
Toll fraud is the single most expensive security incident for a VoIP operation. Attackers compromise a SIP peer or trunk and route calls to premium-rate international numbers, generating charges of thousands of dollars per hour. This section covers prevention, detection, and response.
9.1 Understanding Toll Fraud
How it happens:
- Attacker scans for SIP servers on port 5060 (automated, 24/7)
- Attacker brute-forces SIP credentials (peer names and passwords)
- Attacker registers as a legitimate SIP peer
- Attacker sends INVITE requests to premium-rate numbers (often in Somalia, Cuba, Sierra Leone, Gambia)
- Premium-rate numbers are owned by the attacker or an accomplice
- Calls are charged to YOUR trunk account
- Attacker earns revenue from the premium-rate number
- You receive a bill for thousands of dollars
Warning signs:
- Sudden spike in concurrent calls, especially outside business hours
- Calls to unusual country codes (252, 53, 220, 232, 960, etc.)
- SIP peer registrations from unknown IPs
- Asterisk CDR showing calls you did not initiate
- Trunk provider sending fraud alerts
9.2 Dialplan-Based International Call Restrictions
The most effective toll fraud prevention is simply not allowing calls to international numbers you do not need:
Add to extensions.conf:
; ============================================================
; TOLL FRAUD PREVENTION — Blocked Destinations
; ============================================================
; Block premium-rate international destinations
; These are the most commonly targeted for toll fraud
[toll-fraud-block]
; Premium rate numbers (international)
exten => _900.,1,NoOp(BLOCKED: Premium ${EXTEN})
same => n,Hangup(21)
exten => _0900.,1,NoOp(BLOCKED: Premium ${EXTEN})
same => n,Hangup(21)
; International directory enquiries
exten => _118.,1,NoOp(BLOCKED: Directory ${EXTEN})
same => n,Hangup(21)
; High-fraud-risk country codes (adjust based on your business)
; Somalia +252
exten => _00252.,1,NoOp(BLOCKED: Somalia ${EXTEN})
same => n,Hangup(21)
; Cuba +53
exten => _0053.,1,NoOp(BLOCKED: Cuba ${EXTEN})
same => n,Hangup(21)
; Sierra Leone +232
exten => _00232.,1,NoOp(BLOCKED: Sierra Leone ${EXTEN})
same => n,Hangup(21)
; Gambia +220
exten => _00220.,1,NoOp(BLOCKED: Gambia ${EXTEN})
same => n,Hangup(21)
; Maldives +960
exten => _00960.,1,NoOp(BLOCKED: Maldives ${EXTEN})
same => n,Hangup(21)
; International IRSF destinations (International Revenue Share Fraud)
; Add country codes based on your trunk provider's fraud list
; Include this context in your outbound context:
; include => toll-fraud-block
; Then include your actual allowed destinations AFTER
Example outbound context with fraud protection:
[outbound-vicidial]
; First, check against toll fraud blocklist
include => toll-fraud-block
; Then allow legitimate destinations
; UK domestic
exten => _01XXXXXXXXX,1,Dial(SIP/my_trunk/${EXTEN})
exten => _02XXXXXXXXX,1,Dial(SIP/my_trunk/${EXTEN})
exten => _07XXXXXXXXX,1,Dial(SIP/my_trunk/${EXTEN})
; Allowed international (add only countries you actually call)
exten => _0039.,1,Dial(SIP/my_trunk/${EXTEN}) ; Italy
exten => _0033.,1,Dial(SIP/my_trunk/${EXTEN}) ; France
exten => _0049.,1,Dial(SIP/my_trunk/${EXTEN}) ; Germany
; Block everything else
exten => _X.,1,NoOp(BLOCKED: Unrecognized number ${EXTEN})
same => n,Hangup(21)
9.3 Call Duration Limits
Prevent toll fraud calls from running indefinitely:
In extensions.conf:
; Set absolute timeout on all outbound calls (seconds)
; 3600 = 1 hour maximum call duration
exten => _X.,1,Set(TIMEOUT(absolute)=3600)
same => n,Dial(SIP/my_trunk/${EXTEN},60)
same => n,Hangup()
In sip.conf (per-peer):
[agent_phone]
call-limit = 2 ; Max 2 concurrent calls (1 active + 1 on hold)
In ViciDial admin (System Settings):
| Setting | Recommended Value | Purpose |
|---|---|---|
| Max Call Length | 3600 | Maximum call duration in seconds |
| Concurrent Call Limit | Appropriate for your campaign | Prevents mass simultaneous fraudulent calls |
9.4 After-Hours Call Blocking
Most toll fraud occurs outside business hours when no one is watching:
; Block outbound calls outside business hours
[time-check]
exten => _X.,1,GotoIfTime(08:00-20:00,mon-fri,*,*?allowed,${EXTEN},1)
same => n,GotoIfTime(09:00-17:00,sat,*,*?allowed,${EXTEN},1)
same => n,NoOp(BLOCKED: After hours call to ${EXTEN})
same => n,Hangup(21)
[allowed]
exten => _X.,1,Dial(SIP/my_trunk/${EXTEN})
same => n,Hangup()
9.5 Monitoring for Unusual Patterns
Daily toll fraud detection script — save as /usr/local/bin/toll-fraud-check.sh:
#!/bin/bash
# Daily toll fraud detection for ViciDial
# Checks for unusual calling patterns in the last 24 hours
DB_USER="report_cron"
DB_PASS="YOUR_DB_PASSWORD"
DB_NAME="asterisk"
ALERT_EMAIL="[email protected]"
ALERT=0
echo "=== Toll Fraud Check — $(date '+%Y-%m-%d %H:%M') ==="
# 1. Check for calls to high-risk country codes
echo ""
echo "--- Calls to High-Risk Countries (last 24h) ---"
FRAUD_CALLS=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -N -e "
SELECT phone_number, COUNT(*) as calls, SUM(length_in_sec) as total_sec
FROM vicidial_log
WHERE call_date > DATE_SUB(NOW(), INTERVAL 24 HOUR)
AND (phone_number LIKE '00252%'
OR phone_number LIKE '0053%'
OR phone_number LIKE '00232%'
OR phone_number LIKE '00220%'
OR phone_number LIKE '00960%'
OR phone_number LIKE '00682%'
OR phone_number LIKE '00686%'
OR phone_number LIKE '00269%')
GROUP BY phone_number
ORDER BY calls DESC
LIMIT 20;
")
if [ -n "$FRAUD_CALLS" ]; then
echo "WARNING: Calls to high-risk destinations detected!"
echo "$FRAUD_CALLS"
ALERT=1
else
echo "No calls to high-risk destinations."
fi
# 2. Check for unusually long calls (>1 hour)
echo ""
echo "--- Unusually Long Calls (>3600s, last 24h) ---"
LONG_CALLS=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -N -e "
SELECT phone_number, user, length_in_sec, call_date
FROM vicidial_log
WHERE call_date > DATE_SUB(NOW(), INTERVAL 24 HOUR)
AND length_in_sec > 3600
ORDER BY length_in_sec DESC
LIMIT 20;
")
if [ -n "$LONG_CALLS" ]; then
echo "WARNING: Unusually long calls detected!"
echo "$LONG_CALLS"
ALERT=1
else
echo "No unusually long calls."
fi
# 3. Check for after-hours call activity
echo ""
echo "--- After-Hours Calls (22:00-06:00, last 24h) ---"
AFTERHOURS=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -N -e "
SELECT COUNT(*) as calls, SUM(length_in_sec) as total_sec
FROM vicidial_log
WHERE call_date > DATE_SUB(NOW(), INTERVAL 24 HOUR)
AND (HOUR(call_date) >= 22 OR HOUR(call_date) < 6);
")
echo "After-hours calls: $AFTERHOURS"
# 4. Check for unknown SIP registrations
echo ""
echo "--- Unknown SIP Registrations ---"
asterisk -rx "sip show peers" 2>/dev/null | grep -v "OK\|UNREACHABLE\|Unmonitored\|Name" | head -20
# 5. Send alert if issues found
if [ $ALERT -eq 1 ]; then
echo ""
echo "ALERT: Potential toll fraud indicators detected. Review immediately."
# Uncomment to send email alert:
# echo "Toll fraud indicators detected on $(hostname). Check /var/log/toll-fraud-check.log" | \
# mail -s "TOLL FRAUD ALERT - $(hostname)" "$ALERT_EMAIL"
fi
chmod +x /usr/local/bin/toll-fraud-check.sh
# Add to crontab — run every 6 hours
(crontab -l 2>/dev/null; echo "0 */6 * * * /usr/local/bin/toll-fraud-check.sh >> /var/log/toll-fraud-check.log 2>&1") | crontab -
9.6 SIP Trunk Provider Security
Work with your SIP trunk provider to add server-side protections:
| Protection | Description | Ask Your Provider |
|---|---|---|
| Fraud monitoring | Real-time call pattern analysis | "Do you offer fraud detection alerts?" |
| Daily spend cap | Maximum daily spend limit | "Can you set a daily limit of $X?" |
| Country blocking | Block specific destination countries | "Can you block calls to [country list]?" |
| IP allowlisting | Only accept traffic from your server IPs | "Can you restrict our trunk to these source IPs?" |
| Concurrent call limit | Max simultaneous calls through the trunk | "Set our concurrent call limit to X channels" |
| After-hours blocking | Block calls outside business hours | "Can you block outbound calls between 22:00-06:00?" |
Pro tip: Most trunk providers will set a daily spend cap at no charge. This is your last line of defense — even if every other measure fails, the spend cap limits your financial exposure. Set it to 150% of your highest legitimate daily spend.
10. Monitoring & Alerting
Security without monitoring is just hope. You need visibility into what is happening on your server in real time and the ability to detect anomalies before they become incidents.
10.1 Log Monitoring with Loki and Promtail
Centralized log aggregation lets you query and alert on security events across all your ViciDial servers from a single interface.
Install Promtail on the ViciDial server:
# Download latest Promtail
PROMTAIL_VERSION="2.9.4"
curl -LO "https://github.com/grafana/loki/releases/download/v${PROMTAIL_VERSION}/promtail-linux-amd64.zip"
unzip promtail-linux-amd64.zip
mv promtail-linux-amd64 /usr/local/bin/promtail
chmod +x /usr/local/bin/promtail
Configure Promtail — create /etc/promtail/config.yml:
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://YOUR_LOKI_SERVER:3100/loki/api/v1/push
scrape_configs:
# Asterisk security logs — SIP auth failures, toll fraud indicators
- job_name: asterisk
static_configs:
- targets:
- localhost
labels:
job: asterisk
host: YOUR_SERVER_HOSTNAME
__path__: /var/log/asterisk/messages
# Apache access logs — web attacks, brute force
- job_name: apache_access
static_configs:
- targets:
- localhost
labels:
job: apache
type: access
host: YOUR_SERVER_HOSTNAME
__path__: /var/log/apache2/access_log
# Apache error logs
- job_name: apache_error
static_configs:
- targets:
- localhost
labels:
job: apache
type: error
host: YOUR_SERVER_HOSTNAME
__path__: /var/log/apache2/error_log
# fail2ban logs — ban events
- job_name: fail2ban
static_configs:
- targets:
- localhost
labels:
job: fail2ban
host: YOUR_SERVER_HOSTNAME
__path__: /var/log/fail2ban.log
# SSH auth logs
- job_name: auth
static_configs:
- targets:
- localhost
labels:
job: auth
host: YOUR_SERVER_HOSTNAME
__path__: /var/log/auth.log
# MySQL slow query log
- job_name: mysql_slow
static_configs:
- targets:
- localhost
labels:
job: mysql
type: slow
host: YOUR_SERVER_HOSTNAME
__path__: /var/log/mysql/slow.log
Create systemd service — /etc/systemd/system/promtail.service:
[Unit]
Description=Promtail Log Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/promtail -config.file=/etc/promtail/config.yml
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
mkdir -p /etc/promtail /var/lib/promtail
systemctl daemon-reload
systemctl enable promtail
systemctl start promtail
10.2 Prometheus Alerts for Security Events
If you run Prometheus for monitoring, add these alert rules for security-relevant events.
Create /etc/prometheus/rules/vicidial-security.yml:
groups:
- name: vicidial_security
interval: 60s
rules:
# Alert on high fail2ban ban rate
- alert: HighFail2banBanRate
expr: increase(fail2ban_banned_total[1h]) > 50
for: 5m
labels:
severity: warning
annotations:
summary: "High fail2ban ban rate on {{ $labels.instance }}"
description: "More than 50 IPs banned in the last hour. Possible active attack."
# Alert on unusual SIP registration count
- alert: UnusualSIPRegistrations
expr: asterisk_sip_peers_online > (asterisk_sip_peers_online offset 1h) * 1.5
for: 5m
labels:
severity: warning
annotations:
summary: "Unusual SIP registration spike on {{ $labels.instance }}"
description: "SIP peer count jumped 50% in the last hour."
# Alert if fail2ban is not running
- alert: Fail2banDown
expr: up{job="fail2ban"} == 0 or absent(up{job="fail2ban"})
for: 5m
labels:
severity: critical
annotations:
summary: "fail2ban is down on {{ $labels.instance }}"
description: "fail2ban is not running. Server is unprotected against brute force."
# Alert on disk usage (recordings can fill disk)
- alert: DiskSpaceLow
expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) < 0.1
for: 10m
labels:
severity: critical
annotations:
summary: "Low disk space on {{ $labels.instance }}"
description: "Less than 10% disk space remaining."
# Alert on high concurrent calls (possible toll fraud)
- alert: HighConcurrentCalls
expr: asterisk_active_calls > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Unusually high concurrent calls on {{ $labels.instance }}"
description: "{{ $value }} active calls. Check for toll fraud."
10.3 Daily Security Audit Script
This comprehensive script checks for common security issues and generates a daily report.
Save as /usr/local/bin/daily-security-audit.sh:
#!/bin/bash
# =============================================================
# ViciDial Daily Security Audit Script
# Run daily via cron — generates a security status report
# =============================================================
REPORT_FILE="/var/log/security-audit-$(date +%Y%m%d).log"
ALERT=0
{
echo "============================================================"
echo " DAILY SECURITY AUDIT — $(hostname)"
echo " $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo "============================================================"
echo ""
# 1. OPEN PORTS CHECK
echo "=== 1. Open Ports ==="
ss -tlnp | grep LISTEN | awk '{print $4, $6}' | sort
echo ""
# Check for unexpected ports
UNEXPECTED=$(ss -tlnp | grep LISTEN | grep -v -E ':(9322|80|443|5060|5061|5038|3306|4569|10000|127\.0\.0\.1)' | grep '0.0.0.0')
if [ -n "$UNEXPECTED" ]; then
echo "WARNING: Unexpected ports listening on all interfaces:"
echo "$UNEXPECTED"
ALERT=1
fi
echo ""
# 2. FAILED SSH LOGINS (last 24h)
echo "=== 2. Failed SSH Logins (Last 24h) ==="
if [ -f /var/log/auth.log ]; then
SSH_FAILS=$(grep "Failed password\|Failed publickey" /var/log/auth.log | \
grep "$(date '+%b %d')\|$(date -d yesterday '+%b %d')" | wc -l)
echo "Failed SSH attempts: $SSH_FAILS"
if [ "$SSH_FAILS" -gt 100 ]; then
echo "WARNING: High number of SSH failures!"
ALERT=1
fi
echo "Top source IPs:"
grep "Failed password\|Failed publickey" /var/log/auth.log | \
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort | uniq -c | sort -rn | head -10
elif [ -f /var/log/secure ]; then
SSH_FAILS=$(grep "Failed password\|Failed publickey" /var/log/secure | wc -l)
echo "Failed SSH attempts: $SSH_FAILS"
fi
echo ""
# 3. FAIL2BAN STATUS
echo "=== 3. fail2ban Status ==="
if systemctl is-active --quiet fail2ban; then
echo "Status: RUNNING"
fail2ban-client status 2>/dev/null | grep -E "Jail list|Number of jail"
echo ""
echo "Currently banned IPs:"
for jail in $(fail2ban-client status 2>/dev/null | grep "Jail list" | sed 's/.*://;s/,/ /g'); do
banned=$(fail2ban-client status "$jail" 2>/dev/null | grep "Currently banned" | awk '{print $NF}')
total=$(fail2ban-client status "$jail" 2>/dev/null | grep "Total banned" | awk '{print $NF}')
echo " $jail: $banned current / $total total"
done
else
echo "CRITICAL: fail2ban is NOT running!"
ALERT=1
fi
echo ""
# 4. SIP PEER STATUS
echo "=== 4. SIP Peer Check ==="
if command -v asterisk &>/dev/null; then
TOTAL_PEERS=$(asterisk -rx "sip show peers" 2>/dev/null | tail -1)
echo "Peer summary: $TOTAL_PEERS"
# Check for peers registered from unexpected IPs
echo "Registered peers from external IPs:"
asterisk -rx "sip show peers" 2>/dev/null | grep -v "127.0.0.1\|10\.\|172\.\|192\.168\.\|Unmonitored\|UNREACHABLE\|Name" | head -20
fi
echo ""
# 5. DISK USAGE
echo "=== 5. Disk Usage ==="
df -h / /var/spool/asterisk/monitor 2>/dev/null | grep -v "^Filesystem"
DISK_PCT=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK_PCT" -gt 85 ]; then
echo "WARNING: Root filesystem is ${DISK_PCT}% full!"
ALERT=1
fi
echo ""
# 6. NEW/MODIFIED SYSTEM USERS (last 24h)
echo "=== 6. User Account Changes ==="
NEW_USERS=$(find /etc/passwd -mtime -1 2>/dev/null)
if [ -n "$NEW_USERS" ]; then
echo "WARNING: /etc/passwd was modified in the last 24 hours!"
ALERT=1
else
echo "No user account changes detected."
fi
# Check for unauthorized sudoers changes
SUDO_CHANGES=$(find /etc/sudoers /etc/sudoers.d/ -mtime -1 2>/dev/null)
if [ -n "$SUDO_CHANGES" ]; then
echo "WARNING: sudoers files modified in the last 24 hours!"
echo "$SUDO_CHANGES"
ALERT=1
fi
echo ""
# 7. CRONTAB CHANGES
echo "=== 7. Crontab Check ==="
CRON_CHANGES=$(find /var/spool/cron/ /etc/cron.d/ -mtime -1 2>/dev/null)
if [ -n "$CRON_CHANGES" ]; then
echo "Modified cron files (last 24h):"
echo "$CRON_CHANGES"
else
echo "No crontab changes in the last 24 hours."
fi
echo ""
# 8. ASTERISK LOG ERRORS
echo "=== 8. Asterisk Security Events (Last 24h) ==="
if [ -f /var/log/asterisk/messages ]; then
SECURITY_EVENTS=$(grep -c "SecurityEvent\|failed to authenticate\|Registration.*failed" \
/var/log/asterisk/messages 2>/dev/null)
echo "Security events in Asterisk log: ${SECURITY_EVENTS:-0}"
if [ "${SECURITY_EVENTS:-0}" -gt 100 ]; then
echo "WARNING: High number of Asterisk security events!"
echo "Top attacking IPs:"
grep -oE "RemoteAddress.*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" /var/log/asterisk/messages | \
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort | uniq -c | sort -rn | head -10
ALERT=1
fi
fi
echo ""
# 9. APACHE SECURITY
echo "=== 9. Apache Security Events ==="
if [ -f /var/log/apache2/access_log ] || [ -f /var/log/httpd/access_log ]; then
ACCESS_LOG=$(ls /var/log/apache2/access_log /var/log/httpd/access_log 2>/dev/null | head -1)
VULN_SCANS=$(grep -c -E "\.\.\/|etc\/passwd|wp-login|phpmyadmin|\.env|config\.php|shell\.php" "$ACCESS_LOG" 2>/dev/null)
echo "Vulnerability scan attempts: ${VULN_SCANS:-0}"
HTTP_401=$(grep -c " 401 " "$ACCESS_LOG" 2>/dev/null)
echo "HTTP 401 (Unauthorized): ${HTTP_401:-0}"
HTTP_403=$(grep -c " 403 " "$ACCESS_LOG" 2>/dev/null)
echo "HTTP 403 (Forbidden): ${HTTP_403:-0}"
fi
echo ""
# 10. MYSQL/MARIADB
echo "=== 10. Database Security ==="
if command -v mysql &>/dev/null; then
# Check if MySQL is listening on all interfaces
MYSQL_BIND=$(ss -tlnp | grep 3306 | grep "0.0.0.0")
if [ -n "$MYSQL_BIND" ]; then
echo "WARNING: MySQL is listening on ALL interfaces (0.0.0.0:3306)!"
ALERT=1
else
echo "MySQL binding: OK (localhost only or specific IP)"
fi
# Check for users without passwords
NO_PASS=$(mysql -u root -e "SELECT user, host FROM mysql.user WHERE authentication_string='' OR authentication_string IS NULL;" 2>/dev/null)
if [ -n "$NO_PASS" ]; then
echo "WARNING: Users without passwords found:"
echo "$NO_PASS"
ALERT=1
fi
fi
echo ""
# SUMMARY
echo "============================================================"
if [ $ALERT -eq 1 ]; then
echo " RESULT: WARNINGS FOUND — Review items marked WARNING above"
else
echo " RESULT: ALL CHECKS PASSED"
fi
echo "============================================================"
} | tee "$REPORT_FILE"
# Clean up old audit reports (keep 30 days)
find /var/log/ -name "security-audit-*.log" -mtime +30 -delete 2>/dev/null
chmod +x /usr/local/bin/daily-security-audit.sh
# Add to crontab — run daily at 06:00
(crontab -l 2>/dev/null; echo "0 6 * * * /usr/local/bin/daily-security-audit.sh > /dev/null 2>&1") | crontab -
11. Backup & Recovery
Security is not just about preventing attacks — it is about recovering from them. A compromised server with good backups can be rebuilt in hours. Without backups, you may lose everything.
11.1 What to Backup
| Component | Path | Priority | Frequency |
|---|---|---|---|
| Asterisk configs | /etc/asterisk/ |
Critical | Daily |
| ViciDial database | asterisk database |
Critical | Daily (full), hourly (binlog) |
| ViciDial web files | /var/www/html/vicidial/, /var/www/html/agc/ |
High | Weekly |
| Call recordings | /var/spool/asterisk/monitor/ |
High | Daily |
| Crontabs | /var/spool/cron/ |
Medium | Daily |
| Custom scripts | /usr/local/bin/, /usr/share/astguiclient/ |
High | Daily |
| AGI scripts | /var/lib/asterisk/agi-bin/ |
High | Daily |
| SSL certificates | /etc/letsencrypt/ or /etc/asterisk/keys/ |
High | Weekly |
| fail2ban config | /etc/fail2ban/ |
Medium | Weekly |
| Firewall rules | /etc/iptables/, /etc/ufw/ |
Medium | Weekly |
| System configs | /etc/ssh/sshd_config, /etc/my.cnf |
Medium | Weekly |
11.2 Automated SFTP Backup Script
This production-tested script backs up all critical components and uploads to a remote storage box via SFTP.
Save as /usr/local/bin/vicidial-backup.sh:
#!/bin/bash
# =============================================================
# ViciDial Automated Backup Script
# Backs up configs, database, and recordings to remote storage
# =============================================================
# Configuration
BACKUP_DIR="/root/backups"
REMOTE_HOST="YOUR_BACKUP_SERVER"
REMOTE_USER="YOUR_SFTP_USER"
REMOTE_PASS="YOUR_SFTP_PASSWORD"
REMOTE_DIR="/backups/$(hostname)"
DB_USER="backup"
DB_PASS="YOUR_BACKUP_DB_PASSWORD"
DB_NAME="asterisk"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M)
LOG="/var/log/backup.log"
# Create local backup directory
mkdir -p "$BACKUP_DIR"
echo "$(date): Starting backup" >> "$LOG"
# 1. Database backup (compressed)
echo "$(date): Backing up database..." >> "$LOG"
mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" \
--single-transaction \
--routines \
--triggers \
--events \
--quick \
--lock-tables=false \
| gzip > "$BACKUP_DIR/db_${DB_NAME}_${DATE}.sql.gz"
if [ $? -eq 0 ]; then
echo "$(date): Database backup OK ($(du -sh "$BACKUP_DIR/db_${DB_NAME}_${DATE}.sql.gz" | awk '{print $1}'))" >> "$LOG"
else
echo "$(date): ERROR: Database backup failed!" >> "$LOG"
fi
# 2. Asterisk configuration
echo "$(date): Backing up Asterisk configs..." >> "$LOG"
tar czf "$BACKUP_DIR/asterisk_config_${DATE}.tar.gz" \
/etc/asterisk/ \
2>/dev/null
# 3. ViciDial web files
echo "$(date): Backing up ViciDial web files..." >> "$LOG"
tar czf "$BACKUP_DIR/vicidial_web_${DATE}.tar.gz" \
/var/www/html/vicidial/ \
/var/www/html/agc/ \
2>/dev/null
# 4. Custom scripts and AGI
echo "$(date): Backing up scripts..." >> "$LOG"
tar czf "$BACKUP_DIR/scripts_${DATE}.tar.gz" \
/usr/share/astguiclient/ \
/var/lib/asterisk/agi-bin/ \
/usr/local/bin/*.sh \
2>/dev/null
# 5. Crontabs
echo "$(date): Backing up crontabs..." >> "$LOG"
crontab -l > "$BACKUP_DIR/crontab_root_${DATE}.txt" 2>/dev/null
# 6. System configurations
echo "$(date): Backing up system configs..." >> "$LOG"
tar czf "$BACKUP_DIR/system_config_${DATE}.tar.gz" \
/etc/ssh/sshd_config \
/etc/my.cnf \
/etc/fail2ban/ \
/etc/apache2/ \
/etc/httpd/ \
2>/dev/null
# 7. Upload to remote storage via SFTP
echo "$(date): Uploading to remote storage..." >> "$LOG"
sshpass -p "$REMOTE_PASS" sftp -oBatchMode=no -oStrictHostKeyChecking=no \
"$REMOTE_USER@$REMOTE_HOST" <<SFTP_EOF
-mkdir $REMOTE_DIR
cd $REMOTE_DIR
put $BACKUP_DIR/db_${DB_NAME}_${DATE}.sql.gz
put $BACKUP_DIR/asterisk_config_${DATE}.tar.gz
put $BACKUP_DIR/vicidial_web_${DATE}.tar.gz
put $BACKUP_DIR/scripts_${DATE}.tar.gz
put $BACKUP_DIR/crontab_root_${DATE}.txt
put $BACKUP_DIR/system_config_${DATE}.tar.gz
bye
SFTP_EOF
if [ $? -eq 0 ]; then
echo "$(date): Remote upload OK" >> "$LOG"
else
echo "$(date): ERROR: Remote upload failed!" >> "$LOG"
fi
# 8. Clean up old local backups
echo "$(date): Cleaning up old backups (>${RETENTION_DAYS} days)..." >> "$LOG"
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete 2>/dev/null
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete 2>/dev/null
find "$BACKUP_DIR" -name "*.txt" -mtime +$RETENTION_DAYS -delete 2>/dev/null
# 9. Verify backup sizes (detect empty/failed backups)
echo "$(date): Backup sizes:" >> "$LOG"
ls -lh "$BACKUP_DIR"/*${DATE}* >> "$LOG" 2>/dev/null
for f in "$BACKUP_DIR"/*${DATE}*; do
SIZE=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f" 2>/dev/null)
if [ "${SIZE:-0}" -lt 1000 ]; then
echo "$(date): WARNING: $f is suspiciously small (${SIZE} bytes)" >> "$LOG"
fi
done
echo "$(date): Backup complete" >> "$LOG"
echo "---" >> "$LOG"
chmod +x /usr/local/bin/vicidial-backup.sh
# Run daily at 02:00 (low traffic period)
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/vicidial-backup.sh") | crontab -
11.3 Recording Backup and Cleanup
Recordings consume the most disk space. Implement a separate policy:
# Recording cleanup script — /usr/local/bin/recording-cleanup.sh
#!/bin/bash
# Clean up old recordings after a configurable retention period
RECORDING_DIR="/var/spool/asterisk/monitor"
RETENTION_DAYS=90
LOG="/var/log/recording-cleanup.log"
echo "$(date): Starting recording cleanup (>${RETENTION_DAYS} days)" >> "$LOG"
# Count files before cleanup
BEFORE=$(find "$RECORDING_DIR" -name "*.wav" -o -name "*.mp3" -o -name "*.gsm" 2>/dev/null | wc -l)
# Delete old recordings
find "$RECORDING_DIR" -type f \( -name "*.wav" -o -name "*.mp3" -o -name "*.gsm" \) \
-mtime +$RETENTION_DAYS -delete 2>/dev/null
# Remove empty date directories
find "$RECORDING_DIR" -type d -empty -delete 2>/dev/null
# Count files after cleanup
AFTER=$(find "$RECORDING_DIR" -name "*.wav" -o -name "*.mp3" -o -name "*.gsm" 2>/dev/null | wc -l)
DELETED=$((BEFORE - AFTER))
echo "$(date): Cleanup complete. Deleted $DELETED files. Remaining: $AFTER" >> "$LOG"
echo "$(date): Disk usage: $(du -sh "$RECORDING_DIR" 2>/dev/null | awk '{print $1}')" >> "$LOG"
chmod +x /usr/local/bin/recording-cleanup.sh
# Run daily at 03:00
(crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/recording-cleanup.sh") | crontab -
11.4 Disaster Recovery Checklist
When the worst happens — server compromised, hardware failure, or data corruption — follow this sequence:
| Step | Action | Details |
|---|---|---|
| 1 | Isolate | Disconnect from network or block all traffic except your admin IP |
| 2 | Assess | Determine scope: what was accessed, what was modified, how did they get in |
| 3 | Preserve evidence | Snapshot disk, copy logs before they are overwritten |
| 4 | Rebuild or restore | Clean OS install (preferred) or restore from last known good backup |
| 5 | Restore database | gunzip < db_backup.sql.gz | mysql -u root -p asterisk |
| 6 | Restore configs | Extract Asterisk, Apache, fail2ban configs from backup tarballs |
| 7 | Change ALL credentials | SIP passwords, AMI passwords, MySQL passwords, SSH keys, admin passwords |
| 8 | Apply hardening | Apply all measures from this guide before bringing back online |
| 9 | Monitor closely | Watch logs intensively for 72 hours after restoration |
| 10 | Post-incident review | Document what happened, how, and what to improve |
Quick restore commands:
# Restore database
gunzip < /root/backups/db_asterisk_YYYYMMDD_HHMM.sql.gz | mysql -u root -p asterisk
# Restore Asterisk configs
cd /
tar xzf /root/backups/asterisk_config_YYYYMMDD_HHMM.tar.gz
# Restore ViciDial web files
cd /
tar xzf /root/backups/vicidial_web_YYYYMMDD_HHMM.tar.gz
# Restore scripts
cd /
tar xzf /root/backups/scripts_YYYYMMDD_HHMM.tar.gz
# Restore crontab
crontab /root/backups/crontab_root_YYYYMMDD_HHMM.txt
# Restart all services
systemctl restart asterisk
systemctl restart apache2
systemctl restart mysql
systemctl restart fail2ban
12. Security Audit Checklist
Use this checklist monthly to verify your security posture. The automated script in Section 10.3 covers most of these, but manual verification catches things automation misses.
Monthly Security Audit Checklist
Date: ___________ Auditor: ___________ Server: ___________
SSH & ACCESS CONTROL
[ ] SSH key-only auth is enforced (PasswordAuthentication no)
[ ] SSH is on a non-standard port
[ ] Only authorized keys in ~/.ssh/authorized_keys
[ ] No unexpected users in /etc/passwd
[ ] No unexpected entries in /etc/sudoers or /etc/sudoers.d/
[ ] AllowUsers directive in sshd_config is correct
FIREWALL
[ ] Firewall is active (ufw status / iptables -L)
[ ] No ACCEPT ALL rules in iptables
[ ] SIP (5060) restricted to trunk provider IPs only
[ ] AMI (5038) only on localhost
[ ] MySQL (3306) only on localhost (or cluster IPs)
[ ] No unexpected ports open (ss -tlnp)
FAIL2BAN
[ ] fail2ban service is running
[ ] All configured jails are active
[ ] Whitelist (ignoreip) is correct
[ ] Ban counts are reasonable (not zero — that means it is not working)
[ ] Permanent blocklist is being maintained
ASTERISK
[ ] alwaysauthreject = yes in sip.conf
[ ] allowguest = no in sip.conf
[ ] No SIP peers with weak passwords (<16 chars)
[ ] No SIP peers without ACLs (permit/deny)
[ ] AMI bindaddr = 127.0.0.1
[ ] AMI passwords are strong (>32 chars)
[ ] Unused modules are disabled
WEB SECURITY
[ ] Apache ServerTokens Prod / ServerSignature Off
[ ] HTTPS is working (certificate not expired)
[ ] Admin panel (/vicidial/) has IP restrictions
[ ] Recording directory is not web-accessible
[ ] ViciDial is patched against CVE-2024-8503/8504
[ ] PHP expose_php = Off
DATABASE
[ ] mysql_secure_installation has been run
[ ] No users without passwords
[ ] No users with ALL PRIVILEGES except root@localhost
[ ] bind-address is not 0.0.0.0 (unless multi-server)
[ ] max_statement_time is set
[ ] Reporting users have SELECT only
TOLL FRAUD
[ ] International call restrictions in dialplan
[ ] Trunk provider has daily spend cap
[ ] After-hours monitoring is active
[ ] No unusual patterns in recent CDRs
BACKUPS
[ ] Backup script is running (check last run in logs)
[ ] Remote backups are completing (verify on storage server)
[ ] Backup files are not empty (check sizes)
[ ] Test restore has been performed this quarter
MONITORING
[ ] Promtail/log agent is running and sending logs
[ ] Grafana dashboards show data
[ ] Alert rules are firing correctly (test with a known trigger)
[ ] Daily audit script is running
Automated Audit Script
Save as /usr/local/bin/security-audit-auto.sh for automated monthly checks:
#!/bin/bash
# =============================================================
# Automated Security Audit — ViciDial
# Outputs PASS/FAIL for each check
# =============================================================
PASS=0
FAIL=0
WARN=0
check() {
local name="$1"
local result="$2"
if [ "$result" = "PASS" ]; then
echo " [PASS] $name"
PASS=$((PASS + 1))
elif [ "$result" = "WARN" ]; then
echo " [WARN] $name"
WARN=$((WARN + 1))
else
echo " [FAIL] $name"
FAIL=$((FAIL + 1))
fi
}
echo "============================================================"
echo " AUTOMATED SECURITY AUDIT — $(hostname)"
echo " $(date '+%Y-%m-%d %H:%M:%S')"
echo "============================================================"
echo ""
# SSH checks
echo "--- SSH & Access Control ---"
PASS_AUTH=$(grep -c "^PasswordAuthentication no" /etc/ssh/sshd_config 2>/dev/null)
[ "$PASS_AUTH" -ge 1 ] && check "Password auth disabled" "PASS" || check "Password auth disabled" "FAIL"
SSH_PORT=$(grep "^Port " /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}')
[ "$SSH_PORT" != "22" ] && [ -n "$SSH_PORT" ] && check "SSH non-standard port ($SSH_PORT)" "PASS" || check "SSH non-standard port" "FAIL"
ROOT_LOGIN=$(grep "^PermitRootLogin" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}')
[ "$ROOT_LOGIN" = "prohibit-password" ] || [ "$ROOT_LOGIN" = "without-password" ] && check "Root login key-only" "PASS" || check "Root login key-only" "WARN"
echo ""
# Firewall checks
echo "--- Firewall ---"
FW_ACTIVE=$(iptables -L INPUT 2>/dev/null | grep -c "Chain INPUT")
[ "$FW_ACTIVE" -ge 1 ] && check "Firewall active" "PASS" || check "Firewall active" "FAIL"
ACCEPT_ALL=$(iptables -L INPUT -n 2>/dev/null | grep -c "ACCEPT all -- 0.0.0.0/0 0.0.0.0/0\s*$")
[ "$ACCEPT_ALL" -eq 0 ] && check "No ACCEPT ALL rules" "PASS" || check "No ACCEPT ALL rules" "FAIL"
AMI_LOCALHOST=$(ss -tlnp 2>/dev/null | grep ":5038" | grep -c "127.0.0.1")
AMI_ALL=$(ss -tlnp 2>/dev/null | grep ":5038" | grep -c "0.0.0.0")
[ "$AMI_ALL" -eq 0 ] && check "AMI localhost only" "PASS" || check "AMI localhost only" "FAIL"
MYSQL_LOCALHOST=$(ss -tlnp 2>/dev/null | grep ":3306" | grep -c "127.0.0.1")
MYSQL_ALL=$(ss -tlnp 2>/dev/null | grep ":3306" | grep -c "0.0.0.0")
if [ "$MYSQL_ALL" -eq 0 ]; then
check "MySQL localhost only" "PASS"
else
# Could be intentional for cluster
check "MySQL on all interfaces (verify cluster)" "WARN"
fi
echo ""
# fail2ban checks
echo "--- fail2ban ---"
F2B_RUNNING=$(systemctl is-active fail2ban 2>/dev/null)
[ "$F2B_RUNNING" = "active" ] && check "fail2ban running" "PASS" || check "fail2ban running" "FAIL"
if [ "$F2B_RUNNING" = "active" ]; then
JAIL_COUNT=$(fail2ban-client status 2>/dev/null | grep "Number of jail" | awk '{print $NF}')
[ "${JAIL_COUNT:-0}" -ge 3 ] && check "At least 3 jails active ($JAIL_COUNT)" "PASS" || check "At least 3 jails active (${JAIL_COUNT:-0})" "WARN"
fi
echo ""
# Asterisk checks
echo "--- Asterisk ---"
if [ -f /etc/asterisk/sip.conf ]; then
ALWAYS_REJECT=$(grep -c "alwaysauthreject.*=.*yes" /etc/asterisk/sip.conf 2>/dev/null)
[ "$ALWAYS_REJECT" -ge 1 ] && check "alwaysauthreject enabled" "PASS" || check "alwaysauthreject enabled" "FAIL"
NO_GUEST=$(grep -c "allowguest.*=.*no" /etc/asterisk/sip.conf 2>/dev/null)
[ "$NO_GUEST" -ge 1 ] && check "allowguest disabled" "PASS" || check "allowguest disabled" "FAIL"
fi
echo ""
# Apache checks
echo "--- Apache ---"
APACHE_CONF=$(ls /etc/apache2/httpd.conf /etc/httpd/conf/httpd.conf /etc/apache2/apache2.conf 2>/dev/null | head -1)
if [ -n "$APACHE_CONF" ]; then
SERVER_TOKENS=$(grep -c "ServerTokens Prod" "$APACHE_CONF" 2>/dev/null)
[ "$SERVER_TOKENS" -ge 1 ] && check "ServerTokens Prod" "PASS" || check "ServerTokens Prod" "WARN"
SIG_OFF=$(grep -c "ServerSignature Off" "$APACHE_CONF" 2>/dev/null)
[ "$SIG_OFF" -ge 1 ] && check "ServerSignature Off" "PASS" || check "ServerSignature Off" "WARN"
fi
EXPOSE_PHP=$(php -i 2>/dev/null | grep "expose_php" | grep -c "Off")
[ "${EXPOSE_PHP:-0}" -ge 1 ] && check "PHP expose_php Off" "PASS" || check "PHP expose_php Off" "WARN"
echo ""
# Database checks
echo "--- Database ---"
if command -v mysql &>/dev/null; then
NO_PASS_USERS=$(mysql -u root -e "SELECT COUNT(*) FROM mysql.user WHERE authentication_string='' OR authentication_string IS NULL;" -N 2>/dev/null)
[ "${NO_PASS_USERS:-1}" -eq 0 ] && check "No passwordless DB users" "PASS" || check "No passwordless DB users" "FAIL"
MAX_STMT=$(mysql -u root -e "SHOW VARIABLES LIKE 'max_statement_time';" -N 2>/dev/null | awk '{print $2}')
[ "${MAX_STMT:-0}" != "0" ] && [ -n "$MAX_STMT" ] && check "Query timeout set ($MAX_STMT)" "PASS" || check "Query timeout set" "WARN"
fi
echo ""
# Backup checks
echo "--- Backups ---"
LAST_BACKUP=$(ls -t /root/backups/db_*.sql.gz 2>/dev/null | head -1)
if [ -n "$LAST_BACKUP" ]; then
BACKUP_AGE=$(( ($(date +%s) - $(stat -c%Y "$LAST_BACKUP" 2>/dev/null || echo 0)) / 86400 ))
[ "$BACKUP_AGE" -le 2 ] && check "Recent backup exists (${BACKUP_AGE}d ago)" "PASS" || check "Recent backup (${BACKUP_AGE}d ago)" "WARN"
else
check "Database backup exists" "FAIL"
fi
echo ""
echo "============================================================"
echo " RESULTS: $PASS passed, $WARN warnings, $FAIL failed"
echo " Score: $(( PASS * 100 / (PASS + WARN + FAIL) ))%"
echo "============================================================"
chmod +x /usr/local/bin/security-audit-auto.sh
# Run monthly on the 1st at 07:00
(crontab -l 2>/dev/null; echo "0 7 1 * * /usr/local/bin/security-audit-auto.sh >> /var/log/monthly-security-audit.log 2>&1") | crontab -
13. Real Incident Case Studies
These are sanitized accounts of real security incidents on production ViciDial servers. Names, IPs, and company details have been changed, but the technical details, timelines, and lessons learned are authentic.
Case Study 1: SIP Brute Force Stopped by fail2ban
Timeline:
- Saturday 02:15 — Automated SIP scanner discovers the server on port 5060
- 02:15-02:18 — Scanner sends 847 REGISTER attempts with common usernames (100, 101, admin, test, 1000)
- 02:18 — fail2ban
asterisk-securityjail triggers after 3 failed attempts in 5 minutes - 02:18 — Attacker IP is banned for 24 hours via iptables
- 02:18-02:19 — Scanner receives no response (iptables DROP), moves on to next target
- Monday 09:00 — Admin reviews fail2ban summary, sees the event, no action needed
What worked: The fail2ban jail with a 3-attempt threshold caught the attack within 3 minutes. The 24-hour ban was sufficient — the scanner did not return.
What could be improved: SIP was still open to the world (port 5060 accessible from any IP). After this incident, SIP was restricted to trunk provider IPs only via iptables, making the fail2ban jail a backup rather than the primary defense.
Asterisk log excerpt (sanitized):
[2026-01-18 02:15:33] NOTICE: Registration from '"100"<sip:100@SERVER_IP>' failed for '203.0.113.47:5060' - No matching peer found
[2026-01-18 02:15:33] NOTICE: Registration from '"101"<sip:101@SERVER_IP>' failed for '203.0.113.47:5060' - No matching peer found
[2026-01-18 02:15:34] NOTICE: Registration from '"admin"<sip:admin@SERVER_IP>' failed for '203.0.113.47:5060' - No matching peer found
[...847 more attempts over 3 minutes...]
Case Study 2: Toll Fraud via Compromised SIP Peer
Timeline:
- Friday 23:00 — Agent leaves softphone connected with a weak SIP password (6 digits)
- Saturday 01:30 — Attacker brute-forces the SIP peer password after ~50,000 attempts (fail2ban was not installed)
- Saturday 01:35 — Attacker begins routing calls through the compromised peer to premium-rate numbers in West Africa
- Saturday 01:35-09:45 — 312 calls placed, average duration 8 minutes, total: 2,496 minutes
- Saturday 09:45 — Trunk provider's fraud detection system triggers and suspends the account
- Saturday 10:30 — Admin discovers the issue when agents cannot make calls
- Total damage: Approximately $3,700 in fraudulent call charges
What failed:
- No fail2ban — 50,000 SIP auth attempts went undetected
- Weak SIP password — 6-digit numeric password brute-forced in ~90 minutes
- No after-hours call monitoring
- No concurrent call limit on the SIP peer
- No international call restrictions in the dialplan
What was fixed:
- fail2ban installed with Asterisk SIP jail (3 attempts, 24-hour ban)
- All SIP passwords regenerated to 24+ characters
- SIP port restricted to trunk provider IPs via iptables
- Toll fraud detection script running every 6 hours
- Trunk provider set daily spend cap to $200
Case Study 3: CVE-2024-8503 Exploitation Blocked by IP Restriction
Timeline:
- Wednesday 14:22 — Vulnerability scanner identifies ViciDial admin panel on port 80
- 14:23 — Automated exploit attempts SQL injection on
/vicidial/login.php - 14:23 — Apache returns 403 Forbidden — admin panel is restricted to office IP range
- 14:24-14:30 — Scanner tries 47 variations of the SQLi payload, all receive 403
- 14:30 — Scanner gives up and moves to the agent panel (
/agc/) - 14:31 — Agent panel login page loads (not IP-restricted), but the SQLi vulnerability does not exist in the agent login
- 14:35 — Scanner moves on to the next target
What worked: The Apache IP restriction on the /vicidial/ directory prevented exploitation even though the vulnerable code was present (ViciDial had not been patched). This is defense in depth — the code was vulnerable, but the network-level control prevented access.
What was fixed afterward:
- ViciDial patched to SVN 3848+ (fixes CVE-2024-8503 and CVE-2024-8504)
- Agent panel also received IP restriction (VPN-only access)
- HTTPS enforced for both panels
- fail2ban
apache-viciauthjail added to catch future scanning
Apache access log excerpt (sanitized):
203.0.113.99 - - [12/Nov/2025:14:23:01 +0000] "POST /vicidial/login.php HTTP/1.1" 403 199 "-" "Mozilla/5.0"
203.0.113.99 - - [12/Nov/2025:14:23:02 +0000] "POST /vicidial/login.php HTTP/1.1" 403 199 "-" "Mozilla/5.0"
203.0.113.99 - - [12/Nov/2025:14:23:04 +0000] "GET /vicidial/admin.php?user=admin'%20OR%201=1-- HTTP/1.1" 403 199 "-" "Mozilla/5.0"
Case Study 4: Misconfigured Ring Group Dropping Calls
Timeline:
- Wednesday 16:00 — Admin simplifies the after-hours dialplan for a specific DID, replacing a ring group extension with
Hangup()incustomexte.conf, intending it to only affect test calls - Wednesday 18:00 — Business hours end, ViciDial inbound group switches to after-hours action which routes to the ring group extension
- Wednesday 18:00-Thursday 08:30 — All inbound calls to that DID are silently hung up
- Thursday 08:30 — Agent reports that customers are complaining they cannot reach the company
- Thursday 09:00 — Admin discovers the issue in the dialplan and restores the ring group
- Total impact: 11+ missed calls overnight and the following morning, customer complaints, lost business
What failed:
- The ring group was the last safety net for callers when no agents were logged in
- The change was made directly in production without testing
- No monitoring for call volume drops on that DID
- No backup of the config file before editing
What was fixed:
- Ring group extensions now have comments explaining their purpose
- All dialplan changes require explicit approval and a backup (
cp file file.bak.YYYYMMDD) - Monitoring added for call volume drops (alerts if call count drops >50% vs. same day previous week)
- A rule was established: never replace a ring group with
Hangup()— if the goal is to stop routing, change the ViciDial inbound group action instead of the Asterisk dialplan
Key lesson: Ring groups and fallback extensions are safety nets. They exist for the cases you have not thought of. Removing them causes silent failures that are only detected when customers complain — and by then, you have already lost calls and trust.
Conclusion
Security is not a one-time task — it is an ongoing process. The measures in this guide provide a strong foundation, but they require maintenance:
- Weekly: Review fail2ban ban summaries, check disk space, verify backups
- Monthly: Run the full security audit checklist, review firewall rules, check for ViciDial updates
- Quarterly: Test your backup restoration process, review and rotate credentials, audit user accounts
- After any incident: Conduct a post-incident review, update this guide with new lessons learned
The most important takeaway: defense in depth. No single measure is sufficient. Each layer catches what the previous layer missed:
- Firewall blocks most traffic before it reaches any service
- fail2ban catches what gets through the firewall
- Strong passwords and ACLs prevent successful authentication even if fail2ban misses the attempt
- Dialplan restrictions prevent toll fraud even if a peer is compromised
- Monitoring detects what all other measures missed
- Backups let you recover when everything else fails
Apply these layers, maintain them, and your ViciDial server will be significantly harder to compromise than the thousands of unprotected installations that attackers will target instead.
Tutorial 36 of the Production VoIP Tutorial Series Technologies: ViciDial, Asterisk, fail2ban, UFW, iptables, Apache, MariaDB, Let's Encrypt, Loki