feat: Enable HSTS by default

This enables the HSTS header by default when an SSL certificate is in use. HSTS options can also be managed via the nginx:set command, which also exposes the ability to disable HSTS for your application.
This commit is contained in:
Jose Diaz-Gonzalez
2020-02-01 21:16:39 -05:00
parent d42fb02136
commit 73e7ff7b18
8 changed files with 151 additions and 8 deletions

View File

@@ -14,6 +14,8 @@ nginx:validate [<app>] [--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).

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -0,0 +1 @@
add_header Strict-Transport-Security "{{ $.HSTS_HEADERS }}" always;

View File

@@ -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"