diff --git a/docs/configuration/nginx.md b/docs/configuration/nginx.md index 73852075f..b8e99825c 100644 --- a/docs/configuration/nginx.md +++ b/docs/configuration/nginx.md @@ -14,6 +14,8 @@ nginx:validate [] [--clean] # Validates and optionally cleans up in ## Binding to specific addresses +> New as of 0.19.2 + By default, nginx will listen to all interfaces (`[::]` for IPv6, `0.0.0.0` for IPv4) when proxying requests to applications. This may be changed using the `bind-address-ipv4` and `bind-address-ipv6` properties. This is useful in cases where the proxying should be internal to a network or if there are multiple network interfaces that should respond with different content. ```shell @@ -32,6 +34,26 @@ dokku nginx:set node-js-app bind-address-ipv6 Users with apps that contain a custom `nginx.conf.sigil` file will need to modify the files to respect the new `NGINX_BIND_ADDRESS_IPV4` and `NGINX_BIND_ADDRESS_IPV6` variables. +## HSTS Header + +> New as of 0.20.0 + +If SSL certificates are present, HSTS will be automatically enabled. It can be toggled via `nginx:set`: + +```shell +dokku nginx:set node-js-app hsts true +dokku nginx:set node-js-app hsts false +``` + +The following options are also available via the `nginx:set` command: + +- `hsts` (type: boolean, default: `true`): Enables or disables HSTS for your application. +- `hsts-include-subdomains` (type: boolean, default: `true`): Tells the browser that the HSTS policy also applies to all subdomains of the current domain. +- `hsts-max-age` (type: integer, default: `15724800`): Time in seconds to cache HSTS configuration. +- `hsts-preload` (type: boolean, default: `false`): Tells most major web browsers to include the domain in their HSTS preload lists. + +Beware that if you enable the header and a subsequent deploy of your application results in an HTTP deploy (for whatever reason), the way the header works means that a browser will not attempt to request the HTTP version of your site if the HTTPS version fails until the max-age is reached. + ## Checking access logs You may check nginx access logs via the `nginx:access-logs` command. This assumes that app access logs are being stored in `/var/log/nginx/$APP-access.log`, as is the default in the generated `nginx.conf`. @@ -197,10 +219,6 @@ See the [default site documentation](/docs/configuration/domains.md#default-site See the [load balancer documentation](/docs/configuration/ssl.md#running-behind-a-load-balancer). -## HSTS Header - -See the [HSTS documentation](/docs/configuration/ssl.md#hsts-header). - ## SSL Configuration See the [ssl documentation](/docs/configuration/ssl.md). diff --git a/docs/configuration/ssl.md b/docs/configuration/ssl.md index b099c3fd0..0ad972ad1 100644 --- a/docs/configuration/ssl.md +++ b/docs/configuration/ssl.md @@ -113,9 +113,9 @@ dokku certs:report node-js-app --ssl-enabled ## HSTS Header -The [HSTS header](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) is an HTTP header that can inform browsers that all requests to a given site should be made via HTTPS. Dokku does not, by default, enable this header. It is thus left up to you, the user, to enable it for your site. +The [HSTS header](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) is an HTTP header that can inform browsers that all requests to a given site should be made via HTTPS. Dokku does not enables this header by default -Beware that if you enable the header and a subsequent deploy of your application results in an HTTP deploy (for whatever reason), the way the header works means that a browser will not attempt to request the HTTP version of your site if the HTTPS version fails. +See the [NGINX HSTS documentation](/docs/configuration/nginx.md#hsts-header) for more information. ## HTTP/2 support diff --git a/plugins/nginx-vhosts/functions b/plugins/nginx-vhosts/functions index 9dc232772..dad1cdc9b 100755 --- a/plugins/nginx-vhosts/functions +++ b/plugins/nginx-vhosts/functions @@ -317,6 +317,7 @@ nginx_build_config() { done local PROXY_UPSTREAM_PORTS="$(echo "$PROXY_UPSTREAM_PORTS" | xargs)" + local SSL_INUSE= local NONSSL_VHOSTS=$(get_app_domains "$APP") local NOSSL_SERVER_NAME=$(echo "$NONSSL_VHOSTS" | xargs) if is_ssl_enabled "$APP"; then @@ -412,6 +413,8 @@ nginx_build_config() { dokku_log_info1 "Creating $SCHEME nginx.conf" mv "$NGINX_CONF" "$DOKKU_ROOT/$APP/nginx.conf" + fn-nginx-vhosts-manage-hsts "$APP" "$SSL_INUSE" + plugn trigger nginx-pre-reload "$APP" "$DOKKU_APP_LISTEN_PORT" "$DOKKU_APP_LISTEN_IP" dokku_log_verbose "Reloading nginx" diff --git a/plugins/nginx-vhosts/install b/plugins/nginx-vhosts/install index 6ff611a04..882c8e87c 100755 --- a/plugins/nginx-vhosts/install +++ b/plugins/nginx-vhosts/install @@ -2,9 +2,12 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/internal-functions" +fn-plugin-property-setup "nginx" + NGINX_BIN="$(fn-nginx-vhosts-nginx-location)" NGINX_ROOT="/etc/nginx" NGINX_LOG_ROOT="/var/log/nginx" diff --git a/plugins/nginx-vhosts/internal-functions b/plugins/nginx-vhosts/internal-functions index 8efceb748..1210bc823 100755 --- a/plugins/nginx-vhosts/internal-functions +++ b/plugins/nginx-vhosts/internal-functions @@ -37,6 +37,10 @@ cmd-nginx-report-single() { local flag_map=( "--nginx-bind-address-ipv4: $(fn-plugin-property-get-default "nginx" "$APP" "bind-address-ipv4" "")" "--nginx-bind-address-ipv6: $(fn-plugin-property-get-default "nginx" "$APP" "bind-address-ipv6" "::")" + "--nginx-hsts: $(fn-plugin-property-get-default "nginx" "$APP" "hsts" "true")" + "--nginx-hsts-include-subdomains: $(fn-plugin-property-get-default "nginx" "$APP" "hsts-include-subdomains" "true")" + "--nginx-hsts-max-age: $(fn-plugin-property-get-default "nginx" "$APP" "hsts-max-age" "15724800")" + "--nginx-hsts-preload: $(fn-plugin-property-get-default "nginx" "$APP" "hsts-preload" "false")" ) if [[ -z "$INFO_FLAG" ]]; then @@ -65,6 +69,38 @@ cmd-nginx-report-single() { fi } +fn-nginx-vhosts-manage-hsts() { + declare APP="$1" SSL_ENABLED="$2" + local HSTS="$(fn-plugin-property-get-default "nginx" "$APP" "nginx-hsts" "true")" + local HSTS_INCLUDE_SUBDOMAINS="$(fn-plugin-property-get-default "nginx" "$APP" "nginx-hsts-include-subdomains" "true")" + local HSTS_MAX_AGE="$(fn-plugin-property-get-default "nginx" "$APP" "nginx-hsts-max-age" "15724800")" + local HSTS_PRELOAD="$(fn-plugin-property-get-default "nginx" "$APP" "nginx-hsts-preload" "false")" + local NGINX_HSTS_CONF="$DOKKU_ROOT/$APP/nginx.conf.d/hsts.conf" + local HSTS_TEMPLATE="$PLUGIN_AVAILABLE_PATH/nginx-vhosts/templates/hsts.conf.sigil" + + if [[ "$HSTS" == "false" ]] || [[ "$SSL_ENABLED" != "true" ]]; then + rm -rf "NGINX_HSTS_CONF" + return + fi + + dokku_log_verbose_quiet "Enabling HSTS" + local HSTS_HEADERS="" + if [[ -n "$HSTS_MAX_AGE" ]]; then + HSTS_HEADERS="max-age=$HSTS_MAX_AGE" + fi + + if [[ "$HSTS_INCLUDE_SUBDOMAINS" == "true" ]]; then + HSTS_HEADERS+="; includeSubdomains" + fi + + if [[ "$HSTS_PRELOAD" == "true" ]]; then + HSTS_HEADERS+="; preload" + fi + + mkdir -p "$DOKKU_ROOT/$APP/nginx.conf.d" + sigil -f "$HSTS_TEMPLATE" HSTS_HEADERS="$HSTS_HEADERS" | cat -s >"$NGINX_HSTS_CONF" +} + fn-nginx-vhosts-uses-openresty() { declare desc="returns whether openresty is in use or not" diff --git a/plugins/nginx-vhosts/subcommands/set b/plugins/nginx-vhosts/subcommands/set index 82eca4eaf..6e9a688a8 100755 --- a/plugins/nginx-vhosts/subcommands/set +++ b/plugins/nginx-vhosts/subcommands/set @@ -9,12 +9,12 @@ nginx-set-cmd() { local cmd="nginx:set" argv=("$@") [[ ${argv[0]} == "$cmd" ]] && shift 1 declare APP="$1" KEY="$2" VALUE="$3" - local VALID_KEYS=("bind-address-ipv4" "bind-address-ipv6") + local VALID_KEYS=("bind-address-ipv4" "bind-address-ipv6" "hsts" "hsts-include-subdomains" "hsts-preload" "hsts-max-age") [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" [[ -z "$KEY" ]] && dokku_log_fail "No key specified" if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then - dokku_log_fail "Invalid key specified, valid keys include: bind-address-ipv4, bind-address-ipv6" + dokku_log_fail "Invalid key specified, valid keys include: bind-address-ipv4, bind-address-ipv6, hsts, hsts-include-subdomains, hsts-preload, hsts-max-age" fi if [[ -n "$VALUE" ]]; then diff --git a/plugins/nginx-vhosts/templates/hsts.conf.sigil b/plugins/nginx-vhosts/templates/hsts.conf.sigil new file mode 100644 index 000000000..bfee34e40 --- /dev/null +++ b/plugins/nginx-vhosts/templates/hsts.conf.sigil @@ -0,0 +1 @@ +add_header Strict-Transport-Security "{{ $.HSTS_HEADERS }}" always; diff --git a/tests/unit/40_nginx-vhosts_2.bats b/tests/unit/40_nginx-vhosts_2.bats index cd0857f00..e762b4e74 100644 --- a/tests/unit/40_nginx-vhosts_2.bats +++ b/tests/unit/40_nginx-vhosts_2.bats @@ -107,6 +107,88 @@ assert_error_log() { assert_failure } +@test "(nginx-vhosts) nginx:set hsts" { + local HSTS_CONF="/home/dokku/${TEST_APP}/nginx.conf.d/hsts.conf" + + run deploy_app + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Enabling HSTS" + + run /bin/bash -c "test -f $HSTS_CONF" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "cat $HSTS_CONF | grep includeSubdomains" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "cat $HSTS_CONF | grep 'max-age=15724800'" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "cat $HSTS_CONF | grep preload" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "dokku nginx:set $TEST_APP hsts-include-subdomains false" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku nginx:build-config $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "cat $HSTS_CONF | grep includeSubdomains" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "dokku nginx:set $TEST_APP hsts-max-age 120" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku nginx:build-config $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "cat $HSTS_CONF | grep 'max-age=120'" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku nginx:set $TEST_APP hsts-preload true" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku nginx:build-config $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "cat $HSTS_CONF | grep preload" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku nginx:set $TEST_APP hsts false" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku nginx:build-config $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Enabling HSTS" 0 + + run /bin/bash -c "test -f $DOKKU_ROOT/$TEST_APP/nginx.conf.d/hsts.conf" + echo "output: $output" + echo "status: $status" + assert_failure +} + @test "(nginx-vhosts) nginx:set bind-address" { run deploy_app echo "output: $output"