How to do auto-renewal of TLS certificates with certbot?
Asked Answered
C

2

10

I have an application with Nginx docker container for which the TLS certs are generated manually using the following command in the host (with Ubuntu OS) where application is deployed:

certbot certonly --manual --manual-public-ip-logging-ok --preferred-challenges dns -d my.app.com

When the certificates gets expired, i have to renew them.

But i can't use the following certbot renew command for this purpose as it will give an error:

$ sudo certbot renew

Failed to renew certificate my.app.com with error: The manual plugin is not working; there may be problems with your existing configuration.
The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.')

So, what I'm doing now is to create the certs once again (using the same certbot certonly command that was used previously) instead of renewing them.

How can I fix the error with certbot renew command?

How can I automate this setup?

Coagulate answered 15/3, 2021 at 12:51 Comment(0)
O
23

Here is my setup. It involves the LE secrets living in a docker volume that is shared between nginx and certbot, and nginx proxying the renewal requests to certbot, so you do not have to stop nginx while certbot does its validation.

nginx setup

proxy LE verififcation to certbot backend

Requests on port 80 to letsencrypt validation are forwarded to certbot, anything else gets redirecte to https. (In case you are wondering why I define the proxy pass backend as a variable, see this SO answer)

  server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    location /.well-known/acme-challenge {
      resolver 127.0.0.11 valid=30s;
      set $upstream letsencrypt;
      proxy_pass http://$upstream:80;
      proxy_set_header Host            $host;
      proxy_set_header X-Forwarded-For $remote_addr;
      proxy_set_header X-Forwarded-Proto https;
    }

    location / {
      return 301 https://$host$request_uri;
    }
  }

SSL setup

pretty much standard stuff here:

  server {
    listen 443 ssl;
    server_name ${DOMAINNAME};

    ssl_certificate /etc/letsencrypt/live/${DOMAINNAME}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${DOMAINNAME}/privkey.pem;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1.2;
    ssl_ciphers 'EECDH+AESGCM: EDH+AESGCM:AES256+EECDH:AES256+EDH';
    ssl_prefer_server_ciphers on;

    ssl_session_cache shared:SSL:10m;
    ssl_dhparam dhparam.pem;

    ... your lcoation block goes here ...

}

docker-compose magic

certbot

Have a special "docker-compose-LE.yml" to single run certbot:

version: '3.4'

services:

  letsencrypt:
    image: certbot/certbot:latest
    command: sh -c "certbot certonly --standalone -d ${DOMAINNAME} --text --agree-tos --email [email protected] --server https://acme-v02.api.letsencrypt.org/directory --rsa-key-size 4096 --verbose --keep-until-expiring --preferred-challenges=http"
    entrypoint: ""
    volumes:
      - "letsencrypt:/etc/letsencrypt"
    environment:
      - TERM=xterm

volumes:
  letsencrypt:
    name: letsencrypt_keys

By running "docker-compose -f docker-compose-LE.yml up" you will create and validate a certificate. You can use the same command to renew the certificate, certbot is that smart. You may run this command as often as you like (daily), because it will only renew your certificate when it is about to expire.

See "caveat" below before running this command the first time.

nginx

in docker-compose.yml mount the certificates from a volume. That volume has already been created by letsencrypt, so declare it as external.

services:
  nginx:
    image: nginx:1.18
    restart: always
    volumes:
      - letsencrypt:/etc/letsencrypt:ro

volumes:
  letsencrypt:
    external:
      name: letsencrypt_keys

caveat

This method causes a chicken-egg-problem when creating the certifivate the first time: Without a cert file nginx won't start and can't proxy the LE validation. No nginx means no certificate, and no certificate means no nginx.

To get around this you have to do the very first call of certbot without nginx and using certbots internal http server exposed. So the first time you run certbot add these lines to docker-compose-LE.yml:

  letsencrypt:
    ports:
      - "80:80"

cert renewal

Simply run these two command in a daily cronjob:

docker-compose -f docker-compose-LE.yml up

Will check the certificate and start renewal process once it is due. The now running nginx will proxy the certification validation to certbot.

docker-compose exec nginx nginx -s reload

Once the certificate is updated inplace inside the docker volume certbot and nginx are sharing, simply send a SIGHUP to nginx so it reloads the cert files without interrupting service.

Osteoid answered 15/3, 2021 at 13:28 Comment(1)
This is genius. The part it took me a little while to understand was that resolver 127.0.0.11 is allowing nginx to find the letsencrypt container even though its a separate docker-compose. And it will be able to transfer to letsencrypt port 80 even though we aren't exposing LE:80 externally after first run. So for the brief period letsencrypt is up during periodic renewal phase, nginx redirect will pass to letsencrypt during the challenge period.Gynous
P
1

I needed to use an 'external' network to allow the containers from the two docker-compose files to communicate. Thanks for this!

version: '3.4'
services:
  letsencrypt:
    container_name: "letsencrypt"
      #ports:
        #- "80:80"
    image: certbot/certbot:latest
    command: sh -c "certbot certonly --standalone --rsa-key-size 4096 --agree-tos --preferred-challenges http -d ${DOMAINNAME} -m ${CONTACT_EMAIL} -n"
    entrypoint: ""
    volumes:
      - "letsencrypt:/etc/letsencrypt"
    networks:
      - web_nw
networks:
  web_nw:
    external: true
volumes:
  letsencrypt:
    name: letsencrypt
Puttergill answered 9/12, 2021 at 19:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.