diff --git a/docs/configuration/nginx.md b/docs/configuration/nginx.md index afc4972fb..73852075f 100644 --- a/docs/configuration/nginx.md +++ b/docs/configuration/nginx.md @@ -6,10 +6,32 @@ Dokku uses nginx as its server for routing requests to specific applications. By nginx:access-logs [-t] # Show the nginx access logs for an application (-t follows) nginx:build-config # (Re)builds nginx config for given app nginx:error-logs [-t] # Show the nginx error logs for an application (-t follows) +nginx:report [] [] # Displays a nginx report for one or more apps +nginx:set () # Set or clear an nginx property for an app nginx:show-conf # Display app nginx config nginx:validate [] [--clean] # Validates and optionally cleans up invalid nginx configurations ``` +## Binding to specific addresses + +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 +dokku nginx:set node-js-app bind-address-ipv4 127.0.0.1 +dokku nginx:set node-js-app bind-address-ipv6 ::1 +``` + +This may be reverted by setting an empty bind address. + +```shell +dokku nginx:set node-js-app bind-address-ipv4 +dokku nginx:set node-js-app bind-address-ipv6 +``` + +> Warning: Validation is not performed on either value. + +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. + ## 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`. diff --git a/docs/configuration/ssl.md b/docs/configuration/ssl.md index fd82264ba..b099c3fd0 100644 --- a/docs/configuration/ssl.md +++ b/docs/configuration/ssl.md @@ -125,7 +125,7 @@ Certain versions of nginx have bugs that prevent [HTTP/2](https://nginx.org/en/d Your application has access to the HTTP headers `X-Forwarded-Proto`, `X-Forwarded-Port` and `X-Forwarded-For`. These headers indicate the protocol of the original request (HTTP or HTTPS), the port number, and the IP address of the client making the request, respectively. The default configuration is for Nginx to set these headers. -If your server runs behind an HTTP/S load balancer, then Nginx will see all requests as coming from the load balancer. If your load balancer sets the `X-Forwarded-` headers, you can tell Nginx to pass these headers from load balancer to your application by using the following [nginx custom template](/docs/configuration/nginx.md#customizing-the-nginx-configuration) +If your server runs behind an HTTP(S) load balancer, then Nginx will see all requests as coming from the load balancer. If your load balancer sets the `X-Forwarded-` headers, you can tell Nginx to pass these headers from load balancer to your application by using a [custom nginx template](/docs/configuration/nginx.md#customizing-the-nginx-configuration). The following is a simple example of how to do so. ```go server { @@ -160,7 +160,7 @@ Only use this option if: 1. All requests are terminated at the load balancer, and forwarded to Nginx 2. The load balancer is configured to send the `X-Forwarded-` headers (this may be off by default) -If it's possible to make HTTP/S requests directly to Nginx, bypassing the load balancer, or if the load balancer is not configured to set these headers, then it becomes possible for a client to set these headers to arbitrary values. +If it's possible to make HTTP(S) requests directly to Nginx, bypassing the load balancer, or if the load balancer is not configured to set these headers, then it becomes possible for a client to set these headers to arbitrary values. This could result in security issue, for example, if your application looks at the value of the `X-Forwarded-Proto` to determine if the request was made over HTTPS. diff --git a/plugins/common/functions b/plugins/common/functions index 9fbd0abf0..2cb56ca93 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -957,6 +957,16 @@ is_val_in_list() { echo "$val_in_list" } +fn-in-array() { + declare desc="return true if value ($1) is in list (all other arguments)" + + local e + for e in "${@:2}"; do + [[ "$e" == "$1" ]] && return 0 + done + return 1 +} + remove_val_from_list() { declare desc="remove value ($1) from list ($2) separated by delimiter ($3) (delimiter defaults to comma) and return list" local value="$1" list="$2" delimiter="${3:-,}" diff --git a/plugins/common/properties.go b/plugins/common/properties.go index b4e6d2fa9..cbbf75349 100644 --- a/plugins/common/properties.go +++ b/plugins/common/properties.go @@ -100,6 +100,7 @@ func PropertyGetAll(pluginName string, appName string) (map[string]string, error // PropertyGetDefault returns the value for a given property with a specified default value func PropertyGetDefault(pluginName, appName, property, defaultValue string) (val string) { if !PropertyExists(pluginName, appName, property) { + val = defaultValue return } diff --git a/plugins/git/subcommands/set b/plugins/git/subcommands/set index aa01be113..0437d384a 100755 --- a/plugins/git/subcommands/set +++ b/plugins/git/subcommands/set @@ -4,16 +4,6 @@ source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -fn-in-array() { - declare desc="return true if value ($1) is in list (all other arguments)" - - local e - for e in "${@:2}"; do - [[ "$e" == "$1" ]] && return 0 - done - return 1 -} - git-set-cmd() { declare desc="set or clear a git property for an app" local cmd="git:set" argv=("$@") diff --git a/plugins/nginx-vhosts/functions b/plugins/nginx-vhosts/functions index e3cc79d5e..a76f0ca82 100755 --- a/plugins/nginx-vhosts/functions +++ b/plugins/nginx-vhosts/functions @@ -378,6 +378,10 @@ nginx_build_config() { local NGINX_TEMPLATE_SOURCE="app-supplied" fi + local NGINX_BIND_ADDRESS_IP4="$(fn-plugin-property-get-default "nginx" "$APP" "bind-address-ipv4" "")" + local NGINX_BIND_ADDRESS_IP6="$(fn-plugin-property-get-default "nginx" "$APP" "bind-address-ipv6" "::")" + [[ -z "$NGINX_BIND_ADDRESS_IP6" ]] && NGINX_BIND_ADDRESS_IP6="::" + eval "$(config_export app "$APP")" local SIGIL_PARAMS=(-f "$NGINX_TEMPLATE" APP="$APP" DOKKU_ROOT="$DOKKU_ROOT" NOSSL_SERVER_NAME="$NOSSL_SERVER_NAME" @@ -388,6 +392,8 @@ nginx_build_config() { TLS13_SUPPORTED="$TLS13_SUPPORTED" HTTP2_SUPPORTED="$HTTP2_SUPPORTED" NGINX_LOG_ROOT="$NGINX_LOG_ROOT" + NGINX_BIND_ADDRESS_IP4="$NGINX_BIND_ADDRESS_IP4" + NGINX_BIND_ADDRESS_IP6="$NGINX_BIND_ADDRESS_IP6" HTTP2_PUSH_SUPPORTED="$HTTP2_PUSH_SUPPORTED" GRPC_SUPPORTED="$GRPC_SUPPORTED" DOKKU_APP_LISTEN_PORT="$DOKKU_APP_LISTEN_PORT" DOKKU_APP_LISTEN_IP="$DOKKU_APP_LISTEN_IP" diff --git a/plugins/nginx-vhosts/internal-functions b/plugins/nginx-vhosts/internal-functions index 7dfc3205a..8efceb748 100755 --- a/plugins/nginx-vhosts/internal-functions +++ b/plugins/nginx-vhosts/internal-functions @@ -2,6 +2,68 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" + +cmd-nginx-report() { + declare desc="displays a nginx report for one or more apps" + local cmd="nginx:report" + local INSTALLED_APPS=$(dokku_apps) + local APP="$2" INFO_FLAG="$3" + + if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then + INFO_FLAG="$APP" + APP="" + fi + + if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then + INFO_FLAG="true" + fi + + if [[ -z "$APP" ]]; then + for app in $INSTALLED_APPS; do + cmd-nginx-report-single "$app" "$INFO_FLAG" | tee || true + done + else + cmd-nginx-report-single "$APP" "$INFO_FLAG" + fi +} + +cmd-nginx-report-single() { + declare APP="$1" INFO_FLAG="$2" + if [[ "$INFO_FLAG" == "true" ]]; then + INFO_FLAG="" + fi + verify_app_name "$APP" + 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" "::")" + ) + + if [[ -z "$INFO_FLAG" ]]; then + dokku_log_info2_quiet "${APP} nginx information" + for flag in "${flag_map[@]}"; do + key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')" + dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")" + done + else + local match=false + local value_exists=false + for flag in "${flag_map[@]}"; do + valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)" + if [[ "$flag" == "${INFO_FLAG}:"* ]]; then + value=${flag#*: } + size="${#value}" + if [[ "$size" -ne 0 ]]; then + echo "$value" && match=true && value_exists=true + else + match=true + fi + fi + done + [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" + [[ "$value_exists" == "true" ]] || dokku_log_fail "not deployed" + fi +} fn-nginx-vhosts-uses-openresty() { declare desc="returns whether openresty is in use or not" @@ -112,6 +174,8 @@ nginx_vhosts_help_content_func() { nginx:access-logs [-t], Show the nginx access logs for an application (-t follows) nginx:build-config , (Re)builds nginx config for given app nginx:error-logs [-t], Show the nginx error logs for an application (-t follows) + nginx:report [] [], Displays an nginx report for one or more apps + nginx:set (), Set or clear an nginx property for an app nginx:show-conf , Display app nginx config nginx:validate [] [--clean], Validates and optionally cleans up invalid nginx configurations help_content diff --git a/plugins/nginx-vhosts/report b/plugins/nginx-vhosts/report new file mode 100755 index 000000000..19d04fd8a --- /dev/null +++ b/plugins/nginx-vhosts/report @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-nginx-report-single "$@" diff --git a/plugins/nginx-vhosts/subcommands/access-logs b/plugins/nginx-vhosts/subcommands/access-logs index 074772b83..890a44f72 100755 --- a/plugins/nginx-vhosts/subcommands/access-logs +++ b/plugins/nginx-vhosts/subcommands/access-logs @@ -2,7 +2,6 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/config/functions" source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/functions" nginx_logs_cmd() { diff --git a/plugins/nginx-vhosts/subcommands/report b/plugins/nginx-vhosts/subcommands/report new file mode 100755 index 000000000..9f1e41bbf --- /dev/null +++ b/plugins/nginx-vhosts/subcommands/report @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/nginx/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-nginx-report "$@" diff --git a/plugins/nginx-vhosts/subcommands/set b/plugins/nginx-vhosts/subcommands/set new file mode 100755 index 000000000..82eca4eaf --- /dev/null +++ b/plugins/nginx-vhosts/subcommands/set @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +nginx-set-cmd() { + declare desc="set or clear an nginx property for an app" + 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") + [[ -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" + fi + + if [[ -n "$VALUE" ]]; then + dokku_log_info2_quiet "Setting ${KEY} to ${VALUE}" + fn-plugin-property-write "nginx" "$APP" "$KEY" "$VALUE" + else + dokku_log_info2_quiet "Unsetting ${KEY}" + fn-plugin-property-delete "nginx" "$APP" "$KEY" + fi +} + +nginx-set-cmd "$@" diff --git a/plugins/nginx-vhosts/subcommands/validate b/plugins/nginx-vhosts/subcommands/validate index 05e620482..56f7a2319 100755 --- a/plugins/nginx-vhosts/subcommands/validate +++ b/plugins/nginx-vhosts/subcommands/validate @@ -2,7 +2,6 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/config/functions" source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/functions" nginx_validate_cmd() { diff --git a/plugins/nginx-vhosts/templates/nginx.conf.sigil b/plugins/nginx-vhosts/templates/nginx.conf.sigil index 7817c8f3d..f1abb99b6 100644 --- a/plugins/nginx-vhosts/templates/nginx.conf.sigil +++ b/plugins/nginx-vhosts/templates/nginx.conf.sigil @@ -6,8 +6,8 @@ {{ if eq $scheme "http" }} server { - listen [::]:{{ $listen_port }}; - listen {{ $listen_port }}; + listen [{{ $.NGINX_BIND_ADDRESS_IP6 }}]:{{ $listen_port }}; + listen {{ if $.NGINX_BIND_ADDRESS_IP4 }}{{ $.NGINX_BIND_ADDRESS_IP4 }}:{{end}}{{ $listen_port }}; {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log; @@ -56,8 +56,8 @@ server { } {{ else if eq $scheme "https"}} server { - listen [::]:{{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }}; - listen {{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }}; + listen [{{ $.NGINX_BIND_ADDRESS_IP6 }}]:{{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }}; + listen {{ if $.NGINX_BIND_ADDRESS_IP4 }}{{ $.NGINX_BIND_ADDRESS_IP4 }}:{{end}}{{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }}; {{ if $.SSL_SERVER_NAME }}server_name {{ $.SSL_SERVER_NAME }}; {{ end }} {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; @@ -120,8 +120,8 @@ server { {{ else if eq $scheme "grpc"}} {{ if eq $.GRPC_SUPPORTED "true"}}{{ if eq $.HTTP2_SUPPORTED "true"}} server { - listen [::]:{{ $listen_port }} http2; - listen {{ $listen_port }} http2; + listen [{{ $.NGINX_BIND_ADDRESS_IP6 }}]:{{ $listen_port }} http2; + listen {{ if $.NGINX_BIND_ADDRESS_IP4 }}{{ $.NGINX_BIND_ADDRESS_IP4 }}:{{end}}{{ $listen_port }} http2; {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log; @@ -134,8 +134,8 @@ server { {{ else if eq $scheme "grpcs"}} {{ if eq $.GRPC_SUPPORTED "true"}}{{ if eq $.HTTP2_SUPPORTED "true"}} server { - listen [::]:{{ $listen_port }} ssl http2; - listen {{ $listen_port }} ssl http2; + listen [{{ $.NGINX_BIND_ADDRESS_IP6 }}]:{{ $listen_port }} ssl http2; + listen {{ if $.NGINX_BIND_ADDRESS_IP4 }}{{ $.NGINX_BIND_ADDRESS_IP4 }}:{{end}}{{ $listen_port }} ssl http2; {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log; diff --git a/plugins/scheduler-docker-local/subcommands/set b/plugins/scheduler-docker-local/subcommands/set index 99a37fb3e..b97c206a1 100755 --- a/plugins/scheduler-docker-local/subcommands/set +++ b/plugins/scheduler-docker-local/subcommands/set @@ -4,16 +4,6 @@ source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -fn-in-array() { - declare desc="return true if value ($1) is in list (all other arguments)" - - local e - for e in "${@:2}"; do - [[ "$e" == "$1" ]] && return 0 - done - return 1 -} - scheduler-docker-local-set-cmd() { declare desc="set or clear a scheduler-docker-local property for an app" local cmd="scheduler-docker-local:set" argv=("$@") diff --git a/tests/unit/40_nginx-vhosts_2.bats b/tests/unit/40_nginx-vhosts_2.bats index 3be7ab2cd..cd0857f00 100644 --- a/tests/unit/40_nginx-vhosts_2.bats +++ b/tests/unit/40_nginx-vhosts_2.bats @@ -107,6 +107,55 @@ assert_error_log() { assert_failure } +@test "(nginx-vhosts) nginx:set bind-address" { + run deploy_app + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku nginx:set $TEST_APP bind-address-ipv4 127.0.0.1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku nginx:set $TEST_APP bind-address-ipv6 ::1" + 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 "dokku nginx:show-conf $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_output_contains "[::1]:80;" + assert_output_contains "127.0.0.1:80;" + + run /bin/bash -c "dokku nginx:set $TEST_APP bind-address-ipv4" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku nginx:set $TEST_APP bind-address-ipv6" + 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 "dokku nginx:show-conf $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_output_contains "[::1]:80;" 0 + assert_output_contains "127.0.0.1:80;" 0 +} + @test "(nginx-vhosts) nginx:validate" { deploy_app run /bin/bash -c "dokku nginx:validate"