diff options
author | Bjørn Christian Seime <bjorncs@yahooinc.com> | 2023-02-06 13:01:18 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-06 13:01:18 +0100 |
commit | 95e694413c0d4638c95e1d1285b6a45727dc616a (patch) | |
tree | a7d34e7d915d9a99a19a2d795d611f8438e1a8e5 | |
parent | d7173a4ef8190a8def529794b4f072771c784722 (diff) | |
parent | bc3ce2d5e0b385c6c469ce174a1ee746fb813000 (diff) |
Merge pull request #25862 from vespa-engine/bjorncs/cluster-type-in-node-cert
Bjorncs/cluster type in node cert
19 files changed, 323 insertions, 388 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 5b1a909e109..58a9e0130d6 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 @@ -5,6 +5,7 @@ import com.yahoo.component.annotation.Inject; import com.yahoo.config.provision.Zone; import com.yahoo.net.HostName; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; @@ -65,21 +66,14 @@ public class IdentityDocumentGenerator { String configServerHostname = HostName.getLocalhost(); Instant createdAt = Instant.now(); + var clusterType = ClusterType.from(allocation.membership().cluster().type().name()); String signature = signer.generateSignature( providerUniqueId, providerService, configServerHostname, - node.hostname(), createdAt, ips, identityType, privateKey); - + node.hostname(), createdAt, ips, identityType, clusterType, privateKey); return new SignedIdentityDocument( - signature, - athenzProviderServiceConfig.secretVersion(), - providerUniqueId, - providerService, - SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION, - configServerHostname, - node.hostname(), - createdAt, - ips, - identityType); + signature, athenzProviderServiceConfig.secretVersion(), providerUniqueId, providerService, + SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION, configServerHostname, node.hostname(), + createdAt, ips, identityType, clusterType); } catch (Exception e) { throw new RuntimeException("Exception generating identity document: " + e.getMessage(), e); } 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 6b7a4835aee..d8bbf743d8c 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 @@ -8,6 +8,7 @@ import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; @@ -17,14 +18,17 @@ import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; import java.net.InetAddress; +import java.net.URI; import java.security.PublicKey; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Stream; +import java.util.stream.Collectors; /** * Verifies that the instance's identity document is valid @@ -41,6 +45,7 @@ public class InstanceValidator { public static final String SAN_IPS_ATTRNAME = "sanIP"; public static final String SAN_DNS_ATTRNAME = "sanDNS"; + public static final String SAN_URI_ATTRNAME = "sanURI"; private final AthenzService tenantDockerContainerIdentity; private final IdentityDocumentSigner signer; @@ -69,34 +74,42 @@ public class InstanceValidator { } public boolean isValidInstance(InstanceConfirmation instanceConfirmation) { - SignedIdentityDocument signedIdentityDocument = EntityBindingsMapper.toSignedIdentityDocument(instanceConfirmation.signedIdentityDocument); + try { + validateInstance(instanceConfirmation); + return true; + } catch (ValidationException e) { + log.log(e.logLevel(), e.messageSupplier()); + return false; + } + } + + public void validateInstance(InstanceConfirmation req) throws ValidationException { + SignedIdentityDocument signedIdentityDocument = EntityBindingsMapper.toSignedIdentityDocument(req.signedIdentityDocument); VespaUniqueInstanceId providerUniqueId = signedIdentityDocument.providerUniqueId(); ApplicationId applicationId = ApplicationId.from( providerUniqueId.tenant(), providerUniqueId.application(), providerUniqueId.instance()); - VespaUniqueInstanceId csrProviderUniqueId = getVespaUniqueInstanceId(instanceConfirmation); + VespaUniqueInstanceId csrProviderUniqueId = getVespaUniqueInstanceId(req); if(! providerUniqueId.equals(csrProviderUniqueId)) { - log.log(Level.WARNING, String.format("Instance %s has invalid provider unique ID in CSR (%s)", providerUniqueId, csrProviderUniqueId)); - return false; + var msg = String.format("Instance %s has invalid provider unique ID in CSR (%s)", providerUniqueId, csrProviderUniqueId); + throw new ValidationException(Level.WARNING, () -> msg); } - if (! isSameIdentityAsInServicesXml(applicationId, instanceConfirmation.domain, instanceConfirmation.service)) { - return false; + if (! isSameIdentityAsInServicesXml(applicationId, req.domain, req.service)) { + Supplier<String> msg = () -> "Invalid identity '%s.%s' in services.xml".formatted(req.domain, req.service); + throw new ValidationException(Level.FINE, msg); } log.log(Level.FINE, () -> String.format("Validating instance %s.", providerUniqueId)); PublicKey publicKey = keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion()); if (! signer.hasValidSignature(signedIdentityDocument, publicKey)) { - log.log(Level.SEVERE, () -> String.format("Instance %s has invalid signature.", providerUniqueId)); - return false; + var msg = String.format("Instance %s has invalid signature.", providerUniqueId); + throw new ValidationException(Level.SEVERE, () -> msg); } - if(validateAttributes(instanceConfirmation, providerUniqueId)) { - log.log(Level.FINE, () -> String.format("Instance %s is valid.", providerUniqueId)); - return true; - } - return false; + validateAttributes(req, providerUniqueId); + log.log(Level.FINE, () -> String.format("Instance %s is valid.", providerUniqueId)); } // TODO Add actual validation. Cannot reuse isValidInstance as identity document is not part of the refresh request. @@ -108,7 +121,11 @@ public class InstanceValidator { confirmation.provider, confirmation.attributes.get(SAN_DNS_ATTRNAME))); try { - return validateAttributes(confirmation, getVespaUniqueInstanceId(confirmation)); + validateAttributes(confirmation, getVespaUniqueInstanceId(confirmation)); + return true; + } catch (ValidationException e) { + log.log(e.logLevel(), e.messageSupplier()); + return false; } catch (Exception e) { log.log(Level.WARNING, "Encountered exception while refreshing certificate for confirmation: " + confirmation, e); return false; @@ -129,10 +146,11 @@ public class InstanceValidator { .orElse(null); } - private boolean validateAttributes(InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId) { + private void validateAttributes(InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId) + throws ValidationException { if(vespaUniqueInstanceId == null) { - log.log(Level.WARNING, "Unable to find unique instance ID in refresh request: " + confirmation.toString()); - return false; + 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 @@ -142,8 +160,8 @@ public class InstanceValidator { .findFirst() // Should be only one .orElse(null); if(node == null) { - log.log(Level.WARNING, "Invalid InstanceConfirmation, No nodes matching uniqueId: " + vespaUniqueInstanceId); - return false; + var msg = "Invalid InstanceConfirmation, No nodes matching uniqueId: " + vespaUniqueInstanceId; + throw new ValidationException(Level.WARNING, () -> msg); } // Find list of ipaddresses @@ -160,10 +178,25 @@ public class InstanceValidator { // Validate that ipaddresses in request are valid for node if(! nodeIpAddresses.containsAll(ips)) { - log.log(Level.WARNING, "Invalid InstanceConfirmation, wrong ip in : " + vespaUniqueInstanceId); - return false; + var msg = "Invalid InstanceConfirmation, wrong ip in : " + vespaUniqueInstanceId; + throw new ValidationException(Level.WARNING, () -> msg); + } + + var urisCommaSeparated = confirmation.attributes.get(SAN_URI_ATTRNAME); + Set<URI> requestedUris; + try { + requestedUris = Optional.ofNullable(urisCommaSeparated).stream() + .flatMap(s -> Arrays.stream(s.split(","))).map(URI::create).collect(Collectors.toSet()); + } catch (IllegalArgumentException e) { + throw new ValidationException(Level.WARNING, () -> "Invalid SAN URIs: " + urisCommaSeparated, e); + } + var clusterType = node.allocation().map(a -> a.membership().cluster().type()).orElse(null); + Set<URI> allowedUris = clusterType != null + ? Set.of(ClusterType.from(clusterType.name()).asCertificateSanUri()) : Set.of(); + if (!allowedUris.containsAll(requestedUris)) { + Supplier<String> msg = () -> "Illegal SAN URIs: expected '%s' found '%s'".formatted(allowedUris, requestedUris); + throw new ValidationException(Level.WARNING, msg); } - return true; } private boolean nodeMatchesVespaUniqueId(Node node, VespaUniqueInstanceId vespaUniqueInstanceId) { @@ -217,4 +250,16 @@ public class InstanceValidator { return true; } + + public static class ValidationException extends Exception { + private final Level logLevel; + private final Supplier<String> msg; + + public ValidationException(Level logLevel, Supplier<String> msg) { this(logLevel, msg, null); } + public ValidationException(Level logLevel, Supplier<String> msg, Throwable cause) { super(cause); this.logLevel = logLevel; this.msg = msg; } + + @Override public String getMessage() { return msg.get(); } + public Level logLevel() { return logLevel; } + public Supplier<String> messageSupplier() { return msg; } + } } 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 83a7b850365..fec03afab69 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 @@ -8,12 +8,13 @@ import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; import com.yahoo.text.StringUtilities; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; 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.slime.SlimeUtils; import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity; import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh; import com.yahoo.vespa.hosted.ca.instance.InstanceRegistration; @@ -47,6 +48,7 @@ public class InstanceSerializer { private static final String IDD_CREATED_AT_FIELD = "created-at"; 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 ObjectMapper objectMapper = new ObjectMapper(); static { @@ -96,9 +98,12 @@ public class InstanceSerializer { Set<String> ips = new HashSet<>(); requireField(IDD_IPADDRESSES_FIELD, root).traverse((ArrayTraverser) (__, entry) -> ips.add(entry.asString())); 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; + return new SignedIdentityDocument(signature, (int)signingKeyVersion, providerUniqueId, athenzService, (int)documentVersion, - configserverHostname, instanceHostname, createdAt, ips, identityType); + configserverHostname, instanceHostname, createdAt, ips, identityType, clusterType); } private static Instant getJsr310Instant(double v) { 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 2b5165a815a..b996f52fe28 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 @@ -13,11 +13,13 @@ import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; 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.hosted.athenz.instanceproviderservice.InstanceValidator.ValidationException; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -41,7 +43,9 @@ import java.util.stream.Stream; import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator.SERVICE_PROPERTIES_DOMAIN_KEY; import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator.SERVICE_PROPERTIES_SERVICE_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -59,6 +63,7 @@ public class InstanceValidatorTest { private final String service = "service"; private final AthenzService vespaTenantDomain = new AthenzService("vespa.vespa.tenant"); + private final AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider(); @Test void application_does_not_exist() { @@ -133,6 +138,19 @@ public class InstanceValidatorTest { } @Test + void rejects_invalid_cluster_type_in_csr() { + 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 instanceConfirmation = createRegisterInstanceConfirmation(applicationId, domain, service); + instanceConfirmation.set("sanURI", "vespa://cluster-type/content"); + var exception = assertThrows(ValidationException.class, () -> instanceValidator.validateInstance(instanceConfirmation)); + var expectedMsg = "Illegal SAN URIs: expected '[vespa://cluster-type/container]' found '[vespa://cluster-type/content]'"; + assertEquals(expectedMsg, exception.getMessage()); + } + + @Test void accepts_valid_refresh_requests() { NodeRepository nodeRepository = mock(NodeRepository.class); Nodes nodes = mock(Nodes.class); @@ -188,8 +206,6 @@ 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); - List<Node> nodeList = createNodes(10); Node node = nodeList.get(0); nodeList = allocateNode(nodeList, node, applicationId); @@ -197,18 +213,19 @@ public class InstanceValidatorTest { return nodeRepository; } - private InstanceConfirmation createRegisterInstanceConfirmation(ApplicationId applicationId, String domain, String service) { + private InstanceConfirmation createRegisterInstanceConfirmation( + ApplicationId applicationId, String domain, String service) { VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(0, "default", applicationId.instance().value(), applicationId.application().value(), applicationId.tenant().value(), "us-north-1", "dev", IdentityType.NODE); - SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument(null, - 0, - vespaUniqueInstanceId, - new AthenzService(domain, service), - 0, - "localhost", - "localhost", - Instant.now(), - Collections.emptySet(), - IdentityType.NODE); + var domainService = new AthenzService(domain, service); + var clock = Instant.now(); + var clusterType = ClusterType.CONTAINER; + var signature = new IdentityDocumentSigner() + .generateSignature( + vespaUniqueInstanceId, domainService, "localhost", "localhost", clock, Set.of(), + IdentityType.NODE, clusterType, keyProvider.getPrivateKey(0)); + SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( + signature, 0, vespaUniqueInstanceId, domainService, 0, "localhost", "localhost", + clock, Collections.emptySet(), IdentityType.NODE, clusterType); return createInstanceConfirmation(vespaUniqueInstanceId, domain, service, signedIdentityDocument); } @@ -228,6 +245,7 @@ public class InstanceValidatorTest { .map(EntityBindingsMapper::toSignedIdentityDocumentEntity) .orElse(null)); instanceConfirmation.set("sanDNS", vespaUniqueInstanceId.asDottedString() + ".instanceid.athenz.dev-us-north-1.vespa.yahoo.cloud"); + instanceConfirmation.set("sanURI", "vespa://cluster-type/container"); return instanceConfirmation; } 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 df75e09b957..ca624918beb 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 @@ -7,6 +7,7 @@ import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; import com.yahoo.text.StringUtilities; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; @@ -46,7 +47,8 @@ public class InstanceSerializerTest { "instancehostname", Instant.now().truncatedTo(ChronoUnit.MICROS), // Truncate to the precision given from EntityBindingsMapper.toAttestationData() Collections.emptySet(), - IdentityType.NODE); + IdentityType.NODE, + ClusterType.CONTAINER); var json = String.format("{\n" + " \"provider\": \"provider_prod_us-north-1\",\n" + 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 b903712254b..2e5d269b720 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 @@ -14,7 +14,6 @@ import com.yahoo.vespa.athenz.client.zts.ZtsClientException; import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocumentClient; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.client.CsrGenerator; import com.yahoo.vespa.athenz.identityprovider.client.DefaultIdentityDocumentClient; import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; @@ -180,9 +179,9 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { private void registerIdentity(NodeAgentContext context, ContainerPath privateKeyFile, ContainerPath certificateFile, ContainerPath identityDocumentFile) { KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - SignedIdentityDocument signedIdentityDocument = identityDocumentClient.getNodeIdentityDocument(context.hostname().value()); + var doc = identityDocumentClient.getNodeIdentityDocument(context.hostname().value()); Pkcs10Csr csr = csrGenerator.generateInstanceCsr( - context.identity(), signedIdentityDocument.providerUniqueId(), signedIdentityDocument.ipAddresses(), keyPair); + 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 @@ -193,19 +192,19 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { ztsClient.registerInstance( configserverIdentity, context.identity(), - EntityBindingsMapper.toAttestationData(signedIdentityDocument), + EntityBindingsMapper.toAttestationData(doc), csr); - EntityBindingsMapper.writeSignedIdentityDocumentToFile(identityDocumentFile, signedIdentityDocument); + EntityBindingsMapper.writeSignedIdentityDocumentToFile(identityDocumentFile, doc); writePrivateKeyAndCertificate(privateKeyFile, keyPair.getPrivate(), certificateFile, instanceIdentity.certificate()); context.log(logger, "Instance successfully registered and credentials written to file"); } } private void refreshIdentity(NodeAgentContext context, ContainerPath privateKeyFile, ContainerPath certificateFile, ContainerPath identityDocumentFile) { - SignedIdentityDocument identityDocument = EntityBindingsMapper.readSignedIdentityDocumentFromFile(identityDocumentFile); + var doc = EntityBindingsMapper.readSignedIdentityDocumentFromFile(identityDocumentFile); KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); Pkcs10Csr csr = csrGenerator.generateInstanceCsr( - context.identity(), identityDocument.providerUniqueId(), identityDocument.ipAddresses(), keyPair); + context.identity(), doc.providerUniqueId(), doc.ipAddresses(), doc.clusterType(), keyPair); SSLContext containerIdentitySslContext = new SslContextBuilder().withKeyStore(privateKeyFile, certificateFile) .withTrustStore(ztsTrustStorePath) @@ -221,7 +220,7 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { ztsClient.refreshInstance( configserverIdentity, context.identity(), - identityDocument.providerUniqueId().asDottedString(), + doc.providerUniqueId().asDottedString(), csr); writePrivateKeyAndCertificate(privateKeyFile, keyPair.getPrivate(), certificateFile, instanceIdentity.certificate()); context.log(logger, "Instance successfully refreshed and credentials written to file"); diff --git a/security-utils/src/main/java/com/yahoo/security/tls/Capability.java b/security-utils/src/main/java/com/yahoo/security/tls/Capability.java index 0ae253985a6..3931f6ab7b7 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/Capability.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/Capability.java @@ -7,6 +7,7 @@ import java.util.Arrays; * @author bjorncs */ public enum Capability { + NONE("vespa.none"), // placeholder for no capabilities CONTENT__CLUSTER_CONTROLLER__INTERNAL_STATE_API("vespa.content.cluster_controller.internal_state_api"), CONTENT__DOCUMENT_API("vespa.content.document_api"), CONTENT__METRICS_API("vespa.content.metrics_api"), diff --git a/vespa-athenz/pom.xml b/vespa-athenz/pom.xml index d71948b59d3..29a4058e180 100644 --- a/vespa-athenz/pom.xml +++ b/vespa-athenz/pom.xml @@ -18,16 +18,23 @@ <dependencies> <!-- provided --> <dependency> + <!-- required for bundle-plugin to generate import-package statements for Java's standard library --> <groupId>com.yahoo.vespa</groupId> - <artifactId>container-dev</artifactId> + <artifactId>jdisc_core</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>annotations</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>container-disc</artifactId> <version>${project.version}</version> <scope>provided</scope> - <exclusions> - <exclusion> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - </exclusion> - </exclusions> </dependency> <dependency> <groupId>com.yahoo.vespa</groupId> @@ -46,6 +53,45 @@ <artifactId>jna</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-jsr310</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>vespajlib</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>defaults</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>component</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-lib</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>provided</scope> + </dependency> <!-- test --> <dependency> diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/ClusterType.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/ClusterType.java new file mode 100644 index 00000000000..3702f693a7b --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/ClusterType.java @@ -0,0 +1,40 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.athenz.identityprovider.api; + +import java.net.URI; + +/** + * Vespa cluster type + * + * @author bjorncs + */ +public enum ClusterType { + ADMIN, + CONTAINER, + CONTENT, + COMBINED; + + public static ClusterType from(String cfgValue) { + return switch (cfgValue) { + case "admin" -> ADMIN; + case "container" -> CONTAINER; + case "content" -> CONTENT; + case "combined" -> COMBINED; + default -> throw new IllegalArgumentException("Illegal cluster type '" + cfgValue + "'"); + }; + } + + public String toConfigValue() { + return switch (this) { + case ADMIN -> "admin"; + case CONTAINER -> "container"; + case CONTENT -> "content"; + case COMBINED -> "combined"; + }; + } + + public URI asCertificateSanUri() { return URI.create("vespa://cluster-type/%s".formatted(toConfigValue())); } + +} + 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 08afdd91542..201b550f6ae 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 @@ -14,6 +14,7 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.Optional; import static com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId.fromDottedString; @@ -38,16 +39,17 @@ public class EntityBindingsMapper { public static SignedIdentityDocument toSignedIdentityDocument(SignedIdentityDocumentEntity entity) { return new SignedIdentityDocument( - entity.signature, - entity.signingKeyVersion, - fromDottedString(entity.providerUniqueId), - new AthenzService(entity.providerService), - entity.documentVersion, - entity.configServerHostname, - entity.instanceHostname, - entity.createdAt, - entity.ipAddresses, - IdentityType.fromId(entity.identityType)); + entity.signature(), + entity.signingKeyVersion(), + fromDottedString(entity.providerUniqueId()), + new AthenzService(entity.providerService()), + entity.documentVersion(), + entity.configServerHostname(), + entity.instanceHostname(), + entity.createdAt(), + entity.ipAddresses(), + IdentityType.fromId(entity.identityType()), + ClusterType.from(entity.clusterType())); } public static SignedIdentityDocumentEntity toSignedIdentityDocumentEntity(SignedIdentityDocument model) { @@ -61,7 +63,8 @@ public class EntityBindingsMapper { model.instanceHostname(), model.createdAt(), model.ipAddresses(), - model.identityType().id()); + model.identityType().id(), + Optional.ofNullable(model.clusterType()).map(ClusterType::toConfigValue).orElse(null)); } public static SignedIdentityDocument readSignedIdentityDocumentFromFile(Path file) { 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 81c298efb74..e331fc1f6e8 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 @@ -4,7 +4,6 @@ package com.yahoo.vespa.athenz.identityprovider.api; import com.yahoo.vespa.athenz.api.AthenzService; import java.time.Instant; -import java.util.Objects; import java.util.Set; /** @@ -12,117 +11,10 @@ import java.util.Set; * * @author bjorncs */ -public class SignedIdentityDocument { - public static final int DEFAULT_DOCUMENT_VERSION = 1; +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) { + public static final int DEFAULT_DOCUMENT_VERSION = 2; - private final String signature; - private final int signingKeyVersion; - private final VespaUniqueInstanceId providerUniqueId; - private final AthenzService providerService; - private final int documentVersion; - private final String configServerHostname; - private final String instanceHostname; - private final Instant createdAt; - private final Set<String> ipAddresses; - private final IdentityType identityType; - - public SignedIdentityDocument(String signature, - int signingKeyVersion, - VespaUniqueInstanceId providerUniqueId, - AthenzService providerService, - int documentVersion, - String configServerHostname, - String instanceHostname, - Instant createdAt, - Set<String> ipAddresses, - IdentityType identityType) { - this.signature = signature; - this.signingKeyVersion = signingKeyVersion; - this.providerUniqueId = providerUniqueId; - this.providerService = providerService; - this.documentVersion = documentVersion; - this.configServerHostname = configServerHostname; - this.instanceHostname = instanceHostname; - this.createdAt = createdAt; - this.ipAddresses = ipAddresses; - this.identityType = identityType; - } - - public String signature() { - return signature; - } - - public int signingKeyVersion() { - return signingKeyVersion; - } - - public VespaUniqueInstanceId providerUniqueId() { - return providerUniqueId; - } - - public AthenzService providerService() { - return providerService; - } - - public int documentVersion() { - return documentVersion; - } - - public String configServerHostname() { - return configServerHostname; - } - - public String instanceHostname() { - return instanceHostname; - } - - public Instant createdAt() { - return createdAt; - } - - public Set<String> ipAddresses() { - return ipAddresses; - } - - public IdentityType identityType() { - return identityType; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SignedIdentityDocument that = (SignedIdentityDocument) o; - return signingKeyVersion == that.signingKeyVersion && - documentVersion == that.documentVersion && - Objects.equals(signature, that.signature) && - Objects.equals(providerUniqueId, that.providerUniqueId) && - Objects.equals(providerService, that.providerService) && - Objects.equals(configServerHostname, that.configServerHostname) && - Objects.equals(instanceHostname, that.instanceHostname) && - Objects.equals(createdAt, that.createdAt) && - Objects.equals(ipAddresses, that.ipAddresses) && - identityType == that.identityType; - } - - @Override - public int hashCode() { - return Objects.hash(signature, signingKeyVersion, providerUniqueId, providerService, documentVersion, configServerHostname, instanceHostname, createdAt, ipAddresses, identityType); - } - - @Override - public String toString() { - return "SignedIdentityDocument{" + - "signature='" + signature + '\'' + - ", signingKeyVersion=" + signingKeyVersion + - ", providerUniqueId=" + providerUniqueId + - ", providerService=" + providerService + - ", documentVersion=" + documentVersion + - ", configServerHostname='" + configServerHostname + '\'' + - ", instanceHostname='" + instanceHostname + '\'' + - ", createdAt=" + createdAt + - ", ipAddresses=" + ipAddresses + - ", identityType=" + identityType + - '}'; - } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java index 97181e44747..e74c929223e 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java @@ -1,41 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.identityprovider.api; -import java.util.Objects; - /** * Represents the unique instance id as used in Vespa's integration with Athenz Copper Argos * * @author bjorncs */ -public class VespaUniqueInstanceId { - - private final int clusterIndex; - private final String clusterId; - private final String instance; - private final String application; - private final String tenant; - private final String region; - private final String environment; - private final IdentityType type; - - public VespaUniqueInstanceId(int clusterIndex, - String clusterId, - String instance, - String application, - String tenant, - String region, - String environment, - IdentityType type) { - this.clusterIndex = clusterIndex; - this.clusterId = clusterId; - this.instance = instance; - this.application = application; - this.tenant = tenant; - this.region = region; - this.environment = environment; - this.type = type; - } +public record VespaUniqueInstanceId(int clusterIndex, String clusterId, String instance, String application, + String tenant, String region, String environment, IdentityType type) { public static VespaUniqueInstanceId fromDottedString(String instanceId) { @@ -52,68 +24,4 @@ public class VespaUniqueInstanceId { "%d.%s.%s.%s.%s.%s.%s.%s", clusterIndex, clusterId, instance, application, tenant, region, environment, type.id()); } - - public int clusterIndex() { - return clusterIndex; - } - - public String clusterId() { - return clusterId; - } - - public String instance() { - return instance; - } - - public String application() { - return application; - } - - public String tenant() { - return tenant; - } - - public String region() { - return region; - } - - public String environment() { - return environment; - } - - public IdentityType type() { return type; } - - @Override - public String toString() { - return "VespaUniqueInstanceId{" + - "clusterIndex=" + clusterIndex + - ", clusterId='" + clusterId + '\'' + - ", instance='" + instance + '\'' + - ", application='" + application + '\'' + - ", tenant='" + tenant + '\'' + - ", region='" + region + '\'' + - ", environment='" + environment + '\'' + - ", type=" + type + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - VespaUniqueInstanceId that = (VespaUniqueInstanceId) o; - return clusterIndex == that.clusterIndex && - Objects.equals(clusterId, that.clusterId) && - Objects.equals(instance, that.instance) && - Objects.equals(application, that.application) && - Objects.equals(tenant, that.tenant) && - Objects.equals(region, that.region) && - Objects.equals(environment, that.environment) && - type == that.type; - } - - @Override - public int hashCode() { - return Objects.hash(clusterIndex, clusterId, instance, application, tenant, region, environment, type); - } } 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 c9448a2d97a..2fb709615da 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 @@ -1,89 +1,25 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.identityprovider.api.bindings; -import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Instant; -import java.util.Objects; import java.util.Set; /** * @author bjorncs */ @JsonIgnoreProperties(ignoreUnknown = true) -public class SignedIdentityDocumentEntity { - - @JsonProperty("signature") public final String signature; - @JsonProperty("signing-key-version") public final int signingKeyVersion; - @JsonProperty("provider-unique-id") public final String providerUniqueId; // String representation - @JsonProperty("provider-service") public final String providerService; - @JsonProperty("document-version") public final int documentVersion; - @JsonProperty("configserver-hostname") public final String configServerHostname; - @JsonProperty("instance-hostname") public final String instanceHostname; - @JsonProperty("created-at") public final Instant createdAt; - @JsonProperty("ip-addresses") public final Set<String> ipAddresses; - @JsonProperty("identity-type") public final String identityType; - - @JsonCreator - public SignedIdentityDocumentEntity(@JsonProperty("signature") String signature, - @JsonProperty("signing-key-version") int signingKeyVersion, - @JsonProperty("provider-unique-id") String providerUniqueId, - @JsonProperty("provider-service") String providerService, - @JsonProperty("document-version") int documentVersion, - @JsonProperty("configserver-hostname") String configServerHostname, - @JsonProperty("instance-hostname") String instanceHostname, - @JsonProperty("created-at") Instant createdAt, - @JsonProperty("ip-addresses") Set<String> ipAddresses, - @JsonProperty("identity-type") String identityType) { - this.signature = signature; - this.signingKeyVersion = signingKeyVersion; - this.providerUniqueId = providerUniqueId; - this.providerService = providerService; - this.documentVersion = documentVersion; - this.configServerHostname = configServerHostname; - this.instanceHostname = instanceHostname; - this.createdAt = createdAt; - this.ipAddresses = ipAddresses; - this.identityType = identityType; - } - - @Override - public String toString() { - return "SignedIdentityDocumentEntity{" + - ", signature='" + signature + '\'' + - ", signingKeyVersion=" + signingKeyVersion + - ", providerUniqueId='" + providerUniqueId + '\'' + - ", providerService='" + providerService + '\'' + - ", documentVersion=" + documentVersion + - ", configServerHostname='" + configServerHostname + '\'' + - ", instanceHostname='" + instanceHostname + '\'' + - ", createdAt=" + createdAt + - ", ipAddresses=" + ipAddresses + - ", identityType=" + identityType + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SignedIdentityDocumentEntity that = (SignedIdentityDocumentEntity) o; - return signingKeyVersion == that.signingKeyVersion && - documentVersion == that.documentVersion && - Objects.equals(signature, that.signature) && - Objects.equals(providerUniqueId, that.providerUniqueId) && - Objects.equals(providerService, that.providerService) && - Objects.equals(configServerHostname, that.configServerHostname) && - Objects.equals(instanceHostname, that.instanceHostname) && - Objects.equals(createdAt, that.createdAt) && - Objects.equals(ipAddresses, that.ipAddresses) && - Objects.equals(identityType, that.identityType); - } - - @Override - public int hashCode() { - return Objects.hash(signature, signingKeyVersion, providerUniqueId, providerService, documentVersion, configServerHostname, instanceHostname, createdAt, ipAddresses, identityType); - } +public record SignedIdentityDocumentEntity(@JsonProperty("signature") String signature, + @JsonProperty("signing-key-version") int signingKeyVersion, + @JsonProperty("provider-unique-id") String providerUniqueId, + @JsonProperty("provider-service") String providerService, + @JsonProperty("document-version") int documentVersion, + @JsonProperty("configserver-hostname") String configServerHostname, + @JsonProperty("instance-hostname") String instanceHostname, + @JsonProperty("created-at") Instant createdAt, + @JsonProperty("ip-addresses") Set<String> ipAddresses, + @JsonProperty("identity-type") String identityType, + @JsonProperty("cluster-type") String clusterType) { } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java index 91b95cb6084..cc9d3b2be65 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java @@ -79,6 +79,7 @@ class AthenzCredentialsService { tenantIdentity, document.providerUniqueId(), document.ipAddresses(), + document.clusterType(), keyPair); try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withIdentityProvider(nodeIdentityProvider).build()) { @@ -100,6 +101,7 @@ class AthenzCredentialsService { tenantIdentity, document.providerUniqueId(), document.ipAddresses(), + document.clusterType(), newKeyPair); try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withSslContext(sslContext).build()) { diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java index 0caf3bdfd0b..b9f9f3862c2 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java @@ -4,18 +4,18 @@ package com.yahoo.vespa.athenz.identityprovider.client; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.yahoo.component.annotation.Inject; import com.yahoo.component.AbstractComponent; +import com.yahoo.component.annotation.Inject; import com.yahoo.container.core.identity.IdentityConfig; import com.yahoo.container.jdisc.athenz.AthenzIdentityProvider; import com.yahoo.container.jdisc.athenz.AthenzIdentityProviderException; import com.yahoo.jdisc.Metric; import com.yahoo.metrics.ContainerMetrics; import com.yahoo.security.KeyStoreBuilder; +import com.yahoo.security.MutableX509KeyManager; import com.yahoo.security.Pkcs10Csr; import com.yahoo.security.SslContextBuilder; import com.yahoo.security.X509CertificateWithKey; -import com.yahoo.security.MutableX509KeyManager; import com.yahoo.vespa.athenz.api.AthenzAccessToken; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzRole; @@ -48,7 +48,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; import static com.yahoo.security.KeyStoreType.PKCS12; @@ -299,7 +298,9 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen } private X509Certificate requestRoleCertificate(AthenzRole role) { - Pkcs10Csr csr = csrGenerator.generateRoleCsr(identity, role, credentials.getIdentityDocument().providerUniqueId(), credentials.getKeyPair()); + var doc = credentials.getIdentityDocument(); + Pkcs10Csr csr = csrGenerator.generateRoleCsr( + identity, role, doc.providerUniqueId(), doc.clusterType(), credentials.getKeyPair()); try (ZtsClient client = createZtsClient()) { X509Certificate roleCertificate = client.getRoleCertificate(role, csr); updateRoleKeyManager(role, roleCertificate); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/CsrGenerator.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/CsrGenerator.java index 21ce30fd244..353f0fdf067 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/CsrGenerator.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/CsrGenerator.java @@ -1,12 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.identityprovider.client; -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzRole; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.security.Pkcs10Csr; import com.yahoo.security.Pkcs10CsrBuilder; import com.yahoo.security.SubjectAlternativeName; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzRole; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; +import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import javax.security.auth.x500.X500Principal; import java.security.KeyPair; @@ -14,8 +15,9 @@ import java.util.Set; import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_RSA; import static com.yahoo.security.SubjectAlternativeName.Type.DNS; -import static com.yahoo.security.SubjectAlternativeName.Type.IP; import static com.yahoo.security.SubjectAlternativeName.Type.EMAIL; +import static com.yahoo.security.SubjectAlternativeName.Type.IP; +import static com.yahoo.security.SubjectAlternativeName.Type.URI; /** * Generates a {@link Pkcs10Csr} for an instance. @@ -35,6 +37,7 @@ public class CsrGenerator { public Pkcs10Csr generateInstanceCsr(AthenzIdentity instanceIdentity, VespaUniqueInstanceId instanceId, Set<String> ipAddresses, + ClusterType clusterType, KeyPair keyPair) { X500Principal subject = new X500Principal(String.format("OU=%s, CN=%s", providerService, instanceIdentity.getFullName())); // Add SAN dnsname <service>.<domain-with-dashes>.<provider-dnsname-suffix> @@ -48,6 +51,7 @@ public class CsrGenerator { instanceIdentity.getDomainName().replace(".", "-"), dnsSuffix)) .addSubjectAlternativeName(DNS, getIdentitySAN(instanceId)); + if (clusterType != null) pkcs10CsrBuilder.addSubjectAlternativeName(URI, clusterType.asCertificateSanUri().toString()); ipAddresses.forEach(ip -> pkcs10CsrBuilder.addSubjectAlternativeName(new SubjectAlternativeName(IP, ip))); return pkcs10CsrBuilder.build(); } @@ -55,12 +59,14 @@ public class CsrGenerator { public Pkcs10Csr generateRoleCsr(AthenzIdentity identity, AthenzRole role, VespaUniqueInstanceId instanceId, + ClusterType clusterType, KeyPair keyPair) { X500Principal principal = new X500Principal(String.format("OU=%s, cn=%s:role.%s", providerService, role.domain().getName(), role.roleName())); - return Pkcs10CsrBuilder.fromKeypair(principal, keyPair, SHA256_WITH_RSA) + var b = Pkcs10CsrBuilder.fromKeypair(principal, keyPair, SHA256_WITH_RSA) .addSubjectAlternativeName(DNS, getIdentitySAN(instanceId)) - .addSubjectAlternativeName(EMAIL, String.format("%s.%s@%s", identity.getDomainName(), identity.getName(), dnsSuffix)) - .build(); + .addSubjectAlternativeName(EMAIL, String.format("%s.%s@%s", identity.getDomainName(), identity.getName(), dnsSuffix)); + if (clusterType != null) b.addSubjectAlternativeName(URI, clusterType.asCertificateSanUri().toString()); + return b.build(); } private String getIdentitySAN(VespaUniqueInstanceId instanceId) { diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java index 1c1dcb655c0..bfc1b3aad46 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentSigner.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.athenz.identityprovider.client; import com.yahoo.security.SignatureUtils; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; @@ -34,11 +35,14 @@ public class IdentityDocumentSigner { Instant createdAt, Set<String> ipAddresses, IdentityType identityType, + ClusterType clusterType, PrivateKey privateKey) { try { Signature signer = SignatureUtils.createSigner(privateKey); signer.initSign(privateKey); - writeToSigner(signer, providerUniqueId, providerService, configServerHostname, instanceHostname, createdAt, ipAddresses, identityType); + writeToSigner( + signer, providerUniqueId, providerService, configServerHostname, instanceHostname, createdAt, + ipAddresses, identityType, clusterType); byte[] signature = signer.sign(); return Base64.getEncoder().encodeToString(signature); } catch (GeneralSecurityException e) { @@ -50,7 +54,9 @@ public class IdentityDocumentSigner { try { Signature signer = SignatureUtils.createVerifier(publicKey); signer.initVerify(publicKey); - writeToSigner(signer, doc.providerUniqueId(), doc.providerService(), doc.configServerHostname(), doc.instanceHostname(), doc.createdAt(), doc.ipAddresses(), doc.identityType()); + writeToSigner( + signer, doc.providerUniqueId(), doc.providerService(), doc.configServerHostname(), + doc.instanceHostname(), doc.createdAt(), doc.ipAddresses(), doc.identityType(), doc.clusterType()); return signer.verify(Base64.getDecoder().decode(doc.signature())); } catch (GeneralSecurityException e) { throw new RuntimeException(e); @@ -64,7 +70,8 @@ public class IdentityDocumentSigner { String instanceHostname, Instant createdAt, Set<String> ipAddresses, - IdentityType identityType) throws SignatureException { + IdentityType identityType, + ClusterType clusterType) throws SignatureException { signer.update(providerUniqueId.asDottedString().getBytes(UTF_8)); signer.update(providerService.getFullName().getBytes(UTF_8)); signer.update(configServerHostname.getBytes(UTF_8)); @@ -76,5 +83,6 @@ public class IdentityDocumentSigner { signer.update(ipAddress.getBytes(UTF_8)); } signer.update(identityType.id().getBytes(UTF_8)); + if (clusterType != null) signer.update(clusterType.toConfigValue().getBytes(UTF_8)); } } 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 5253fee0802..f53518d9a07 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 @@ -4,6 +4,7 @@ package com.yahoo.vespa.athenz.identityprovider.client; 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.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; @@ -24,24 +25,41 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class IdentityDocumentSignerTest { public static final int KEY_VERSION = 0; + private static final IdentityType identityType = TENANT; + private static final VespaUniqueInstanceId id = + new VespaUniqueInstanceId(1, "cluster-id", "instance", "application", "tenant", "region", "environment", identityType); + private static final AthenzService providerService = new AthenzService("vespa", "service"); + private static final KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); + private static final String configserverHostname = "configserverhostname"; + private static final String instanceHostname = "instancehostname"; + 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; + @Test void generates_and_validates_signature() { IdentityDocumentSigner signer = new IdentityDocumentSigner(); - IdentityType identityType = TENANT; - VespaUniqueInstanceId id = - new VespaUniqueInstanceId(1, "cluster-id", "instance", "application", "tenant", "region", "environment", identityType); - AthenzService providerService = new AthenzService("vespa", "service"); - KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - String configserverHostname = "configserverhostname"; - String instanceHostname = "instancehostname"; - Instant createdAt = Instant.EPOCH; - HashSet<String> ipAddresses = new HashSet<>(Arrays.asList("1.2.3.4", "::1")); String signature = - signer.generateSignature(id, providerService, configserverHostname, instanceHostname, createdAt, ipAddresses, identityType, keyPair.getPrivate()); + signer.generateSignature(id, providerService, configserverHostname, instanceHostname, createdAt, + ipAddresses, identityType, clusterType, keyPair.getPrivate()); + + SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( + signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname, + instanceHostname, createdAt, ipAddresses, identityType, clusterType); + + assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic())); + } + + @Test + void handles_missing_cluster_type() { + IdentityDocumentSigner signer = new IdentityDocumentSigner(); + String signature = + signer.generateSignature(id, providerService, configserverHostname, instanceHostname, createdAt, + ipAddresses, identityType, /*clusterType*/null, keyPair.getPrivate()); SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( - signature, KEY_VERSION, id, providerService, - DEFAULT_DOCUMENT_VERSION, configserverHostname, instanceHostname, createdAt, ipAddresses, identityType); + signature, KEY_VERSION, id, providerService, DEFAULT_DOCUMENT_VERSION, configserverHostname, + instanceHostname, createdAt, ipAddresses, identityType, /*clusterType*/null); assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic())); } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/InstanceCsrGeneratorTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/InstanceCsrGeneratorTest.java index 3e4f8541aaa..5124f11fd05 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/InstanceCsrGeneratorTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/InstanceCsrGeneratorTest.java @@ -3,15 +3,20 @@ package com.yahoo.vespa.athenz.identityprovider.client; import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; +import com.yahoo.security.Pkcs10Csr; +import com.yahoo.security.SubjectAlternativeName; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.security.Pkcs10Csr; import org.junit.jupiter.api.Test; import javax.security.auth.x500.X500Principal; import java.security.KeyPair; import java.util.Collections; +import java.util.Set; +import static com.yahoo.security.SubjectAlternativeName.Type.DNS; +import static com.yahoo.security.SubjectAlternativeName.Type.URI; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -24,14 +29,20 @@ public class InstanceCsrGeneratorTest { private static final String ATHENZ_SERVICE = "foo.bar"; @Test - void it_generates_csr_with_correct_subject() { + void generates_correct_subject_and_alternative_names() { CsrGenerator csrGenerator = new CsrGenerator(DNS_SUFFIX, PROVIDER_SERVICE); AthenzService service = new AthenzService(ATHENZ_SERVICE); VespaUniqueInstanceId vespaUniqueInstanceId = VespaUniqueInstanceId.fromDottedString("0.default.default.foo-app.vespa.us-north-1.prod.node"); KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - Pkcs10Csr csr = csrGenerator.generateInstanceCsr(service, vespaUniqueInstanceId, Collections.emptySet(), keyPair); + Pkcs10Csr csr = csrGenerator.generateInstanceCsr(service, vespaUniqueInstanceId, Collections.emptySet(), ClusterType.CONTAINER, keyPair); assertEquals(new X500Principal(String.format("OU=%s, CN=%s", PROVIDER_SERVICE, ATHENZ_SERVICE)), csr.getSubject()); + var actualSans = Set.copyOf(csr.getSubjectAlternativeNames()); + var expectedSans = Set.of( + new SubjectAlternativeName(DNS, "bar.foo.prod-us-north-1.vespa.yahoo.cloud"), + new SubjectAlternativeName(DNS, "0.default.default.foo-app.vespa.us-north-1.prod.node.instanceid.athenz.prod-us-north-1.vespa.yahoo.cloud"), + new SubjectAlternativeName(URI, "vespa://cluster-type/container")); + assertEquals(expectedSans, actualSans); } } |