Skip to content

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.small or 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 13 trixie suite.

1. AWS and DNS prerequisites

  1. Associate an Elastic IP with the EC2 instance.
  2. In Cloudflare, create an A record for sip.example.com pointing to the Elastic IP.
  3. Set the Cloudflare record to DNS only (gray cloud). Cloudflare's proxy must not front the SIP hostname.
  4. Configure the EC2 Security Group with these inbound rules:
ProtocolPort(s)SourcePurpose
TCP22Administrator IP onlySSH
TCP800.0.0.0/0Let's Encrypt HTTP-01 validation and renewal
TCP50610.0.0.0/0 initiallySIP over TLS
UDP16384-164840.0.0.0/0 initiallyRTP/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:

bash
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_Lumpur

Optional: add swap. It helps package operations and source builds; real-time calls should not depend on it.

bash
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
fi

Enable the host firewall. The EC2 Security Group should still restrict SSH to the administrator IP.

bash
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 verbose

3. Public TLS certificate

With DNS resolving to the instance and TCP 80 open:

bash
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-run

The resulting files are:

text
/etc/letsencrypt/live/sip.example.com/fullchain.pem
/etc/letsencrypt/live/sip.example.com/privkey.pem

Keep 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.

bash
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 freeswitch

Do 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:

bash
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_TOKEN

5. Install and verify Opus

The WhatsApp-facing leg needs Opus. Install it explicitly, even if freeswitch-meta-vanilla is installed:

bash
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 opus

Expected output includes a line similar to:

text
codec,OPUS (STANDARD),mod_opus

An 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:

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:

xml
<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:

bash
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.xml

7. 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.

bash
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.pem

Restart FreeSWITCH and verify both the listener and the externally verifiable certificate chain:

bash
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/null

The expected final line is:

text
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:

bash
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-run

8. Configure WhatsApp, ElevenLabs, and the FreeSWITCH route

WhatsApp SIP settings

For the business phone number, configure these Meta calling settings:

SettingValue
SIP statusENABLED
SIP serversip.example.com
SIP port5061
SRTP key exchangeSDES
SIP webhook deliveryOptional; 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:

SettingValue
Inbound SIP serversip:sip.rtc.elevenlabs.io:5061;transport=tls
Inbound media encryptionRequired
Allowed addressesThe EC2 Elastic IP only, for example 203.0.113.10/32
Allowed numbersAll numbers initially
Remote domainssip.example.com
Outbound addresssip.example.com (hostname only)
Outbound transportTLS
Outbound media encryptionRequired
Enabled codecsG.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:

xml
<!-- /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:

bash
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.xml

wa-inbound dialplan

Install this dialplan as /etc/freeswitch/dialplan/wa-inbound.xml, replacing the example number where needed:

xml
<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:

bash
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.xml

Expected 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

EnvironmentCompute starting pointAvailabilityNotes
Demot3.small — 2 vCPU, 2 GiB RAMOne instanceSuitable for setup and a few test calls.
Pre-productiont3.medium — 2 vCPU, 4 GiB RAMOne instanceUse to validate the production configuration and load test realistic concurrent calls.
ProductionAt least two non-burstable instances, such as c7i.large or equivalent — 2 vCPU, 4 GiB RAM eachMulti-AZ / failure-tolerant designSize 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 5061 and 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 443 if no HTTPS application uses them.

Add a production WhatsApp number

For each production business phone number:

  1. Confirm the WhatsApp app is in Live mode and has the required production permissions and calling prerequisites.
  2. Using an access token belonging to the intended production app, configure SIP with the production FQDN and TCP 5061.
  3. Select the SRTP mode deliberately. This runbook uses SDES because it is validated with the FreeSWITCH-to-ElevenLabs route; use DTLS only after validating full ICE/DTLS-SRTP interoperability end to end.
  4. Retrieve and store that number's Meta SIP password in a secret manager. Meta generates a distinct credential per business-number/app combination.
  5. Add one FreeSWITCH directory user per number. For example, number +15550935083 uses user ID 15550935083 and a dedicated file under directory/default/.
  6. Add one dialplan extension per number. Each extension should match only its own destination number and bridge only to its approved agent or queue.
  7. 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.

  1. Configure +60123456789 as a SIP-enabled number in Meta, pointing to the same sip.example.com:5061 server. Retrieve its new, number-specific Meta SIP password.
  2. Import +60123456789 into 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.
  3. Create /etc/freeswitch/directory/default/60123456789.xml with that number's Meta password:
xml
<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>
  1. Add this second <extension> as a sibling of the existing extension inside the same wa-inbound context. Do not replace the first extension:
xml
<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>
  1. Install the user file with freeswitch:freeswitch ownership and 640 permissions. Validate the edited dialplan, then reload XML without dropping active calls:
bash
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

bash
# 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

bash
# 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 ens5

For extended CPU, disk-I/O, and network history, install sysstat and enable its collector:

bash
sudo apt install -y sysstat
systemctl list-timers --all | grep sysstat

mpstat -P ALL 1
iostat -xz 1
sar -n DEV 1 10

Enable 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

bash
# 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

bash
# 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

bash
# 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

bash
# 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 locationCommon settings / purpose
/etc/freeswitch/vars.xmlDomain name, external SIP/RTP address, RTP port range, codec order, TLS version, internal profile defaults.
/etc/freeswitch/sip_profiles/internal.xmlSIP listener, TLS-only behavior, digest realm, context, TLS certificate directory, SIP authentication.
/etc/freeswitch/directory/default/NUMBER.xmlPer-business-number Meta SIP digest password and user_context.
/etc/freeswitch/dialplan/wa-inbound.xmlDestination-number matching, SDES-SRTP policy, codec constraints on the ElevenLabs leg, bridge target.
/etc/freeswitch/tls/all.pemLet’s Encrypt full chain plus private key; read by agent.pem and tls.pem symlinks.
/etc/freeswitch/tls/cafile.pemCA bundle used for peer TLS validation.
/var/log/freeswitch/freeswitch.logPrimary FreeSWITCH application and SIP diagnostic log.
EC2 Security Group and UFWTCP 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:

bash
sudo getent passwd freeswitch
sudo journalctl -u freeswitch -b -n 50 --no-pager

After the package account is present, reset the failed state and start the service:

bash
sudo systemctl reset-failed freeswitch
sudo systemctl restart freeswitch
sudo systemctl status freeswitch --no-pager

WhatsApp call fails before FreeSWITCH logs a SIP INVITE

Check the public listener, firewall, and DNS:

bash
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:

bash
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:

bash
sudo fs_cli -x "console loglevel debug"
sudo fs_cli -x "sofia global siptrace on"
sudo tail -Fn 0 /var/log/freeswitch/freeswitch.log

After the test, stop the tail and restore normal logging:

bash
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:

bash
ssh-keygen -R sip.example.com
ssh-keygen -R YOUR_ELASTIC_IP

Reconnect 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

  1. Place a call from an approved WhatsApp test user to the business test number.
  2. Confirm the ElevenLabs agent answers and two-way audio works.
  3. After validation, restrict EC2 SSH to the administrator's public IP and remove TCP 443 if no HTTPS service uses it.
  4. For production, add caller authorization policy, monitoring, log retention controls, and a deliberate password-rotation procedure.