Skip to content

Commit cb43513

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

File tree

2 files changed

+100
-13
lines changed

2 files changed

+100
-13
lines changed

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

Lines changed: 8 additions & 4 deletions
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

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import static org.elasticsearch.packaging.util.Docker.waitForPathToExist;
3939
import static org.elasticsearch.packaging.util.FileMatcher.p600;
4040
import static org.elasticsearch.packaging.util.FileMatcher.p660;
41+
import static org.elasticsearch.packaging.util.FileMatcher.p775;
4142
import static org.elasticsearch.packaging.util.FileUtils.append;
4243
import static org.elasticsearch.packaging.util.FileUtils.getTempDir;
4344
import static org.elasticsearch.packaging.util.FileUtils.rm;
@@ -52,14 +53,15 @@
5253
import static org.hamcrest.Matchers.is;
5354
import static org.hamcrest.Matchers.not;
5455
import static org.hamcrest.Matchers.nullValue;
56+
import static org.junit.Assume.assumeFalse;
5557
import static org.junit.Assume.assumeTrue;
5658

5759
import java.io.IOException;
60+
import java.nio.charset.StandardCharsets;
5861
import java.nio.file.Files;
5962
import java.nio.file.Path;
6063
import java.nio.file.Paths;
6164
import java.util.Arrays;
62-
import java.util.Collections;
6365
import java.util.HashMap;
6466
import java.util.HashSet;
6567
import 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

Comments
 (0)