Configuración de paquetes Nginx / LetsEncrypt en Docker Swarm

Hay bastantes artículos sobre cómo elevar el contenedor Nginx y configurar la renovación automática de los certificados LetsEncrypt para él. Esto describirá un esquema más bien no estándar. Destacar:


  1. Nginx se implementa como un servicio en Docker Swarm, y no como un contenedor independiente;
  2. Para la verificación, se utiliza el esquema DNS-01, y no el mucho más popular HTTP-01;
  3. Para el proveedor de DNS GoDaddy, actualmente no hay un complemento de DNS para certbot, pero hay una API para administrar registros de dominio.

¿Qué es DNS-01 y por qué es necesario?


Cuando se solicita un certificado de LetsEncrypt, debe asegurarse de que quien lo solicita tenga derechos sobre el dominio correspondiente. Para esto, LetsEncrypt usa cheques. El cheque más popular se llama HTTP-01. Consiste en el hecho de que al cliente se le emite primero un token especial, y luego el servidor LetsEncrypt solicita una dirección http://<DOMAIN>/.well-known/acme-challenge/<TOKEN>y verifica que la respuesta contenga el mismo token + clave hash del mismo cliente en el que se escribió el token. Pero hay 2 puntos:


  • La solicitud siempre se ejecuta exactamente como se describe anteriormente. Por lo tanto, debe tener el puerto 80 abierto, y la ruta especificada siempre es accesible desde el exterior sin ninguna autenticación;
  • Los certificados comodín no son compatibles. Aunque puede escribir un certificado para varios subdominios a la vez (en este caso, LetsEncrypt realizará solicitudes para cada uno de los subdominios).

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