diff options
author | Morten Tokle <mortent@yahooinc.com> | 2022-11-22 13:34:48 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@yahooinc.com> | 2022-11-22 13:43:04 +0100 |
commit | 23ba858ac77a5735c84570a040a1498c044d2a3b (patch) | |
tree | 2525f113a78aca9d7dd5828dc6c594a4737a8220 /config-model/src | |
parent | dd070e60aa612e806c83ce7115fce08d777ff910 (diff) |
Generate config for clients
Diffstat (limited to 'config-model/src')
5 files changed, 281 insertions, 7 deletions
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..774e90c81f4 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,7 @@ public abstract class ContainerCluster<CONTAINER extends Container> private String jvmGCOptions = null; private boolean deferChangesUntilRestart = false; + private List<Client> clients; public ContainerCluster(AbstractConfigProducer<?> parent, String configSubId, String clusterId, DeployState deployState, boolean zooKeeperLocalhostAffinity) { this(parent, configSubId, clusterId, deployState, zooKeeperLocalhostAffinity, 1); @@ -352,6 +354,14 @@ public abstract class ContainerCluster<CONTAINER extends Container> return http; } + public void setClients(List<Client> clients) { + this.clients = clients; + } + + public List<Client> getClients() { + return clients; + } + public ContainerDocproc getDocproc() { return containerDocproc; } 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..f588f4f5962 --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Client.java @@ -0,0 +1,43 @@ +// 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 static Client createLegacyClient(List<X509Certificate> certificates) { +// return new Client(true, "default", List.of(AclMapping.Action.READ.name(), AclMapping.Action.WRITE.name()), certificates); +// } +// +// public static Client createClient(String id, List<String> permissions, List<X509Certificate> certificates) { +// return new Client(false, id, permissions, 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..953b7f21985 --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilter.java @@ -0,0 +1,40 @@ +// 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.container.bundle.BundleInstantiationSpecification; +import com.yahoo.jdisc.http.filter.security.cloud.config.CloudDataPlaneFilterConfig; +import com.yahoo.osgi.provider.model.ComponentModel; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.vespa.model.container.ApplicationContainerCluster; +import com.yahoo.vespa.model.container.component.SimpleComponent; +import com.yahoo.vespa.model.container.http.Client; + +import java.util.List; + +public class CloudDataPlaneFilter extends SimpleComponent 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; + + public CloudDataPlaneFilter(ApplicationContainerCluster cluster, boolean legacyMode) { + super(new ComponentModel(BundleInstantiationSpecification.fromStrings(CLASS, CLASS, BUNDLE))); + this.cluster = cluster; + this.legacyMode = legacyMode; + } + + @Override + public void getConfig(CloudDataPlaneFilterConfig.Builder builder) { + List<Client> clients = cluster.getClients(); + builder.legacyMode(legacyMode); + 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 5dc937fe585..1301ab5055b 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 @@ -4,6 +4,7 @@ package com.yahoo.vespa.model.container.xml; import com.google.common.collect.ImmutableList; import com.yahoo.component.Version; 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; @@ -30,8 +31,10 @@ import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.AclMapping; 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; @@ -72,6 +75,7 @@ import com.yahoo.vespa.model.container.component.UserBindingPattern; 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.FilterChains; import com.yahoo.vespa.model.container.http.Http; @@ -87,7 +91,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; @@ -201,6 +208,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); @@ -441,6 +449,54 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { } } + 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(AclMapping.Action.READ.name(), AclMapping.Action.WRITE.name()), + 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(clients); + cluster.addComponent(new CloudDataPlaneFilter(cluster, legacyMode)); + } + + 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); + } + } + private void addDefaultConnectorHostedFilterBinding(ApplicationContainerCluster cluster) { cluster.getHttp().getAccessControl() .ifPresent(accessControl -> accessControl.configureDefaultHostedConnector(cluster.getHttp())); ; @@ -456,7 +512,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"); } @@ -469,7 +528,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 { @@ -479,15 +538,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/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..bfde7fa1939 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/CloudDataPlaneFilterTest.java @@ -0,0 +1,116 @@ +// 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.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.http.ConnectorFactory; +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; + +public class CloudDataPlaneFilterTest extends ContainerModelBuilderTestBase { + + @TempDir + public File applicationFolder; + + @Test + public void it_generates_correct_config() throws IOException { + Path security = applicationFolder.toPath().resolve("security"); + Files.createDirectories(security); + Path certFile = security.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(); + String certPem = X509CertificateUtils.toPem(certificate); + Files.writeString(certFile, certPem); + + 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(true)) + .zone(new Zone(SystemName.PublicCd, Environment.dev, RegionName.defaultName())) + .build(); + createModel(root, state, null, clusterElem); + + CloudDataPlaneFilterConfig config = root.getConfig(CloudDataPlaneFilterConfig.class, "container/component/com.yahoo.jdisc.http.filter.security.cloud.CloudDataPlaneFilter"); + 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(certPem), client.certificates()); + + 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(1, caCerts.size()); + assertEquals(List.of(certificate), caCerts); + } + + + + static X509Certificate createCertificate() { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256); + X500Principal subject = new X500Principal("CN=mysubject"); + return X509CertificateBuilder + .fromKeypair( + keyPair, subject, Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA512_WITH_ECDSA, BigInteger.valueOf(1)) + .build(); + } + +} |