From b57f35893cfce58f593db6c9d52b37177b53838f Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 26 May 2019 17:06:43 -0300 Subject: [PATCH 1/2] Allow the user that runs the app to be specified via an env var See gh-16973 --- .../src/main/asciidoc/deployment.adoc | 5 ++++ .../boot/loader/tools/launch.script | 20 +++++++++++++ .../launchscript/SysVinitLaunchScriptIT.java | 30 +++++++++++++++++++ .../launch-with-run-as-invalid-user.sh | 7 +++++ ...launch-with-run-as-prefer-user-informed.sh | 13 ++++++++ .../launch-with-run-as-root-required.sh | 9 ++++++ .../resources/scripts/launch-with-run-as.sh | 10 +++++++ 7 files changed, 94 insertions(+) create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-invalid-user.sh create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-prefer-user-informed.sh create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-root-required.sh create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as.sh diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc index a4431c5515c..ded04227f03 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc @@ -708,6 +708,11 @@ The following environment properties are supported with the default script: The default depends on the way the jar was built but is usually `auto` (meaning it tries to guess if it is an init script by checking if it is a symlink in a directory called `init.d`). You can explicitly set it to `service` so that the `stop\|start\|status\|restart` commands work or to `run` if you want to run the script in the foreground. +| `RUN_AS_USER` +| If set, the application will be executed as the informed user. + For security reasons, you should never run an user space application as `root`, therefore it's recommended to set this property. + Defaults to the user who owns the jar file. + | `USE_START_STOP_DAEMON` | Whether the `start-stop-daemon` command, when it's available, should be used to control the process. Defaults to `true`. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script index d732403ea65..20175904168 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script @@ -128,6 +128,26 @@ log_file="$LOG_FOLDER/$LOG_FILENAME" # shellcheck disable=SC2012 [[ $(id -u) == "0" ]] && run_user=$(ls -ld "$jarfile" | awk '{print $3}') +# Force run as informed user (from environment variable) +if [[ -n "$RUN_AS_USER" ]]; then + # checks performed for all actions except 'status' and 'run' + if ! [[ "$action" =~ ^(status|run)$ ]]; then + # Issue a error if informed user is not valid + id -u "$RUN_AS_USER" || { + echoRed "Cannot run as '$RUN_AS_USER': no such user" + exit 5 + } + + # Issue a error if we are not root + [[ $(id -u) == 0 ]] || { + echoRed "root required to run as '$RUN_AS_USER'" + exit 6 + } + fi + + run_user="$RUN_AS_USER" +fi + # Issue a warning if the application will run as root [[ $(id -u ${run_user}) == "0" ]] && { echoYellow "Application is running as root (UID 0). This is considered insecure."; } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java index d0dc4315f9d..b6c2fc03f2b 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java @@ -267,6 +267,36 @@ public class SysVinitLaunchScriptIT { assertThat(output).contains("Log written"); } + @ParameterizedTest(name = "{0} {1}") + @MethodSource("parameters") + public void launchWithRunAs(String os, String version) throws Exception { + String output = doTest(os, version, "launch-with-run-as.sh"); + assertThat(output).contains("wagner root"); + } + + @ParameterizedTest(name = "{0} {1}") + @MethodSource("parameters") + public void launchWithRunAsInvalidUser(String os, String version) throws Exception { + String output = doTest(os, version, "launch-with-run-as-invalid-user.sh"); + assertThat(output).contains("Status: 5"); + assertThat(output).has(coloredString(AnsiColor.RED, "Cannot run as 'johndoe': no such user")); + } + + @ParameterizedTest(name = "{0} {1}") + @MethodSource("parameters") + public void launchWithRunAsPreferUserInformed(String os, String version) throws Exception { + String output = doTest(os, version, "launch-with-run-as-prefer-user-informed.sh"); + assertThat(output).contains("wagner root"); + } + + @ParameterizedTest(name = "{0} {1}") + @MethodSource("parameters") + public void launchWithRunAsRootRequired(String os, String version) throws Exception { + String output = doTest(os, version, "launch-with-run-as-root-required.sh"); + assertThat(output).contains("Status: 6"); + assertThat(output).has(coloredString(AnsiColor.RED, "root required to run as 'wagner'")); + } + static List parameters() { List parameters = new ArrayList<>(); for (File os : new File("src/test/resources/conf").listFiles()) { diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-invalid-user.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-invalid-user.sh new file mode 100644 index 00000000000..f6384046dcd --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-invalid-user.sh @@ -0,0 +1,7 @@ +source ./test-functions.sh +install_service + +echo 'RUN_AS_USER=johndoe' > /test-service/spring-boot-app.conf + +start_service +echo "Status: $?" diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-prefer-user-informed.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-prefer-user-informed.sh new file mode 100644 index 00000000000..730b8197bb9 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-prefer-user-informed.sh @@ -0,0 +1,13 @@ +source ./test-functions.sh +install_service + +useradd wagner +echo 'RUN_AS_USER=wagner' > /test-service/spring-boot-app.conf + +useradd phil +chown phil /test-service/spring-boot-app.jar + +start_service +await_app + +ls -la /var/log/spring-boot-app.log diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-root-required.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-root-required.sh new file mode 100644 index 00000000000..3cd83374e22 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-root-required.sh @@ -0,0 +1,9 @@ +source ./test-functions.sh +install_service + +useradd wagner +echo 'RUN_AS_USER=wagner' > /test-service/spring-boot-app.conf +echo "JAVA_HOME='$JAVA_HOME'" >> /test-service/spring-boot-app.conf + +su - wagner -c "$(which service) spring-boot-app start" +echo "Status: $?" diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as.sh new file mode 100644 index 00000000000..6be8eee0f0c --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as.sh @@ -0,0 +1,10 @@ +source ./test-functions.sh +install_service + +useradd wagner +echo 'RUN_AS_USER=wagner' > /test-service/spring-boot-app.conf + +start_service +await_app + +ls -la /var/log/spring-boot-app.log From 79b5fd9d73d3baa393310334f19e8e8f9810a153 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 21 Sep 2019 20:14:49 +0100 Subject: [PATCH 2/2] Polish "Allow the user that runs the app to be specified via an env var" See gh-16973 --- .../src/main/asciidoc/deployment.adoc | 12 +++++----- .../boot/loader/tools/launch.script | 13 ++++------- .../launchscript/SysVinitLaunchScriptIT.java | 22 ++++++++++--------- ...ith-run-as-user-preferred-to-jar-owner.sh} | 0 ... launch-with-run-as-user-root-required.sh} | 0 ...h-run-as.sh => launch-with-run-as-user.sh} | 0 6 files changed, 22 insertions(+), 25 deletions(-) rename spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/{launch-with-run-as-prefer-user-informed.sh => launch-with-run-as-user-preferred-to-jar-owner.sh} (100%) rename spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/{launch-with-run-as-root-required.sh => launch-with-run-as-user-root-required.sh} (100%) rename spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/{launch-with-run-as.sh => launch-with-run-as-user.sh} (100%) diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc index ded04227f03..4152101a374 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc @@ -491,9 +491,10 @@ For example, on Debian, you could use the following command: NOTE: The following is a set of guidelines on how to secure a Spring Boot application that runs as an init.d service. It is not intended to be an exhaustive list of everything that should be done to harden an application and the environment in which it runs. -When executed as root, as is the case when root is being used to start an init.d service, the default executable script runs the application as the user who owns the jar file. -You should never run a Spring Boot application as `root`, so your application's jar file should never be owned by root. -Instead, create a specific user to run your application and use `chown` to make it the owner of the jar file, as shown in the following example: +When executed as root, as is the case when root is being used to start an init.d service, the default executable script runs the application as the user specified in the `RUN_AS_USER` environment variable. +When the environment variable is not set, the user who owns the jar file is used instead. +You should never run a Spring Boot application as `root`, so `RUN_AS_USER` should never be root and your application's jar file should never be owned by root. +Instead, create a specific user to run your application and set the `RUN_AS_USER` environment variable or use `chown` to make it the owner of the jar file, as shown in the following example: [indent=0,subs="verbatim,quotes,attributes"] ---- @@ -709,9 +710,8 @@ The following environment properties are supported with the default script: You can explicitly set it to `service` so that the `stop\|start\|status\|restart` commands work or to `run` if you want to run the script in the foreground. | `RUN_AS_USER` -| If set, the application will be executed as the informed user. - For security reasons, you should never run an user space application as `root`, therefore it's recommended to set this property. - Defaults to the user who owns the jar file. +| The user that will be used to run the application. + When not set, the user that owns the jar file will be used. | `USE_START_STOP_DAEMON` | Whether the `start-stop-daemon` command, when it's available, should be used to control the process. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script index 20175904168..a7bbd78b09d 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script @@ -128,23 +128,18 @@ log_file="$LOG_FOLDER/$LOG_FILENAME" # shellcheck disable=SC2012 [[ $(id -u) == "0" ]] && run_user=$(ls -ld "$jarfile" | awk '{print $3}') -# Force run as informed user (from environment variable) +# Run as user specified in RUN_AS_USER if [[ -n "$RUN_AS_USER" ]]; then - # checks performed for all actions except 'status' and 'run' if ! [[ "$action" =~ ^(status|run)$ ]]; then - # Issue a error if informed user is not valid id -u "$RUN_AS_USER" || { echoRed "Cannot run as '$RUN_AS_USER': no such user" - exit 5 + exit 2 } - - # Issue a error if we are not root [[ $(id -u) == 0 ]] || { - echoRed "root required to run as '$RUN_AS_USER'" - exit 6 + echoRed "Cannot run as '$RUN_AS_USER': current user is not root" + exit 4 } fi - run_user="$RUN_AS_USER" fi diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java index b6c2fc03f2b..2cb1aa13717 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/java/org/springframework/boot/launchscript/SysVinitLaunchScriptIT.java @@ -269,32 +269,34 @@ public class SysVinitLaunchScriptIT { @ParameterizedTest(name = "{0} {1}") @MethodSource("parameters") - public void launchWithRunAs(String os, String version) throws Exception { - String output = doTest(os, version, "launch-with-run-as.sh"); + public void launchWithRunAsUser(String os, String version) throws Exception { + String output = doTest(os, version, "launch-with-run-as-user.sh"); assertThat(output).contains("wagner root"); } @ParameterizedTest(name = "{0} {1}") @MethodSource("parameters") - public void launchWithRunAsInvalidUser(String os, String version) throws Exception { + public void whenRunAsUserDoesNotExistLaunchFailsWithInvalidArgument(String os, String version) throws Exception { String output = doTest(os, version, "launch-with-run-as-invalid-user.sh"); - assertThat(output).contains("Status: 5"); + assertThat(output).contains("Status: 2"); assertThat(output).has(coloredString(AnsiColor.RED, "Cannot run as 'johndoe': no such user")); } @ParameterizedTest(name = "{0} {1}") @MethodSource("parameters") - public void launchWithRunAsPreferUserInformed(String os, String version) throws Exception { - String output = doTest(os, version, "launch-with-run-as-prefer-user-informed.sh"); + public void whenJarOwnerAndRunAsUserAreBothSpecifiedRunAsUserTakesPrecedence(String os, String version) + throws Exception { + String output = doTest(os, version, "launch-with-run-as-user-preferred-to-jar-owner.sh"); assertThat(output).contains("wagner root"); } @ParameterizedTest(name = "{0} {1}") @MethodSource("parameters") - public void launchWithRunAsRootRequired(String os, String version) throws Exception { - String output = doTest(os, version, "launch-with-run-as-root-required.sh"); - assertThat(output).contains("Status: 6"); - assertThat(output).has(coloredString(AnsiColor.RED, "root required to run as 'wagner'")); + public void whenLaunchedUsingNonRootUserWithRunAsUserSpecifiedLaunchFailsWithInsufficientPrivilege(String os, + String version) throws Exception { + String output = doTest(os, version, "launch-with-run-as-user-root-required.sh"); + assertThat(output).contains("Status: 4"); + assertThat(output).has(coloredString(AnsiColor.RED, "Cannot run as 'wagner': current user is not root")); } static List parameters() { diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-prefer-user-informed.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-user-preferred-to-jar-owner.sh similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-prefer-user-informed.sh rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-user-preferred-to-jar-owner.sh diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-root-required.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-user-root-required.sh similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-root-required.sh rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-user-root-required.sh diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-user.sh similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as.sh rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/test/resources/scripts/launch-with-run-as-user.sh