FreeSWITCH demo installation runbook
This runbook provisions a native FreeSWITCH Community Edition server on AWS EC2 for a WhatsApp Business Calling SIP demo. It covers the server through a verified public SIP-TLS listener, SRTP/RTP networking, and Opus availability. It does not configure the final WhatsApp digest user, dialplan, or ElevenLabs trunk.
Target environment
- EC2:
t3.smallor larger, x86_64 - OS: Debian 13 (
trixie) - DNS name:
sip.example.com(replace throughout) - Public IP: EC2 Elastic IP
- Installation: native APT packages; no Docker
Do not use the SignalWire APT repository on Ubuntu 24.04 (
noble) unless SignalWire publishes that suite. Do not substitute another repository codename manually. This runbook uses the supported Debian 13trixiesuite.
1. AWS and DNS prerequisites
- Associate an Elastic IP with the EC2 instance.
- In Cloudflare, create an
Arecord forsip.example.compointing to the Elastic IP. - Set the Cloudflare record to DNS only (gray cloud). Cloudflare's proxy must not front the SIP hostname.
- Configure the EC2 Security Group with these inbound rules:
| Protocol | Port(s) | Source | Purpose |
|---|---|---|---|
| TCP | 22 | Administrator IP only | SSH |
| TCP | 80 | 0.0.0.0/0 | Let's Encrypt HTTP-01 validation and renewal |
| TCP | 5061 | 0.0.0.0/0 initially | SIP over TLS |
| UDP | 16384-16484 | 0.0.0.0/0 initially | RTP/SRTP/DTLS-SRTP media |
Keep the RTP range small and configure FreeSWITCH to match it later. Do not open SIP UDP 5060 for Meta.
2. Base Debian setup
Connect using the Debian EC2 user (commonly admin) and run:
sudo apt update
sudo apt full-upgrade -y
sudo apt install -y \
ca-certificates curl wget gnupg lsb-release \
ufw fail2ban btop fzf ncdu snapd
sudo timedatectl set-timezone Asia/Kuala_LumpurOptional: add swap. It helps package operations and source builds; real-time calls should not depend on it.
if ! swapon --show | grep -q '/swapfile'; then
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab
fiEnable the host firewall. The EC2 Security Group should still restrict SSH to the administrator IP.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 5061/tcp
sudo ufw allow 16384:16484/udp
sudo ufw --force enable
sudo ufw status verbose3. Public TLS certificate
With DNS resolving to the instance and TCP 80 open:
sudo systemctl enable --now snapd
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/local/bin/certbot
sudo certbot certonly --standalone \
-d sip.example.com \
--agree-tos \
-m admin@example.com \
--no-eff-email
sudo certbot renew --dry-runThe resulting files are:
/etc/letsencrypt/live/sip.example.com/fullchain.pem
/etc/letsencrypt/live/sip.example.com/privkey.pemKeep TCP 80 open for the automatic HTTP-01 renewal process. Do not share the private key. Do not use Cloudflare Universal SSL or a Cloudflare Origin Certificate for this SIP endpoint.
4. FreeSWITCH Community Edition packages
Create a free SignalWire Space and generate a Personal Access Token in the SignalWire dashboard's Personal Access Tokens section. This token is used only for authenticated APT package downloads.
read -rsp "SignalWire Personal Access Token: " SW_TOKEN
echo
sudo wget --http-user=signalwire --http-password="$SW_TOKEN" \
-O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \
https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
printf 'machine freeswitch.signalwire.com login signalwire password %s\n' "$SW_TOKEN" \
| sudo tee /etc/apt/auth.conf.d/freeswitch.conf > /dev/null
sudo chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ trixie main" \
| sudo tee /etc/apt/sources.list.d/freeswitch.list
unset SW_TOKEN
sudo apt update
sudo apt install -y freeswitch-meta-vanilla freeswitch-sounds-en-us-callie
sudo systemctl enable --now freeswitchDo not include freeswitch-sounds-music in the install command. It is not currently published in this repository's Trixie package set and causes the entire APT transaction to fail. Music on hold is not necessary for this demo.
If a later apt update returns 401 Unauthorized, the credentials file was likely overwritten with an empty token after SW_TOKEN had been unset. Restore it without exposing the token in shell history:
read -rsp "SignalWire Personal Access Token: " SW_TOKEN
echo
printf 'machine freeswitch.signalwire.com login signalwire password %s\n' "$SW_TOKEN" \
| sudo tee /etc/apt/auth.conf.d/freeswitch.conf > /dev/null
sudo chmod 600 /etc/apt/auth.conf.d/freeswitch.conf
unset SW_TOKEN5. Install and verify Opus
The WhatsApp-facing leg needs Opus. Install it explicitly, even if freeswitch-meta-vanilla is installed:
sudo apt install -y freeswitch-mod-opus
sudo systemctl restart freeswitch
sudo systemctl status freeswitch --no-pager
sudo fs_cli -x "version"
sudo fs_cli -x "show codecs" | grep -i opusExpected output includes a line similar to:
codec,OPUS (STANDARD),mod_opusAn active FreeSWITCH service and this codec line confirm the base installation is ready for SIP-TLS and WhatsApp media configuration.
6. Configure the WhatsApp-facing SIP-TLS profile
This demo uses FreeSWITCH's internal Sofia profile as a dedicated, TLS-only public SIP listener. It listens on TCP 5061; it does not expose plain SIP on port 5060.
Before copying the configuration, make these changes in vars.xml:
<!-- Use a long, unique value; never commit the real value. -->
<X-PRE-PROCESS cmd="set" data="default_password=REPLACE_WITH_A_LONG_RANDOM_SECRET"/>
<X-PRE-PROCESS cmd="set" data="domain=sip.example.com"/>
<X-PRE-PROCESS cmd="set" data="global_codec_prefs=OPUS,G722,PCMU,PCMA"/>
<X-PRE-PROCESS cmd="set" data="outbound_codec_prefs=OPUS,G722,PCMU,PCMA"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=host:sip.example.com"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=host:sip.example.com"/>
<X-PRE-PROCESS cmd="set" data="rtp_start_port=16384"/>
<X-PRE-PROCESS cmd="set" data="rtp_end_port=16484"/>
<X-PRE-PROCESS cmd="set" data="sip_tls_version=tlsv1.2,tlsv1.3"/>
<X-PRE-PROCESS cmd="set" data="internal_auth_calls=true"/>
<X-PRE-PROCESS cmd="set" data="internal_tls_port=5061"/>
<X-PRE-PROCESS cmd="set" data="internal_ssl_enable=true"/>In sip_profiles/internal.xml, configure the following profile parameters:
<param name="context" value="wa-inbound"/>
<param name="tls" value="$${internal_ssl_enable}"/>
<param name="tls-only" value="true"/>
<param name="tls-sip-port" value="$${internal_tls_port}"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="tls-verify-policy" value="out"/>
<param name="disable-transfer" value="true"/>
<param name="disable-register" value="true"/>
<param name="auth-calls" value="$${internal_auth_calls}"/>
<param name="challenge-realm" value="$${domain}"/>Do not retain apply-inbound-acl=domains on this public profile. Meta's SIP source addresses are external and can change; FreeSWITCH challenges the incoming INVITE with SIP digest authentication instead. Disable the stock ws-binding and wss-binding entries unless browser SIP clients are intentionally needed.
Validate XML locally before deployment, then copy both files to the instance:
xmlstarlet val -e vars.xml
xmlstarlet val -e internal.xml
sudo install -o freeswitch -g freeswitch -m 640 \
vars.xml /etc/freeswitch/vars.xml
sudo install -o freeswitch -g freeswitch -m 640 \
internal.xml /etc/freeswitch/sip_profiles/internal.xml7. Install the Let's Encrypt chain for FreeSWITCH
FreeSWITCH's SIP TLS transport reads agent.pem and cafile.pem from tls-cert-dir. Build an all.pem file from the certificate full chain followed by its private key. Link both agent.pem and tls.pem to it; this ensures Sofia serves the certificate chain correctly.
sudo install -d -o freeswitch -g freeswitch -m 750 /etc/freeswitch/tls
sudo sh -c 'cat \
/etc/letsencrypt/live/sip.example.com/fullchain.pem \
/etc/letsencrypt/live/sip.example.com/privkey.pem \
> /etc/freeswitch/tls/all.pem'
sudo ln -sfn all.pem /etc/freeswitch/tls/agent.pem
sudo ln -sfn all.pem /etc/freeswitch/tls/tls.pem
sudo sh -c 'cat \
/etc/letsencrypt/live/sip.example.com/chain.pem \
/etc/ssl/certs/ca-certificates.crt \
> /etc/freeswitch/tls/cafile.pem'
sudo chown -h freeswitch:freeswitch \
/etc/freeswitch/tls/all.pem \
/etc/freeswitch/tls/agent.pem \
/etc/freeswitch/tls/tls.pem \
/etc/freeswitch/tls/cafile.pem
sudo chmod 640 /etc/freeswitch/tls/all.pem /etc/freeswitch/tls/cafile.pemRestart FreeSWITCH and verify both the listener and the externally verifiable certificate chain:
sudo systemctl restart freeswitch
sudo systemctl status freeswitch --no-pager
sudo fs_cli -x "sofia status profile internal"
sudo ss -ltnp | grep ':5061'
openssl s_client \
-connect sip.example.com:5061 \
-servername sip.example.com \
-verify_hostname sip.example.com \
-showcerts </dev/nullThe expected final line is:
Verify return code: 0 (ok)If it instead reports unable to verify the first certificate, FreeSWITCH is serving only the leaf certificate. Rebuild all.pem, recreate both symlinks, restart the service, and rerun the test.
Refresh FreeSWITCH TLS files after renewal
Certbot renews the files under /etc/letsencrypt/live; FreeSWITCH does not automatically reload them. Create this Certbot deploy hook so a successful renewal rebuilds FreeSWITCH's files and restarts the service:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/freeswitch-tls >/dev/null <<'EOF'
#!/bin/sh
set -eu
CERT_DIR=/etc/letsencrypt/live/sip.example.com
FS_TLS_DIR=/etc/freeswitch/tls
umask 077
cat "$CERT_DIR/fullchain.pem" "$CERT_DIR/privkey.pem" > "$FS_TLS_DIR/all.pem"
ln -sfn all.pem "$FS_TLS_DIR/agent.pem"
ln -sfn all.pem "$FS_TLS_DIR/tls.pem"
cat "$CERT_DIR/chain.pem" /etc/ssl/certs/ca-certificates.crt > "$FS_TLS_DIR/cafile.pem"
chown -h freeswitch:freeswitch "$FS_TLS_DIR/all.pem" "$FS_TLS_DIR/agent.pem" "$FS_TLS_DIR/tls.pem" "$FS_TLS_DIR/cafile.pem"
chmod 640 "$FS_TLS_DIR/all.pem" "$FS_TLS_DIR/cafile.pem"
systemctl restart freeswitch
EOF
sudo chmod 700 /etc/letsencrypt/renewal-hooks/deploy/freeswitch-tls
sudo certbot renew --dry-run8. Configure WhatsApp, ElevenLabs, and the FreeSWITCH route
WhatsApp SIP settings
For the business phone number, configure these Meta calling settings:
| Setting | Value |
|---|---|
| SIP status | ENABLED |
| SIP server | sip.example.com |
| SIP port | 5061 |
| SRTP key exchange | SDES |
| SIP webhook delivery | Optional; ENABLED if call lifecycle webhooks are wanted |
Retrieve the Meta-generated SIP password using include_sip_credentials=true. The digest username is the normalized business phone number without the leading +; do not put the password in source control or shell history.
ElevenLabs SIP-trunk settings
Import the same business number in E.164 format into ElevenLabs and assign it to the intended agent. For a TLS/SDES-SRTP demo, use:
| Setting | Value |
|---|---|
| Inbound SIP server | sip:sip.rtc.elevenlabs.io:5061;transport=tls |
| Inbound media encryption | Required |
| Allowed addresses | The EC2 Elastic IP only, for example 203.0.113.10/32 |
| Allowed numbers | All numbers initially |
| Remote domains | sip.example.com |
| Outbound address | sip.example.com (hostname only) |
| Outbound transport | TLS |
| Outbound media encryption | Required |
| Enabled codecs | G.722, PCMU, PCMA |
Leave ElevenLabs digest authentication empty for this call direction: FreeSWITCH originates the call to ElevenLabs and ElevenLabs authorizes it using the Elastic-IP allowlist. Do not use the ElevenLabs outbound-call feature until separate FreeSWITCH authentication for ElevenLabs is configured.
Meta SIP credential user
Create a directory user for the normalized business number. The domain set in vars.xml causes the standard directory/default tree to represent sip.example.com.
Example for business number +15550935083:
<!-- /etc/freeswitch/directory/default/15550935083.xml -->
<include>
<user id="15550935083">
<params>
<param name="password" value="REPLACE_WITH_META_SIP_PASSWORD"/>
</params>
<variables>
<variable name="user_context" value="wa-inbound"/>
</variables>
</user>
</include>Use sudoedit to enter the actual password after installation. Restrict the file to the freeswitch account:
sudo install -o freeswitch -g freeswitch -m 640 \
/tmp/whatsapp-meta-user.xml \
/etc/freeswitch/directory/default/15550935083.xml
sudoedit /etc/freeswitch/directory/default/15550935083.xmlwa-inbound dialplan
Install this dialplan as /etc/freeswitch/dialplan/wa-inbound.xml, replacing the example number where needed:
<include>
<context name="wa-inbound">
<extension name="whatsapp-to-elevenlabs">
<condition field="destination_number" expression="^\+?15550935083$">
<!-- Require SDES-SRTP on the WhatsApp leg. -->
<action application="set" data="rtp_secure_media=mandatory:AES_CM_128_HMAC_SHA1_80"/>
<action application="set" data="effective_caller_id_number=+15550935083"/>
<action application="set" data="effective_caller_id_name=WhatsApp Business"/>
<!-- Codec restriction is deliberately scoped to the ElevenLabs B-leg. -->
<action application="bridge" data="{ignore_early_media=true,rtp_secure_media=mandatory:AES_CM_128_HMAC_SHA1_80,absolute_codec_string=G722,PCMU,PCMA}sofia/internal/sip:+15550935083@sip.rtc.elevenlabs.io:5061;transport=tls"/>
</condition>
</extension>
</context>
</include>Do not add a standalone set(absolute_codec_string=G722,PCMU,PCMA) action before the bridge. It applies to the WhatsApp A-leg as well, removes Opus from that leg, and causes INCOMPATIBLE_DESTINATION. The bridge-scoped value limits codecs only on the ElevenLabs B-leg, allowing FreeSWITCH to transcode WhatsApp Opus to G.722/PCMU/PCMA.
Deploy and validate:
sudo install -o freeswitch -g freeswitch -m 640 \
/tmp/wa-inbound.xml \
/etc/freeswitch/dialplan/wa-inbound.xml
sudo systemctl restart freeswitch
sudo fs_cli -x "sofia status profile internal"
sudo fs_cli -x "user_data 15550935083@sip.example.com var user_context"
sudo grep -n 'absolute_codec_string' /etc/freeswitch/dialplan/wa-inbound.xmlExpected results are Challenge Realm sip.example.com, wa-inbound from the user lookup, and exactly one absolute_codec_string occurrence inside the bridge action.
9. Pre-production plan
The current single-EC2 setup is appropriate for a controlled demo. Treat it as a functional reference, not a production availability design.
Starting infrastructure specification
| Environment | Compute starting point | Availability | Notes |
|---|---|---|---|
| Demo | t3.small — 2 vCPU, 2 GiB RAM | One instance | Suitable for setup and a few test calls. |
| Pre-production | t3.medium — 2 vCPU, 4 GiB RAM | One instance | Use to validate the production configuration and load test realistic concurrent calls. |
| Production | At least two non-burstable instances, such as c7i.large or equivalent — 2 vCPU, 4 GiB RAM each | Multi-AZ / failure-tolerant design | Size from measured CPU, memory, packet loss, jitter, and peak concurrent-call load. |
Use a 30–50 GiB encrypted gp3 root volume, an Elastic IP (or a stable SIP-aware load-balancer/SBC address), and no dependency on swap for real-time media. Burstable t* instances are fine for a demo but are a poor choice for sustained transcoding once CPU credits become a concern.
Do not estimate production capacity solely from vCPU count. Opus-to-G.722/PCMU/PCMA transcoding, call recordings, monitoring, and any future AI/media features all change the usable concurrent-call capacity. Load-test the exact codec/SRTP configuration and retain CPU headroom for call spikes.
Production SIP topology
- Keep a stable, public FQDN such as
sip.example.com; its certificate SAN/CN must cover that exact name. - Keep the DNS record DNS-only. Do not put Cloudflare's HTTP proxy in front of SIP/TLS traffic.
- Allow TCP
5061and the selected UDP media range at both the EC2 Security Group and host firewall. Use a wider, documented media range only when expected call concurrency requires it. - Use an HA-aware SIP SBC/load-balancer or an active/passive failover plan behind the stable FQDN. SIP dialog state and RTP/SRTP handling must remain coherent during a failure; do not assume a generic HTTP load balancer is suitable.
- Monitor certificate expiry and verify the Certbot deploy hook restarts FreeSWITCH after every renewal.
- Keep SSH restricted to administrator IPs or a controlled access path; remove unused ports such as TCP
443if no HTTPS application uses them.
Add a production WhatsApp number
For each production business phone number:
- Confirm the WhatsApp app is in Live mode and has the required production permissions and calling prerequisites.
- Using an access token belonging to the intended production app, configure SIP with the production FQDN and TCP
5061. - Select the SRTP mode deliberately. This runbook uses
SDESbecause it is validated with the FreeSWITCH-to-ElevenLabs route; useDTLSonly after validating full ICE/DTLS-SRTP interoperability end to end. - Retrieve and store that number's Meta SIP password in a secret manager. Meta generates a distinct credential per business-number/app combination.
- Add one FreeSWITCH directory user per number. For example, number
+15550935083uses user ID15550935083and a dedicated file underdirectory/default/. - Add one dialplan extension per number. Each extension should match only its own destination number and bridge only to its approved agent or queue.
- Enable SIP lifecycle webhooks only if the application consumes and protects the resulting call metadata.
Never reuse a test number's SIP password for a production number. If Meta credential rotation is needed, follow Meta's disable SIP → remove server → re-add server flow, then update the corresponding FreeSWITCH secret and test the new credential before accepting calls.
Add another business number to the same FreeSWITCH server
Adding a second business number, such as +60123456789, is more than changing the existing dialplan condition. Keep the first number's route intact and add a separate credential plus a separate dialplan extension.
- Configure
+60123456789as a SIP-enabled number in Meta, pointing to the samesip.example.com:5061server. Retrieve its new, number-specific Meta SIP password. - Import
+60123456789into ElevenLabs and assign its agent. If it uses the same agent as another number, it is still imported as a separate phone-number record so the SIP URI identifier matches. - Create
/etc/freeswitch/directory/default/60123456789.xmlwith that number's Meta password:
<include>
<user id="60123456789">
<params>
<param name="password" value="REPLACE_WITH_META_PASSWORD_FOR_60123456789"/>
</params>
<variables>
<variable name="user_context" value="wa-inbound"/>
</variables>
</user>
</include>- Add this second
<extension>as a sibling of the existing extension inside the samewa-inboundcontext. Do not replace the first extension:
<extension name="whatsapp-60123456789-to-elevenlabs">
<condition field="destination_number" expression="^\+?60123456789$">
<action application="set" data="rtp_secure_media=mandatory:AES_CM_128_HMAC_SHA1_80"/>
<action application="set" data="effective_caller_id_number=+60123456789"/>
<action application="set" data="effective_caller_id_name=WhatsApp Business"/>
<action application="bridge" data="{ignore_early_media=true,rtp_secure_media=mandatory:AES_CM_128_HMAC_SHA1_80,absolute_codec_string=G722,PCMU,PCMA}sofia/internal/sip:+60123456789@sip.rtc.elevenlabs.io:5061;transport=tls"/>
</condition>
</extension>- Install the user file with
freeswitch:freeswitchownership and640permissions. Validate the edited dialplan, then reload XML without dropping active calls:
sudo xmlstarlet val -e /etc/freeswitch/dialplan/wa-inbound.xml
sudo fs_cli -x "reloadxml"
sudo fs_cli -x "user_data 60123456789@sip.example.com var user_context"The user lookup should return wa-inbound. A service restart is not required for only directory/dialplan additions; use reloadxml. Each number needs its own Meta SIP password and its own exact dialplan match, but they can share the TLS profile, RTP range, EC2 instance, and ElevenLabs agent.
Add the matching production number in ElevenLabs
Import the same E.164 production number into ElevenLabs, assign the intended agent, and configure TLS plus required media encryption. Restrict the inbound allowed-address list to the public Elastic IP(s) or SBC egress range that originate calls from your infrastructure. Set the Remote domains entry to the FreeSWITCH/SBC certificate FQDN so in-dialog TLS connections such as BYE validate correctly.
If the production SIP infrastructure uses multiple changing egress IPs, prefer SIP digest authentication or ElevenLabs' enterprise static-SIP option over an unrestricted 0.0.0.0/0 allowlist. Validate call concurrency limits and billing behavior in the ElevenLabs plan before launch.
Production controls and acceptance checklist
- Restrict which callers may reach an agent when the business use case requires it. The default route allows any WhatsApp user who can call the business number after Meta authenticates the SIP leg.
- Keep the SIP dialplan least-privilege: no arbitrary destination dialing, registration, transfer, or unauthenticated contexts.
- Store Meta passwords, API tokens, and certificate private keys outside source control; limit their filesystem permissions.
- Collect service health, certificate-expiry, registration/connection, call-failure, RTP packet-loss, jitter, and CPU alerts. Avoid retaining raw SDP or SIP authorization data in general logs.
- Define call recordings, retention, access control, and user-notice requirements before recording production calls.
- Test inbound calling, two-way audio, hangup in both directions, agent failure, certificate renewal, instance restart, and failover before launch.
- Establish an incident runbook: how to disable SIP for a number, revoke/rotate credentials, fail traffic over, and contact Meta or ElevenLabs support.
10. FreeSWITCH operator cheat sheet
Run FreeSWITCH CLI commands as root with sudo fs_cli -x "COMMAND". Omit -x to open the interactive console. Treat CLI output and logs as sensitive: they can contain caller information and call identifiers.
Service and health
# Service state, recent boot logs, and start-on-boot status
sudo systemctl status freeswitch --no-pager
sudo journalctl -u freeswitch -b -n 100 --no-pager
sudo systemctl is-enabled freeswitch
# Normal service lifecycle
sudo systemctl restart freeswitch # Drops active calls; use in a maintenance window
sudo systemctl enable --now freeswitch
# FreeSWITCH version and active modules/codecs
sudo fs_cli -x "version"
sudo fs_cli -x "show modules"
sudo fs_cli -x "show codecs"
sudo fs_cli -x "show codecs" | grep -Ei 'opus|g722|pcmu|pcma'Performance, resources, and capacity
# Quick host snapshot
uptime
free -h
swapon --show
df -h
df -ih
lsblk
# Interactive process, memory, and disk explorer (installed in the base setup)
btop
# Per-second CPU, run queue, memory, and I/O view; Ctrl+C stops it
vmstat 1
# FreeSWITCH process and active sessions
sudo systemd-cgtop
sudo fs_cli -x "status"
sudo fs_cli -x "show channels"
# Socket summary and interface packet/error counters
sudo ss -s
ip -s link show ens5For extended CPU, disk-I/O, and network history, install sysstat and enable its collector:
sudo apt install -y sysstat
systemctl list-timers --all | grep sysstat
mpstat -P ALL 1
iostat -xz 1
sar -n DEV 1 10Enable the distribution-provided sysstat collection timer shown by the command above if persistent historical data is needed; the exact unit name can differ by Debian release.
During a test call, watch btop or vmstat 1 for sustained CPU saturation, memory pressure, swap use, or I/O wait. Watch ip -s link show ens5 for packet errors/drops. A short CPU spike at call setup is normal; sustained high CPU or growing packet errors under concurrent calls means the instance needs capacity investigation before production.
For production, send host metrics and logs to CloudWatch (or an equivalent monitoring platform) and alert on: instance CPU, memory, disk space/inodes, network errors, FreeSWITCH service restarts, SIP/TLS failures, active-call and failed-call counts, RTP packet loss/jitter, and certificate-expiry/renewal failure. Collect call-volume and failure-rate trends rather than relying only on point-in-time CLI output.
SIP profile, TLS, and network checks
# All Sofia profiles, then the WhatsApp-facing profile in detail
sudo fs_cli -x "sofia status"
sudo fs_cli -x "sofia status profile internal"
# Confirm the listener and its public certificate
sudo ss -ltnp | grep ':5061'
openssl s_client \
-connect sip.example.com:5061 \
-servername sip.example.com \
-verify_hostname sip.example.com </dev/null
# Check runtime values that affect NAT and media advertisements
sudo fs_cli -x 'eval $${external_sip_ip}'
sudo fs_cli -x 'eval $${external_rtp_ip}'
sudo fs_cli -x 'eval $${rtp_start_port}'
sudo fs_cli -x 'eval $${rtp_end_port}'The certificate test must finish with Verify return code: 0 (ok).
Configuration reloads
# Validate edited XML before copying it to /etc/freeswitch
xmlstarlet val -e FILE.xml
# Reload XML dialplan and directory data without restarting active calls
sudo fs_cli -x "reloadxml"
# Verify the Meta credential user resolves to the intended context
sudo fs_cli -x "user_data 15550935083@sip.example.com var user_context"
# Inspect the loaded dialplan context
sudo fs_cli -x "xml_locate dialplan context wa-inbound"Use reloadxml after directory-user or dialplan-only edits. Restart the whole service after changing vars.xml, Sofia profile TLS/bind settings, installed modules, or certificate files. A service restart terminates active calls.
Live calls and call control
# Active channels/calls (contains caller data)
sudo fs_cli -x "show channels"
sudo fs_cli -x "show calls"
# Inspect or terminate one specific call UUID
sudo fs_cli -x "uuid_dump CALL_UUID"
sudo fs_cli -x "uuid_kill CALL_UUID"Obtain CALL_UUID from show channels or the FreeSWITCH log. uuid_kill immediately ends the selected call; use it only when intentionally terminating a call.
Logging and one-call SIP investigation
# Live application log
sudo tail -Fn 0 /var/log/freeswitch/freeswitch.log
# Enable detailed SIP trace for one test call
sudo fs_cli -x "console loglevel debug"
sudo fs_cli -x "sofia global siptrace on"
# Stop trace immediately after the test and restore normal volume
sudo fs_cli -x "sofia global siptrace off"
sudo fs_cli -x "console loglevel notice"SIP trace includes authorization hashes and SRTP key material. Do not copy raw trace logs into tickets, chat, or shared storage. Redact authentication headers, SDP a=crypto values, Set Local/Remote ... Key lines, phone numbers, and Meta identity headers.
Configuration map
| File or location | Common settings / purpose |
|---|---|
/etc/freeswitch/vars.xml | Domain name, external SIP/RTP address, RTP port range, codec order, TLS version, internal profile defaults. |
/etc/freeswitch/sip_profiles/internal.xml | SIP listener, TLS-only behavior, digest realm, context, TLS certificate directory, SIP authentication. |
/etc/freeswitch/directory/default/NUMBER.xml | Per-business-number Meta SIP digest password and user_context. |
/etc/freeswitch/dialplan/wa-inbound.xml | Destination-number matching, SDES-SRTP policy, codec constraints on the ElevenLabs leg, bridge target. |
/etc/freeswitch/tls/all.pem | Let’s Encrypt full chain plus private key; read by agent.pem and tls.pem symlinks. |
/etc/freeswitch/tls/cafile.pem | CA bundle used for peer TLS validation. |
/var/log/freeswitch/freeswitch.log | Primary FreeSWITCH application and SIP diagnostic log. |
| EC2 Security Group and UFW | TCP 5061, UDP RTP/SRTP range, TCP 80 for renewal, and restricted SSH. |
Avoid editing package files under /usr/share/freeswitch unless following an intentional upgrade/customization procedure. Keep private values out of vars.xml and version control; place individual SIP credentials in protected directory-user files or a managed secret workflow.
Troubleshooting
Service cannot start: chown: invalid user: 'freeswitch:freeswitch'
Check whether package setup created the system account:
sudo getent passwd freeswitch
sudo journalctl -u freeswitch -b -n 50 --no-pagerAfter the package account is present, reset the failed state and start the service:
sudo systemctl reset-failed freeswitch
sudo systemctl restart freeswitch
sudo systemctl status freeswitch --no-pagerWhatsApp call fails before FreeSWITCH logs a SIP INVITE
Check the public listener, firewall, and DNS:
sudo ufw status verbose
dig +short A sip.example.com @1.1.1.1
sudo ss -ltnp | grep ':5061'The DNS lookup must return the EC2 Elastic IP, and the profile must listen on TCP 5061. To determine whether Meta reaches the instance, capture only TCP metadata while placing one test call:
sudo apt install -y tcpdump dnsutils
sudo tcpdump -ni any -vvv -s 0 'tcp port 5061'A TCP handshake plus TLS data from Meta proves that AWS networking and the certificate path work. incorrect outbound checksum annotations from tcpdump on EC2 are normally caused by NIC checksum offloading.
Call returns 488 Not Acceptable Here or INCOMPATIBLE_DESTINATION
Enable a one-call SIP trace and read FreeSWITCH's own log file:
sudo fs_cli -x "console loglevel debug"
sudo fs_cli -x "sofia global siptrace on"
sudo tail -Fn 0 /var/log/freeswitch/freeswitch.logAfter the test, stop the tail and restore normal logging:
sudo fs_cli -x "sofia global siptrace off"
sudo fs_cli -x "console loglevel notice"Confirm the trace shows Meta's 407 followed by its authenticated INVITE, ElevenLabs 100/180/200, and compatible media: Opus on the WhatsApp leg and G.722, PCMU, or PCMA on the ElevenLabs leg. A 488 after ElevenLabs returns 200 OK commonly means the A-leg codec was incorrectly constrained; use the bridge-scoped absolute_codec_string shown above.
Never paste raw SIP traces into chat or tickets. Redact or remove Authorization, Proxy-Authorization, SDP a=crypto lines, Set Local/Remote ... Key log lines, call IDs, phone numbers, and x-wa-meta-* identity headers.
Verify an EC2 host key after rebuilding an instance
If an Elastic IP or hostname has been reassigned to a new instance, first verify the IP in the AWS console, then remove the old SSH entry locally:
ssh-keygen -R sip.example.com
ssh-keygen -R YOUR_ELASTIC_IPReconnect normally and accept the new host key only after confirming it belongs to the intended EC2 instance. Do not disable strict host-key checking.
Next steps
- Place a call from an approved WhatsApp test user to the business test number.
- Confirm the ElevenLabs agent answers and two-way audio works.
- After validation, restrict EC2 SSH to the administrator's public IP and remove TCP
443if no HTTPS service uses it. - For production, add caller authorization policy, monitoring, log retention controls, and a deliberate password-rotation procedure.