FreeBSD.software
Home/Blog/How to Set Up Let's Encrypt SSL on FreeBSD with Certbot
tutorial2026-03-29

How to Set Up Let's Encrypt SSL on FreeBSD with Certbot

Step-by-step guide to setting up free SSL/TLS certificates on FreeBSD with Let's Encrypt and Certbot. Covers NGINX plugin, Apache plugin, standalone mode, wildcard certs, auto-renewal, and troubleshooting.

# How to Set Up Let's Encrypt SSL on FreeBSD with Certbot

Every public-facing web server needs TLS. There is no legitimate reason to serve traffic over plain HTTP in 2026. Let's Encrypt provides free, automated, and widely trusted SSL/TLS certificates, and Certbot is the standard client for obtaining and managing them. This guide covers every practical method for getting Let's Encrypt certificates on FreeBSD -- from the one-command NGINX plugin to manual DNS challenges for wildcard certs.

By the end of this tutorial you will have a working TLS setup with automatic renewal, strong cipher configuration, and HSTS headers. If you have not yet installed your web server, start with the [NGINX setup guide](/blog/nginx-freebsd-production-setup/) first.

Why Let's Encrypt

Let's Encrypt is a non-profit Certificate Authority run by the Internet Security Research Group (ISRG). Three things make it the default choice for server operators:

- **Free certificates.** No payment, no registration fees, no per-domain charges. You can issue certificates for as many domains as you control.

- **Automated issuance and renewal.** Certbot handles the entire ACME protocol flow. A single cron job keeps your certificates current without manual intervention.

- **Trusted by all major browsers and operating systems.** Let's Encrypt certificates chain to the ISRG Root X1, which is in every modern trust store. Your visitors will never see a certificate warning.

Certificates are valid for 90 days. This short lifetime is intentional -- it encourages automation and limits the damage window if a private key is compromised. With proper auto-renewal configured, you will never think about expiration dates again.

Prerequisites

Before you begin, make sure you have:

- A FreeBSD server (13.x or 14.x) with root or sudo access

- A registered domain name with DNS A/AAAA records pointing to your server's IP address

- Port 80 (HTTP) reachable from the internet (required for most validation methods)

- A working web server installation, or willingness to use standalone mode

If you are starting from scratch, the [FreeBSD VPS setup guide](/blog/freebsd-vps-setup/) covers initial server configuration and the [FreeBSD hardening guide](/blog/hardening-freebsd-server/) covers firewall rules and SSH lockdown.

Installing Certbot on FreeBSD

Certbot is available in the FreeBSD ports tree and as a binary package. The package route is faster:

bash

pkg install py311-certbot

This installs Certbot and its Python dependencies. Verify the installation:

bash

certbot --version

Installing Web Server Plugins

If you plan to use the NGINX or Apache plugins (recommended for most users), install the corresponding package:

For NGINX:

bash

pkg install py311-certbot-nginx

For Apache:

bash

pkg install py311-certbot-apache

These plugins can automatically modify your web server configuration to enable SSL, saving you from manual config file editing.

**Note on Python versions:** FreeBSD packages may use different Python version prefixes (py311, py312, etc.) depending on your FreeBSD version and package repository. If py311-certbot is not found, check available versions with pkg search certbot.

Method 1: NGINX Plugin (Recommended)

The NGINX plugin is the easiest path. It reads your existing NGINX configuration, obtains a certificate for the domains it finds, and writes the SSL directives directly into your server blocks.

Make sure NGINX is running with a valid configuration that includes a server_name directive:

bash

service nginx status

Then run Certbot with the --nginx flag:

bash

certbot --nginx -d example.com -d www.example.com

Certbot will:

1. Verify you control the domain by placing a temporary file in the webroot and having Let's Encrypt fetch it over HTTP.

2. Obtain the certificate and private key.

3. Modify your NGINX configuration to add ssl_certificate, ssl_certificate_key, and a redirect from HTTP to HTTPS.

4. Reload NGINX.

You can specify multiple -d flags for additional domains or subdomains. All domains will be covered by a single certificate (SAN certificate).

If you want the certificate without automatic NGINX configuration changes:

bash

certbot certonly --nginx -d example.com -d www.example.com

The certonly subcommand obtains the certificate but does not modify any configuration files. You then configure NGINX manually (see the SSL configuration section below).

Method 2: Apache Plugin

The Apache plugin works the same way as the NGINX plugin but targets Apache (httpd) configurations:

bash

certbot --apache -d example.com -d www.example.com

On FreeBSD, Apache's configuration lives under /usr/local/etc/apache24/. Certbot will find your VirtualHost definitions and add the SSL directives.

For certificate-only mode without config changes:

bash

certbot certonly --apache -d example.com -d www.example.com

Method 3: Standalone Mode

Standalone mode runs its own temporary web server on port 80 to handle the ACME challenge. Use this when you do not have a web server installed, or you want to obtain certificates before configuring one.

**Important:** Nothing else can be listening on port 80. Stop NGINX or Apache first if they are running:

bash

service nginx stop

certbot certonly --standalone -d example.com -d www.example.com

service nginx start

Standalone mode is useful for mail servers, database servers, or any machine that needs a certificate but does not run a web server full-time.

You can also bind to port 443 instead using the --preferred-challenges tls-sni option, though the HTTP-01 challenge on port 80 is more commonly supported.

Method 4: Webroot Mode

Webroot mode places the ACME challenge files in an existing web server's document root. The web server keeps running -- no downtime required.

bash

certbot certonly --webroot -w /usr/local/www/example.com -d example.com -d www.example.com

The -w flag specifies the webroot directory. Certbot creates a .well-known/acme-challenge/ directory inside it, places a token file there, and Let's Encrypt verifies it over HTTP.

Your web server must serve files from that directory. For NGINX, ensure your server block includes:

nginx

server {

listen 80;

server_name example.com www.example.com;

location /.well-known/acme-challenge/ {

root /usr/local/www/example.com;

}

location / {

return 301 https://$host$request_uri;

}

}

Webroot mode is a good choice when you cannot afford even a brief interruption on port 80, or when you have multiple virtual hosts and want fine-grained control over which webroot is used for each domain.

Method 5: DNS Challenge for Wildcard Certificates

Wildcard certificates (e.g., *.example.com) require DNS-01 validation. You prove domain ownership by creating a specific TXT record in your DNS zone.

bash

certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com

Certbot will display a TXT record name and value:


Please deploy a DNS TXT record under the name:

_acme-challenge.example.com

with the following value:

kJ3nR7..."

Log in to your DNS provider, add the TXT record, wait for propagation (usually 1-5 minutes), then press Enter in the Certbot prompt.

You can verify DNS propagation before confirming:

bash

drill TXT _acme-challenge.example.com

Or if drill is not installed:

bash

host -t TXT _acme-challenge.example.com

Automating DNS Challenges

The manual DNS method does not support automatic renewal because it requires human interaction. For automated wildcard renewals, use a DNS plugin that supports your DNS provider's API. For example, with Cloudflare:

bash

pkg install py311-certbot-dns-cloudflare

Create a credentials file at /usr/local/etc/letsencrypt/cloudflare.ini:

ini

dns_cloudflare_api_token = YOUR_API_TOKEN_HERE

Secure it:

bash

chmod 600 /usr/local/etc/letsencrypt/cloudflare.ini

Then obtain the wildcard certificate:

bash

certbot certonly --dns-cloudflare \

--dns-cloudflare-credentials /usr/local/etc/letsencrypt/cloudflare.ini \

-d "*.example.com" -d example.com

This method is fully automatable and works with the renewal cron job described below.

Setting Up Auto-Renewal

Certificates expire after 90 days. Certbot's renew command checks all installed certificates and renews any that are within 30 days of expiration.

Option 1: Cron Job

Add a cron entry for root:

bash

crontab -e

Add this line:

cron

0 3 * * * /usr/local/bin/certbot renew --quiet --deploy-hook "service nginx reload"

This runs at 3 AM daily. The --quiet flag suppresses output unless there is an error. The --deploy-hook only executes when a certificate is actually renewed, triggering an NGINX reload to pick up the new certificate.

For Apache, change the deploy hook:

cron

0 3 * * * /usr/local/bin/certbot renew --quiet --deploy-hook "service apache24 reload"

Option 2: FreeBSD Periodic Task

FreeBSD's periodic system is an alternative to raw cron. Create /usr/local/etc/periodic/weekly/certbot-renew:

bash

#!/bin/sh

/usr/local/bin/certbot renew --quiet --deploy-hook "service nginx reload"

Make it executable:

bash

chmod +x /usr/local/etc/periodic/weekly/certbot-renew

Weekly renewal checks are sufficient since Certbot only renews certificates within 30 days of expiration.

Testing Renewal

Always test renewal after initial setup to catch configuration problems before they cause an outage:

bash

certbot renew --dry-run

This simulates the renewal process without making any changes. You should see output like:


Congratulations, all simulated renewals succeeded:

/usr/local/etc/letsencrypt/live/example.com/fullchain.pem (success)

If the dry run fails, fix the issue now rather than discovering it at 2 AM when your certificate expires.

Certificate Locations and Permissions

Certbot stores certificates under /usr/local/etc/letsencrypt/ on FreeBSD (not /etc/letsencrypt/ as on Linux). The key paths for each domain:

| File | Path | Contents |

|------|------|----------|

| Certificate | /usr/local/etc/letsencrypt/live/example.com/cert.pem | Your domain certificate only |

| Full chain | /usr/local/etc/letsencrypt/live/example.com/fullchain.pem | Certificate + intermediate CA |

| Private key | /usr/local/etc/letsencrypt/live/example.com/privkey.pem | RSA/ECDSA private key |

| Chain | /usr/local/etc/letsencrypt/live/example.com/chain.pem | Intermediate CA certificate |

For web server configuration, use fullchain.pem (not cert.pem) and privkey.pem. The full chain ensures clients receive the intermediate certificate needed to build the trust path.

The files under live/ are symlinks to the current version in archive/. Certbot manages the symlinks during renewal -- your web server configuration always points to the same paths.

Permissions

The private key must be protected:

bash

ls -la /usr/local/etc/letsencrypt/live/example.com/

The privkey.pem file should be readable only by root (mode 0600). Certbot sets this correctly by default. If your web server runs as a non-root user and cannot read the key, the standard solution is to let the master process (which starts as root) read the key before dropping privileges -- both NGINX and Apache do this automatically.

Do not chmod the key to 644. If an application requires non-root access to the key, use group permissions carefully or copy the key to a location with appropriate ownership.

NGINX SSL Configuration Best Practices

After obtaining your certificate, configure NGINX with modern TLS settings. Here is a complete server block:

nginx

server {

listen 80;

server_name example.com www.example.com;

return 301 https://$host$request_uri;

}

server {

listen 443 ssl http2;

server_name example.com www.example.com;

ssl_certificate /usr/local/etc/letsencrypt/live/example.com/fullchain.pem;

ssl_certificate_key /usr/local/etc/letsencrypt/live/example.com/privkey.pem;

# TLS configuration

ssl_protocols TLSv1.2 TLSv1.3;

ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;

ssl_prefer_server_ciphers off;

# SSL session caching

ssl_session_cache shared:SSL:10m;

ssl_session_timeout 1d;

ssl_session_tickets off;

# OCSP stapling

ssl_stapling on;

ssl_stapling_verify on;

ssl_trusted_certificate /usr/local/etc/letsencrypt/live/example.com/chain.pem;

resolver 1.1.1.1 8.8.8.8 valid=300s;

resolver_timeout 5s;

# Security headers

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

add_header X-Content-Type-Options "nosniff" always;

add_header X-Frame-Options "SAMEORIGIN" always;

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Document root

root /usr/local/www/example.com;

index index.html;

# ACME challenge location (for webroot renewals)

location /.well-known/acme-challenge/ {

root /usr/local/www/example.com;

}

location / {

try_files $uri $uri/ =404;

}

}

Key points about this configuration:

- **TLS 1.2 and 1.3 only.** TLS 1.0 and 1.1 are deprecated and should never be enabled.

- **Modern cipher suite.** The cipher list above supports all current browsers while excluding weak algorithms. Setting ssl_prefer_server_ciphers off is correct for TLS 1.3 where the client's preference order is used.

- **OCSP stapling.** Your server fetches the OCSP response from the CA and staples it to the TLS handshake. This speeds up connection establishment and improves privacy (the client does not need to contact the CA separately).

- **HSTS with a two-year max-age.** Tells browsers to only connect over HTTPS. The preload directive allows submission to the HSTS preload list. Only add preload if you are certain your entire domain will always use HTTPS.

- **Session tickets disabled.** Session tickets can weaken forward secrecy unless you rotate the ticket keys regularly. Disabling them is the safer default.

Test your configuration before reloading:

bash

nginx -t

service nginx reload

After deployment, verify your TLS configuration at [SSL Labs](https://www.ssllabs.com/ssltest/). You should score an A or A+ with this configuration.

Troubleshooting

Rate Limits

Let's Encrypt enforces rate limits to prevent abuse:

- **50 certificates per registered domain per week.** A "registered domain" is the base domain (e.g., example.com), not subdomains.

- **5 duplicate certificates per week.** Requesting the exact same set of domain names counts toward this limit.

- **5 failed validation attempts per hostname per hour.**

If you hit rate limits during testing, use the staging environment:

bash

certbot certonly --staging --nginx -d example.com

Staging certificates are not trusted by browsers, but the rate limits are much higher. Remove --staging when you are ready for production.

DNS Issues

If Certbot fails with "DNS problem" or "NXDOMAIN":

1. Verify your A record resolves correctly:

bash

drill example.com

2. Check that the DNS record points to your server's actual IP address, not a load balancer or CDN that might intercept the request.

3. If you recently changed DNS records, wait for TTL expiration. Check current propagation:

bash

drill @8.8.8.8 example.com

Port 80 Blocked

The HTTP-01 challenge requires inbound traffic on port 80. Common blockers:

- **Firewall rules.** Check your PF or IPFW rules:

bash

pfctl -sr | grep 80

- **ISP blocking port 80.** Some residential ISPs block inbound port 80. Use the DNS challenge method instead.

- **Another process using port 80.** Find what is listening:

bash

sockstat -4 -l | grep :80

Permission Errors

If Certbot fails with permission errors:

- Run Certbot as root or with sudo. Certbot needs to write to /usr/local/etc/letsencrypt/ and read your web server configuration.

- Check that the webroot directory is writable:

bash

ls -la /usr/local/www/example.com/

Certificate Not Trusted in Browser

- Make sure you are using fullchain.pem, not cert.pem, in your web server configuration. The full chain includes the intermediate certificate that browsers need.

- If you used --staging for testing, you need to re-issue with the production server (remove --staging).

Renewal Fails Silently

Check the renewal log:

bash

cat /var/log/letsencrypt/letsencrypt.log

Common renewal failures:

- Web server configuration changed since initial issuance

- Domain no longer points to the server

- Port 80 is no longer accessible

- Certbot was upgraded and the renewal config references an old authenticator

Review the renewal configuration:

bash

cat /usr/local/etc/letsencrypt/renewal/example.com.conf

Alternative: acme.sh

If you prefer a lighter-weight solution, [acme.sh](https://github.com/acmesh-official/acme.sh) is a pure shell script ACME client with no dependencies beyond what ships with FreeBSD base.

Install it:

bash

pkg install acme.sh

Or install from source:

bash

curl https://get.acme.sh | sh

Issue a certificate using webroot mode:

bash

acme.sh --issue -d example.com -d www.example.com -w /usr/local/www/example.com

Install the certificate to a permanent location:

bash

acme.sh --install-cert -d example.com \

--key-file /usr/local/etc/ssl/example.com.key \

--fullchain-file /usr/local/etc/ssl/example.com.fullchain.pem \

--reloadcmd "service nginx reload"

acme.sh automatically sets up a cron job for renewal during installation. It supports over 100 DNS APIs for automated wildcard certificate issuance.

The main advantages of acme.sh over Certbot:

- No Python dependency (pure POSIX shell)

- Smaller footprint

- Built-in support for a very large number of DNS providers

- Native FreeBSD package available

The main disadvantage is that it does not have NGINX/Apache plugins that automatically configure your web server. You handle the configuration manually.

For most users, Certbot with the NGINX plugin is the fastest path. For minimalists or systems where Python is unwanted, acme.sh is the better choice.

FAQ

How long do Let's Encrypt certificates last?

Let's Encrypt certificates are valid for 90 days. With auto-renewal configured (see above), Certbot will renew them when they are within 30 days of expiration. You will typically see renewals every 60 days without any manual action.

Can I get a wildcard certificate from Let's Encrypt?

Yes. Wildcard certificates require the DNS-01 challenge. You cannot use HTTP validation for wildcards. Use certbot certonly --manual --preferred-challenges dns -d "*.example.com" for one-time issuance, or a DNS plugin like certbot-dns-cloudflare for automated renewal. See Method 5 above.

What happens if my certificate expires?

Browsers will display a security warning and most users will not proceed past it. Search engines may also flag your site. This is why auto-renewal is critical. If your certificate has already expired, simply run certbot renew --force-renewal to obtain a new one immediately, then reload your web server.

Can I use Let's Encrypt for internal or private servers?

Let's Encrypt requires domain validation over the public internet. If your server is not reachable from the internet, you have two options: use the DNS challenge method (which does not require inbound connectivity, only the ability to create DNS records), or use a different CA that supports manual validation. For purely internal services with no public DNS, consider generating your own CA with tools like easyrsa or step-ca.

Does Let's Encrypt support IP address certificates?

No. Let's Encrypt only issues certificates for domain names, not IP addresses. You need a fully qualified domain name with proper DNS records. If you only have an IP address, register a domain or use a free DNS service.

How do I revoke a certificate?

If your private key is compromised, revoke the certificate immediately:

bash

certbot revoke --cert-path /usr/local/etc/letsencrypt/live/example.com/cert.pem

Then delete the local files and issue a new certificate:

bash

certbot delete --cert-name example.com

certbot certonly --nginx -d example.com

Can I use the same certificate on multiple servers?

Yes. Copy the certificate, private key, and chain files to each server. However, you need to manage renewal carefully -- only one server should renew, then distribute the updated files. For multi-server setups, the DNS challenge method is often simplest because it does not depend on which server handles HTTP traffic.

Summary

Setting up Let's Encrypt on FreeBSD takes about five minutes with Certbot and the NGINX plugin. The essential steps are:

1. Install Certbot: pkg install py311-certbot py311-certbot-nginx

2. Obtain a certificate: certbot --nginx -d example.com

3. Set up auto-renewal: add certbot renew to cron

4. Verify: certbot renew --dry-run

Combine this with the TLS configuration from the NGINX section above and you have a production-grade SSL setup that maintains itself. Check the [NGINX production setup guide](/blog/nginx-freebsd-production-setup/) for the full web server configuration, and the [FreeBSD hardening guide](/blog/hardening-freebsd-server/) for additional security measures beyond TLS.