Skip to content
Alex Kovalev

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.

Guide5min read
Pieter Bruegel's The Harvesters: peasants cutting wheat and eating lunch under a pear tree.
Pieter Bruegel's The Harvesters: peasants cutting wheat and eating lunch under a pear tree.

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.com controls example.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:

  1. Certbot asks Let’s Encrypt for a certificate for example.com.
  2. Let’s Encrypt says: put this weird text in a file with this weird name.
  3. You place it at http://example.com/.well-known/acme-challenge/WEIRD-NAME.
  4. Let’s Encrypt downloads the file and sees the expected text.
  5. Only somebody controlling the website could have done that, so it issues the certificate.
Not securehttp://example.com

Certbot hands you a file:

I only ever look in /.well-known/acme-challenge/. Every time.

no certificate yetexample.com · Let's Encrypt · valid 90 days
Drag the file into a folder, or click it and then a folder. The robot does the rest.

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:

Terminal window
sudo certbot --nginx
# or
sudo certbot --apache

That is the correct answer for most servers. Test renewal once and leave it alone:

Terminal window
sudo certbot renew --dry-run

Most 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/public

Make 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:

Terminal window
sudo mkdir -p /var/www/example.com/public/.well-known/acme-challenge
echo works | sudo tee /var/www/example.com/public/.well-known/acme-challenge/test
curl http://example.com/.well-known/acme-challenge/test
sudo rm /var/www/example.com/public/.well-known/acme-challenge/test

If curl prints works, request the certificate:

Terminal window
sudo certbot certonly --webroot \
-w /var/www/example.com/public \
-d example.com \
-d www.example.com

Certbot will store it under:

/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem

Point 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:

Terminal window
sudo nginx -t
sudo systemctl reload nginx
sudo certbot renew --dry-run

Renewal 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/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx >/dev/null <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
sudo certbot renew --dry-run --run-deploy-hooks
https://example.comexpires Dec 2, 2026NET::ERR_CERT_DATE_INVALID

disk

  • live/example.com/
  • fullchain.pem
  • privkey.pem
  • archive/example.com/
  • privkey1.pem
  • fullchain1.pem
  • privkey2.pem
  • fullchain2.pem

expires Dec 2, 2026

nginx memory

cert #1
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:

Terminal window
sudo certbot certonly --manual \
--preferred-challenges=http \
--agree-tos \
-m you@example.com \
-d example.com \
-d www.example.com

Certbot will show a file name, a file value, and a URL resembling:

http://example.com/.well-known/acme-challenge/a1b2c3-weird-token

Do not press Enter yet. In your hosting file manager:

  1. Open the domain’s web root, usually public_html.
  2. Create .well-known inside it. Dot directories may be hidden by default.
  3. Create acme-challenge inside .well-known.
  4. Create the exact file Certbot requested, with the exact contents it printed.
  5. Open the URL in a browser or with curl and check that it returns only that value.
  6. 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:

FileWhat it is
cert.pemYour domain certificate
chain.pemThe intermediate Certificate Authority certificates
fullchain.pemcert.pem and chain.pem together
privkey.pemYour 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/

public
-----BEGIN CERTIFICATE-----
⋮ example.com
-----END CERTIFICATE-----
public
-----BEGIN CERTIFICATE-----
⋮ Let's Encrypt
-----END CERTIFICATE-----
public
-----BEGIN CERTIFICATE-----
⋮ example.com
-----END CERTIFICATE----------BEGIN CERTIFICATE-----
⋮ Let's Encrypt
-----END CERTIFICATE-----
secret
-----BEGIN PRIVATE KEY-----
⋮ only on your machine
-----END PRIVATE KEY-----

HostPanel SSL/TLS
panel asks for

  • 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.

https://example.comexpires Dec 2, 2026

Your connection is not private

NET::ERR_CERT_DATE_INVALID

  1. certbot certonly --manual
  2. upload the token to public_html
  3. paste the files into the panel

bounced 0 · at ~140 visitors a day

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-known was created outside the actual web root;
  • WordPress, Nginx, or a security rule intercepts the challenge URL;
  • the www hostname was requested but has no DNS record;
  • an IPv6 AAAA record points somewhere different from the IPv4 A record;
  • the file manager helpfully added .txt to 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.


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.

I’ll chirp when there’s a new post

One email per new post, usually on a Friday. Nothing else, and you can unsubscribe anytime.

Prefer a feed reader? There’s an RSS feed.

© Alex Kovalev. All rights reserved. · Privacy Policy