SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor

Self-hosted VoIP server: Asterisk and FreePBX

Run Asterisk or FreePBX on your own VPS: SIP trunks, RTP port ranges, the firewall rules that keep scanners out, and how toll fraud starts.

What a self-hosted VoIP server is

A self-hosted VoIP server is a SIP server running on a VPS you control, so your calls are routed by your own machine instead of by a hosted phone service. VoIP (voice over IP) carries speech as UDP packets. SIP (session initiation protocol) is the signalling that sets up and ends a call. The audio does not travel over SIP, and that one fact causes most of the trouble below.

Four parts make a working system.

  • The PBX (private branch exchange) software. Asterisk is the usual choice. It holds the extensions and the dialplan.
  • The endpoints. Desk phones or softphones that register to the PBX with a username and a secret.
  • The SIP trunk. A paid account with a provider that connects you to the public telephone network and rents you real phone numbers.
  • The media path. RTP (real-time transport protocol) flows that carry the audio on their own UDP ports.

Self-hosting the PBX does not mean self-hosting phone numbers. Numbers come from a carrier, and you still pay per number and per minute. What you own is the call routing, the voicemail, the recordings and the extension list. You also own the security of a service that people attack for money.

Which ports does a self-hosted VoIP server need?

SIP signalling uses port 5060 for UDP and TCP, and port 5061 for SIP over TLS (transport layer security). Those ports carry call setup only. The audio of each call is a separate UDP flow, sent to a port taken from the RTP range. Asterisk ships a sample rtp.conf that sets rtpstart=10000 and rtpend=20000, and the compiled-in defaults are 5000 and 31000. Each call takes two ports from that range, one for RTP and one for RTCP (RTP control protocol).

This split is where most first attempts fail. The call connects, both phones show it as answered, and neither side hears anything, because the firewall allows 5060 and drops every RTP packet. Signalling and media are separate flows, so they need separate firewall rules. If that distinction is new, how ports and listening sockets work on Linux is worth reading before you open anything.

Shrink the range before you open it. Twenty thousand ports is far more than a small system needs. Two ports per call means a range of two hundred covers one hundred calls at once.

[general]
rtpstart=10000
rtpend=10200

Apply it with sudo asterisk -rx "core reload".

Should you install Asterisk or FreePBX?

Asterisk is the engine. You configure it with text files in /etc/asterisk and you write the dialplan yourself. FreePBX is a web interface written in PHP and JavaScript that sits on top of Asterisk, generates those files for you, and adds modules for voicemail and call queues.

The difference that matters on a VPS is ownership of the machine. As of August 2026 the official FreePBX 17 installer expects a vanilla Debian 12 system, and it installs Asterisk, a web server, a database server and PHP. Point it at a box that already runs other services and you will not enjoy the result. Give FreePBX its own VPS.

wget https://github.com/FreePBX/sng_freepbx_debian_install/raw/master/sng_freepbx_debian_install.sh -O /tmp/sng_freepbx_debian_install.sh
sudo bash /tmp/sng_freepbx_debian_install.sh

The install log lands in /var/log/pbx/freepbx17-install.log, which is the first place to look when the script stops early.

FreePBX owns the configuration files it generates. Edit pjsip.conf by hand on a FreePBX box and your change disappears the next time the GUI writes that file. FreePBX reads separate files with _custom in the name for hand written config, and it leaves those alone.

The honest trade is this. FreePBX gives you a GUI, and it puts a login page for your phone system on the public internet. Raw Asterisk has no web surface at all, and every setting is a documented directive you can read in a file and keep in git. If you install FreePBX, restrict its web port to your own address or reach it over a VPN, because an admin GUI for a PBX is a target with a direct route to money.

Version note, correct as of August 2026: Asterisk 22 is the current long term support release, published in October 2024 and receiving security fixes until October 2028. Asterisk 23 is the standard release. Ubuntu 24.04 carries Asterisk 20.6.0 in the universe repository.

Install Asterisk on Ubuntu 24.04

The distribution package is the quick path. Ubuntu patches it and it starts under systemd on its own.

sudo apt update
sudo apt install -y asterisk
sudo asterisk -rx "core show version"

Building from source gets you the current long term support release instead.

sudo apt update
sudo apt install -y build-essential wget
cd /usr/local/src
sudo wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-22-current.tar.gz
sudo tar -xzf asterisk-22-current.tar.gz
cd asterisk-22.*
sudo contrib/scripts/install_prereq install
sudo ./configure
sudo make menuselect
sudo make -j"$(nproc)"
sudo make install
sudo make samples
sudo make config
sudo ldconfig

install_prereq install pulls the build dependencies for your distribution, and install_prereq test prints the commands it would run without touching anything. make menuselect opens the module picker, which is where you enable codec_opus under Codec Translators. Run make samples only on a fresh install, since it writes the sample configuration into /etc/asterisk. make config installs the init script at /etc/init.d/asterisk, and systemd drives that through its SysV compatibility layer, so sudo systemctl enable --now asterisk works afterwards.

Whichever path you took, sudo asterisk -rvvv attaches to the running daemon and gives you the CLI. Running core show version there prints what you actually installed.

Configure a SIP trunk and one extension

PJSIP is the SIP channel driver in current Asterisk. Its configuration lives in /etc/asterisk/pjsip.conf and is built from small typed sections that reference each other by name. Sections of different types may share a name, which is why every block below is called mytrunk.

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0

[mytrunk]
type=registration
outbound_auth=mytrunk
server_uri=sip:sip.example.com
client_uri=sip:1234567890@sip.example.com
retry_interval=60

[mytrunk]
type=auth
auth_type=userpass
username=1234567890
password=REPLACE_WITH_A_LONG_RANDOM_SECRET

[mytrunk]
type=aor
contact=sip:sip.example.com:5060

[mytrunk]
type=endpoint
context=from-trunk
disallow=all
allow=ulaw
outbound_auth=mytrunk
aors=mytrunk

[mytrunk]
type=identify
endpoint=mytrunk
match=sip.example.com

The registration object sends the REGISTER that tells your provider where to deliver your calls. The identify object is how a call arriving from the provider is matched to this endpoint by source address, and a provider usually publishes several addresses to list there. Outbound registration and the endpoint are separate objects on purpose: one tells the provider where you are, the other decides what happens to the calls.

A desk phone needs three more objects.

[6001]
type=endpoint
context=internal
disallow=all
allow=ulaw
auth=auth6001
aors=6001
direct_media=no

[auth6001]
type=auth
auth_type=userpass
username=6001
password=REPLACE_WITH_A_LONG_RANDOM_SECRET

[6001]
type=aor
max_contacts=1

direct_media=no keeps Asterisk in the media path. Leave it out and Asterisk will try to make the two endpoints send RTP straight to each other, which fails when a phone sits behind NAT (network address translation) on a home router. Never let the secret match the extension number. The upstream sample uses password=6001 for extension 6001 to keep the example readable, and scanners try exactly that pattern first.

The dialplan in /etc/asterisk/extensions.conf decides what each context is allowed to do.

[internal]
exten => 6001,1,Dial(PJSIP/6001,20)
exten => _9X.,1,Dial(PJSIP/${EXTEN:1}@mytrunk,60)

[from-trunk]
exten => 1234567890,1,Dial(PJSIP/6001,20)
same => n,Hangup()

Those two contexts are a security boundary. from-trunk handles calls arriving from the provider and can only ring extension 6001. It cannot reach the _9X. pattern, so a call from outside cannot dial back out through your trunk. Merge the contexts and you have built the classic toll fraud path: a stranger calls your number, your dialplan dials out on your account, and you pay for it.

Apply the configuration and check it.

sudo asterisk -rx "pjsip reload"
sudo asterisk -rx "pjsip show registrations"
sudo asterisk -rx "pjsip show endpoints"

pjsip show registrations should list mytrunk with the status Registered. Rejected means the provider refused your credentials. Unregistered means your REGISTER is going unanswered, so look at the firewall next.

Firewall rules for SIP and RTP

Signalling and media deserve different treatment because they carry different risk. Restrict 5060 to the addresses your provider actually uses and to the networks your phones sit on.

sudo ufw allow proto udp from 203.0.113.10 to any port 5060
sudo ufw allow proto tcp from 203.0.113.10 to any port 5060
sudo ufw allow 10000:10200/udp
sudo ufw status verbose

The RTP range is the part you cannot narrow as far, because media often arrives from a different address than the signalling. Ask your provider for their media subnets and restrict to those if they publish them. Keep the range only as large as your busiest hour needs. Default policies, rule order and the rest of the ufw model are covered in the ufw firewall basics guide for a VPS.

Check IPv6 too. If IPV6=no is set in /etc/default/ufw, ufw does not filter IPv6 at all, so a daemon bound to :: is reachable over IPv6 with none of the rules above applied. Opening ports for IPv6 with ufw explains how the two families differ. Most providers also give you a separate network firewall in the control panel, and that one is enforced before your packets reach the VPS, so a port has to be open in both places.

SIP brute force and toll fraud are not optional problems

Put 5060 on a public address and the scans start. The pattern is steady: REGISTER and INVITE requests from many source addresses, trying common extension numbers with common secrets. Asterisk logs each failure, and the line has this shape.

Request 'REGISTER' from '<sip:1000@198.51.100.20>' failed for '198.51.100.20:5060' (callid: 5f1a5c0d) - No matching endpoint found

The reason this deserves real effort is money. A stolen extension is used to place expensive international calls, often to premium rate numbers that pay the attacker a share, and the bill is yours because the calls carried your credentials. It runs at machine speed and it usually runs overnight.

Six controls are mandatory rather than optional hardening.

  • Never create an extension whose secret is its own number or a short word. Generate one with openssl rand -base64 24 and paste it in.
  • Leave anonymous inbound calls off. PJSIP refuses unidentified calls by default and only accepts them if you create an endpoint named anonymous. Do not create one.
  • Keep the trunk context separate from any context that can dial out, as shown above.
  • Restrict signalling by source address, both in ufw and in your provider's network firewall.
  • Set a spending cap with your SIP trunk provider and disable the international destinations you never call. This is the only control that limits the loss when the others fail.
  • Run fail2ban against the Asterisk log.

Asterisk can also raise a security event after repeated unmatched requests from one address. The [global] section of pjsip.conf takes unidentified_request_count, which defaults to 5, and unidentified_request_period, which defaults to 5 seconds. Together they mean five unmatched requests from one address inside five seconds produce a security event that fail2ban can act on.

Ban the scanners with fail2ban

fail2ban ships an asterisk jail already written. It covers ports 5060 and 5061, reads /var/log/asterisk/messages, and defaults to maxretry = 10. The jail stays off until you enable it in /etc/fail2ban/jail.local.

[asterisk]
enabled = true
maxretry = 5
findtime = 600
bantime = 86400
sudo systemctl restart fail2ban
sudo fail2ban-client status asterisk

A healthy result lists the jail's log file and a count of currently banned addresses. On a public 5060 that count stops being zero within a day. The shipped filter matches the No matching endpoint found notices above, and it also matches Asterisk's structured SecurityEvent lines. Those events go to a separate log channel that is commented out in /etc/asterisk/logger.conf, so enable it there and add the file to the jail's logpath if you want them.

[logfiles]
console => notice,warning,error
messages.log => notice,warning,error
security.log => security

Reload the logger with sudo asterisk -rx "logger reload". The filter also carries a journalmatch for asterisk.service, so a journal backend works if you would rather not keep log files. Installation, jail.local structure and unbanning an address you locked out are covered in the fail2ban guide for Ubuntu 24.04.

Latency and codec choice on a distant VPS

Delay is set by geography and you cannot configure it away. ITU-T G.114 recommends keeping one-way delay under 150 ms for normal conversation and treats up to about 400 ms as still usable. Audio from a phone goes to your VPS and then out to your trunk provider, so a VPS in the wrong region pays that trip twice. Put it near the phones or near the provider, and prefer near the phones when those pull in different directions, because that leg usually runs over consumer internet where jitter is worst.

Codec choice sets the bandwidth per call. Each codec here sends a packet every 20 ms, which is 50 packets per second, and each packet carries 40 bytes of IP, UDP and RTP headers on top of the audio payload.

ChartBandwidth per concurrent call, one direction, 20 ms packets
The data behind this chart
[
  {
    "label": "G.711 ulaw",
    "payload_kbps": 64,
    "ip_kbps": 80
  },
  {
    "label": "G.722",
    "payload_kbps": 64,
    "ip_kbps": 80
  },
  {
    "label": "Opus at 24 kbps",
    "payload_kbps": 24,
    "ip_kbps": 40
  },
  {
    "label": "G.729",
    "payload_kbps": 8,
    "ip_kbps": 24
  }
]

G.711 ulaw is the default on most trunks. Its payload is 64 kbps, and with headers a call in progress costs 80 kbps in each direction. Opus at 24 kbps sits at 40 kbps, and G.729 drops to 24 kbps at the cost of audio quality and CPU time. These figures are header arithmetic rather than measurements: payload rate plus 40 bytes per packet at 50 packets per second. Ethernet or VLAN framing adds a little more on the wire.

Transcoding costs CPU. If your phones and your trunk both speak ulaw, allow only ulaw and Asterisk passes the audio through untouched. Opus holds up well on lossy links, but transcoding between Opus and G.711 needs the external codec_opus module, which you select in make menuselect and which is not built by default.

What breaks, and the string you will see

The call connects and nobody hears anything. RTP is not arriving. Confirm the range in rtp.conf is the range you opened in the firewall, then watch for packets with sudo tcpdump -ni any udp portrange 10000-10200 while you place a call. No packets at all means your firewall or your provider's network firewall is dropping them.

Audio in one direction only. One side is sending RTP to an address that cannot receive it, which is an address problem rather than a port problem. If your VPS has its public address directly on its interface, no NAT handling is needed. If the provider gives the VPS a private address with a one to one public mapping, set the public address on the transport and list your private range in local_net.

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0
local_net=10.0.0.0/8
external_media_address=198.51.100.5
external_signaling_address=198.51.100.5

No matching endpoint found in the log. The request matched no endpoint by IP address or by username. From a scanner that is normal and fail2ban will handle it. From your own provider it means the identify section does not list the address they call you from.

The registration status is Rejected. The provider refused the credentials in your auth section. Run pjsip set logger on at the CLI, watch one REGISTER and the response to it, then compare client_uri and username against what the provider issued you.

Nothing useful in the log at all. Asterisk writes notice level and above to messages.log by default. Raise it with core set verbose 4 and pjsip set logger on while you reproduce the problem, then turn both off, because the SIP logger writes every packet.

Before you expose it

A PBX is not like the other things you self-host. A broken web app costs you a page. A broken PBX becomes a phone bill, in hours, while you are asleep. Run it on a VPS that does nothing else, keep 5060 restricted to known addresses, give every extension a random secret, and put a spending cap on the trunk account. The rest of the box needs the same baseline as any exposed server, which this look at how safe VPS hosting really is sets out.

FAQ

Which ports do I need to open for a self-hosted VoIP server?

Port 5060 for SIP signalling over UDP and TCP, port 5061 if you use SIP over TLS, and a range of UDP ports for RTP media. Asterisk's sample rtp.conf uses 10000 to 20000, and the compiled-in defaults are 5000 to 31000. Each call consumes two ports from the range, so a range of two hundred ports handles one hundred simultaneous calls. Open the RTP range as UDP, and restrict 5060 to your provider's addresses and your own networks rather than leaving it open to everyone.

Should I install Asterisk on its own or use FreePBX?

Install raw Asterisk when you want a small attack surface and configuration files you can keep in git, and you are willing to write the dialplan. Install FreePBX when you want a GUI for extensions, voicemail and call queues. As of August 2026 the FreePBX 17 installer expects a vanilla Debian 12 machine and installs Asterisk, a web server, a database server and PHP, so give it a VPS of its own. FreePBX regenerates the config files it manages, so hand edits belong in its _custom files.

Why is there no audio after the call connects?

The signalling worked and the media did not. SIP set the call up on port 5060, and the audio is a separate UDP flow to a port in the RTP range that something is dropping. Check that the range in rtp.conf matches the range open in your firewall, and check your provider's network firewall as well as the one on the server. Run sudo tcpdump -ni any udp portrange 10000-10200 during a call: no packets means they are blocked before they arrive.

How do I stop SIP brute force attacks and toll fraud?

Give every extension a long random secret, never one matching its extension number. Keep the context used by your trunk separate from any context that can dial out, so an inbound call cannot dial back out on your account. Restrict port 5060 to your provider's addresses. Enable the asterisk jail in fail2ban, which reads /var/log/asterisk/messages and bans the addresses producing No matching endpoint found failures. Then set a spending cap and block unused international destinations with your provider, because that is the only control that caps the loss if the rest fails.

Does a VPS in a distant region hurt call quality?

Yes, because the audio takes two legs: phone to VPS, then VPS to trunk provider. ITU-T G.114 recommends one-way delay under 150 ms, and a badly placed VPS can spend most of that budget on distance alone. Choose a region close to the phones, since that leg usually runs over consumer internet where jitter is worst. Codec choice does not fix delay, it only changes bandwidth, so G.729 saves bytes but will not rescue a 200 ms path.

#voip#asterisk#freepbx#sip#self-hosting