summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java3
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java8
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java18
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java60
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java5
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java33
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java9
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java1
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java22
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializer.java33
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java3
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java31
14 files changed, 239 insertions, 9 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java
index 2b18729e2f2..da8f56e26b4 100644
--- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java
+++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java
@@ -19,6 +19,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
+import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -135,6 +136,8 @@ public interface ModelContext {
// Allow disabling mTLS for now, harden later
default boolean allowDisableMtls() { return true; }
+
+ default List<X509Certificate> operatorCertificates() { return List.of(); }
}
@Retention(RetentionPolicy.RUNTIME)
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 e6bf3a835c6..3f77e2010ec 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;
@@ -59,6 +60,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea
private String jvmOmitStackTraceInFastThrowOption;
private int numDistributorStripes = 0;
private boolean allowDisableMtls = true;
+ private List<X509Certificate> operatorCertificates = Collections.emptyList();
@Override public ModelContext.FeatureFlags featureFlags() { return this; }
@Override public boolean multitenant() { return multitenant; }
@@ -98,6 +100,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<X509Certificate> operatorCertificates() { return operatorCertificates; }
public TestProperties setFeedConcurrency(double feedConcurrency) {
this.feedConcurrency = feedConcurrency;
@@ -236,6 +239,11 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea
return this;
}
+ public TestProperties setOperatorCertificates(List<X509Certificate> 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<ContainerModel> {
// 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<ContainerModel> {
.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<ContainerModel> {
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<X509Certificate> trustedCertificates = deployState.tlsClientAuthority()
+ .map(X509CertificateUtils::certificateListFromPem)
+ .orElse(Collections.emptyList());
+ ArrayList<X509Certificate> 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..6649087f454 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,14 @@ 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.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -82,6 +94,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;
@@ -818,6 +831,53 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase {
}
@Test
+ public void operator_certificates_are_joined_with_clients_pem() {
+ var applicationPackage = new MockApplicationPackage.Builder()
+ .withRoot(applicationFolder.getRoot())
+ .build();
+
+ KeyPair key = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ var applicationTrustCert = X509CertificateUtils.toPem(
+ X509CertificateBuilder
+ .fromKeypair(key, new X500Principal("CN=application"), Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA512_WITH_ECDSA, BigInteger.valueOf(1))
+ .build());
+ var operatorCert = X509CertificateBuilder
+ .fromKeypair(key, new X500Principal("CN=operator"), Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA512_WITH_ECDSA, BigInteger.valueOf(1))
+ .build();
+
+ applicationPackage.getFile(Path.fromString("security")).createDirectory();
+ applicationPackage.getFile(Path.fromString("security/clients.pem")).writeFile(new StringReader(applicationTrustCert));
+
+ var deployState = new DeployState.Builder().properties(
+ new TestProperties()
+ .setOperatorCertificates(List.of(operatorCert))
+ .setHostedVespa(true)
+ .setEndpointCertificateSecrets(Optional.of(new EndpointCertificateSecrets("CERT", "KEY"))))
+ .zone(new Zone(SystemName.PublicCd, Environment.dev, RegionName.defaultName()))
+ .applicationPackage(applicationPackage)
+ .build();
+
+ Element clusterElem = DomBuilderTest.parse("<container version='1.0' />");
+
+ createModel(root, deployState, null, clusterElem);
+
+ ApplicationContainer container = (ApplicationContainer)root.getProducer("container/container.0");
+ List<ConnectorFactory> connectorFactories = container.getHttp().getHttpServer().get().getConnectorFactories();
+ ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow();
+
+ ConnectorConfig.Builder builder = new ConnectorConfig.Builder();
+ tlsPort.getConfig(builder);
+
+ ConnectorConfig connectorConfig = new ConnectorConfig(builder);
+ var caCerts = X509CertificateUtils.certificateListFromPem(connectorConfig.ssl().caCertificate());
+ assertEquals(2, caCerts.size());
+ List<String> certnames = caCerts.stream()
+ .map(cert -> cert.getSubjectX500Principal().getName())
+ .collect(Collectors.toList());
+ assertThat(certnames, containsInAnyOrder("CN=operator", "CN=application"));
+ }
+
+ @Test
public void environment_vars_are_honoured() {
Element clusterElem = DomBuilderTest.parse(
"<container version='1.0'>",
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 d5b0c6fce4a..e48848417fc 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;
@@ -281,6 +282,7 @@ public class ModelContextImpl implements ModelContext {
private final SecretStore secretStore;
private final StringFlag jvmGCOptionsFlag;
private final boolean allowDisableMtls;
+ private final List<X509Certificate> operatorCertificates;
public Properties(ApplicationId applicationId,
ConfigserverConfig configserverConfig,
@@ -294,7 +296,8 @@ public class ModelContextImpl implements ModelContext {
Optional<ApplicationRoles> applicationRoles,
Optional<Quota> maybeQuota,
List<TenantSecretStore> tenantSecretStores,
- SecretStore secretStore) {
+ SecretStore secretStore,
+ List<X509Certificate> operatorCertificates) {
this.featureFlags = new FeatureFlags(flagSource, applicationId);
this.applicationId = applicationId;
this.multitenant = configserverConfig.multitenant() || configserverConfig.hostedVespa() || Boolean.getBoolean("multitenant");
@@ -317,6 +320,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; }
@@ -385,6 +389,11 @@ public class ModelContextImpl implements ModelContext {
return allowDisableMtls;
}
+ @Override
+ public List<X509Certificate> operatorCertificates() {
+ return operatorCertificates;
+ }
+
public String flagValueForClusterType(StringFlag flag, Optional<ClusterSpec.Type> 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 5c0207878f1..ada278709fd 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
@@ -37,7 +37,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;
@@ -163,7 +165,8 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
.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> applicationRoles;
private final Optional<Quota> quota;
private final List<TenantSecretStore> tenantSecretStores;
+ private final List<X509Certificate> operatorCertificates;
private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors,
boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion,
@@ -76,7 +85,7 @@ public final class PrepareParams {
Optional<EndpointCertificateMetadata> endpointCertificateMetadata,
Optional<DockerImage> dockerImageRepository, Optional<AthenzDomain> athenzDomain,
Optional<ApplicationRoles> applicationRoles, Optional<Quota> quota, List<TenantSecretStore> tenantSecretStores,
- boolean force, boolean waitForResourcesInPrepare) {
+ boolean force, boolean waitForResourcesInPrepare, List<X509Certificate> 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> applicationRoles = Optional.empty();
private Optional<Quota> quota = Optional.empty();
private List<TenantSecretStore> tenantSecretStores = List.of();
+ private List<X509Certificate> operatorCertificates = List.of();
public Builder() { }
@@ -245,11 +256,17 @@ public final class PrepareParams {
return this;
}
+ public Builder withOperatorCertificates(List<X509Certificate> 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<X509Certificate> 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<TenantSecretStore> tenantSecretStores() {
return tenantSecretStores;
}
+
+ public List<X509Certificate> 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<Session> {
sessionZooKeeperClient.writeTenantSecretStores(tenantSecretStores);
}
+ public void setOperatorCertificates(List<X509Certificate> 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<Session> {
return sessionZooKeeperClient.readTenantSecretStores();
}
+ public List<X509Certificate> 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 30cdc0f6e8a..9360a2b1a2a 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
@@ -51,6 +51,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;
@@ -204,7 +205,8 @@ public class SessionPreparer {
applicationRoles,
params.quota(),
params.tenantSecretStores(),
- secretStore);
+ secretStore,
+ params.operatorCertificates());
this.fileDistributionProvider = fileDistributionFactory.createProvider(serverDbSessionDir);
this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry,
permanentApplicationPackage,
@@ -277,7 +279,8 @@ public class SessionPreparer {
prepareResult.allocatedHosts(),
athenzDomain,
params.quota(),
- params.tenantSecretStores());
+ params.tenantSecretStores(),
+ params.operatorCertificates());
checkTimeout("write state to zookeeper");
}
@@ -327,7 +330,8 @@ public class SessionPreparer {
AllocatedHosts allocatedHosts,
Optional<AthenzDomain> athenzDomain,
Optional<Quota> quota,
- List<TenantSecretStore> tenantSecretStores) {
+ List<TenantSecretStore> tenantSecretStores,
+ List<X509Certificate> operatorCertificates) {
ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger);
try {
zkDeployer.deploy(applicationPackage, fileRegistryMap, allocatedHosts);
@@ -339,6 +343,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 cb46d65c4c5..0386b1ca4ef 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
@@ -259,6 +259,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<X509Certificate> certificates) {
+ if( ! certificates.isEmpty()) {
+ var bytes = uncheck(() -> SlimeUtils.toJsonBytes(OperatorCertificateSerializer.toSlime(certificates)));
+ configCurator.putData(operatorCertificatesPath(), bytes);
+ }
+ }
+
+ public List<X509Certificate> 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..7fe2ab5e12f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/OperatorCertificateSerializer.java
@@ -0,0 +1,33 @@
+// 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 {
+
+ public static Slime toSlime(List<X509Certificate> certificateList) {
+ Slime slime = new Slime();
+ Cursor array = slime.setArray();
+ certificateList.stream()
+ .map(X509CertificateUtils::toPem)
+ .forEach(array::addString);
+ return slime;
+ }
+
+ public static List<X509Certificate> fromSlime(Inspector array) {
+ return SlimeUtils.entriesStream(array)
+ .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..06ff9f4b3f6 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,11 @@ 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.slime.ArrayInserter;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Injector;
@@ -20,10 +25,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 +190,26 @@ 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);
+
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ X500Principal subject = new X500Principal("CN=myservice");
+ X509Certificate cert =
+ X509CertificateBuilder.fromKeypair(keyPair, subject, Instant.now(),
+ Instant.now().plus(1, ChronoUnit.DAYS), SignatureAlgorithm.SHA256_WITH_ECDSA,
+ BigInteger.valueOf(1))
+ .setBasicConstraints(true, true)
+ .build();
+ array.addString(X509CertificateUtils.toPem(cert));
+ PrepareParams prepareParams = PrepareParams.fromJson(SlimeUtils.toJsonBytes(slime), TenantName.from("foo"), Duration.ofSeconds(60));
+ assertEquals(1, prepareParams.operatorCertificates().size());
+ assertEquals(cert, prepareParams.operatorCertificates().get(0));
+ }
+
private void assertPrepareParamsEqual(PrepareParams urlParams, PrepareParams jsonParams) {
assertEquals(urlParams.ignoreValidationErrors(), jsonParams.ignoreValidationErrors());
assertEquals(urlParams.isDryRun(), jsonParams.isDryRun());