diff --git a/Dockerfile b/Dockerfile index 6b145fd1..97e1855d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # docker run -it tellform-prod FROM phusion/baseimage:0.9.19 -MAINTAINER David Baldwynn +MAINTAINER Arielle Baldwynn # Install Utilities RUN apt-get update -q \ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..f51c2573 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3" +services: + redis: + restart: always + image: redis + volumes: + - "${PWD}/.env:/opt/tellform/.env" + networks: + - back-tier + mongo: + restart: always + image: mongo + volumes: + - "$ROOT/mongo:/data" + networks: + - back-tier + tellform: + build: + context: . + env_file: + - .env + volumes: + # - "${PWD}/.env:/opt/tellform/.env" + - .:/opt/tellform + links: + - mongo + - redis + ports: + - "5000:5000" + # - "20523:20523" + depends_on: + - mongo + - redis + networks: + - back-tier + web: + # image: tellform/nginx:stable + build: + context: ./nginx + # image: nginx:1.13 + restart: always + ports: + - "80:80" + - "443:443" + - "20523:20523" + env_file: + - .env + volumes: + - "$ROOT/certs:/certs" + # - ./nginx/conf.d:/etc/nginx/conf.d + networks: + - back-tier + +networks: + back-tier: + driver: bridge + diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..3184ee05 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:edge +RUN apk add --no-cache nginx certbot openssl python py-jinja2 + +COPY *.py / +COPY conf /conf + +RUN chmod +x /start.py +RUN chmod +x /letsencrypt.py +RUN chmod +x /config.py + +CMD /start.py diff --git a/nginx/conf/nginx.conf b/nginx/conf/nginx.conf new file mode 100644 index 00000000..42385be7 --- /dev/null +++ b/nginx/conf/nginx.conf @@ -0,0 +1,116 @@ +# Basic configuration +user nginx; +worker_processes 1; +error_log /dev/stderr info; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + # Standard HTTP configuration with slight hardening + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /dev/stdout; + sendfile on; + keepalive_timeout 65; + server_tokens off; + + #Websockets Server + server { + + {% if NODE_ENV == "development" %} + listen {{SOCKET_PORT}}; + {% else %} + listen 80; + listen [::]:80; + server_name {{ SOCKETS_URL }}; + + # Only enable HTTPS if TLS is enabled with no error + {% if TLS and not TLS_ERROR %} + listen 443 ssl; + listen [::]:443 ssl; + + include /etc/nginx/tls.conf; + add_header Strict-Transport-Security max-age=15768000; + + if ($scheme = http) { + return 301 https://$host$request_uri; + } + {% endif %} + + {% endif %} + + location / { + proxy_pass http://tellform:20523; + proxy_read_timeout 90; + + 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-Proto $scheme; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + {% if TLS and not TLS_ERROR %} + proxy_set_header X-Forwarded-Proto https; + {% endif %} + } + + {% if TLS_FLAVOR == 'letsencrypt' %} + location ^~ /.well-known/acme-challenge/ { + proxy_pass http://127.0.0.1:8008; + } + {% endif %} + } + + server { + #Add server_name for per-user subdomains + {% if SUBDOMAINS_DISABLED == "FALSE" %} + server_name {{BASE_URL}} {{SUBDOMAIN_URL}}; + {% else %} + server_name {{BASE_URL}}; + {% endif %} + + listen 80; + listen [::]:80; + + # Only enable HTTPS if TLS is enabled with no error + {% if TLS and not TLS_ERROR %} + listen 443 ssl; + listen [::]:443 ssl; + + include /etc/nginx/tls.conf; + add_header Strict-Transport-Security max-age=15768000; + + if ($scheme = http) { + return 301 https://$host$request_uri; + } + {% endif %} + + root /usr/share/nginx/html; + index index.html index.htm; + + location / { + proxy_pass http://tellform:5000; + 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 ; + + {% if TLS and not TLS_ERROR %} + proxy_set_header X-Forwarded-Proto https; + {% endif %} + } + + {% if TLS_FLAVOR == 'letsencrypt' %} + location ^~ /.well-known/acme-challenge/ { + proxy_pass http://127.0.0.1:8008; + } + {% endif %} + } +} diff --git a/nginx/conf/tls.conf b/nginx/conf/tls.conf new file mode 100644 index 00000000..af2d9587 --- /dev/null +++ b/nginx/conf/tls.conf @@ -0,0 +1,7 @@ +ssl_protocols TLSv1.1 TLSv1.2; +ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384'; +ssl_prefer_server_ciphers on; +ssl_session_timeout 10m; +ssl_certificate {{ TLS[0] }}; +ssl_certificate_key {{ TLS[1] }}; +ssl_dhparam /certs/dhparam.pem; \ No newline at end of file diff --git a/nginx/config.py b/nginx/config.py new file mode 100644 index 00000000..eca39ce3 --- /dev/null +++ b/nginx/config.py @@ -0,0 +1,26 @@ +#!/usr/bin/python + +import jinja2 +import os + +convert = lambda src, dst, args: open(dst, "w").write(jinja2.Template(open(src).read()).render(**args)) + +args = os.environ.copy() + +# TLS configuration +args["TLS"] = { + "cert": ("/certs/cert.pem", "/certs/key.pem"), + "letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem", + "/certs/letsencrypt/live/mailu/privkey.pem"), + "notls": None +}[args["TLS_FLAVOR"]] + +if args["TLS"] and not all(os.path.exists(file_path) for file_path in args["TLS"]): + print("Missing cert or key file, disabling TLS") + args["TLS_ERROR"] = "yes" + + +# Build final configuration paths +convert("/conf/tls.conf", "/etc/nginx/tls.conf", args) +convert("/conf/nginx.conf", "/etc/nginx/nginx.conf", args) +os.system("nginx -s reload") \ No newline at end of file diff --git a/nginx/letsencrypt.py b/nginx/letsencrypt.py new file mode 100644 index 00000000..cb5a098d --- /dev/null +++ b/nginx/letsencrypt.py @@ -0,0 +1,29 @@ +#!/usr/bin/python + +import os +import time +import subprocess + + +command = [ + "certbot", + "-n", "--agree-tos", # non-interactive + "-d", os.environ["HOSTNAMES"], + "-m", "{}@{}".format(os.environ["POSTMASTER"], os.environ["DOMAIN"]), + "certonly", "--standalone", + "--server", "https://acme-v02.api.letsencrypt.org/directory", + "--cert-name", "tellform", + "--preferred-challenges", "http", "--http-01-port", "8008", + "--keep-until-expiring", + "--rsa-key-size", "4096", + "--config-dir", "/certs/letsencrypt", + "--post-hook", "./config.py" +] + +# Wait for nginx to start +time.sleep(5) + +# Run certbot every hour +while True: + subprocess.call(command) + time.sleep(3600) diff --git a/nginx/start.py b/nginx/start.py new file mode 100644 index 00000000..4a1946b9 --- /dev/null +++ b/nginx/start.py @@ -0,0 +1,25 @@ +#!/usr/bin/python + +import os +import subprocess + +#Set default port +if not os.environ["PORT"]: + os.environ["PORT"] = "5000" + +#Set default sockets port +if not os.environ["SOCKET_PORT"]: + os.environ["SOCKET_PORT"] = "20523" + +# Actual startup script +if not os.path.exists("/certs/dhparam.pem") and os.environ["TLS_FLAVOR"] != "notls": + os.system("openssl dhparam -out /certs/dhparam.pem 2048") + +if os.environ["TLS_FLAVOR"] == "letsencrypt": + subprocess.Popen(["/letsencrypt.py"]) +elif os.environ["TLS_FLAVOR"] == "cert": + if not os.path.exists("/certs/cert.pem"): + os.system("openssl req -newkey rsa:2048 -x509 -keyout /certs/key.pem -out /certs/cert.pem -days 365 -nodes -subj '/C=NA/ST=None/L=None/O=None/CN=" + os.environ["BASE_URL"] + "'") + +subprocess.call(["/config.py"]) +os.execv("/usr/sbin/nginx", ["nginx", "-g", "daemon off;"]) \ No newline at end of file