From 057b88a27172d2e6b8912cfcff67ab341f19affa Mon Sep 17 00:00:00 2001 From: Morten Tokle Date: Fri, 28 May 2021 08:29:28 +0200 Subject: Revert "Revert mortent/cfg operator cert" --- .../yahoo/config/model/deploy/TestProperties.java | 8 ++++ .../model/container/xml/ContainerModelBuilder.java | 18 ++++++- .../container/xml/ContainerModelBuilderTest.java | 56 ++++++++++++++++++++++ .../config/server/deploy/ModelContextImpl.java | 11 ++++- .../modelfactory/ActivatedModelsBuilder.java | 5 +- .../vespa/config/server/session/PrepareParams.java | 33 ++++++++++++- .../yahoo/vespa/config/server/session/Session.java | 9 ++++ .../config/server/session/SessionPreparer.java | 11 +++-- .../config/server/session/SessionRepository.java | 1 + .../server/session/SessionZooKeeperClient.java | 22 +++++++++ .../tenant/OperatorCertificateSerializer.java | 37 ++++++++++++++ .../vespa/config/server/ModelContextImplTest.java | 3 +- .../config/server/session/PrepareParamsTest.java | 24 ++++++++++ .../tenant/OperatorCertificateSerializerTest.java | 30 ++++++++++++ .../com/yahoo/security/X509CertificateUtils.java | 17 +++++++ 15 files changed, 276 insertions(+), 9 deletions(-) create mode 100644 configserver/src/main/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializer.java create mode 100644 configserver/src/test/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializerTest.java 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 2a530b78b86..5dd6ffe7247 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 @@ -16,6 +16,7 @@ import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.Zone; import java.net.URI; +import java.security.cert.X509Certificate; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -60,6 +61,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea private String jvmOmitStackTraceInFastThrowOption; private int numDistributorStripes = 0; private boolean allowDisableMtls = true; + private List operatorCertificates = Collections.emptyList(); @Override public ModelContext.FeatureFlags featureFlags() { return this; } @Override public boolean multitenant() { return multitenant; } @@ -99,6 +101,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea @Override public String jvmOmitStackTraceInFastThrowOption(ClusterSpec.Type type) { return jvmOmitStackTraceInFastThrowOption; } @Override public int numDistributorStripes() { return numDistributorStripes; } @Override public boolean allowDisableMtls() { return allowDisableMtls; } + @Override public List operatorCertificates() { return operatorCertificates; } @Override public boolean useExternalRankExpressions() { return useExternalRankExpression; } @Override public boolean distributeExternalRankExpressions() { return useExternalRankExpression; } @@ -243,6 +246,11 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea return this; } + public TestProperties setOperatorCertificates(List operatorCertificates) { + this.operatorCertificates = List.copyOf(operatorCertificates); + 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/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index b477587bcac..4ce0a9c9dbb 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 @@ -34,6 +34,7 @@ import com.yahoo.container.logging.FileConnectionLog; import com.yahoo.osgi.provider.model.ComponentModel; import com.yahoo.search.rendering.RendererRegistry; import com.yahoo.searchdefinition.derived.RankProfileList; +import com.yahoo.security.X509CertificateUtils; import com.yahoo.text.XML; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.model.AbstractService; @@ -89,6 +90,7 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; import java.net.URI; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -431,6 +433,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder { // If the deployment contains certificate/private key reference, setup TLS port HostedSslConnectorFactory connectorFactory; + boolean enableHttp2 = deployState.featureFlags().enableJdiscHttp2(); if (deployState.endpointCertificateSecrets().isPresent()) { boolean authorizeClient = deployState.zone().system().isPublic(); if (authorizeClient && deployState.tlsClientAuthority().isEmpty()) { @@ -444,7 +447,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder { .orElse(false); connectorFactory = authorizeClient - ? HostedSslConnectorFactory.withProvidedCertificateAndTruststore(serverName, endpointCertificateSecrets, deployState.tlsClientAuthority().get()) + ? HostedSslConnectorFactory.withProvidedCertificateAndTruststore(serverName, endpointCertificateSecrets, getTlsClientAuthorities(deployState)) : HostedSslConnectorFactory.withProvidedCertificate(serverName, endpointCertificateSecrets, enforceHandshakeClientAuth); } else { connectorFactory = HostedSslConnectorFactory.withDefaultCertificateAndTruststore(serverName); @@ -453,6 +456,19 @@ public class ContainerModelBuilder extends ConfigModelBuilder { server.addConnector(connectorFactory); } + /* + 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 trustedCertificates = deployState.tlsClientAuthority() + .map(X509CertificateUtils::certificateListFromPem) + .orElse(Collections.emptyList()); + ArrayList x509Certificates = new ArrayList<>(trustedCertificates); + x509Certificates.addAll(deployState.getProperties().operatorCertificates()); + return X509CertificateUtils.toPem(x509Certificates); + } + private static boolean isHostedTenantApplication(ConfigModelContext context) { var deployState = context.getDeployState(); boolean isTesterApplication = deployState.getProperties().applicationId().instance().isTester(); 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 7f862afa1b0..543318f9224 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 @@ -40,6 +40,11 @@ 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.KeyAlgorithm; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.SignatureAlgorithm; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.X509CertificateUtils; import com.yahoo.security.tls.TlsContext; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.model.AbstractService; @@ -53,6 +58,7 @@ 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.CoreMatchers; import org.hamcrest.Matchers; import org.hamcrest.core.IsEqual; import org.junit.Rule; @@ -61,8 +67,15 @@ import org.junit.rules.TemporaryFolder; import org.w3c.dom.Element; import org.xml.sax.SAXException; +import javax.security.auth.x500.X500Principal; import java.io.IOException; import java.io.StringReader; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -82,6 +95,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -817,6 +831,48 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { 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(""); + + createModel(root, deployState, null, clusterElem); + + ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0"); + List 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 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( diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java index 195f9dbf8a7..1a635d5236a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java @@ -37,6 +37,7 @@ import com.yahoo.vespa.flags.UnboundFlag; import java.io.File; import java.net.URI; +import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; import java.util.Set; @@ -287,6 +288,7 @@ public class ModelContextImpl implements ModelContext { private final SecretStore secretStore; private final StringFlag jvmGCOptionsFlag; private final boolean allowDisableMtls; + private final List operatorCertificates; public Properties(ApplicationId applicationId, ConfigserverConfig configserverConfig, @@ -300,7 +302,8 @@ public class ModelContextImpl implements ModelContext { Optional applicationRoles, Optional maybeQuota, List tenantSecretStores, - SecretStore secretStore) { + SecretStore secretStore, + List operatorCertificates) { this.featureFlags = new FeatureFlags(flagSource, applicationId); this.applicationId = applicationId; this.multitenant = configserverConfig.multitenant() || configserverConfig.hostedVespa() || Boolean.getBoolean("multitenant"); @@ -323,6 +326,7 @@ public class ModelContextImpl implements ModelContext { .with(FetchVector.Dimension.APPLICATION_ID, applicationId.serializedForm()); this.allowDisableMtls = PermanentFlags.ALLOW_DISABLE_MTLS.bindTo(flagSource) .with(FetchVector.Dimension.APPLICATION_ID, applicationId.serializedForm()).value(); + this.operatorCertificates = operatorCertificates; } @Override public ModelContext.FeatureFlags featureFlags() { return featureFlags; } @@ -391,6 +395,11 @@ public class ModelContextImpl implements ModelContext { return allowDisableMtls; } + @Override + public List operatorCertificates() { + return operatorCertificates; + } + public String flagValueForClusterType(StringFlag flag, Optional clusterType) { return clusterType.map(type -> flag.with(CLUSTER_TYPE, type.name())) .orElse(flag) diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java index 4e7afa7b3db..2b7ce234777 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java @@ -38,7 +38,9 @@ import com.yahoo.vespa.config.server.tenant.TenantRepository; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.flags.FlagSource; +import java.security.cert.X509Certificate; import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.logging.Level; @@ -164,7 +166,8 @@ public class ActivatedModelsBuilder extends ModelsBuilder { .readApplicationRoles(applicationId), zkClient.readQuota(), zkClient.readTenantSecretStores(), - secretStore); + secretStore, + zkClient.readOperatorCertificates()); } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java index ea2a525b440..1b43e57c01a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java @@ -1,6 +1,7 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.config.server.session; +import com.google.common.collect.ImmutableList; import com.yahoo.component.Version; import com.yahoo.config.model.api.ApplicationRoles; import com.yahoo.config.model.api.ContainerEndpoint; @@ -11,6 +12,8 @@ import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; @@ -20,14 +23,18 @@ import com.yahoo.vespa.config.server.http.SessionHandler; import com.yahoo.vespa.config.server.tenant.ContainerEndpointSerializer; import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataSerializer; import com.yahoo.vespa.config.server.tenant.TenantSecretStoreSerializer; +import org.eclipse.jetty.util.ssl.X509; +import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Function; +import java.util.stream.Collectors; /** * Parameters for preparing an application. Immutable. @@ -52,6 +59,7 @@ public final class PrepareParams { static final String TENANT_SECRET_STORES_PARAM_NAME = "tenantSecretStores"; static final String FORCE_PARAM_NAME = "force"; static final String WAIT_FOR_RESOURCES_IN_PREPARE = "waitForResourcesInPrepare"; + static final String OPERATOR_CERTIFICATES = "operatorCertificates"; private final ApplicationId applicationId; private final TimeoutBudget timeoutBudget; @@ -69,6 +77,7 @@ public final class PrepareParams { private final Optional applicationRoles; private final Optional quota; private final List tenantSecretStores; + private final List operatorCertificates; private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors, boolean dryRun, boolean verbose, boolean isBootstrap, Optional vespaVersion, @@ -76,7 +85,7 @@ public final class PrepareParams { Optional endpointCertificateMetadata, Optional dockerImageRepository, Optional athenzDomain, Optional applicationRoles, Optional quota, List tenantSecretStores, - boolean force, boolean waitForResourcesInPrepare) { + boolean force, boolean waitForResourcesInPrepare, List operatorCertificates) { this.timeoutBudget = timeoutBudget; this.applicationId = Objects.requireNonNull(applicationId); this.ignoreValidationErrors = ignoreValidationErrors; @@ -93,6 +102,7 @@ public final class PrepareParams { this.tenantSecretStores = tenantSecretStores; this.force = force; this.waitForResourcesInPrepare = waitForResourcesInPrepare; + this.operatorCertificates = operatorCertificates; } public static class Builder { @@ -113,6 +123,7 @@ public final class PrepareParams { private Optional applicationRoles = Optional.empty(); private Optional quota = Optional.empty(); private List tenantSecretStores = List.of(); + private List operatorCertificates = List.of(); public Builder() { } @@ -245,11 +256,17 @@ public final class PrepareParams { return this; } + public Builder withOperatorCertificates(List operatorCertificates) { + this.operatorCertificates = List.copyOf(operatorCertificates); + return this; + } + public PrepareParams build() { return new PrepareParams(applicationId, timeoutBudget, ignoreValidationErrors, dryRun, verbose, isBootstrap, vespaVersion, containerEndpoints, endpointCertificateMetadata, dockerImageRepository, athenzDomain, - applicationRoles, quota, tenantSecretStores, force, waitForResourcesInPrepare); + applicationRoles, quota, tenantSecretStores, force, waitForResourcesInPrepare, + operatorCertificates); } } @@ -292,6 +309,7 @@ public final class PrepareParams { .tenantSecretStores(SlimeUtils.optionalString(params.field(TENANT_SECRET_STORES_PARAM_NAME)).orElse(null)) .force(booleanValue(params, FORCE_PARAM_NAME)) .waitForResourcesInPrepare(booleanValue(params, WAIT_FOR_RESOURCES_IN_PREPARE)) + .withOperatorCertificates(deserialize(params.field(OPERATOR_CERTIFICATES), PrepareParams::readOperatorCertificates, Collections.emptyList())) .build(); } @@ -343,6 +361,13 @@ public final class PrepareParams { return Optional.ofNullable(request.getProperty(propertyName)); } + private static List readOperatorCertificates(Inspector array) { + return SlimeUtils.entriesStream(array) + .map(Inspector::asString) + .map(X509CertificateUtils::fromPem) + .collect(Collectors.toList()); + } + public String getApplicationName() { return applicationId.application().value(); } @@ -400,4 +425,8 @@ public final class PrepareParams { public List tenantSecretStores() { return tenantSecretStores; } + + public List operatorCertificates() { + return operatorCertificates; + } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java index f1044b28049..542b54d877e 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java @@ -17,6 +17,7 @@ import com.yahoo.transaction.Transaction; import com.yahoo.vespa.config.server.application.ApplicationSet; import com.yahoo.vespa.config.server.tenant.TenantRepository; +import java.security.cert.X509Certificate; import java.time.Instant; import java.util.List; import java.util.Optional; @@ -137,6 +138,10 @@ public abstract class Session implements Comparable { sessionZooKeeperClient.writeTenantSecretStores(tenantSecretStores); } + public void setOperatorCertificates(List operatorCertificates) { + sessionZooKeeperClient.writeOperatorCertificates(operatorCertificates); + } + /** Returns application id read from ZooKeeper. Will throw RuntimeException if not found */ public ApplicationId getApplicationId() { return sessionZooKeeperClient.readApplicationId() @@ -172,6 +177,10 @@ public abstract class Session implements Comparable { return sessionZooKeeperClient.readTenantSecretStores(); } + public List getOperatorCertificates() { + return sessionZooKeeperClient.readOperatorCertificates(); + } + private Transaction createSetStatusTransaction(Status status) { return sessionZooKeeperClient.createWriteStatusTransaction(status); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java index 50b9ac55bda..dedd9e08655 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java @@ -49,6 +49,7 @@ import com.yahoo.vespa.flags.FlagSource; import java.io.File; import java.io.IOException; +import java.security.cert.X509Certificate; import java.time.Instant; import java.util.Collection; import java.util.List; @@ -202,7 +203,8 @@ public class SessionPreparer { applicationRoles, params.quota(), params.tenantSecretStores(), - secretStore); + secretStore, + params.operatorCertificates()); this.fileDistributionProvider = fileDistributionFactory.createProvider(serverDbSessionDir); this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry, permanentApplicationPackage, @@ -275,7 +277,8 @@ public class SessionPreparer { prepareResult.allocatedHosts(), athenzDomain, params.quota(), - params.tenantSecretStores()); + params.tenantSecretStores(), + params.operatorCertificates()); checkTimeout("write state to zookeeper"); } @@ -325,7 +328,8 @@ public class SessionPreparer { AllocatedHosts allocatedHosts, Optional athenzDomain, Optional quota, - List tenantSecretStores) { + List tenantSecretStores, + List operatorCertificates) { ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger); try { zkDeployer.deploy(applicationPackage, fileRegistryMap, allocatedHosts); @@ -337,6 +341,7 @@ public class SessionPreparer { zooKeeperClient.writeAthenzDomain(athenzDomain); zooKeeperClient.writeQuota(quota); zooKeeperClient.writeTenantSecretStores(tenantSecretStores); + zooKeeperClient.writeOperatorCertificates(operatorCertificates); } catch (RuntimeException | IOException e) { zkDeployer.cleanup(); throw new RuntimeException("Error preparing session", e); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java index 7d74b53fdff..ac350db5c21 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java @@ -256,6 +256,7 @@ public class SessionRepository { session.setDockerImageRepository(existingSession.getDockerImageRepository()); session.setAthenzDomain(existingSession.getAthenzDomain()); session.setTenantSecretStores(existingSession.getTenantSecretStores()); + session.setOperatorCertificates(existingSession.getOperatorCertificates()); return session; } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java index c7c4f1926d7..c3d6bba0ac2 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java @@ -21,6 +21,7 @@ import com.yahoo.transaction.Transaction; import com.yahoo.vespa.config.server.UserConfigDefinitionRepo; import com.yahoo.vespa.config.server.deploy.ZooKeeperClient; import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer; +import com.yahoo.vespa.config.server.tenant.OperatorCertificateSerializer; import com.yahoo.vespa.config.server.tenant.TenantRepository; import com.yahoo.vespa.config.server.tenant.TenantSecretStoreSerializer; import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; @@ -29,6 +30,7 @@ import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.curator.transaction.CuratorOperations; import com.yahoo.vespa.curator.transaction.CuratorTransaction; +import java.security.cert.X509Certificate; import java.time.Instant; import java.util.List; import java.util.Optional; @@ -57,6 +59,7 @@ public class SessionZooKeeperClient { private static final String ATHENZ_DOMAIN = "athenzDomain"; private static final String QUOTA_PATH = "quota"; private static final String TENANT_SECRET_STORES_PATH = "tenantSecretStores"; + private static final String OPERATOR_CERTIFICATES_PATH = "operatorCertificates"; private final Curator curator; private final ConfigCurator configCurator; @@ -191,6 +194,10 @@ public class SessionZooKeeperClient { return sessionPath.append(TENANT_SECRET_STORES_PATH).getAbsolute(); } + private String operatorCertificatesPath() { + return sessionPath.append(OPERATOR_CERTIFICATES_PATH).getAbsolute(); + } + public void writeVespaVersion(Version version) { configCurator.putData(versionPath(), version.toString()); } @@ -282,6 +289,21 @@ public class SessionZooKeeperClient { .orElse(List.of()); } + public void writeOperatorCertificates(List certificates) { + if( ! certificates.isEmpty()) { + var bytes = uncheck(() -> SlimeUtils.toJsonBytes(OperatorCertificateSerializer.toSlime(certificates))); + configCurator.putData(operatorCertificatesPath(), bytes); + } + } + + public List readOperatorCertificates() { + if ( ! configCurator.exists(operatorCertificatesPath())) return List.of(); + return Optional.ofNullable(configCurator.getData(operatorCertificatesPath())) + .map(SlimeUtils::jsonToSlime) + .map(slime -> OperatorCertificateSerializer.fromSlime(slime.get())) + .orElse(List.of()); + } + /** * Create necessary paths atomically for a new session. * diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializer.java new file mode 100644 index 00000000000..3dbdf1380f1 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializer.java @@ -0,0 +1,37 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.config.server.tenant; + +import com.yahoo.config.model.api.ApplicationRoles; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Inspector; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.stream.Collectors; + +public class OperatorCertificateSerializer { + + private final static String certificateField = "certificates"; + + + public static Slime toSlime(List certificateList) { + Slime slime = new Slime(); + var root = slime.setObject(); + Cursor array = root.setArray(certificateField); + certificateList.stream() + .map(X509CertificateUtils::toPem) + .forEach(array::addString); + return slime; + } + + public static List fromSlime(Inspector object) { + return SlimeUtils.entriesStream(object.field(certificateField)) + .map(Inspector::asString) + .map(X509CertificateUtils::fromPem) + .collect(Collectors.toList()); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java index 7b9420b6b9e..0acf4404326 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java @@ -74,7 +74,8 @@ public class ModelContextImplTest { Optional.empty(), Optional.empty(), List.of(), - new SecretStoreProvider().get()), + new SecretStoreProvider().get(), + List.of()), Optional.empty(), Optional.empty(), new Version(7), diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java index f50238f2b85..08794cf0b78 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java @@ -7,6 +7,12 @@ import com.yahoo.config.model.api.EndpointCertificateMetadata; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; +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.security.X509CertificateWithKey; import com.yahoo.slime.ArrayInserter; import com.yahoo.slime.Cursor; import com.yahoo.slime.Injector; @@ -20,10 +26,16 @@ import com.yahoo.vespa.config.server.tenant.ContainerEndpointSerializer; import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataSerializer; import org.junit.Test; +import javax.security.auth.x500.X500Principal; import java.io.IOException; +import java.math.BigInteger; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.cert.X509Certificate; import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.Objects; @@ -179,6 +191,18 @@ public class PrepareParamsTest { assertPrepareParamsEqual(urlPrepareParams, jsonPrepareParams); } + @Test + public void testOperatorCertificates() throws IOException { + Slime slime = SlimeUtils.jsonToSlime(json); + Cursor cursor = slime.get(); + Cursor array = cursor.setArray(PrepareParams.OPERATOR_CERTIFICATES); + X509Certificate certificate = X509CertificateUtils.createSelfSigned("cn=myservice", Duration.ofDays(1)).certificate(); + array.addString(X509CertificateUtils.toPem(certificate)); + PrepareParams prepareParams = PrepareParams.fromJson(SlimeUtils.toJsonBytes(slime), TenantName.from("foo"), Duration.ofSeconds(60)); + assertEquals(1, prepareParams.operatorCertificates().size()); + assertEquals(certificate, prepareParams.operatorCertificates().get(0)); + } + private void assertPrepareParamsEqual(PrepareParams urlParams, PrepareParams jsonParams) { assertEquals(urlParams.ignoreValidationErrors(), jsonParams.ignoreValidationErrors()); assertEquals(urlParams.isDryRun(), jsonParams.isDryRun()); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializerTest.java new file mode 100644 index 00000000000..b77248f0840 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializerTest.java @@ -0,0 +1,30 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.config.server.tenant; + +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.security.X509CertificateWithKey; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class OperatorCertificateSerializerTest { + + @Test + public void testSerialization() { + X509Certificate certificate = X509CertificateUtils.createSelfSigned("cn=mycn", Duration.ofDays(1)).certificate(); + Slime slime = OperatorCertificateSerializer.toSlime(List.of(certificate)); + List deserialized = OperatorCertificateSerializer.fromSlime(slime.get()); + assertEquals(1, deserialized.size()); + assertEquals(certificate, deserialized.get(0)); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java b/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java index cefa8ab2f51..215dc311af3 100644 --- a/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java +++ b/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java @@ -18,13 +18,18 @@ import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.UncheckedIOException; +import java.math.BigInteger; import java.security.GeneralSecurityException; +import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -161,4 +166,16 @@ public class X509CertificateUtils { } } + public static X509CertificateWithKey createSelfSigned(String cn, Duration duration) { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256); + X500Principal subject = new X500Principal(cn); + Instant now = Instant.now(); + X509Certificate cert = + X509CertificateBuilder.fromKeypair(keyPair, subject, now, + now.plus(duration), SignatureAlgorithm.SHA256_WITH_ECDSA, + BigInteger.ONE) + .setBasicConstraints(true, true) + .build(); + return new X509CertificateWithKey(cert, keyPair.getPrivate()); + } } -- cgit v1.2.3