--- title: "Get Let's Encrypt certificates for IP addresses with acme.sh" tags: ['Let''s Encrypt', 'SSL', 'Nginx'] published: 2026-01-15 18:55:48 updated: 2026-01-15 18:55:48 excerpt: "Issue free TLS certificates for IP addresses with acme.sh and Let's Encrypt, then serve them with NGINX over HTTP/2 and HTTP/3." --- > This file is the raw Markdown source of an m.ac post; the canonical version lives at https://m.ac/letsencrypt-ip-certificate/ > The full index of posts and pages is at https://m.ac/llms.txt — read the index first before crawling further. This guide uses `acme.sh` to obtain Let's Encrypt TLS certificates for IP addresses. For a long time, Let's Encrypt issued certificates only for **domain names**. After several months of [testing](https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate), Let's Encrypt made six-day and IP address certificates generally available on January 15, 2026. ## 1. Why issue certificates for IP addresses Sometimes a domain name is unnecessary, but **HTTPS is still required**: 1. **DNS over HTTPS (DoH) services that operate independently of domain resolution** Running DoH directly on an IP address avoids the old philosophical headache: doing an insecure DNS lookup before opening a secure DNS connection. 2. **Default server blocks that should not expose real names** A default HTTPS vhost can present an IP certificate instead of leaking one of your real hostnames. 3. **Temporary services or test environments** You can bring up HTTPS without creating DNS records first. 4. **Avoiding domain exposure in Certificate Transparency logs** If a service does not need a domain name, an IP certificate avoids publishing that domain name in public CT logs. ## 2. Prerequisites The commands below use `sudo`. If you prefer a root shell, run `sudo -i` first and drop `sudo` from the commands. Update `acme.sh` first: ```bash acme.sh --upgrade ``` IP address certificates can only use [http-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) or [tls-alpn-01](https://letsencrypt.org/docs/challenge-types/#tls-alpn-01) validation. Make sure your firewall allows public access to TCP port 80 and TCP/UDP port 443. ## 3. Configure the NGINX default server for port 80 Set up a default NGINX server block on port 80. On Debian or Ubuntu systems with NGINX installed, replace `/etc/nginx/sites-available/default` with this configuration: ```nginx server { # Listen on port 80 for all IPv4 and IPv6 addresses listen 80 default_server; listen [::]:80 default_server; # Match all domain names server_name _; # Merge Let's Encrypt and SSL verification path configuration location ~ ^/.well-known/(acme-challenge|pki-validation)/ { add_header Content-Type text/plain; root /var/www/letsencrypt; } # Redirect all other HTTP requests to HTTPS using 301 permanent redirect location / { return 301 https://$host$request_uri; } } ``` Create the required directories and reload NGINX: ```bash sudo mkdir -p /var/www/letsencrypt sudo mkdir -p /etc/nginx/ssl sudo nginx -t sudo nginx -s reload ``` ## 4. Issue the IP address certificate with acme.sh Assume your server's IP addresses are `192.0.2.2` and `2001:db8::2`, and request the `shortlived` profile: ```bash acme.sh --issue --server letsencrypt -d 192.0.2.2 -d 2001:db8::2 \ -w /var/www/letsencrypt \ --certificate-profile shortlived \ --days 3 ``` The [shortlived](https://letsencrypt.org/docs/profiles/#shortlived) profile is required for IP address certificates. Let's Encrypt limits these certificates to 160 hours, so `acme.sh` needs to check renewal more often than it would for a normal 90-day certificate. `--days 3` checks every 3 days. A value of 4 or 5 can still work, but 6 or higher is asking for an avoidable expiry page. If issuance succeeds, you will see output like this: ``` [Wed Dec 17 05:46:28 AM UTC 2025] Using CA: https://acme-v02.api.letsencrypt.org/directory [Wed Dec 17 05:46:28 AM UTC 2025] Multi domain='IP:192.0.2.2,IP:2001:db8::2' [Wed Dec 17 05:46:30 AM UTC 2025] Getting webroot for domain='192.0.2.2' [Wed Dec 17 05:46:30 AM UTC 2025] Getting webroot for domain='2001:db8::2' [Wed Dec 17 05:46:30 AM UTC 2025] Verifying: 192.0.2.2 [Wed Dec 17 05:46:31 AM UTC 2025] Pending. The CA is processing your order, please wait. (1/30) [Wed Dec 17 05:46:34 AM UTC 2025] Success [Wed Dec 17 05:46:34 AM UTC 2025] Verifying: 2001:db8::2 [Wed Dec 17 05:46:35 AM UTC 2025] Pending. The CA is processing your order, please wait. (1/30) [Wed Dec 17 05:46:38 AM UTC 2025] Success [Wed Dec 17 05:46:38 AM UTC 2025] Verification finished, beginning signing. [Wed Dec 17 05:46:38 AM UTC 2025] Let's finalize the order. [Wed Dec 17 05:46:38 AM UTC 2025] Le_OrderFinalize='https://acme-v02.api.letsencrypt.org/acme/finalize/blablablablablablablabla/blablablablablablablabla' [Wed Dec 17 05:46:41 AM UTC 2025] Downloading cert. [Wed Dec 17 05:46:41 AM UTC 2025] Le_LinkCert='https://acme-v02.api.letsencrypt.org/acme/cert/blablablablablablablabla' [Wed Dec 17 05:46:42 AM UTC 2025] Cert success. -----BEGIN CERTIFICATE----- blablablablablablablablablablablablablablablablablablabla -----END CERTIFICATE----- [Wed Dec 17 05:46:42 AM UTC 2025] Your cert is in: /root/.acme.sh/192.0.2.2_ecc/192.0.2.2.cer [Wed Dec 17 05:46:42 AM UTC 2025] Your cert key is in: /root/.acme.sh/192.0.2.2_ecc/192.0.2.2.key [Wed Dec 17 05:46:42 AM UTC 2025] The intermediate CA cert is in: /root/.acme.sh/192.0.2.2_ecc/ca.cer [Wed Dec 17 05:46:42 AM UTC 2025] And the full-chain cert is in: /root/.acme.sh/192.0.2.2_ecc/fullchain.cer ``` Install the issued certificate into `/etc/nginx/ssl`: ```bash acme.sh --install-cert -d 192.0.2.2 \ --key-file /etc/nginx/ssl/ip.key \ --fullchain-file /etc/nginx/ssl/ip.crt \ --ca-file /etc/nginx/ssl/ip.ca.crt \ --reloadcmd "systemctl restart nginx" ``` ## 5. Configure the NGINX default server for port 443 After installing the certificate, add the default HTTPS server block to `/etc/nginx/sites-available/default`: ```nginx # HTTPS server block for all HTTPS requests server { # Standard TLS listening listen 443 ssl default_server; listen [::]:443 ssl default_server; # HTTP/2 protocol support http2 on; # HTTP/3 QUIC protocol support listen 443 quic reuseport; listen [::]:443 quic reuseport; add_header Alt-Svc 'h3=":443"; ma=86400' always; add_header X-Protocol $server_protocol always; # Match all domain names server_name _; return 403; # modern configuration ssl_protocols TLSv1.3; ssl_ecdh_curve X25519:prime256v1:secp384r1; ssl_prefer_server_ciphers off; ssl_certificate /etc/nginx/ssl/ip.crt; ssl_certificate_key /etc/nginx/ssl/ip.key; } ``` Test the configuration and reload NGINX: ```bash sudo nginx -t sudo nginx -s reload ``` After this is configured, `https://192.0.2.2/` returns a 403 page. Check the certificate details and the `Subject Alt Names` field should show `IP Address`: [![IP Certificate Works](https://macdn.net/2025/12/17/image_sK4sr.png)](https://macdn.net/2025/12/17/image_sK4sr.png)