5959import static org .elasticsearch .packaging .util .Docker .waitForPathToExist ;
6060import static org .elasticsearch .packaging .util .FileMatcher .p600 ;
6161import static org .elasticsearch .packaging .util .FileMatcher .p660 ;
62+ import static org .elasticsearch .packaging .util .FileMatcher .p775 ;
6263import static org .elasticsearch .packaging .util .FileUtils .append ;
6364import static org .elasticsearch .packaging .util .FileUtils .getTempDir ;
6465import static org .elasticsearch .packaging .util .FileUtils .rm ;
7374import static org .hamcrest .Matchers .is ;
7475import static org .hamcrest .Matchers .not ;
7576import static org .hamcrest .Matchers .nullValue ;
77+ import static org .junit .Assume .assumeFalse ;
7678import static org .junit .Assume .assumeTrue ;
7779
7880public class DockerTests extends PackagingTestCase {
@@ -334,10 +336,51 @@ public void test081ConfigurePasswordThroughEnvironmentVariableFile() throws Exce
334336 assertThat ("Expected server to require authentication" , statusCode , equalTo (401 ));
335337 }
336338
339+ /**
340+ * Check that when verifying the file permissions of _FILE environment variables, symlinks
341+ * are followed.
342+ */
343+ public void test082SymlinksAreFollowedWithEnvironmentVariableFiles () throws Exception {
344+ // Test relies on configuring security
345+ assumeTrue (distribution .isDefault ());
346+ // Test relies on symlinks
347+ assumeFalse (Platforms .WINDOWS );
348+
349+ final String xpackPassword = "hunter2" ;
350+ final String passwordFilename = "password.txt" ;
351+ final String symlinkFilename = "password_symlink" ;
352+
353+ // ELASTIC_PASSWORD_FILE
354+ Files .writeString (tempDir .resolve (passwordFilename ), xpackPassword + "\n " );
355+
356+ // Link to the password file. We can't use an absolute path for the target, because
357+ // it won't resolve inside the container.
358+ Files .createSymbolicLink (tempDir .resolve (symlinkFilename ), Path .of (passwordFilename ));
359+
360+ Map <String , String > envVars = Map .of (
361+ "ELASTIC_PASSWORD_FILE" ,
362+ "/run/secrets/" + symlinkFilename ,
363+ // Enable security so that we can test that the password has been used
364+ "xpack.security.enabled" ,
365+ "true"
366+ );
367+
368+ // File permissions need to be secured in order for the ES wrapper to accept
369+ // them for populating env var values. The wrapper will resolve the symlink
370+ // and check the target's permissions.
371+ Files .setPosixFilePermissions (tempDir .resolve (passwordFilename ), p600 );
372+
373+ final Map <Path , Path > volumes = Map .of (tempDir , Path .of ("/run/secrets" ));
374+
375+ // Restart the container - this will check that Elasticsearch started correctly,
376+ // and didn't fail to follow the symlink and check the file permissions
377+ runContainer (distribution (), volumes , envVars );
378+ }
379+
337380 /**
338381 * Check that environment variables cannot be used with _FILE environment variables.
339382 */
340- public void test081CannotUseEnvVarsAndFiles () throws Exception {
383+ public void test083CannotUseEnvVarsAndFiles () throws Exception {
341384 final String optionsFilename = "esJavaOpts.txt" ;
342385
343386 // ES_JAVA_OPTS_FILE
@@ -368,7 +411,7 @@ public void test081CannotUseEnvVarsAndFiles() throws Exception {
368411 * Check that when populating environment variables by setting variables with the suffix "_FILE",
369412 * the files' permissions are checked.
370413 */
371- public void test082EnvironmentVariablesUsingFilesHaveCorrectPermissions () throws Exception {
414+ public void test084EnvironmentVariablesUsingFilesHaveCorrectPermissions () throws Exception {
372415 final String optionsFilename = "esJavaOpts.txt" ;
373416
374417 // ES_JAVA_OPTS_FILE
@@ -390,11 +433,60 @@ public void test082EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws
390433 );
391434 }
392435
436+ /**
437+ * Check that when verifying the file permissions of _FILE environment variables, symlinks
438+ * are followed, and that invalid target permissions are detected.
439+ */
440+ public void test085SymlinkToFileWithInvalidPermissionsIsRejected () throws Exception {
441+ // Test relies on configuring security
442+ assumeTrue (distribution .isDefault ());
443+ // Test relies on symlinks
444+ assumeFalse (Platforms .WINDOWS );
445+
446+ final String xpackPassword = "hunter2" ;
447+ final String passwordFilename = "password.txt" ;
448+ final String symlinkFilename = "password_symlink" ;
449+
450+ // ELASTIC_PASSWORD_FILE
451+ Files .writeString (tempDir .resolve (passwordFilename ), xpackPassword + "\n " );
452+
453+ // Link to the password file. We can't use an absolute path for the target, because
454+ // it won't resolve inside the container.
455+ Files .createSymbolicLink (tempDir .resolve (symlinkFilename ), Path .of (passwordFilename ));
456+
457+ Map <String , String > envVars = Map .of (
458+ "ELASTIC_PASSWORD_FILE" ,
459+ "/run/secrets/" + symlinkFilename ,
460+ // Enable security so that we can test that the password has been used
461+ "xpack.security.enabled" ,
462+ "true"
463+ );
464+
465+ // Set invalid permissions on the file that the symlink targets
466+ Files .setPosixFilePermissions (tempDir .resolve (passwordFilename ), p775 );
467+
468+ final Map <Path , Path > volumes = Map .of (tempDir , Path .of ("/run/secrets" ));
469+
470+ // Restart the container
471+ final Result dockerLogs = runContainerExpectingFailure (distribution (), volumes , envVars );
472+
473+ assertThat (
474+ dockerLogs .stderr ,
475+ containsString (
476+ "ERROR: File "
477+ + passwordFilename
478+ + " (target of symlink /run/secrets/"
479+ + symlinkFilename
480+ + " from ELASTIC_PASSWORD_FILE) must have file permissions 400 or 600, but actually has: 775"
481+ )
482+ );
483+ }
484+
393485 /**
394486 * Check that environment variables are translated to -E options even for commands invoked under
395487 * `docker exec`, where the Docker image's entrypoint is not executed.
396488 */
397- public void test83EnvironmentVariablesAreRespectedUnderDockerExec () {
489+ public void test086EnvironmentVariablesAreRespectedUnderDockerExec () {
398490 // This test relies on a CLI tool attempting to connect to Elasticsearch, and the
399491 // tool in question is only in the default distribution.
400492 assumeTrue (distribution .isDefault ());
@@ -405,10 +497,7 @@ public void test83EnvironmentVariablesAreRespectedUnderDockerExec() {
405497 final Result result = sh .runIgnoreExitCode ("elasticsearch-setup-passwords auto" );
406498
407499 assertFalse ("elasticsearch-setup-passwords command should have failed" , result .isSuccess ());
408- assertThat (
409- result .stdout ,
410- containsString ("java.net.UnknownHostException: this.is.not.valid: Name or service not known" )
411- );
500+ assertThat (result .stdout , containsString ("java.net.UnknownHostException: this.is.not.valid: Name or service not known" ));
412501 }
413502
414503 /**
0 commit comments