Skip to content

Commit 0dfb72e

Browse files
authored
Follow symlinks in Docker entrypoint (#50927)
Closes #49653. When using _FILE environment variables to supply values to Elasticsearch, following symlinks when checking that file permissions are secure.
1 parent ca1a32c commit 0dfb72e

File tree

2 files changed

+104
-11
lines changed

2 files changed

+104
-11
lines changed

distribution/src/bin/elasticsearch-env-from-file

+8-4
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ for VAR_NAME_FILE in $(env | cut -f1 -d= | grep '_FILE$'); do
2424
exit 1
2525
fi
2626

27-
FILE_PERMS="$(stat -c '%a' ${!VAR_NAME_FILE})"
28-
29-
if [[ "$FILE_PERMS" != "400" && "$FILE_PERMS" != 600 ]]; then
30-
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
27+
FILE_PERMS="$(stat -L -c '%a' ${!VAR_NAME_FILE})"
28+
29+
if [[ "$FILE_PERMS" != "400" && "$FILE_PERMS" != "600" ]]; then
30+
if [[ -h "${!VAR_NAME_FILE}" ]]; then
31+
echo "ERROR: File $(readlink "${!VAR_NAME_FILE}") (target of symlink ${!VAR_NAME_FILE} from $VAR_NAME_FILE) must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
32+
else
33+
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
34+
fi
3135
exit 1
3236
fi
3337

qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java

+96-7
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import static org.elasticsearch.packaging.util.Docker.waitForPathToExist;
6060
import static org.elasticsearch.packaging.util.FileMatcher.p600;
6161
import static org.elasticsearch.packaging.util.FileMatcher.p660;
62+
import static org.elasticsearch.packaging.util.FileMatcher.p775;
6263
import static org.elasticsearch.packaging.util.FileUtils.append;
6364
import static org.elasticsearch.packaging.util.FileUtils.getTempDir;
6465
import static org.elasticsearch.packaging.util.FileUtils.rm;
@@ -73,6 +74,7 @@
7374
import static org.hamcrest.Matchers.is;
7475
import static org.hamcrest.Matchers.not;
7576
import static org.hamcrest.Matchers.nullValue;
77+
import static org.junit.Assume.assumeFalse;
7678
import static org.junit.Assume.assumeTrue;
7779

7880
public 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

Comments
 (0)