如何在 AWS EC2 运行 和 NGINX Docker 容器上启用 HTTPS?

How to enable HTTPS on AWS EC2 running an NGINX Docker container?

我在 AWS 上有一个 EC2 实例 运行s Amazon Linux 2.

我在上面安装了Git、docker和docker-compose。完成后,我克隆了我的存储库和 运行 docker-compose up 来启动我的生产环境。我转到 public DNS,它工作正常。

我现在想在网站上启用 HTTPS。

我的项目有一个在 Nginx-alpine 服务器上使用 React to 运行 的前端。后端是 NodeJS 服务器。

这是我的 nginx.conf 文件:

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri /index.html;
    }

    location /api/ {
        proxy_pass http://${PROJECT_NAME}_backend:${NODE_PORT}/;
    }    

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

这是我的 docker-compose.yml 文件:

version: "3.7"
services:
##############################
# Back-End Container
##############################
  backend: # Node-Express backend that acts as an API.
    container_name: ${PROJECT_NAME}_backend
    init: true
    build:
      context: ./backend/
      target: production
    restart: always
    environment:
      - NODE_PATH=${EXPRESS_NODE_PATH}
      - AWS_REGION=${AWS_REGION}
      - NODE_ENV=production
      - DOCKER_BUILDKIT=1
      - PORT=${NODE_PORT}
    networks:
      - client
##############################
# Front-End Container
##############################
  nginx:
    container_name: ${PROJECT_NAME}_frontend
    build:
      context: ./frontend/
      target: production
      args:
        - NODE_PATH=${REACT_NODE_PATH}
        - SASS_PATH=${SASS_PATH}
    restart: always
    environment:
      - PROJECT_NAME=${PROJECT_NAME}
      - NODE_PORT=${NODE_PORT}
      - DOCKER_BUILDKIT=1
    command: /bin/ash -c "envsubst '$$PROJECT_NAME $$NODE_PORT' < /etc/nginx/conf.d/nginx.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
    expose:
      - "80"
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - client
##############################
# General Config
##############################
networks:
  client:

我知道 certbot 有一个 Docker 图片,但我不确定如何使用它。我还担心我通过 http 将对 /api/ 的请求代理到服务器的方式。这也会给我带来任何问题吗?


编辑:

尝试 #1:Traefik

我创建了一个 Traefik 容器来通过 HTTPS 路由所有流量。

version: '2'

services:
  traefik:
    image: traefik
    restart: always
    ports:
      - 80:80
      - 443:443
    networks:
      - web
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/traefik/traefik.toml:/traefik.toml
      - /opt/traefik/acme.json:/acme.json
    container_name: traefik

networks:
  web:
    external: true

对于toml文件,我添加了以下内容:

debug = false

logLevel = "ERROR"
defaultEntryPoints = ["https","http"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
  [entryPoints.https.tls]

[retry]

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "ec2-00-000-000-00.eu-west-1.compute.amazonaws.com"
watch = true
exposedByDefault = false

[acme]
storage = "acme.json"
entryPoint = "https"
onHostRule = true
[acme.httpChallenge]
entryPoint = "http"

我将其添加到我的 docker-compose 生产文件中:

labels:
  - "traefik.docker.network=web"
  - "traefik.enable=true"
  - "traefik.basic.frontend.rule=Host:ec2-00-000-000-00.eu-west-1.compute.amazonaws.com"
  - "traefik.basic.port=80"
  - "traefik.basic.protocol=https"

我 运行 docker-compose up 用于 Traefik 容器,然后 运行 docker-compose up 在我的生产映像上。我收到以下错误:

unable to obtain acme certificate

我正在阅读 Traefik 文档,显然有一种方法可以专门为 Amazon ECS 配置 toml 文件:https://docs.traefik.io/configuration/backends/ecs/

我走在正确的轨道上吗?

最简单的方法是设置 ALB 并将其用于 HTTPS。

  1. 创建 ALB
  2. 将 443 侦听器添加到 ALB
  3. 使用 AWS Certificate Manager 生成证书
  4. 将证书设置为负载均衡器的默认证书
  5. 创建目标组
  6. 将您的 EC2 实例添加到目标组
  7. 将 ALB 指向目标组

请求将使用 ALB 和 https

按照 Nginx and Let's Encrypt with Docker in Less Than 5 Minutes 上的教程启用 SSL。我在关注它时 运行 遇到了一些问题,所以我会尝试在这里澄清一些事情。

这些步骤包括将以下内容添加到 docker-compose.yml

##############################
# Certbot Container
##############################
  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./frontend/data/certbot/conf:/etc/letsencrypt
      - ./frontend/data/certbot/www:/var/www/certbot

至于 docker-compose.ymlNginx Container 部分,应进行修改以包含添加到 Certbot Container[= 的相同卷86=],以及添加端口和公开配置:

  service_name:
    container_name: container_name
    image: nginx:alpine
    command: /bin/ash -c "exec nginx -g 'daemon off;'"
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    expose:
      - "80"
      - "443"
    ports:
      - "80:80"
      - "443:443"
    networks:
      - default

data 文件夹可以保存在其他任何地方,但请确保知道它的位置,并确保在以后重新使用时正确引用它。在这个例子中,我只是将它保存在与 docker-compose.yml 文件相同的目录中。

完成上述配置后,需要执行几个步骤来初始化证书的颁发。

首先,您的 Nginx 配置 (default.conf) 将被更改以适应域验证请求:

server {
    listen       80;
    server_name example.com www.example.com;
    server_tokens off;

    location / {
        return 301 https://$server_name$request_uri;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
}

server {
     listen 443 ssl;
     server_name example.com www.example.com;
     server_tokens off;

     ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
     include /etc/letsencrypt/options-ssl-nginx.conf;
     ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

     location / {
         root   /usr/share/nginx/html;
         index  index.html index.htm;
         try_files $uri /index.html;
         proxy_set_header    Host                $http_host;
         proxy_set_header    X-Real-IP           $remote_addr;
         proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
     }
}

修改 Nginx 配置文件后,将创建一个虚拟证书以允许进行 Let's Encrypt 验证。有一个脚本可以自动执行所有这些操作,可以使用 CURL 下载到项目的根目录,然后再进行修改以适应环境。该脚本还需要使用 chmod 命令使脚本可执行:

curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh > init-letsencrypt.sh && chmod +x init-letsencrypt.sh

脚本下载后修改如下:

#!/bin/bash

if ! [ -x "$(command -v docker-compose)" ]; then
  echo 'Error: docker-compose is not installed.' >&2
  exit 1
fi

-domains=(example.org www.example.org)
+domains=(example.com www.example.com)
rsa_key_size=4096
-data_path="./data/certbot"
+data_path="./data/certbot"
-email="" # Adding a valid address is strongly recommended
+email="admin@example.com" # Adding a valid address is strongly recommended
staging=0 # Set to 1 when testing setup to avoid hitting request limits

if [ -d "$data_path" ]; then
  read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
  if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
    exit
  fi
fi

if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
  echo "### Downloading recommended TLS parameters ..."
  mkdir -p "$data_path/conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
  echo
fi

echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
-docker-compose run --rm --entrypoint "\
+docker-compose -f docker-compose.yml run --rm --entrypoint "\
  openssl req -x509 -nodes -newkey rsa:1024 -days 1\
    -keyout '$path/privkey.pem' \
    -out '$path/fullchain.pem' \
    -subj '/CN=localhost'" certbot
echo

echo "### Starting nginx ..."
-docker-compose up --force-recreate -d nginx
+docker-compose -f docker-compose.yml up --force-recreate -d service_name
echo

echo "### Deleting dummy certificate for $domains ..."
-docker-compose run --rm --entrypoint "\
+docker-compose -f docker-compose.yml run --rm --entrypoint "\
  rm -Rf /etc/letsencrypt/live/$domains && \
  rm -Rf /etc/letsencrypt/archive/$domains && \
  rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo

echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
  domain_args="$domain_args -d $domain"
done

# Select appropriate email arg
case "$email" in
  "") email_arg="--register-unsafely-without-email" ;;
  *) email_arg="--email $email" ;;
esac

# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi

-docker-compose run --rm --entrypoint "\
+docker-compose -f docker-compose.yml run --rm --entrypoint "\
  certbot certonly --webroot -w /var/www/certbot \
    $staging_arg \
    $email_arg \
    $domain_args \
    --rsa-key-size $rsa_key_size \
    --agree-tos \
    --force-renewal" certbot
echo

echo "### Reloading nginx ..."
-docker-compose exec nginx nginx -s reload
+docker-compose exec service_name nginx -s reload

我确保始终在 docker-compose 命令中包含 -f 标志,以防万一有人在自定义名为 docker-compose.yml 的文件中不知道要更改什么.我还确保将服务名称设置为 service_name 以确保区分服务名称和 Nginx 命令,这与教程不同。

注意: 如果不确定设置是否正常工作,请确保将暂存设置为 1 以避免达到请求限制。重要的是要记住在测试完成后将其设置回 0 并重做修改 init-letsencrypt.sh 文件的所有步骤。完成测试并将暂存设置为 0 后,重要的是停止之前的 运行ning 容器并删除数据文件夹以确保正确的初始认证:

$ docker-compose -f docker-compose.yml down && yes | docker system prune -a --volumes && sudo rm -rf ./data

证书准备好初始化后,脚本将 运行 使用 sudo;使用 sudo 非常重要,因为如果 运行 没有它,容器内的权限会出现问题。

$ sudo ./init-letsencrypt.sh

证书颁发后,有自动更新证书的事情;需要做两件事:

  • Nginx Container中,Nginx会通过以下修改重新加载新获取的证书:
service_name:
...
- command: /bin/ash -c "exec nginx -g 'daemon off;'"
+ command: /bin/ash -c "while :; do sleep 6h & wait $${!}; nginx -s reload; done & exec nginx -g 'daemon off;'"
...
  • Certbot Container 部分,将添加以下内容以检查证书是否每十二小时更新一次,正如 Let's Encrypt 所建议的:
certbot:
...
+ entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"

在 运行 宁 docker-compose -f docker-compose.yml up 之前,data 文件夹的所有权应更改为 ec2-user;这是为了避免 运行 在 运行 宁 docker-compose -f docker-compose.yml up 或 运行 以 sudo 模式 运行 宁它时出现权限错误:

sudo chown ec2-user:ec2-user -R /path/to/data/

不要忘记在您的 DNS 提供商中为 Let's Encrypt 添加 CAA 记录。您可以阅读 here 以获取有关如何操作的更多信息。

如果你 运行 遇到 Nginx 容器的任何问题,因为你正在替换变量并且 $server_name$request_uri 没有正确显示,你可以参考 this issue .