diff options
author | Harald Musum <musum@verizonmedia.com> | 2021-05-10 18:29:17 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-10 18:29:17 +0200 |
commit | 6fa898de746704ed0fa9a6e95b76637e57e6cdfd (patch) | |
tree | e9861d539ed191c36a120e98e490d87ab809e5e8 /controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate | |
parent | f5a9459ced27144d60455eb450f77769fea95f90 (diff) |
Revert "Include legacy endpoint in certificate in public"
Diffstat (limited to 'controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate')
2 files changed, 176 insertions, 132 deletions
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 new file mode 100644 index 00000000000..6f964999fba --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java @@ -0,0 +1,176 @@ +// 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 deleted file mode 100644 index fead9e26181..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java +++ /dev/null @@ -1,132 +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.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); - } - -} |