diff options
5 files changed, 385 insertions, 338 deletions
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java index d676dc29c94..2f962855470 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java @@ -2,10 +2,20 @@ package com.yahoo.vespa.model.container.xml; import com.yahoo.component.ComponentId; +import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.builder.xml.test.DomBuilderTest; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; +import com.yahoo.config.model.test.MockApplicationPackage; import com.yahoo.config.provision.AthenzDomain; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Zone; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.path.Path; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.security.tls.TlsContext; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.model.container.ApplicationContainer; import com.yahoo.vespa.model.container.http.AccessControl; @@ -13,8 +23,14 @@ import com.yahoo.vespa.model.container.http.ConnectorFactory; import com.yahoo.vespa.model.container.http.FilterChains; import com.yahoo.vespa.model.container.http.Http; import com.yahoo.vespa.model.container.http.ssl.HostedSslConnectorFactory; +import org.hamcrest.Matchers; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.w3c.dom.Element; +import java.io.StringReader; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -22,6 +38,8 @@ import java.util.Set; import java.util.stream.Collectors; import static com.yahoo.vespa.defaults.Defaults.getDefaults; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -30,9 +48,13 @@ import static org.junit.Assert.fail; /** * @author gjoranv * @author bjorncs + * @author mortent */ public class AccessControlTest extends ContainerModelBuilderTestBase { + @Rule + public TemporaryFolder applicationFolder = new TemporaryFolder(); + @Test public void access_control_filter_chains_are_set_up() { Http http = createModelAndGetHttp( @@ -318,6 +340,182 @@ public class AccessControlTest extends ContainerModelBuilderTestBase { assertEquals(AccessControl.DEFAULT_CONNECTOR_HOSTED_REQUEST_CHAIN_ID, defaultChain.get()); } + @Test + public void client_authentication_is_enforced() { + Element clusterElem = DomBuilderTest.parse( + "<container version='1.0'>", + nodesXml, + " <http><filtering>" + + " <access-control domain=\"vespa\" tls-handshake-client-auth=\"need\"/>" + + " </filtering></http>" + + "</container>" ); + + DeployState state = new DeployState.Builder().properties( + new TestProperties() + .setHostedVespa(true) + .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))) + .build(); + createModel(root, state, null, clusterElem); + ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); + + List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); + ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); + + ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); + tlsPort.getConfig(builder); + + ConnectorConfig connectorConfig = new ConnectorConfig(builder); + assertTrue(connectorConfig.ssl().enabled()); + assertEquals(ConnectorConfig.Ssl.ClientAuth.Enum.NEED_AUTH, connectorConfig.ssl().clientAuth()); + assertEquals("CERT", connectorConfig.ssl().certificate()); + assertEquals("KEY", connectorConfig.ssl().privateKey()); + assertEquals(4443, connectorConfig.listenPort()); + + assertEquals("Connector must use Athenz truststore in a non-public system.", + "/opt/yahoo/share/ssl/certs/athenz_certificate_bundle.pem", + connectorConfig.ssl().caCertificateFile()); + assertTrue(connectorConfig.ssl().caCertificate().isEmpty()); + } + + @Test + public void missing_security_clients_pem_fails_in_public() { + Element clusterElem = DomBuilderTest.parse("<container version='1.0' />"); + + try { + DeployState state = new DeployState.Builder() + .properties( + new TestProperties() + .setHostedVespa(true) + .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))) + .zone(new Zone(SystemName.Public, Environment.prod, RegionName.defaultName())) + .build(); + createModel(root, state, null, clusterElem); + } catch (RuntimeException e) { + assertEquals("Client certificate authority security/clients.pem is missing - see: https://cloud.vespa.ai/en/security-model#data-plane", + e.getMessage()); + return; + } + fail(); + } + + @Test + public void security_clients_pem_is_picked_up() { + var applicationPackage = new MockApplicationPackage.Builder() + .withRoot(applicationFolder.getRoot()) + .build(); + + applicationPackage.getFile(Path.fromString("security")).createDirectory(); + applicationPackage.getFile(Path.fromString("security/clients.pem")).writeFile(new StringReader("I am a very nice certificate")); + + var deployState = DeployState.createTestState(applicationPackage); + + Element clusterElem = DomBuilderTest.parse("<container version='1.0' />"); + + createModel(root, deployState, null, clusterElem); + assertEquals(Optional.of("I am a very nice certificate"), getContainerCluster("container").getTlsClientAuthority()); + } + + @Test + public void operator_certificates_are_joined_with_clients_pem() { + var applicationPackage = new MockApplicationPackage.Builder() + .withRoot(applicationFolder.getRoot()) + .build(); + + var applicationTrustCert = X509CertificateUtils.toPem( + X509CertificateUtils.createSelfSigned("CN=application", Duration.ofDays(1)).certificate()); + var operatorCert = X509CertificateUtils.createSelfSigned("CN=operator", Duration.ofDays(1)).certificate(); + + applicationPackage.getFile(Path.fromString("security")).createDirectory(); + applicationPackage.getFile(Path.fromString("security/clients.pem")).writeFile(new StringReader(applicationTrustCert)); + + var deployState = new DeployState.Builder().properties( + new TestProperties() + .setOperatorCertificates(List.of(operatorCert)) + .setHostedVespa(true) + .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))) + .zone(new Zone(SystemName.PublicCd, Environment.dev, RegionName.defaultName())) + .applicationPackage(applicationPackage) + .build(); + + Element clusterElem = DomBuilderTest.parse("<container version='1.0' />"); + + createModel(root, deployState, null, clusterElem); + + ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); + List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); + ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); + + ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); + tlsPort.getConfig(builder); + + ConnectorConfig connectorConfig = new ConnectorConfig(builder); + var caCerts = X509CertificateUtils.certificateListFromPem(connectorConfig.ssl().caCertificate()); + assertEquals(2, caCerts.size()); + List<String> certnames = caCerts.stream() + .map(cert -> cert.getSubjectX500Principal().getName()) + .collect(Collectors.toList()); + assertThat(certnames, containsInAnyOrder("CN=operator", "CN=application")); + } + + @Test + public void require_allowed_ciphers() { + Element clusterElem = DomBuilderTest.parse( + "<container version='1.0'>", + nodesXml, + "</container>" ); + + DeployState state = new DeployState.Builder().properties(new TestProperties().setHostedVespa(true).setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))).build(); + createModel(root, state, null, clusterElem); + ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); + + List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); + ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); + ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); + tlsPort.getConfig(builder); + + ConnectorConfig connectorConfig = new ConnectorConfig(builder); + + assertThat(connectorConfig.ssl().enabledCipherSuites(), containsInAnyOrder(TlsContext.ALLOWED_CIPHER_SUITES.toArray())); + } + + @Test + public void providing_endpoint_certificate_secrets_opens_port_4443() { + Element clusterElem = DomBuilderTest.parse( + "<container version='1.0'>", + nodesXml, + "</container>" ); + + DeployState state = new DeployState.Builder().properties(new TestProperties().setHostedVespa(true).setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))).build(); + createModel(root, state, null, clusterElem); + ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); + + // Verify that there are two connectors + List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); + assertEquals(2, connectorFactories.size()); + List<Integer> ports = connectorFactories.stream() + .map(ConnectorFactory::getListenPort) + .collect(Collectors.toList()); + assertThat(ports, Matchers.containsInAnyOrder(8080, 4443)); + + ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); + + ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); + tlsPort.getConfig(builder); + + + ConnectorConfig connectorConfig = new ConnectorConfig(builder); + assertTrue(connectorConfig.ssl().enabled()); + assertEquals(ConnectorConfig.Ssl.ClientAuth.Enum.WANT_AUTH, connectorConfig.ssl().clientAuth()); + assertEquals("CERT", connectorConfig.ssl().certificate()); + assertEquals("KEY", connectorConfig.ssl().privateKey()); + assertEquals(4443, connectorConfig.listenPort()); + + assertEquals("Connector must use Athenz truststore in a non-public system.", + "/opt/yahoo/share/ssl/certs/athenz_certificate_bundle.pem", + connectorConfig.ssl().caCertificateFile()); + assertTrue(connectorConfig.ssl().caCertificate().isEmpty()); + } + private Http createModelAndGetHttp(String... httpElement) { AthenzDomain tenantDomain = AthenzDomain.from("my-tenant-domain"); DeployState state = new DeployState.Builder().properties( @@ -327,6 +525,7 @@ public class AccessControlTest extends ContainerModelBuilderTestBase { .build(); return createModelAndGetHttp(state, httpElement); } + private Http createModelAndGetHttp(DeployState state, String... httpElement) { List<String> servicesXml = new ArrayList<>(); servicesXml.add("<container version='1.0'>"); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java index 0486d463279..81b6de8f2ee 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java @@ -2,7 +2,14 @@ package com.yahoo.vespa.model.container.xml; import com.yahoo.component.ComponentId; +import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.builder.xml.test.DomBuilderTest; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.model.deploy.TestProperties; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Zone; import com.yahoo.container.core.AccessLogConfig; import com.yahoo.container.logging.ConnectionLogConfig; import com.yahoo.container.logging.FileConnectionLog; @@ -13,8 +20,12 @@ import com.yahoo.vespa.model.container.component.Component; import org.junit.Test; import org.w3c.dom.Element; +import java.util.logging.Level; + +import static com.yahoo.config.model.test.TestUtil.joinLines; import static com.yahoo.text.StringUtilities.quote; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -132,4 +143,38 @@ public class AccessLogTest extends ContainerModelBuilderTestBase { Component<?, ?> fileConnectionLogComponent = getContainerComponent("default", FileConnectionLog.class.getName()); assertNull(fileConnectionLogComponent); } + + @Test + public void hosted_applications_get_a_log_warning_when_overriding_accesslog() { + String containerService = joinLines("<container id='foo' version='1.0'>", + " <accesslog type='json' fileNamePattern='logs/vespa/qrs/access.%Y%m%d%H%M%S' symlinkName='json_access' />", + " <nodes count=\"2\">", + " </nodes>", + "</container>"); + + String deploymentXml = joinLines("<deployment version='1.0'>", + " <prod>", + " <region>us-east-1</region>", + " </prod>", + "</deployment>"); + + ApplicationPackage applicationPackage = new MockApplicationPackage.Builder() + .withServices(containerService) + .withDeploymentSpec(deploymentXml) + .build(); + + TestLogger logger = new TestLogger(); + DeployState deployState = new DeployState.Builder() + .applicationPackage(applicationPackage) + .zone(new Zone(Environment.prod, RegionName.from("us-east-1"))) + .properties(new TestProperties().setHostedVespa(true)) + .deployLogger(logger) + .build(); + createModel(root, deployState, null, DomBuilderTest.parse(containerService)); + assertFalse(logger.msgs.isEmpty()); + assertEquals(Level.WARNING, logger.msgs.get(0).getFirst()); + assertEquals("Applications are not allowed to override the 'accesslog' element", + logger.msgs.get(0).getSecond()); + } + } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java index b65570d29cc..3a241d9607f 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java @@ -7,9 +7,7 @@ import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.NullConfigModelRegistry; import com.yahoo.config.model.api.ApplicationClusterEndpoint; import com.yahoo.config.model.api.ContainerEndpoint; -import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.api.ModelContext; -import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.model.builder.xml.test.DomBuilderTest; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; @@ -21,7 +19,6 @@ import com.yahoo.config.model.test.MockRoot; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.RegionName; -import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.Zone; import com.yahoo.config.provisioning.FlavorsConfig; import com.yahoo.container.ComponentsConfig; @@ -33,15 +30,10 @@ import com.yahoo.container.handler.VipStatusHandler; import com.yahoo.container.handler.metrics.MetricsV2Handler; import com.yahoo.container.handler.observability.ApplicationStatusHandler; import com.yahoo.container.jdisc.JdiscBindingsConfig; -import com.yahoo.container.jdisc.secretstore.SecretStoreConfig; import com.yahoo.container.usability.BindingsOverviewHandler; -import com.yahoo.jdisc.http.ConnectorConfig; import com.yahoo.net.HostName; -import com.yahoo.path.Path; import com.yahoo.prelude.cluster.QrMonitorConfig; import com.yahoo.search.config.QrStartConfig; -import com.yahoo.security.X509CertificateUtils; -import com.yahoo.security.tls.TlsContext; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.model.AbstractService; import com.yahoo.vespa.model.VespaModel; @@ -49,25 +41,17 @@ import com.yahoo.vespa.model.container.ApplicationContainer; import com.yahoo.vespa.model.container.ApplicationContainerCluster; import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.container.ContainerModelEvaluation; -import com.yahoo.vespa.model.container.SecretStore; import com.yahoo.vespa.model.container.component.Component; -import com.yahoo.vespa.model.container.http.ConnectorFactory; import com.yahoo.vespa.model.content.utils.ContentClusterUtils; import com.yahoo.vespa.model.test.VespaModelTester; import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg; -import org.hamcrest.Matchers; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TemporaryFolder; import org.w3c.dom.Element; import org.xml.sax.SAXException; import java.io.IOException; -import java.io.StringReader; -import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.logging.Level; @@ -80,11 +64,10 @@ import static com.yahoo.vespa.model.container.ContainerCluster.ROOT_HANDLER_BIND import static com.yahoo.vespa.model.container.ContainerCluster.STATE_HANDLER_BINDING_1; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -103,9 +86,6 @@ import static org.junit.Assert.fail; */ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { - @Rule - public TemporaryFolder applicationFolder = new TemporaryFolder(); - @Test public void model_evaluation_bundles_are_deployed() { createBasicContainerModel(); @@ -556,6 +536,7 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { assertEquals(50.0, qr.shutdown().timeout(), 0.00000000000001); assertFalse(qr.shutdown().dumpHeapOnTimeout()); } + private QrConfig getQrConfig(ModelContext.Properties properties) throws IOException, SAXException { String servicesXml = "<services>" + @@ -588,184 +569,6 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { } @Test - public void secret_store_can_be_set_up() { - Element clusterElem = DomBuilderTest.parse( - "<container version='1.0'>", - " <secret-store type='oath-ckms'>", - " <group name='group1' environment='env1'/>", - " </secret-store>", - "</container>"); - createModel(root, clusterElem); - SecretStore secretStore = getContainerCluster("container").getSecretStore().get(); - assertEquals("group1", secretStore.getGroups().get(0).name); - assertEquals("env1", secretStore.getGroups().get(0).environment); - } - - @Test - public void cloud_secret_store_requires_configured_secret_store() { - Element clusterElem = DomBuilderTest.parse( - "<container version='1.0'>", - " <secret-store type='cloud'>", - " <store id='store'>", - " <aws-parameter-store account='store1' region='eu-north-1'/>", - " </store>", - " </secret-store>", - "</container>"); - try { - DeployState state = new DeployState.Builder() - .properties(new TestProperties().setHostedVespa(true)) - .zone(new Zone(SystemName.Public, Environment.prod, RegionName.defaultName())) - .build(); - createModel(root, state, null, clusterElem); - fail("secret store not defined"); - } catch (RuntimeException e) { - assertEquals("No configured secret store named store1", e.getMessage()); - } - } - - - @Test - public void cloud_secret_store_can_be_set_up() { - Element clusterElem = DomBuilderTest.parse( - "<container version='1.0'>", - " <secret-store type='cloud'>", - " <store id='store'>", - " <aws-parameter-store account='store1' region='eu-north-1'/>", - " </store>", - " </secret-store>", - "</container>"); - - DeployState state = new DeployState.Builder() - .properties( - new TestProperties() - .setHostedVespa(true) - .setTenantSecretStores(List.of(new TenantSecretStore("store1", "1234", "role", Optional.of("externalid"))))) - .zone(new Zone(SystemName.Public, Environment.prod, RegionName.defaultName())) - .build(); - createModel(root, state, null, clusterElem); - - ApplicationContainerCluster container = getContainerCluster("container"); - assertComponentConfigured(container, "com.yahoo.jdisc.cloud.aws.AwsParameterStore"); - CloudSecretStore secretStore = (CloudSecretStore) container.getComponentsMap().get(ComponentId.fromString("com.yahoo.jdisc.cloud.aws.AwsParameterStore")); - - - SecretStoreConfig.Builder configBuilder = new SecretStoreConfig.Builder(); - secretStore.getConfig(configBuilder); - SecretStoreConfig secretStoreConfig = configBuilder.build(); - - assertEquals(1, secretStoreConfig.awsParameterStores().size()); - assertEquals("store1", secretStoreConfig.awsParameterStores().get(0).name()); - } - - @Test - public void cloud_secret_store_fails_to_set_up_in_non_public_zone() { - try { - Element clusterElem = DomBuilderTest.parse( - "<container version='1.0'>", - " <secret-store type='cloud'>", - " <store id='store'>", - " <aws-parameter-store account='store1' region='eu-north-1'/>", - " </store>", - " </secret-store>", - "</container>"); - - DeployState state = new DeployState.Builder() - .properties( - new TestProperties() - .setHostedVespa(true) - .setTenantSecretStores(List.of(new TenantSecretStore("store1", "1234", "role", Optional.of("externalid"))))) - .zone(new Zone(SystemName.main, Environment.prod, RegionName.defaultName())) - .build(); - createModel(root, state, null, clusterElem); - } catch (RuntimeException e) { - assertEquals("Cloud secret store is not supported in non-public system, see the documentation", - e.getMessage()); - return; - } - fail(); - } - - @Test - public void missing_security_clients_pem_fails_in_public() { - Element clusterElem = DomBuilderTest.parse("<container version='1.0' />"); - - try { - DeployState state = new DeployState.Builder() - .properties( - new TestProperties() - .setHostedVespa(true) - .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))) - .zone(new Zone(SystemName.Public, Environment.prod, RegionName.defaultName())) - .build(); - createModel(root, state, null, clusterElem); - } catch (RuntimeException e) { - assertEquals("Client certificate authority security/clients.pem is missing - see: https://cloud.vespa.ai/en/security-model#data-plane", - e.getMessage()); - return; - } - fail(); - } - - @Test - public void security_clients_pem_is_picked_up() { - var applicationPackage = new MockApplicationPackage.Builder() - .withRoot(applicationFolder.getRoot()) - .build(); - - applicationPackage.getFile(Path.fromString("security")).createDirectory(); - applicationPackage.getFile(Path.fromString("security/clients.pem")).writeFile(new StringReader("I am a very nice certificate")); - - var deployState = DeployState.createTestState(applicationPackage); - - Element clusterElem = DomBuilderTest.parse("<container version='1.0' />"); - - createModel(root, deployState, null, clusterElem); - assertEquals(Optional.of("I am a very nice certificate"), getContainerCluster("container").getTlsClientAuthority()); - } - - @Test - public void operator_certificates_are_joined_with_clients_pem() { - var applicationPackage = new MockApplicationPackage.Builder() - .withRoot(applicationFolder.getRoot()) - .build(); - - var applicationTrustCert = X509CertificateUtils.toPem( - X509CertificateUtils.createSelfSigned("CN=application", Duration.ofDays(1)).certificate()); - var operatorCert = X509CertificateUtils.createSelfSigned("CN=operator", Duration.ofDays(1)).certificate(); - - applicationPackage.getFile(Path.fromString("security")).createDirectory(); - applicationPackage.getFile(Path.fromString("security/clients.pem")).writeFile(new StringReader(applicationTrustCert)); - - var deployState = new DeployState.Builder().properties( - new TestProperties() - .setOperatorCertificates(List.of(operatorCert)) - .setHostedVespa(true) - .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))) - .zone(new Zone(SystemName.PublicCd, Environment.dev, RegionName.defaultName())) - .applicationPackage(applicationPackage) - .build(); - - Element clusterElem = DomBuilderTest.parse("<container version='1.0' />"); - - createModel(root, deployState, null, clusterElem); - - ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); - List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); - ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); - - ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); - tlsPort.getConfig(builder); - - ConnectorConfig connectorConfig = new ConnectorConfig(builder); - var caCerts = X509CertificateUtils.certificateListFromPem(connectorConfig.ssl().caCertificate()); - assertEquals(2, caCerts.size()); - List<String> certnames = caCerts.stream() - .map(cert -> cert.getSubjectX500Principal().getName()) - .collect(Collectors.toList()); - assertThat(certnames, containsInAnyOrder("CN=operator", "CN=application")); - } - - @Test public void environment_vars_are_honoured() { Element clusterElem = DomBuilderTest.parse( "<container version='1.0'>", @@ -816,102 +619,6 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { } @Test - public void requireThatProvidingEndpointCertificateSecretsOpensPort4443() { - Element clusterElem = DomBuilderTest.parse( - "<container version='1.0'>", - nodesXml, - "</container>" ); - - DeployState state = new DeployState.Builder().properties(new TestProperties().setHostedVespa(true).setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))).build(); - createModel(root, state, null, clusterElem); - ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); - - // Verify that there are two connectors - List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); - assertEquals(2, connectorFactories.size()); - List<Integer> ports = connectorFactories.stream() - .map(ConnectorFactory::getListenPort) - .collect(Collectors.toList()); - assertThat(ports, Matchers.containsInAnyOrder(8080, 4443)); - - ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); - - ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); - tlsPort.getConfig(builder); - - - ConnectorConfig connectorConfig = new ConnectorConfig(builder); - assertTrue(connectorConfig.ssl().enabled()); - assertEquals(ConnectorConfig.Ssl.ClientAuth.Enum.WANT_AUTH, connectorConfig.ssl().clientAuth()); - assertEquals("CERT", connectorConfig.ssl().certificate()); - assertEquals("KEY", connectorConfig.ssl().privateKey()); - assertEquals(4443, connectorConfig.listenPort()); - - assertEquals("Connector must use Athenz truststore in a non-public system.", - "/opt/yahoo/share/ssl/certs/athenz_certificate_bundle.pem", - connectorConfig.ssl().caCertificateFile()); - assertTrue(connectorConfig.ssl().caCertificate().isEmpty()); - } - - @Test - public void requireThatClientAuthenticationIsEnforced() { - Element clusterElem = DomBuilderTest.parse( - "<container version='1.0'>", - nodesXml, - " <http><filtering>" + - " <access-control domain=\"vespa\" tls-handshake-client-auth=\"need\"/>" + - " </filtering></http>" + - "</container>" ); - - DeployState state = new DeployState.Builder().properties( - new TestProperties() - .setHostedVespa(true) - .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))) - .build(); - createModel(root, state, null, clusterElem); - ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); - - List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); - ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); - - ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); - tlsPort.getConfig(builder); - - ConnectorConfig connectorConfig = new ConnectorConfig(builder); - assertTrue(connectorConfig.ssl().enabled()); - assertEquals(ConnectorConfig.Ssl.ClientAuth.Enum.NEED_AUTH, connectorConfig.ssl().clientAuth()); - assertEquals("CERT", connectorConfig.ssl().certificate()); - assertEquals("KEY", connectorConfig.ssl().privateKey()); - assertEquals(4443, connectorConfig.listenPort()); - - assertEquals("Connector must use Athenz truststore in a non-public system.", - "/opt/yahoo/share/ssl/certs/athenz_certificate_bundle.pem", - connectorConfig.ssl().caCertificateFile()); - assertTrue(connectorConfig.ssl().caCertificate().isEmpty()); - } - - @Test - public void require_allowed_ciphers() { - Element clusterElem = DomBuilderTest.parse( - "<container version='1.0'>", - nodesXml, - "</container>" ); - - DeployState state = new DeployState.Builder().properties(new TestProperties().setHostedVespa(true).setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY")))).build(); - createModel(root, state, null, clusterElem); - ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); - - List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories(); - ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); - ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); - tlsPort.getConfig(builder); - - ConnectorConfig connectorConfig = new ConnectorConfig(builder); - - assertThat(connectorConfig.ssl().enabledCipherSuites(), containsInAnyOrder(TlsContext.ALLOWED_CIPHER_SUITES.toArray())); - } - - @Test public void cluster_with_zookeeper() { Function<Integer, String> servicesXml = (nodeCount) -> "<container version='1.0' id='default'>" + "<nodes count='" + nodeCount + "'/>" + @@ -995,44 +702,6 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { logger.msgs.get(1).getSecond()); } - @Test - public void logs_accesslog_not_overidable_in_hosted() { - String containerService = joinLines("<container id='foo' version='1.0'>", - " <accesslog type='json' fileNamePattern='logs/vespa/qrs/access.%Y%m%d%H%M%S' symlinkName='json_access' />", - " <nodes count=\"2\">", - " </nodes>", - "</container>"); - - String deploymentXml = joinLines("<deployment version='1.0'>", - " <prod>", - " <region>us-east-1</region>", - " </prod>", - "</deployment>"); - - ApplicationPackage applicationPackage = new MockApplicationPackage.Builder() - .withServices(containerService) - .withDeploymentSpec(deploymentXml) - .build(); - - TestLogger logger = new TestLogger(); - DeployState deployState = new DeployState.Builder() - .applicationPackage(applicationPackage) - .zone(new Zone(Environment.prod, RegionName.from("us-east-1"))) - .properties(new TestProperties().setHostedVespa(true)) - .deployLogger(logger) - .build(); - createModel(root, deployState, null, DomBuilderTest.parse(containerService)); - assertFalse(logger.msgs.isEmpty()); - assertEquals(Level.WARNING, logger.msgs.get(0).getFirst()); - assertEquals("Applications are not allowed to override the 'accesslog' element", - logger.msgs.get(0).getSecond()); - } - - private void assertComponentConfigured(ApplicationContainerCluster cluster, String componentId) { - Component<?, ?> component = cluster.getComponentsMap().get(ComponentId.fromString(componentId)); - assertNotNull(component); - } - private void assertComponentConfigured(ApplicationContainer container, String id) { assertTrue(container.getComponents().getComponents().stream().anyMatch(component -> id.equals(component.getComponentId().getName()))); } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java index 7e8852ce85a..3d7b17d37e0 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.List; import java.util.logging.Level; +import static org.junit.Assert.assertNotNull; + /** * Utility functions for testing the ContainerModelBuilder. Note that XML validation will * not be done when using this class @@ -51,6 +53,11 @@ public abstract class ContainerModelBuilderTestBase { protected MockRoot root; + @Before + public void prepareTest() { + root = new MockRoot("root"); + } + protected void createBasicContainerModel() { Element clusterElem = DomBuilderTest.parse("<container id='default' version='1.0' />"); createModel(root, clusterElem); @@ -83,11 +90,6 @@ public abstract class ContainerModelBuilderTestBase { search.initializeSearchChains(Collections.emptyMap()); } - @Before - public void prepareTest() { - root = new MockRoot("root"); - } - protected ComponentsConfig componentsConfig() { return root.getConfig(ComponentsConfig.class, "default"); } @@ -109,4 +111,9 @@ public abstract class ContainerModelBuilderTestBase { ComponentId.fromString(componentId)); } + void assertComponentConfigured(ApplicationContainerCluster cluster, String componentId) { + Component<?, ?> component = cluster.getComponentsMap().get(ComponentId.fromString(componentId)); + assertNotNull(component); + } + } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/SecretStoreTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/SecretStoreTest.java new file mode 100644 index 00000000000..8c6eee72cde --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/SecretStoreTest.java @@ -0,0 +1,127 @@ +package com.yahoo.vespa.model.container.xml; + +import com.yahoo.component.ComponentId; +import com.yahoo.config.model.api.TenantSecretStore; +import com.yahoo.config.model.builder.xml.test.DomBuilderTest; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.model.deploy.TestProperties; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.secretstore.SecretStoreConfig; +import com.yahoo.vespa.model.container.ApplicationContainerCluster; +import com.yahoo.vespa.model.container.SecretStore; +import org.junit.Test; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author tokle + */ +public class SecretStoreTest extends ContainerModelBuilderTestBase { + + @Test + public void secret_store_can_be_set_up() { + Element clusterElem = DomBuilderTest.parse( + "<container version='1.0'>", + " <secret-store type='oath-ckms'>", + " <group name='group1' environment='env1'/>", + " </secret-store>", + "</container>"); + createModel(root, clusterElem); + SecretStore secretStore = getContainerCluster("container").getSecretStore().get(); + assertEquals("group1", secretStore.getGroups().get(0).name); + assertEquals("env1", secretStore.getGroups().get(0).environment); + } + + @Test + public void cloud_secret_store_requires_configured_secret_store() { + Element clusterElem = DomBuilderTest.parse( + "<container version='1.0'>", + " <secret-store type='cloud'>", + " <store id='store'>", + " <aws-parameter-store account='store1' region='eu-north-1'/>", + " </store>", + " </secret-store>", + "</container>"); + try { + DeployState state = new DeployState.Builder() + .properties(new TestProperties().setHostedVespa(true)) + .zone(new Zone(SystemName.Public, Environment.prod, RegionName.defaultName())) + .build(); + createModel(root, state, null, clusterElem); + fail("secret store not defined"); + } catch (RuntimeException e) { + assertEquals("No configured secret store named store1", e.getMessage()); + } + } + + + @Test + public void cloud_secret_store_can_be_set_up() { + Element clusterElem = DomBuilderTest.parse( + "<container version='1.0'>", + " <secret-store type='cloud'>", + " <store id='store'>", + " <aws-parameter-store account='store1' region='eu-north-1'/>", + " </store>", + " </secret-store>", + "</container>"); + + DeployState state = new DeployState.Builder() + .properties( + new TestProperties() + .setHostedVespa(true) + .setTenantSecretStores(List.of(new TenantSecretStore("store1", "1234", "role", Optional.of("externalid"))))) + .zone(new Zone(SystemName.Public, Environment.prod, RegionName.defaultName())) + .build(); + createModel(root, state, null, clusterElem); + + ApplicationContainerCluster container = getContainerCluster("container"); + assertComponentConfigured(container, "com.yahoo.jdisc.cloud.aws.AwsParameterStore"); + CloudSecretStore secretStore = (CloudSecretStore) container.getComponentsMap().get(ComponentId.fromString("com.yahoo.jdisc.cloud.aws.AwsParameterStore")); + + + SecretStoreConfig.Builder configBuilder = new SecretStoreConfig.Builder(); + secretStore.getConfig(configBuilder); + SecretStoreConfig secretStoreConfig = configBuilder.build(); + + assertEquals(1, secretStoreConfig.awsParameterStores().size()); + assertEquals("store1", secretStoreConfig.awsParameterStores().get(0).name()); + } + + @Test + public void cloud_secret_store_fails_to_set_up_in_non_public_zone() { + try { + Element clusterElem = DomBuilderTest.parse( + "<container version='1.0'>", + " <secret-store type='cloud'>", + " <store id='store'>", + " <aws-parameter-store account='store1' region='eu-north-1'/>", + " </store>", + " </secret-store>", + "</container>"); + + DeployState state = new DeployState.Builder() + .properties( + new TestProperties() + .setHostedVespa(true) + .setTenantSecretStores(List.of(new TenantSecretStore("store1", "1234", "role", Optional.of("externalid"))))) + .zone(new Zone(SystemName.main, Environment.prod, RegionName.defaultName())) + .build(); + createModel(root, state, null, clusterElem); + } catch (RuntimeException e) { + assertEquals("Cloud secret store is not supported in non-public system, see the documentation", + e.getMessage()); + return; + } + fail(); + } + +} |