Skip to content

Generating SSL Certificates ​

Passkeys, secure session cookies and several browser features only work over HTTPS. The browser connects to the Panel and, unless a node uses Wings Proxy Mode, to each Wings node directly, so each of them needs a certificate that is valid for its hostname. This guide walks you through getting one from Let's Encrypt, which issues certificates for free, using whichever ACME client you prefer. Certificates from other sources are covered at the end.

You need this before you:

What You End Up With ​

Whichever method you pick, the result is a pair of files:

FileWhat it isWho needs it
fullchain.pemYour certificate plus the intermediate chain browsers need to verify itThe cert side of every configuration
privkey.pemThe private key. Never share it or commit it anywhereThe key side of every configuration

Certbot stores them under /etc/letsencrypt/live/<domain>/; acme.sh copies them wherever you tell it. Let's Encrypt certificates are valid for 90 days, so the renewal step at the end of each method is not optional. Both tools set up automatic renewal for you; the guide shows how to check that it actually works.

Before You Begin ​

  • Decide which hostnames you need. One for the Panel (panel.example.com) and one per Wings node (node1.example.com). A single certificate can cover several names, and a wildcard certificate (*.example.com) covers every node at once, though wildcards need the DNS challenge.
  • Point the hostnames at the right machines. Each hostname needs an A record (and AAAA for IPv6) that resolves to the public IP of the machine that will present the certificate. Let's Encrypt looks the name up during issuance, so this has to be in place first.
  • Pick a challenge type. Let's Encrypt has to verify you control the domain. The HTTP challenge needs port 80 reachable from the internet on that machine. The DNS challenge needs API access to your DNS provider instead and works anywhere, including behind NAT.

Every command below is run on the machine that will use the certificate: the Panel host for the Panel's certificate, each node for its own.

The most common method. Use it when port 80 on the machine is reachable from the internet.

1. Install certbot ​

Commands below are for Debian-based distributions using APT. For other systems, see the official certbot website.

bash
sudo apt update
sudo apt install -y certbot
# Only if you use Nginx
sudo apt install -y python3-certbot-nginx
# Only if you use Apache
sudo apt install -y python3-certbot-apache

The Nginx and Apache plugins let certbot answer the challenge through your running webserver, so nothing has to be stopped during issuance or renewal. If nothing listens on port 80 yet (a fresh Wings node, for instance), certbot can run its own temporary webserver instead.

2. Generate the certificate ​

Replace example.com with the hostname you're issuing a certificate for. To cover multiple hostnames with one certificate, repeat the -d flag (e.g. -d panel.example.com -d node1.example.com).

bash
# If Nginx is installed and running on this machine
sudo certbot certonly --nginx -d example.com
# If Apache is installed and running on this machine
sudo certbot certonly --apache -d example.com
# Standalone: nothing else listens on port 80 on this machine
# (typical for a Wings node). Stop nginx/apache first if one is running.
sudo certbot certonly --standalone -d example.com

You'll be prompted for an email address, which Let's Encrypt uses for expiry notices, then certbot issues the certificate. The files end up in /etc/letsencrypt/live/example.com/:

bash
sudo ls -l /etc/letsencrypt/live/example.com/

Certbot's live directory contains symlinks into /etc/letsencrypt/archive/; the links are updated on every renewal so configurations that point at live/ never need to change.

3. Renewal ​

Certbot installs a systemd timer (or cron job) that checks twice a day and renews any certificate within 30 days of expiry. If you used the --nginx or --apache plugin, that's all you need: the plugin reloads your webserver as part of the renewal.

Wings re-reads its certificate files from disk once a day, so a renewed certificate is picked up on its own within 24 hours. If you would rather have Wings switch immediately, or you used --standalone and want a webserver reloaded, add a deploy hook:

bash
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh
bash
#!/bin/bash
systemctl restart wings 2>/dev/null
bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh

Certbot runs every script in renewal-hooks/deploy/ after a successful renewal. Test the whole flow without waiting for the real expiry:

bash
sudo certbot renew --dry-run

If the dry run passes, renewal is working. Check the timer is enabled as well:

bash
systemctl list-timers certbot.timer

Troubleshooting ​

SymptomFix
Browser shows "Insecure Connection" or another SSL/TLS errorAlmost always means the certificate has expired. If certbot renew fails with something like Error: Attempting to renew cert (domain) from /etc/letsencrypt/renew/domain.conf produced an unexpected error, it's usually because port 80 is already in use; using the --nginx / --apache plugin flags (as above) avoids this. Otherwise, stop the webserver, renew, then start it again: sudo systemctl stop nginx, sudo certbot renew, sudo systemctl start nginx.
Wings doesn't pick up the renewed certificate within a dayRestart it manually: sudo systemctl restart wings.

Which Method Should I Use? ​

SituationRecommended method
Public webserver, port 80 openCertbot (HTTP Challenge)
Internal/NAT'd node, using a provider with a certbot DNS pluginCertbot (DNS Challenge)
One wildcard certificate for every nodeCertbot (DNS Challenge) or lego
Want a lighter tool without Pythonacme.sh (shell script) or lego (single binary)
DNS provider without a certbot pluginlego, which supports far more providers, or acme.sh
Using Caddy, Traefik or Nginx Proxy Manager as your reverse proxyNone of the above. The proxy issues and renews its own certificate; see below.
Domain proxied through CloudflareLet's Encrypt still works. A Cloudflare Origin certificate is an alternative that never needs renewing.
Panel and nodes only reachable on your LANA private CA. Let's Encrypt cannot verify a name that does not resolve publicly.

Other Sources of Certificates ​

Let's Encrypt is the default because it is free and renews itself, but any certificate that browsers trust for the hostname works. Whatever the source, the result is the same pair of files from What You End Up With, and the rest of the docs applies unchanged.

Certificates Issued by the Reverse Proxy ​

Caddy, Traefik and Nginx Proxy Manager all talk to Let's Encrypt on their own for every hostname they serve. If the Panel and Wings both sit behind such a proxy, you never touch a certificate file: point the proxy at the hostname and it issues, installs and renews the certificate itself. The reverse proxy guide covers the configuration for each.

The catch is that these proxies keep the certificate in their own storage, in a layout that changes between versions, so reusing it for Wings' built-in SSL is fragile. Put Wings behind the proxy as well, or issue a separate certificate for the node with one of the methods above.

Cloudflare Origin CA ​

If your domain is proxied through Cloudflare (orange cloud), Cloudflare can issue an Origin certificate for it: a free certificate, valid for up to 15 years, that only Cloudflare's edge trusts. Browsers never see it, because they talk to Cloudflare, and Cloudflare presents its own publicly trusted certificate to them.

In the Cloudflare dashboard, open SSL/TLS → Origin Server → Create Certificate, keep the defaults, and copy the certificate into fullchain.pem and the private key into privkey.pem. The key is shown only once. Then set the zone's SSL/TLS encryption mode to Full (strict).

This only works for hostnames whose traffic actually passes through Cloudflare. That is fine for the Panel, and for a Wings node whose HTTP hostname is proxied too, since browsers reach Wings through Cloudflare in that case. It does not work for a node that browsers connect to directly, because they will not trust the certificate. SFTP and game ports do not use the certificate at all, so they are unaffected either way.

A Certificate You Bought ​

A certificate from a commercial CA arrives as a certificate file, one or more intermediate certificates, and the private key you generated with the request. Combine the certificate and the intermediates, in that order, into the full chain:

bash
cat your-domain.crt intermediate.crt > /etc/ssl/certs/example.com/fullchain.pem
cp your-domain.key /etc/ssl/certs/example.com/privkey.pem
chmod 600 /etc/ssl/certs/example.com/privkey.pem

If you were given a single .pfx or .p12 bundle instead, split it with OpenSSL:

bash
openssl pkcs12 -in certificate.pfx -nokeys -out fullchain.pem
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out privkey.pem

Nothing renews these for you. Put the expiry date in your calendar, and repeat the steps with the new files when the CA reissues the certificate.

Private CA for LAN-only Setups ​

When neither the Panel nor the nodes are reachable from the internet, Let's Encrypt cannot verify the hostname. You can still get a certificate browsers trust by running your own certificate authority and installing its root on every device that uses the Panel. Passkeys and everything else that needs HTTPS then works on the LAN.

mkcert makes this a two-command job. Run it on your workstation, where it creates a root CA and adds it to the system and browser trust stores, then issue a certificate for each hostname or IP:

bash
mkcert -install
mkcert panel.lan 192.168.1.10

It writes panel.lan+1.pem (the certificate, use it as fullchain.pem) and panel.lan+1-key.pem (the key, use it as privkey.pem). Copy them to the Panel host or node and reference them as usual. Every other device that opens the Panel needs the root from mkcert -CAROOT imported into its trust store, otherwise it shows a certificate warning. mkcert certificates are valid for about two years; issue a new one before then.

Using the Certificate with Wings in Docker ​

The Wings compose file mounts /etc/ssl/certs/wings/ from the host into the container, and no other certificate directory. Certbot's /etc/letsencrypt/live/ directory is not visible inside the container, and mounting only live/ does not work either, because the files in it are symlinks into archive/.

Copy the certificate into the mounted directory instead, and let a deploy hook keep the copy fresh:

bash
sudo mkdir -p /etc/ssl/certs/wings
sudo nano /etc/letsencrypt/renewal-hooks/deploy/wings-certs.sh
bash
#!/bin/bash
cp -L /etc/letsencrypt/live/example.com/fullchain.pem /etc/ssl/certs/wings/fullchain.pem
cp -L /etc/letsencrypt/live/example.com/privkey.pem   /etc/ssl/certs/wings/privkey.pem
chmod 600 /etc/ssl/certs/wings/privkey.pem
bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/wings-certs.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/wings-certs.sh

The last line runs the hook once by hand so the files exist right away; certbot runs it after every renewal from then on. Point Wings at the copies in config/config.yml:

yaml
api:
  ssl:
    enabled: true
    cert: /etc/ssl/certs/wings/fullchain.pem
    key: /etc/ssl/certs/wings/privkey.pem

Then restart the container from the Wings compose directory:

bash
docker compose restart wings

Wings re-reads the files daily, so renewed certificates are picked up without another restart.