Handling TLS certs on Fedora IoT with acme.sh

If you're running Fedora IoT (or Fedora CoreOS outside of Kubenetes) then you'll need to run your ACME client within a container if you want free auto-updating TLS certificates.

One option for doing this is acme.sh. It wasn't obvious to me how this was expected to work, hence these notes. My primary resources were:

  1. acme.sh wiki page on using it within Docker
  2. Podman docs

Note that I perform all of the following as the root user (see Shortcomings of Rootless Podman), because one of my uses for the certs is nginx bound to port 443.

Firstly we'll need to create a volume that acme.sh can write to and any certificate-consuming applications can read from:

  podman volume create acme-certs

…then we can run acme.sh and point it at our volume (the :z option when specifying the volume applies an SELinux label denoting shared content):

    # Only needed for the first run:
  podman run --rm -it -v acme-certs:/acme.sh:z \
    docker.io/neilpang/acme.sh --register-account -m inbox@example.com

Once the initial registration is done you should be able to issue certs:

  podman run --rm -it -v acme-certs:/acme.sh:z \
    -e GANDI_LIVEDNS_KEY=api-key-gibberish \
    docker.io/neilpang/acme.sh \
    --issue --dns dns_gandi_livedns -d host.example.com

In this example I'm using Gandi LiveDNS for verification, the acme.sh DNS API supports many providers so probably yours is in there.

Assuming the certificate issue operation succeeds you will see output like this:

  Your cert is in: /acme.sh/host.example.com/host.example.com.cer
  Your cert key is in: /acme.sh/host.example.com/host.example.com.key
  The intermediate CA cert is in: /acme.sh/host.example.com/ca.cer
  And the full chain certs is there: /acme.sh/host.example.com/fullchain.cer

Take note of the names/paths as you'll need them when configuring whichever consuming applications you'll use.

Now all that's left is to automate certificate renewal. I've done this with a systemd timer because everything in Fedora IoT is unit files already, but it works just as well via cron.

  cat <<EOF > /etc/systemd/system/acme-certs.service
  [Unit]
  Description=Renew TLS certificates via acme.sh
  After=network.target

  [Service]
  Type=simple
  ExecStart=podman run --rm -it -v acme-certs:/acme.sh:z \
      -e GANDI_LIVEDNS_KEY=api-key-gibberish \
      docker.io/neilpang/acme.sh --cron
  EOF

  cat <<EOF > /etc/systemd/system/acme-certs.timer
  [Unit]
  Description=Check and renew TLS certificates daily

  [Timer]
  OnCalendar=daily
  Persistent=true

  [Install]
  WantedBy=timers.target
  EOF

  systemctl daemon-reload

  # test manually
  systemctl start acme-certs.service
  journalctl --unit acme-certs.service

  # enable the timer
  systemctl enable acme-certs.timer
  systemctl start acme-certs.timer

  systemctl status acme-certs.timer acme-certs.service