summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorandreer <andreer@verizonmedia.com>2020-05-26 13:13:46 +0200
committerandreer <andreer@verizonmedia.com>2020-05-26 13:13:46 +0200
commit7cd4cbfe87388c094942a742cba9bf7d2d251623 (patch)
tree5cdacc89821d7cc644d5c24236641f44d1347421 /controller-server
parent3f1df98f07c3a00610a1627e78370e865a70d653 (diff)
only require rotation endpoints for prod zones (+ unit tests and fixes)
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java41
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java97
2 files changed, 101 insertions, 37 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
index c2ea9be044f..513709aefc8 100644
--- 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
@@ -123,7 +123,7 @@ public class EndpointCertificateManager {
// Reprovision certificate if it is missing SANs for the zone we are deploying to
var sansInCertificate = currentCertificateMetadata.get().requestedDnsSans();
- var requiredSansForZone = dnsNamesOf(instance.id(), List.of(zone));
+ var requiredSansForZone = dnsNamesOf(instance.id(), zone);
if (sansInCertificate.isPresent() && !sansInCertificate.get().containsAll(requiredSansForZone)) {
var reprovisionedCertificateMetadata = provisionEndpointCertificate(instance, currentCertificateMetadata, zone, instanceSpec);
curator.writeEndpointCertificateMetadata(instance.id(), reprovisionedCertificateMetadata);
@@ -222,29 +222,30 @@ public class EndpointCertificateManager {
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() && instanceSpec.isPresent()) {
+ if (!deploymentZone.environment().isManuallyDeployed()) {
zoneCandidateList.stream()
- .filter(z -> instanceSpec.get().deploysTo(Environment.prod, z.region()) || z.environment().isTest())
+ .filter(z -> z.environment().isTest() || instanceSpec.isPresent() && instanceSpec.get().deploysTo(Environment.prod, z.region()))
.forEach(requiredZones::add);
}
var requiredNames = requiredZones.stream()
- .flatMap(zone -> dnsNamesOf(instance.id(), List.of(zone)).stream())
- .collect(Collectors.toCollection(ArrayList::new));
+ .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(), List.of(zone)))
- .filter(zoneNames -> !currentlyPresentNames.containsAll(zoneNames))
+ .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()));
+ 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(), requiredNames, currentMetadata);
+ return endpointCertificateProvider.requestCaSignedCertificate(instance.id(), List.copyOf(requiredNames), currentMetadata);
}
private void validateEndpointCertificate(EndpointCertificateMetadata endpointCertificateMetadata, Instance instance, ZoneId zone) {
@@ -279,7 +280,7 @@ public class EndpointCertificateManager {
.filter(san -> san.getType().equals(SubjectAlternativeName.Type.DNS_NAME))
.map(SubjectAlternativeName::getValue).collect(Collectors.toSet());
- var dnsNamesOfZone = dnsNamesOf(instance.id(), List.of(zone));
+ var dnsNamesOfZone = dnsNamesOf(instance.id(), zone);
if (!subjectAlternativeNames.containsAll(dnsNamesOfZone))
throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate is missing required SANs for zone " + zone.value());
@@ -295,22 +296,24 @@ public class EndpointCertificateManager {
}
}
- private List<String> dnsNamesOf(ApplicationId applicationId, List<ZoneId> zones) {
+ 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()));
- var globalDefaultEndpoint = Endpoint.of(applicationId).named(EndpointId.defaultId());
- var rotationEndpoints = Endpoint.of(applicationId).wildcard();
+ List<Endpoint.EndpointBuilder> endpoints = new ArrayList<>();
- var zoneLocalEndpoints = zones.stream().flatMap(zone -> Stream.of(
- Endpoint.of(applicationId).target(ClusterSpec.Id.from("default"), zone),
- Endpoint.of(applicationId).wildcard(zone)
- ));
+ if(zone.environment().isProduction()) {
+ endpoints.add(Endpoint.of(applicationId).named(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));
- Stream.concat(Stream.of(globalDefaultEndpoint, rotationEndpoints), zoneLocalEndpoints)
+ endpoints.stream()
.map(endpoint -> endpoint.routingMethod(RoutingMethod.exclusive))
.map(endpoint -> endpoint.on(Endpoint.Port.tls()))
.map(endpointBuilder -> endpointBuilder.in(zoneRegistry.system()))
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/EndpointCertificateManagerTest.java
index fbdc094b240..d29a1c539bb 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/EndpointCertificateManagerTest.java
@@ -1,6 +1,7 @@
package com.yahoo.vespa.hosted.controller.certificate;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.security.KeyAlgorithm;
@@ -25,8 +26,10 @@ import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -45,20 +48,49 @@ public class EndpointCertificateManagerTest {
private final EndpointCertificateManager endpointCertificateManager =
new EndpointCertificateManager(zoneRegistryMock, mockCuratorDb, secretStore, endpointCertificateMock, clock, inMemoryFlagSource);
+ private static List<String> expectedSans = List.of(
+ "vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud",
+ "default.default.global.vespa.oath.cloud",
+ "*.default.default.global.vespa.oath.cloud",
+ "default.default.aws-us-east-1a.vespa.oath.cloud",
+ "*.default.default.aws-us-east-1a.vespa.oath.cloud",
+ "default.default.us-east-1.test.vespa.oath.cloud",
+ "*.default.default.us-east-1.test.vespa.oath.cloud",
+ "default.default.us-east-3.staging.vespa.oath.cloud",
+ "*.default.default.us-east-3.staging.vespa.oath.cloud"
+ );
+
+ private static List<String> expectedAdditionalSans = List.of(
+ "default.default.ap-northeast-1.vespa.oath.cloud",
+ "*.default.default.ap-northeast-1.vespa.oath.cloud"
+ );
+
+ private static List<String> expectedCombinedSans = new ArrayList<>() {{
+ addAll(expectedSans);
+ addAll(expectedAdditionalSans);
+ }};
+
+ private static List<String> expectedDevSans = List.of(
+ "vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud",
+ "default.default.us-east-1.dev.vespa.oath.cloud",
+ "*.default.default.us-east-1.dev.vespa.oath.cloud"
+ );
+
private static final KeyPair testKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 192);
- private static final X509Certificate testCertificate = X509CertificateBuilder
- .fromKeypair(
- testKeyPair,
- new X500Principal("CN=test"),
- Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES),
- SignatureAlgorithm.SHA256_WITH_ECDSA,
- X509CertificateBuilder.generateRandomSerialNumber())
- .addSubjectAlternativeName("vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud")
- .addSubjectAlternativeName("default.default.global.vespa.oath.cloud")
- .addSubjectAlternativeName("*.default.default.global.vespa.oath.cloud")
- .addSubjectAlternativeName("default.default.us-east-1.test.vespa.oath.cloud")
- .addSubjectAlternativeName("*.default.default.us-east-1.test.vespa.oath.cloud")
- .build();
+ private static final X509Certificate testCertificate = makeTestCert(expectedSans);
+ private static final X509Certificate testCertificate2 = makeTestCert(expectedCombinedSans);
+
+ private static X509Certificate makeTestCert(List<String> sans) {
+ X509CertificateBuilder x509CertificateBuilder = X509CertificateBuilder
+ .fromKeypair(
+ testKeyPair,
+ new X500Principal("CN=test"),
+ Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES),
+ SignatureAlgorithm.SHA256_WITH_ECDSA,
+ X509CertificateBuilder.generateRandomSerialNumber());
+ for (String san : sans) x509CertificateBuilder = x509CertificateBuilder.addSubjectAlternativeName(san);
+ return x509CertificateBuilder.build();
+ }
private final Instance testInstance = new Instance(ApplicationId.defaultId());
private final String testKeyName = "testKeyName";
@@ -68,24 +100,36 @@ public class EndpointCertificateManagerTest {
@Before
public void setUp() {
zoneRegistryMock.exclusiveRoutingIn(zoneRegistryMock.zones().all().zones());
- testZone = zoneRegistryMock.zones().directlyRouted().zones().stream().findFirst().orElseThrow().getId();
+ testZone = zoneRegistryMock.zones().directlyRouted().in(Environment.prod).zones().stream().findFirst().orElseThrow().getId();
inMemoryFlagSource.withBooleanFlag(Flags.VALIDATE_ENDPOINT_CERTIFICATES.id(), true);
}
@Test
- public void provisions_new_certificate() {
+ 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());
+ 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(expectedDevSans, endpointCertificateMetadata.get().requestedDnsSans().orElseThrow());
+ }
+
+ @Test
+ public void provisions_new_certificate_in_prod() {
Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(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().orElseThrow());
}
@Test
public void reuses_stored_certificate_metadata() {
mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, 7));
secretStore.setSecret(testKeyName, KeyUtils.toPem(testKeyPair.getPrivate()), 7);
- secretStore.setSecret(testCertName, X509CertificateUtils.toPem(testCertificate)+X509CertificateUtils.toPem(testCertificate), 7);
+ secretStore.setSecret(testCertName, X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 7);
Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
assertEquals(testKeyName, endpointCertificateMetadata.get().keyName());
@@ -101,7 +145,7 @@ public class EndpointCertificateManagerTest {
secretStore.setSecret(testCertName, "cert", 7);
secretStore.setSecret(testKeyName, KeyUtils.toPem(testKeyPair.getPrivate()), 8);
secretStore.setSecret(testKeyName, KeyUtils.toPem(testKeyPair.getPrivate()), 9);
- secretStore.setSecret(testCertName, X509CertificateUtils.toPem(testCertificate)+X509CertificateUtils.toPem(testCertificate), 8);
+ secretStore.setSecret(testCertName, X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 8);
mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, 7));
Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
@@ -114,11 +158,28 @@ public class EndpointCertificateManagerTest {
public void reprovisions_certificate_when_necessary() {
mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, -1, Optional.of("uuid"), Optional.of(List.of()), 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);
+ secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 0);
Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(testInstance, testZone, Optional.empty());
assertTrue(endpointCertificateMetadata.isPresent());
assertEquals(0, endpointCertificateMetadata.get().version());
assertEquals(endpointCertificateMetadata, mockCuratorDb.readEndpointCertificateMetadata(testInstance.id()));
}
+ @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();
+
+ mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, -1, Optional.of("uuid"), Optional.of(expectedSans), Optional.of("mockCa")));
+ secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), -1);
+ secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), -1);
+
+ 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());
+ assertTrue(endpointCertificateMetadata.isPresent());
+ assertEquals(0, endpointCertificateMetadata.get().version());
+ assertEquals(endpointCertificateMetadata, mockCuratorDb.readEndpointCertificateMetadata(testInstance.id()));
+ assertEquals(Set.copyOf(expectedCombinedSans), Set.copyOf(endpointCertificateMetadata.get().requestedDnsSans().orElseThrow()));
+ }
}