commit 45aff8bc5b54dd3014420743bc0fdc599be07ff8 Author: Robert von Burg Date: Thu Jan 6 01:23:39 2022 +0100 [Project] Initial Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d405d8 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# nginx-lets-encrypt-proxy + +This is a docker container as a nginx proxy with Let's Encrypt and virtual hosts support. + +Inspirations: + +* https://github.com/gilyes/docker-nginx-letsencrypt-sample +* https://ilhicas.com/2019/03/02/Nginx-Letsencrypt-Docker.html +* https://github.com/Ilhicas/nginx-letsencrypt + + +Create necessary directories and files: + + mkdir ssl + mkdir nginx/modules-enabled + mkdir letsencrypt/ + touch letsencrypt/configured-domains + + +Create the dhparams: + + openssl dhparam -out ssl/dhparam.pem 2048 + + +And then add your virtual hosts in `nginx/sites-enabled`, but make sure to use the following certificates: + + ssl_certificate /etc/letsencrypt/snake-oil/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/snake-oil/private-key.pem; + +The internal scripts will then replace this and reload nginx when necessary. + +Now add your domains as separate lines in `letsencrypt/configured-domains`. + +Now build the image: + + docker-compose build --pull + +And then start: + + docker-compose up diff --git a/build/.dockerignore b/build/.dockerignore new file mode 100644 index 0000000..1d1fe94 --- /dev/null +++ b/build/.dockerignore @@ -0,0 +1 @@ +Dockerfile \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..97041b2 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,12 @@ +FROM nginx:alpine +RUN apk add bash inotify-tools certbot openssl ca-certificates +RUN mkdir -p /var/www/certbot/ +RUN mkdir -p /etc/ssl/ +WORKDIR /opt +COPY certbot-obtain.sh certbot-obtain.sh +COPY certbot-renewal.sh certbot-renewal.sh +COPY nginx-reload.sh nginx-reload.sh +COPY entrypoint.sh entrypoint.sh +COPY default.conf /etc/nginx/nginx.conf +RUN chmod +x certbot-obtain.sh && chmod +x certbot-renewal.sh && chmod +x nginx-reload.sh && chmod +x entrypoint.sh +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/build/certbot-obtain.sh b/build/certbot-obtain.sh new file mode 100644 index 0000000..1adaf42 --- /dev/null +++ b/build/certbot-obtain.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +if [[ "$#" != "3" ]] ; then + echo -e "ERROR: Usage: $0 " + exit 1 +fi + +WATCH_FILE="$1" +LETSENCRYPT_DIR="$2" +CERTBOT_WWW_DIR="$3" + +WATCH_FILE_NAME=$(basename $(realpath ${WATCH_FILE})) +WATCH_DIR=$(dirname $(realpath ${WATCH_FILE})) + +sleep 5s + +echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Watching directory ${WATCH_DIR} for changes to file ${WATCH_FILE_NAME}..." + +function parseFile() { + file="$1" + cat ${file} | while read line ; do + line=${line##*( )} + line=${line%%*( )} + [[ "$line" == "" ]] && continue + [[ $line =~ ^#.* ]] && continue + + obtainCertificate ${line} + done +} + +function obtainCertificate() { + domain="$1" + + nginx_file="/etc/nginx/sites-enabled/${domain}" + letsencrypt_file="${LETSENCRYPT_DIR}/live/${domain}" + + [[ ! -f ${nginx_file} ]] && continue + [[ -d ${letsencrypt_file} ]] && continue + + echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Obtaining certificate for new domain ${domain}" + if ! echo certbot certonly \ + --test-cert \ + --dry-run \ + --config-dir ${LETSENCRYPT_DIR} \ + --agree-tos \ + --domain "${domain}" \ + --email "${LETS_ENCRYPT_EMAIL}" \ + --expand \ + --noninteractive \ + --webroot \ + --webroot-path ${CERTBOT_WWW_DIR} ; then + echo -e "$(date +'%Y-%m-%d %H:%M:%S') ERROR: Failed to obtain certificate for $domain" + echo nginx -s quit + exit 1 + fi +} + +echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Initially obtaining certificates..." +parseFile ${WATCH_FILE} +echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Done." + +inotifywait -m -e CLOSE_WRITE "${WATCH_DIR}" | while read filename eventlist eventfile ; do + [[ $eventfile != ${WATCH_FILE_NAME} ]] && continue + echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Obtaining certificates as changes detected in file $filename $eventlist $eventfile" + parseFile ${WATCH_FILE} +done + +exit 0 \ No newline at end of file diff --git a/build/certbot-renewal.sh b/build/certbot-renewal.sh new file mode 100644 index 0000000..d5e3e78 --- /dev/null +++ b/build/certbot-renewal.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +sleep 30s + +echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Renewing certificates every 12h." +while : +do + sleep "12h" + echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Renewing certificates..." + + if ! certbot -q renew --post-hook nginx -s reload ; then + echo -e "$(date +'%Y-%m-%d %H:%M:%S') ERROR: Failed to renew certificates stopping nginx!" + nginx -s stop + fi +done + +exit 0 \ No newline at end of file diff --git a/build/default.conf b/build/default.conf new file mode 100644 index 0000000..92adec3 --- /dev/null +++ b/build/default.conf @@ -0,0 +1,63 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 1024; +} + +http { + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + #ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + #ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + ## + # Gzip Settings + ## + gzip on; + + # gzip_vary on; + # gzip_proxied any; + # gzip_comp_level 6; + # gzip_buffers 16 8k; + # gzip_http_version 1.1; + # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # Virtual Host Configs + ## + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} \ No newline at end of file diff --git a/build/entrypoint.sh b/build/entrypoint.sh new file mode 100644 index 0000000..7039e14 --- /dev/null +++ b/build/entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Create a self signed default certificate, so Nginx can start before we have +# any real certificates. + +#Ensure we have folders available + +WATCH_FILE="/etc/letsencrypt/configured-domains" +LETSENCRYPT_DIR="/etc/letsencrypt" +CERTBOT_WWW_DIR="/var/www/certbot" + +if [[ ! -f "${WATCH_FILE}" ]] ; then + echo -e "E$(date +'%Y-%m-%d %H:%M:%S') RROR: Failed to read configured-domains file: ${WATCH_FILE}" + exit 1 +fi + +if [[ ! -d "${CERTBOT_WWW_DIR}" ]] ; then + echo -e "E$(date +'%Y-%m-%d %H:%M:%S') RROR: Certbot directory does not exist at ${CERTBOT_WWW_DIR}" + exit 1 +fi + +LETS_ENCRYPT_EMAIL=eitch@eitchnet.ch +NGINX_HOST=proxy.eitchnet.ch +NGINX_PORT=80 + +if [[ ! -f /etc/letsencrypt/snake-oil/fullchain.pem ]] ; then + echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Creating snake-oil certificate..." + mkdir -p /etc/letsencrypt/snake-oil + openssl genrsa -out /etc/letsencrypt/snake-oil/private-key.pem 4096 + openssl req -new -key /etc/letsencrypt/snake-oil/private-key.pem -out /etc/letsencrypt/snake-oil/snake-oil.csr -nodes -subj \ + "/C=CH/ST=Solothurn/L=Solothurn/O=Home/OU=Lab/CN=${NGINX_HOST}" + openssl x509 -req -days 365 -in /etc/letsencrypt/snake-oil/snake-oil.csr -signkey /etc/letsencrypt/snake-oil/private-key.pem -out /etc/letsencrypt/snake-oil/fullchain.pem +fi + +trap "echo $(date +'%Y-%m-%d %H:%M:%S') exiting due to signal ; kill $(jobs -p)" SIGHUP SIGINT SIGTERM SIGQUIT SIGTRAP SIGABRT SIGSTOP +trap "echo $(date +'%Y-%m-%d %H:%M:%S') exiting due to error! ; exit" ERR + +nginx -g "daemon off;" & +nginx_sid=$! + +/opt/certbot-obtain.sh ${WATCH_FILE} ${LETSENCRYPT_DIR} ${CERTBOT_WWW_DIR} & +/opt/certbot-renewal.sh "12h" & +/opt/nginx-reload.sh & + +echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Sleeping indefinitely..." +wait ${nginx_sid} +echo -e "INFO: nginx has exited, exiting script!" +kill $(jobs -p) +exit 0 \ No newline at end of file diff --git a/build/nginx-reload.sh b/build/nginx-reload.sh new file mode 100644 index 0000000..b9e13a0 --- /dev/null +++ b/build/nginx-reload.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +sleep 5s + +WATCH_DIR="/etc/nginx/sites-enabled" + +echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Watching directory ${WATCH_DIR} for changes to file ${WATCH_FILE_NAME}..." + +lastReload=0 + +inotifywait -m -e CREATE -e CLOSE_WRITE "${WATCH_DIR}" | while read filename eventlist eventfile ; do + [[ $eventfile =~ ^\..* ]] && continue + now=$(date +%s) + diff=$((now-lastReload)) + if [[ ${diff} -lt 10 ]] ; then + echo -e "INFO: Ignoring duplicate action to $eventfile" + continue + fi + + echo -e "$(date +'%Y-%m-%d %H:%M:%S') INFO: Reloading nginx as changes detected in file $filename $eventlist $eventfile" + nginx -s reload +done + +exit 0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1d9fa9f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.9" + +services: + proxy: + restart: unless-stopped + image: eitch-proxy + build: ./build + container_name: eitch-proxy + ports: + - "80:80" + - "443:443" + volumes: + - "./letsencrypt:/etc/letsencrypt" + - "./ssl/dhparam.pem:/etc/ssl/dhparam.pem:ro" + - "./nginx/conf.d:/etc/nginx/conf.d" + - "./nginx/modules-enabled:/etc/nginx/modules-enabled" + - "./nginx/sites-enabled:/etc/nginx/sites-enabled" + - "./nginx/cache:/var/cache/nginx" + - "./nginx/pid:/var/run" + environment: + - TZ=Europe/Zurich + - LETS_ENCRYPT_EMAIL=eitch@eitchnet.ch + - NGINX_HOST=proxy.eitchnet.ch + - NGINX_PORT=80 + networks: + proxy-network: + ipv4_address: 10.254.0.2 +networks: + proxy-network: + external: true diff --git a/nginx/conf.d/ssl.conf b/nginx/conf.d/ssl.conf new file mode 100644 index 0000000..bb0de95 --- /dev/null +++ b/nginx/conf.d/ssl.conf @@ -0,0 +1,8 @@ +ssl_session_cache shared:SSL:50m; +ssl_session_timeout 5m; +ssl_dhparam /etc/ssl/dhparam.pem; + +ssl_prefer_server_ciphers on; +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + +ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4; \ No newline at end of file diff --git a/nginx/sites-enabled/00_default b/nginx/sites-enabled/00_default new file mode 100644 index 0000000..25d5a18 --- /dev/null +++ b/nginx/sites-enabled/00_default @@ -0,0 +1,18 @@ +server { + listen 80; + server_name localhost; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + location / { + return 301 https://$host$request_uri; + } +} +server { + listen 443 ssl; + server_name localhost; + ssl_certificate /etc/letsencrypt/snake-oil/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/snake-oil/private-key.pem; + #include /etc/nginx/ssl.conf; +} \ No newline at end of file