aboutsummaryrefslogtreecommitdiffstats
path: root/security-tools
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2019-05-31 16:18:56 +0200
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2019-05-31 16:18:56 +0200
commit628432bf640ef1c20c39718dcbb508e4a56bf8b8 (patch)
tree48142da36f0a1e716424dcfe5aaa27c9c963df8a /security-tools
parentb0c953ea8f0c6ad4e757797001de85639b3ccdda (diff)
Add implementation of the 'vespa-security-env' tool
Diffstat (limited to 'security-tools')
-rw-r--r--security-tools/pom.xml11
-rw-r--r--security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/CliOptions.java69
-rw-r--r--security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java78
-rw-r--r--security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/OutputVariable.java29
-rw-r--r--security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/UnixShell.java74
-rw-r--r--security-tools/src/test/java/com/yahoo/vespa/security/tool/securityenv/MainTest.java114
-rw-r--r--security-tools/src/test/resources/bash-output.txt3
-rw-r--r--security-tools/src/test/resources/csh-output.txt3
-rw-r--r--security-tools/src/test/resources/expected-help-output.txt10
9 files changed, 390 insertions, 1 deletions
diff --git a/security-tools/pom.xml b/security-tools/pom.xml
index aa07e96d628..76882927059 100644
--- a/security-tools/pom.xml
+++ b/security-tools/pom.xml
@@ -19,6 +19,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>commons-cli</groupId>
+ <artifactId>commons-cli</artifactId>
+ <version>1.4</version>
+ <scope>compile</scope>
+ </dependency>
<!-- test scope -->
<dependency>
@@ -27,6 +33,11 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/CliOptions.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/CliOptions.java
new file mode 100644
index 00000000000..f3ec73236c4
--- /dev/null
+++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/CliOptions.java
@@ -0,0 +1,69 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.securityenv;
+
+import com.yahoo.security.tls.TransportSecurityUtils;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+import static java.util.stream.Collectors.joining;
+
+/**
+ * Defines the program's command line parameters.
+ *
+ * @author bjorncs
+ */
+class CliOptions {
+ static final String SHELL_OPTION = "shell";
+ static final String HELP_OPTION = "help";
+
+ private static final Options OPTIONS = new Options()
+ .addOption(
+ Option.builder("s")
+ .longOpt(SHELL_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc(String.format("Shell type. Shell type is auto-detected if option not present. Valid values: %s.",
+ Arrays.stream(UnixShell.values())
+ .map(shell -> String.format("'%s'", shell.configName()))
+ .collect(joining(", ", "[", "]"))))
+ .build())
+ .addOption(Option.builder("h")
+ .longOpt(HELP_OPTION)
+ .hasArg(false)
+ .required(false)
+ .desc("Show help")
+ .build());
+
+ static CommandLine parseCliArguments(String[] cliArgs) throws ParseException {
+ CommandLineParser parser = new DefaultParser();
+ return parser.parse(OPTIONS, cliArgs);
+ }
+
+ static void printHelp(PrintStream out) {
+ HelpFormatter formatter = new HelpFormatter();
+ PrintWriter writer = new PrintWriter(out);
+ formatter.printHelp(
+ writer,
+ formatter.getWidth(),
+ "vespa-security-env <options>",
+ String.format("Generates shell commands that defines environments variables based on the content of %s.",
+ TransportSecurityUtils.CONFIG_FILE_ENVIRONMENT_VARIABLE),
+ OPTIONS,
+ formatter.getLeftPadding(),
+ formatter.getDescPadding(),
+ String.format("The output may include the following variables:\n%s\n",
+ Arrays.stream(OutputVariable.values())
+ .map(variable -> String.format(" - '%s': %s", variable.variableName(), variable.description()))
+ .collect(joining("\n"))));
+ writer.flush();
+ }
+}
diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java
index f57575b406a..74c08c2d602 100644
--- a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java
+++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java
@@ -1,11 +1,87 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.security.tool.securityenv;
+import com.yahoo.security.tls.MixedMode;
+import com.yahoo.security.tls.TransportSecurityOptions;
+import com.yahoo.security.tls.TransportSecurityUtils;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.ParseException;
+
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
+
+import static com.yahoo.vespa.security.tool.securityenv.CliOptions.HELP_OPTION;
+import static com.yahoo.vespa.security.tool.securityenv.CliOptions.SHELL_OPTION;
+
/**
+ * Implementation of the 'vespa-security-env' command line utility.
+ *
* @author bjorncs
*/
public class Main {
+
+ private final PrintStream stdOut;
+ private final PrintStream stdError;
+
+ Main(PrintStream stdOut, PrintStream stdError) {
+ this.stdOut = stdOut;
+ this.stdError = stdError;
+ }
+
public static void main(String[] args) {
- System.out.println("TODO implementation");
+ Main program = new Main(System.out, System.err);
+ int statusCode = program.execute(args, System.getenv());
+ System.exit(statusCode);
+ }
+
+ int execute(String[] cliArgs, Map<String, String> envVars) {
+ boolean debugMode = envVars.containsKey("VESPA_DEBUG");
+ try {
+ CommandLine arguments = CliOptions.parseCliArguments(cliArgs);
+ if (arguments.hasOption(HELP_OPTION)) {
+ CliOptions.printHelp(stdOut);
+ return 0;
+ }
+ UnixShell shell = arguments.hasOption(SHELL_OPTION)
+ ? UnixShell.fromConfigName(arguments.getOptionValue(SHELL_OPTION))
+ : UnixShell.detect(envVars.get("SHELL"));
+
+ Optional<TransportSecurityOptions> options = TransportSecurityUtils.getOptions(envVars);
+ if (options.isEmpty()) {
+ return 0;
+ }
+ Map<String, String> outputVariables = new TreeMap<>();
+ options.get().getCaCertificatesFile()
+ .ifPresent(caCertFile -> addOutputVariable(outputVariables, OutputVariable.CA_CERTIFICATE, caCertFile.toString()));
+ MixedMode mixedMode = TransportSecurityUtils.getInsecureMixedMode(envVars);
+ if (mixedMode != MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER) {
+ options.get().getCertificatesFile()
+ .ifPresent(certificateFile -> addOutputVariable(outputVariables, OutputVariable.CERTIFICATE, certificateFile.toString()));
+ options.get().getPrivateKeyFile()
+ .ifPresent(privateKeyFile -> addOutputVariable(outputVariables, OutputVariable.PRIVATE_KEY, privateKeyFile.toString()));
+ }
+ shell.writeOutputVariables(stdOut, outputVariables);
+ return 0;
+ } catch (ParseException e) {
+ return handleException("Failed to parse command line arguments: " + e.getMessage(), e, debugMode);
+ } catch (IllegalArgumentException e) {
+ return handleException("Invalid command line arguments: " + e.getMessage(), e, debugMode);
+ } catch (Exception e) {
+ return handleException("Failed to generate security environment variables: " + e.getMessage(), e, debugMode);
+ }
+ }
+
+ private static void addOutputVariable(Map<String, String> outputVariables, OutputVariable variable, String value) {
+ outputVariables.put(variable.variableName(), value);
+ }
+
+ private int handleException(String message, Exception exception, boolean debugMode) {
+ stdError.println(message);
+ if (debugMode) {
+ exception.printStackTrace(stdError);
+ }
+ return 1;
}
}
diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/OutputVariable.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/OutputVariable.java
new file mode 100644
index 00000000000..9cd4cc1fc67
--- /dev/null
+++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/OutputVariable.java
@@ -0,0 +1,29 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.securityenv;
+
+/**
+ * Define the possible environment variables that the program may output.
+ *
+ * @author bjorncs
+ */
+enum OutputVariable {
+ CA_CERTIFICATE("VESPA_TLS_CA_CERT", "Path to CA certificates file"),
+ CERTIFICATE("VESPA_TLS_CERT", "Path to certificate file"),
+ PRIVATE_KEY("VESPA_TLS_PRIVATE_KEY", "Path to private key file");
+
+ private final String variableName;
+ private final String description;
+
+ OutputVariable(String variableName, String description) {
+ this.variableName = variableName;
+ this.description = description;
+ }
+
+ String variableName() {
+ return variableName;
+ }
+
+ String description() {
+ return description;
+ }
+}
diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/UnixShell.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/UnixShell.java
new file mode 100644
index 00000000000..1b4a9696c69
--- /dev/null
+++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/UnixShell.java
@@ -0,0 +1,74 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.securityenv;
+
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Definition of some unix shell variants and how to export environments variable for those supported.
+ * The output format is inspired by ssh-agent's output.
+ *
+ * @author bjorncs
+ */
+enum UnixShell {
+ BOURNE("bourne", List.of("bash", "sh")) {
+ @Override
+ void writeOutputVariables(PrintStream out, Map<String, String> outputVariables) {
+ outputVariables.forEach((name, value) -> {
+ out.print(name);
+ out.print('=');
+ out.print(value); // note: value is assumed to need no escaping
+ out.print("; export ");
+ out.print(name);
+ out.println(';');
+ });
+ }
+ },
+ CSHELL("cshell", List.of("csh", "fish")) {
+ @Override
+ void writeOutputVariables(PrintStream out, Map<String, String> outputVariables) {
+ outputVariables.forEach((name, value) -> {
+ out.print("setenv ");
+ out.print(name);
+ out.print(' ');
+ out.print(value); // note: value is assumed to need no escaping
+ out.println(';');
+ });
+ }
+ };
+
+ private static final UnixShell DEFAULT = BOURNE;
+
+ private final String configName;
+ private final List<String> knownShellBinaries;
+
+ UnixShell(String configName, List<String> knownShellBinaries) {
+ this.configName = configName;
+ this.knownShellBinaries = knownShellBinaries;
+ }
+
+ abstract void writeOutputVariables(PrintStream out, Map<String, String> outputVariables);
+
+ String configName() {
+ return configName;
+ }
+
+ static UnixShell fromConfigName(String configName) {
+ return Arrays.stream(values())
+ .filter(shell -> shell.configName.equals(configName))
+ .findAny()
+ .orElseThrow(() -> new IllegalArgumentException("Unknown shell: " + configName));
+ }
+
+ static UnixShell detect(String shellEnvVariable) {
+ if (shellEnvVariable == null || shellEnvVariable.isEmpty()) return DEFAULT;
+ int lastSlash = shellEnvVariable.lastIndexOf('/');
+ String shellName = lastSlash != -1 ? shellEnvVariable.substring(lastSlash + 1) : shellEnvVariable;
+ return Arrays.stream(values())
+ .filter(shell -> shell.knownShellBinaries.contains(shellName))
+ .findAny()
+ .orElse(DEFAULT);
+ }
+}
diff --git a/security-tools/src/test/java/com/yahoo/vespa/security/tool/securityenv/MainTest.java b/security-tools/src/test/java/com/yahoo/vespa/security/tool/securityenv/MainTest.java
new file mode 100644
index 00000000000..6b25c2a2bce
--- /dev/null
+++ b/security-tools/src/test/java/com/yahoo/vespa/security/tool/securityenv/MainTest.java
@@ -0,0 +1,114 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.securityenv;
+
+import com.yahoo.security.tls.MixedMode;
+import com.yahoo.security.tls.TransportSecurityOptions;
+import com.yahoo.security.tls.TransportSecurityUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/**
+ * @author bjorncs
+ */
+public class MainTest {
+
+ private final ByteArrayOutputStream stdOutBytes = new ByteArrayOutputStream();
+ private final ByteArrayOutputStream stdErrBytes = new ByteArrayOutputStream();
+ private final PrintStream stdOut = new PrintStream(stdOutBytes);
+ private final PrintStream stdError = new PrintStream(stdErrBytes);
+
+ @Rule
+ public TemporaryFolder tmpFolder = new TemporaryFolder();
+
+ @Test
+ public void prints_help_page_on_help_option() throws IOException {
+ int exitCode = runMain(List.of("--help"), Map.of());
+ assertThat(exitCode).isEqualTo(0);
+ assertThat(stdOut()).isEqualTo(readTestResource("expected-help-output.txt"));
+ }
+
+ @Test
+ public void prints_no_output_when_no_security_config() {
+ int exitCode = runMain(List.of(), Map.of());
+ assertThat(exitCode).isEqualTo(0);
+ assertThat(stdErr()).isEmpty();
+ }
+
+ @Test
+ public void prints_security_variables_with_specified_shell() throws IOException {
+ Path configFile = generateConfigFile();
+ Map<String, String> env = Map.of(TransportSecurityUtils.CONFIG_FILE_ENVIRONMENT_VARIABLE, configFile.toString());
+ int exitCode = runMain(List.of(), env);
+ assertThat(exitCode).isEqualTo(0);
+ assertThat(stdOut()).isEqualTo(readTestResource("bash-output.txt"));
+ }
+
+ @Test
+ public void prints_security_variables_with_auto_detected_shell() throws IOException {
+ Path configFile = generateConfigFile();
+ Map<String, String> env = Map.of(
+ TransportSecurityUtils.CONFIG_FILE_ENVIRONMENT_VARIABLE, configFile.toString(),
+ TransportSecurityUtils.INSECURE_MIXED_MODE_ENVIRONMENT_VARIABLE, MixedMode.TLS_CLIENT_MIXED_SERVER.configValue(),
+ "SHELL", "/usr/local/bin/fish");
+ int exitCode = runMain(List.of(), env);
+ assertThat(exitCode).isEqualTo(0);
+ assertThat(stdOut()).isEqualTo(readTestResource("csh-output.txt"));
+ }
+
+
+ @Test
+ public void prints_error_message_on_unknown_shell_name() {
+ int exitCode = runMain(List.of("--shell", "invalid-shell-name"), Map.of());
+ assertThat(exitCode).isEqualTo(1);
+ assertThat(stdErr()).isEqualTo("Invalid command line arguments: Unknown shell: invalid-shell-name\n");
+ }
+
+ @Test
+ public void prints_error_message_on_unknown_command_line_parameter() {
+ int exitCode = runMain(List.of("--unknown-parameter"), Map.of());
+ assertThat(exitCode).isEqualTo(1);
+ assertThat(stdErr()).isEqualTo("Failed to parse command line arguments: Unrecognized option: --unknown-parameter\n");
+ }
+
+ private int runMain(List<String> args, Map<String, String> env) {
+ return new Main(stdOut, stdError).execute(args.toArray(new String[0]), env);
+ }
+
+ private String stdOut() {
+ stdOut.flush();
+ return stdOutBytes.toString();
+ }
+
+ private String stdErr() {
+ stdError.flush();
+ return stdErrBytes.toString();
+ }
+
+ private static String readTestResource(String fileName) throws IOException {
+ return Files.readString(Paths.get(MainTest.class.getResource('/' + fileName).getFile()));
+ }
+
+ private Path generateConfigFile() throws IOException {
+ TransportSecurityOptions options = new TransportSecurityOptions.Builder()
+ .withCertificates(Paths.get("/path/to/certificate"), Paths.get("/path/to/key"))
+ .withCaCertificates(Paths.get("/path/to/cacerts"))
+ .build();
+ Path configFile = tmpFolder.newFile().toPath();
+ options.toJsonFile(configFile);
+ return configFile;
+ }
+
+} \ No newline at end of file
diff --git a/security-tools/src/test/resources/bash-output.txt b/security-tools/src/test/resources/bash-output.txt
new file mode 100644
index 00000000000..9d603883953
--- /dev/null
+++ b/security-tools/src/test/resources/bash-output.txt
@@ -0,0 +1,3 @@
+VESPA_TLS_CA_CERT=/path/to/cacerts; export VESPA_TLS_CA_CERT;
+VESPA_TLS_CERT=/path/to/certificate; export VESPA_TLS_CERT;
+VESPA_TLS_PRIVATE_KEY=/path/to/key; export VESPA_TLS_PRIVATE_KEY;
diff --git a/security-tools/src/test/resources/csh-output.txt b/security-tools/src/test/resources/csh-output.txt
new file mode 100644
index 00000000000..6db3e613d90
--- /dev/null
+++ b/security-tools/src/test/resources/csh-output.txt
@@ -0,0 +1,3 @@
+setenv VESPA_TLS_CA_CERT /path/to/cacerts;
+setenv VESPA_TLS_CERT /path/to/certificate;
+setenv VESPA_TLS_PRIVATE_KEY /path/to/key;
diff --git a/security-tools/src/test/resources/expected-help-output.txt b/security-tools/src/test/resources/expected-help-output.txt
new file mode 100644
index 00000000000..e16f1b1dab0
--- /dev/null
+++ b/security-tools/src/test/resources/expected-help-output.txt
@@ -0,0 +1,10 @@
+usage: vespa-security-env <options>
+Generates shell commands that defines environments variables based on the
+content of VESPA_TLS_CONFIG_FILE.
+ -h,--help Show help
+ -s,--shell <arg> Shell type. Shell type is auto-detected if option not
+ present. Valid values: ['bourne', 'cshell'].
+The output may include the following variables:
+ - 'VESPA_TLS_CA_CERT': Path to CA certificates file
+ - 'VESPA_TLS_CERT': Path to certificate file
+ - 'VESPA_TLS_PRIVATE_KEY': Path to private key file