Проверка LetsEncrypt с помощью Docker, Nginx, AWS ECS завершается ошибкой в ​​половине случаев - PullRequest
0 голосов
/ 22 мая 2018

Хорошо - у меня есть очень интересное поведение в приложении Rails при развертывании в производство на AWS ECS.

Быстрый фон:

  • Приложение Rails, докеризованное с помощью прокси-сервера nginx * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ”* '* * *) * * * *
  • * * * * * * * * * * * * * * * * * * * * * * *.1015 * и с тех пор обновился до обезвоженной, последняя версия
  • У меня есть доступ по ssh / root ко всем моим серверам в AWS, я буду запускать любые рекомендуемые тесты
  • Сценарии сертификатов работают локально

Итак, вот что происходит.Наш процесс CI / CD проходит через travis-ci, передает изображение в ECR, а затем обновляет наши задачи / службы в ECS, чтобы использовать этот новый образ.Прекрасно работает.

Теперь, после реализации функциональности SSL, он не работает 100% времени при попытке запросить и проверить сертификат в контейнере nginx.С ошибкой 404 (см. Вывод журнала ниже).

Я борюсь с этим уже почти неделю, и это мешает нам начать жить.Вот мои вопросы:

  • Как я могу проверить и идентифицировать проблему?
  • Почему только один сервер позволяет мне успешно выполнить проверку?
  • Каким образом я могу реорганизовать это, чтобы получить рабочее решение?
  • Может ли облачный свет вызывать эти проблемы?

Я открыт для всех идей иhelp!

Больше контекста:

  • Я могу успешно поместить файл "test.txt" в мой $ SSL_ROOT / .well-known / acme-просматривает dir и просматривает его в браузере
  • $ WELLKNOWN определен и работает в моем файле config.sh
  • Еще более странно, я могу вручную запустить SSH, запустить docker и запуститьточно такая же команда успешно!Но только на одном из серверов, а не на обоих.Это, кажется, совершенно случайно, и я не смог определить, как / почему это работает, когда это иногда происходит 1/20 раз.(cloudflare?)
  • Запуск AMI, оптимизированного для Linux, для ECS (см. выпуск os ниже)
  • Я использую проверку http-01
  • Я использую Cloudflare для управления нашим DNS(Приложение находится на поддомене, веб-сайт - в корневом домене)

Ниже приведены мои docker-compose, nginx config, web_cmd.sh и вывод журнала, с очевидными значениями, отредактированными для примера, и т. Д .:

Журналы:

+ Generating account key...
+ Registering account key with ACME server...
 + Creating chain cache directory ./chains
Processing orders.example.com
 + Creating new directory ./certs/orders.example.com ...
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting authorization for orders.example.com...
 + 1 pending challenge(s)
 + Deploying challenge tokens...
 + Responding to challenge for orders.example.com authorization...
172.31.37.243 - - [21/May/2018:20:41:19 +0000] "GET /.well-known/acme-challenge/5z5x0pgn-eaoHTxvlvIHB6ZjdVu39DH2CjFDhlX-Hqo HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "2600:1f14:ac6:4f10:505a:1249:9e33:edae, 108.162.245.41"
172.31.37.243 - - [21/May/2018:20:41:19 +0000] "GET /.well-known/acme-challenge/5z5x0pgn-eaoHTxvlvIHB6ZjdVu39DH2CjFDhlX-Hqo HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server;     +https://www.letsencrypt.org)" "2600:3000:2710:300:0:0:0:1d, 172.68.174.5"
 + Cleaning challenge tokens...
 + Challenge validation has failed :(
ERROR: Challenge is invalid! (returned: invalid) (result: {
  "type": "http-01",
  "status": "invalid",
  "error": {
    "type": "urn:acme:error:unauthorized",
    "detail": "Invalid response from http://orders.example.com/.well-known/acme-challenge/5z5x0pgn-eaoHTxvlvIHB6ZjdVu39DH2CjFDhlX-Hqo: \"\u003chtml\u003e\r\n\u003chead\u003e\u003ctitle\u003e404 Not Found\u003c/title\u003e\u003c/head\u003e\r\n\u003cbody bgcolor=\"white\"\u003e\r\n\u003ccenter\u003e\u003ch1\u003e404 Not Found\u003c/h1\u003e\u003c/center\u003e\r\n\u003chr\u003e\u003ccenter\u003e    \"",
    "status": 403
  },
  "uri": "https://acme-staging.api.letsencrypt.org/acme/challenge/OLGNI3nejESFWljtiNb1SYSA31r1nCLl2SGMTqY-a24/129269548",
  "token": "5z5x0pgn-eaoHTxvlvIHB6ZjdVu39DH2CjFDhlX-Hqo",
  "keyAuthorization": "5z5x0pgn-eaoHTxvlvIHB6ZjdVu39DH2CjFDhlX-Hqo.izK8nKawDYb0eZQodIC5v1Mb-w-yOqGDz2_9tC5arJg",
  "validationRecord": [
    {
      "url": "http://orders.example.com/.well-known/acme-challenge/5z5x0pgn-eaoHTxvlvIHB6ZjdVu39DH2CjFDhlX-Hqo",
      "hostname": "orders.example.com",
      "port": "80",
      "addressesResolved": [
        "104.27.178.217",
        "104.27.179.217",
        "2400:cb00:2048:1::681b:b2d9",
        "2400:cb00:2048:1::681b:b3d9"
      ],
      "addressUsed": "2400:cb00:2048:1::681b:b2d9"
        }
      ]
    })

Конфигурация Nginx:

upstream puma {
  server app:3000;
}

server {
  # expect SSL requests, try to use HTTP2
  listen 443 ssl;

  # define our domain; CHANGE ME
  server_name orders.example.com;

  # define the public application root
  root   $RAILS_ROOT/public;
  index  index.html;

  # configure SSL
  ssl_certificate $SSL_CERT_HOME/fullchain.pem;
  ssl_certificate_key $SSL_CERT_HOME/privkey.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_session_tickets off;
  ssl_dhparam $SSL_CERT_HOME/dhparam.pem;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
  ssl_prefer_server_ciphers on;

  # define where Nginx should write its logs
  access_log $RAILS_ROOT/log/nginx.access.log;
  error_log $RAILS_ROOT/log/nginx.error.log;

  # deny requests for files that should never be accessed
  location ~ /\. {
    deny all;
  }

  location ~* ^.+\.(rb|log)$ {
    deny all;
  }

  # serve static (compiled) assets directly if they exist (for rails production)
  location ~ ^/(assets|images|javascripts|stylesheets|swfs|system)/ {
    try_files $uri @rails;

    access_log off;
    gzip_static on; # to serve pre-gzipped version

    expires max;
    add_header Cache-Control public;

    # Some browsers still send conditional-GET requests if there's a
    # Last-Modified header or an ETag header even if they haven't
    # reached the expiry date sent in the Expires header.
    add_header Last-Modified "";
    add_header ETag "";
    break;
  }

  # send non-static file requests to the app server
  location / {
    try_files $uri @rails;
  }

  location @rails {
    proxy_set_header X-Forwarded-Proto $scheme; # prevent infinite request loop
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://puma;
  }
}

server {
  # many clients will send unencrypted requests
  listen 80;

  # accept unencrypted ACME challenge requests
  location ^~ /.well-known/acme-challenge {
    alias $SSL_ROOT/.well-known/acme-challenge/;
  }

  # force insecure requests through SSL
  location / {
    return 301 https://$host$request_uri;
  }
}

docker-compose.yml

version: '2'
services:
  postgres:
    image: postgres:9.6-alpine
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: app_pass
    volumes:
      - postgres:/var/lib/postgresql/data

  selenium:
    image: selenium/standalone-chrome:latest

  web:
    environment:
      CA_SSL: "false" # change to "true" for production

    build:
      context: .
      dockerfile: Dockerfile-nginx
    links:
      - app
    ports:
      - "80:80"
      - "443:443"

    volumes:
      - ssl_certs:/var/www/ssl

  mailcatcher:
    image: schickling/mailcatcher
    ports:
      - '1080:1080'

  app:
    build: .
    volumes:
      - .:/var/www/example
    environment:
      - DATABASE_NAME=example
      - DATABASE_USER=app_user
      - DATABASE_PASS=app_pass
      - DATABASE_HOST=postgres
      - DATABASE_PORT=5432
      - RAILS_ENV
      - SELENIUM_REMOTE_HOST=selenium
    expose:
      - "3000"
    depends_on: 
      - 'postgres'
      - 'selenium'
      - 'mailcatcher'

volumes:
  postgres:
  ssl_certs:

Dockerfile-nginx:

# Base image:
FROM nginx

# install essential Linux packages
RUN apt-get update -qq && apt-get -y install apache2-utils curl

# establish where Nginx should look for files
ENV RAILS_ROOT /var/www/example
#
# where we store everything SSL-related
ENV SSL_ROOT /var/www/ssl

# where Nginx looks for SSL files
ENV SSL_CERT_HOME $SSL_ROOT/certs/live

# copy over the script that is run by the container
COPY web_cmd.sh /tmp/

# Set our working directory inside the image
WORKDIR $RAILS_ROOT

# create log directory
RUN mkdir log

# copy over static assets
COPY public public/

# Copy Nginx config template
COPY config/nginx.conf /tmp/docker_example.nginx

# substitute variable references in the Nginx config template for real values from the environment
# put the final config in its place
RUN envsubst '$RAILS_ROOT:$SSL_ROOT:$SSL_CERT_HOME' < /tmp/docker_example.nginx > /etc/nginx/conf.d/default.conf

#RUN rm -rf /etc/nginx/sites-available/default
#ADD config/nginx.conf /etc/nginx/sites-enabled/nginx.conf

EXPOSE 80

# Define the script we want run once the container boots
# Use the "exec" form of CMD so Nginx shuts down gracefully on SIGTERM (i.e. `docker stop`)
CMD [ "/tmp/web_cmd.sh" ]

web_cmd.sh (команда docker для контейнера nginx):

#!/usr/bin/env bash

# initialize the dehydrated environment
setup_letsencrypt() {

  # create the directory that will serve ACME challenges
  mkdir -p .well-known/acme-challenge
  chmod -R 755 .well-known # See https://github.com/lukas2511/dehydrated/blob/master/docs/domains_txt.md
  echo "orders.example.com" > domains.txt

  # See https://github.com/lukas2511/letsencrypt.sh/blob/master/docs/staging.md
  echo "CA=\"https://acme-staging.api.letsencrypt.org/directory\"" > config.sh

  # See https://github.com/lukas2511/letsencrypt.sh/blob/master/docs/wellknown.md
  echo "WELLKNOWN=\"$SSL_ROOT/.well-known/acme-challenge\"" >> config.sh

  # fetch stable version of dehydrated
  curl "https://raw.githubusercontent.com/lukas2511/dehydrated/v0.6.2/dehydrated" > dehydrated
  chmod 755 dehydrated
}

# creates self-signed SSL files
# these files are used in development and get production up and running so dehydrated can do its work
create_pems() {
  openssl req \
      -x509 \
      -nodes \
      -newkey rsa:1024 \
      -keyout privkey.pem \
      -out fullchain.pem \
      -days 3650 \
      -sha256 \
      -config <(cat <<EOF
[ req ]
prompt = no
distinguished_name = subject
x509_extensions    = x509_ext

[ subject ]
commonName = orders.example.com

[ x509_ext ]
subjectAltName = @alternate_names

[ alternate_names ]
DNS.1 = localhost
IP.1 = 127.0.0.1
EOF
)

  openssl dhparam -out dhparam.pem 2048
  chmod 600 *.pem
}

# if we have not already done so initialize Docker volume to hold SSL files
if [ ! -d "$SSL_CERT_HOME" ]; then
  mkdir -p $SSL_CERT_HOME
  chmod 755 $SSL_ROOT
  chmod -R 700 $SSL_ROOT/certs
  cd $SSL_CERT_HOME
  create_pems
  cd $SSL_ROOT
  setup_letsencrypt
fi

# if we are configured to run SSL with a real certificate authority run dehydrated to retrieve/renew SSL certs
if [ "$CA_SSL" = "true" ]; then

  touch $SSL_ROOT/.well-known/acme-challenge/test.txt
  # Nginx must be running for challenges to proceed
  # run in daemon mode so our script can continue
  nginx

  sleep 25

  cd $SSL_ROOT

  # retrieve/renew SSL certs
  ./dehydrated --accept-terms --config config.sh --cron

  # copy the fresh certs to where Nginx expects to find them
  cp $SSL_ROOT/certs/orders.example.com/fullchain.pem $SSL_ROOT/certs/orders.example.com/privkey.pem $SSL_CERT_HOME

  # pull Nginx out of daemon mode
  nginx -s stop
fi

# start Nginx in foreground so Docker container doesn't exit
nginx -g "daemon off;"

вывод cat /etc/os-release:

NAME="Amazon Linux AMI"
VERSION="2018.03"
ID="amzn"
ID_LIKE="rhel fedora"
VERSION_ID="2018.03"
PRETTY_NAME="Amazon Linux AMI 2018.03"
ANSI_COLOR="0;33"
CPE_NAME="cpe:/o:amazon:linux:2018.03:ga"
HOME_URL="http://aws.amazon.com/amazon-linux-ami/"

Заранее спасибо всем, кто мне здесь помогает.Я так близок, успешно подписываю сертификаты, просто не могу их сохранить и заставить работать во время первоначального выполнения контейнера.Если вам понадобится что-нибудь от меня, чтобы прояснить это, я с радостью предоставлю это.

...