How to Build and Deploy Asterisk 22 LTS on Debian 13 with PJSIP, TLS/SRTP and fail2ban
A comprehensive, battle-tested guide to compiling Asterisk 22 LTS from source on Debian 13 Trixie, deploying it in an Incus container, configuring PJSIP trunks and extensions, securing with TLS/SRTP and fail2ban, and running a production PBX with music on hold, call forwarding and transfers. Covers ARM (aarch64) and x86 architectures.
Asterisk version: 22 LTS (supported until 2029) OS: Debian 13 (Trixie) Container: Incus (works equally with LXD)
Why Compile from Source?
Asterisk 22 is the current Long Term Support release. While Debian 13 includes an Asterisk package, it has unresolved dependency issues and unpatched PJSIP vulnerabilities at the time of writing. Compiling from source gives you full control over the build, the ability to patch quickly when security advisories drop, and a clean production environment without compiler toolchains.
This guide uses two separate containers: one for building and one for production. The build container handles all the compilation dependencies (gcc, make, dev headers), while the production container only needs runtime libraries. This keeps the production environment minimal and reduces the attack surface.
Part 1: Compiling Asterisk
Create the Build Container
Launch a fresh Debian 13 container for compilation:
incus launch images:debian/13 asterisk-build
incus shell asterisk-build
apt update && apt upgrade -yInstall Build Dependencies
apt install -y build-essential git wget curl \
libncurses-dev libssl-dev libxml2-dev \
libsqlite3-dev uuid-dev libjansson-dev libspeex-dev \
libspeexdsp-dev libogg-dev libvorbis-dev libasound2-dev \
libcurl4-openssl-dev libical-dev libneon27-dev libsrtp2-dev \
unixodbc-dev libiksemel-dev libnewt-dev libpopt-dev \
libedit-dev libradcli-dev libopus-dev libsystemd-dev \
subversion checkinstallNotable packages:
libsystemd-devenables systemd notify integration so Asterisk signals readiness to systemdlibsrtp2-devprovides SRTP support for encrypted media streamslibopus-devprovides the Opus codec (only available on x86; on ARM it is skipped)checkinstallwraps the compiled output into a .deb package for clean deploymentsubversionis required by the MP3 source download script
A note on libjansson-dev: Debian 13 ships version 2.14 of the Jansson JSON library, which exceeds the minimum requirement of 2.11. There is no need to use --with-jansson-bundled as some guides suggest. That flag is only useful on distributions shipping older Jansson versions.
Download and Extract
cd /usr/src
wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-22-current.tar.gz
tar -xzf asterisk-22-current.tar.gz
cd asterisk-22*/Add MP3 Support
contrib/scripts/get_mp3_source.sh
contrib/scripts/install_prereq installConfigure
Since libpjproject-dev is not available in Debian 13 repositories, use the bundled PJSIP:
./configure --with-pjproject-bundledSelect Modules
make menuselectUnder Add-ons: verify format_mp3 is enabled. Under Codec Translators: disable codec_opus on ARM platforms (only available as an external binary for x86). Save and exit.
Compile
make -j$(nproc)On ARM (Hetzner CAX31), compilation takes approximately 10 to 15 minutes.
Package as .deb
checkinstall --pkgname=asterisk --pkgversion=22.9.0 --pkgrelease=1 \
--pkggroup=comm --maintainer="you@example.com" --nodoc -y make installImportant: if you need to rebuild later (for example, to add a missing library), remove the previous package first with dpkg -r asterisk before running checkinstall again. Otherwise make install fails with “File exists” errors.
Copy to Host
exit # Leave build container
mkdir -p /root/asterisk
incus file pull asterisk-build/usr/src/asterisk-22.9.0/asterisk_22.9.0-1_arm64.deb /root/asterisk/Part 2: Production Container Setup
Create the Container
incus launch images:debian/13 asterisk -d eth0,ipv4.address=10.100.0.50
incus shell asterisk
apt update && apt upgrade -yInstall Asterisk and Dependencies
Push the .deb from the host:
# From the host
incus file push /root/asterisk/asterisk_22.9.0-1_arm64.deb asterisk/root/
incus shell asteriskInstall:
dpkg -i /root/asterisk_22.9.0-1_arm64.deb
apt install -y libxslt1.1 libjansson4 liburiparser1 libsrtp2-1 libgsm1 libspeex1 libspeexdsp1Verify all shared library dependencies are satisfied:
ldd /usr/sbin/asterisk | grep "not found"If this returns no output, all dependencies are resolved. Then verify:
asterisk -VInstall Additional Packages
The minimal Debian 13 container does not include cron or nftables. Install them and remove the unnecessary mail server that cron pulls in:
apt install -y cron nftables curl socat
apt purge -y exim4-base exim4-config exim4-daemon-light
apt autoremove -yCreate Asterisk User and Directories
groupadd asterisk
useradd -r -d /var/lib/asterisk -g asterisk -s /usr/sbin/nologin asterisk
mkdir -p /etc/asterisk /etc/asterisk/pjsip.d /etc/asterisk/extensions.d /etc/asterisk/tls
mkdir -p /var/lib/asterisk/{sounds,moh,keys}
mkdir -p /var/log/asterisk
mkdir -p /var/spool/asterisk/voicemail
mkdir -p /var/run/asterisk
chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asteriskInstall Sound Files and Music on Hold
cd /var/lib/asterisk/sounds
wget https://downloads.asterisk.org/pub/telephony/sounds/asterisk-core-sounds-en-wav-current.tar.gz
tar -xzf asterisk-core-sounds-en-wav-current.tar.gz
rm asterisk-core-sounds-en-wav-current.tar.gz
wget https://downloads.asterisk.org/pub/telephony/sounds/asterisk-moh-opsound-wav-current.tar.gz
tar -xzf asterisk-moh-opsound-wav-current.tar.gz -C /var/lib/asterisk/moh/
rm asterisk-moh-opsound-wav-current.tar.gz
chown -R asterisk:asterisk /var/lib/asterisk/sounds /var/lib/asterisk/mohPart 3: Configuration Files
The key principle is autoload = no with explicit module loading. This is cleaner and more secure than autoloading hundreds of modules and suppressing errors from unused ones.
asterisk.conf
cat > /etc/asterisk/asterisk.conf << 'EOF'
[directories]
astetcdir => /etc/asterisk
astmoddir => /usr/lib/asterisk/modules
astvarlibdir => /var/lib/asterisk
astdbdir => /var/lib/asterisk
astkeydir => /var/lib/asterisk
astdatadir => /var/lib/asterisk
astagidir => /var/lib/asterisk/agi-bin
astspooldir => /var/spool/asterisk
astrundir => /var/run/asterisk
astlogdir => /var/log/asterisk
[options]
runuser = asterisk
rungroup = asterisk
documentation_language = en_US
[files]
astctlpermissions = 0660
astctlowner = asterisk
astctlgroup = asterisk
EOFmodules.conf
Every module in this list was verified as necessary during deployment. Modules were added one by one as errors and missing functionality revealed dependencies.
cat > /etc/asterisk/modules.conf << 'EOF'
[modules]
autoload = no
; Timing (required for DTMF detection)
load => res_timing_timerfd.so
; Sorcery (internal PJSIP dependency framework)
load => res_sorcery_astdb.so
load => res_sorcery_config.so
load => res_sorcery_memory.so
load => res_sorcery_memory_cache.so
load => res_sorcery_realtime.so
; Core
load => res_musiconhold.so
load => res_rtp_asterisk.so
load => res_crypto.so
load => res_srtp.so
; Security logging
load => res_security_log.so
; PJSIP stack
load => res_pjproject.so
load => res_pjsip.so
load => res_pjsip_authenticator_digest.so
load => res_pjsip_caller_id.so
load => res_pjsip_dtmf_info.so
load => res_pjsip_endpoint_identifier_anonymous.so
load => res_pjsip_endpoint_identifier_ip.so
load => res_pjsip_endpoint_identifier_user.so
load => res_pjsip_header_funcs.so
load => res_pjsip_logger.so
load => res_pjsip_nat.so
load => res_pjsip_outbound_authenticator_digest.so
load => res_pjsip_outbound_registration.so
load => res_pjsip_path.so
load => res_pjsip_pubsub.so
load => res_pjsip_refer.so
load => res_pjsip_registrar.so
load => res_pjsip_rfc3326.so
load => res_pjsip_sdp_rtp.so
load => res_pjsip_session.so
load => chan_pjsip.so
; Dialplan
load => pbx_config.so
; Core applications
load => app_dial.so
load => app_playback.so
load => app_voicemail.so
load => app_directory.so
load => app_echo.so
load => app_transfer.so
load => app_verbose.so
load => app_stack.so
load => app_senddtmf.so
; Functions
load => func_callerid.so
load => func_channel.so
load => func_strings.so
load => func_timeout.so
load => func_logic.so
load => func_db.so
; Codecs
load => codec_gsm.so
load => codec_ulaw.so
load => codec_alaw.so
load => codec_g722.so
; Formats
load => format_gsm.so
load => format_pcm.so
load => format_wav.so
load => format_wav_gsm.so
load => format_mp3.so
load => format_sln.so
; Bridges
load => bridge_simple.so
load => bridge_softmix.so
load => bridge_builtin_features.so
load => bridge_holding.so
; NOTE: bridge_native_rtp is intentionally NOT loaded.
; Native RTP bridging bypasses Asterisk's media path,
; which prevents Music on Hold, call recording, and
; DTMF detection from working. For small deployments
; the CPU difference is negligible.
EOFKey modules and why they are needed:
res_timing_timerfd.so— without this, DTMF detection fails with “No timing module loaded” warnings, and Asterisk may crashres_sorcery_*.so(5 modules) — internal PJSIP dependency framework; missing these causes silent registration failuresres_security_log.so— structured security event logging to a dedicated log file, required for fail2ban integrationres_pjsip_refer.so— enables SIP REFER for call transfers via softphone transfer buttonsres_pjsip_pubsub.so— required by dialog and presence modulesfunc_db.so— AstDB functions for call forwarding; without this,${DB()}returns empty and forwarding silently failsbridge_softmix.so— required for Music on Hold to work after disabling native RTP bridging
logger.conf
cat > /etc/asterisk/logger.conf << 'EOF'
[general]
dateformat = %F %T
[logfiles]
console => notice,warning,error
messages => notice,warning,error
full => notice,warning,error,debug,verbose
security => security
EOFThe security => security line creates /var/log/asterisk/security with structured authentication events. This is the log that fail2ban parses to detect brute force attacks.
rtp.conf
cat > /etc/asterisk/rtp.conf << 'EOF'
[general]
rtpstart = 16000
rtpend = 16100
strictrtp = yes
icesupport = no
EOFA small port range (100 ports) is sufficient for a typical office PBX and minimizes the firewall attack surface. SRTP uses these same ports; no additional ports are needed for encrypted media.
indications.conf
Replace the tone definitions with the correct values for your country. This example uses Belgian tones:
cat > /etc/asterisk/indications.conf << 'EOF'
[general]
country = be
[be]
description = Belgium
ringcadence = 1000,3000
dial = 425
busy = 425/500,0/500
ring = 425/1000,0/3000
congestion = 425/167,0/167
callwaiting = 1400/175,0/175,1400/175,0/3500
dialrecall = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440
record = 1400/500,0/15000
info = 900/330,1400/330,1800/330,0/1000
stutter = 425/1000,0/250
EOFfeatures.conf
cat > /etc/asterisk/features.conf << 'EOF'
[general]
[featuremap]
blindxfer => ##
atxfer => *2
EOFNote: softphones like MicroSIP use their built-in transfer button (SIP REFER) rather than DTMF feature codes. The ## and *2 codes are primarily useful for hardware phones.
Supporting Configuration Files
These files silence warning messages from modules that look for them at startup:
cat > /etc/asterisk/stasis.conf << 'EOF'
[threadpool]
initial_size = 5
idle_timeout_sec = 20
max_size = 50
minimum_size = 5
EOF
cat > /etc/asterisk/pjproject.conf << 'EOF'
[startup]
type = startup
EOF
cat > /etc/asterisk/cdr.conf << 'EOF'
[general]
enable = yes
EOF
cat > /etc/asterisk/cel.conf << 'EOF'
[general]
enable = no
EOF
echo -n > /etc/asterisk/acl.conf
cat > /etc/asterisk/udptl.conf << 'EOF'
[general]
EOF
cat > /etc/asterisk/ccss.conf << 'EOF'
[general]
EOF
cat > /etc/asterisk/manager.conf << 'EOF'
[general]
enabled = no
EOF
cat > /etc/asterisk/voicemail.conf << 'EOF'
[general]
format = wav49|gsm|wav
serveremail = asterisk@localhost
attach = yes
maxmsg = 100
maxsecs = 300
[default]
EOF
cat > /etc/asterisk/musiconhold.conf << 'EOF'
[default]
mode = files
directory = /var/lib/asterisk/moh
EOFPart 4: PJSIP Configuration
File Structure
PJSIP configuration is split into separate files for maintainability. Each SIP trunk gets its own file. All extensions are in one file. This structure becomes a template for multi-tenant deployments.
/etc/asterisk/pjsip.conf → global, transports, includes
/etc/asterisk/pjsip.d/trunk-*.conf → one file per SIP trunk
/etc/asterisk/pjsip.d/extensions.conf → all extensionspjsip.conf — Global Settings and Transports
cat > /etc/asterisk/pjsip.conf << 'EOF'
; =============================================
; Global settings
; =============================================
[global]
type = global
user_agent = Asterisk PBX
unidentified_request_count = 3
unidentified_request_period = 5
; =============================================
; Transport - UDP (SIP trunk)
; =============================================
[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
external_media_address = 203.0.113.50
external_signaling_address = 203.0.113.50
local_net = 10.100.0.0/24
; =============================================
; Transport - TLS (phones)
; =============================================
[transport-tls]
type = transport
protocol = tls
bind = 0.0.0.0:5061
external_media_address = 203.0.113.50
external_signaling_address = 203.0.113.50
local_net = 10.100.0.0/24
cert_file = /etc/asterisk/tls/yourdomain-fullchain.crt
priv_key_file = /etc/asterisk/tls/yourdomain.key
method = tlsv1_2
; =============================================
; Include trunks and extensions
; =============================================
#include pjsip.d/*.conf
EOFReplace 203.0.113.50 with your server’s public IP and 10.100.0.0/24 with your container network range.
The unidentified_request_count and unidentified_request_period settings are critical for security. They tell PJSIP to drop requests from unidentified sources after 3 attempts within 5 seconds, reducing log spam and CPU load from brute force attacks. Without these settings, a single attacker can flood your server with hundreds of registration attempts per second.
Trunk Configuration
Each SIP trunk provider number gets its own file. This example shows a trunk registered with a SIP provider:
cat > /etc/asterisk/pjsip.d/trunk-01234567890.conf << 'EOF'
; =============================================
; SIP trunk - 01234567890
; =============================================
[trunk-01234567890]
type = registration
transport = transport-udp
outbound_auth = trunk-01234567890-auth
server_uri = sip:sip.provider.com
client_uri = sip:01234567890@sip.provider.com
contact_user = 01234567890
retry_interval = 60
max_retries = 0
auth_rejection_permanent = no
forbidden_retry_interval = 300
expiration = 120
[trunk-01234567890-auth]
type = auth
auth_type = userpass
username = 01234567890
password = your-trunk-password
[trunk-01234567890-aor]
type = aor
contact = sip:sip.provider.com
[trunk-01234567890-identify]
type = identify
endpoint = trunk-01234567890-endpoint
match = sip.provider.com
[trunk-01234567890-endpoint]
type = endpoint
transport = transport-udp
context = from-trunk
disallow = all
allow = alaw
allow = ulaw
allow = g722
outbound_auth = trunk-01234567890-auth
aors = trunk-01234567890-aor
from_user = 01234567890
from_domain = sip.provider.com
direct_media = no
force_rport = yes
rewrite_contact = yes
rtp_symmetric = yes
dtmf_mode = rfc4733
EOFThree critical settings that are easy to miss:
contact_user — without this, Asterisk registers with Contact: <sip:s@your-ip> instead of Contact: <sip:01234567890@your-ip>. The provider then sends inbound calls to extension s instead of the actual phone number, causing “extension not found” errors.
max_retries = 0 — this means infinite retries. The default is a limited number, and once reached, Asterisk permanently stops trying to register. If your provider has a brief outage, Asterisk gives up after a few minutes and never recovers until you manually reload. This is a production-breaking default that differs from the old chan_sip behaviour which retried indefinitely.
auth_rejection_permanent = no — the default is yes, which means if the provider ever returns a 401/403 rejection (even temporarily during maintenance), PJSIP treats it as a permanent failure and stops trying to register forever. This is a well-known, long-standing issue that has bitten many production deployments. Setting it to no tells PJSIP to keep retrying after authentication rejections.
forbidden_retry_interval = 300 — after a 403 Forbidden response, wait 5 minutes then try again. Without this, a temporary provider-side issue causes permanent registration failure.
direct_media = no — prevents Asterisk from stepping out of the media path. Required for Music on Hold, call recording and DTMF detection to work.
Extension Configuration
Pre-provision extensions with random passwords using a loop:
# Create the first extension manually
cat > /etc/asterisk/pjsip.d/extensions.conf << EOF
; =============================================
; Extension 200
; =============================================
[200]
type = endpoint
context = from-internal
disallow = all
allow = alaw
allow = ulaw
allow = g722
auth = 200-auth
aors = 200
callerid = "Reception" <01234567890>
direct_media = no
force_rport = yes
rewrite_contact = yes
rtp_symmetric = yes
dtmf_mode = rfc4733
media_encryption_optimistic = yes
[200-auth]
type = auth
auth_type = userpass
username = 200
password = $(openssl rand -base64 18)
[200]
type = aor
max_contacts = 3
remove_existing = yes
qualify_frequency = 30
EOF
# Append extensions 201-209 with random passwords
for i in $(seq 201 209); do
PW=$(openssl rand -base64 18)
cat >> /etc/asterisk/pjsip.d/extensions.conf << EOF
; =============================================
; Extension $i
; =============================================
[$i]
type = endpoint
context = from-internal
disallow = all
allow = alaw
allow = ulaw
allow = g722
auth = $i-auth
aors = $i
callerid = "Extension $i" <01234567890>
direct_media = no
force_rport = yes
rewrite_contact = yes
rtp_symmetric = yes
dtmf_mode = rfc4733
media_encryption_optimistic = yes
[$i-auth]
type = auth
auth_type = userpass
username = $i
password = $PW
[$i]
type = aor
max_contacts = 3
remove_existing = yes
qualify_frequency = 30
EOF
doneNote the use of unquoted EOF (no single quotes) so that variables expand. Extension 200 is created first with its own block, then 201-209 are appended in a loop with unique random passwords.
Key design decisions:
No transport line on endpoints — this is intentional. Without a transport directive, Asterisk auto-detects the transport based on how the phone registers. This allows TLS softphones and UDP hardware phones to coexist on the same endpoint configuration. If you specify transport = transport-tls, UDP phones cannot register.
media_encryption_optimistic = yes — Asterisk offers SRTP to all phones but accepts unencrypted RTP if the phone does not support it. This is essential for mixed environments where some devices support TLS/SRTP and others do not. Using media_encryption = sdes instead would reject phones that do not support SRTP.
Extension range 200-209 — deliberately avoids the 100 range. In Belgium (and many European countries), emergency numbers like 100, 101 and 112 fall in the 1XX range. Using extensions in that range creates ambiguity in the dialplan. The 200 range is safe.
Part 5: Dialplan
File Structure
/etc/asterisk/extensions.conf → general settings, includes
/etc/asterisk/extensions.d/inbound-*.conf → one file per DID
/etc/asterisk/extensions.d/internal.conf → feature codes, outbound, emergencyextensions.conf
cat > /etc/asterisk/extensions.conf << 'EOF'
[general]
static = yes
writeprotect = no
#include extensions.d/*.conf
EOFInbound Route (one file per DID)
Each DID gets its own file. Asterisk merges the [from-trunk] context across all included files.
cat > /etc/asterisk/extensions.d/inbound-01234567890.conf << 'EOF'
; Inbound routing for 01234567890 (main number)
[from-trunk]
exten => 01234567890,1,NoOp(Inbound call from ${CALLERID(all)} to ${EXTEN})
same => n,Set(CHANNEL(tonezone)=be)
same => n,Set(FWD=${DB(CF/01234567890)})
same => n,GotoIf($[${LEN(${FWD})} > 0]?forward)
same => n,Dial(PJSIP/200,20)
same => n,VoiceMail(200@default,u)
same => n,Hangup()
same => n(forward),Dial(PJSIP/${FWD}@trunk-01234567890-endpoint,30)
same => n,VoiceMail(200@default,u)
same => n,Hangup()
EOFThe forwarding logic uses AstDB: if a forwarding number is stored for this DID, the call is forwarded to that number via the trunk. If no forwarding is set, the call rings the extension.
Internal Dialplan
cat > /etc/asterisk/extensions.d/internal.conf << 'EOF'
; =============================================
; Internal/outbound calls
; =============================================
[from-internal]
; Internal extension dialing
exten => _20X,1,NoOp(Internal call to extension ${EXTEN})
same => n,Dial(PJSIP/${EXTEN},30)
same => n,VoiceMail(${EXTEN}@default,u)
same => n,Hangup()
; Echo test
exten => *43,1,Answer()
same => n,Echo()
same => n,Hangup()
; Voicemail access
exten => *97,1,Answer()
same => n,VoiceMailMain(${CALLERID(num)}@default)
same => n,Hangup()
; Enable call forward on all DIDs: dial *72 followed by number
exten => _*72.,1,Answer()
same => n,Set(DB(CF/01234567890)=${EXTEN:3})
same => n,Playback(beep)
same => n,Hangup()
; Disable call forward on all DIDs
exten => *73,1,Answer()
same => n,Set(dummy=${DB_DELETE(CF/01234567890)})
same => n,Playback(beep)
same => n,Hangup()
; Emergency numbers (adjust for your country)
exten => 100,1,Dial(PJSIP/${EXTEN}@trunk-01234567890-endpoint,120)
exten => 101,1,Dial(PJSIP/${EXTEN}@trunk-01234567890-endpoint,120)
exten => 112,1,Dial(PJSIP/${EXTEN}@trunk-01234567890-endpoint,120)
; Outbound via trunk (main number as caller ID)
exten => _0X.,1,NoOp(Outbound call to ${EXTEN})
same => n,Set(CALLERID(num)=01234567890)
same => n,Dial(PJSIP/${EXTEN}@trunk-01234567890-endpoint,120)
same => n,Hangup()
EOFNote: the call forwarding disable code uses DB_DELETE() function, not DBdel application. The old DBdel application does not exist in modern Asterisk and will cause “No application ‘DBdel'” errors.
Set Ownership
chown -R asterisk:asterisk /etc/asteriskPart 6: TLS/SRTP for Encrypted Phone Connections
Obtain a Wildcard Certificate
For a hosted PBX setup, a wildcard certificate covers all future instances (pbx10, pbx11, pbx12, etc.). This example uses acme.sh with Let’s Encrypt and manual DNS validation:
curl https://get.acme.sh | sh
/root/.acme.sh/acme.sh --set-default-ca --server letsencrypt
/root/.acme.sh/acme.sh --issue -d "*.yourdomain.eu" --dns --yes-I-know-dns-manual-mode-enough-go-ahead-pleaseAdd the TXT record to your DNS, then complete:
/root/.acme.sh/acme.sh --renew -d "*.yourdomain.eu" --dns --yes-I-know-dns-manual-mode-enough-go-ahead-pleaseInstall the certificate:
/root/.acme.sh/acme.sh --install-cert -d "*.yourdomain.eu" \
--cert-file /etc/asterisk/tls/yourdomain.crt \
--key-file /etc/asterisk/tls/yourdomain.key \
--fullchain-file /etc/asterisk/tls/yourdomain-fullchain.crt \
--reloadcmd "asterisk -rx 'core reload'"
chown -R asterisk:asterisk /etc/asterisk/tls
chmod 640 /etc/asterisk/tls/*.keyFor automated renewal, configure acme.sh with your DNS provider’s API (PowerDNS, Cloudflare, Porkbun, etc.) instead of manual mode.
TLS Transport Configuration
The TLS transport is already included in the pjsip.conf from Part 4. It uses method = tlsv1_2 which enforces TLS 1.2 as the minimum. Older devices with outdated SSL libraries (such as the Cisco SPA504G with firmware from 2010) will not be able to connect via TLS. These devices should continue using UDP on port 5060.
SRTP Configuration
SRTP is configured per-endpoint with media_encryption_optimistic = yes, already included in the extensions configuration from Part 4. This offers SRTP to all phones but falls back to plain RTP for devices that do not support it.
Mixed Transport Environments
A known PJSIP limitation: when multiple devices register for the same extension using different transports (for example, one on TLS and another on UDP), Asterisk cannot fork inbound calls to both contacts simultaneously. It will only ring one device, typically the TLS one.
The workaround is to assign separate extension numbers to devices on different transports and ring both in the dialplan:
exten => 01234567890,n,Dial(PJSIP/200&PJSIP/202,20)This rings extension 200 (TLS softphone) and 202 (UDP desk phone) simultaneously.
Part 7: Systemd Service
cat > /etc/systemd/system/asterisk.service << 'EOF'
[Unit]
Description=Asterisk PBX and telephony daemon.
After=network.target
[Service]
Type=notify
Environment=HOME=/var/lib/asterisk
WorkingDirectory=/var/lib/asterisk
User=asterisk
Group=asterisk
ExecStart=/usr/sbin/asterisk -mqf -C /etc/asterisk/asterisk.conf
ExecReload=/usr/sbin/asterisk -rx 'core reload'
RuntimeDirectory=asterisk
LimitCORE=infinity
Restart=always
RestartSec=4
StandardOutput=null
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable asterisk
systemctl start asteriskKey settings:
Type=notifyrequireslibsystemd-devcompiled in. Asterisk signals readiness when all modules are loaded.RuntimeDirectory=asterisktells systemd to create/var/run/asteriskwith the correct ownership on every start. This is essential in container environments where/var/runis a tmpfs that gets cleared on restart. Without this, the control socket directory is missing andasterisk -rxcommands fail.Restart=alwayswithRestartSec=4ensures Asterisk recovers from crashes. This was proven during deployment when an Asterisk crash during DTMF testing was automatically recovered within 4 seconds.
Part 8: Firewall Rules (Host)
On the Incus host, forward SIP and RTP traffic to the Asterisk container. Replace 10.100.0.50 with your container’s IP:
# SIP trunk (UDP, source restricted to provider IPs)
iifname $WAN ip saddr { provider-sip-ip, provider-media-subnet/24 } udp dport 5060 dnat to 10.100.0.50
# SIP phones (UDP, open for remote phones)
iifname $WAN udp dport 5060 dnat to 10.100.0.50
# SIP phones (TLS)
iifname $WAN tcp dport 5061 dnat to 10.100.0.50
# RTP/SRTP media
iifname $WAN udp dport 16000-16100 dnat to 10.100.0.50Note: TLS uses TCP, not UDP. SRTP uses the same RTP ports as regular RTP; no additional port forwarding is needed for encrypted media.
Part 9: Security with fail2ban
Install and Configure
apt install -y fail2banCreate Filters
The asterisk-security filter matches authentication failures from the dedicated security log:
cat > /etc/fail2ban/filter.d/asterisk-security.conf << 'EOF'
[Definition]
failregex = SecurityEvent="FailedACL".*RemoteAddress="IPV4/UDP/<HOST>/.*"
SecurityEvent="InvalidAccountID".*RemoteAddress="IPV4/UDP/<HOST>/.*"
SecurityEvent="InvalidPassword".*RemoteAddress="IPV4/UDP/<HOST>/.*"
SecurityEvent="ChallengeResponseFailed".*RemoteAddress="IPV4/UDP/<HOST>/.*"
ignoreregex =
EOFThe asterisk-probe filter catches endpoint scanning from the main log:
cat > /etc/fail2ban/filter.d/asterisk-probe.conf << 'EOF'
[Definition]
failregex = Request '.*' from '.*' failed for '<HOST>:\d+' .* - No matching endpoint found
Request '.*' from '.*' failed for '<HOST>:\d+' .* - Failed to authenticate
ignoreregex =
EOFConfigure Jails
cat > /etc/fail2ban/jail.d/sshd.conf << 'EOF'
[sshd]
enabled = false
EOF
cat > /etc/fail2ban/jail.d/asterisk.conf << 'EOF'
[asterisk]
enabled = true
backend = auto
filter = asterisk-security
logpath = /var/log/asterisk/security
protocol = tcp,udp
maxretry = 3
findtime = 600
bantime = 86400
banaction = nftables-allports
[asterisk-probe]
enabled = true
backend = auto
filter = asterisk-probe
logpath = /var/log/asterisk/full
protocol = tcp,udp
maxretry = 1
findtime = 600
bantime = 86400
banaction = nftables-allports
[recidive]
enabled = true
backend = auto
logpath = /var/log/fail2ban.log
maxretry = 2
findtime = 604800
bantime = 2592000
banaction = nftables-allports
EOFWhitelist your own IP to avoid locking yourself out during testing:
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
ignoreip = 127.0.0.1/8 your.public.ip.here
EOFEnable and start:
systemctl enable fail2ban
systemctl start fail2banBan Strategy
The three jails provide layered protection:
- asterisk — 3 wrong passwords within 10 minutes triggers a 24-hour ban. Catches brute force authentication attempts.
- asterisk-probe — a single endpoint scan with a non-existent username triggers a 24-hour ban. No legitimate user ever triggers “No matching endpoint found.” This is aggressive by design.
- recidive — if an IP is banned twice within 7 days, it gets banned for 30 days. Catches persistent attackers.
Both TCP and UDP are blocked (protocol = tcp,udp). The default fail2ban nftables action only blocks TCP, which is insufficient for SIP which primarily uses UDP.
Registration Monitoring
PJSIP has a known issue where trunk registrations can permanently stop retrying after a provider outage, even with max_retries = 0 and auth_rejection_permanent = no. A core reload is often insufficient to reset the PJSIP registration state machine; a full service restart is required. A monitoring script provides a safety net:
cat > /usr/local/bin/check-sip-reg.sh << 'EOF'
#!/bin/bash
REJECTED=$(asterisk -rx "pjsip show registrations" | grep -c "Rejected")
if [ "$REJECTED" -gt 0 ]; then
logger -t asterisk-monitor "WARNING: $REJECTED SIP registrations rejected, restarting Asterisk"
systemctl restart asterisk
fi
EOF
chmod +x /usr/local/bin/check-sip-reg.sh
echo "*/5 * * * * root /usr/local/bin/check-sip-reg.sh" > /etc/cron.d/asterisk-monitorThe script checks every 5 minutes. Using systemctl restart asterisk instead of asterisk -rx "core reload" because a full restart resets the PJSIP registration state machine completely. A reload often leaves stuck registrations in their failed state.
Part 10: Testing Checklist
After completing the setup, verify each feature:
- Trunk registration —
asterisk -rx "pjsip show registrations"shows all trunks as Registered - Endpoint registration —
asterisk -rx "pjsip show endpoints"shows phones as Available - Outbound calls — dial an external number from a registered phone
- Inbound calls — call each DID from a mobile phone
- Internal calls — dial between extensions (e.g. 200 to 204)
- Echo test — dial
*43to verify audio path - Music on Hold — press hold during a call; the remote party should hear music
- Call transfer — use the softphone’s transfer button to transfer a call to another extension
- Call forwarding — dial
*72followed by a number to enable,*73to disable - TLS/SRTP — verify with
asterisk -rx "pjsip show endpoints"that TLS contacts showtransport=TLS - fail2ban —
fail2ban-client statusshows all jails active - Security log —
tail /var/log/asterisk/securityshows authentication events
Keeping Asterisk Updated
Since we compiled from source, there is no apt upgrade for Asterisk. Subscribe to the Asterisk Security Advisories mailing list at asterisk.org. The update workflow:
- Start the build container
- Download the new source tarball
make clean,./configure --with-pjproject-bundled,make menuselect,make- Remove the old package:
dpkg -r asterisk - Package with
checkinstallusing an incremented release number - Push the new .deb to the production container
dpkg -iand restart the service
What is Next
Part 2 covers advanced features: IVR/auto-attendant, ring groups, time-based routing, call recording and voicemail to email.
