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