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