3838import static org .elasticsearch .packaging .util .Docker .waitForPathToExist ;
3939import static org .elasticsearch .packaging .util .FileMatcher .p600 ;
4040import static org .elasticsearch .packaging .util .FileMatcher .p660 ;
41+ import static org .elasticsearch .packaging .util .FileMatcher .p775 ;
4142import static org .elasticsearch .packaging .util .FileUtils .append ;
4243import static org .elasticsearch .packaging .util .FileUtils .getTempDir ;
4344import static org .elasticsearch .packaging .util .FileUtils .rm ;
5253import static org .hamcrest .Matchers .is ;
5354import static org .hamcrest .Matchers .not ;
5455import static org .hamcrest .Matchers .nullValue ;
56+ import static org .junit .Assume .assumeFalse ;
5557import static org .junit .Assume .assumeTrue ;
5658
5759import java .io .IOException ;
60+ import java .nio .charset .StandardCharsets ;
5861import java .nio .file .Files ;
5962import java .nio .file .Path ;
6063import java .nio .file .Paths ;
6164import java .util .Arrays ;
62- import java .util .Collections ;
6365import java .util .HashMap ;
6466import java .util .HashSet ;
6567import java .util .List ;
@@ -338,10 +340,48 @@ public void test081ConfigurePasswordThroughEnvironmentVariableFile() throws Exce
338340 assertThat ("Expected server to require authentication" , statusCode , equalTo (401 ));
339341 }
340342
343+ /**
344+ * Check that when verifying the file permissions of _FILE environment variables, symlinks
345+ * are followed.
346+ */
347+ public void test082SymlinksAreFollowedWithEnvironmentVariableFiles () throws Exception {
348+ // Test relies on configuring security
349+ assumeTrue (distribution .isDefault ());
350+ // Test relies on symlinks
351+ assumeFalse (Platforms .WINDOWS );
352+
353+ final String xpackPassword = "hunter2" ;
354+ final String passwordFilename = "password.txt" ;
355+ final String symlinkFilename = "password_symlink" ;
356+
357+ // ELASTIC_PASSWORD_FILE
358+ Files .write (tempDir .resolve (passwordFilename ), (xpackPassword + "\n " ).getBytes (StandardCharsets .UTF_8 ));
359+
360+ // Link to the password file. We can't use an absolute path for the target, because
361+ // it won't resolve inside the container.
362+ Files .createSymbolicLink (tempDir .resolve (symlinkFilename ), Paths .get (passwordFilename ));
363+
364+ // Enable security so that we can test that the password has been used
365+ Map <String , String > envVars = new HashMap <>();
366+ envVars .put ("ELASTIC_PASSWORD_FILE" , "/run/secrets/" + symlinkFilename );
367+ envVars .put ("xpack.security.enabled" , "true" );
368+
369+ // File permissions need to be secured in order for the ES wrapper to accept
370+ // them for populating env var values. The wrapper will resolve the symlink
371+ // and check the target's permissions.
372+ Files .setPosixFilePermissions (tempDir .resolve (passwordFilename ), p600 );
373+
374+ final Map <Path , Path > volumes = singletonMap (tempDir , Paths .get ("/run/secrets" ));
375+
376+ // Restart the container - this will check that Elasticsearch started correctly,
377+ // and didn't fail to follow the symlink and check the file permissions
378+ runContainer (distribution (), volumes , envVars );
379+ }
380+
341381 /**
342382 * Check that environment variables cannot be used with _FILE environment variables.
343383 */
344- public void test081CannotUseEnvVarsAndFiles () throws Exception {
384+ public void test083CannotUseEnvVarsAndFiles () throws Exception {
345385 final String optionsFilename = "esJavaOpts.txt" ;
346386
347387 // ES_JAVA_OPTS_FILE
@@ -369,7 +409,7 @@ public void test081CannotUseEnvVarsAndFiles() throws Exception {
369409 * Check that when populating environment variables by setting variables with the suffix "_FILE",
370410 * the files' permissions are checked.
371411 */
372- public void test082EnvironmentVariablesUsingFilesHaveCorrectPermissions () throws Exception {
412+ public void test084EnvironmentVariablesUsingFilesHaveCorrectPermissions () throws Exception {
373413 final String optionsFilename = "esJavaOpts.txt" ;
374414
375415 // ES_JAVA_OPTS_FILE
@@ -393,25 +433,68 @@ public void test082EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws
393433 );
394434 }
395435
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 .write (tempDir .resolve (passwordFilename ), (xpackPassword + "\n " ).getBytes (StandardCharsets .UTF_8 ));
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 ), Paths .get (passwordFilename ));
456+
457+ // Enable security so that we can test that the password has been used
458+ Map <String , String > envVars = new HashMap <>();
459+ envVars .put ("ELASTIC_PASSWORD_FILE" , "/run/secrets/" + symlinkFilename );
460+ envVars .put ("xpack.security.enabled" , "true" );
461+
462+ // Set invalid permissions on the file that the symlink targets
463+ Files .setPosixFilePermissions (tempDir .resolve (passwordFilename ), p775 );
464+
465+ final Map <Path , Path > volumes = singletonMap (tempDir , Paths .get ("/run/secrets" ));
466+
467+ // Restart the container
468+ final Result dockerLogs = runContainerExpectingFailure (distribution (), volumes , envVars );
469+
470+ assertThat (
471+ dockerLogs .stderr ,
472+ containsString (
473+ "ERROR: File "
474+ + passwordFilename
475+ + " (target of symlink /run/secrets/"
476+ + symlinkFilename
477+ + " from ELASTIC_PASSWORD_FILE) must have file permissions 400 or 600, but actually has: 775"
478+ )
479+ );
480+ }
481+
396482 /**
397483 * Check that environment variables are translated to -E options even for commands invoked under
398484 * `docker exec`, where the Docker image's entrypoint is not executed.
399485 */
400- public void test83EnvironmentVariablesAreRespectedUnderDockerExec () {
486+ public void test086EnvironmentVariablesAreRespectedUnderDockerExec () {
401487 // This test relies on a CLI tool attempting to connect to Elasticsearch, and the
402488 // tool in question is only in the default distribution.
403489 assumeTrue (distribution .isDefault ());
404490
405- runContainer (distribution (), null , Collections . singletonMap ("http.host" , "this.is.not.valid" ));
491+ runContainer (distribution (), null , singletonMap ("http.host" , "this.is.not.valid" ));
406492
407493 // This will fail if the env var above is passed as a -E argument
408494 final Result result = sh .runIgnoreExitCode ("elasticsearch-setup-passwords auto" );
409495
410496 assertFalse ("elasticsearch-setup-passwords command should have failed" , result .isSuccess ());
411- assertThat (
412- result .stdout ,
413- containsString ("java.net.UnknownHostException: this.is.not.valid: Name or service not known" )
414- );
497+ assertThat (result .stdout , containsString ("java.net.UnknownHostException: this.is.not.valid: Name or service not known" ));
415498 }
416499
417500 /**
0 commit comments