Configuring a Nginx / LetsEncrypt Bundle in Docker Swarm

There are quite a few articles on how to raise the Nginx container and configure auto-renewal of LetsEncrypt certificates for it. This will describe a rather non-standard scheme. Highlights:


  1. Nginx is deployed as a service in Docker Swarm, and not as a standalone container;
  2. For verification, the DNS-01 scheme is used, and not the much more popular HTTP-01;
  3. For the DNS provider GoDaddy, there is currently no DNS plugin for the certbot, but there is an API for managing domain records.

What is DNS-01 and why is it needed


When a certificate is requested from LetsEncrypt, it needs to make sure that the one who requests it has rights to the corresponding domain. For this, LetsEncrypt uses checks. The most popular check is called HTTP-01. It consists in the fact that the client is first issued a special token, and then the LetsEncrypt server makes a request for the address http://<DOMAIN>/.well-known/acme-challenge/<TOKEN>and checks that the response contains the same token + hashed key of the same client to which the token was written out. But there are 2 points:


  • The request is always executed exactly as described above. Therefore, you must have port 80 open, and the specified path is always accessible from the outside without any authentication;
  • Wildcard certificates are not supported. Although you can write one certificate for several subdomains at once (in this case, LetsEncrypt will make requests for each of the subdomains).

DNS-01 , -, 80-, -, Wildcard-. , certbot', DNS- _acme-challenge.<YOUR_DOMAIN> TXT. , . API DNS-, dns-plugin' certbot', API. GoDaddy , API .


Docker Swarm?


Docker Swarm β€” Docker . Kubernetes , . , Docker Swarm , :


  • (stack/service vs container);
  • ;
  • ;
  • secret' ( , Kubernetes).

Swarm.



, example.com, GoDaddy. -gateway . , , . Swarm' , , , . , gateway, . SSL. , nginx. SSL-.


wildcard- , nginx docker swarm, . nginx' . , gateway.example.com, .


Docker Swarm


, Docker Engine .


  1. , Swarm' :

    $ docker swarm init
  2. , :

    $ docker node ls

SSL-


, LetsEncrypt GoDaddy.


  1. API DNS-. GoDaddy, https://developer.godaddy.com/ API Key. Production.
  2. ACME- certbot'. Certbot :


    $ docker run --rm -it --mount type=bind,source=/opt/letsencrypt,target=/etc/letsencrypt certbot/certbot:v1.3.0 --email account@example.com --agree-tos -d *.example.com --manual --preferred-challenges dns certonly

    /opt/letsencrypt β€” , nginx' ;
    account@example.com β€” LetsEncrypt;
    *.example.com β€” , ( , wildcard).


    , email ( ), , IP ( ). certbot , TOKEN_STRING TXT- _acme-challenge.example.com.


  3. ( certbot , DNS). curl' API GoDaddy . , .


    1. payload:


      $ cat <<EOF > payload.json
      [{
        "data": "TOKEN_STRING",
        "name": "_acme-challenge",
        "type": "TXT"
      }]
      EOF

      TOKEN_STRING β€” , certbot'.


    2. GoDaddy, 1:


      $ export GODADDY_KEY=<KEY>
      $ export GODADDY_SECRET=<SECRET>

    3. _acme-challenge ( , URL ):


      $ curl -XPUT -d @payload.json -H "Content-Type: application/json" -H "Authorization: sso-key $GODADDY_KEY:$GODADDY_SECRET" https://api.godaddy.com/v1/domains/example.com/records/TXT/_acme-challenge

    4. , DNS ( , . , ):


      $ dig -t txt _acme-challenge.example.com
      ...
      ;; ANSWER SECTION:
      _acme-challenge.example.com. 600 IN TXT "TOKEN_STRING"
      ...


  4. certbot' Enter. , /opt/letsencrypt/live/example.com , .

Nginx


, nginx.conf. , SSL, HTTP 200 "It works!". , .


  1. nginx.conf /opt/nginx/conf:


    nginx.conf
    user nginx;
    worker_processes 1;
    
    error_log /var/log/nginx/error.log warn;
    pid /var/run/nginx.pid;
    
    events {
      worker_connections 1024;
    }
    
    http {
      include /etc/nginx/mime.types;
      default_type application/octet-stream;
    
      log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    
      access_log /var/log/nginx/access.log main;
    
      sendfile on;
    
      keepalive_timeout 65;
    
      server {
        listen 443 ssl default_server;
        server_name _;
    
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
        location / {
          return 200 'It works!';
          add_header Content-Type text/plain;
        }
      }
    }



  1. nginx:

    $ docker service create --name nginx -p 443:443 --mount type=bind,source=/opt/nginx/conf/nginx.conf,target=/etc/nginx/nginx.conf,ro --mount type=bind,source=/opt/letsencrypt,target=/etc/letsencrypt,ro nginx:1.17.9
  2. https://gateway.example.com , "It works!".
  3. , :

    $ docker service update --force nginx


:) LetsEncrypt 90 . LetsEncrypt 60. 2 , . , :



, β€” ( , gateway) cron. β€” , cron', - GoDaddy, . docker secrets, Swarm-. , (. . 2 Swarm). JaaS o Alex Ellis (https://github.com/alexellis/jaas).


, - , . , - , root', API GoDaddy . cron, docker run , .


  1. jaas:


    $ git clone https://github.com/alexellis/jaas

  2. :


    $ cd jaas
    $ docker run --rm -v "$PWD":/usr/src/jaas -w /usr/src/jaas golang:1.13 bash -c "go get -d -v github.com/alexellis/jaas && go build -v"

  3. /usr/local/bin ( ):


    $ sudo cp jaas /usr/local/bin

  4. jaas:


    $ jaas run --image alpine:3.8 --env FOO=bar --command "env"

    image' alpine, FOO=bar. env, , . , jaas'.


  5. secret' API GoDaddy:


    $ printf $GODADDY_KEY | docker secret create godaddy-key -
    $ printf $GODADDY_SECRET | docker secret create godaddy-secret -

  6. DNS certbot' authenticate- cleanup- (https://certbot.eff.org/docs/using.html#hooks). , docker- certbot. curl', wget' PUT-. python 3.8 requests. authenticate.sh cleanup.sh /opt/godaddy-hooks ( ).


    , certbot dig, , DNS ( , ). 60 ( 45, , ). , DNS-, . , GoDaddy - DNS-. Cleanup- API , , .


  7. :


    $ chmod +x /opt/godaddy-hooks/authenticate.sh /opt/godaddy-hooks/cleanup.sh

  8. :


    $ jaas run --timeout 90s --mount /opt/letsencrypt=/etc/letsencrypt --mount /opt/godaddy-hooks=/opt/hooks -s godaddy-key -s godaddy-secret --image certbot/certbot:v1.3.0 --command "certbot --manual --manual-auth-hook /opt/hooks/authenticate.sh --manual-cleanup-hook /opt/hooks/cleanup.sh renew --dry-run --no-random-sleep-on-renew"

  9. , . cron, , , anacron. ( 2 , 3- 4 ). , certbot renew , 8 . , , , CA . "" --no-random-sleep-on-renew, jaas'. :


    #!/bin/sh
    jaas run --timeout 90s --mount /opt/letsencrypt=/etc/letsencrypt --mount /opt/godaddy-hooks=/opt/hooks -s godaddy-key -s godaddy-secret --image certbot/certbot:v1.3.0 --command "certbot --manual --manual-auth-hook /opt/hooks/authenticate.sh --manual-cleanup-hook /opt/hooks/cleanup.sh renew --no-random-sleep-on-renew"
    docker service update --force nginx


GoDaddy


authenticate.sh
#!/bin/sh
read key < /run/secrets/godaddy-key
read secret < /run/secrets/godaddy-secret

python - <<EOF
import requests
requests.put(
    url = 'https://api.godaddy.com/v1/domains/$CERTBOT_DOMAIN/records/TXT/_acme-challenge',
    json = [{'type': 'TXT', 'name': '_acme-challenge', 'data': '$CERTBOT_VALIDATION'}],
    headers = {'Authorization': 'sso-key $key:$secret'}
)
EOF
sleep 60

cleanup.sh
#!/bin/sh
read key < /run/secrets/godaddy-key
read secret < /run/secrets/godaddy-secret

python - <<EOF
import requests
response = requests.get(
    url = 'https://api.godaddy.com/v1/domains/$CERTBOT_DOMAIN/records',
    headers = {'Authorization': 'sso-key $key:$secret'}
)
requests.put(
    url = 'https://api.godaddy.com/v1/domains/$CERTBOT_DOMAIN/records',
    json = [record for record in response.json() if record['name'] != '_acme-challenge'],
    headers = {'Authorization': 'sso-key $key:$secret'}
)
EOF

All Articles