diff options
author | Martin Polden <mpolden@mpolden.no> | 2018-10-18 10:25:16 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2018-10-18 11:13:07 +0200 |
commit | 4f6043b866dc39865eb680bd62fd31cfe1eb9a4a (patch) | |
tree | 59bf1ff99d3b57654ecb20e13a3ed6d434b9c64e /controller-server | |
parent | 7e7ff62f6d9352cc196a62a67ab64e1bb6c94361 (diff) |
Add SSL provider for configuring controller HTTPS connector
Diffstat (limited to 'controller-server')
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); + } + } + +} |