Let's Encrypt TLS Certificates for Asterisk PJSIP

Security Asterisk 18+ -- Last reviewed 2026-06-27 security tls pjsip letsencrypt certificates Found this useful? Upvote it. ×

The Problem

Asterisk ships with or is commonly set up with self-signed TLS certificates for SIP-TLS transport. While functional, self-signed certs cause validation warnings in SIP clients and provide no chain of trust. If you already have a Let's Encrypt certificate on the server (e.g., for nginx), Asterisk should use it too.

The Complication

Let's Encrypt stores certificates under /etc/letsencrypt/live/ and /etc/letsencrypt/archive/, both of which are restricted to root (mode 700). Asterisk runs as the asterisk user and cannot read these paths directly. Symlinks will not work because the parent directory permissions block traversal.

Solution: Copy + Deploy Hook

Copy the certificates to Asterisk's key directory with correct ownership, then create a certbot deploy hook to auto-copy on renewal.

Step 1: Replace the Self-Signed Certificate

# Back up the existing self-signed cert
sudo cp /etc/asterisk/keys/asterisk.crt /etc/asterisk/keys/asterisk.crt.self-signed.bak
sudo cp /etc/asterisk/keys/asterisk.key /etc/asterisk/keys/asterisk.key.self-signed.bak

# Copy Let's Encrypt cert (use fullchain for proper chain verification)
sudo cp /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem /etc/asterisk/keys/asterisk.crt
sudo cp /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem /etc/asterisk/keys/asterisk.key

# Set ownership and permissions
sudo chown asterisk:asterisk /etc/asterisk/keys/asterisk.crt /etc/asterisk/keys/asterisk.key
sudo chmod 600 /etc/asterisk/keys/asterisk.crt /etc/asterisk/keys/asterisk.key

Use fullchain.pem (not cert.pem) for the certificate file. This includes the intermediate certificate so clients can verify the full chain.

Step 2: Create the Certbot Deploy Hook

sudo tee /etc/letsencrypt/renewal-hooks/deploy/asterisk-cert.sh > /dev/null << 'EOF'
#!/bin/bash
# Deploy Let's Encrypt cert to Asterisk on renewal
# Certbot sets $RENEWED_LINEAGE to the live cert directory

ASTERISK_KEYS=/etc/asterisk/keys

cp "${RENEWED_LINEAGE}/fullchain.pem" "${ASTERISK_KEYS}/asterisk.crt"
cp "${RENEWED_LINEAGE}/privkey.pem" "${ASTERISK_KEYS}/asterisk.key"
chown asterisk:asterisk "${ASTERISK_KEYS}/asterisk.crt" "${ASTERISK_KEYS}/asterisk.key"
chmod 600 "${ASTERISK_KEYS}/asterisk.crt" "${ASTERISK_KEYS}/asterisk.key"

# Restart Asterisk to ensure TLS transport picks up new cert
# (allow_reload=false on TLS transport means a module reload may not suffice)
systemctl restart asterisk 2>/dev/null

logger -t certbot-asterisk "Deployed renewed certificate to Asterisk"
EOF

sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/asterisk-cert.sh

The deploy hook runs only when a certificate is actually renewed, not on every certbot run. Certbot sets $RENEWED_LINEAGE to the path containing the new certs.

We use systemctl restart asterisk rather than asterisk -rx 'module reload res_pjsip' because the TLS transport has allow_reload = false by default. A reload may not cause the transport to re-read the certificate files.

Step 3: Restart Asterisk

sudo systemctl restart asterisk

Step 4: Verify

# Check the cert being served on the TLS port
echo | openssl s_client -connect 127.0.0.1:5061 \
    -servername YOUR_DOMAIN 2>/dev/null | \
    openssl x509 -noout -issuer -subject -dates

# Expected output (issuer will be one of Let's Encrypt's current intermediate CAs):
# issuer=C = US, O = Let's Encrypt, CN = R10
# subject=CN = YOUR_DOMAIN
# notBefore=...
# notAfter=...

PJSIP Transport Reference

For reference, the PJSIP TLS transport configuration in pjsip.conf:

[transport-tls]
type = transport
protocol = tls
bind = 0.0.0.0:5061
cert_file = /etc/asterisk/keys/asterisk.crt
priv_key_file = /etc/asterisk/keys/asterisk.key
verify_client = no
verify_server = no

Omitting method lets OpenSSL negotiate the highest mutually supported version (TLS 1.2 or 1.3). Setting method = tlsv1_2 restricts the transport to TLS 1.2 only and prevents TLS 1.3 connections.

No changes needed here; the cert files at these paths are now the LE certs.

Certbot Renewal with Firewalled Port 80

If your server's iptables blocks port 80 from the internet (common for PBX servers), certbot's HTTP-01 challenge will fail during renewal. Two approaches:

DNS-01 validates domain ownership via a DNS TXT record and requires no open ports. It works regardless of firewall rules. You need a certbot DNS plugin for your DNS provider.

# Example with Cloudflare (adjust for your DNS provider)
sudo apt install python3-certbot-dns-cloudflare

# Create credentials file
sudo mkdir -p /etc/letsencrypt/secrets
sudo tee /etc/letsencrypt/secrets/cloudflare.ini > /dev/null << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/secrets/cloudflare.ini

# Issue or renew using DNS-01
sudo certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /etc/letsencrypt/secrets/cloudflare.ini \
    -d YOUR_DOMAIN

Common provider plugins: python3-certbot-dns-route53 (AWS Route 53), python3-certbot-dns-digitalocean, python3-certbot-dns-google.

Option B: Firewall Pre/Post Hooks

Temporarily open port 80 during the ACME challenge:

# Pre-hook: open port 80
sudo tee /etc/letsencrypt/renewal-hooks/pre/open-http.sh > /dev/null << 'EOF'
#!/bin/bash
iptables -I INPUT 5 -p tcp --dport 80 -j ACCEPT
logger -t certbot-firewall "Opened port 80 for ACME challenge"
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/pre/open-http.sh

# Post-hook: close port 80
sudo tee /etc/letsencrypt/renewal-hooks/post/close-http.sh > /dev/null << 'EOF'
#!/bin/bash
iptables -D INPUT -p tcp --dport 80 -j ACCEPT
logger -t certbot-firewall "Closed port 80 after ACME challenge"
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/close-http.sh

The rule number in -I INPUT 5 should place the ACCEPT rule before any DROP rules in your chain. Adjust the number based on your iptables layout.

Verify Renewal Works

sudo certbot renew --dry-run

Important Notes

User Notes

Know a tip or gotcha for this topic? Share it below and help others.

Contribute a note

Share a tip, gotcha, or practical example. Keep it under 2000 characters. No questions (use the Asterisk community forums for support). Wrap code in backticks.

Moderated before publishing. Email never shown.
Related Snippets