summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2018-10-18 10:25:16 +0200
committerMartin Polden <mpolden@mpolden.no>2018-10-18 11:13:07 +0200
commit4f6043b866dc39865eb680bd62fd31cfe1eb9a4a (patch)
tree59bf1ff99d3b57654ecb20e13a3ed6d434b9c64e /controller-server
parent7e7ff62f6d9352cc196a62a67ab64e1bb6c94361 (diff)
Add SSL provider for configuring controller HTTPS connector
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java84
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java8
-rw-r--r--controller-server/src/main/resources/configdefinitions/tls.def9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java33
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java94
6 files changed, 255 insertions, 0 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java
new file mode 100644
index 00000000000..41523be07e4
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java
@@ -0,0 +1,84 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tls;
+
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.container.jdisc.secretstore.SecretStore;
+import com.yahoo.jdisc.http.ssl.SslContextFactoryProvider;
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.vespa.hosted.controller.tls.config.TlsConfig;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Configures the controller's HTTPS connector with certificate and private key from a secret store.
+ *
+ * @author mpolden
+ */
+@SuppressWarnings("unused") // Injected
+public class ControllerSslContextFactoryProvider extends AbstractComponent implements SslContextFactoryProvider {
+
+ private final SecretStore secretStore;
+ private final TlsConfig config;
+ private final SslContextFactory sslContextFactory;
+
+ @Inject
+ public ControllerSslContextFactoryProvider(SecretStore secretStore, TlsConfig config) {
+ this.secretStore = Objects.requireNonNull(secretStore, "secretStore must be non-null");
+ this.config = Objects.requireNonNull(config, "config must be non-null");
+ this.sslContextFactory = create();
+ }
+
+ @Override
+ public SslContextFactory getInstance(String containerId, int port) {
+ return sslContextFactory;
+ }
+
+ /** Create a SslContextFactory backed by an in-memory key and trust store */
+ private SslContextFactory create() {
+ if (!Files.isReadable(Paths.get(config.caTrustStore()))) {
+ throw new IllegalArgumentException("CA trust store file is not readable: " + config.caTrustStore());
+ }
+ SslContextFactory factory = new SslContextFactory();
+
+ // Do not exclude TLS_RSA_* ciphers
+ String[] excludedCiphers = Arrays.stream(factory.getExcludeCipherSuites())
+ .filter(cipherPattern -> !cipherPattern.equals("^TLS_RSA_.*$"))
+ .toArray(String[]::new);
+ factory.setExcludeCipherSuites(excludedCiphers);
+ factory.setWantClientAuth(true);
+
+ // Trust store containing CA trust store from file
+ factory.setTrustStore(KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .fromFile(Paths.get(config.caTrustStore()))
+ .build());
+
+ // Key store containing key pair from secret store
+ factory.setKeyStore(KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry(getClass().getSimpleName(), privateKey(), certificate())
+ .build());
+
+ factory.setKeyStorePassword("");
+ return factory;
+ }
+
+ /** Get private key from secret store */
+ private PrivateKey privateKey() {
+ return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(config.privateKeySecret()));
+ }
+
+ /** Get certificate from secret store */
+ private X509Certificate certificate() {
+ return X509CertificateUtils.fromPem(secretStore.getSecret(config.certificateSecret()));
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java
new file mode 100644
index 00000000000..508b8cc9423
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author mpolden
+ */
+@ExportPackage
+package com.yahoo.vespa.hosted.controller.tls;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/resources/configdefinitions/tls.def b/controller-server/src/main/resources/configdefinitions/tls.def
new file mode 100644
index 00000000000..ddaa1e635db
--- /dev/null
+++ b/controller-server/src/main/resources/configdefinitions/tls.def
@@ -0,0 +1,9 @@
+# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.hosted.controller.tls.config
+
+# Path to the CA trust store
+caTrustStore string
+
+# Secret store key names for certificate and private key
+certificateSecret string default=vespa_hosted.tls.cert
+privateKeySecret string default=vespa_hosted.tls.key
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java
new file mode 100644
index 00000000000..7d1e540b20d
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java
@@ -0,0 +1,33 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tls;
+
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SignatureAlgorithm;
+import com.yahoo.security.X509CertificateBuilder;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * @author mpolden
+ */
+public class Keys {
+
+ public static final KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ public static final X509Certificate certificate = createCertificate(keyPair);
+
+ private static X509Certificate createCertificate(KeyPair keyPair) {
+ Instant now = Instant.now();
+ return X509CertificateBuilder.fromKeypair(keyPair, new X500Principal("CN=localhost"), now,
+ now.plus(Duration.ofDays(1)),
+ SignatureAlgorithm.SHA512_WITH_ECDSA,
+ BigInteger.valueOf(1))
+ .build();
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java
new file mode 100644
index 00000000000..6b5fa6503b3
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java
@@ -0,0 +1,27 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tls;
+
+import com.google.inject.Inject;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.vespa.hosted.controller.tls.config.TlsConfig;
+
+/**
+ * A secret store mock that's pre-populated with a certificate and key.
+ *
+ * @author mpolden
+ */
+@SuppressWarnings("unused") // Injected
+public class SecretStoreMock extends com.yahoo.vespa.hosted.controller.integration.SecretStoreMock {
+
+ @Inject
+ public SecretStoreMock(TlsConfig config) {
+ addKeyPair(config);
+ }
+
+ private void addKeyPair(TlsConfig config) {
+ setSecret(config.privateKeySecret(), KeyUtils.toPem(Keys.keyPair.getPrivate()));
+ setSecret(config.certificateSecret(), X509CertificateUtils.toPem(Keys.certificate));
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java
new file mode 100644
index 00000000000..c2c8f592738
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java
@@ -0,0 +1,94 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tls;
+
+import com.yahoo.application.Networking;
+import com.yahoo.application.container.JDisc;
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.application.container.handler.Response;
+import com.yahoo.component.ComponentId;
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyStoreUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.security.KeyStore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author mpolden
+ */
+public class SecureContainerTest {
+
+ private JDisc container;
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Before
+ public void startContainer() {
+ container = JDisc.fromServicesXml(servicesXml(writeKeyStore()), Networking.enable);
+ }
+
+ @After
+ public void stopContainer() {
+ container.close();
+ }
+
+ @Test
+ public void test_https_request() {
+ assertNotNull("SslContextFactoryProvider is created", sslContextFactoryProvider());
+ assertResponse(Request.Method.GET, "/", 200);
+ }
+
+ private void assertResponse(Request.Method method, String path, int expectedStatusCode) {
+ Response response = container.handleRequest(new Request("https://localhost:9999" + path, new byte[0], method));
+ assertEquals("Status code", expectedStatusCode, response.getStatus());
+ }
+
+ private ControllerSslContextFactoryProvider sslContextFactoryProvider() {
+ return (ControllerSslContextFactoryProvider) container.components().getComponent(ComponentId.fromString("ssl-provider@default"));
+ }
+
+ private String servicesXml(Path trustStore) {
+ return "<jdisc version='1.0'>\n" +
+ " <config name=\"container.handler.threadpool\">\n" +
+ " <maxthreads>10</maxthreads>\n" +
+ " </config> \n" +
+ " <config name='vespa.hosted.controller.tls.config.tls'>\n" +
+ " <caTrustStore>" + trustStore.toString() + "</caTrustStore>\n" +
+ " <certificateSecret>controller.cert</certificateSecret>\n" +
+ " <privateKeySecret>controller.key</privateKeySecret>\n" +
+ " </config>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.tls.SecretStoreMock'/>\n" +
+ " <http>\n" +
+ " <server id='default' port='9999'>\n" +
+ " <ssl-provider class='com.yahoo.vespa.hosted.controller.tls.ControllerSslContextFactoryProvider' bundle='controller-server'/>\n" +
+ " </server>\n" +
+ " </http>\n" +
+ "</jdisc>";
+ }
+
+ private Path writeKeyStore() {
+ KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry(getClass().getSimpleName(),
+ Keys.keyPair.getPrivate(), new char[0], Keys.certificate)
+ .build();
+ try {
+ Path path = folder.newFile().toPath();
+ KeyStoreUtils.writeKeyStoreToFile(keyStore, path);
+ return path;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+}