Konfigurieren eines Nginx / LetsEncrypt-Bundles in Docker Swarm

Es gibt einige Artikel zum Erhöhen des Nginx-Containers und zum Konfigurieren der automatischen Erneuerung von LetsEncrypt-Zertifikaten. Dies wird ein eher nicht standardmäßiges Schema beschreiben. Highlights:


  1. Nginx wird als Dienst in Docker Swarm und nicht als eigenständiger Container bereitgestellt.
  2. Das DNS-01-Schema wird zur Überprüfung verwendet, nicht jedoch das viel beliebtere HTTP-01.
  3. Für den DNS-Anbieter GoDaddy gibt es derzeit kein DNS-Plugin für certbot, aber eine API zum Verwalten von Domäneneinträgen.

Was ist DNS-01 und warum wird es benötigt?


Wenn ein Zertifikat von LetsEncrypt angefordert wird, muss sichergestellt werden, dass derjenige, der es anfordert, Rechte für die entsprechende Domäne hat. Zu diesem Zweck verwendet LetsEncrypt Überprüfungen. Die beliebteste Prüfung heißt HTTP-01. Es besteht darin, dass dem Client zuerst ein spezielles Token ausgestellt wird und der LetsEncrypt-Server dann eine Anfrage nach der Adresse stellt http://<DOMAIN>/.well-known/acme-challenge/<TOKEN>und überprüft, ob die Antwort dasselbe Token + denselben Hash-Schlüssel desselben Clients enthält, auf den das Token geschrieben wurde. Aber es gibt 2 Punkte:


  • Die Anfrage wird immer genau wie oben beschrieben ausgeführt. Daher muss Port 80 geöffnet sein, und auf den angegebenen Pfad kann von außen immer ohne Authentifizierung zugegriffen werden.
  • Platzhalterzertifikate werden nicht unterstützt. Obwohl Sie ein Zertifikat für mehrere Subdomains gleichzeitig schreiben können (in diesem Fall stellt LetsEncrypt Anforderungen für jede der 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