Peasant's Guide to SSL, or You Don't Need PunchSalad
A practical guide to issuing a free SSL certificate with Certbot, completing the ACME HTTP challenge through .well-known, and setting up renewal without a paid SSL service.

Most hosting panels now have a button called SSL, Let’s Encrypt, or AutoSSL. Press it. If Cloudflare, cPanel, Plesk, your host, or somebody else already renews the certificate automatically, close this article and go outside. You have won.
This guide is for the other kind of hosting. The kind that sells SSL as an extra, gives you a file manager from 2009, and somehow convinces you that you need PunchSalad to keep the browser padlock alive.
You don’t. PunchSalad is doing the same ACME challenge that Certbot can do for free.
Very briefly, what is SSL?
SSL is the old name everybody still uses for TLS, the encryption behind HTTPS.
When a browser connects, your server flashes its certificate like an ID at the door. The browser checks three things: a Certificate Authority it trusts signed it, the name on it matches your domain, and it hasn’t expired. If all three pass, the two agree on temporary keys and carry on with plain old HTTP inside an encrypted tunnel.
The certificate does two useful things:
- proves the server answering for
example.comcontrolsexample.com; - encrypts the traffic between the visitor and that server.
It does not prove that the owner is nice, that the site is secure, or that the WordPress installation does not contain a plugin last updated during the Obama administration.
How the ACME challenge works
Let’s Encrypt gives certificates away, but first it needs proof that the domain is yours. ACME is the protocol used for this conversation.
In grog terms:
- Certbot asks Let’s Encrypt for a certificate for
example.com. - Let’s Encrypt says: put this weird text in a file with this weird name.
- You place it at
http://example.com/.well-known/acme-challenge/WEIRD-NAME. - Let’s Encrypt downloads the file and sees the expected text.
- Only somebody controlling the website could have done that, so it issues the certificate.
Certbot hands you a file:
I only ever look in /.well-known/acme-challenge/. Every time.
This is called the HTTP-01 challenge. Port 80 must be publicly reachable. The .well-known directory is not magic; it is just an agreed-upon cupboard where certificate robots look for proof.
First try the boring automatic option
If you have root access and use Nginx or Apache, Certbot can perform the challenge, edit the web-server config, and arrange renewal for you:
sudo certbot --nginx
# orsudo certbot --apacheThat is the correct answer for most servers. Test renewal once and leave it alone:
sudo certbot renew --dry-runMost Certbot installations include a systemd timer or cron job which runs certbot renew. You do not renew the certificate by remembering it every few months. Computers are quite good at calendars.
Use webroot when you want to control the config
Sometimes you do not want Certbot editing Nginx, or the web-server plugin cannot understand your setup. The webroot plugin proves domain control by writing the challenge file into your existing website directory without stopping the server.
Suppose the site is served from:
/var/www/example.com/publicMake sure Nginx serves the challenge directory over plain HTTP:
server { listen 80; server_name example.com www.example.com; root /var/www/example.com/public;
location ^~ /.well-known/acme-challenge/ { default_type text/plain; try_files $uri =404; }
location / { return 301 https://$host$request_uri; }}Before involving certificates, test the cupboard:
sudo mkdir -p /var/www/example.com/public/.well-known/acme-challengeecho works | sudo tee /var/www/example.com/public/.well-known/acme-challenge/testcurl http://example.com/.well-known/acme-challenge/testsudo rm /var/www/example.com/public/.well-known/acme-challenge/testIf curl prints works, request the certificate:
sudo certbot certonly --webroot \ -w /var/www/example.com/public \ -d example.com \ -d www.example.comCertbot will store it under:
/etc/letsencrypt/live/example.com/fullchain.pem/etc/letsencrypt/live/example.com/privkey.pemPoint the HTTPS server at those files:
server { listen 443 ssl; server_name example.com www.example.com; root /var/www/example.com/public;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
# The rest of your site config goes here.}Then check and reload Nginx:
sudo nginx -tsudo systemctl reload nginxsudo certbot renew --dry-runRenewal remembers that you used webroot, places a fresh challenge in the same directory, receives a new certificate, and replaces the files behind those paths. Nginx still needs a reload to notice the replacement. Add a persistent deploy hook which runs only after a successful renewal:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploysudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx >/dev/null <<'EOF'#!/bin/shsystemctl reload nginxEOFsudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
sudo certbot renew --dry-run --run-deploy-hooksdisk
- live/example.com/
- fullchain.pem
- privkey.pem
- archive/example.com/
- privkey1.pem
- fullchain1.pem
- privkey2.pem
- fullchain2.pem
expires Dec 2, 2026
nginx memory
expires Dec 2, 2026
Over 30 days left. Certbot checks, shrugs, sleeps.
The truly manual way for terrible hosting
Now we arrive at the PunchSalad use case: the website is online, but you have no SSH access and no Let’s Encrypt button. You can still run Certbot on another Linux machine and upload the proof through FTP or the hosting file manager.
No other Linux machine either? I built an SSL certificate tool that runs this same conversation in the browser: it gives you the files (or the DNS records), asks Let’s Encrypt to check them, and hands back the certificate, key and CA bundle. The steps below are what it does for you.
Run:
sudo certbot certonly --manual \ --preferred-challenges=http \ --agree-tos \ -m you@example.com \ -d example.com \ -d www.example.comCertbot will show a file name, a file value, and a URL resembling:
http://example.com/.well-known/acme-challenge/a1b2c3-weird-tokenDo not press Enter yet. In your hosting file manager:
- Open the domain’s web root, usually
public_html. - Create
.well-knowninside it. Dot directories may be hidden by default. - Create
acme-challengeinside.well-known. - Create the exact file Certbot requested, with the exact contents it printed.
- Open the URL in a browser or with
curland check that it returns only that value. - Return to Certbot and press Enter.
If you requested both the bare domain and www, Certbot may ask for another challenge. Repeat the process. Each hostname must point at this hosting account and serve the correct file.
After validation, Certbot gives you these files:
| File | What it is |
|---|---|
cert.pem | Your domain certificate |
chain.pem | The intermediate Certificate Authority certificates |
fullchain.pem | cert.pem and chain.pem together |
privkey.pem | Your private key; do not share it |
Your hosting panel may ask for Certificate, Private Key, and CA Bundle separately. Give it cert.pem, privkey.pem, and chain.pem. If it asks for one certificate chain file, use fullchain.pem.
live/example.com/
-----BEGIN CERTIFICATE-----
⋮ example.com
-----END CERTIFICATE----------BEGIN CERTIFICATE-----
⋮ Let's Encrypt
-----END CERTIFICATE----------BEGIN CERTIFICATE----- ⋮ example.com -----END CERTIFICATE----------BEGIN CERTIFICATE----- ⋮ Let's Encrypt -----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
⋮ only on your machine
-----END PRIVATE KEY------ Chrome
- Firefox
- curl, apps
curl: (60) SSL certificate problem: unable to get local issuer certificate
The classic: no CA Bundle. curl notices.
The catch with manual mode
Certificates obtained with --manual do not renew automatically unless you write authentication hooks which can upload the challenge for you. Repeat the same command and reinstall the new certificate well before the current one expires.
This is not a Certbot failure. Automatic renewal requires some automatic way to prove control and install the result. A human dragging a secret file into public_html is, regrettably, not an API.
Your connection is not private
NET::ERR_CERT_DATE_INVALID
- certbot certonly --manual
- upload the token to public_html
- paste the files into the panel
bounced 0 · at ~140 visitors a day
Renew SSL for example.com
The timer fails from day 60. You’re the backup.
If the host gives you SSH access later, switch to --webroot. If it adds native Let’s Encrypt support, use that. If it continues charging yearly for the privilege of pasting free certificates into a form, consider changing hosts.
When it fails
Almost every HTTP-01 failure is one of these:
- the domain points to a different server;
- port 80 is blocked;
.well-knownwas created outside the actual web root;- WordPress, Nginx, or a security rule intercepts the challenge URL;
- the
wwwhostname was requested but has no DNS record; - an IPv6
AAAArecord points somewhere different from the IPv4Arecord; - the file manager helpfully added
.txtto the token name.
Do not keep rerunning Certbot and hoping the internet changes its mind. Open the exact challenge URL. Until it returns the exact value, Let’s Encrypt cannot validate it either.
Useful links
- Certbot: getting certificates and choosing plugins
- Let’s Encrypt challenge types
- Why Let’s Encrypt recommends keeping port 80 open
Cover: Pieter Bruegel the Elder, The Harvesters, 1565, The Metropolitan Museum of Art, public domain.
Comments
Comments live in GitHub Discussions (via giscus), so you need a GitHub account to join in.