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 68f5d131..364a3464 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /data/certbot +!/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..3eb5711d --- /dev/null +++ b/certbot/docker-entrypoint.sh @@ -0,0 +1,67 @@ +#!/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 + +# 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 ..." + + # join $domains to -d args + domain_args="" + for domain in "${domains[@]}"; do + domain_args="$domain_args -d $domain" + done + + # 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 \ + --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 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..c2d3b159 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,36 @@ version: '3' +volumes: + ssl: {} services: nginx: - image: nginx:1.15-alpine + build: + context: nginx + dockerfile: Dockerfile restart: unless-stopped - volumes: - - ./data/nginx:/etc/nginx/conf.d - - ./data/certbot/conf:/etc/letsencrypt - - ./data/certbot/www:/var/www/certbot + healthcheck: + test: ["CMD", "curl", "--silent", "--fail", "http://localhost"] + interval: 45s + timeout: 5s + retries: 3 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/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 diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..34ba0a73 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,23 @@ +FROM nginx:1.19-alpine + +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" \ +&& 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/* + +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/nginx/conf-templates/app.conf.template b/nginx/conf-templates/app.conf.template new file mode 100644 index 00000000..4a549124 --- /dev/null +++ b/nginx/conf-templates/app.conf.template @@ -0,0 +1,39 @@ +# vim:sw=4:ts=4:et:ft=nginx + +server { + listen 80; + 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; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name ${nginx_domain}; + server_tokens off; + + 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://${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 "$@"