diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-05-10 15:16:54 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-10 15:16:54 +0200 |
commit | 37610a9818a990bd6104d5b42c4e195f3106d89b (patch) | |
tree | 2ed0d5044aeb0f28efaedbf54a73bec8823c9ef2 | |
parent | 3c43c8d986959713b1200b89f02884c098591b84 (diff) | |
parent | 75206c19fa16e85b368191241ae56c13e4461e68 (diff) |
Merge pull request #17801 from vespa-engine/mpolden/include-legacy-endpoint-in-cert
Include legacy endpoint in certificate in public
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())); } + } |