Хорошо - у меня есть очень интересное поведение в приложении 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/"
Заранее спасибо всем, кто мне здесь помогает.Я так близок, успешно подписываю сертификаты, просто не могу их сохранить и заставить работать во время первоначального выполнения контейнера.Если вам понадобится что-нибудь от меня, чтобы прояснить это, я с радостью предоставлю это.