summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2021-05-10 15:16:54 +0200
committerGitHub <noreply@github.com>2021-05-10 15:16:54 +0200
commit37610a9818a990bd6104d5b42c4e195f3106d89b (patch)
tree2ed0d5044aeb0f28efaedbf54a73bec8823c9ef2
parent3c43c8d986959713b1200b89f02884c098591b84 (diff)
parent75206c19fa16e85b368191241ae56c13e4461e68 (diff)
Merge pull request #17801 from vespa-engine/mpolden/include-legacy-endpoint-in-cert
Include legacy endpoint in certificate in public
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMock.java3
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java3
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java45
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java176
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java132
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java (renamed from controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java)90
8 files changed, 254 insertions, 215 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMock.java
index b5ee78251f0..07da6969b64 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMock.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMock.java
@@ -34,11 +34,12 @@ public class EndpointCertificateMock implements EndpointCertificateProvider {
@Override
public List<EndpointCertificateMetadata> listCertificates() {
- return Collections.emptyList();
+ return List.of();
}
@Override
public void deleteCertificate(ApplicationId applicationId, EndpointCertificateMetadata endpointCertificateMetadata) {
dnsNames.remove(applicationId);
}
+
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java
index ea92f84f72f..9ce4fccc375 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java
@@ -16,6 +16,9 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
+/**
+ * @author andreer
+ */
public class EndpointCertificateValidatorImpl implements EndpointCertificateValidator {
private final SecretStore secretStore;
private final Clock clock;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java
index 7e89a3becd5..780701b3b77 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java
@@ -5,7 +5,11 @@ import com.yahoo.config.provision.zone.ZoneId;
import java.util.List;
+/**
+ * @author andreer
+ */
public class EndpointCertificateValidatorMock implements EndpointCertificateValidator {
+
@Override
public void validate(
EndpointCertificateMetadata endpointCertificateMetadata,
@@ -14,4 +18,5 @@ public class EndpointCertificateValidatorMock implements EndpointCertificateVali
List<String> requiredNamesForZone) {
// Mock does no validation - for unit tests only!
}
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
index e6c96134ca7..0f9188d1f65 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
@@ -52,7 +52,7 @@ import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
-import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificateManager;
+import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificates;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
@@ -118,7 +118,7 @@ public class ApplicationController {
private final Clock clock;
private final DeploymentTrigger deploymentTrigger;
private final ApplicationPackageValidator applicationPackageValidator;
- private final EndpointCertificateManager endpointCertificateManager;
+ private final EndpointCertificates endpointCertificates;
private final StringFlag dockerImageRepoFlag;
private final BillingController billingController;
@@ -137,12 +137,9 @@ public class ApplicationController {
deploymentTrigger = new DeploymentTrigger(controller, clock);
applicationPackageValidator = new ApplicationPackageValidator(controller);
- endpointCertificateManager = new EndpointCertificateManager(
- controller.zoneRegistry(),
- curator,
- controller.serviceRegistry().endpointCertificateProvider(),
- controller.serviceRegistry().endpointCertificateValidator(),
- clock);
+ endpointCertificates = new EndpointCertificates(controller,
+ controller.serviceRegistry().endpointCertificateProvider(),
+ controller.serviceRegistry().endpointCertificateValidator());
// Update serialization format of all applications
Once.after(Duration.ofMinutes(1), () -> {
@@ -382,7 +379,7 @@ public class ApplicationController {
&& run.testerCertificate().isPresent())
applicationPackage = applicationPackage.withTrustedCertificate(run.testerCertificate().get());
- endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(instance, zone, applicationPackage.deploymentSpec().instance(instance.name()));
+ endpointCertificateMetadata = endpointCertificates.getMetadata(instance, zone, applicationPackage.deploymentSpec().instance(instance.name()));
containerEndpoints = controller.routing().containerEndpointsOf(application.get(), job.application().instance(), zone);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
index 433b2b340d5..12329351a59 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
@@ -3,12 +3,16 @@ package com.yahoo.vespa.hosted.controller;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentInstanceSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.flags.BooleanFlag;
@@ -34,6 +38,7 @@ import com.yahoo.vespa.hosted.controller.routing.RoutingId;
import com.yahoo.vespa.hosted.controller.routing.RoutingPolicies;
import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -149,6 +154,39 @@ public class RoutingController {
return Collections.unmodifiableMap(endpoints);
}
+ /** Returns the wildcard endpoints for given deployment */
+ public List<String> wildcardEndpoints(DeploymentId deployment) {
+ List<String> endpointDnsNames = new ArrayList<>();
+
+ // We add first an endpoint name based on a hash of the application ID,
+ // as the certificate provider requires the first CN to be < 64 characters long.
+ endpointDnsNames.add(commonNameHashOf(deployment.applicationId(), controller.system()));
+
+ // Add wildcard names for global endpoints when deploying to production
+ List<Endpoint.EndpointBuilder> builders = new ArrayList<>();
+ if (deployment.zoneId().environment().isProduction()) {
+ builders.add(Endpoint.of(deployment.applicationId()).target(EndpointId.defaultId()));
+ builders.add(Endpoint.of(deployment.applicationId()).wildcard());
+ }
+
+ // Add wildcard names for zone endpoints
+ builders.add(Endpoint.of(deployment.applicationId()).target(ClusterSpec.Id.from("default"), deployment.zoneId()));
+ builders.add(Endpoint.of(deployment.applicationId()).wildcard(deployment.zoneId()));
+
+ // Build all endpoints
+ for (var builder : builders) {
+ builder = builder.routingMethod(RoutingMethod.exclusive)
+ .on(Port.tls());
+ Endpoint endpoint = builder.in(controller.system());
+ endpointDnsNames.add(endpoint.dnsName());
+ if (controller.system().isPublic()) {
+ Endpoint legacyEndpoint = builder.legacy().in(controller.system());
+ endpointDnsNames.add(legacyEndpoint.dnsName());
+ }
+ }
+ return Collections.unmodifiableList(endpointDnsNames);
+ }
+
/** Change status of all global endpoints for given deployment */
public void setGlobalRotationStatus(DeploymentId deployment, EndpointStatus status) {
endpointsOf(deployment.applicationId()).requiresRotation().primary().ifPresent(endpoint -> {
@@ -349,6 +387,13 @@ public class RoutingController {
.isPresent();
}
+ /** Create a common name based on a hash of given application. This must be less than 64 characters long. */
+ private static String commonNameHashOf(ApplicationId application, SystemName system) {
+ HashCode sha1 = Hashing.sha1().hashString(application.serializedForm(), StandardCharsets.UTF_8);
+ String base32 = BaseEncoding.base32().omitPadding().lowerCase().encode(sha1.asBytes());
+ return 'v' + base32 + Endpoint.dnsSuffix(system);
+ }
+
/** Returns direct routing endpoints if any exist and feature flag is set for given application */
// TODO: Remove this when feature flag is removed, and in-line .direct() filter where relevant
public EndpointList directEndpoints(EndpointList endpoints, ApplicationId application) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java
deleted file mode 100644
index 6f964999fba..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.certificate;
-
-import com.google.common.hash.Hashing;
-import com.google.common.io.BaseEncoding;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import org.jetbrains.annotations.NotNull;
-
-import java.nio.charset.Charset;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Looks up stored endpoint certificate metadata, provisions new certificates if none is found,
- * re-provisions if zone is not covered, and uses refreshed certificates if a newer version is available.
- * <p>
- * See also EndpointCertificateMaintainer, which handles refreshes, deletions and triggers deployments
- *
- * @author andreer
- */
-public class EndpointCertificateManager {
-
- private static final Logger log = Logger.getLogger(EndpointCertificateManager.class.getName());
-
- private final ZoneRegistry zoneRegistry;
- private final CuratorDb curator;
- private final EndpointCertificateProvider endpointCertificateProvider;
- private final Clock clock;
- private final EndpointCertificateValidator endpointCertificateValidator;
-
- public EndpointCertificateManager(ZoneRegistry zoneRegistry,
- CuratorDb curator,
- EndpointCertificateProvider endpointCertificateProvider,
- EndpointCertificateValidator endpointCertificateValidator,
- Clock clock) {
- this.zoneRegistry = zoneRegistry;
- this.curator = curator;
- this.endpointCertificateProvider = endpointCertificateProvider;
- this.clock = clock;
- this.endpointCertificateValidator = endpointCertificateValidator;
- }
-
- public Optional<EndpointCertificateMetadata> getEndpointCertificateMetadata(Instance instance, ZoneId zone, Optional<DeploymentInstanceSpec> instanceSpec) {
- var t0 = Instant.now();
- Optional<EndpointCertificateMetadata> metadata = getOrProvision(instance, zone, instanceSpec);
- metadata.ifPresent(m -> curator.writeEndpointCertificateMetadata(instance.id(), m.withLastRequested(clock.instant().getEpochSecond())));
- Duration duration = Duration.between(t0, Instant.now());
- if (duration.toSeconds() > 30)
- log.log(Level.INFO, String.format("Getting endpoint certificate metadata for %s took %d seconds!", instance.id().serializedForm(), duration.toSeconds()));
- return metadata;
- }
-
- @NotNull
- private Optional<EndpointCertificateMetadata> getOrProvision(Instance instance, ZoneId zone, Optional<DeploymentInstanceSpec> instanceSpec) {
- final var currentCertificateMetadata = curator.readEndpointCertificateMetadata(instance.id());
-
- if (currentCertificateMetadata.isEmpty()) {
- var provisionedCertificateMetadata = provisionEndpointCertificate(instance, Optional.empty(), zone, instanceSpec);
- // We do not verify the certificate if one has never existed before - because we do not want to
- // wait for it to be available before we deploy. This allows the config server to start
- // provisioning nodes ASAP, and the risk is small for a new deployment.
- curator.writeEndpointCertificateMetadata(instance.id(), provisionedCertificateMetadata);
- return Optional.of(provisionedCertificateMetadata);
- }
-
- // Re-provision certificate if it is missing SANs for the zone we are deploying to
- var requiredSansForZone = dnsNamesOf(instance.id(), zone);
- if (!currentCertificateMetadata.get().requestedDnsSans().containsAll(requiredSansForZone)) {
- var reprovisionedCertificateMetadata =
- provisionEndpointCertificate(instance, currentCertificateMetadata, zone, instanceSpec)
- .withRequestId(currentCertificateMetadata.get().request_id()); // We're required to keep the original request_id
- curator.writeEndpointCertificateMetadata(instance.id(), reprovisionedCertificateMetadata);
- // Verification is unlikely to succeed in this case, as certificate must be available first - controller will retry
- endpointCertificateValidator.validate(reprovisionedCertificateMetadata, instance.id().serializedForm(), zone, requiredSansForZone);
- return Optional.of(reprovisionedCertificateMetadata);
- }
-
- endpointCertificateValidator.validate(currentCertificateMetadata.get(), instance.id().serializedForm(), zone, requiredSansForZone);
- return currentCertificateMetadata;
- }
-
- private EndpointCertificateMetadata provisionEndpointCertificate(Instance instance, Optional<EndpointCertificateMetadata> currentMetadata, ZoneId deploymentZone, Optional<DeploymentInstanceSpec> instanceSpec) {
-
- List<String> currentlyPresentNames = currentMetadata.isPresent() ?
- currentMetadata.get().requestedDnsSans() : Collections.emptyList();
-
- var requiredZones = new LinkedHashSet<>(Set.of(deploymentZone));
-
- var zoneCandidateList = zoneRegistry.zones().controllerUpgraded().zones().stream().map(ZoneApi::getId).collect(Collectors.toList());
-
- // If not deploying to a dev or perf zone, require all prod zones in deployment spec + test and staging
- if (!deploymentZone.environment().isManuallyDeployed()) {
- zoneCandidateList.stream()
- .filter(z -> z.environment().isTest() || instanceSpec.isPresent() && instanceSpec.get().deploysTo(z.environment(), z.region()))
- .forEach(requiredZones::add);
- }
-
- var requiredNames = requiredZones.stream()
- .flatMap(zone -> dnsNamesOf(instance.id(), zone).stream())
- .collect(Collectors.toCollection(LinkedHashSet::new));
-
- // Make sure all currently present names will remain present.
- // Instead of just adding "currently present names", we regenerate them in case the names for a zone have changed.
- zoneCandidateList.stream()
- .map(zone -> dnsNamesOf(instance.id(), zone))
- .filter(zoneNames -> zoneNames.stream().anyMatch(currentlyPresentNames::contains))
- .filter(currentlyPresentNames::containsAll)
- .forEach(requiredNames::addAll);
-
- // This check must be relaxed if we ever remove from the set of names generated.
- if (!requiredNames.containsAll(currentlyPresentNames))
- throw new RuntimeException("SANs to be requested do not cover all existing names! Missing names: "
- + currentlyPresentNames.stream().filter(s -> !requiredNames.contains(s)).collect(Collectors.joining(", ")));
-
- return endpointCertificateProvider.requestCaSignedCertificate(instance.id(), List.copyOf(requiredNames), currentMetadata);
- }
-
-
- private List<String> dnsNamesOf(ApplicationId applicationId, ZoneId zone) {
- List<String> endpointDnsNames = new ArrayList<>();
-
- // We add first an endpoint name based on a hash of the applicationId,
- // as the certificate provider requires the first CN to be < 64 characters long.
- endpointDnsNames.add(commonNameHashOf(applicationId, zoneRegistry.system()));
-
- List<Endpoint.EndpointBuilder> endpoints = new ArrayList<>();
-
- if (zone.environment().isProduction()) {
- endpoints.add(Endpoint.of(applicationId).target(EndpointId.defaultId()));
- endpoints.add(Endpoint.of(applicationId).wildcard());
- }
-
- endpoints.add(Endpoint.of(applicationId).target(ClusterSpec.Id.from("default"), zone));
- endpoints.add(Endpoint.of(applicationId).wildcard(zone));
-
- endpoints.stream()
- .map(endpoint -> endpoint.routingMethod(RoutingMethod.exclusive))
- .map(endpoint -> endpoint.on(Endpoint.Port.tls()))
- .map(endpointBuilder -> endpointBuilder.in(zoneRegistry.system()))
- .map(Endpoint::dnsName).forEach(endpointDnsNames::add);
-
- return Collections.unmodifiableList(endpointDnsNames);
- }
-
- /** Create a common name based on a hash of the ApplicationId. This should always be less than 64 characters long. */
- @SuppressWarnings("UnstableApiUsage")
- private static String commonNameHashOf(ApplicationId application, SystemName system) {
- var hashCode = Hashing.sha1().hashString(application.serializedForm(), Charset.defaultCharset());
- var base32encoded = BaseEncoding.base32().omitPadding().lowerCase().encode(hashCode.asBytes());
- return 'v' + base32encoded + Endpoint.dnsSuffix(system);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java
new file mode 100644
index 00000000000..fead9e26181
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java
@@ -0,0 +1,132 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.certificate;
+
+import com.yahoo.config.application.api.DeploymentInstanceSpec;
+import com.yahoo.config.provision.zone.ZoneApi;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.Instance;
+import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
+import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Looks up stored endpoint certificate metadata, provisions new certificates if none is found,
+ * re-provisions if zone is not covered, and uses refreshed certificates if a newer version is available.
+ *
+ * See also {@link com.yahoo.vespa.hosted.controller.maintenance.EndpointCertificateMaintainer}, which handles
+ * refreshes, deletions and triggers deployments.
+ *
+ * @author andreer
+ */
+public class EndpointCertificates {
+
+ private static final Logger log = Logger.getLogger(EndpointCertificates.class.getName());
+
+ private final Controller controller;
+ private final CuratorDb curator;
+ private final Clock clock;
+ private final EndpointCertificateProvider certificateProvider;
+ private final EndpointCertificateValidator certificateValidator;
+
+ public EndpointCertificates(Controller controller, EndpointCertificateProvider certificateProvider,
+ EndpointCertificateValidator certificateValidator) {
+ this.controller = controller;
+ this.curator = controller.curator();
+ this.clock = controller.clock();
+ this.certificateProvider = certificateProvider;
+ this.certificateValidator = certificateValidator;
+ }
+
+ /** Returns certificate metadata for endpoints of given instance and zone */
+ public Optional<EndpointCertificateMetadata> getMetadata(Instance instance, ZoneId zone, Optional<DeploymentInstanceSpec> instanceSpec) {
+ Instant start = clock.instant();
+ Optional<EndpointCertificateMetadata> metadata = getOrProvision(instance, zone, instanceSpec);
+ metadata.ifPresent(m -> curator.writeEndpointCertificateMetadata(instance.id(), m.withLastRequested(clock.instant().getEpochSecond())));
+ Duration duration = Duration.between(start, clock.instant());
+ if (duration.toSeconds() > 30)
+ log.log(Level.INFO, String.format("Getting endpoint certificate metadata for %s took %d seconds!", instance.id().serializedForm(), duration.toSeconds()));
+ return metadata;
+ }
+
+ private Optional<EndpointCertificateMetadata> getOrProvision(Instance instance, ZoneId zone, Optional<DeploymentInstanceSpec> instanceSpec) {
+ final var currentCertificateMetadata = curator.readEndpointCertificateMetadata(instance.id());
+
+ DeploymentId deployment = new DeploymentId(instance.id(), zone);
+
+ if (currentCertificateMetadata.isEmpty()) {
+ var provisionedCertificateMetadata = provisionEndpointCertificate(deployment, Optional.empty(), instanceSpec);
+ // We do not verify the certificate if one has never existed before - because we do not want to
+ // wait for it to be available before we deploy. This allows the config server to start
+ // provisioning nodes ASAP, and the risk is small for a new deployment.
+ curator.writeEndpointCertificateMetadata(instance.id(), provisionedCertificateMetadata);
+ return Optional.of(provisionedCertificateMetadata);
+ }
+
+ // Re-provision certificate if it is missing SANs for the zone we are deploying to
+ var requiredSansForZone = controller.routing().wildcardEndpoints(deployment);
+ if (!currentCertificateMetadata.get().requestedDnsSans().containsAll(requiredSansForZone)) {
+ var reprovisionedCertificateMetadata =
+ provisionEndpointCertificate(deployment, currentCertificateMetadata, instanceSpec)
+ .withRequestId(currentCertificateMetadata.get().request_id()); // We're required to keep the original request_id
+ curator.writeEndpointCertificateMetadata(instance.id(), reprovisionedCertificateMetadata);
+ // Verification is unlikely to succeed in this case, as certificate must be available first - controller will retry
+ certificateValidator.validate(reprovisionedCertificateMetadata, instance.id().serializedForm(), zone, requiredSansForZone);
+ return Optional.of(reprovisionedCertificateMetadata);
+ }
+
+ certificateValidator.validate(currentCertificateMetadata.get(), instance.id().serializedForm(), zone, requiredSansForZone);
+ return currentCertificateMetadata;
+ }
+
+ private EndpointCertificateMetadata provisionEndpointCertificate(DeploymentId deployment, Optional<EndpointCertificateMetadata> currentMetadata, Optional<DeploymentInstanceSpec> instanceSpec) {
+
+ List<String> currentlyPresentNames = currentMetadata.isPresent() ?
+ currentMetadata.get().requestedDnsSans() : Collections.emptyList();
+
+ var requiredZones = new LinkedHashSet<>(Set.of(deployment.zoneId()));
+
+ var zoneCandidateList = controller.zoneRegistry().zones().controllerUpgraded().zones().stream().map(ZoneApi::getId).collect(Collectors.toList());
+
+ // If not deploying to a dev or perf zone, require all prod zones in deployment spec + test and staging
+ if (!deployment.zoneId().environment().isManuallyDeployed()) {
+ zoneCandidateList.stream()
+ .filter(z -> z.environment().isTest() || instanceSpec.isPresent() && instanceSpec.get().deploysTo(z.environment(), z.region()))
+ .forEach(requiredZones::add);
+ }
+
+ var requiredNames = requiredZones.stream()
+ .flatMap(zone -> controller.routing().wildcardEndpoints(new DeploymentId(deployment.applicationId(), zone)).stream())
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+
+ // Make sure all currently present names will remain present.
+ // Instead of just adding "currently present names", we regenerate them in case the names for a zone have changed.
+ zoneCandidateList.stream()
+ .map(zone -> controller.routing().wildcardEndpoints(new DeploymentId(deployment.applicationId(), zone)))
+ .filter(zoneNames -> zoneNames.stream().anyMatch(currentlyPresentNames::contains))
+ .filter(currentlyPresentNames::containsAll)
+ .forEach(requiredNames::addAll);
+
+ // This check must be relaxed if we ever remove from the set of names generated.
+ if (!requiredNames.containsAll(currentlyPresentNames))
+ throw new RuntimeException("SANs to be requested do not cover all existing names! Missing names: "
+ + currentlyPresentNames.stream().filter(s -> !requiredNames.contains(s)).collect(Collectors.joining(", ")));
+
+ return certificateProvider.requestCaSignedCertificate(deployment.applicationId(), List.copyOf(requiredNames), currentMetadata);
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java
index e30f044c579..22a41740b91 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java
@@ -12,22 +12,20 @@ import com.yahoo.security.KeyUtils;
import com.yahoo.security.SignatureAlgorithm;
import com.yahoo.security.X509CertificateBuilder;
import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
+import com.yahoo.test.ManualClock;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMock;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMock;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorImpl;
import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
-import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
+import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import org.junit.Before;
import org.junit.Test;
import javax.security.auth.x500.X500Principal;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
-import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -41,17 +39,19 @@ import static org.junit.Assert.assertTrue;
/**
* @author andreer
*/
-public class EndpointCertificateManagerTest {
+public class EndpointCertificatesTest {
+ private final ControllerTester tester = new ControllerTester();
private final SecretStoreMock secretStore = new SecretStoreMock();
- private final ZoneRegistryMock zoneRegistryMock = new ZoneRegistryMock(SystemName.main);
- private final MockCuratorDb mockCuratorDb = new MockCuratorDb();
+ private final CuratorDb mockCuratorDb = tester.curator();
+ private final ManualClock clock = tester.clock();
private final EndpointCertificateMock endpointCertificateMock = new EndpointCertificateMock();
- private final InMemoryFlagSource inMemoryFlagSource = new InMemoryFlagSource();
- private static final Clock clock = Clock.fixed(Instant.EPOCH, java.time.ZoneId.systemDefault());
private final EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock);
- private final EndpointCertificateManager endpointCertificateManager =
- new EndpointCertificateManager(zoneRegistryMock, mockCuratorDb, endpointCertificateMock, endpointCertificateValidator, clock);
+ private final EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateMock, endpointCertificateValidator);
+ private final KeyPair testKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 192);
+
+ private X509Certificate testCertificate;
+ private X509Certificate testCertificate2;
private static final List<String> expectedSans = List.of(
"vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud",
@@ -81,11 +81,7 @@ public class EndpointCertificateManagerTest {
"*.default.default.us-east-1.dev.vespa.oath.cloud"
);
- private static final KeyPair testKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 192);
- private static final X509Certificate testCertificate = makeTestCert(expectedSans);
- private static final X509Certificate testCertificate2 = makeTestCert(expectedCombinedSans);
-
- private static X509Certificate makeTestCert(List<String> sans) {
+ private X509Certificate makeTestCert(List<String> sans) {
X509CertificateBuilder x509CertificateBuilder = X509CertificateBuilder
.fromKeypair(
testKeyPair,
@@ -104,14 +100,17 @@ public class EndpointCertificateManagerTest {
@Before
public void setUp() {
- zoneRegistryMock.exclusiveRoutingIn(zoneRegistryMock.zones().all().zones());
- testZone = zoneRegistryMock.zones().directlyRouted().in(Environment.prod).zones().stream().findFirst().orElseThrow().getId();
+ tester.zoneRegistry().exclusiveRoutingIn(tester.zoneRegistry().zones().all().zones());
+ testZone = tester.zoneRegistry().zones().directlyRouted().in(Environment.prod).zones().stream().findFirst().orElseThrow().getId();
+ clock.setInstant(Instant.EPOCH);
+ testCertificate = makeTestCert(expectedSans);
+ testCertificate2 = makeTestCert(expectedCombinedSans);
}
@Test
public void provisions_new_certificate_in_dev() {
- ZoneId testZone = zoneRegistryMock.zones().directlyRouted().in(Environment.dev).zones().stream().findFirst().orElseThrow().getId();
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
+ ZoneId testZone = tester.zoneRegistry().zones().directlyRouted().in(Environment.dev).zones().stream().findFirst().orElseThrow().getId();
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key"));
assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert"));
@@ -121,7 +120,39 @@ public class EndpointCertificateManagerTest {
@Test
public void provisions_new_certificate_in_prod() {
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty());
+ assertTrue(endpointCertificateMetadata.isPresent());
+ assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key"));
+ assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert"));
+ assertEquals(0, endpointCertificateMetadata.get().version());
+ assertEquals(expectedSans, endpointCertificateMetadata.get().requestedDnsSans());
+ }
+
+ @Test
+ public void provisions_new_certificate_in_public_prod() {
+ ControllerTester tester = new ControllerTester(SystemName.Public);
+ EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock);
+ EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateMock, endpointCertificateValidator);
+ List<String> expectedSans = List.of(
+ "vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.public.vespa.oath.cloud",
+ "default.default.global.public.vespa.oath.cloud",
+ "default.default.g.vespa-app.cloud",
+ "*.default.default.global.public.vespa.oath.cloud",
+ "*.default.default.g.vespa-app.cloud",
+ "default.default.aws-us-east-1a.public.vespa.oath.cloud",
+ "default.default.aws-us-east-1a.z.vespa-app.cloud",
+ "*.default.default.aws-us-east-1a.public.vespa.oath.cloud",
+ "*.default.default.aws-us-east-1a.z.vespa-app.cloud",
+ "default.default.aws-us-east-1c.test.public.vespa.oath.cloud",
+ "default.default.aws-us-east-1c.test.z.vespa-app.cloud",
+ "*.default.default.aws-us-east-1c.test.public.vespa.oath.cloud",
+ "*.default.default.aws-us-east-1c.test.z.vespa-app.cloud",
+ "default.default.aws-us-east-1c.staging.public.vespa.oath.cloud",
+ "default.default.aws-us-east-1c.staging.z.vespa-app.cloud",
+ "*.default.default.aws-us-east-1c.staging.public.vespa.oath.cloud",
+ "*.default.default.aws-us-east-1c.staging.z.vespa-app.cloud"
+ );
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key"));
assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert"));
@@ -140,7 +171,7 @@ public class EndpointCertificateManagerTest {
"", Optional.empty(), Optional.empty()));
secretStore.setSecret(testKeyName, KeyUtils.toPem(testKeyPair.getPrivate()), 7);
secretStore.setSecret(testCertName, X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 7);
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
assertEquals(testKeyName, endpointCertificateMetadata.get().keyName());
assertEquals(testCertName, endpointCertificateMetadata.get().certName());
@@ -152,7 +183,7 @@ public class EndpointCertificateManagerTest {
mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, -1, 0, "uuid", List.of(), "issuer", Optional.empty(), Optional.empty()));
secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), 0);
secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 0);
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
assertEquals(0, endpointCertificateMetadata.get().version());
assertEquals(endpointCertificateMetadata, mockCuratorDb.readEndpointCertificateMetadata(testInstance.id()));
@@ -160,7 +191,7 @@ public class EndpointCertificateManagerTest {
@Test
public void reprovisions_certificate_with_added_sans_when_deploying_to_new_zone() {
- ZoneId testZone = zoneRegistryMock.zones().directlyRouted().in(Environment.prod).zones().stream().skip(1).findFirst().orElseThrow().getId();
+ ZoneId testZone = tester.zoneRegistry().zones().directlyRouted().in(Environment.prod).zones().stream().skip(1).findFirst().orElseThrow().getId();
mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, -1, 0, "original-request-uuid", expectedSans, "mockCa", Optional.empty(), Optional.empty()));
secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), -1);
@@ -169,7 +200,7 @@ public class EndpointCertificateManagerTest {
secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), 0);
secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate2) + X509CertificateUtils.toPem(testCertificate2), 0);
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
assertEquals(0, endpointCertificateMetadata.get().version());
assertEquals(endpointCertificateMetadata, mockCuratorDb.readEndpointCertificateMetadata(testInstance.id()));
@@ -190,12 +221,13 @@ public class EndpointCertificateManagerTest {
" </instance>\n" +
"</deployment>\n");
- ZoneId testZone = zoneRegistryMock.zones().controllerUpgraded().in(Environment.staging).zones().stream().findFirst().orElseThrow().getId();
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.of(deploymentSpec.requireInstance("default")));
+ ZoneId testZone = tester.zoneRegistry().zones().all().in(Environment.staging).zones().stream().findFirst().orElseThrow().getId();
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.of(deploymentSpec.requireInstance("default")));
assertTrue(endpointCertificateMetadata.isPresent());
assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key"));
assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert"));
assertEquals(0, endpointCertificateMetadata.get().version());
assertEquals(Set.copyOf(expectedCombinedSans), Set.copyOf(endpointCertificateMetadata.get().requestedDnsSans()));
}
+
}