From b14fc1f0c13fd14f3fffddfa27fadddad6d3e504 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 5 Apr 2020 19:08:21 -0400 Subject: [PATCH] [add] initial commit --- certbot/Dockerfile | 28 +++++ certbot/files/domains.env | 2 + certbot/files/entrypoint.sh | 230 ++++++++++++++++++++++++++++++++++++ docker-compose.yml | 90 ++++++++++++++ kweb/Dockerfile | 21 ++++ kweb/files/index.html | 6 + kweb/files/nginx.conf | 31 +++++ proxy/Dockerfile | 29 +++++ proxy/files/entrypoint.sh | 141 ++++++++++++++++++++++ proxy/files/index.html | 6 + proxy/files/nginx.conf | 68 +++++++++++ 11 files changed, 652 insertions(+) create mode 100644 certbot/Dockerfile create mode 100644 certbot/files/domains.env create mode 100644 certbot/files/entrypoint.sh create mode 100644 docker-compose.yml create mode 100644 kweb/Dockerfile create mode 100644 kweb/files/index.html create mode 100644 kweb/files/nginx.conf create mode 100644 proxy/Dockerfile create mode 100644 proxy/files/entrypoint.sh create mode 100644 proxy/files/index.html create mode 100644 proxy/files/nginx.conf diff --git a/certbot/Dockerfile b/certbot/Dockerfile new file mode 100644 index 0000000..545da18 --- /dev/null +++ b/certbot/Dockerfile @@ -0,0 +1,28 @@ +FROM certbot/certbot:v1.3.0 + +ARG USER_UUID=12000 +ARG USER_GUID=12000 +ARG GROUP_NAME=certbot +ARG USER_NAME=certbot + +# http://linuxcommand.org/lc3_man_pages/seth.html +RUN set -eux; \ + addgroup -g $USER_GUID $GROUP_NAME; \ + adduser -u $USER_UUID -G $GROUP_NAME -s /sbin/nologin -D $USER_NAME; + +# create temp dir for service +RUN mkdir -p /service/temp \ + && chown -R $USER_NAME:$GROUP_NAME /service/temp + +# create and set permission for proxy mounts +RUN mkdir -p /service/html \ + && chown -R $USER_NAME:$GROUP_NAME /service/html \ + && mkdir -p /service/ssl \ + && chown -R $USER_NAME:$GROUP_NAME /service/ssl + +COPY ./files/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +USER $USER_NAME + +ENTRYPOINT [ "/entrypoint.sh" ] \ No newline at end of file diff --git a/certbot/files/domains.env b/certbot/files/domains.env new file mode 100644 index 0000000..20fc242 --- /dev/null +++ b/certbot/files/domains.env @@ -0,0 +1,2 @@ +DOMAINS=example.com www api test\n example.dev www api test +SERVER=kweb\n test diff --git a/certbot/files/entrypoint.sh b/certbot/files/entrypoint.sh new file mode 100644 index 0000000..07a94b5 --- /dev/null +++ b/certbot/files/entrypoint.sh @@ -0,0 +1,230 @@ +#!/bin/sh + +# configuration +web_root_path="/service/html" +cert_root_path="/service/ssl" +temp_path="/service/temp" +cert_bot_path="/etc/letsencrypt" +server_retries=10 +log_file="" # leave empty for stdout + + +# variables +local_webserver="http://$(ip route show | awk '/default/ {print $3}')" +domain_file="$temp_path/domains.txt" +certs_updated="$cert_root_path/.updated" + +#functions +function log() +{ + local time=$(date +%F_%T) + local prefix="LOG" + case $1 in + ERR | ERROR) + prefix="ERR" + shift + ;; + WAR | WARNING) + prefix="WAR" + shift + ;; + esac + local func=$1; shift + # remove any \n + #local text=$(echo "$@"| tr '\n' '#' | sed 's|#|\\n |g') + case "$log_file" in + "") + echo "[$time]:[$prefix]:[$0][$func]: $@" 2>&1 + ;; + *) + echo "[$time]:[$prefix]:[$0][$func]: $@" >> log_path + ;; + esac +} + +function env_domain_file() +{ + log env_domain_file $(echo "ENV DOMAINS: $DOMAINS" | tr '\n' '#' | sed 's|#|\\n |g') + echo -e "$DOMAINS" > $1 +} + +function verify_domain() +{ + # TODO: create and verify token in web_root_path via curl + if nslookup $1 > /dev/null 2>&1; then + # domain exists + return 0 + else + # false + return 1 + fi +} + +function read_file() +{ + local func=$1 + local file=$2 + local result=0 + + while read -r line || [[ -n "$line" ]]; do + # skip empty lines if READ_EMPTY_LINES is not set + [ -z ${READ_EMPTY_LINES+x} ] && [ "${#line}" -lt 1 ] && continue + log read_file "LINE: $line" + log read_file "FUNC START [$func]" + if $func $line; then + log read_file "FUNC END [$func]" + else + result=1 + log ERROR read_file "FUNC END [$func]" + fi + done < "$file" + + return $result +} + +function init_cert() +{ + local domain=$1 + local cert_path="$cert_root_path/$domain" + if [[ ! -f "$cert_path/fullchain.pem" ]]; then + log init_cert "Creating temporary cert for [$domain]" + mkdir -p "$cert_path" + openssl genrsa -out "$cert_path/privkey.pem" 4096 + openssl req -new -key "$cert_path/privkey.pem" -out "$temp_path/$domain.csr" -nodes -subj \ + "/C=PT/ST=World/L=World/O=${domain}/OU=temp lda/CN=${domain}/emailAddress=contact@${domain}" + openssl x509 -req -days 365 -in "$temp_path/$domain.csr" -signkey "$cert_path/privkey.pem" -out "$cert_path/fullchain.pem" + fi +} + +function copy_cert() +{ + local domain=$1 + local cert_path="$cert_root_path/$domain" + if [[ -f "$cert_bot_path/live/$domain/privkey.pem" ]]; then + cp "$cert_bot_path/live/$domain/privkey.pem" "$cert_path/privkey.pem" + cp "$cert_bot_path/live/$domain/fullchain.pem" "$cert_path/fullchain.pem" + return 0 + else + log ERROR copy_cert "No key exists at "$cert_bot_path/live/$domain/privkey.pem" for [$domain]" + return 1 + fi +} + +function verify_webserver() +{ + wget -q -O /dev/null "$1/check/" > /dev/null 2>&1 +} + +function verify_local_webserver() +{ + local domain=$1 + # todo check if configured for domain + wget -q -O /dev/null "$local_webserver/check/" > /dev/null 2>&1 +} + +function get_cert() +{ + local root_domain=$1 + local domains=$2 # comma seperated list, first is file path + local retries=0 + local webserver_exists=0 + verify_local_webserver $root_domain && webserver_exists=1 + while [ ! $webserver_exists -eq 1 ] && [ "$retries" -lt "$server_retries" ]; do + retries=$(( retries+1 )) + log get_cert WARNING "Webserver for [$root_domain] not reachable, trying again in 1s" + sleep 1 + verify_local_webserver $root_domain && webserver_exists=1 + done + + if [ $webserver_exists -eq 1 ] ; then + log get_cert "Getting cert for [$root_domain] with [$domains]" + # certbotx certonly \ + # --config-dir "$cert_bot_path" \ + # --agree-tos \ + # --domains "$domains" \ + # --email "contact@${root_domain}" \ + # --no-eff-email \ + # --expand \ + # --noninteractive \ + # --webroot \ + # --webroot-path "$web_root_path" \ + # $OPTIONS || true + else + log get_cert ERROR "Webserver for [$root_domain] not reachable after $server_retries retries" + return 1 + fi +} + +function domain_init() +{ + if [ "$1" == "" ]; then + log domain_init "Skipping empty input" + return 0 + fi + local domain=$1; + # don't really care at this point if the domain exists + # if ! verify_domain $domain; then + # log ERROR domain_init "Root domain [$domain] does not exist" + # return 1 + # fi + init_cert $domain +} + +function domain_handler() +{ + if [ "$1" == "" ]; then + log domain_handler "Skipping empty input" + return 0 + fi + local domain=$1; shift + local subs=$@ + local domain_string="$domain" + log domain_handler "Checking root domain [$domain]" + if ! verify_domain $domain; then + log ERROR domain_handler "Root domain [$domain] does not exist" + return 1 + fi + for sub in $subs ; do + local subdomain="$sub.$domain" + log domain_handler "Checking sub domain [$subdomain]" + if ! verify_domain $subdomain; then + log WARNING domain_handler "Sub domain [$subdomain] does not exist" + else + domain_string="$domain_string,$subdomain" + fi + done + init_cert $domain + if ! get_cert $domain $domain_string; then + log ERROR domain_handler "No cert generated by certbot for [$domain]" + return 1 + fi + copy_cert $domain +} + +function main() +{ + intervall="${1-12h}" + while :; do + log main "Starting intervall" + + # load env variable with domains + env_domain_file "$domain_file" + + # create temp certs for domains if none exists + # nginx need certs to start + read_file domain_init "$domain_file" + + # get or renew domain certs + # requires nginx to be started + # timout is set via $server_retries (default 10s) + read_file domain_handler "$domain_file" + + # indicated that certs have been updated to nginx + touch "$certs_updated" + log main "Ending intervall" + log main "Next interval in $intervall" + sleep "$intervall" + done +} + +main $@ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fe56d3f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,90 @@ + +version: '3.7' + +services: + proxy: + build: + context: ./proxy + args: + USER_UID: ${UID_PROXY:-12001} + GROUP_UID: ${UID_PROXY:-12001} + env_file: + - ./certbot/files/domains.env + restart: always + image: proxy + container_name: proxy + ports: + - "80:8080" + - "443:8443" + restart: always + networks: + - service_kweb + volumes: + # cert volume + - proxy_ssl:/service/ssl:ro + # /.well-known/acme-challenge/ volume + - proxy_certbot:/service/html/certbot:ro + + certbot: + depends_on: + - proxy + build: + context: ./certbot + args: + USER_UID: ${UID_CERTBOT:-12002} + GROUP_UID: ${UID_PROXY:-12002} + env_file: + - ./certbot/files/domains.env + restart: always + image: certbot + container_name: certbot + restart: always + volumes: + # cert volume + - proxy_ssl:/service/ssl:rw + # /.well-known/acme-challenge/ volume (webroot) + - proxy_certbot:/service/html:rw + # certbot directories + - certbot_ssl:/etc/letsencrypt:rw + - certbot_work:/var/lib/letsencrypt:rw + - certbot_logs:/var/log/letsencrypt:rw + + # TODO: put this in its own docker-compose + # as the proxy does not require it + kweb: + build: + context: ./kweb + args: + USER_UID: ${UID_KWEB:-12003} + GROUP_UID: ${UID_KWEB:-12003} + restart: always + image: kweb + container_name: kweb + restart: always + networks: + - service_kweb + volumes: + - service_kweb:/service/html:ro + +volumes: + # proxy volumes + proxy_ssl: + name: proxy_ssl + proxy_certbot: + name: proxy_certbot + proxy_www: + name: proxy_www + # certbot volumes + certbot_ssl: + name: certbot_ssl + certbot_work: + name: certbot_work + certbot_logs: + name: certbot_logs + # service volumes + service_kweb: + name: service_kweb + +networks: + service_kweb: + name: service_kweb \ No newline at end of file diff --git a/kweb/Dockerfile b/kweb/Dockerfile new file mode 100644 index 0000000..f52e7bc --- /dev/null +++ b/kweb/Dockerfile @@ -0,0 +1,21 @@ +FROM nginx:1.17.9-alpine + +ARG USER_UUID=12000 +ARG USER_GUID=12000 +ARG GROUP_NAME=proxy +ARG USER_NAME=proxy + +RUN set -eux; \ + addgroup -g $USER_GUID $GROUP_NAME; \ + adduser -u $USER_UUID -G $GROUP_NAME -s /sbin/nologin -D $USER_NAME; + +# create temp dir for service +RUN mkdir -p /service/temp \ + && chown -R $USER_NAME:$GROUP_NAME /service/temp + +# copy configuration and entrypoint.sh +COPY ./files/nginx.conf /etc/nginx/nginx.conf + +COPY ./files/index.html /service/html/index.html + +USER $USER_NAME diff --git a/kweb/files/index.html b/kweb/files/index.html new file mode 100644 index 0000000..df54704 --- /dev/null +++ b/kweb/files/index.html @@ -0,0 +1,6 @@ + + + kweb + + online + diff --git a/kweb/files/nginx.conf b/kweb/files/nginx.conf new file mode 100644 index 0000000..9497bc9 --- /dev/null +++ b/kweb/files/nginx.conf @@ -0,0 +1,31 @@ +worker_processes 1; + +events { worker_connections 1024; } + +# required for non root +pid /tmp/nginx.pid; + +http { + + # required for non root + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + client_max_body_size 1m; + + server { + listen 8080 default_server; + + # prevent redirecting to 8080 port + # (e.g. when trailing slash is missed) + port_in_redirect off; + + location / { + root /service/html; + } + } + +} diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..4d5c962 --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,29 @@ +FROM nginx:1.17.9-alpine + +ARG USER_UUID=12000 +ARG USER_GUID=12000 +ARG GROUP_NAME=proxy +ARG USER_NAME=proxy + +RUN set -eux; \ + addgroup -g $USER_GUID $GROUP_NAME; \ + adduser -u $USER_UUID -G $GROUP_NAME -s /sbin/nologin -D $USER_NAME; + +# create temp dir for service +RUN mkdir -p /service/temp \ + && chown -R $USER_NAME:$GROUP_NAME /service/temp + +# install required packages +RUN apk --no-cache add inotify-tools + +# copy configuration and entrypoint.sh +COPY ./files/nginx.conf /etc/nginx/nginx.conf +COPY ./files/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# copy check file for port 80 +COPY ./files/index.html /service/html/check/index.html + +USER $USER_NAME + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/proxy/files/entrypoint.sh b/proxy/files/entrypoint.sh new file mode 100644 index 0000000..b629cc0 --- /dev/null +++ b/proxy/files/entrypoint.sh @@ -0,0 +1,141 @@ +#!/bin/sh + +# configuration +web_root_path="/service/html" +cert_root_path="/service/ssl" +temp_path="/service/temp" +nginx_pid="/tmp/nginx.pid" +server_retries=10 +log_file="" # leave empty for stdout + +# variables +domain_file="$temp_path/domains.txt" +certs_updated="$cert_root_path/.updated" + +#functions +function log() +{ + local time=$(date +%F_%T) + local prefix="LOG" + case $1 in + ERR | ERROR) + prefix="ERR" + shift + ;; + WAR | WARNING) + prefix="WAR" + shift + ;; + esac + local func=$1; shift + # remove any \n + #local text=$(echo "$@"| tr '\n' '#' | sed 's|#|\\n |g') + case "$log_file" in + "") + echo "[$time]:[$prefix]:[$0][$func]: $@" 2>&1 + ;; + *) + echo "[$time]:[$prefix]:[$0][$func]: $@" >> log_path + ;; + esac +} + +function var_to_file() +{ + local var=$1 + local file=$2 + log var_to_file "ENV VAR -> [$file]: $var" + echo -e "$var" > $file +} + +function env_domain_file() +{ + log env_domain_file "ENV DOMAINS: $DOMAINS" + echo -e "$DOMAINS" > $1 +} + +function read_file() +{ + local func=$1 + local file=$2 + local result=0 + + while read -r line || [[ -n "$line" ]]; do + # skip empty lines if READ_EMPTY_LINES is not set + [ -z ${READ_EMPTY_LINES+x} ] && [ "${#line}" -lt 1 ] && continue + log read_file "LINE: $line" + log read_file "FUNC START [$func]" + if $func $line; then + log read_file "FUNC END [$func]" + else + result=1 + log ERROR read_file "FUNC END [$func]" + fi + done < "$file" + + return $result +} + +function cert_exists() +{ + local domain=$1 + local cert_path="$cert_root_path/$domain" + [ -f "$cert_path/fullchain.pem" ] +} + +function certs_daemon() +{ + ( + # wait for nginx to start + while [ ! -f "$nginx_pid" ]; do + log certs_daemon "Waiting for nginx to start" + sleep 1 + done + + # wait for inital cert update + while [ ! -f "$certs_updated" ]; do + log certs_daemon "Waiting for initial cert update" + sleep 1 + done + + # reload nginx + nginx -s reload + + # cert watcher loop + while inotifywait -e attrib "$certs_updated"; do + log certs_daemon "Certs updated, reloading nginx" + nginx -s reload + done + ) & +} + +function nginx_handler() +{ + local retries=0 + + # start the daemon (it forks itself) + certs_daemon + + # wait for certs to exist before starting nginx + # just have nginx crash after n retries (default 10) + while ! read_file cert_exists "$domain_file" && [ "$retries" -lt "$server_retries" ]; do + log nginx_handler "Waiting for certs to be created" + retries=$(( retries+1 )) + sleep 1 + done + + # starting nginx in foreground + log nginx_handler "Starting nginx in foreground" + nginx -g "daemon off;" + +} + +function main() +{ + log main "Starting nginx container" + # TODO: load domains from nginx.conf + env_domain_file "$domain_file" + nginx_handler +} + +main $@ \ No newline at end of file diff --git a/proxy/files/index.html b/proxy/files/index.html new file mode 100644 index 0000000..4631879 --- /dev/null +++ b/proxy/files/index.html @@ -0,0 +1,6 @@ + + + check + + online + diff --git a/proxy/files/nginx.conf b/proxy/files/nginx.conf new file mode 100644 index 0000000..df961fa --- /dev/null +++ b/proxy/files/nginx.conf @@ -0,0 +1,68 @@ +worker_processes 1; + +events { worker_connections 1024; } + +# required for non root +pid /tmp/nginx.pid; + +http { + + # required for non root + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + client_max_body_size 1m; + + server { + listen 8080 default_server; + + # prevent redirecting to 8080 port + # (e.g. when trailing slash is missed) + port_in_redirect off; + + # letsencrypt + location /.well-known/acme-challenge/ { + root /service/html/certbot; + } + + location /check { + # set alias (instead of root) to not append /check to path + alias /service/html/check; + } + + location / { + return 302 https://$host$request_uri; + } + } + + server { + listen 8443 ssl; + + # prevent redirecting to 8080 port + # (e.g. when trailing slash is missed) + port_in_redirect off; + + server_name example.com www.example.com localhost; + + ssl_certificate /service/ssl/example.com/fullchain.pem; + ssl_certificate_key /service/ssl/example.com/privkey.pem; + + location / { + # allow for upstream to be offline by setting + # docker dns resolver and a variable for the server + resolver 127.0.0.11 valid=30s; + set $kweb kweb; + proxy_pass http://$kweb:8080; + + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + } + +}