diff options
17 files changed, 204 insertions, 65 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java index 5143a38b2c1..5138bee1ff6 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java @@ -3,7 +3,10 @@ package com.yahoo.vespa.hosted.athenz.instanceproviderservice; import com.yahoo.component.annotation.Inject; import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.net.HostName; +import com.yahoo.security.KeyUtils; + import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; @@ -32,17 +35,20 @@ public class IdentityDocumentGenerator { private final NodeRepository nodeRepository; private final Zone zone; private final KeyProvider keyProvider; + private final SecretStore secretStore; private final AthenzProviderServiceConfig athenzProviderServiceConfig; @Inject public IdentityDocumentGenerator(AthenzProviderServiceConfig config, NodeRepository nodeRepository, Zone zone, - KeyProvider keyProvider) { + KeyProvider keyProvider, + SecretStore secretStore) { this.athenzProviderServiceConfig = config; this.nodeRepository = nodeRepository; this.zone = zone; this.keyProvider = keyProvider; + this.secretStore = secretStore; } public SignedIdentityDocument generateSignedIdentityDocument(String hostname, IdentityType identityType) { @@ -61,7 +67,7 @@ public class IdentityDocumentGenerator { Set<String> ips = new HashSet<>(node.ipConfig().primary()); - PrivateKey privateKey = keyProvider.getPrivateKey(athenzProviderServiceConfig.secretVersion()); + PrivateKey privateKey = privateKey(node); AthenzService providerService = new AthenzService(athenzProviderServiceConfig.domain(), athenzProviderServiceConfig.serviceName()); String configServerHostname = HostName.getLocalhost(); @@ -73,11 +79,28 @@ public class IdentityDocumentGenerator { return new SignedIdentityDocument( signature, athenzProviderServiceConfig.secretVersion(), providerUniqueId, providerService, SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION, configServerHostname, node.hostname(), - createdAt, ips, identityType, clusterType); + createdAt, ips, identityType, clusterType, ztsUrl(node)); } catch (Exception e) { throw new RuntimeException("Exception generating identity document: " + e.getMessage(), e); } } + private PrivateKey privateKey(Node node) { + // return sisSecret for public non-enclave hosts. secret otherwise + if (zone.system().isPublic() && !node.cloudAccount().isEnclave(zone)) { + String keyPem = secretStore.getSecret(athenzProviderServiceConfig.sisSecretName(), athenzProviderServiceConfig.sisSecretVersion()); + return KeyUtils.fromPemEncodedPrivateKey(keyPem); + } else { + return keyProvider.getPrivateKey(athenzProviderServiceConfig.secretVersion()); + } + } + private String ztsUrl(Node node) { + // return sisUrl for public non-enclave hosts, ztsUrl otherwise + if (zone.system().isPublic() && !node.cloudAccount().isEnclave(zone)) { + return athenzProviderServiceConfig.sisUrl(); + } else { + return athenzProviderServiceConfig.ztsUrl(); + } + } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java index d8bbf743d8c..a349bddc76b 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java @@ -7,6 +7,9 @@ import com.yahoo.config.model.api.ApplicationInfo; import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.secretstore.SecretStore; +import com.yahoo.security.KeyUtils; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; @@ -52,25 +55,32 @@ public class InstanceValidator { private final KeyProvider keyProvider; private final SuperModelProvider superModelProvider; private final NodeRepository nodeRepository; + private final SecretStore secretStore; + private final String sisSecretName; @Inject public InstanceValidator(KeyProvider keyProvider, SuperModelProvider superModelProvider, NodeRepository nodeRepository, - AthenzProviderServiceConfig config) { - this(keyProvider, superModelProvider, nodeRepository, new IdentityDocumentSigner(), new AthenzService(config.tenantService())); + AthenzProviderServiceConfig config, + SecretStore secretStore) { + this(keyProvider, superModelProvider, nodeRepository, new IdentityDocumentSigner(), new AthenzService(config.tenantService()), secretStore, config.sisSecretName()); } public InstanceValidator(KeyProvider keyProvider, SuperModelProvider superModelProvider, NodeRepository nodeRepository, IdentityDocumentSigner identityDocumentSigner, - AthenzService tenantIdentity){ + AthenzService tenantIdentity, + SecretStore secretStore, + String sisSecretName) { this.keyProvider = keyProvider; this.superModelProvider = superModelProvider; this.nodeRepository = nodeRepository; this.signer = identityDocumentSigner; this.tenantDockerContainerIdentity = tenantIdentity; + this.secretStore = secretStore; + this.sisSecretName = sisSecretName; } public boolean isValidInstance(InstanceConfirmation instanceConfirmation) { @@ -102,16 +112,30 @@ public class InstanceValidator { log.log(Level.FINE, () -> String.format("Validating instance %s.", providerUniqueId)); - PublicKey publicKey = keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion()); + // Find node matching vespa unique id + Node node = getNode(providerUniqueId); + + PublicKey publicKey = publicKey(node, signedIdentityDocument.signingKeyVersion()); if (! signer.hasValidSignature(signedIdentityDocument, publicKey)) { var msg = String.format("Instance %s has invalid signature.", providerUniqueId); throw new ValidationException(Level.SEVERE, () -> msg); } - validateAttributes(req, providerUniqueId); + validateAttributes(node, req, providerUniqueId); log.log(Level.FINE, () -> String.format("Instance %s is valid.", providerUniqueId)); } + private PublicKey publicKey(Node node, int version) { + // return sisSecret for public non-enclave hosts. + Zone zone = nodeRepository.zone(); + if (zone.system().isPublic() && !node.cloudAccount().isEnclave(zone)) { + String keyPem = secretStore.getSecret(sisSecretName, version); + return KeyUtils.extractPublicKey(KeyUtils.fromPemEncodedPrivateKey(keyPem)); + } else { + return keyProvider.getPublicKey(version); + } + } + // TODO Add actual validation. Cannot reuse isValidInstance as identity document is not part of the refresh request. // We'll have to perform some validation on the instance id and other fields of the attribute map. // Separate between tenant and node certificate as well. @@ -121,7 +145,8 @@ public class InstanceValidator { confirmation.provider, confirmation.attributes.get(SAN_DNS_ATTRNAME))); try { - validateAttributes(confirmation, getVespaUniqueInstanceId(confirmation)); + VespaUniqueInstanceId vespaUniqueInstanceId = getVespaUniqueInstanceId(confirmation); + validateAttributes(getNode(vespaUniqueInstanceId), confirmation, vespaUniqueInstanceId); return true; } catch (ValidationException e) { log.log(e.logLevel(), e.messageSupplier()); @@ -146,24 +171,13 @@ public class InstanceValidator { .orElse(null); } - private void validateAttributes(InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId) + private void validateAttributes(Node node, InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId) throws ValidationException { if(vespaUniqueInstanceId == null) { var msg = "Unable to find unique instance ID in refresh request: " + confirmation.toString(); throw new ValidationException(Level.WARNING, () -> msg); } - // Find node matching vespa unique id - Node node = nodeRepository.nodes().list().stream() - .filter(n -> n.allocation().isPresent()) - .filter(n -> nodeMatchesVespaUniqueId(n, vespaUniqueInstanceId)) - .findFirst() // Should be only one - .orElse(null); - if(node == null) { - var msg = "Invalid InstanceConfirmation, No nodes matching uniqueId: " + vespaUniqueInstanceId; - throw new ValidationException(Level.WARNING, () -> msg); - } - // Find list of ipaddresses List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get(SAN_IPS_ATTRNAME)) .map(s -> s.split(",")) @@ -199,6 +213,20 @@ public class InstanceValidator { } } + private Node getNode(VespaUniqueInstanceId vespaUniqueInstanceId) throws ValidationException { + // Find node matching vespa unique id + Node node = nodeRepository.nodes().list().stream() + .filter(n -> n.allocation().isPresent()) + .filter(n -> nodeMatchesVespaUniqueId(n, vespaUniqueInstanceId)) + .findFirst() // Should be only one + .orElse(null); + if(node == null) { + var msg = "Invalid InstanceConfirmation, No nodes matching uniqueId: " + vespaUniqueInstanceId; + throw new ValidationException(Level.WARNING, () -> msg); + } + return node; + } + private boolean nodeMatchesVespaUniqueId(Node node, VespaUniqueInstanceId vespaUniqueInstanceId) { return node.allocation().map(allocation -> allocation.membership().index() == vespaUniqueInstanceId.clusterIndex() && diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java index 531a815922b..231f22ac56b 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java @@ -65,7 +65,7 @@ public class CertificateAuthorityApiHandler extends ThreadedHttpRequestHandler { super(ctx); this.secretStore = secretStore; this.certificates = certificates; - this.caPrivateKeySecretName = athenzProviderServiceConfig.secretName(); + this.caPrivateKeySecretName = athenzProviderServiceConfig.sisSecretName(); this.caCertificateSecretName = athenzProviderServiceConfig.caCertSecretName(); this.instanceValidator = instanceValidator; } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java index fec03afab69..8c575a6403b 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java @@ -49,6 +49,7 @@ public class InstanceSerializer { private static final String IDD_IPADDRESSES_FIELD = "ip-addresses"; private static final String IDD_IDENTITY_TYPE_FIELD = "identity-type"; private static final String IDD_CLUSTER_TYPE_FIELD = "cluster-type"; + private static final String IDD_ZTS_URL_FIELD = "zts-url"; private static final ObjectMapper objectMapper = new ObjectMapper(); static { @@ -100,10 +101,12 @@ public class InstanceSerializer { IdentityType identityType = IdentityType.fromId(requireField(IDD_IDENTITY_TYPE_FIELD, root).asString()); var clusterTypeField = root.field(IDD_CLUSTER_TYPE_FIELD); var clusterType = clusterTypeField.valid() ? ClusterType.from(clusterTypeField.asString()) : null; + var ztsUrlField = root.field(IDD_ZTS_URL_FIELD); + var ztsUrl = ztsUrlField.valid() ? ztsUrlField.asString() : ""; return new SignedIdentityDocument(signature, (int)signingKeyVersion, providerUniqueId, athenzService, (int)documentVersion, - configserverHostname, instanceHostname, createdAt, ips, identityType, clusterType); + configserverHostname, instanceHostname, createdAt, ips, identityType, clusterType, ztsUrl); } private static Instant getJsr310Instant(double v) { diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGeneratorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGeneratorTest.java index 9205baff0fc..340be33c2a3 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGeneratorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGeneratorTest.java @@ -18,6 +18,7 @@ import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; +import com.yahoo.vespa.hosted.ca.restapi.mock.SecretStoreMock; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Allocation; @@ -74,11 +75,12 @@ public class IdentityDocumentGeneratorTest { when(nodes.node(eq(parentHostname))).thenReturn(Optional.of(parentNode)); when(nodes.node(eq(containerHostname))).thenReturn(Optional.of(containerNode)); AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider(); + SecretStoreMock secretStore = new SecretStoreMock(); String dnsSuffix = "vespa.dns.suffix"; AthenzProviderServiceConfig config = getAthenzProviderConfig("domain", "service", dnsSuffix); IdentityDocumentGenerator identityDocumentGenerator = - new IdentityDocumentGenerator(config, nodeRepository, ZONE, keyProvider); + new IdentityDocumentGenerator(config, nodeRepository, ZONE, keyProvider, secretStore); SignedIdentityDocument signedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(containerHostname, IdentityType.TENANT); // Verify attributes diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java index a7947aff283..42d6b92dea1 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidatorTest.java @@ -9,9 +9,16 @@ import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Zone; +import com.yahoo.security.KeyAlgorithm; +import com.yahoo.security.KeyUtils; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; @@ -19,15 +26,19 @@ import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner; +import com.yahoo.vespa.curator.mock.MockCurator; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator.ValidationException; +import com.yahoo.vespa.hosted.ca.restapi.mock.SecretStoreMock; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.Nodes; import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors; +import com.yahoo.vespa.hosted.provision.testutils.MockNodeRepository; import org.junit.jupiter.api.Test; +import java.security.PrivateKey; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -64,11 +75,13 @@ public class InstanceValidatorTest { private final AthenzService vespaTenantDomain = new AthenzService("vespa.vespa.tenant"); private final AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider(); + private final SecretStoreMock secretStore = new SecretStoreMock(); + private final String sisSecretName = "sis-secret-name"; @Test void application_does_not_exist() { SuperModelProvider superModelProvider = mockSuperModelProvider(); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain, secretStore, sisSecretName); assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -76,7 +89,7 @@ public class InstanceValidatorTest { void application_does_not_have_domain_set() { SuperModelProvider superModelProvider = mockSuperModelProvider( mockApplicationInfo(applicationId, 5, Collections.emptyList())); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, new IdentityDocumentSigner(), vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, new IdentityDocumentSigner(), vespaTenantDomain, secretStore, sisSecretName); assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -88,7 +101,7 @@ public class InstanceValidatorTest { SuperModelProvider superModelProvider = mockSuperModelProvider( mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo))); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain, secretStore, sisSecretName); assertFalse(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -106,7 +119,7 @@ public class InstanceValidatorTest { mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo))); IdentityDocumentSigner signer = mock(IdentityDocumentSigner.class); when(signer.hasValidSignature(any(), any())).thenReturn(true); - InstanceValidator instanceValidator = new InstanceValidator(mock(KeyProvider.class), superModelProvider, mockNodeRepo(), signer, vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(mock(KeyProvider.class), superModelProvider, mockNodeRepo(), signer, vespaTenantDomain, secretStore, sisSecretName); assertTrue(instanceValidator.isValidInstance(createRegisterInstanceConfirmation(applicationId, domain, service))); } @@ -114,7 +127,7 @@ public class InstanceValidatorTest { @Test void rejects_invalid_provider_unique_id_in_csr() { SuperModelProvider superModelProvider = mockSuperModelProvider(); - InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null, null, vespaTenantDomain, secretStore, sisSecretName); InstanceConfirmation instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service); VespaUniqueInstanceId tamperedId = new VespaUniqueInstanceId(0, "default", "instance", "app", "tenant", "us-north-1", "dev", IdentityType.NODE); instanceConfirmation.set("sanDNS", tamperedId.asDottedString() + ".instanceid.athenz.dev-us-north-1.vespa.yahoo.cloud"); @@ -124,7 +137,7 @@ public class InstanceValidatorTest { @Test void rejects_unknown_ips_in_csr() { NodeRepository nodeRepository = mockNodeRepo(); - InstanceValidator instanceValidator = new InstanceValidator(null, mockSuperModelProvider(), nodeRepository, null, vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, mockSuperModelProvider(), nodeRepository, null, vespaTenantDomain, secretStore, sisSecretName); InstanceConfirmation instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service); Set<String> nodeIp = nodeRepository.nodes().list().owner(applicationId).stream().findFirst() .map(Node::ipConfig) @@ -142,7 +155,7 @@ public class InstanceValidatorTest { var props = Map.of(SERVICE_PROPERTIES_DOMAIN_KEY, domain, SERVICE_PROPERTIES_SERVICE_KEY, service); var info = new ServiceInfo("serviceName", "type", List.of(), props, "confId", "hostName"); var provider = mockSuperModelProvider(mockApplicationInfo(applicationId, 5, List.of(info))); - var instanceValidator = new InstanceValidator(keyProvider, provider, mockNodeRepo(), new IdentityDocumentSigner(), vespaTenantDomain); + var instanceValidator = new InstanceValidator(keyProvider, provider, mockNodeRepo(), new IdentityDocumentSigner(), vespaTenantDomain, secretStore, sisSecretName); var instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service); instanceConfirmation.set("sanURI", "vespa://cluster-type/content"); var exception = assertThrows(ValidationException.class, () -> instanceValidator.validateInstance(instanceConfirmation)); @@ -155,7 +168,7 @@ public class InstanceValidatorTest { NodeRepository nodeRepository = mock(NodeRepository.class); Nodes nodes = mock(Nodes.class); when(nodeRepository.nodes()).thenReturn(nodes); - InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain, secretStore, sisSecretName); List<Node> nodeList = createNodes(10); Node node = nodeList.get(0); @@ -170,7 +183,7 @@ public class InstanceValidatorTest { @Test void rejects_refresh_on_ip_mismatch() { NodeRepository nodeRepository = mockNodeRepo(); - InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain, secretStore, sisSecretName); Set<String> nodeIp = nodeRepository.nodes().list().owner(applicationId).stream().findFirst() .map(Node::ipConfig) @@ -191,7 +204,7 @@ public class InstanceValidatorTest { Nodes nodes = mock(Nodes.class); when(nodeRepository.nodes()).thenReturn(nodes); - InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain); + InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository, new IdentityDocumentSigner(), vespaTenantDomain, secretStore, sisSecretName); List<Node> nodeList = createNodes(10); @@ -202,11 +215,40 @@ public class InstanceValidatorTest { } + @Test + public void uses_correct_keys_in_public() throws ValidationException { + var sisKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048); + secretStore.setSecret(sisSecretName, KeyUtils.toPem(sisKeyPair.getPrivate())); + + var props = Map.of(SERVICE_PROPERTIES_DOMAIN_KEY, domain, SERVICE_PROPERTIES_SERVICE_KEY, service); + var info = new ServiceInfo("serviceName", "type", List.of(), props, "confId", "hostName"); + var provider = mockSuperModelProvider(mockApplicationInfo(applicationId, 5, List.of(info))); + + { + // non Enclave hosts in public should use the SIS key + var nonEnclaveHosts = createNodes(10, Optional.empty()); + var instanceValidator = new InstanceValidator(keyProvider, provider, mockNodeRepo(nonEnclaveHosts, SystemName.PublicCd), new IdentityDocumentSigner(), vespaTenantDomain, secretStore, sisSecretName); + var instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service, sisKeyPair.getPrivate()); + instanceValidator.validateInstance(instanceConfirmation); + } + { + // Enclave hosts should use the Vespa provider key + var enclaveHosts = createNodes(10, Optional.of(CloudAccount.from("123456789012"))); + var instanceValidator = new InstanceValidator(keyProvider, provider, mockNodeRepo(enclaveHosts, SystemName.PublicCd), new IdentityDocumentSigner(), vespaTenantDomain, secretStore, sisSecretName); + var instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service, keyProvider.getPrivateKey(0)); + instanceValidator.validateInstance(instanceConfirmation); + } + } + private NodeRepository mockNodeRepo() { + return mockNodeRepo(createNodes(10), SystemName.cd); + } + + private NodeRepository mockNodeRepo(List<Node> nodeList, SystemName systemName) { NodeRepository nodeRepository = mock(NodeRepository.class); Nodes nodes = mock(Nodes.class); when(nodeRepository.nodes()).thenReturn(nodes); - List<Node> nodeList = createNodes(10); + when(nodeRepository.zone()).thenReturn(new Zone(systemName, Environment.dev, RegionName.from("us-north-1"))); Node node = nodeList.get(0); nodeList = allocateNode(nodeList, node, applicationId); when(nodes.list()).thenReturn(NodeList.copyOf(nodeList)); @@ -215,6 +257,11 @@ public class InstanceValidatorTest { private InstanceConfirmation createRegisterInstanceConfirmation( ApplicationId applicationId, String domain, String service) { + return createRegisterInstanceConfirmation(applicationId, domain, service, keyProvider.getPrivateKey(0)); + } + + private InstanceConfirmation createRegisterInstanceConfirmation( + ApplicationId applicationId, String domain, String service, PrivateKey signingKey) { VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(0, "default", applicationId.instance().value(), applicationId.application().value(), applicationId.tenant().value(), "us-north-1", "dev", IdentityType.NODE); var domainService = new AthenzService(domain, service); var clock = Instant.now(); @@ -222,11 +269,12 @@ public class InstanceValidatorTest { var signature = new IdentityDocumentSigner() .generateSignature( vespaUniqueInstanceId, domainService, "localhost", "localhost", clock, Set.of(), - IdentityType.NODE, keyProvider.getPrivateKey(0)); + IdentityType.NODE, signingKey); SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( signature, 0, vespaUniqueInstanceId, domainService, 0, "localhost", "localhost", - clock, Collections.emptySet(), IdentityType.NODE, clusterType); + clock, Collections.emptySet(), IdentityType.NODE, clusterType, "https://zts.url"); return createInstanceConfirmation(vespaUniqueInstanceId, domain, service, signedIdentityDocument); + } private InstanceConfirmation createRefreshInstanceConfirmation(ApplicationId applicationId, String domain, String service, List<String> ips) { @@ -275,11 +323,17 @@ public class InstanceValidatorTest { } private List<Node> createNodes(int num) { + return createNodes(num, Optional.empty()); + } + + private List<Node> createNodes(int num, Optional<CloudAccount> cloudAccount) { MockNodeFlavors flavors = new MockNodeFlavors(); List<Node> nodeList = new ArrayList<>(); for (int i = 0; i < num; i++) { - Node node = Node.create("foo" + i, new IP.Config(Set.of("::1" + i, "::2" + i, "::3" + i), Set.of()), - "foo" + i, flavors.getFlavorOrThrow("default"), NodeType.tenant).build(); + Node.Builder builder = Node.create("foo" + i, new IP.Config(Set.of("::1" + i, "::2" + i, "::3" + i), Set.of()), + "foo" + i, flavors.getFlavorOrThrow("default"), NodeType.tenant); + cloudAccount.ifPresent(builder::cloudAccount); + Node node = builder.build(); nodeList.add(node); } return nodeList; diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java index 8112f5779e5..d880fd5220b 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/ContainerTester.java @@ -64,6 +64,8 @@ public class ContainerTester { " <serviceName>servicename</serviceName>\n" + " <secretName>secretname</secretName>\n" + " <secretVersion>0</secretVersion>\n" + + " <sisSecretName>secretname</sisSecretName>\n" + + " <sisSecretVersion>0</sisSecretVersion>\n" + " <caCertSecretName>vespa.external.ca.cert</caCertSecretName>\n" + " <certDnsSuffix>suffix</certDnsSuffix>\n" + " <ztsUrl>https://localhost:123/</ztsUrl>\n" + diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java index ca624918beb..02398b19627 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializerTest.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.ca.restapi; +import com.yahoo.config.provision.Cloud; import com.yahoo.security.Pkcs10CsrUtils; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Slime; @@ -48,7 +49,8 @@ public class InstanceSerializerTest { Instant.now().truncatedTo(ChronoUnit.MICROS), // Truncate to the precision given from EntityBindingsMapper.toAttestationData() Collections.emptySet(), IdentityType.NODE, - ClusterType.CONTAINER); + ClusterType.CONTAINER, + "https://zts.url"); var json = String.format("{\n" + " \"provider\": \"provider_prod_us-north-1\",\n" + diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java index 4151c1f15d7..163db3e107e 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/restapi/mock/InstanceValidatorMock.java @@ -10,7 +10,7 @@ import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator; public class InstanceValidatorMock extends InstanceValidator { public InstanceValidatorMock() { - super(null, null, null, null, null); + super(null, null, null, null, null, null,""); } @Override diff --git a/configdefinitions/src/vespa/athenz-provider-service.def b/configdefinitions/src/vespa/athenz-provider-service.def index 2131aa88d30..4c9c74f9b8f 100644 --- a/configdefinitions/src/vespa/athenz-provider-service.def +++ b/configdefinitions/src/vespa/athenz-provider-service.def @@ -13,6 +13,11 @@ secretName string # Secret version secretVersion int +# Tempory resources +sisSecretName string default="" +sisSecretVersion int default=0 +sisUrl string default = "" + # Secret name of CA certificate caCertSecretName string diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 3989b45b9ac..9b4b04a3d62 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -345,6 +345,13 @@ public class Flags { "Takes effect on the next tick.", ZONE_ID, NODE_TYPE, HOSTNAME); + public static final UnboundBooleanFlag VESPA_ATHENZ_PROVIDER = defineFeatureFlag( + "vespa-athenz-provider", false, + List.of("mortent"), "2023-02-22", "2023-05-01", + "Enable athenz provider in public systems", + "Takes effect on next config server container start", + ZONE_ID); + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, List<String> owners, String createdAt, String expiresAt, String description, diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java index fc49dcc744c..6bd7d98e207 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java @@ -41,6 +41,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; @@ -189,11 +190,9 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { Pkcs10Csr csr = csrGenerator.generateInstanceCsr( context.identity(), doc.providerUniqueId(), doc.ipAddresses(), doc.clusterType(), keyPair); - // Set up a hostname verified for zts if this is configured to use the config server (internal zts) apis - HostnameVerifier ztsHostNameVerifier = useInternalZts - ? new AthenzIdentityVerifier(Set.of(configserverIdentity)) - : null; - try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withIdentityProvider(hostIdentityProvider).withHostnameVerifier(ztsHostNameVerifier).build()) { + // Allow all zts hosts while removing SIS + HostnameVerifier ztsHostNameVerifier = (hostname, sslSession) -> true; + try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint(doc)).withIdentityProvider(hostIdentityProvider).withHostnameVerifier(ztsHostNameVerifier).build()) { InstanceIdentity instanceIdentity = ztsClient.registerInstance( configserverIdentity, @@ -206,6 +205,15 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { } } + /** + * Return zts url from identity document, fallback to ztsEndpoint + */ + private URI ztsEndpoint(SignedIdentityDocument doc) { + return Optional.ofNullable(doc.ztsUrl()) + .filter(s -> !s.isBlank()) + .map(URI::create) + .orElse(ztsEndpoint); + } private void refreshIdentity(NodeAgentContext context, ContainerPath privateKeyFile, ContainerPath certificateFile, ContainerPath identityDocumentFile, SignedIdentityDocument doc) { KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); @@ -217,11 +225,9 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { .build(); try { - // Set up a hostname verified for zts if this is configured to use the config server (internal zts) apis - HostnameVerifier ztsHostNameVerifier = useInternalZts - ? new AthenzIdentityVerifier(Set.of(configserverIdentity)) - : null; - try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withSslContext(containerIdentitySslContext).withHostnameVerifier(ztsHostNameVerifier).build()) { + // Allow all zts hosts while removing SIS + HostnameVerifier ztsHostNameVerifier = (hostname, sslSession) -> true; + try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint(doc)).withSslContext(containerIdentitySslContext).withHostnameVerifier(ztsHostNameVerifier).build()) { InstanceIdentity instanceIdentity = ztsClient.refreshInstance( configserverIdentity, diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java index 9b7b666e353..067e8a6b00f 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java @@ -58,6 +58,7 @@ public class EntityBindingsMapper { entity.ipAddresses(), IdentityType.fromId(entity.identityType()), Optional.ofNullable(entity.clusterType()).map(ClusterType::from).orElse(null), + entity.ztsUrl(), entity.unknownAttributes()); } @@ -74,6 +75,7 @@ public class EntityBindingsMapper { model.ipAddresses(), model.identityType().id(), Optional.ofNullable(model.clusterType()).map(ClusterType::toConfigValue).orElse(null), + model.ztsUrl(), model.unknownAttributes()); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java index 49a39d25e87..eba89b72d87 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.athenz.identityprovider.api; import com.yahoo.vespa.athenz.api.AthenzService; +import java.net.URL; import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -17,7 +18,7 @@ import java.util.Set; public record SignedIdentityDocument(String signature, int signingKeyVersion, VespaUniqueInstanceId providerUniqueId, AthenzService providerService, int documentVersion, String configServerHostname, String instanceHostname, Instant createdAt, Set<String> ipAddresses, - IdentityType identityType, ClusterType clusterType, Map<String, Object> unknownAttributes) { + IdentityType identityType, ClusterType clusterType, String ztsUrl, Map<String, Object> unknownAttributes) { public SignedIdentityDocument { ipAddresses = Set.copyOf(ipAddresses); @@ -33,9 +34,9 @@ public record SignedIdentityDocument(String signature, int signingKeyVersion, Ve public SignedIdentityDocument(String signature, int signingKeyVersion, VespaUniqueInstanceId providerUniqueId, AthenzService providerService, int documentVersion, String configServerHostname, String instanceHostname, Instant createdAt, Set<String> ipAddresses, - IdentityType identityType, ClusterType clusterType) { + IdentityType identityType, ClusterType clusterType, String ztsUrl) { this(signature, signingKeyVersion, providerUniqueId, providerService, documentVersion, configServerHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType, Map.of()); + instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, Map.of()); } public static final int DEFAULT_DOCUMENT_VERSION = 2; diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java index c37dd2f9147..edbe032ec26 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java @@ -17,7 +17,7 @@ import java.util.Set; public record SignedIdentityDocumentEntity( String signature, int signingKeyVersion, String providerUniqueId, String providerService, int documentVersion, String configServerHostname, String instanceHostname, Instant createdAt, Set<String> ipAddresses, - String identityType, String clusterType, Map<String, Object> unknownAttributes) { + String identityType, String clusterType, String ztsUrl, Map<String, Object> unknownAttributes) { @JsonCreator public SignedIdentityDocumentEntity(@JsonProperty("signature") String signature, @@ -30,9 +30,10 @@ public record SignedIdentityDocumentEntity( @JsonProperty("created-at") Instant createdAt, @JsonProperty("ip-addresses") Set<String> ipAddresses, @JsonProperty("identity-type") String identityType, - @JsonProperty("cluster-type") String clusterType) { + @JsonProperty("cluster-type") String clusterType, + @JsonProperty("zts-url") String ztsUrl) { this(signature, signingKeyVersion, providerUniqueId, providerService, documentVersion, configServerHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType, new HashMap<>()); + instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, new HashMap<>()); } @JsonProperty("signature") @Override public String signature() { return signature; } @@ -46,6 +47,7 @@ public record SignedIdentityDocumentEntity( @JsonProperty("ip-addresses") @Override public Set<String> ipAddresses() { return ipAddresses; } @JsonProperty("identity-type") @Override public String identityType() { return identityType; } @JsonProperty("cluster-type") @Override public String clusterType() { return clusterType; } + @JsonProperty("zts-url") @Override public String ztsUrl() { return ztsUrl; } @JsonAnyGetter @Override public Map<String, Object> unknownAttributes() { return unknownAttributes; } @JsonAnySetter public void set(String name, Object value) { unknownAttributes.put(name, value); } } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java index f8c119190a6..2a68f6fd231 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapperTest.java @@ -30,6 +30,7 @@ class EntityBindingsMapperTest { "ip-addresses": [], "identity-type": "node", "cluster-type": "admin", + "zts-url": "https://zts.url/", "unknown-string": "string-value", "unknown-object": { "member-in-unknown-object": 123 } } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java index 0b8ff4277f1..72798b03fa8 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSignerTest.java @@ -36,6 +36,7 @@ public class IdentityDocumentSignerTest { private static final Instant createdAt = Instant.EPOCH; private static final HashSet<String> ipAddresses = new HashSet<>(Arrays.asList("1.2.3.4", "::1")); private static final ClusterType clusterType = ClusterType.CONTAINER; + private static final String ztsUrl = "https://foo"; @Test void generates_and_validates_signature() { @@ -46,27 +47,27 @@ public class IdentityDocumentSignerTest { SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType); + instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl); assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic())); } @Test - void ignores_cluster_type() { + void ignores_cluster_type_and_zts_url() { IdentityDocumentSigner signer = new IdentityDocumentSigner(); String signature = signer.generateSignature(id, providerService, configserverHostname, instanceHostname, createdAt, ipAddresses, identityType, keyPair.getPrivate()); - var docWithoutClusterType = new SignedIdentityDocument( + var docWithoutIgnoredFields = new SignedIdentityDocument( signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname, - instanceHostname, createdAt, ipAddresses, identityType, null); - var docWithClusterType = new SignedIdentityDocument( + instanceHostname, createdAt, ipAddresses, identityType, null, null); + var docWithIgnoredFields = new SignedIdentityDocument( signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType); + instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl); - assertTrue(signer.hasValidSignature(docWithoutClusterType, keyPair.getPublic())); - assertEquals(docWithClusterType.signature(), docWithoutClusterType.signature()); + assertTrue(signer.hasValidSignature(docWithoutIgnoredFields, keyPair.getPublic())); + assertEquals(docWithIgnoredFields.signature(), docWithoutIgnoredFields.signature()); } }
\ No newline at end of file |