diff options
19 files changed, 34 insertions, 1002 deletions
diff --git a/client/go/go.mod b/client/go/go.mod index 1160a82afff..66c9f1087d1 100644 --- a/client/go/go.mod +++ b/client/go/go.mod @@ -8,7 +8,7 @@ require ( github.com/fatih/color v1.17.0 // This is the most recent version compatible with Go 1.20. Upgrade when we upgrade our Go version github.com/go-json-experiment/json v0.0.0-20230324203220-04923b7a9528 - github.com/klauspost/compress v1.17.8 + github.com/klauspost/compress v1.17.9 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c diff --git a/client/go/go.sum b/client/go/go.sum index 95180dfe739..c225854786c 100644 --- a/client/go/go.sum +++ b/client/go/go.sum @@ -30,6 +30,8 @@ github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLA github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/dependency-versions/pom.xml b/dependency-versions/pom.xml index 04c67018f06..e0437d7a09f 100644 --- a/dependency-versions/pom.xml +++ b/dependency-versions/pom.xml @@ -68,7 +68,7 @@ <assertj.vespa.version>3.26.0</assertj.vespa.version> <!-- Athenz dependencies. Make sure these dependencies match those in Vespa's internal repositories --> - <aws-sdk.vespa.version>1.12.740</aws-sdk.vespa.version> + <aws-sdk.vespa.version>1.12.741</aws-sdk.vespa.version> <athenz.vespa.version>1.11.59</athenz.vespa.version> <!-- Athenz END --> diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostDeprovisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostDeprovisioner.java index 4026a294111..8a532131d90 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostDeprovisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostDeprovisioner.java @@ -6,6 +6,7 @@ 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.provisioning.HostProvisioner; +import com.yahoo.vespa.hosted.provision.provisioning.ThrottleProvisioningException; import java.time.Duration; import java.util.logging.Level; @@ -48,6 +49,9 @@ public class HostDeprovisioner extends NodeRepositoryMaintainer { // if we want to support aborting deprovision if operator manually intervenes if (hostProvisioner.deprovision(host)) nodeRepository().nodes().removeRecursively(host, true); + } catch (ThrottleProvisioningException e) { + log.log(Level.INFO, "Failed to deprovision " + host.hostname() + ", will retry in " + interval() + ": " + e.getMessage()); + break; } catch (RuntimeException e) { failures++; log.log(Level.WARNING, "Failed to deprovision " + host.hostname() + ", will retry in " + interval(), e); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisioner.java index 2e3c6d1755a..08243290a28 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisioner.java @@ -15,6 +15,7 @@ import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.provisioning.FatalProvisioningException; import com.yahoo.vespa.hosted.provision.provisioning.HostIpConfig; import com.yahoo.vespa.hosted.provision.provisioning.HostProvisioner; +import com.yahoo.vespa.hosted.provision.provisioning.ThrottleProvisioningException; import com.yahoo.yolean.Exceptions; import javax.naming.NamingException; @@ -60,6 +61,9 @@ public class HostResumeProvisioner extends NodeRepositoryMaintainer { } catch (IllegalArgumentException | IllegalStateException e) { log.log(Level.INFO, "Could not provision " + host.hostname() + ", will retry in " + interval() + ": " + Exceptions.toMessageString(e)); + } catch (ThrottleProvisioningException e) { + log.log(Level.INFO, "Failed to provision " + host.hostname() + ", will retry in " + interval() + ": " + e.getMessage()); + break; } catch (FatalProvisioningException e) { // FatalProvisioningException is thrown if node is not found in the cloud, allow for // some time for the state to propagate diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ThrottleProvisioningException.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ThrottleProvisioningException.java new file mode 100644 index 00000000000..c41e5c94fee --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ThrottleProvisioningException.java @@ -0,0 +1,16 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.provisioning; + +import com.yahoo.config.provision.TransientException; + +/** + * Thrown by {@link HostProvisioner} to indicate that (de)provisioning of a host has + * failed due request being throttled by the cloud provider. + * + * @author freva + */ +public class ThrottleProvisioningException extends TransientException { + public ThrottleProvisioningException(String message) { + super(message); + } +} 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 4bbdd24db32..ac620d2f6d4 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 @@ -4,11 +4,9 @@ package com.yahoo.vespa.athenz.identityprovider.api; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.bindings.DefaultSignedIdentityDocumentEntity; import com.yahoo.vespa.athenz.identityprovider.api.bindings.IdentityDocumentEntity; -import com.yahoo.vespa.athenz.identityprovider.api.bindings.LegacySignedIdentityDocumentEntity; import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; import com.yahoo.vespa.athenz.utils.AthenzIdentities; import com.yahoo.yolean.Exceptions; @@ -54,25 +52,7 @@ public class EntityBindingsMapper { } public static SignedIdentityDocument toSignedIdentityDocument(SignedIdentityDocumentEntity entity) { - if (entity instanceof LegacySignedIdentityDocumentEntity docEntity) { - IdentityDocument doc = new IdentityDocument( - fromDottedString(docEntity.providerUniqueId()), - new AthenzService(docEntity.providerService()), - docEntity.configServerHostname(), - docEntity.instanceHostname(), - docEntity.createdAt(), - docEntity.ipAddresses(), - IdentityType.fromId(docEntity.identityType()), - Optional.ofNullable(docEntity.clusterType()).map(ClusterType::from).orElse(null), - docEntity.ztsUrl(), - Optional.ofNullable(docEntity.serviceIdentity()).map(AthenzIdentities::from).orElse(null), - docEntity.unknownAttributes()); - return new LegacySignedIdentityDocument( - docEntity.signature(), - docEntity.signingKeyVersion(), - entity.documentVersion(), - doc); - } else if (entity instanceof DefaultSignedIdentityDocumentEntity docEntity) { + if (entity instanceof DefaultSignedIdentityDocumentEntity docEntity) { return new DefaultSignedIdentityDocument(docEntity.signature(), docEntity.signingKeyVersion(), docEntity.documentVersion(), @@ -83,24 +63,7 @@ public class EntityBindingsMapper { } public static SignedIdentityDocumentEntity toSignedIdentityDocumentEntity(SignedIdentityDocument model) { - if (model instanceof LegacySignedIdentityDocument legacyModel) { - IdentityDocument idDoc = legacyModel.identityDocument(); - return new LegacySignedIdentityDocumentEntity( - legacyModel.signature(), - legacyModel.signingKeyVersion(), - idDoc.providerUniqueId().asDottedString(), - idDoc.providerService().getFullName(), - legacyModel.documentVersion(), - idDoc.configServerHostname(), - idDoc.instanceHostname(), - idDoc.createdAt(), - idDoc.ipAddresses(), - idDoc.identityType().id(), - Optional.ofNullable(idDoc.clusterType()).map(ClusterType::toConfigValue).orElse(null), - idDoc.ztsUrl(), - Optional.ofNullable(idDoc.serviceIdentity()).map(AthenzIdentity::getFullName).orElse(null), - idDoc.unknownAttributes()); - } else if (model instanceof DefaultSignedIdentityDocument defaultModel){ + if (model instanceof DefaultSignedIdentityDocument defaultModel){ return new DefaultSignedIdentityDocumentEntity(defaultModel.signature(), defaultModel.signingKeyVersion(), defaultModel.documentVersion(), diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/LegacySignedIdentityDocument.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/LegacySignedIdentityDocument.java deleted file mode 100644 index dfab93b2a28..00000000000 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/LegacySignedIdentityDocument.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.athenz.identityprovider.api; - -public record LegacySignedIdentityDocument(String signature, int signingKeyVersion, int documentVersion, - IdentityDocument identityDocument) implements SignedIdentityDocument { -} 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 8ab07d97e74..39629d878db 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 @@ -8,10 +8,9 @@ package com.yahoo.vespa.athenz.identityprovider.api; */ public interface SignedIdentityDocument { - int LEGACY_DEFAULT_DOCUMENT_VERSION = 3; int DEFAULT_DOCUMENT_VERSION = 4; - default boolean outdated() { return documentVersion() < LEGACY_DEFAULT_DOCUMENT_VERSION; } + default boolean outdated() { return documentVersion() < DEFAULT_DOCUMENT_VERSION; } IdentityDocument identityDocument(); String signature(); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/LegacySignedIdentityDocumentEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/LegacySignedIdentityDocumentEntity.java deleted file mode 100644 index 647ca474420..00000000000 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/LegacySignedIdentityDocumentEntity.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright Vespa.ai. 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.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.net.URI; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * @author bjorncs - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public record LegacySignedIdentityDocumentEntity ( - String signature, int signingKeyVersion, String providerUniqueId, String providerService, int documentVersion, - String configServerHostname, String instanceHostname, Instant createdAt, Set<String> ipAddresses, - String identityType, String clusterType, URI ztsUrl, String serviceIdentity, Map<String, Object> unknownAttributes) implements SignedIdentityDocumentEntity { - - @JsonCreator - public LegacySignedIdentityDocumentEntity(@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, - @JsonProperty("zts-url") String ztsUrl, - @JsonProperty("service-identity") String serviceIdentity) { - this(signature, signingKeyVersion, providerUniqueId, providerService, documentVersion, configServerHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType, URI.create(ztsUrl), serviceIdentity, new HashMap<>()); - } - - @JsonProperty("signature") @Override public String signature() { return signature; } - @JsonProperty("signing-key-version") @Override public int signingKeyVersion() { return signingKeyVersion; } - @JsonProperty("provider-unique-id") @Override public String providerUniqueId() { return providerUniqueId; } - @JsonProperty("provider-service") @Override public String providerService() { return providerService; } - @JsonProperty("document-version") @Override public int documentVersion() { return documentVersion; } - @JsonProperty("configserver-hostname") @Override public String configServerHostname() { return configServerHostname; } - @JsonProperty("instance-hostname") @Override public String instanceHostname() { return instanceHostname; } - @JsonProperty("created-at") @Override public Instant createdAt() { return createdAt; } - @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 URI ztsUrl() { return ztsUrl; } - @JsonProperty("service-identity") @Override public String serviceIdentity() { return serviceIdentity; } - @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/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 c5c39fb6590..d909849e9ce 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 @@ -9,7 +9,6 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.type.TypeFactory; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import java.io.IOException; import java.util.Objects; @@ -55,10 +54,7 @@ class SignedIdentityDocumentEntityTypeResolver implements TypeIdResolver { @Override public JavaType typeFromId(DatabindContext databindContext, String s) throws IOException { try { - int version = Integer.parseInt(s); - Class<? extends SignedIdentityDocumentEntity> cls = version <= SignedIdentityDocument.LEGACY_DEFAULT_DOCUMENT_VERSION - ? LegacySignedIdentityDocumentEntity.class - : DefaultSignedIdentityDocumentEntity.class; + Class<? extends SignedIdentityDocumentEntity> cls = DefaultSignedIdentityDocumentEntity.class; return TypeFactory.defaultInstance().constructSpecializedType(javaType,cls); } catch (NumberFormatException e) { throw new IllegalArgumentException("Unable to deserialize document with version: \"%s\"".formatted(s)); 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 deleted file mode 100644 index 63c966004e5..00000000000 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright Vespa.ai. 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.container.core.identity.IdentityConfig; -import com.yahoo.security.KeyAlgorithm; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.Pkcs10Csr; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient; -import com.yahoo.vespa.athenz.client.zts.InstanceIdentity; -import com.yahoo.vespa.athenz.client.zts.ZtsClient; -import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; -import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; -import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocumentClient; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; -import com.yahoo.vespa.athenz.utils.SiaUtils; -import com.yahoo.vespa.defaults.Defaults; - -import javax.net.ssl.SSLContext; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.time.Clock; -import java.time.Duration; -import java.util.Optional; - -import static java.util.Collections.singleton; - -/** - * A service that provides method for initially registering the instance and refreshing it. - * - * @author bjorncs - */ -class AthenzCredentialsService { - private static final Duration EXPIRATION_MARGIN = Duration.ofDays(2); - private static final Path VESPA_SIA_DIRECTORY = Paths.get(Defaults.getDefaults().underVespaHome("var/vespa/sia")); - private static final Path IDENTITY_DOCUMENT_FILE = VESPA_SIA_DIRECTORY.resolve("vespa-tenant-identity-document.json"); - - private final AthenzService tenantIdentity; - private final URI configserverEndpoint; - private final URI ztsEndpoint; - private final AthenzService configserverIdentity; - private final ServiceIdentityProvider nodeIdentityProvider; - private final String hostname; - private final CsrGenerator csrGenerator; - private final Clock clock; - - AthenzCredentialsService(IdentityConfig identityConfig, - ServiceIdentityProvider nodeIdentityProvider, - String hostname, - Clock clock) { - this.tenantIdentity = new AthenzService(identityConfig.domain(), identityConfig.service()); - this.configserverEndpoint = URI.create("https://" + identityConfig.loadBalancerAddress() + ":4443"); - this.ztsEndpoint = URI.create(identityConfig.ztsUrl()); - this.configserverIdentity = new AthenzService(identityConfig.configserverIdentityName()); - this.nodeIdentityProvider = nodeIdentityProvider; - this.hostname = hostname; - this.csrGenerator = new CsrGenerator(identityConfig.athenzDnsSuffix(), identityConfig.configserverIdentityName()); - this.clock = clock; - } - - Path certificatePath() { return SiaUtils.getCertificateFile(VESPA_SIA_DIRECTORY, tenantIdentity); } - Path privateKeyPath() { return SiaUtils.getPrivateKeyFile(VESPA_SIA_DIRECTORY, tenantIdentity); } - - AthenzCredentials registerInstance() { - Optional<AthenzCredentials> athenzCredentialsFromDisk = tryReadCredentialsFromDisk(); - if (athenzCredentialsFromDisk.isPresent()) { - return athenzCredentialsFromDisk.get(); - } - KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - IdentityDocumentClient identityDocumentClient = createIdentityDocumentClient(); - // Use legacy version for now. - SignedIdentityDocument signedDocument = identityDocumentClient.getTenantIdentityDocument(hostname, SignedIdentityDocument.LEGACY_DEFAULT_DOCUMENT_VERSION).orElseThrow(); - IdentityDocument document = signedDocument.identityDocument(); - Pkcs10Csr csr = csrGenerator.generateInstanceCsr( - tenantIdentity, - document.providerUniqueId(), - document.ipAddresses(), - document.clusterType(), - keyPair); - - try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withIdentityProvider(nodeIdentityProvider).build()) { - InstanceIdentity instanceIdentity = - ztsClient.registerInstance( - configserverIdentity, - tenantIdentity, - EntityBindingsMapper.toAttestationData(signedDocument), - csr); - X509Certificate certificate = instanceIdentity.certificate(); - writeCredentialsToDisk(keyPair.getPrivate(), certificate, signedDocument); - return new AthenzCredentials(certificate, keyPair, signedDocument); - } - } - - AthenzCredentials updateCredentials(SignedIdentityDocument signedDocument, SSLContext sslContext) { - KeyPair newKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - IdentityDocument document = signedDocument.identityDocument(); - Pkcs10Csr csr = csrGenerator.generateInstanceCsr( - tenantIdentity, - document.providerUniqueId(), - document.ipAddresses(), - document.clusterType(), - newKeyPair); - - try (ZtsClient ztsClient = new DefaultZtsClient.Builder(ztsEndpoint).withSslContext(sslContext).build()) { - InstanceIdentity instanceIdentity = - ztsClient.refreshInstance( - configserverIdentity, - tenantIdentity, - document.providerUniqueId().asDottedString(), - csr); - X509Certificate certificate = instanceIdentity.certificate(); - writeCredentialsToDisk(newKeyPair.getPrivate(), certificate, signedDocument); - return new AthenzCredentials(certificate, newKeyPair, signedDocument); - } - } - - private Optional<AthenzCredentials> tryReadCredentialsFromDisk() { - Optional<PrivateKey> privateKey = SiaUtils.readPrivateKeyFile(VESPA_SIA_DIRECTORY, tenantIdentity); - if (privateKey.isEmpty()) return Optional.empty(); - Optional<X509Certificate> certificate = SiaUtils.readCertificateFile(VESPA_SIA_DIRECTORY, tenantIdentity); - if (certificate.isEmpty()) return Optional.empty(); - if (isExpired(certificate.get())) { - return Optional.empty(); - } - if (Files.notExists(IDENTITY_DOCUMENT_FILE)) return Optional.empty(); - SignedIdentityDocument signedIdentityDocument = EntityBindingsMapper.readSignedIdentityDocumentFromFile(IDENTITY_DOCUMENT_FILE); - KeyPair keyPair = new KeyPair(KeyUtils.extractPublicKey(privateKey.get()), privateKey.get()); - return Optional.of(new AthenzCredentials(certificate.get(), keyPair, signedIdentityDocument)); - } - - private boolean isExpired(X509Certificate certificate) { - return clock.instant().isAfter(certificate.getNotAfter().toInstant().minus(EXPIRATION_MARGIN)); - } - - private void writeCredentialsToDisk(PrivateKey privateKey, - X509Certificate certificate, - SignedIdentityDocument identityDocument) { - SiaUtils.writePrivateKeyFile(VESPA_SIA_DIRECTORY, tenantIdentity, privateKey); - SiaUtils.writeCertificateFile(VESPA_SIA_DIRECTORY, tenantIdentity, certificate); - EntityBindingsMapper.writeSignedIdentityDocumentToFile(IDENTITY_DOCUMENT_FILE, identityDocument); - } - - private DefaultIdentityDocumentClient createIdentityDocumentClient() { - return new DefaultIdentityDocumentClient( - configserverEndpoint, - nodeIdentityProvider, - new AthenzIdentityVerifier(singleton(configserverIdentity))); - } -} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java index 30ae4fff59d..dec919cada0 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java @@ -7,24 +7,17 @@ import com.yahoo.container.jdisc.athenz.AthenzIdentityProvider; import com.yahoo.jdisc.Metric; import javax.inject.Inject; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; /** * @author olaa */ public class AthenzIdentityProviderProvider implements Provider<AthenzIdentityProvider> { - private final Path NODE_ADMIN_MANAGED_IDENTITY_DOCUMENT = Paths.get("/var/lib/sia/vespa-tenant-identity-document.json"); private final AthenzIdentityProvider athenzIdentityProvider; @Inject public AthenzIdentityProviderProvider(IdentityConfig config, Metric metric) { - if (Files.exists(NODE_ADMIN_MANAGED_IDENTITY_DOCUMENT)) - athenzIdentityProvider = new AthenzIdentityProviderImpl(config, metric); - else - athenzIdentityProvider = new LegacyAthenzIdentityProviderImpl(config, metric); + athenzIdentityProvider = new AthenzIdentityProviderImpl(config, metric); } @Override 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 fd2cefbc93e..43f32a3bae7 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,11 +3,9 @@ package com.yahoo.vespa.athenz.identityprovider.client; import com.yahoo.security.SignatureUtils; import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.DefaultSignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; -import com.yahoo.vespa.athenz.identityprovider.api.LegacySignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; @@ -22,7 +20,6 @@ import java.util.Base64; import java.util.Set; import java.util.TreeSet; -import static com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument.LEGACY_DEFAULT_DOCUMENT_VERSION; import static java.nio.charset.StandardCharsets.UTF_8; /** @@ -44,39 +41,8 @@ public class IdentityDocumentSigner { } } - public String generateLegacySignature(IdentityDocument doc, PrivateKey privateKey) { - return generateSignature(doc.providerUniqueId(), doc.providerService(), doc.configServerHostname(), - doc.instanceHostname(), doc.createdAt(), doc.ipAddresses(), doc.identityType(), privateKey, doc.serviceIdentity()); - } - - // Cluster type is ignored due to old Vespa versions not forwarding unknown fields in signed identity document - private String generateSignature(VespaUniqueInstanceId providerUniqueId, - AthenzIdentity providerService, - String configServerHostname, - String instanceHostname, - Instant createdAt, - Set<String> ipAddresses, - IdentityType identityType, - PrivateKey privateKey, - AthenzIdentity serviceIdentity) { - try { - Signature signer = SignatureUtils.createSigner(privateKey); - signer.initSign(privateKey); - writeToSigner( - signer, providerUniqueId, providerService, configServerHostname, instanceHostname, createdAt, - ipAddresses, identityType); - writeToSigner(signer, serviceIdentity); - byte[] signature = signer.sign(); - return Base64.getEncoder().encodeToString(signature); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - } - public boolean hasValidSignature(SignedIdentityDocument doc, PublicKey publicKey) { - if (doc instanceof LegacySignedIdentityDocument signedDoc) { - return validateLegacySignature(signedDoc, publicKey); - } else if (doc instanceof DefaultSignedIdentityDocument signedDoc) { + if (doc instanceof DefaultSignedIdentityDocument signedDoc) { try { Signature signer = SignatureUtils.createVerifier(publicKey); signer.initVerify(publicKey); @@ -89,46 +55,4 @@ public class IdentityDocumentSigner { throw new IllegalArgumentException("Unknown identity document type: " + doc.getClass().getName()); } } - - private boolean validateLegacySignature(SignedIdentityDocument doc, PublicKey publicKey) { - try { - IdentityDocument iddoc = doc.identityDocument(); - Signature signer = SignatureUtils.createVerifier(publicKey); - signer.initVerify(publicKey); - writeToSigner( - signer, iddoc.providerUniqueId(), iddoc.providerService(), iddoc.configServerHostname(), - iddoc.instanceHostname(), iddoc.createdAt(), iddoc.ipAddresses(), iddoc.identityType()); - if (doc.documentVersion() >= LEGACY_DEFAULT_DOCUMENT_VERSION) { - writeToSigner(signer, iddoc.serviceIdentity()); - } - return signer.verify(Base64.getDecoder().decode(doc.signature())); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - } - - private static void writeToSigner(Signature signer, - VespaUniqueInstanceId providerUniqueId, - AthenzIdentity providerService, - String configServerHostname, - String instanceHostname, - Instant createdAt, - Set<String> ipAddresses, - IdentityType identityType) throws SignatureException { - signer.update(providerUniqueId.asDottedString().getBytes(UTF_8)); - signer.update(providerService.getFullName().getBytes(UTF_8)); - signer.update(configServerHostname.getBytes(UTF_8)); - signer.update(instanceHostname.getBytes(UTF_8)); - ByteBuffer timestampAsBuffer = ByteBuffer.allocate(Long.BYTES); - timestampAsBuffer.putLong(createdAt.toEpochMilli()); - signer.update(timestampAsBuffer.array()); - for (String ipAddress : new TreeSet<>(ipAddresses)) { - signer.update(ipAddress.getBytes(UTF_8)); - } - signer.update(identityType.id().getBytes(UTF_8)); - } - - private static void writeToSigner(Signature signer, AthenzIdentity serviceIdentity) throws SignatureException{ - signer.update(serviceIdentity.getFullName().getBytes(UTF_8)); - } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImpl.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImpl.java deleted file mode 100644 index c00149e0e4b..00000000000 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImpl.java +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.athenz.identityprovider.client; - -import ai.vespa.metrics.ContainerMetrics; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -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.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.vespa.athenz.api.AthenzAccessToken; -import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.athenz.api.AthenzRole; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.api.ZToken; -import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient; -import com.yahoo.vespa.athenz.client.zts.ZtsClient; -import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; -import com.yahoo.vespa.athenz.identity.SiaIdentityProvider; -import com.yahoo.vespa.athenz.utils.SiaUtils; -import com.yahoo.vespa.defaults.Defaults; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.X509ExtendedKeyManager; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.yahoo.security.KeyStoreType.PKCS12; - -/** - * A {@link AthenzIdentityProvider} / {@link ServiceIdentityProvider} component that provides the tenant identity. - * - * @author mortent - * @author bjorncs - */ -// This class should probably not implement ServiceIdentityProvider, -// as that interface is intended for providing the node's identity, not the tenant's application identity. -public final class LegacyAthenzIdentityProviderImpl extends AbstractComponent implements AthenzIdentityProvider, ServiceIdentityProvider { - - private static final Logger log = Logger.getLogger(LegacyAthenzIdentityProviderImpl.class.getName()); - - // TODO Make some of these values configurable through config. Match requested expiration of register/update requests. - // TODO These should match the requested expiration - static final Duration UPDATE_PERIOD = Duration.ofDays(1); - static final Duration AWAIT_TERMINTATION_TIMEOUT = Duration.ofSeconds(90); - private final static Duration ROLE_SSL_CONTEXT_EXPIRY = Duration.ofHours(2); - // TODO CMS expects 10min or less token ttl. Use 10min default until we have configurable expiry - private final static Duration ROLE_TOKEN_EXPIRY = Duration.ofMinutes(10); - - // TODO Make path to trust store paths config - private static final Path CLIENT_TRUST_STORE = Paths.get("/opt/yahoo/share/ssl/certs/yahoo_certificate_bundle.pem"); - private static final Path ATHENZ_TRUST_STORE = Paths.get("/opt/yahoo/share/ssl/certs/athenz_certificate_bundle.pem"); - - public static final String CERTIFICATE_EXPIRY_METRIC_NAME = ContainerMetrics.ATHENZ_TENANT_CERT_EXPIRY_SECONDS.baseName(); - - private volatile AthenzCredentials credentials; - private final Metric metric; - private final Path trustStore; - private final AthenzCredentialsService athenzCredentialsService; - private final ScheduledExecutorService scheduler; - private final Clock clock; - private final AthenzService identity; - private final URI ztsEndpoint; - - private final MutableX509KeyManager identityKeyManager = new MutableX509KeyManager(); - private final SSLContext identitySslContext; - private final LoadingCache<AthenzRole, X509Certificate> roleSslCertCache; - private final Map<AthenzRole, MutableX509KeyManager> roleKeyManagerCache; - private final LoadingCache<AthenzRole, ZToken> roleSpecificRoleTokenCache; - private final LoadingCache<AthenzDomain, ZToken> domainSpecificRoleTokenCache; - private final LoadingCache<AthenzDomain, AthenzAccessToken> domainSpecificAccessTokenCache; - private final LoadingCache<List<AthenzRole>, AthenzAccessToken> roleSpecificAccessTokenCache; - private final CsrGenerator csrGenerator; - - @Inject - public LegacyAthenzIdentityProviderImpl(IdentityConfig config, Metric metric) { - this(config, - metric, - CLIENT_TRUST_STORE, - new AthenzCredentialsService(config, - createNodeIdentityProvider(config), - Defaults.getDefaults().vespaHostname(), - Clock.systemUTC()), - new ScheduledThreadPoolExecutor(1), - Clock.systemUTC()); - } - - // Test only - LegacyAthenzIdentityProviderImpl(IdentityConfig config, - Metric metric, - Path trustStore, - AthenzCredentialsService athenzCredentialsService, - ScheduledExecutorService scheduler, - Clock clock) { - this.metric = metric; - this.trustStore = trustStore; - this.athenzCredentialsService = athenzCredentialsService; - this.scheduler = scheduler; - this.clock = clock; - this.identity = new AthenzService(config.domain(), config.service()); - this.ztsEndpoint = URI.create(config.ztsUrl()); - roleSslCertCache = crateAutoReloadableCache(ROLE_SSL_CONTEXT_EXPIRY, this::requestRoleCertificate, this.scheduler); - roleKeyManagerCache = new HashMap<>(); - roleSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); - domainSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); - domainSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); - roleSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); - this.csrGenerator = new CsrGenerator(config.athenzDnsSuffix(), config.configserverIdentityName()); - this.identitySslContext = createIdentitySslContext(identityKeyManager, trustStore); - registerInstance(); - } - - private static <KEY, VALUE> LoadingCache<KEY, VALUE> createCache(Duration expiry, Function<KEY, VALUE> cacheLoader) { - return CacheBuilder.newBuilder() - .refreshAfterWrite(expiry.dividedBy(2).toMinutes(), TimeUnit.MINUTES) - .expireAfterWrite(expiry.toMinutes(), TimeUnit.MINUTES) - .build(new CacheLoader<KEY, VALUE>() { - @Override - public VALUE load(KEY key) { - return cacheLoader.apply(key); - } - }); - } - - private static <KEY, VALUE> LoadingCache<KEY, VALUE> crateAutoReloadableCache(Duration expiry, Function<KEY, VALUE> cacheLoader, ScheduledExecutorService scheduler) { - LoadingCache<KEY, VALUE> cache = createCache(expiry, cacheLoader); - - // The cache above will reload it's contents if and only if a request for the key is made. Scheduling - // a cache reloader to reload all keys in this cache. - scheduler.scheduleAtFixedRate(() -> { cache.asMap().keySet().forEach(cache::getUnchecked);}, - expiry.dividedBy(4).toMinutes(), - expiry.dividedBy(4).toMinutes(), - TimeUnit.MINUTES); - return cache; - } - - private static SSLContext createIdentitySslContext(X509ExtendedKeyManager keyManager, Path trustStore) { - return new SslContextBuilder() - .withKeyManager(keyManager) - .withTrustStore(trustStore) - .build(); - } - - private void registerInstance() { - try { - updateIdentityCredentials(this.athenzCredentialsService.registerInstance()); - this.scheduler.scheduleAtFixedRate(this::refreshCertificate, UPDATE_PERIOD.toMinutes(), UPDATE_PERIOD.toMinutes(), TimeUnit.MINUTES); - this.scheduler.scheduleAtFixedRate(this::reportMetrics, 0, 5, TimeUnit.MINUTES); - } catch (Throwable t) { - throw new AthenzIdentityProviderException("Could not retrieve Athenz credentials", t); - } - } - - @Override - public AthenzService identity() { - return identity; - } - - @Override - public String domain() { - return identity.getDomain().getName(); - } - - @Override - public String service() { - return identity.getName(); - } - - @Override - public SSLContext getIdentitySslContext() { - return identitySslContext; - } - - @Override - public X509CertificateWithKey getIdentityCertificateWithKey() { - AthenzCredentials copy = this.credentials; - return new X509CertificateWithKey(copy.getCertificate(), copy.getKeyPair().getPrivate()); - } - - @Override public Path certificatePath() { return athenzCredentialsService.certificatePath(); } - - @Override public Path privateKeyPath() { return athenzCredentialsService.privateKeyPath(); } - - @Override - public SSLContext getRoleSslContext(String domain, String role) { - try { - AthenzRole athenzRole = new AthenzRole(new AthenzDomain(domain), role); - // Make sure to request a certificate which triggers creating a new key manager for this role - X509Certificate x509Certificate = getRoleCertificate(athenzRole); - MutableX509KeyManager keyManager = roleKeyManagerCache.get(athenzRole); - return new SslContextBuilder() - .withKeyManager(keyManager) - .withTrustStore(trustStore) - .build(); - } catch (Exception e) { - throw new AthenzIdentityProviderException("Could not retrieve role certificate: " + e.getMessage(), e); - } - } - - @Override - public String getRoleToken(String domain) { - try { - return domainSpecificRoleTokenCache.get(new AthenzDomain(domain)).getRawToken(); - } catch (Exception e) { - throw new AthenzIdentityProviderException("Could not retrieve role token: " + e.getMessage(), e); - } - } - - @Override - public String getRoleToken(String domain, String role) { - try { - return roleSpecificRoleTokenCache.get(new AthenzRole(domain, role)).getRawToken(); - } catch (Exception e) { - throw new AthenzIdentityProviderException("Could not retrieve role token: " + e.getMessage(), e); - } - } - - @Override - public String getAccessToken(String domain) { - try { - return domainSpecificAccessTokenCache.get(new AthenzDomain(domain)).value(); - } catch (Exception e) { - throw new AthenzIdentityProviderException("Could not retrieve access token: " + e.getMessage(), e); - } - } - - @Override - public String getAccessToken(String domain, List<String> roles) { - try { - List<AthenzRole> roleList = roles.stream() - .map(roleName -> new AthenzRole(domain, roleName)) - .toList(); - return roleSpecificAccessTokenCache.get(roleList).value(); - } catch (Exception e) { - throw new AthenzIdentityProviderException("Could not retrieve access token: " + e.getMessage(), e); - } - } - - @Override - public String getAccessToken(String domain, List<String> roles, List<String> proxyPrincipal) { - throw new UnsupportedOperationException("Not implemented in legacy client"); - } - - @Override - public PrivateKey getPrivateKey() { - return credentials.getKeyPair().getPrivate(); - } - - @Override - public Path trustStorePath() { - return trustStore; - } - - @Override - public List<X509Certificate> getIdentityCertificate() { - return List.of(credentials.getCertificate()); - } - - @Override - public X509Certificate getRoleCertificate(String domain, String role) { - return getRoleCertificate(new AthenzRole(new AthenzDomain(domain), role)); - } - - private X509Certificate getRoleCertificate(AthenzRole athenzRole) { - try { - return roleSslCertCache.get(athenzRole); - } catch (Exception e) { - throw new AthenzIdentityProviderException("Could not retrieve role certificate: " + e.getMessage(), e); - } - } - - private void updateIdentityCredentials(AthenzCredentials credentials) { - this.credentials = credentials; - this.identityKeyManager.updateKeystore( - KeyStoreBuilder.withType(PKCS12) - .withKeyEntry("default", credentials.getKeyPair().getPrivate(), credentials.getCertificate()) - .build(), - new char[0]); - } - - private X509Certificate requestRoleCertificate(AthenzRole role) { - var doc = credentials.getIdentityDocument().identityDocument(); - 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); - log.info(String.format("Requester role certificate for role %s, expires: %s", role.toResourceNameString(), roleCertificate.getNotAfter().toInstant().toString())); - return roleCertificate; - } - } - - private void updateRoleKeyManager(AthenzRole role, X509Certificate certificate) { - MutableX509KeyManager keyManager = roleKeyManagerCache.computeIfAbsent(role, r -> new MutableX509KeyManager()); - keyManager.updateKeystore( - KeyStoreBuilder.withType(PKCS12) - .withKeyEntry("default", credentials.getKeyPair().getPrivate(), certificate) - .build(), - new char[0]); - } - - private ZToken createRoleToken(AthenzRole athenzRole) { - try (ZtsClient client = createZtsClient()) { - return client.getRoleToken(athenzRole, ROLE_TOKEN_EXPIRY); - } - } - - private ZToken createRoleToken(AthenzDomain domain) { - try (ZtsClient client = createZtsClient()) { - return client.getRoleToken(domain, ROLE_TOKEN_EXPIRY); - } - } - - private AthenzAccessToken createAccessToken(AthenzDomain domain) { - try (ZtsClient client = createZtsClient()) { - return client.getAccessToken(domain); - } - } - - private AthenzAccessToken createAccessToken(List<AthenzRole> roles) { - try (ZtsClient client = createZtsClient()) { - return client.getAccessToken(roles); - } - } - - private DefaultZtsClient createZtsClient() { - return new DefaultZtsClient.Builder(ztsEndpoint).withSslContext(getIdentitySslContext()).build(); - } - - @Override - public void deconstruct() { - try { - scheduler.shutdownNow(); - scheduler.awaitTermination(AWAIT_TERMINTATION_TIMEOUT.getSeconds(), TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - private static SiaIdentityProvider createNodeIdentityProvider(IdentityConfig config) { - return new SiaIdentityProvider( - new AthenzService(config.nodeIdentityName()), SiaUtils.DEFAULT_SIA_DIRECTORY, CLIENT_TRUST_STORE); - } - - private boolean isExpired(AthenzCredentials credentials) { - return clock.instant().isAfter(getExpirationTime(credentials)); - } - - private static Instant getExpirationTime(AthenzCredentials credentials) { - return credentials.getCertificate().getNotAfter().toInstant(); - } - - void refreshCertificate() { - try { - updateIdentityCredentials(isExpired(credentials) - ? athenzCredentialsService.registerInstance() - : athenzCredentialsService.updateCredentials(credentials.getIdentityDocument(), identitySslContext)); - } catch (Throwable t) { - log.log(Level.WARNING, "Failed to update credentials: " + t.getMessage(), t); - } - } - - void reportMetrics() { - try { - Instant expirationTime = getExpirationTime(credentials); - Duration remainingLifetime = Duration.between(clock.instant(), expirationTime); - Metric.Context dimensions = metric.createContext(Map.of("implementation", this.getClassName())); - metric.set(CERTIFICATE_EXPIRY_METRIC_NAME, remainingLifetime.getSeconds(), dimensions); - } catch (Throwable t) { - log.log(Level.WARNING, "Failed to update metrics: " + t.getMessage(), t); - } - } -} - diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/ServiceIdentityProviderProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/ServiceIdentityProviderProvider.java index d0267d406ce..5dd49206c6d 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/ServiceIdentityProviderProvider.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/ServiceIdentityProviderProvider.java @@ -22,7 +22,6 @@ public class ServiceIdentityProviderProvider implements Provider<ServiceIdentity @Override public ServiceIdentityProvider get() { if (athenzIdentityProvider instanceof AthenzIdentityProviderImpl impl) return impl; - if (athenzIdentityProvider instanceof LegacyAthenzIdentityProviderImpl legacyImpl) return legacyImpl; return null; } 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 297f0c904d9..377aee22ab1 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 @@ -7,12 +7,8 @@ import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; -import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author bjorncs @@ -20,38 +16,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class EntityBindingsMapperTest { @Test - public void legacy_persists_unknown_json_members() throws IOException { - var originalJson = - """ - { - "signature": "sig", - "signing-key-version": 0, - "provider-unique-id": "0.cluster.instance.app.tenant.us-west-1.test.node", - "provider-service": "domain.service", - "document-version": 2, - "configserver-hostname": "cfg", - "instance-hostname": "host", - "created-at": 12345.0, - "ip-addresses": [], - "identity-type": "node", - "cluster-type": "admin", - "zts-url": "https://zts.url/", - "unknown-string": "string-value", - "unknown-object": { "member-in-unknown-object": 123 } - } - """; - var entity = EntityBindingsMapper.fromString(originalJson); - assertInstanceOf(LegacySignedIdentityDocument.class, entity); - assertEquals(2, entity.identityDocument().unknownAttributes().size(), entity.identityDocument().unknownAttributes().toString()); - var json = EntityBindingsMapper.toAttestationData(entity); - - var expectedMemberInJson = "member-in-unknown-object"; - assertTrue(json.contains(expectedMemberInJson), - () -> "Expected JSON to contain '%s', but got \n'%s'".formatted(expectedMemberInJson, json)); - assertEquals(EntityBindingsMapper.mapper.readTree(originalJson), EntityBindingsMapper.mapper.readTree(json)); - } - - @Test public void reads_unknown_json_members() throws IOException { var iddoc = """ { 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 2532a394f4e..3845d9db5b2 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 @@ -10,7 +10,6 @@ import com.yahoo.vespa.athenz.identityprovider.api.DefaultSignedIdentityDocument import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; -import com.yahoo.vespa.athenz.identityprovider.api.LegacySignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import org.junit.jupiter.api.Test; @@ -23,8 +22,6 @@ import java.util.List; import static com.yahoo.vespa.athenz.identityprovider.api.IdentityType.TENANT; import static com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION; -import static com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument.LEGACY_DEFAULT_DOCUMENT_VERSION; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -47,21 +44,6 @@ public class IdentityDocumentSignerTest { private static final AthenzIdentity serviceIdentity = new AthenzService("vespa", "node"); @Test - void legacy_generates_and_validates_signature() { - IdentityDocumentSigner signer = new IdentityDocumentSigner(); - IdentityDocument identityDocument = new IdentityDocument( - id, providerService, configserverHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity); - String signature = - signer.generateLegacySignature(identityDocument, keyPair.getPrivate()); - - SignedIdentityDocument signedIdentityDocument = new LegacySignedIdentityDocument( - signature, KEY_VERSION, LEGACY_DEFAULT_DOCUMENT_VERSION, identityDocument); - - assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic())); - } - - @Test void generates_and_validates_signature() { IdentityDocumentSigner signer = new IdentityDocumentSigner(); IdentityDocument identityDocument = new IdentityDocument( @@ -76,41 +58,4 @@ public class IdentityDocumentSignerTest { assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic())); } - - @Test - void legacy_ignores_cluster_type_and_zts_url() { - IdentityDocumentSigner signer = new IdentityDocumentSigner(); - IdentityDocument identityDocument = new IdentityDocument( - id, providerService, configserverHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity); - IdentityDocument withoutIgnoredFields = new IdentityDocument( - id, providerService, configserverHostname, - instanceHostname, createdAt, ipAddresses, identityType, null, null, serviceIdentity); - - String signature = - signer.generateLegacySignature(identityDocument, keyPair.getPrivate()); - - var docWithoutIgnoredFields = new LegacySignedIdentityDocument( - signature, KEY_VERSION, LEGACY_DEFAULT_DOCUMENT_VERSION, withoutIgnoredFields); - var docWithIgnoredFields = new LegacySignedIdentityDocument( - signature, KEY_VERSION, LEGACY_DEFAULT_DOCUMENT_VERSION, identityDocument); - - assertTrue(signer.hasValidSignature(docWithoutIgnoredFields, keyPair.getPublic())); - assertEquals(docWithIgnoredFields.signature(), docWithoutIgnoredFields.signature()); - } - - @Test - void validates_signature_for_new_and_old_versions() { - IdentityDocumentSigner signer = new IdentityDocumentSigner(); - IdentityDocument identityDocument = new IdentityDocument( - id, providerService, configserverHostname, - instanceHostname, createdAt, ipAddresses, identityType, clusterType, ztsUrl, serviceIdentity); - String signature = - signer.generateLegacySignature(identityDocument, keyPair.getPrivate()); - - SignedIdentityDocument signedIdentityDocument = new LegacySignedIdentityDocument( - signature, KEY_VERSION, LEGACY_DEFAULT_DOCUMENT_VERSION, identityDocument); - - assertTrue(signer.hasValidSignature(signedIdentityDocument, keyPair.getPublic())); - } } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImplTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImplTest.java deleted file mode 100644 index 90853ff7cfa..00000000000 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImplTest.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright Vespa.ai. 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.container.core.identity.IdentityConfig; -import com.yahoo.container.jdisc.athenz.AthenzIdentityProviderException; -import com.yahoo.jdisc.Metric; -import com.yahoo.security.KeyAlgorithm; -import com.yahoo.security.KeyStoreBuilder; -import com.yahoo.security.KeyStoreType; -import com.yahoo.security.KeyStoreUtils; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.Pkcs10Csr; -import com.yahoo.security.Pkcs10CsrBuilder; -import com.yahoo.security.SignatureAlgorithm; -import com.yahoo.security.X509CertificateBuilder; -import com.yahoo.test.ManualClock; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import javax.security.auth.x500.X500Principal; - -import java.io.File; -import java.io.IOException; -import java.math.BigInteger; -import java.nio.file.Path; -import java.security.KeyPair; -import java.security.cert.X509Certificate; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.Supplier; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * @author mortent - * @author bjorncs - */ -public class LegacyAthenzIdentityProviderImplTest { - - @TempDir - public File tempDir; - - public static final Duration certificateValidity = Duration.ofDays(30); - - private static final IdentityConfig IDENTITY_CONFIG = - new IdentityConfig(new IdentityConfig.Builder() - .service("tenantService") - .domain("tenantDomain") - .nodeIdentityName("vespa.tenant") - .configserverIdentityName("vespa.configserver") - .loadBalancerAddress("cfg") - .ztsUrl("https:localhost:4443/zts/v1") - .athenzDnsSuffix("dev-us-north-1.vespa.cloud")); - - private final KeyPair caKeypair = KeyUtils.generateKeypair(KeyAlgorithm.EC); - private Path trustStoreFile; - private X509Certificate caCertificate; - - @BeforeEach - public void createTrustStoreFile() throws IOException { - caCertificate = X509CertificateBuilder - .fromKeypair( - caKeypair, - new X500Principal("CN=mydummyca"), - Instant.EPOCH, - Instant.EPOCH.plus(10000, ChronoUnit.DAYS), - SignatureAlgorithm.SHA256_WITH_ECDSA, - BigInteger.ONE) - .build(); - trustStoreFile = File.createTempFile("junit", null, tempDir).toPath(); - KeyStoreUtils.writeKeyStoreToFile( - KeyStoreBuilder.withType(KeyStoreType.JKS) - .withKeyEntry("default", caKeypair.getPrivate(), caCertificate) - .build(), - trustStoreFile); - } - - @Test - void component_creation_fails_when_credentials_not_found() { - assertThrows(AthenzIdentityProviderException.class, () -> { - AthenzCredentialsService credentialService = mock(AthenzCredentialsService.class); - when(credentialService.registerInstance()) - .thenThrow(new RuntimeException("athenz unavailable")); - - new LegacyAthenzIdentityProviderImpl(IDENTITY_CONFIG, mock(Metric.class), trustStoreFile, credentialService, mock(ScheduledExecutorService.class), new ManualClock(Instant.EPOCH)); - }); - } - - @Test - void metrics_updated_on_refresh() { - ManualClock clock = new ManualClock(Instant.EPOCH); - Metric metric = mock(Metric.class); - - AthenzCredentialsService athenzCredentialsService = mock(AthenzCredentialsService.class); - - KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); - X509Certificate certificate = getCertificate(keyPair, getExpirationSupplier(clock)); - - when(athenzCredentialsService.registerInstance()) - .thenReturn(new AthenzCredentials(certificate, keyPair, null)); - - when(athenzCredentialsService.updateCredentials(any(), any())) - .thenThrow(new RuntimeException("#1")) - .thenThrow(new RuntimeException("#2")) - .thenReturn(new AthenzCredentials(certificate, keyPair, null)); - - LegacyAthenzIdentityProviderImpl identityProvider = - new LegacyAthenzIdentityProviderImpl(IDENTITY_CONFIG, metric, trustStoreFile, athenzCredentialsService, mock(ScheduledExecutorService.class), clock); - - identityProvider.reportMetrics(); - verify(metric).set(eq(LegacyAthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.getSeconds()), any()); - - // Advance 1 day, refresh fails, cert is 1 day old - clock.advance(Duration.ofDays(1)); - identityProvider.refreshCertificate(); - identityProvider.reportMetrics(); - verify(metric).set(eq(LegacyAthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.minus(Duration.ofDays(1)).getSeconds()), any()); - - // Advance 1 more day, refresh fails, cert is 2 days old - clock.advance(Duration.ofDays(1)); - identityProvider.refreshCertificate(); - identityProvider.reportMetrics(); - verify(metric).set(eq(LegacyAthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.minus(Duration.ofDays(2)).getSeconds()), any()); - - // Advance 1 more day, refresh succeds, cert is new - clock.advance(Duration.ofDays(1)); - identityProvider.refreshCertificate(); - identityProvider.reportMetrics(); - verify(metric).set(eq(LegacyAthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.getSeconds()), any()); - - } - - private Supplier<Date> getExpirationSupplier(ManualClock clock) { - return () -> new Date(clock.instant().plus(certificateValidity).toEpochMilli()); - } - - private X509Certificate getCertificate(KeyPair keyPair, Supplier<Date> expiry) { - Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(new X500Principal("CN=dummy"), keyPair, SignatureAlgorithm.SHA256_WITH_ECDSA) - .build(); - return X509CertificateBuilder - .fromCsr(csr, - caCertificate.getSubjectX500Principal(), - Instant.EPOCH, - expiry.get().toInstant(), - caKeypair.getPrivate(), - SignatureAlgorithm.SHA256_WITH_ECDSA, - BigInteger.ONE) - .build(); - } - -} |