From zero to a running SIP proxy on Ubuntu 24.04. Every command is real. Every config line is explained. Victory condition: a softphone registers, you watch it happen in the logs, and you understand exactly what you saw.
Chapter 1 stays as the hands-on Kamailio build, and Chapter 2 is now split into its own page. Use this page as the merged hub for the series when you publish it on Cloudflare Pages.
Installation, first registration, PostgreSQL, NAT, security, and the proof-of-life workflow.
Docker-based Kamailio packaging, Compose workflow, schema bootstrap, and repeatable deployment.
Static files only. No build step required. Cloudflare Pages can serve this folder as-is.
You already have this. You built BGP at MTN Uganda in 2006. You multihomed Tullow Oil. You know exactly what a routing layer does — it does not carry the payload, it decides where the payload goes. Kamailio is that layer, but for SIP.
BGP is to IP packets what Kamailio is to SIP messages. Your Juniper MX does not host websites — it routes the packets that reach the servers that do. Kamailio does not process calls — it routes the SIP signalling that reaches the Asterisk nodes that do. One layer routes. The other layer processes. Never confuse them.
When a SIP phone powers on and sends a REGISTER message, that message hits Kamailio first.
Kamailio authenticates it against PostgreSQL, stores the phone's location (IP address and port),
and returns 200 OK. Kamailio never touches audio. Not one byte of RTP ever
passes through it.
When a call comes in, Kamailio looks up where the destination phone last registered, then forwards the SIP INVITE to Asterisk. Asterisk answers it, bridges the audio, runs the IVR, records the call. Kamailio's job is already done before the first word is spoken.
This separation is why Kamailio scales to hundreds of thousands of registrations on modest hardware. It is stateless for most operations. It does not hold open audio streams. It is a SIP router, and it is extraordinarily good at that one thing.
For this chapter — learning, testing, first registration — a single VM is fine. In production you will separate Kamailio, PostgreSQL, and Asterisk onto different hosts. For today, everything on one machine. Ubuntu 24.04 LTS (Noble Numbat).
| Resource | Minimum (Lab) | Recommended (Lab) |
|---|---|---|
| CPU | 1 vCPU | 2 vCPU |
| RAM | 1 GB | 2 GB |
| Disk | 10 GB | 20 GB |
| OS | Ubuntu 24.04 LTS (64-bit) | |
| Network | A static IP or one that won't change mid-session | |
| Ports open | UDP/TCP 5060 (SIP), UDP 5060 inbound | |
Kamailio binds to a specific IP address. Get it now and keep it in view.
Run ip addr show and note the IP of your main interface (eth0, ens3, or whatever it is).
You will put this in the config. If you use the wrong IP, nothing works and the error
messages will not obviously tell you why.
# Run this. Note the output. Write it down. ip addr show | grep "inet " | grep -v 127.0.0.1 # Example output you are looking for: # inet 192.168.1.50/24 brd 192.168.1.255 scope global eth0 # Your IP in this example is 192.168.1.50 # This chapter uses MY_SERVER_IP as a placeholder — replace it everywhere
Kamailio maintains its own official Debian/Ubuntu package repository. Ubuntu 24.04 Noble Numbat is fully supported. Do not use the version in the Ubuntu main repos — it may lag behind by a release. Use the official Kamailio repo for the latest stable.
kamailio — the core binary and base modules. kamailio-postgres-modules — the module that lets Kamailio talk to PostgreSQL for authentication and location storage. kamailio-utils — kamctl and kamdbctl, the command-line tools you use to manage everything. kamailio-tls-modules — TLS support for secure SIP. You will want this before going to production.
apt update && apt upgrade -y apt install -y gnupg2 curl software-properties-common
The GPG key authenticates the packages. The sources.list entry points to the Noble (Ubuntu 24.04) branch of the Kamailio 6.x series.
# Fetch and install the Kamailio GPG signing key curl -fsSL https://deb.kamailio.org/kamailiodebkey.gpg \ | gpg --dearmor \ | tee /usr/share/keyrings/kamailio-archive-keyring.gpg > /dev/null # Add the repository — Ubuntu 24.04 Noble, Kamailio 6.x stable echo "deb [signed-by=/usr/share/keyrings/kamailio-archive-keyring.gpg] \ http://deb.kamailio.org/kamailio60 noble main" \ | tee /etc/apt/sources.list.d/kamailio.list # Refresh package lists apt update
apt install -y \ kamailio \ kamailio-postgres-modules \ kamailio-utils \ kamailio-tls-modules \ kamailio-presence-modules
This will pull in all required dependencies automatically. The install does not start Kamailio automatically — it waits for you to configure it first.
kamailio -V # Expected output (version numbers will match your install): # version: kamailio 6.0.x (x86_64/linux) # flags: USE_TCP USE_TLS USE_SCTP DISABLE_NAGLE USE_RAW_SOCKS ...
The Kamailio binary is installed at /usr/sbin/kamailio.
Modules live at /usr/lib/x86_64-linux-gnu/kamailio/modules/.
The main config file location is /etc/kamailio/kamailio.cfg.
The helper config is /etc/kamailio/kamctlrc.
Kamailio is not running yet. That is correct.
Kamailio stores subscriber credentials, phone locations, and call records in PostgreSQL.
The tool kamdbctl creates the entire schema for you — you do not write
a single SQL statement by hand.
apt install -y postgresql postgresql-contrib
systemctl enable postgresql
systemctl start postgresql
# Verify it is running
systemctl status postgresql | grep "Active:"
sudo -u postgres psql
-- inside psql shell:
CREATE ROLE kamailio WITH LOGIN PASSWORD 'KamailioRW2024!';
CREATE ROLE kamailioro WITH LOGIN PASSWORD 'KamailioRO2024!';
CREATE DATABASE kamailio OWNER kamailio;
GRANT CONNECT ON DATABASE kamailio TO kamailioro;
\q
On current PostgreSQL releases, scram-sha-256 is the recommended password
verifier and authentication method. Confirm the server is using SCRAM for new passwords,
then rotate role passwords if needed.
sudo -u postgres psql -c "SHOW password_encryption;" # If this is not 'scram-sha-256', set it: sudo -u postgres psql -c "ALTER SYSTEM SET password_encryption='scram-sha-256';" sudo -u postgres psql -c "SELECT pg_reload_conf();" # Then reset Kamailio role passwords so they are stored as SCRAM verifiers sudo -u postgres psql -c "ALTER ROLE kamailio PASSWORD 'KamailioRW2024!';" sudo -u postgres psql -c "ALTER ROLE kamailioro PASSWORD 'KamailioRO2024!';"
Restrict database access to local connections (or specific Kamailio hosts) and force SCRAM auth. Exact path can vary by PostgreSQL major version.
# Keep peer auth for postgres local admin local all postgres peer # Kamailio DB users on localhost (IPv4/IPv6) host kamailio kamailio 127.0.0.1/32 scram-sha-256 host kamailio kamailio ::1/128 scram-sha-256 host kamailio kamailioro 127.0.0.1/32 scram-sha-256 host kamailio kamailioro ::1/128 scram-sha-256 # For remote DB host deployments, replace loopback ranges above with # your Kamailio server subnet(s) only. Never use 0.0.0.0/0.
systemctl reload postgresql systemctl status postgresql | grep "Active:"
kamctlrc is where you tell the Kamailio toolset how to reach PostgreSQL.
Edit it now — the database creation step reads it.
## Uncomment and set these lines in /etc/kamailio/kamctlrc SIP_DOMAIN=sip.yourdomain.com # or your server IP for lab DBENGINE=PGSQL DBHOST=localhost DBPORT=5432 DBNAME=kamailio DBRWUSER="kamailio" DBRWPW="KamailioRW2024!" # choose your own DBROUSER="kamailioro" DBROPW="KamailioRO2024!" # choose your own DBROOTUSER="postgres" DBROOTPW="YourPostgresSuperuserPassword"
kamdbctl create # You will be prompted to confirm. Type 'y'. # This creates: # - the 'kamailio' database # - kamailio and kamailioro PostgreSQL users # - ~30 tables including: subscriber, location, acc, missed_calls # # If it asks for the PostgreSQL superuser password, enter it.
After kamdbctl create, grant only what the read-only user needs.
Keep write privileges on kamailio only.
sudo -u postgres psql -d kamailio REVOKE CREATE ON SCHEMA public FROM PUBLIC; REVOKE ALL ON SCHEMA public FROM kamailioro; GRANT USAGE ON SCHEMA public TO kamailioro; GRANT SELECT ON ALL TABLES IN SCHEMA public TO kamailioro; ALTER DEFAULT PRIVILEGES FOR ROLE kamailio IN SCHEMA public GRANT SELECT ON TABLES TO kamailioro; \q
This creates the account 1001@yourdomain with password test1234. This is what your softphone will use to register.
kamctl add 1001 test1234 kamctl add 1002 test1234 # Verify they were created: kamctl show 1001 # Expected output: # username | domain | password | ha1 ... # 1001 | sip.yourdomain | test1234 | ...
This is the heart of Kamailio. Everything it does is defined here. The default config that ships with the package is a complete, working, production-capable configuration for basic SIP proxy operation. You do not rewrite it — you configure it via preprocessor defines at the top.
The top section uses C-style #!define and #!ifdef preprocessor
directives to enable features. You define what you want, and the rest of the file
uses those definitions to activate the right code paths. Think of it like
feature flags at the top of the file, and the actual routing logic below.
Edit /etc/kamailio/kamailio.cfg. Find and modify these lines near the top.
Most are already present but commented out or need your values substituted in.
#!KAMAILIO # ####### Global Parameters ######### # ── FEATURE FLAGS ──────────────────────────────────────────────────── # Uncomment WITH_POSTGRES to enable PostgreSQL auth and location storage #!define WITH_POSTGRES # Uncomment WITH_AUTH to require authentication on REGISTER and INVITE #!define WITH_AUTH # Uncomment WITH_USRLOCDB to store registrations in PostgreSQL (not memory) #!define WITH_USRLOCDB # ── DATABASE URL ───────────────────────────────────────────────────── # Replace the credentials to match what you set in kamctlrc #!define DBURL "postgres://kamailio:KamailioRW2024!@localhost/kamailio" # ── SERVER IDENTITY ────────────────────────────────────────────────── # This must match the domain you used when creating subscribers domain = "sip.yourdomain.com" # or your server IP for lab # ── LISTEN ADDRESSES ───────────────────────────────────────────────── # Replace MY_SERVER_IP with the actual IP of your VM (from Section 02) listen = udp:MY_SERVER_IP:5060 listen = tcp:MY_SERVER_IP:5060 # ── LOGGING ────────────────────────────────────────────────────────── debug=3 # 0=errors only, 3=info, 5=full debug (very noisy) log_stderror=no # log to syslog, not stderr log_facility=LOG_LOCAL0 # ── PERFORMANCE ────────────────────────────────────────────────────── # For a lab VM, these defaults are fine children=4 # worker processes, set to number of CPU cores
If Kamailio refuses to start or phones cannot register, it is almost always one of three things:
(1) DBURL has wrong credentials or database name,
(2) listen IP does not match the actual interface IP,
(3) domain does not match what you used with kamctl add.
Check these three first, always.
Below the configuration section, the file contains routing logic. You do not need to modify this for basic operation, but you need to understand what it does. Here are the key route blocks:
request_route { # ── Max Forwards check ─────────────────────────────────────────── # Prevents SIP routing loops. If a packet has been forwarded # more than 10 times, reject it. Same concept as IP TTL. if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } # ── OPTIONS keepalive (ping/pong) ───────────────────────────────── # SIP phones periodically send OPTIONS to check if the server is alive. # We reply 200 OK immediately without processing further. if (is_method("OPTIONS") && uri==myself) { sl_send_reply("200", "OK"); exit; } # ── Route REGISTER to the registrar handler ────────────────────── # All REGISTER messages (phone checking in) go to route[REGISTRAR] if (is_method("REGISTER")) { route(REGISTRAR); exit; } # ── Everything else: look up where to send it ──────────────────── # For calls and other messages, look up the destination in the # location table (where did they last register from?) if (!lookup("location")) { # Destination not found in our location table sl_send_reply("404", "Not Found"); exit; } route(RELAY); # Forward it to wherever lookup() found }
route[REGISTRAR] { # ── Authentication ─────────────────────────────────────────────── # www_authorize checks the phone's credentials against the # 'subscriber' table in PostgreSQL. If wrong password → 401 Unauthorized. if (is_method("REGISTER")) { if (!www_authorize("$fd", "subscriber")) { www_challenge("$fd", "0"); exit; } } # ── Save the registration ──────────────────────────────────────── # save() writes the phone's current IP:port into the location table. # This is how Kamailio knows where to forward calls to this extension. if (!save("location")) { sl_reply_error(); } exit; }
Before starting, always run the config check. Kamailio's config parser will catch syntax errors and tell you exactly which line is wrong. Never start Kamailio without checking first — in production this habit will save you from outages.
kamailio -c -f /etc/kamailio/kamailio.cfg # Good output ends with: # Success: Configuration file '/etc/kamailio/kamailio.cfg' OK # # If there is a syntax error it will say: # ERROR: ... line 47: unexpected token # Go to that line number and fix it before proceeding.
# Start Kamailio systemctl start kamailio # Enable it to start on boot systemctl enable kamailio # Check it is running systemctl status kamailio # Verify it is listening on port 5060 ss -ulnp | grep 5060 ss -tlnp | grep 5060 # Expected output: # udp UNCONN 0 0 MY_SERVER_IP:5060 0.0.0.0:* users:(("kamailio",...))
Kamailio logs to syslog. On Ubuntu 24.04 this goes to the journal. Open a second terminal and keep this running while you test — this is your window into everything that is happening.
# Method 1: journalctl (preferred on Ubuntu 24.04) journalctl -u kamailio -f --output=cat # Method 2: syslog directly tail -f /var/log/syslog | grep kamailio # Method 3: debug mode — maximum verbosity (for when you are stuck) # Stop the service first, then run in foreground: systemctl stop kamailio kamailio -D -E -d -d -d -f /etc/kamailio/kamailio.cfg # This prints every SIP message in full to your terminal # Ctrl+C to stop, then systemctl start kamailio to go back to normal
Now you register a SIP softphone. Use any SIP client you have — Zoiper, Linphone, MicroSIP (Windows), or the Linphone app on your mobile. The settings are the same for all of them.
| Setting | Value | Notes |
|---|---|---|
| SIP Username / Extension | 1001 |
The user you created with kamctl add |
| SIP Password | test1234 |
As set in kamctl add |
| SIP Domain / Server | MY_SERVER_IP |
Your VM's IP address |
| SIP Port | 5060 |
Default SIP port |
| Transport | UDP |
Start with UDP; TCP later |
| STUN | Disabled | Not needed for same-LAN lab |
Point the phone at your server IP and let it register. While it connects, watch your log terminal. You should see a burst of activity. Here is what you are looking for:
# First attempt — phone sends REGISTER without credentials REGISTER sip:MY_SERVER_IP SIP/2.0 # Kamailio challenges it — asks for credentials SIP/2.0 401 Unauthorized # Phone retries with credentials (digest auth) REGISTER sip:MY_SERVER_IP SIP/2.0 # this time with Authorization header # Kamailio verifies against PostgreSQL subscriber table # Writes phone's IP:port into location table SIP/2.0 200 OK # ← this is your victory # Log line: "REGISTERED aor=<sip:1001@yourdomain> expires=3600"
The definitive proof: run kamctl ul show 1001 and you will see the phone's
current IP address, port, user agent string, and when the registration expires.
If this shows data, Kamailio is working correctly.
# Show where extension 1001 is currently registered kamctl ul show 1001 # Show ALL currently registered phones kamctl ul show # Expected output for a registered phone: # Contact: <sip:[email protected]:5060> # Expires: 3600 # User-Agent: Zoiper ... # Register a second phone on extension 1002, then try calling 1001 # from 1002. Kamailio looks up 1001's location and routes the call.
Registration looked like magic from the phone's perspective — it connected and showed a green tick. Here is exactly what happened, message by message, so that when something goes wrong you will know where in this sequence to look.
| # | Direction | Message | What it means |
|---|---|---|---|
1 |
Phone → Kamailio | REGISTER (no auth) |
Phone announces itself, asks to register. No credentials yet. |
2 |
Kamailio → Phone | 401 Unauthorized |
Challenge: "Prove who you are." Contains a nonce (random challenge value). |
3 |
Phone → Kamailio | REGISTER (with auth) |
Phone hashes username+password+nonce and sends the result. Password never travels in clear text. |
4 |
Kamailio → PostgreSQL | SELECT ha1 from subscriber | Fetches the stored password hash for this user. |
5 |
PostgreSQL → Kamailio | Hash returned | Kamailio computes the expected response and compares to what the phone sent. |
6 |
Kamailio → PostgreSQL | INSERT/UPDATE location | Stores phone's current IP:port with expiry time. This is how calls reach this phone. |
7 |
Kamailio → Phone | 200 OK |
Registered. Phone shows green. Repeats before expiry (usually every 60–600 seconds). |
SIP digest authentication is a challenge-response system. The phone never sends
your password across the network. It sends MD5(username:realm:password:nonce).
Kamailio computes the same hash server-side and compares. This is why Kamailio stores
both the plaintext password and the pre-computed HA1 hash in the subscriber table —
it needs the hash to verify without receiving the password.
These are the commands you will use constantly. They should become muscle memory before you move to the next chapter.
| Command | What it does |
|---|---|
kamctl ul show | Show all currently registered phones (user location table) |
kamctl ul show 1001 | Show where extension 1001 is registered right now |
kamctl add 1003 password | Create a new SIP subscriber |
kamctl passwd 1001 newpassword | Change a subscriber's password |
kamctl rm 1001 | Delete a subscriber |
kamctl monitor | Live statistics — calls/sec, registrations, memory usage |
kamctl fifo get_statistics all | Detailed runtime statistics dump |
kamailio -c | Check config syntax (always run before reload) |
systemctl reload kamailio | Reload config without dropping calls |
kamcmd ul.dump | Alternative location table dump via RPC |
kamcmd stats.get_statistics all | Full stats via RPC interface |
kamctl monitor 1 # Refreshes every 1 second. Shows: # Now: X processes, X listening, X registered contacts # Last 1s: X processed, X forwarded, X dropped # Memory: used / available # SHM memory (shared): status
1. Check systemctl status kamailio — is it actually running?
2. Check ss -ulnp | grep 5060 — is it listening on the right IP?
3. Check firewall: ufw status — is UDP 5060 allowed?
4. Run kamailio -D -E -f /etc/kamailio/kamailio.cfg in foreground and watch raw SIP messages.
5. Check that the domain in kamailio.cfg matches the SIP domain in kamctlrc and in your softphone config.
6. Check PostgreSQL is running: systemctl status postgresql.
7. Test the DB connection manually: psql -h localhost -U kamailio -d kamailio -c "SELECT * FROM subscriber LIMIT 5;"
8. If auth fails, check PostgreSQL logs for pg_hba.conf rejects and bad password verifier errors.
Your chapter is excellent for same-LAN lab success. The first gap to close for real-world clients
is NAT behavior. Kamailio docs around nathelper, registrar, and usrloc
make this explicit: store the received source tuple for REGISTER and use standards-conforming alias
handling for in-dialog routing.
If you use fix_nated_register(), set received_avp to the same value in both
nathelper and registrar. If these values differ, registrations can appear to work
but re-INVITE/BYE and keepalive behavior becomes inconsistent.
# NAT detection + received tuple storage modparam("nathelper", "received_avp", "$avp(s:rcv)") modparam("registrar", "received_avp", "$avp(s:rcv)") # Optional SIP keepalive pings for UDP NATed endpoints modparam("nathelper", "natping_interval", 15) modparam("nathelper", "ping_nated_only", 1) modparam("nathelper", "sipping_method", "OPTIONS") # In REQUEST_ROUTE if (nat_uac_test("19")) { # 1=private Contact, 2=Via mismatch, 16=port mismatch fix_nated_register(); }
Kamailio documentation warns that fix_nated_contact() rewrites Contact and can break strict
clients. Prefer set_contact_alias() and handle_ruri_alias() for a more standards-conforming path,
especially when TCP/TLS connection reuse matters.
Kamailio core docs for myself, alias, and listen show a common first-production
failure pattern: the proxy identifies local domains incorrectly, then routing and loose_route behavior become
unpredictable. Your build should define identity intentionally.
# Core identity for myself checks listen=udp:MY_SERVER_IP:5060 listen=tcp:MY_SERVER_IP:5060 alias=sip.yourdomain.com:5060 alias=MY_SERVER_IP:5060 # If you serve many domains/userspaces, align these: modparam("auth_db", "use_domain", 1) modparam("usrloc", "use_domain", 1)
Keep four values aligned: softphone domain, SIP_DOMAIN in kamctlrc, auth realm in challenges,
and what Kamailio treats as local (alias/myself). Alignment here removes a huge class of 401/404 confusion.
The official auth and permissions modules add controls missing from a pure lab setup.
For internet-facing SIP, add source ACLs, stronger digest behavior, and challenge integrity checks.
| Control | Why it matters | Minimum recommendation |
|---|---|---|
permissions ACL | Block unknown source networks early | allow_source_address("1") gate for trunks/admin endpoints |
auth qop | Adds nonce-count semantics and better replay resistance | modparam("auth","qop","auth") |
nonce_count | Detects nonce reuse/replay | modparam("auth","nonce_count",1) |
auth_checks_* | Binds nonce to message identity/source traits | Enable conservative bits first (uri and source IP) |
modparam("auth", "qop", "auth") modparam("auth", "nonce_count", 1) modparam("auth", "nonce_expire", 300) modparam("auth", "auth_checks_register", 9) modparam("auth", "auth_checks_no_dlg", 9) # 9 = URI(1) + source IP(8) # Avoid enabling call-id/from-tag checks until endpoint behavior is verified.
If you expose UDP 5060 publicly with weak subscriber passwords and no ACL/rate controls, credential stuffing starts quickly. Keep this chapter in lab mode unless these controls are in place.
Kamailio usrloc documentation defines five practical storage modes. Picking the wrong one causes
confusion around restart behavior and performance. This is a key concept worth making explicit in Chapter 1.
db_mode | Behavior | When to use |
|---|---|---|
0 | Memory only, no DB persistence | Fast lab tests where restart loss is acceptable |
1 | Write-through (every change to DB immediately) | Higher durability, lower speed |
2 | Write-back (memory + periodic DB flush) | Common balance for single-node setups |
3 | DB-only (no memory cache) | Shared DB multi-proxy designs; slower per request |
4 | Load at startup then memory only | Specialized replication patterns |
# Good baseline for your current architecture modparam("usrloc", "db_mode", 2) modparam("usrloc", "timer_interval", 60)
Core cookbook and module docs reviewed: core configuration (listen, alias, myself),
auth, auth_db, registrar, usrloc, nathelper, and permissions.
Chapter 2 builds on this foundation: adding Asterisk behind Kamailio, the PJSIP trunk configuration, and routing inbound calls from Kamailio to your first IVR. The BGP router now has something to route calls to.