diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2022-11-28 11:27:17 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-28 11:27:17 +0100 |
commit | 326b765c033dd1083de7eeeffc2f40df8cbe1734 (patch) | |
tree | 61db372f7e7f5e8c731bd6d1f23efdc074953889 /config-model/src | |
parent | 1b33df53fd3daef51c9abd15301e440a8f8e6230 (diff) | |
parent | afd2cbfe0bb8dc3ee74f40ca2b62e102bd0a6d1c (diff) |
Merge pull request #24960 from vespa-engine/mortent/clients-config
Cloud data plane filter config
Diffstat (limited to 'config-model/src')
9 files changed, 434 insertions, 8 deletions
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java index 6b8428a07ac..328f1b19f10 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java @@ -82,6 +82,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea private Architecture adminClusterNodeResourcesArchitecture = Architecture.getDefault(); private boolean useRestrictedDataPlaneBindings = false; private Optional<CloudAccount> cloudAccount = Optional.empty(); + private boolean enableDataPlaneFilter = false; @Override public ModelContext.FeatureFlags featureFlags() { return this; } @Override public boolean multitenant() { return multitenant; } @@ -137,6 +138,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea @Override public boolean useTwoPhaseDocumentGc() { return useTwoPhaseDocumentGc; } @Override public boolean useRestrictedDataPlaneBindings() { return useRestrictedDataPlaneBindings; } @Override public Optional<CloudAccount> cloudAccount() { return cloudAccount; } + @Override public boolean enableDataPlaneFilter() { return enableDataPlaneFilter; } public TestProperties sharedStringRepoNoReclaim(boolean sharedStringRepoNoReclaim) { this.sharedStringRepoNoReclaim = sharedStringRepoNoReclaim; @@ -366,6 +368,11 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea return this; } + public TestProperties setEnableDataPlaneFilter(boolean enableDataPlaneFilter) { + this.enableDataPlaneFilter = enableDataPlaneFilter; + return this; + } + public static class Spec implements ConfigServerSpec { private final String hostName; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java index f5f25c65cd4..f1b3c74a55d 100755 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java @@ -52,6 +52,7 @@ import com.yahoo.vespa.model.container.component.chain.ProcessingHandler; import com.yahoo.vespa.model.container.configserver.ConfigserverCluster; import com.yahoo.vespa.model.container.docproc.ContainerDocproc; import com.yahoo.vespa.model.container.docproc.DocprocChains; +import com.yahoo.vespa.model.container.http.Client; import com.yahoo.vespa.model.container.http.Http; import com.yahoo.vespa.model.container.processing.ProcessingChains; import com.yahoo.vespa.model.container.search.ContainerSearch; @@ -161,6 +162,8 @@ public abstract class ContainerCluster<CONTAINER extends Container> private String jvmGCOptions = null; private boolean deferChangesUntilRestart = false; + private boolean clientsLegacyMode; + private List<Client> clients = List.of(); public ContainerCluster(AbstractConfigProducer<?> parent, String configSubId, String clusterId, DeployState deployState, boolean zooKeeperLocalhostAffinity) { this(parent, configSubId, clusterId, deployState, zooKeeperLocalhostAffinity, 1); @@ -352,6 +355,17 @@ public abstract class ContainerCluster<CONTAINER extends Container> return http; } + public void setClients(boolean legacyMode, List<Client> clients) { + clientsLegacyMode = legacyMode; + this.clients = clients; + } + + public List<Client> getClients() { + return clients; + } + + public boolean clientsLegacyMode() { return clientsLegacyMode; } + public ContainerDocproc getDocproc() { return containerDocproc; } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/SystemBindingPattern.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/SystemBindingPattern.java index 201e26d3575..606557670a5 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/component/SystemBindingPattern.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/SystemBindingPattern.java @@ -14,6 +14,7 @@ public class SystemBindingPattern extends BindingPattern { public static SystemBindingPattern fromHttpPath(String path) { return new SystemBindingPattern("http", "*", null, path);} public static SystemBindingPattern fromPattern(String binding) { return new SystemBindingPattern(binding);} public static SystemBindingPattern fromHttpPortAndPath(String port, String path) { return new SystemBindingPattern("http", "*", port, path); } + public static SystemBindingPattern fromHttpPortAndPath(int port, String path) { return new SystemBindingPattern("http", "*", Integer.toString(port), path); } @Override public String toString() { diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java new file mode 100644 index 00000000000..c851ab2bee6 --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java @@ -0,0 +1,34 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.container.http; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Represents a client. The client is identified by one of the provided certificates and have a set of permissions. + * + * @author mortent + */ +public class Client { + private String id; + private List<String> permissions; + private List<X509Certificate> certificates; + + public Client(String id, List<String> permissions, List<X509Certificate> certificates) { + this.id = id; + this.permissions = permissions; + this.certificates = certificates; + } + + public String id() { + return id; + } + + public List<String> permissions() { + return permissions; + } + + public List<X509Certificate> certificates() { + return certificates; + } +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java new file mode 100644 index 00000000000..6767a61d02b --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java @@ -0,0 +1,53 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.container.xml; + +import com.yahoo.component.ComponentSpecification; +import com.yahoo.component.chain.dependencies.Dependencies; +import com.yahoo.component.chain.model.ChainedComponentModel; +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.jdisc.http.filter.security.cloud.config.CloudDataPlaneFilterConfig; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.vespa.model.container.ApplicationContainerCluster; +import com.yahoo.vespa.model.container.http.Client; +import com.yahoo.vespa.model.container.http.Filter; + +import java.util.List; + +class CloudDataPlaneFilter extends Filter implements CloudDataPlaneFilterConfig.Producer { + + private static final String CLASS = "com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter"; + private static final String BUNDLE = "jdisc-security-filters"; + + private final ApplicationContainerCluster cluster; + private final boolean legacyMode; + + CloudDataPlaneFilter(ApplicationContainerCluster cluster, boolean legacyMode) { + super(model()); + this.cluster = cluster; + this.legacyMode = legacyMode; + } + + private static ChainedComponentModel model() { + return new ChainedComponentModel( + new BundleInstantiationSpecification( + new ComponentSpecification(CLASS), null, new ComponentSpecification(BUNDLE)), + Dependencies.emptyDependencies()); + } + + @Override + public void getConfig(CloudDataPlaneFilterConfig.Builder builder) { + if (legacyMode) { + builder.legacyMode(true); + } else { + List<Client> clients = cluster.getClients(); + builder.legacyMode(false); + List<CloudDataPlaneFilterConfig.Clients.Builder> clientsList = clients.stream() + .map(x -> new CloudDataPlaneFilterConfig.Clients.Builder() + .id(x.id()) + .certificates(X509CertificateUtils.toPem(x.certificates())) + .permissions(x.permissions())) + .toList(); + builder.clients(clientsList); + } + } +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 03c9335bbc4..798ca55bdfa 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -2,8 +2,13 @@ package com.yahoo.vespa.model.container.xml; import com.google.common.collect.ImmutableList; +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; import com.yahoo.component.Version; +import com.yahoo.component.chain.dependencies.Dependencies; +import com.yahoo.component.chain.model.ChainedComponentModel; import com.yahoo.config.application.Xml; +import com.yahoo.config.application.api.ApplicationFile; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.application.api.DeploymentInstanceSpec; @@ -31,8 +36,10 @@ import com.yahoo.config.provision.LoadBalancerSettings; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.Zone; +import com.yahoo.container.bundle.BundleInstantiationSpecification; import com.yahoo.container.logging.FileConnectionLog; import com.yahoo.osgi.provider.model.ComponentModel; +import com.yahoo.path.Path; import com.yahoo.schema.OnnxModel; import com.yahoo.schema.derived.RankProfileList; import com.yahoo.search.rendering.RendererRegistry; @@ -70,10 +77,14 @@ import com.yahoo.vespa.model.container.component.Handler; import com.yahoo.vespa.model.container.component.SimpleComponent; import com.yahoo.vespa.model.container.component.SystemBindingPattern; import com.yahoo.vespa.model.container.component.UserBindingPattern; +import com.yahoo.vespa.model.container.component.chain.Chain; import com.yahoo.vespa.model.container.docproc.ContainerDocproc; import com.yahoo.vespa.model.container.docproc.DocprocChains; import com.yahoo.vespa.model.container.http.AccessControl; +import com.yahoo.vespa.model.container.http.Client; import com.yahoo.vespa.model.container.http.ConnectorFactory; +import com.yahoo.vespa.model.container.http.Filter; +import com.yahoo.vespa.model.container.http.FilterBinding; import com.yahoo.vespa.model.container.http.FilterChains; import com.yahoo.vespa.model.container.http.Http; import com.yahoo.vespa.model.container.http.JettyHttpServer; @@ -88,7 +99,10 @@ import com.yahoo.vespa.model.content.StorageGroup; import org.w3c.dom.Element; import org.w3c.dom.Node; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; @@ -106,6 +120,7 @@ import java.util.logging.Level; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static com.yahoo.vespa.model.container.ContainerCluster.VIP_HANDLER_BINDING; import static java.util.logging.Level.WARNING; /** @@ -202,6 +217,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { addStatusHandlers(cluster, context.getDeployState().isHosted()); addUserHandlers(deployState, cluster, spec, context); + addClients(deployState, spec, cluster, context); addHttp(deployState, spec, cluster, context); addAccessLogs(deployState, cluster, spec); @@ -439,6 +455,86 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { addHostedImplicitAccessControlIfNotPresent(deployState, cluster); addDefaultConnectorHostedFilterBinding(cluster); addAdditionalHostedConnector(deployState, cluster, context); + addCloudDataPlaneFilter(deployState, cluster); + } + } + + private static void addCloudDataPlaneFilter(DeployState deployState, ApplicationContainerCluster cluster) { + if (!deployState.isHosted() || !deployState.zone().system().isPublic() || !deployState.featureFlags().enableDataPlaneFilter()) return; + + // Setup secure filter chain + var secureChain = new Chain<Filter>(FilterChains.emptyChainSpec(ComponentId.fromString("cloud-data-plane-secure"))); + secureChain.addInnerComponent(new CloudDataPlaneFilter(cluster, cluster.clientsLegacyMode())); + cluster.getHttp().getFilterChains().add(secureChain); + // Set cloud data plane filter as default request filter chain for data plane connector + cluster.getHttp().getHttpServer().orElseThrow().getConnectorFactories().stream() + .filter(c -> c.getListenPort() == HOSTED_VESPA_DATAPLANE_PORT).findAny().orElseThrow() + .setDefaultRequestFilterChain(secureChain.getComponentId()); + + // Setup insecure filter chain + var insecureChain = new Chain<Filter>(FilterChains.emptyChainSpec(ComponentId.fromString("cloud-data-plane-insecure"))); + insecureChain.addInnerComponent(new Filter( + new ChainedComponentModel( + new BundleInstantiationSpecification( + new ComponentSpecification("com.yahoo.jdisc.http.filter.security.misc.NoopFilter"), + null, new ComponentSpecification("jdisc-security-filters")), + Dependencies.emptyDependencies()))); + cluster.getHttp().getFilterChains().add(insecureChain); + var insecureChainComponentSpec = new ComponentSpecification(insecureChain.getComponentId().toString()); + FilterBinding insecureBinding = + FilterBinding.create(FilterBinding.Type.REQUEST, insecureChainComponentSpec, VIP_HANDLER_BINDING); + cluster.getHttp().getBindings().add(insecureBinding); + // Set insecure filter as default request filter chain for default connector + cluster.getHttp().getHttpServer().orElseThrow().getConnectorFactories().stream() + .filter(c -> c.getListenPort() == Defaults.getDefaults().vespaWebServicePort()).findAny().orElseThrow() + .setDefaultRequestFilterChain(insecureChain.getComponentId()); + + } + + protected void addClients(DeployState deployState, Element spec, ApplicationContainerCluster cluster, ConfigModelContext context) { + if (!deployState.isHosted() || !deployState.zone().system().isPublic() || !deployState.featureFlags().enableDataPlaneFilter()) return; + + List<Client> clients; + Element clientsElement = XML.getChild(spec, "clients"); + boolean legacyMode = false; + if (clientsElement == null) { + Client defaultClient = new Client("default", + List.of(), + getCertificates(app.getFile(Path.fromString("security/clients.pem")))); + clients = List.of(defaultClient); + legacyMode = true; + } else { + clients = XML.getChildren(clientsElement, "client").stream() + .map(this::getCLient) + .toList(); + } + cluster.setClients(legacyMode, clients); + } + + private Client getCLient(Element clientElement) { + String id = XML.attribute("id", clientElement).orElseThrow(); + List<String> permissions = XML.attribute("permissions", clientElement) + .map(p -> p.split(",")).stream() + .flatMap(Arrays::stream) + .toList(); + + List<X509Certificate> x509Certificates = XML.getChildren(clientElement, "certificate").stream() + .map(certElem -> Path.fromString(certElem.getAttribute("file"))) + .map(path -> app.getFile(path)) + .map(this::getCertificates) + .flatMap(Collection::stream) + .toList(); + return new Client(id, permissions, x509Certificates); + } + + private List<X509Certificate> getCertificates(ApplicationFile file) { + try { + InputStream inputStream = file.createInputStream(); + byte[] bytes = inputStream.readAllBytes(); + inputStream.close(); + return X509CertificateUtils.certificateListFromPem(new String(bytes, StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); } } @@ -457,7 +553,10 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { boolean proxyProtocolMixedMode = deployState.getProperties().featureFlags().enableProxyProtocolMixedMode(); if (deployState.endpointCertificateSecrets().isPresent()) { boolean authorizeClient = deployState.zone().system().isPublic(); - if (authorizeClient && deployState.tlsClientAuthority().isEmpty()) { + List<X509Certificate> clientCertificates = deployState.featureFlags().enableDataPlaneFilter() + ? getClientCertificates(cluster) + : deployState.tlsClientAuthority().map(X509CertificateUtils::certificateListFromPem).orElse(List.of()); + if (authorizeClient && clientCertificates.isEmpty()) { throw new IllegalArgumentException("Client certificate authority security/clients.pem is missing - " + "see: https://cloud.vespa.ai/en/security-model#data-plane"); } @@ -470,7 +569,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { connectorFactory = authorizeClient ? HostedSslConnectorFactory.withProvidedCertificateAndTruststore( - serverName, endpointCertificateSecrets, getTlsClientAuthorities(deployState), tlsCiphersOverride, proxyProtocolMixedMode, HOSTED_VESPA_DATAPLANE_PORT) + serverName, endpointCertificateSecrets, getTlsClientAuthorities(clientCertificates, deployState), tlsCiphersOverride, proxyProtocolMixedMode, HOSTED_VESPA_DATAPLANE_PORT) : HostedSslConnectorFactory.withProvidedCertificate( serverName, endpointCertificateSecrets, enforceHandshakeClientAuth, tlsCiphersOverride, proxyProtocolMixedMode, HOSTED_VESPA_DATAPLANE_PORT); } else { @@ -480,15 +579,21 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { server.addConnector(connectorFactory); } + // Returns the client certificates defined in + private List<X509Certificate> getClientCertificates(ApplicationContainerCluster cluster) { + return cluster.getClients() + .stream() + .map(Client::certificates) + .flatMap(Collection::stream) + .toList(); + } + /* Return trusted certificates as a PEM encoded string containing the concatenation of trusted certs from the application package and all operator certificates. */ - String getTlsClientAuthorities(DeployState deployState) { - List<X509Certificate> trustedCertificates = deployState.tlsClientAuthority() - .map(X509CertificateUtils::certificateListFromPem) - .orElse(Collections.emptyList()); - ArrayList<X509Certificate> x509Certificates = new ArrayList<>(trustedCertificates); + String getTlsClientAuthorities(List<X509Certificate> applicationCertificates, DeployState deployState) { + ArrayList<X509Certificate> x509Certificates = new ArrayList<>(applicationCertificates); x509Certificates.addAll(deployState.getProperties().operatorCertificates()); return X509CertificateUtils.toPem(x509Certificates); } diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc index 8cd7071462e..938932c3df6 100644 --- a/config-model/src/main/resources/schema/containercluster.rnc +++ b/config-model/src/main/resources/schema/containercluster.rnc @@ -24,7 +24,8 @@ ContainerServices = AccessLog* & SecretStore? & ZooKeeper? & - GenericConfig* + GenericConfig* & + Clients? # TODO(ogronnesby): Change this configuration syntax ClientAuthorize = element client-authorize { empty } @@ -128,6 +129,18 @@ Threadpool = element threadpool { element queue-size { xsd:nonNegativeInteger } } +Clients = element clients { + Client* +} + +Client = element client { + ComponentId & + attribute permissions { string } & + element certificate { + attribute file { string } + }+ +} + # SEARCH: SearchInContainer = element search { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java new file mode 100644 index 00000000000..39d2da11465 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java @@ -0,0 +1,189 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.container.xml; + +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.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.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.filter.security.cloud.config.CloudDataPlaneFilterConfig; +import com.yahoo.security.KeyAlgorithm; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.SignatureAlgorithm; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.vespa.model.container.ApplicationContainer; +import com.yahoo.vespa.model.container.ContainerModel; +import com.yahoo.vespa.model.container.http.ConnectorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.w3c.dom.Element; + +import javax.security.auth.x500.X500Principal; +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CloudDataPlaneFilterTest extends ContainerModelBuilderTestBase { + + @TempDir + public File applicationFolder; + + Path securityFolder; + private static final String cloudDataPlaneFilterConfigId = "container/filters/chain/cloud-data-plane-secure/component/" + + "com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter"; + + @BeforeEach + public void setup() throws IOException { + securityFolder = applicationFolder.toPath().resolve("security"); + Files.createDirectories(securityFolder); + } + + @Test + public void it_generates_correct_config() throws IOException { + Path certFile = securityFolder.resolve("foo.pem"); + Element clusterElem = DomBuilderTest.parse( + """ + <container version='1.0'> + <clients> + <client id="foo" permissions="read,write"> + <certificate file="%s"/> + </client> + </clients> + </container> + """ + .formatted(applicationFolder.toPath().relativize(certFile).toString())); + X509Certificate certificate = createCertificate(certFile); + + buildModel(true, clusterElem); + + CloudDataPlaneFilterConfig config = root.getConfig(CloudDataPlaneFilterConfig.class, cloudDataPlaneFilterConfigId); + assertFalse(config.legacyMode()); + List<CloudDataPlaneFilterConfig.Clients> clients = config.clients(); + assertEquals(1, clients.size()); + CloudDataPlaneFilterConfig.Clients client = clients.get(0); + assertEquals("foo", client.id()); + assertIterableEquals(List.of("read", "write"), client.permissions()); + assertIterableEquals(List.of(X509CertificateUtils.toPem(certificate)), client.certificates()); + + ConnectorConfig connectorConfig = connectorConfig(); + var caCerts = X509CertificateUtils.certificateListFromPem(connectorConfig.ssl().caCertificate()); + assertEquals(1, caCerts.size()); + assertEquals(List.of(certificate), caCerts); + var srvCfg = root.getConfig(ServerConfig.class, "container/http"); + assertEquals("cloud-data-plane-insecure", srvCfg.defaultFilters().get(0).filterId()); + assertEquals(8080, srvCfg.defaultFilters().get(0).localPort()); + assertEquals("cloud-data-plane-secure", srvCfg.defaultFilters().get(1).filterId()); + assertEquals(4443, srvCfg.defaultFilters().get(1).localPort()); + } + + @Test + public void it_generates_correct_legacy_config() throws IOException { + Path certFile = securityFolder.resolve("clients.pem"); + Element clusterElem = DomBuilderTest.parse("<container version='1.0' />"); + X509Certificate certificate = createCertificate(certFile); + + buildModel(true, clusterElem); + + CloudDataPlaneFilterConfig config = root.getConfig(CloudDataPlaneFilterConfig.class, cloudDataPlaneFilterConfigId); + assertTrue(config.legacyMode()); + List<CloudDataPlaneFilterConfig.Clients> clients = config.clients(); + assertEquals(0, clients.size()); + + ConnectorConfig connectorConfig = connectorConfig(); + var caCerts = X509CertificateUtils.certificateListFromPem(connectorConfig.ssl().caCertificate()); + assertEquals(1, caCerts.size()); + assertEquals(List.of(certificate), caCerts); + } + + @Test + public void it_generates_correct_config_when_filter_not_enabled () throws IOException { + Path certFile = securityFolder.resolve("clients.pem"); + Element clusterElem = DomBuilderTest.parse( + """ + <container version='1.0'> + <clients> + <client id="foo" permissions="read,write"> + <certificate file="%s"/> + </client> + </clients> + </container> + """ + .formatted(applicationFolder.toPath().relativize(certFile).toString())); + X509Certificate certificate = createCertificate(certFile); + + buildModel(false, clusterElem); + + // Data plane filter config is not configured + assertFalse(root.getConfigIds().contains("container/component/com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter")); + + // Connector config configures ca certs from security/clients.pem + ConnectorConfig connectorConfig = connectorConfig(); + var caCerts = X509CertificateUtils.certificateListFromPem(connectorConfig.ssl().caCertificate()); + assertEquals(1, caCerts.size()); + assertEquals(List.of(certificate), caCerts); + } + + private ConnectorConfig connectorConfig() { + 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); + + return new ConnectorConfig(builder); + } + + /* + Creates cert, returns + */ + static X509Certificate createCertificate(Path certFile) throws IOException { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256); + X500Principal subject = new X500Principal("CN=mysubject"); + X509Certificate certificate = X509CertificateBuilder + .fromKeypair( + keyPair, subject, Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA512_WITH_ECDSA, BigInteger.valueOf(1)) + .build(); + String certPem = X509CertificateUtils.toPem(certificate); + Files.writeString(certFile, certPem); + return certificate; + } + + public List<ContainerModel> buildModel(boolean enableFilter, Element... clusterElem) { + var applicationPackage = new MockApplicationPackage.Builder() + .withRoot(applicationFolder) + .build(); + + DeployState state = new DeployState.Builder() + .applicationPackage(applicationPackage) + .properties( + new TestProperties() + .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY"))) + .setHostedVespa(true) + .setEnableDataPlaneFilter(enableFilter)) + .zone(new Zone(SystemName.PublicCd, Environment.dev, RegionName.defaultName())) + .build(); + return createModel(root, state, null, clusterElem); + } +} diff --git a/config-model/src/test/schema-test-files/services.xml b/config-model/src/test/schema-test-files/services.xml index 543f76ca136..7976b1f5524 100644 --- a/config-model/src/test/schema-test-files/services.xml +++ b/config-model/src/test/schema-test-files/services.xml @@ -209,6 +209,16 @@ </environment-variables> <node hostalias="host1" /> </nodes> + + <clients> + <client id="client1" permissions="read,write"> + <certificate file="security/file.pem" /> + </client> + <client id="client2" permissions="write"> + <certificate file="security/file1.pem" /> + <certificate file="security/file2.pem" /> + </client> + </clients> </container> <container id='qrsCluster_2' version='1.0'> |