summaryrefslogtreecommitdiffstats
path: root/config-model
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2022-11-28 11:27:17 +0100
committerGitHub <noreply@github.com>2022-11-28 11:27:17 +0100
commit326b765c033dd1083de7eeeffc2f40df8cbe1734 (patch)
tree61db372f7e7f5e8c731bd6d1f23efdc074953889 /config-model
parent1b33df53fd3daef51c9abd15301e440a8f8e6230 (diff)
parentafd2cbfe0bb8dc3ee74f40ca2b62e102bd0a6d1c (diff)
Merge pull request #24960 from vespa-engine/mortent/clients-config
Cloud data plane filter config
Diffstat (limited to 'config-model')
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java7
-rwxr-xr-xconfig-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java14
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/SystemBindingPattern.java1
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java34
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java53
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java119
-rw-r--r--config-model/src/main/resources/schema/containercluster.rnc15
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java189
-rw-r--r--config-model/src/test/schema-test-files/services.xml10
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'>