diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 44720039c..4d9642710 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -219,6 +219,19 @@ jobs:
name: coverage.${{ matrix.index }}
path: test-results/coverage
+ sudo-preserve-env:
+ name: sudo-preserve-env
+ runs-on: ubuntu-26.04
+ needs: check-commit
+ if: ${{ needs.check-commit.outputs.skip != 'true' }}
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: check that the preserved environment survives sudo-rs
+ run: ./tests/ci/sudo_preserve_env.sh
+
publish-test-results:
name: publish-test-results
needs: unit-tests
diff --git a/Makefile b/Makefile
index 6fafe4b3c..bab57eee7 100644
--- a/Makefile
+++ b/Makefile
@@ -126,10 +126,10 @@ else
endif
plugin-dependencies: plugn procfile-util
- sudo -E dokku plugin:install-dependencies --core
+ sudo dokku plugin:install-dependencies --core
plugins: plugn procfile-util docker
- sudo -E dokku plugin:install --core
+ sudo dokku plugin:install --core
dependencies: apt-update jq docker-image-labeler docker-container-healthchecker lambda-builder netrc sshcommand plugn procfile-util docker help2man man-db sigil dos2unix parallel
$(MAKE) -e stack
diff --git a/bootstrap.sh b/bootstrap.sh
index 714c07ebb..eb923d379 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -110,14 +110,14 @@ install-dokku() {
elif [[ "$use_plugin" == "true" ]]; then
install-dokku-from-package "$DOKKU_SEMVER"
echo "--> Running post-install dependency installation"
- sudo -E dokku plugin:install-dependencies --core
+ sudo dokku plugin:install-dependencies --core
else
install-dokku-from-source "$DOKKU_TAG"
fi
else
install-dokku-from-package
echo "--> Running post-install dependency installation"
- sudo -E dokku plugin:install-dependencies --core
+ sudo dokku plugin:install-dependencies --core
fi
}
diff --git a/docs/configuration/environment-variables.md b/docs/configuration/environment-variables.md
index afbc7861c..968c63308 100644
--- a/docs/configuration/environment-variables.md
+++ b/docs/configuration/environment-variables.md
@@ -138,6 +138,9 @@ The following config variables have special meanings and can be set in a variety
| `DOKKU_EVENTS_LOGFILE` | `$DOKKU_LOGS_DIR/events.log` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | Where the events log file is written to. |
| `DOKKU_APP_NAME` | none | `--app APP` flag | Name of application to work on. Respected by core plugins. |
| `DOKKU_APPS_FORCE_DELETE` | none | `--force` flag | Whether to force delete an application. Also used by other plugins for destructive actions. |
+| `DOKKU_DISABLE_TTY` | none | `dokku run --no-tty`
environment | Forces `dokku run` to treat the session as non-interactive. |
+| `DOKKU_FORCE_TTY` | none | `dokku run --force-tty`
environment | Forces `dokku run` to treat the session as interactive. |
+| `DOKKU_PRESERVE_ENV` | none | `/etc/default/dokku`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*`
environment | Comma-separated list of extra environment variable names to carry across the privilege drop to the `dokku` user. See [Forwarding environment variables](#forwarding-environment-variables). |
| `DOKKU_CHECKS_URL` | `https://dokku.com/docs/deployment/zero-downtime-deploys/` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | Url displayed during deployment when no CHECKS file exists. |
| `DOKKU_QUIET_OUTPUT` | none | `--quiet` flag | Silences certain header output for `dokku` commands. |
| `DOKKU_RM_CONTAINER` | none | `dokku config:set`
| Deprecated: Whether to keep `dokku run` containers around or not. |
@@ -145,6 +148,45 @@ The following config variables have special meanings and can be set in a variety
| `DOKKU_SYSTEM_GROUP` | `dokku` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | System group to chown files as. |
| `DOKKU_SYSTEM_USER` | `dokku` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | System user to chown files as. |
+## Forwarding environment variables
+
+When `dokku` is invoked by any user other than `dokku` - `root`, or a member of the `sudo` group - it
+re-executes itself as the `dokku` user. Most configuration does not need to survive that step, since
+`/etc/default/dokku`, `~dokku/dokkurc` and `~dokku/.dokkurc/*` are read again by the re-executed
+process, and `/etc/environment` is applied by PAM.
+
+What cannot be recomputed is state that belongs to the invocation itself, so those variables are
+named explicitly and forwarded:
+
+| Variable | Source |
+| --- | --- |
+| `DOKKU_APP_NAME`, `DOKKU_APPS_FORCE_DELETE`, `DOKKU_QUIET_OUTPUT`, `DOKKU_TRACE` | the `--app`, `--force`, `--quiet` and `--trace` global flags, or the environment |
+| `DOKKU_GLOBAL_FLAGS` | every global flag as given, replayed by plugins that need them |
+| `DOKKU_DISABLE_TTY`, `DOKKU_FORCE_TTY` | the `dokku run` tty flags, or the environment |
+| `DOKKU_PRESERVE_ENV` | the list below |
+| `SSH_USER`, `SSH_NAME`, `NAME`, `FINGERPRINT`, `SSH_ORIGINAL_COMMAND` | the ssh session the command arrived on |
+| `DOCKER_BIN`, `DOKKU_CNB_BUILDER`, `DOKKU_EVENTS_LOGFILE`, `DOKKU_HOST_ROOT`, `DOKKU_IMAGE`, `DOKKU_LIB_PATH`, `DOKKU_LOGS_DIR`, `DOKKU_LOGS_HOST_DIR`, `DOKKU_ROOT`, `DOKKU_SYSTEM_GROUP`, `DOKKU_SYSTEM_USER`, `PLUGIN_PATH`, `PLUGIN_AVAILABLE_PATH`, `PLUGIN_ENABLED_PATH`, `PLUGIN_CORE_PATH`, `PLUGIN_CORE_AVAILABLE_PATH`, `PLUGIN_CORE_ENABLED_PATH` | the environment, so the re-executed process derives the same paths as the caller |
+| `DOKKU_INIT_SYSTEM`, `DOKKU_LIB_HOST_ROOT` | the environment, which is where they live while a Docker image is being built |
+
+`DOKKU_PID` is deliberately not forwarded: it is unset before the privilege drop so the re-executed
+process gets its own.
+
+To forward anything else - a variable read by a third-party plugin, or a one-off override of a
+setting that normally lives in `/etc/default/dokku` - name it in `DOKKU_PRESERVE_ENV`:
+
+```shell
+DOKKU_PRESERVE_ENV=DOCKER_BUILDKIT DOCKER_BUILDKIT=0 dokku ps:rebuild node-js-app
+```
+
+Set it persistently to forward a variable for every invocation:
+
+```shell
+echo "export DOKKU_PRESERVE_ENV=MY_PLUGIN_DEBUG" | sudo tee -a /etc/default/dokku
+```
+
+Variables sudo manages for the target user - `HOME`, `PATH`, `SHELL`, `USER`, `LOGNAME`, `MAIL` and
+`SUDO_*` - are never forwarded, and naming them in `DOKKU_PRESERVE_ENV` is not supported.
+
## Properties
### Internal properties
diff --git a/docs/deployment/user-management.md b/docs/deployment/user-management.md
index 27922f67c..f9597dac3 100644
--- a/docs/deployment/user-management.md
+++ b/docs/deployment/user-management.md
@@ -135,3 +135,9 @@ Use `visudo /etc/sudoers.d/dokku-users`, or `visudo /etc/sudoers` to add the fol
```
%dokku ALL=(ALL:ALL) NOPASSWD:SETENV: /usr/bin/dokku
```
+
+The `SETENV:` tag is required. When a user other than `dokku` runs the `dokku` binary, it re-executes
+itself as the `dokku` user and names the environment variables that have to survive that step via
+`sudo --preserve-env=`. Without `SETENV:` those variables are filtered out, and global flags such as
+`--quiet`, `--trace` and `--force` stop taking effect. See
+[Forwarding environment variables](/docs/configuration/environment-variables.md#forwarding-environment-variables).
diff --git a/dokku b/dokku
index 495e7aa9f..4c3609258 100755
--- a/dokku
+++ b/dokku
@@ -67,6 +67,9 @@ export DOKKU_GLOBAL_RUN_ARGS="--label=org.label-schema.schema-version=1.0 --labe
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
+# the original argv is kept so the re-exec as the dokku user can replay it verbatim
+# and let the child re-parse the global flags for itself. parse_args and the
+# flag-stripping loop below must therefore stay pure functions of argv.
parse_args "$@"
args=("$@")
skip_arg=false
@@ -91,7 +94,9 @@ if [[ $(id -un) != "dokku" ]]; then
if [[ ! $1 =~ plugin:* ]] && [[ $1 != "ssh-keys:add" ]] && [[ $1 != "ssh-keys:remove" ]] && [[ $1 != "scheduler-k3s:initialize" ]] && [[ $1 != "scheduler-k3s:uninstall" ]]; then
unset DOKKU_PID TMP TMPDIR TEMP TEMPDIR
export SSH_USER=$(id -un)
- sudo -u dokku -E -H "$0" "$@"
+ read -ra preserved_env_names <<<"$(fn-cli-preserved-env-names)"
+ preserve_env_flag="$(fn-sudo-preserve-env-flag "${preserved_env_names[@]}")"
+ sudo -u dokku "$preserve_env_flag" -H "$0" "${args[@]}"
exit $?
fi
fi
diff --git a/plugins/common/functions b/plugins/common/functions
index 74750073f..2b13b9416 100755
--- a/plugins/common/functions
+++ b/plugins/common/functions
@@ -476,6 +476,56 @@ is_abs_path() {
fi
}
+fn-cli-preserved-env-names() {
+ declare desc="names the environment variables the re-exec as the dokku user must carry"
+
+ # every variable dokku honours from the environment has to be listed, or the child
+ # derives different values than the parent - during the docker image build these only
+ # ever exist in the environment. DOKKU_PID is the one exclusion: it is unset above so
+ # the child gets its own. DOKKU_PRESERVE_ENV carries anything else
+ echo "DOKKU_APP_NAME DOKKU_APPS_FORCE_DELETE DOKKU_DISABLE_TTY DOKKU_FORCE_TTY" \
+ "DOKKU_GLOBAL_FLAGS DOKKU_PRESERVE_ENV DOKKU_QUIET_OUTPUT DOKKU_TRACE" \
+ "FINGERPRINT NAME SSH_NAME SSH_ORIGINAL_COMMAND SSH_USER" \
+ "DOCKER_BIN DOKKU_CNB_BUILDER DOKKU_EVENTS_LOGFILE DOKKU_HOST_ROOT DOKKU_IMAGE" \
+ "DOKKU_INIT_SYSTEM DOKKU_LIB_HOST_ROOT DOKKU_LIB_PATH DOKKU_LOGS_DIR" \
+ "DOKKU_LOGS_HOST_DIR DOKKU_ROOT DOKKU_SYSTEM_GROUP DOKKU_SYSTEM_USER" \
+ "PLUGIN_AVAILABLE_PATH PLUGIN_CORE_AVAILABLE_PATH PLUGIN_CORE_ENABLED_PATH" \
+ "PLUGIN_CORE_PATH PLUGIN_ENABLED_PATH PLUGIN_PATH"
+}
+
+fn-sudo-preserve-env-flag() {
+ declare desc="prints the sudo flag that carries environment variables across a privilege drop"
+ local names=("$@")
+ local extra_names=()
+ local joined name
+
+ # sudo-rs, the default sudo on ubuntu 25.10 and later, ignores -E and bare
+ # --preserve-env, so every variable that has to survive must be named
+ if [[ "${#names[@]}" -eq 0 ]]; then
+ for name in $(compgen -e); do
+ # preserving these would defeat -H and override the sudoers secure_path
+ case "$name" in
+ HOME | LOGNAME | MAIL | PATH | SHELL | USER | _ | SUDO_*)
+ continue
+ ;;
+ esac
+ names+=("$name")
+ done
+ fi
+
+ if [[ -n "$DOKKU_PRESERVE_ENV" ]]; then
+ read -ra extra_names <<<"${DOKKU_PRESERVE_ENV//,/ }"
+ names+=("${extra_names[@]}")
+ fi
+
+ if [[ "${#names[@]}" -eq 0 ]]; then
+ return 0
+ fi
+
+ printf -v joined "%s," "${names[@]}"
+ echo "--preserve-env=${joined%,}"
+}
+
parse_args() {
declare desc="top-level cli arg parser"
local next_index=1
diff --git a/plugins/plugin/subcommands/trigger b/plugins/plugin/subcommands/trigger
index 81065f147..4d9ac5989 100755
--- a/plugins/plugin/subcommands/trigger
+++ b/plugins/plugin/subcommands/trigger
@@ -1,13 +1,22 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
+source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
cmd-plugin-trigger() {
declare desc="trigger an arbitrary plugin hook"
declare cmd="plugin:trigger"
[[ "$1" == "$cmd" ]] && shift 1
- sudo -u dokku -E -H plugn trigger "$@"
+ local sudo_args=(-u dokku)
+ local preserve_env_flag
+ preserve_env_flag="$(fn-sudo-preserve-env-flag)"
+ if [[ -n "$preserve_env_flag" ]]; then
+ sudo_args+=("$preserve_env_flag")
+ fi
+ sudo_args+=(-H)
+
+ sudo "${sudo_args[@]}" plugn trigger "$@"
}
cmd-plugin-trigger "$@"
diff --git a/tests/ci/sudo_preserve_env.sh b/tests/ci/sudo_preserve_env.sh
new file mode 100755
index 000000000..51896556b
--- /dev/null
+++ b/tests/ci/sudo_preserve_env.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+# Checks that the environment dokku forwards across its privilege drop survives the
+# local sudo implementation. sudo-rs, the default sudo on ubuntu 25.10 and later,
+# ignores -E and bare --preserve-env, so dokku names every variable it needs.
+set -eo pipefail
+
+PLUGIN_CORE_AVAILABLE_PATH="${PLUGIN_CORE_AVAILABLE_PATH:-$(dirname "$0")/../../plugins}"
+export PLUGIN_CORE_AVAILABLE_PATH
+source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
+
+FAILURES=0
+
+fail() {
+ declare desc="report a failed check"
+ echo "not ok - $1" 1>&2
+ FAILURES=$((FAILURES + 1))
+}
+
+pass() {
+ declare desc="report a passing check"
+ echo "ok - $1"
+}
+
+report-whole-environment-support() {
+ declare desc="record whether this sudo honours -E, for the log"
+ local warning
+
+ warning="$(DOKKU_TEST_CROSSING=1 sudo -E -u nobody true 2>&1 >/dev/null || true)"
+ if [[ -n "$warning" ]]; then
+ echo "# sudo -E is not supported here: $warning"
+ else
+ echo "# sudo -E is supported here"
+ fi
+}
+
+check-named-list-crosses() {
+ declare desc="the named variables reach the target user"
+ local flag output
+
+ flag="$(DOKKU_QUIET_OUTPUT=1 DOKKU_TRACE=1 SSH_USER=ci fn-sudo-preserve-env-flag DOKKU_QUIET_OUTPUT DOKKU_TRACE SSH_USER)"
+ output="$(DOKKU_QUIET_OUTPUT=1 DOKKU_TRACE=1 SSH_USER=ci sudo "$flag" -u nobody /usr/bin/env)"
+
+ local name
+ for name in DOKKU_QUIET_OUTPUT=1 DOKKU_TRACE=1 SSH_USER=ci; do
+ if grep -qxF "$name" <<<"$output"; then
+ pass "$name crossed the privilege drop"
+ else
+ fail "$name did not cross the privilege drop"
+ fi
+ done
+}
+
+check-named-list-is-silent() {
+ declare desc="no warning is written to stderr"
+ local flag stderr
+
+ flag="$(SSH_USER=ci fn-sudo-preserve-env-flag SSH_USER)"
+ stderr="$(SSH_USER=ci sudo "$flag" -u nobody true 2>&1 >/dev/null || true)"
+ if [[ -z "$stderr" ]]; then
+ pass "no warning on stderr"
+ else
+ fail "unexpected stderr: $stderr"
+ fi
+}
+
+check-absent-names-are-tolerated() {
+ declare desc="naming a variable that is not set is not an error"
+ local flag
+
+ flag="$(fn-sudo-preserve-env-flag DOKKU_DEFINITELY_NOT_SET_HERE SSH_USER)"
+ if sudo "$flag" -u nobody true; then
+ pass "unset names are ignored"
+ else
+ fail "sudo rejected a list naming an unset variable"
+ fi
+}
+
+check-home-still-wins() {
+ declare desc="-H is not defeated by the preserved list"
+ local flag output
+ local names=()
+
+ read -ra names <<<"$(fn-cli-preserved-env-names)"
+ flag="$(fn-sudo-preserve-env-flag "${names[@]}")"
+ output="$(sudo "$flag" -H -u nobody /usr/bin/env)"
+ if grep -qxF "HOME=$(getent passwd nobody | cut -d: -f6)" <<<"$output"; then
+ pass "-H still sets HOME for the target user"
+ else
+ fail "HOME was not reset for the target user"
+ fi
+}
+
+main() {
+ declare desc="run every check"
+
+ echo "# sudo version:"
+ sudo --version | head -1 | sed -e 's/^/# /'
+
+ report-whole-environment-support
+ check-named-list-crosses
+ check-named-list-is-silent
+ check-absent-names-are-tolerated
+ check-home-still-wins
+
+ if [[ "$FAILURES" -gt 0 ]]; then
+ echo "$FAILURES check(s) failed" 1>&2
+ exit 1
+ fi
+ echo "all checks passed"
+}
+
+main "$@"
diff --git a/tests/unit/core_1.bats b/tests/unit/core_1.bats
index b28d3c510..11cacfdca 100644
--- a/tests/unit/core_1.bats
+++ b/tests/unit/core_1.bats
@@ -120,3 +120,77 @@ teardown() {
assert_line 0 "--label"
assert_line 1 "a b"
}
+
+@test "(common) [preserve-env] fn-sudo-preserve-env-flag emits only the requested names" {
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; fn-sudo-preserve-env-flag DOKKU_TRACE DOKKU_APP_NAME"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "--preserve-env=DOKKU_TRACE,DOKKU_APP_NAME"
+}
+
+@test "(common) [preserve-env] fn-sudo-preserve-env-flag omits sudo managed names" {
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; export DOKKU_TEST_CROSSING=1; fn-sudo-preserve-env-flag | sed 's/^--preserve-env=//' | tr ',' '\\n' | grep -cx DOKKU_TEST_CROSSING"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "1"
+
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; fn-sudo-preserve-env-flag | sed 's/^--preserve-env=//' | tr ',' '\\n' | grep -cxE 'HOME|LOGNAME|MAIL|PATH|SHELL|USER|_|SUDO_.*'"
+ echo "output: $output"
+ echo "status: $status"
+ assert_output "0"
+}
+
+@test "(common) [preserve-env] fn-sudo-preserve-env-flag honors DOKKU_PRESERVE_ENV" {
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; DOKKU_PRESERVE_ENV=ALPHA,BETA fn-sudo-preserve-env-flag DOKKU_TRACE"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "--preserve-env=DOKKU_TRACE,ALPHA,BETA"
+
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; DOKKU_PRESERVE_ENV=ALPHA fn-sudo-preserve-env-flag | sed 's/^--preserve-env=//' | tr ',' '\\n' | grep -cx ALPHA"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "1"
+}
+
+@test "(common) [preserve-env] fn-cli-preserved-env-names covers every parse_args export" {
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; fn-cli-preserved-env-names | tr ' ' '\\n' | grep -cxE 'DOKKU_APP_NAME|DOKKU_APPS_FORCE_DELETE|DOKKU_GLOBAL_FLAGS|DOKKU_PRESERVE_ENV|DOKKU_QUIET_OUTPUT|DOKKU_TRACE|SSH_USER'"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "7"
+}
+
+@test "(common) [preserve-env] fn-cli-preserved-env-names covers the docker image build variables" {
+ # neither is read by the dokku script itself, so the derivation test below does not
+ # catch them. both exist only in the environment while the docker image is built
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; fn-cli-preserved-env-names | tr ' ' '\\n' | grep -cxE 'DOKKU_INIT_SYSTEM|DOKKU_LIB_HOST_ROOT'"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "2"
+}
+
+@test "(common) [preserve-env] every environment-overridable dokku variable is preserved" {
+ # test_helper leaves these unexported, so the single-quoted script below cannot see
+ # them without this
+ export PLUGIN_CORE_AVAILABLE_PATH
+ export DOKKU_SOURCE="${BATS_TEST_DIRNAME}/../../dokku"
+ run /bin/bash -c 'source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"; comm -23 <(grep -oE "[{][A-Z_]+:=" "$DOKKU_SOURCE" | tr -cd "A-Z_\n" | sort -u | grep -vx DOKKU_PID) <(fn-cli-preserved-env-names | tr " " "\n" | sort -u)'
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output ""
+ unset DOKKU_SOURCE
+}
+
+@test "(common) [preserve-env] parse_args records global flags for replay" {
+ run /bin/bash -c "source '$PLUGIN_CORE_AVAILABLE_PATH/common/functions'; parse_args --label=a=b run foo env; echo \"\$DOKKU_GLOBAL_FLAGS\""
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "--label=a=b"
+}
diff --git a/tests/unit/core_3.bats b/tests/unit/core_3.bats
new file mode 100644
index 000000000..832bb51b1
--- /dev/null
+++ b/tests/unit/core_3.bats
@@ -0,0 +1,105 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+setup() {
+ global_setup
+ create_app
+}
+
+teardown() {
+ destroy_app
+ global_teardown
+}
+
+@test "(core) [global-flags] --quiet crosses the privilege drop" {
+ run /bin/bash -c "dokku apps:list"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output_contains "My Apps"
+
+ run /bin/bash -c "dokku --quiet apps:list"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output_not_contains "My Apps"
+ assert_output_contains "$TEST_APP"
+}
+
+@test "(core) [global-flags] --trace crosses the privilege drop" {
+ run /bin/bash -c "dokku --trace apps:list 2>&1 >/dev/null"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output_contains "execute_dokku_cmd"
+}
+
+@test "(core) [global-flags] --force crosses the privilege drop" {
+ local force_app="${TEST_APP}-force"
+ run /bin/bash -c "dokku apps:create $force_app"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+
+ run /bin/bash -c "dokku --force apps:destroy $force_app &1 >/dev/null"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output ""
+}
+
+@test "(core) [global-flags] the caller environment crosses the privilege drop" {
+ run /bin/bash -c "DOKKU_QUIET_OUTPUT=1 dokku apps:list"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output_not_contains "My Apps"
+ assert_output_contains "$TEST_APP"
+}
+
+@test "(core) [global-flags] an unlisted variable only crosses when DOKKU_PRESERVE_ENV names it" {
+ # DOKKU_TRACE is set in the environment rather than passed as --trace so that the
+ # parent traces too: the sudo line naming the forwarded variables is only emitted there
+ run /bin/bash -c "DOKKU_TRACE=1 DOKKU_TEST_CROSSING=1 dokku apps:list 2>&1 >/dev/null | grep -o -- '--preserve-env=[^ ]*' | head -1"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output_contains "--preserve-env="
+ assert_output_not_contains "DOKKU_TEST_CROSSING"
+
+ run /bin/bash -c "DOKKU_TRACE=1 DOKKU_PRESERVE_ENV=DOKKU_TEST_CROSSING DOKKU_TEST_CROSSING=1 dokku apps:list 2>&1 >/dev/null | grep -o -- '--preserve-env=[^ ]*' | head -1"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output_contains "DOKKU_TEST_CROSSING"
+}
+
+@test "(core) [global-flags] the system sudo supports a named preserve-env list" {
+ run /bin/bash -c "DOKKU_TEST_CROSSING=1 sudo -u dokku --preserve-env=DOKKU_TEST_CROSSING -H /usr/bin/env | grep -cxE 'DOKKU_TEST_CROSSING=1|HOME=/home/dokku'"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+ assert_output "2"
+}
diff --git a/tests/unit/plugin.bats b/tests/unit/plugin.bats
index c4037754e..fe15deab9 100644
--- a/tests/unit/plugin.bats
+++ b/tests/unit/plugin.bats
@@ -116,7 +116,7 @@ teardown() {
echo "status: $status"
assert_success
- run /bin/bash -c "sudo -E -u nobody dokku plugin:uninstall $TEST_PLUGIN_NAME"
+ run /bin/bash -c "sudo -u nobody dokku plugin:uninstall $TEST_PLUGIN_NAME"
echo "output: $output"
echo "status: $status"
assert_failure
@@ -158,7 +158,7 @@ teardown() {
echo "status: $status"
assert_success
- run /bin/bash -c "sudo -E -u nobody dokku plugin:uninstall $TEST_PLUGIN_NAME"
+ run /bin/bash -c "sudo -u nobody dokku plugin:uninstall $TEST_PLUGIN_NAME"
echo "output: $output"
echo "status: $status"
assert_failure
@@ -212,7 +212,7 @@ teardown() {
}
@test "(plugin) plugin:install, plugin:disable, plugin:uninstall as non-root user failure" {
- run /bin/bash -c "sudo -E -u nobody dokku plugin:install $TEST_PLUGIN_GIT_REPO"
+ run /bin/bash -c "sudo -u nobody dokku plugin:install $TEST_PLUGIN_GIT_REPO"
echo "output: $output"
echo "status: $status"
assert_failure
@@ -227,7 +227,7 @@ teardown() {
echo "status: $status"
assert_success
- run /bin/bash -c "sudo -E -u nobody dokku plugin:disable $TEST_PLUGIN_NAME"
+ run /bin/bash -c "sudo -u nobody dokku plugin:disable $TEST_PLUGIN_NAME"
echo "output: $output"
echo "status: $status"
assert_failure
@@ -372,3 +372,10 @@ teardown() {
assert_output_contains "root" "0"
assert_output_contains "dokku" "3"
}
+
+@test "(plugin:trigger) forwards the caller environment across the privilege drop" {
+ run /bin/bash -c "DOKKU_TEST_CROSSING=1 DOKKU_TRACE=1 dokku plugin:trigger app-list 2>&1 >/dev/null | grep -q -- '--preserve-env=[^ ]*DOKKU_TEST_CROSSING'"
+ echo "output: $output"
+ echo "status: $status"
+ assert_success
+}
diff --git a/tests/unit/resource_3.bats b/tests/unit/resource_3.bats
index 174d2084d..fcf3423e8 100644
--- a/tests/unit/resource_3.bats
+++ b/tests/unit/resource_3.bats
@@ -101,6 +101,8 @@ teardown() {
echo "status: $status"
assert_success
+ # DOKKU_OMIT_RESOURCE_ARGS has to survive the privilege drop in
+ # plugins/plugin/subcommands/trigger, so this also covers that forwarding
run_plugn_trigger DOKKU_OMIT_RESOURCE_ARGS=1 docker-args-process-build "$TEST_APP" herokuish
echo "output: $output"
echo "status: $status"
diff --git a/tests/unit/run_2.bats b/tests/unit/run_2.bats
index 86cf27e70..0dc6203b9 100644
--- a/tests/unit/run_2.bats
+++ b/tests/unit/run_2.bats
@@ -35,6 +35,9 @@ teardown() {
echo "status: $status"
assert_success
+ # the leading --label is replayed out of DOKKU_GLOBAL_FLAGS by
+ # plugins/scheduler-docker-local/scheduler-run, so this also covers that variable
+ # surviving the re-exec as the dokku user
RANDOM_RUN_CID="$(dokku --label=com.dokku.test-label=value run:detached $TEST_APP sleep 300)"
run /bin/bash -c "docker inspect -f '{{ .State.Status }}' $RANDOM_RUN_CID"
echo "output: $output"