From 91a15d24a89add7ae80b6a67c50e7376c93f9a11 Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Thu, 8 Apr 2021 07:41:55 +0200 Subject: [PATCH] Add ALLOWED_HOSTS the proxy responds with a 421, if a host header with an invalid host name is encountered. If nothing is provided (see default .env) all hosts are allowed. Examples: ALLOWED_HOSTS="localhost:8000 127.0.0.1:8000" ALLOWED_HOSTS="some.domain.example.com" Add EXTERNAL_HTTPS_PORT. See .env for the configuration. --- README.rst | 10 ++- caddy/Caddyfile | 3 + caddy/Caddyfile.dev | 26 ++++---- caddy/entrypoint | 68 ++++++++++++++++++++- docker/.env | 33 +++++++++- docker/docker-compose.yml.m4 | 19 +++++- docker/docker-stack.yml.m4 | 19 +++++- server/openslides/users/config_variables.py | 4 +- 8 files changed, 157 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index e755862ab..d5241c2b6 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,8 @@ first and initialize all submodules:: git submodule update --init -Setup Docker images -------------------- +Setup Docker Compose +-------------------- You need to build the Docker images and have to setup some configuration. First, configure HTTPS by checking the `Using HTTPS`_ section. In this section are @@ -64,11 +64,15 @@ Afterwards, generate the configuration file:: m4 docker-compose.yml.m4 > docker-compose.yml +You can configure OpenSlides using the `.env` file. See `More settings`_. Another +hint: If you choose to deploy the default configuration, a https certificate is +needed, so make sure you have set it up beforehand. + Finally, you can start the instance using ``docker-compose``:: docker-compose up -OpenSlides is accessible on http://localhost:8000/ (or https, if configured). +OpenSlides is accessible on https://localhost/ (or https, if configured). Use can also use daemonized instance:: diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 68dcdf45f..df025ad45 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -1,4 +1,5 @@ import endpoint + import invalid_host* reverse_proxy /system/* autoupdate:8002 { flush_interval -1 @@ -15,3 +16,5 @@ import endpoint reverse_proxy client:4200 } + +import redirect* diff --git a/caddy/Caddyfile.dev b/caddy/Caddyfile.dev index 8bd43e5e3..fada40731 100644 --- a/caddy/Caddyfile.dev +++ b/caddy/Caddyfile.dev @@ -1,15 +1,15 @@ -localhost:8000 +:8000 { + reverse_proxy /system/* autoupdate:8002 { + flush_interval -1 + } -reverse_proxy /system/* autoupdate:8002 { - flush_interval -1 + @server { + path /apps/* + path /rest/* + path /server-version.txt + path /media/* + } + reverse_proxy @server server:8000 + + reverse_proxy client:4200 } - -@server { - path /apps/* - path /rest/* - path /server-version.txt - path /media/* -} -reverse_proxy @server server:8000 - -reverse_proxy client:4200 diff --git a/caddy/entrypoint b/caddy/entrypoint index eea7dd8aa..510d8cf37 100755 --- a/caddy/entrypoint +++ b/caddy/entrypoint @@ -2,15 +2,77 @@ set -e +if [[ -z "$EXTERNAL_HTTP_PORT" ]] && [[ -z "$EXTERNAL_HTTPS_PORT" ]]; then + echo "EXTERNAL_HTTP_PORT and EXTERNAL_HTTPS_PORT are not set. Aborting." + exit 1 +fi + if [[ -f "/certs/key.pem" ]] && [[ -f "/certs/cert.pem" ]]; then + certs_exists=1 +fi + +if [[ -n "$EXTERNAL_HTTPS_PORT" ]] && [[ -z "$certs_exists" ]]; then + echo "Configured https, but no certificates found. Aborting" + exit 1 +fi + +# config: https +if [[ -n "$EXTERNAL_HTTPS_PORT" ]] ; then cat <> /etc/caddy/endpoint -https://:8000 { +https://:8001 { tls /certs/cert.pem /certs/key.pem EOF echo "Configured https" -else +fi + +# config: http and no https +if [[ -n "$EXTERNAL_HTTP_PORT" ]] && [[ -z "$EXTERNAL_HTTPS_PORT" ]] ; then echo "http://:8000 {" > /etc/caddy/endpoint - echo "Configured http" + echo "Configured http only" +fi + +# config: https and additionally http -> create redirect-file +if [[ -n "$EXTERNAL_HTTP_PORT" ]] && [[ -n "$EXTERNAL_HTTPS_PORT" ]] ; then + cat <> /etc/caddy/redirect +http://:8000 { + redir https://$INSTANCE_DOMAIN{uri} +} +EOF + echo "Configured http to https redirect" +fi + +# Add allowed hosts from $ALLOWED_HOSTS +# If the variable is empty, all hosts are allowed. +# The hosts are ORed, so the request is valid, if one host matches. +# Example: ALLOWED_HOSTS="localhost:8000 127.0.0.1:8000" +# +# @invalid-host { +# not { +# header Host localhost:8000 +# header Host 127.0.0.1:8000 +# } +# } +# respond @invalid-host "Misdirected Request" 421 { +# close +# } +if [[ ! -z "$ALLOWED_HOSTS" ]]; then + cat <> /etc/caddy/invalid_host + @invalid-host { + not { +EOF + for host in $ALLOWED_HOSTS; do + echo " host $host" >> /etc/caddy/invalid_host + done + cat <> /etc/caddy/invalid_host + } + } + respond @invalid-host "Misdirected Request" 421 { + close + } +EOF + echo "Configured allowed hosts: $ALLOWED_HOSTS" +else + echo "All hosts allowed" fi exec "$@" diff --git a/docker/.env b/docker/.env index ec965b245..9536a154a 100644 --- a/docker/.env +++ b/docker/.env @@ -10,9 +10,40 @@ # General # ------- + +# The domain your OpenSlides installation in reachable. E.g. example.com or +# 127.0.0.1 for a local deployment. This domain is used when generating links +# in emails and so on, so it should be the public facing domain. The default +# is 127.0.0.1 +# If you do not have any port-changing proxies, this setting should be kept in +# sync with the EXTERNAL_*_PORTS below INSTANCE_DOMAIN= -PROJECT_STACK_NAME= + +# The schema (http or https) to use for generating public links. The default +# is https. +INSTANCE_URL_SCHEME= + +# The ports the setup binds to to listen for http/https requests. To not bind +# to http or https, leave the port empty. Behavior of port-combinations: +# - If both ports are set, the server listens to https. Additionally, a http to +# https redirect is activated on the http port. +# - If no ports are set, the https port defaults to 443. +# - If exactly one port is set, the server listens to the given port. +# If the https port is set, there must be a ssl certificate. See the README +# for more information. EXTERNAL_HTTP_PORT= +EXTERNAL_HTTPS_PORT= + +# A list of hosts, that are allowed to accept. If there is a not accepted host, +# a 421 response will be returned. +# The default is an empty list, so all hosts are accepted. +# Example with two hosts: ALLOWED_HOSTS="127.0.0.1:443 example.com" +ALLOWED_HOSTS= + +# The name for the docker stack used. +PROJECT_STACK_NAME= + +# The default registry. Defaults to "openslides". DEFAULT_DOCKER_REGISTRY= # Docker Images diff --git a/docker/docker-compose.yml.m4 b/docker/docker-compose.yml.m4 index b93304289..de9aa3908 100644 --- a/docker/docker-compose.yml.m4 +++ b/docker/docker-compose.yml.m4 @@ -42,6 +42,14 @@ ifelse(read_env(`PGNODE_3_ENABLED'), 1, `,pgnode3')') define(`PROJECT_DIR', ifdef(`PROJECT_DIR',PROJECT_DIR,.)) define(`ADMIN_SECRET_AVAILABLE', `syscmd(`test -f 'PROJECT_DIR`/secrets/adminsecret.env')sysval') define(`USER_SECRET_AVAILABLE', `syscmd(`test -f 'PROJECT_DIR`/secrets/usersecret.env')sysval') + +dnl set EXTERNAL_HTTPS_PORT to 443 if EXTERNAL_HTTPS_PORT and EXTERNAL_HTTP_PORT are empty +define( + `EXTERNAL_HTTPS_PORT', + ifelse(read_env(`EXTERNAL_HTTPS_PORT')read_env(`EXTERNAL_HTTP_PORT'),,443,read_env(`EXTERNAL_HTTPS_PORT'))dnl +) +define(`EXTERNAL_HTTP_PORT',read_env(`EXTERNAL_HTTP_PORT')) + divert(0)dnl dnl ---------------------------------------- # This configuration was created from a template file. Before making changes, @@ -75,7 +83,8 @@ x-osserver-env: &default-osserver-env ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)" ENABLE_CHAT: "ifenvelse(`ENABLE_CHAT', False)" ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)" - INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)" + INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', 127.0.0.1)" + INSTANCE_URL_SCHEME: "ifenvelse(`INSTANCE_URL_SCHEME', https)" JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)" JITSI_ROOM_PASSWORD: "ifenvelse(`JITSI_ROOM_PASSWORD',)" JITSI_ROOM_NAME: "ifenvelse(`JITSI_ROOM_NAME',)" @@ -108,11 +117,17 @@ services: - client - autoupdate - media + environment: + INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', 127.0.0.1:443)" + `EXTERNAL_HTTP_PORT': "EXTERNAL_HTTP_PORT" + `EXTERNAL_HTTPS_PORT': "EXTERNAL_HTTPS_PORT" + ALLOWED_HOSTS: "ifenvelse(`ALLOWED_HOSTS',)" networks: - front - back ports: - - "127.0.0.1:ifenvelse(`EXTERNAL_HTTP_PORT', 8000):8000" + ifelse(EXTERNAL_HTTP_PORT,,,- "127.0.0.1:EXTERNAL_HTTP_PORT:8000") + ifelse(EXTERNAL_HTTPS_PORT,,,- "127.0.0.1:EXTERNAL_HTTPS_PORT:8001") server: << : *default-osserver diff --git a/docker/docker-stack.yml.m4 b/docker/docker-stack.yml.m4 index 1b5f5377c..10abd4f27 100644 --- a/docker/docker-stack.yml.m4 +++ b/docker/docker-stack.yml.m4 @@ -42,6 +42,14 @@ ifelse(read_env(`PGNODE_3_ENABLED'), 1, `,pgnode3')') define(`PROJECT_DIR', ifdef(`PROJECT_DIR',PROJECT_DIR,.)) define(`ADMIN_SECRET_AVAILABLE', `syscmd(`test -f 'PROJECT_DIR`/secrets/adminsecret.env')sysval') define(`USER_SECRET_AVAILABLE', `syscmd(`test -f 'PROJECT_DIR`/secrets/usersecret.env')sysval') + +dnl set EXTERNAL_HTTPS_PORT to 443 if EXTERNAL_HTTPS_PORT and EXTERNAL_HTTP_PORT are empty +define( + `EXTERNAL_HTTPS_PORT', + ifelse(read_env(`EXTERNAL_HTTPS_PORT')read_env(`EXTERNAL_HTTP_PORT'),,443,read_env(`EXTERNAL_HTTPS_PORT'))dnl +) +define(`EXTERNAL_HTTP_PORT',read_env(`EXTERNAL_HTTP_PORT')) + divert(0)dnl dnl ---------------------------------------- # This configuration was created from a template file. Before making changes, @@ -74,7 +82,8 @@ x-osserver-env: &default-osserver-env ENABLE_ELECTRONIC_VOTING: "ifenvelse(`ENABLE_ELECTRONIC_VOTING', False)" ENABLE_CHAT: "ifenvelse(`ENABLE_CHAT', False)" ENABLE_SAML: "ifenvelse(`ENABLE_SAML', False)" - INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', http://example.com:8000)" + INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', 127.0.0.1)" + INSTANCE_URL_SCHEME: "ifenvelse(`INSTANCE_URL_SCHEME', https)" JITSI_DOMAIN: "ifenvelse(`JITSI_DOMAIN',)" JITSI_ROOM_PASSWORD: "ifenvelse(`JITSI_ROOM_PASSWORD',)" JITSI_ROOM_NAME: "ifenvelse(`JITSI_ROOM_NAME',)" @@ -101,11 +110,17 @@ x-pgnode-env: &default-pgnode-env services: proxy: image: PROXY_IMAGE + environment: + INSTANCE_DOMAIN: "ifenvelse(`INSTANCE_DOMAIN', 127.0.0.1:443)" + `EXTERNAL_HTTP_PORT': "EXTERNAL_HTTP_PORT" + `EXTERNAL_HTTPS_PORT': "EXTERNAL_HTTPS_PORT" + ALLOWED_HOSTS: "ifenvelse(`ALLOWED_HOSTS',)" networks: - front - back ports: - - "0.0.0.0:ifenvelse(`EXTERNAL_HTTP_PORT', 8000):8000" + ifelse(EXTERNAL_HTTP_PORT,,,- "127.0.0.1:EXTERNAL_HTTP_PORT:8000") + ifelse(EXTERNAL_HTTPS_PORT,,,- "127.0.0.1:EXTERNAL_HTTPS_PORT:8001") deploy: restart_policy: condition: on-failure diff --git a/server/openslides/users/config_variables.py b/server/openslides/users/config_variables.py index 82683e1f4..c166d72cc 100644 --- a/server/openslides/users/config_variables.py +++ b/server/openslides/users/config_variables.py @@ -77,7 +77,9 @@ def get_config_variables(): # TODO: Use Django's URLValidator here. yield ConfigVariable( name="users_pdf_url", - default_value=os.getenv("INSTANCE_DOMAIN", default="http://example.com:8000"), + default_value=os.getenv("INSTANCE_URL_SCHEME", default="https") + + "://" + + os.getenv("INSTANCE_DOMAIN", default="127.0.0.1"), label="System URL", help_text="Used for QRCode in PDF of access data.", weight=540,