تكوين حزمة Nginx / LetsEncrypt في Docker Swarm

هناك عدد غير قليل من المقالات حول كيفية رفع حاوية Nginx وتكوين التجديد التلقائي لشهادات LetsEncrypt لها. سيصف هذا مخططًا غير قياسي إلى حد ما. يسلط الضوء:


  1. يتم نشر Nginx كخدمة في Docker Swarm ، وليس كحاوية قائمة بذاتها ؛
  2. للتحقق ، يتم استخدام نظام DNS-01 وليس HTTP-01 الأكثر شيوعًا ؛
  3. بالنسبة لموفر DNS GoDaddy ، لا يوجد حاليًا مكون إضافي لـ DNS لـ certbot ، ولكن هناك واجهة برمجة تطبيقات لإدارة سجلات المجال.

ما هو DNS-01 ولماذا هو مطلوب


عند طلب شهادة من LetsEncrypt ، يجب التأكد من أن الشخص الذي يطلبها لديه حقوق المجال المقابل. لهذا ، يستخدم LetsEncrypt الشيكات. يُطلق على الفحص الأكثر شيوعًا HTTP-01. وهو يتألف من حقيقة أن العميل يُصدر أولاً رمزًا مميزًا خاصًا ، ثم يقوم خادم LetsEncrypt بتقديم طلب للعنوان http://<DOMAIN>/.well-known/acme-challenge/<TOKEN>ويتحقق من أن الاستجابة تحتوي على نفس المفتاح المجزأ + المميز لنفس العميل الذي تمت كتابة الرمز المميز إليه. ولكن هناك نقطتان:


  • يتم تنفيذ الطلب دائمًا كما هو موضح أعلاه. لذلك ، يجب أن يكون لديك منفذ 80 مفتوحًا ، ويمكن الوصول دائمًا إلى المسار المحدد من الخارج دون أي مصادقة ؛
  • شهادات أحرف البدل غير معتمدة. على الرغم من أنه يمكنك كتابة شهادة واحدة لعدة نطاقات فرعية في آن واحد (في هذه الحالة ، سيقوم LetsEncrypt بتقديم طلبات لكل نطاق فرعي).

يتيح التحقق من DNS-01 ، أولاً ، الاستغناء عن 80 منفذًا مفتوحًا ، وثانيًا ، استخدام شهادات Wildcard. ولكن لهذا ، تحتاج إلى نشر الرمز المميز المستلم من certbot كسجل DNS _acme-challenge.<YOUR_DOMAIN>بنوع TXT. لا يعد القيام بذلك يدويًا مشكلة ، ولكن الأتمتة تتطلب دعمًا من الموفر. يوفر جميع موفري الخدمات الرئيسيين تقريبًا واجهات برمجة التطبيقات لإدارة سجلات نظام أسماء النطاقات ، وبالنسبة للبعض هناك مكونات إضافية لنظام أسماء النطاقات لـ certbot التي تنفذ اتصالات العميل بواجهات برمجة التطبيقات هذه. ولكن لا يوجد مكون إضافي لموفر GoDaddy ، على الرغم من وجود API.


لماذا سرب دوكر؟


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