关于如何提高Nginx容器并为其配置LetsEncrypt证书的自动更新的文章很多。这将描述一个相当不标准的方案。强调:
- Nginx是作为服务部署在Docker Swarm中的,而不是作为独立容器部署的;
- 为了进行验证,使用的是DNS-01方案,而不使用更为流行的HTTP-01;
- 对于DNS提供商GoDaddy,当前没有用于certbot的DNS插件,但是有一个用于管理域记录的API。
什么是DNS-01,为什么需要
从LetsEncrypt请求证书时,它需要确保请求证书的人对相应域具有权限。为此,LetsEncrypt使用检查。最受欢迎的检查称为HTTP-01。它包含以下事实:首先向客户端发出特殊令牌,然后LetsEncrypt服务器对地址进行请求,http://<DOMAIN>/.well-known/acme-challenge/<TOKEN>
并检查响应是否包含与向其写入令牌的同一客户端相同的令牌+哈希密钥。但是有两点:
- 该请求始终完全如上所述执行。因此,必须打开端口80,并且始终可以从外部访问指定的路径而无需任何身份验证;
- 不支持通配符证书。尽管您可以一次为多个子域编写一个证书(在这种情况下,LetsEncrypt将为每个子域发出请求)。
首先,检查DNS-01允许不使用开放的80端口,其次,可以使用通配符证书。但是为此,您需要将从certbot接收到的令牌发布为_acme-challenge.<YOUR_DOMAIN>
TXT类型的DNS记录。手动执行此操作不是问题,但是自动化需要提供者的支持。几乎所有主要的提供商都提供用于管理DNS记录的API,并且对于某些人,还有用于实现与这些API的客户端连接的certbot的dns插件。尽管该API确实存在,但是没有GoDaddy提供程序的插件。
为什么选择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 .
- , Swarm' :
$ docker swarm init
- , :
$ docker node ls
SSL-
, LetsEncrypt GoDaddy.
- API DNS-. GoDaddy, https://developer.godaddy.com/ API Key.
Production
. 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
.
( certbot , DNS). curl' API GoDaddy . , .
payload:
$ cat <<EOF > payload.json
[{
"data": "TOKEN_STRING",
"name": "_acme-challenge",
"type": "TXT"
}]
EOF
TOKEN_STRING — , certbot'.
GoDaddy, 1:
$ export GODADDY_KEY=<KEY>
$ export GODADDY_SECRET=<SECRET>
_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
, DNS ( , . , ):
$ dig -t txt _acme-challenge.example.com
...
;; ANSWER SECTION:
_acme-challenge.example.com. 600 IN TXT "TOKEN_STRING"
...
- certbot' Enter. ,
/opt/letsencrypt/live/example.com
, .
Nginx
, nginx.conf
. , SSL, HTTP 200 "It works!
". , .
nginx.conf /opt/nginx/conf:
nginx.confuser 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;
}
}
}
- 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
https://gateway.example.com
, "It works!".- , :
$ docker service update --force nginx
:) LetsEncrypt 90 . LetsEncrypt 60. 2 , . , :
- . , , , nginx — ;
- Docker Swarm , ( : https://github.com/docker/swarmkit/issues/2852);
- , , Nginx reload, restart. reload HUP, ;
- : , nginx , foreground-
while true; do nginx -s reload; sleep 1d; done
.
, — ( , gateway) cron
. — , cron', - GoDaddy, . docker secrets, Swarm-. , (. . 2 Swarm). JaaS o Alex Ellis (https://github.com/alexellis/jaas).
, - , . , - , root', API GoDaddy . cron, docker run , .
jaas:
$ git clone https://github.com/alexellis/jaas
:
$ 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"
/usr/local/bin ( ):
$ sudo cp jaas /usr/local/bin
jaas:
$ jaas run --image alpine:3.8 --env FOO=bar --command "env"
image' alpine, FOO=bar. env, , . , jaas'.
secret' API GoDaddy:
$ printf $GODADDY_KEY | docker secret create godaddy-key -
$ printf $GODADDY_SECRET | docker secret create godaddy-secret -
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 , , .
:
$ chmod +x /opt/godaddy-hooks/authenticate.sh /opt/godaddy-hooks/cleanup.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 --dry-run --no-random-sleep-on-renew"
, . 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