From 617d8870adb48985c239fba9e72c1871a8f4384c Mon Sep 17 00:00:00 2001 From: Andrius Kairiukstis Date: Sun, 25 Oct 2020 15:51:53 +0100 Subject: [PATCH 1/4] WIP: Opening MR --- .gitignore | 4 +++ data/nginx/app.conf | 31 ------------------ docker-compose.yml | 2 +- init-letsencrypt.sh | 80 --------------------------------------------- 4 files changed, 5 insertions(+), 112 deletions(-) delete mode 100644 data/nginx/app.conf delete mode 100755 init-letsencrypt.sh diff --git a/.gitignore b/.gitignore index 68f5d131..53a54635 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /data/certbot +!/date/certbot/.keep +/.env +!/.env*sample + diff --git a/data/nginx/app.conf b/data/nginx/app.conf deleted file mode 100644 index 52dc0e78..00000000 --- a/data/nginx/app.conf +++ /dev/null @@ -1,31 +0,0 @@ -server { - listen 80; - server_name example.org; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - return 301 https://$host$request_uri; - } -} - -server { - listen 443 ssl; - server_name example.org; - server_tokens off; - - ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - - location / { - proxy_pass http://example.org; - 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; - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 9615cc1f..9fc97e62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: image: nginx:1.15-alpine restart: unless-stopped volumes: - - ./data/nginx:/etc/nginx/conf.d + - ./data/nginx/conf.d:/etc/nginx/conf.d - ./data/certbot/conf:/etc/letsencrypt - ./data/certbot/www:/var/www/certbot ports: diff --git a/init-letsencrypt.sh b/init-letsencrypt.sh deleted file mode 100755 index 13eaa757..00000000 --- a/init-letsencrypt.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/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) -rsa_key_size=4096 -data_path="./data/certbot" -email="" # Adding a valid address is strongly recommended -staging=0 # Set to 1 if you're testing your 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/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/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 "\ - 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 -echo - -echo "### Deleting dummy certificate for $domains ..." -docker-compose 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 "\ - 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 From 2e52f772561bdaed31b796bbefb837acc89f1b75 Mon Sep 17 00:00:00 2001 From: Andrius Kairiukstis Date: Sun, 25 Oct 2020 17:34:27 +0100 Subject: [PATCH 2/4] Certbot depends on healthy nginx service - Created `Dockerfile` for nginx; - Downloading `options-ssl-nginx.conf` and `ssl-dhparams.pem` during docker build phase; - Relevant part in the `app.conf` updated; - Creating temp SSL certificate through `docker-entrypoint.sh` when nginx service is starting; --- data/nginx/Dockerfile | 16 +++++++++++++ data/nginx/conf.d/app.conf | 31 +++++++++++++++++++++++++ data/nginx/scripts/docker-entrypoint.sh | 16 +++++++++++++ docker-compose.yml | 13 ++++++++++- 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 data/nginx/Dockerfile create mode 100644 data/nginx/conf.d/app.conf create mode 100755 data/nginx/scripts/docker-entrypoint.sh diff --git a/data/nginx/Dockerfile b/data/nginx/Dockerfile new file mode 100644 index 00000000..2d7598b7 --- /dev/null +++ b/data/nginx/Dockerfile @@ -0,0 +1,16 @@ +FROM nginx:1.15-alpine + +RUN apk add --no-cache \ + curl \ + bash \ + openssl \ +&& mkdir -p /etc/nginx/letsencrypt \ +&& curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "/etc/nginx/letsencrypt/options-ssl-nginx.conf" \ +&& curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "/etc/nginx/letsencrypt/ssl-dhparams.pem" \ +&& chown -R nginx /etc/nginx/letsencrypt \ +&& rm -rf /var/cache/apk/* \ + /tmp/* \ + /var/tmp/* + +COPY ./scripts/docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/nginx/conf.d/app.conf b/data/nginx/conf.d/app.conf new file mode 100644 index 00000000..37497b40 --- /dev/null +++ b/data/nginx/conf.d/app.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_name example.org; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name example.org; + server_tokens off; + + ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem; + include /etc/nginx/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/nginx/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://example.org; + 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; + } +} diff --git a/data/nginx/scripts/docker-entrypoint.sh b/data/nginx/scripts/docker-entrypoint.sh new file mode 100755 index 00000000..36a4779d --- /dev/null +++ b/data/nginx/scripts/docker-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +domain="example.org" +rsa_key_size=4096 +data_path="/etc/letsencrypt" + +echo "### Creating dummy certificate for $domain ..." +path="/etc/letsencrypt/live/$domain" +mkdir -p "$path" + +openssl req -x509 -nodes -newkey rsa:1024 -days 1 \ + -keyout "$path/privkey.pem" \ + -out "$path/fullchain.pem" \ + -subj '/CN=localhost' + +exec "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 9fc97e62..8afc7e78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,15 @@ version: '3' services: nginx: - image: nginx:1.15-alpine + build: + context: ./data/nginx + dockerfile: Dockerfile restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost"] + interval: 45s + timeout: 5s + retries: 3 volumes: - ./data/nginx/conf.d:/etc/nginx/conf.d - ./data/certbot/conf:/etc/letsencrypt @@ -12,9 +19,13 @@ services: - "80:80" - "443:443" command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + certbot: image: certbot/certbot restart: unless-stopped + depends_on: + nginx: + condition: service_healthy volumes: - ./data/certbot/conf:/etc/letsencrypt - ./data/certbot/www:/var/www/certbot From df7be29e4f4c3cee1f804d662304985cfd1130aa Mon Sep 17 00:00:00 2001 From: Andrius Kairiukstis Date: Mon, 2 Nov 2020 17:20:35 +0100 Subject: [PATCH 3/4] Finished migrating bootstrap logic from the `init-letsencrypt.sh` - certbot container waiting for healthy nginx container; - during startup of the nginx container: - an initial certificate get created; - it starts nginx daemon and await for the certbot container, then removing temporally certificate; - it does start a "API" that allow certbot to reload nginx daemon; - --- .env-sample | 8 +++ .gitignore | 4 +- certbot/Dockerfile | 12 ++++ certbot/docker-entrypoint.sh | 61 ++++++++++++++++++ data/nginx/scripts/docker-entrypoint.sh | 16 ----- docker-compose.yml | 26 ++++---- {data/nginx => nginx}/Dockerfile | 13 +++- .../conf-templates/app.conf.template | 18 ++++-- nginx/docker-entrypoint.sh | 63 +++++++++++++++++++ 9 files changed, 184 insertions(+), 37 deletions(-) create mode 100644 .env-sample create mode 100644 certbot/Dockerfile create mode 100755 certbot/docker-entrypoint.sh delete mode 100755 data/nginx/scripts/docker-entrypoint.sh rename {data/nginx => nginx}/Dockerfile (62%) rename data/nginx/conf.d/app.conf => nginx/conf-templates/app.conf.template (56%) create mode 100755 nginx/docker-entrypoint.sh diff --git a/.env-sample b/.env-sample new file mode 100644 index 00000000..63b8015f --- /dev/null +++ b/.env-sample @@ -0,0 +1,8 @@ +# Set to 1 if you're testing your setup to avoid hitting request limits +staging=0 +domains=example.org +# Adding a valid address is strongly recommended +email="" +rsa_key_size=4096 +nginx_api_user=foo +nginx_api_password=bar diff --git a/.gitignore b/.gitignore index 53a54635..364a3464 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /data/certbot -!/date/certbot/.keep +!/data/certbot/scripts /.env !/.env*sample - +/TMP diff --git a/certbot/Dockerfile b/certbot/Dockerfile new file mode 100644 index 00000000..01f7a60f --- /dev/null +++ b/certbot/Dockerfile @@ -0,0 +1,12 @@ +FROM certbot/certbot + +RUN set -x \ +&& apk add --no-cache \ + curl \ + bash \ +&& rm -rf /var/cache/apk/* \ + /tmp/* \ + /var/tmp/* + +COPY docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/certbot/docker-entrypoint.sh b/certbot/docker-entrypoint.sh new file mode 100755 index 00000000..99bec575 --- /dev/null +++ b/certbot/docker-entrypoint.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# vim:sw=2:ts=2:et + +set -ueo pipefail +# DEBUG +# set -x + +# convert space-delimited string from the ENV to array +domains=(${domains:-example.org}) + +domain=${domains[0]} + +data_path="/etc/letsencrypt" +path="$data_path/live/$domain" + +rsa_key_size=${rsa_key_size:-4096} + +trap exit TERM + +echo "### Let's nginx bootstrap" +sleep 10s + +if [ ! -f "$path/privkey.pem" ]; then + 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" + else + staging_arg="" + fi + + certbot certonly --webroot -w /var/www/certbot \ + $staging_arg \ + $email_arg \ + $domain_args \ + --rsa-key-size $rsa_key_size \ + --agree-tos \ + --force-renewal + + echo "### Reloading nginx ..." + curl --fail --silent --user ${nginx_api_user}:${nginx_api_password} http://nginx/nginx/reload +fi + +while :; do + certbot renew + curl --fail --silent --user ${nginx_api_user}:${nginx_api_password} http://nginx/nginx/reload + sleep 12h & wait ${!} +done diff --git a/data/nginx/scripts/docker-entrypoint.sh b/data/nginx/scripts/docker-entrypoint.sh deleted file mode 100755 index 36a4779d..00000000 --- a/data/nginx/scripts/docker-entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -domain="example.org" -rsa_key_size=4096 -data_path="/etc/letsencrypt" - -echo "### Creating dummy certificate for $domain ..." -path="/etc/letsencrypt/live/$domain" -mkdir -p "$path" - -openssl req -x509 -nodes -newkey rsa:1024 -days 1 \ - -keyout "$path/privkey.pem" \ - -out "$path/fullchain.pem" \ - -subj '/CN=localhost' - -exec "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 8afc7e78..c2d3b159 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,32 +1,36 @@ version: '3' +volumes: + ssl: {} services: nginx: build: - context: ./data/nginx + context: nginx dockerfile: Dockerfile restart: unless-stopped healthcheck: - test: ["CMD", "curl", "--fail", "http://localhost"] + test: ["CMD", "curl", "--silent", "--fail", "http://localhost"] interval: 45s timeout: 5s retries: 3 - volumes: - - ./data/nginx/conf.d:/etc/nginx/conf.d - - ./data/certbot/conf:/etc/letsencrypt - - ./data/certbot/www:/var/www/certbot ports: - "80:80" - "443:443" - command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + env_file: .env + volumes: + - ./nginx/conf-templates:/etc/nginx/templates + - ssl:/etc/letsencrypt + - ssl:/var/www/certbot certbot: - image: certbot/certbot + build: + context: certbot + dockerfile: Dockerfile restart: unless-stopped depends_on: nginx: condition: service_healthy + env_file: .env volumes: - - ./data/certbot/conf:/etc/letsencrypt - - ./data/certbot/www:/var/www/certbot - entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + - ssl:/etc/letsencrypt + - ssl:/var/www/certbot diff --git a/data/nginx/Dockerfile b/nginx/Dockerfile similarity index 62% rename from data/nginx/Dockerfile rename to nginx/Dockerfile index 2d7598b7..34ba0a73 100644 --- a/data/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,8 +1,11 @@ -FROM nginx:1.15-alpine +FROM nginx:1.19-alpine -RUN apk add --no-cache \ +RUN set -x \ +&& apk add --no-cache \ + apache2-utils \ curl \ bash \ + netcat-openbsd \ openssl \ && mkdir -p /etc/nginx/letsencrypt \ && curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "/etc/nginx/letsencrypt/options-ssl-nginx.conf" \ @@ -12,5 +15,9 @@ RUN apk add --no-cache \ /tmp/* \ /var/tmp/* -COPY ./scripts/docker-entrypoint.sh / +RUN curl -s https://raw.githubusercontent.com/nginxinc/docker-nginx/master/mainline/alpine/docker-entrypoint.sh > "/nginx-entrypoint.sh" \ +&& chmod +x /nginx-entrypoint.sh +COPY docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["nginx", "-g", "daemon off;"] diff --git a/data/nginx/conf.d/app.conf b/nginx/conf-templates/app.conf.template similarity index 56% rename from data/nginx/conf.d/app.conf rename to nginx/conf-templates/app.conf.template index 37497b40..4a549124 100644 --- a/data/nginx/conf.d/app.conf +++ b/nginx/conf-templates/app.conf.template @@ -1,8 +1,16 @@ +# vim:sw=4:ts=4:et:ft=nginx + server { listen 80; - server_name example.org; + server_name ${nginx_domain}; server_tokens off; + location /nginx/reload { + auth_basic 'Access restriction'; + auth_basic_user_file /tmp/.htpasswd; + proxy_pass http://localhost:9000; + } + location /.well-known/acme-challenge/ { root /var/www/certbot; } @@ -14,16 +22,16 @@ server { server { listen 443 ssl; - server_name example.org; + server_name ${nginx_domain}; server_tokens off; - ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem; + ssl_certificate /etc/letsencrypt/live/${nginx_domain}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${nginx_domain}/privkey.pem; include /etc/nginx/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/nginx/letsencrypt/ssl-dhparams.pem; location / { - proxy_pass http://example.org; + proxy_pass http://${nginx_domain}; 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; diff --git a/nginx/docker-entrypoint.sh b/nginx/docker-entrypoint.sh new file mode 100755 index 00000000..f3d37a7f --- /dev/null +++ b/nginx/docker-entrypoint.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# vim:sw=2:ts=2:et + +set -ueo pipefail +# DEBUG +# set -x + +# convert space-delimited string from the ENV to array +domains=(${domains:-example.org}) +domain="${domains[0]}" +export nginx_domain="${domain}" + +data_path="/etc/letsencrypt" +path="$data_path/live/$domain" + +rsa_key_size=${rsa_key_size:-4096} + +wait_certbot(){ + ( + echo "### Waiting for certbot container" + + retries="${1:-180}" + + set +e + until ping -c 1 certbot > /dev/null 2>&1 || [ "$retries" -eq 0 ]; do + : $((retries--)) + echo "### certbot is not up yet!" + sleep 1s + done + set -e + + [ "${retries}" -ne 0 ] || (echo "### certbot service did not get up"; exit 1) + + echo "### Removing self-signed SSL from $path" + rm -rf "$path" + ) & +} + +if [ ! -f "$path/privkey.pem" ]; then + sleep 5 + echo "### Creating dummy certificate for $domain ..." + + mkdir -p "$path" + + openssl req -x509 -nodes -newkey rsa:1024 -days 1 \ + -keyout "$path/privkey.pem" \ + -out "$path/fullchain.pem" \ + -subj '/CN=localhost' + + wait_certbot +fi + +# API service that reloads nginx on request +htpasswd -bc /tmp/.htpasswd "${nginx_api_user}" "${nginx_api_password}" > /dev/null 2>&1 +( + while true + do + { echo -e "HTTP/1.1 200 OK\n\nNGINX reload requested at: $(date)"; nginx -s reload & } | nc -l -p 9000 -q 1 + done +) & + +# original entrypoint for nginx +exec /nginx-entrypoint.sh "$@" From 35ae03c68d8672b93b2ab78203fce8a37edb0aeb Mon Sep 17 00:00:00 2001 From: Andrius Kairiukstis Date: Sat, 16 Jan 2021 11:28:05 +0000 Subject: [PATCH 4/4] Updated certbot renew command syntax, it were failing withour webroot instruction --- certbot/docker-entrypoint.sh | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/certbot/docker-entrypoint.sh b/certbot/docker-entrypoint.sh index 99bec575..3eb5711d 100755 --- a/certbot/docker-entrypoint.sh +++ b/certbot/docker-entrypoint.sh @@ -20,6 +20,12 @@ trap exit TERM echo "### Let's nginx bootstrap" sleep 10s +# Select appropriate email arg +case "$email" in + "") email_arg="--register-unsafely-without-email" ;; + *) email_arg="--email $email" ;; +esac + if [ ! -f "$path/privkey.pem" ]; then echo "### Requesting Let's Encrypt certificate for $domains ..." @@ -29,12 +35,6 @@ if [ ! -f "$path/privkey.pem" ]; then 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" @@ -42,7 +42,8 @@ if [ ! -f "$path/privkey.pem" ]; then staging_arg="" fi - certbot certonly --webroot -w /var/www/certbot \ + certbot certonly \ + --webroot -w /var/www/certbot \ $staging_arg \ $email_arg \ $domain_args \ @@ -55,7 +56,12 @@ if [ ! -f "$path/privkey.pem" ]; then fi while :; do - certbot renew + certbot renew \ + --webroot -w /var/www/certbot \ + $email_arg \ + --rsa-key-size $rsa_key_size \ + --agree-tos + curl --fail --silent --user ${nginx_api_user}:${nginx_api_password} http://nginx/nginx/reload sleep 12h & wait ${!} done