From a10c3bd03d03de4d591764da4f1e88ceeddc8ade Mon Sep 17 00:00:00 2001 From: Ola Aunronning Date: Fri, 21 Apr 2023 11:43:09 +0200 Subject: Write tenant service identity at same location as AthenzCredentialsService. Use Vespa version and application ID as flag dimensions --- .../src/main/java/com/yahoo/vespa/flags/Flags.java | 2 +- .../identity/AthenzCredentialsMaintainer.java | 25 +++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index ea17d9967ae..345a6f700fd 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -390,7 +390,7 @@ public class Flags { List.of("olaa"), "2023-04-12", "2023-06-12", "Whether AthenzCredentialsMaintainer in node-admin should create tenant service identity certificate", "Takes effect on next tick", - ZONE_ID, HOSTNAME + ZONE_ID, HOSTNAME, VESPA_VERSION, APPLICATION_ID ); public static final UnboundBooleanFlag ENABLE_CROWDSTRIKE = defineFeatureFlag( 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 3fb9c73367d..d3da5407472 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 @@ -1,6 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.maintenance.identity; +import com.yahoo.component.Version; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.Pkcs10Csr; @@ -68,6 +70,7 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { private static final Duration REFRESH_BACKOFF = Duration.ofHours(1); // Backoff when refresh fails to ensure ZTS is not DDoS'ed. private static final String CONTAINER_SIA_DIRECTORY = "/var/lib/sia"; + private static final String VESPA_SIA_DIRECTORY = "/opt/vespa/var/vespa/sia"; private final URI ztsEndpoint; private final Path ztsTrustStorePath; @@ -112,7 +115,7 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { try { context.log(logger, Level.FINE, "Checking certificate"); - ContainerPath siaDirectory = context.paths().of(CONTAINER_SIA_DIRECTORY, context.users().vespa()); + ContainerPath siaDirectory = context.paths().of(identityType.getSiaDirectory(), context.users().vespa()); ContainerPath identityDocumentFile = siaDirectory.resolve(identityType.getIdentityDocument()); AthenzIdentity athenzIdentity = getAthenzIdentity(context, identityType, identityDocumentFile); ContainerPath privateKeyFile = (ContainerPath) SiaUtils.getPrivateKeyFile(siaDirectory, athenzIdentity); @@ -164,6 +167,8 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { public void clearCredentials(NodeAgentContext context) { FileFinder.files(context.paths().of(CONTAINER_SIA_DIRECTORY)) .deleteRecursively(context); + FileFinder.files(context.paths().of(VESPA_SIA_DIRECTORY)) + .deleteRecursively(context); lastRefreshAttempt.remove(context.containerName()); } @@ -312,20 +317,30 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { } private boolean shouldWriteTenantServiceIdentity(NodeAgentContext context) { + var version = context.node().currentVespaVersion() + .orElse(context.node().wantedVespaVersion().orElse(Version.emptyVersion)); + var appId = context.node().owner().orElse(ApplicationId.defaultId()); return tenantServiceIdentityFlag - .with(FetchVector.Dimension.HOSTNAME, context.hostname().value()) + .with(FetchVector.Dimension.VESPA_VERSION, version.toFullString()) + .with(FetchVector.Dimension.APPLICATION_ID, appId.serializedForm()) .value(); } enum IdentityType { - NODE("vespa-node-identity-document.json"), - TENANT("vespa-tenant-identity-document.json"); + NODE(CONTAINER_SIA_DIRECTORY, "vespa-node-identity-document.json"), + TENANT(VESPA_SIA_DIRECTORY, "vespa-tenant-identity-document.json"); + private String siaDirectory; private String identityDocument; - IdentityType(String identityDocument) { + IdentityType(String siaDirectory, String identityDocument) { + this.siaDirectory = siaDirectory; this.identityDocument = identityDocument; } + public String getSiaDirectory() { + return siaDirectory; + } + public String getIdentityDocument() { return identityDocument; } -- cgit v1.2.3 From 02944bd423dede485eee68d63c2eef0fd81f5144 Mon Sep 17 00:00:00 2001 From: Ola Aunronning Date: Tue, 25 Apr 2023 10:27:48 +0200 Subject: Write creds in /var/lib/sia. Delete if flag is disabled --- .../identity/AthenzCredentialsMaintainer.java | 34 +++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) 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 d3da5407472..45973ee6784 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 @@ -70,7 +70,6 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { private static final Duration REFRESH_BACKOFF = Duration.ofHours(1); // Backoff when refresh fails to ensure ZTS is not DDoS'ed. private static final String CONTAINER_SIA_DIRECTORY = "/var/lib/sia"; - private static final String VESPA_SIA_DIRECTORY = "/opt/vespa/var/vespa/sia"; private final URI ztsEndpoint; private final Path ztsTrustStorePath; @@ -107,6 +106,8 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { modified |= maintain(context, NODE); if (shouldWriteTenantServiceIdentity(context)) modified |= maintain(context, TENANT); + else + modified |= deleteTenantCredentials(context); return modified; } @@ -115,7 +116,7 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { try { context.log(logger, Level.FINE, "Checking certificate"); - ContainerPath siaDirectory = context.paths().of(identityType.getSiaDirectory(), context.users().vespa()); + ContainerPath siaDirectory = context.paths().of(CONTAINER_SIA_DIRECTORY, context.users().vespa()); ContainerPath identityDocumentFile = siaDirectory.resolve(identityType.getIdentityDocument()); AthenzIdentity athenzIdentity = getAthenzIdentity(context, identityType, identityDocumentFile); ContainerPath privateKeyFile = (ContainerPath) SiaUtils.getPrivateKeyFile(siaDirectory, athenzIdentity); @@ -167,8 +168,6 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { public void clearCredentials(NodeAgentContext context) { FileFinder.files(context.paths().of(CONTAINER_SIA_DIRECTORY)) .deleteRecursively(context); - FileFinder.files(context.paths().of(VESPA_SIA_DIRECTORY)) - .deleteRecursively(context); lastRefreshAttempt.remove(context.containerName()); } @@ -192,6 +191,21 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { return "node-certificate"; } + private boolean deleteTenantCredentials(NodeAgentContext context) { + var siaDirectory = context.paths().of(CONTAINER_SIA_DIRECTORY, context.users().vespa()); + var identityDocumentFile = siaDirectory.resolve(TENANT.getIdentityDocument()); + var athenzIdentity = getAthenzIdentity(context, TENANT, identityDocumentFile); + var privateKeyFile = (ContainerPath) SiaUtils.getPrivateKeyFile(siaDirectory, athenzIdentity); + var certificateFile = (ContainerPath) SiaUtils.getCertificateFile(siaDirectory, athenzIdentity); + try { + return Files.deleteIfExists(identityDocumentFile) || + Files.deleteIfExists(privateKeyFile) || + Files.deleteIfExists(certificateFile); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + private boolean shouldRefreshCredentials(Duration age) { return age.compareTo(REFRESH_PERIOD) >= 0; } @@ -327,20 +341,14 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer { } enum IdentityType { - NODE(CONTAINER_SIA_DIRECTORY, "vespa-node-identity-document.json"), - TENANT(VESPA_SIA_DIRECTORY, "vespa-tenant-identity-document.json"); + NODE("vespa-node-identity-document.json"), + TENANT("vespa-tenant-identity-document.json"); - private String siaDirectory; private String identityDocument; - IdentityType(String siaDirectory, String identityDocument) { - this.siaDirectory = siaDirectory; + IdentityType(String identityDocument) { this.identityDocument = identityDocument; } - public String getSiaDirectory() { - return siaDirectory; - } - public String getIdentityDocument() { return identityDocument; } -- cgit v1.2.3 From 8436d1e3127f6e5d1f131f23ac6d35be3692362f Mon Sep 17 00:00:00 2001 From: Ola Aunronning Date: Tue, 25 Apr 2023 10:29:13 +0200 Subject: Consider credentials existence when setting up AthenzIdentityProvider --- .../container/ApplicationContainerCluster.java | 1 - .../vespa/model/container/IdentityProvider.java | 2 +- .../model/container/xml/ContainerModelBuilder.java | 4 +- container-disc/abi-spec.json | 3 +- .../jdisc/AthenzIdentityProviderProvider.java | 3 + .../jdisc/athenz/AthenzIdentityProvider.java | 2 +- .../client/AthenzIdentityProviderImplV2.java | 349 +++++++++++++++++++++ .../client/AthenzIdentityProviderProvider.java | 38 +++ 8 files changed, 396 insertions(+), 6 deletions(-) create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplV2.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java index d101cea428e..62589857d22 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java @@ -111,7 +111,6 @@ public final class ApplicationContainerCluster extends ContainerCluster { Zone zone, DeploymentSpec spec) { spec.athenzDomain() - .ifPresent(domain -> { + .ifPresentOrElse(domain -> { AthenzService service = spec.instance(app.getApplicationId().instance()) .flatMap(instanceSpec -> instanceSpec.athenzService(zone.environment(), zone.region())) .or(spec::athenzService) @@ -1175,7 +1175,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder { container.setProp("identity.domain", domain.value()); container.setProp("identity.service", service.value()); }); - }); + }, () -> cluster.addComponent(new SimpleComponent("com.yahoo.container.jdisc.AthenzIdentityProviderProvider"))); } private HostName getLoadBalancerName(HostName loadbalancerName, List configServerSpecs) { diff --git a/container-disc/abi-spec.json b/container-disc/abi-spec.json index dd681e4124f..75246a77e03 100644 --- a/container-disc/abi-spec.json +++ b/container-disc/abi-spec.json @@ -19,7 +19,8 @@ "public abstract java.util.List getIdentityCertificate()", "public abstract java.security.cert.X509Certificate getRoleCertificate(java.lang.String, java.lang.String)", "public abstract java.security.PrivateKey getPrivateKey()", - "public abstract java.nio.file.Path trustStorePath()" + "public abstract java.nio.file.Path trustStorePath()", + "public abstract void deconstruct()" ], "fields" : [ ] }, diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/AthenzIdentityProviderProvider.java b/container-disc/src/main/java/com/yahoo/container/jdisc/AthenzIdentityProviderProvider.java index f04e2291ee8..9d2e06ed9da 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/AthenzIdentityProviderProvider.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/AthenzIdentityProviderProvider.java @@ -89,6 +89,9 @@ public class AthenzIdentityProviderProvider implements Provider roleSslCertCache; + private final Map roleKeyManagerCache; + private final LoadingCache roleSpecificRoleTokenCache; + private final LoadingCache domainSpecificRoleTokenCache; + private final LoadingCache domainSpecificAccessTokenCache; + private final LoadingCache, AthenzAccessToken> roleSpecificAccessTokenCache; + private final CsrGenerator csrGenerator; + + @Inject + public AthenzIdentityProviderImplV2(IdentityConfig config, Metric metric) { + this(config, metric, CLIENT_TRUST_STORE, new ScheduledThreadPoolExecutor(1), Clock.systemUTC()); + } + + // Test only + AthenzIdentityProviderImplV2(IdentityConfig config, + Metric metric, + Path trustStore, + ScheduledExecutorService scheduler, + Clock clock) { + this.metric = metric; + this.trustStore = trustStore; + this.scheduler = scheduler; + this.clock = clock; + this.identity = new AthenzService(config.domain(), config.service()); + this.ztsEndpoint = URI.create(config.ztsUrl()); + this.roleSslCertCache = crateAutoReloadableCache(ROLE_SSL_CONTEXT_EXPIRY, this::requestRoleCertificate, this.scheduler); + this.roleKeyManagerCache = new HashMap<>(); + this.roleSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); + this.domainSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); + this.domainSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); + this.roleSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); + this.csrGenerator = new CsrGenerator(config.athenzDnsSuffix(), config.configserverIdentityName()); + this.autoReloadingX509KeyManager = AutoReloadingX509KeyManager.fromPemFiles(privateKeyPath(),certificatePath()); + this.identitySslContext = createIdentitySslContext(autoReloadingX509KeyManager, trustStore); + this.scheduler.scheduleAtFixedRate(this::reportMetrics, 0, 5, TimeUnit.MINUTES); + } + + private static LoadingCache createCache(Duration expiry, Function cacheLoader) { + return CacheBuilder.newBuilder() + .refreshAfterWrite(expiry.dividedBy(2).toMinutes(), TimeUnit.MINUTES) + .expireAfterWrite(expiry.toMinutes(), TimeUnit.MINUTES) + .build(new CacheLoader() { + @Override + public VALUE load(KEY key) { + return cacheLoader.apply(key); + } + }); + } + + private static LoadingCache crateAutoReloadableCache(Duration expiry, Function cacheLoader, ScheduledExecutorService scheduler) { + LoadingCache 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(); + } + + @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() { + var copy = this.autoReloadingX509KeyManager.getCurrentCertificateWithKey(); + return new X509CertificateWithKey(copy.certificate(), copy.privateKey()); + } + + @Override public Path certificatePath() { return SiaUtils.getCertificateFile(identity); } + + @Override public Path privateKeyPath() { return SiaUtils.getPrivateKeyFile(identity); } + + @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 roles) { + try { + List 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 PrivateKey getPrivateKey() { + return autoReloadingX509KeyManager.getPrivateKey(AutoReloadingX509KeyManager.CERTIFICATE_ALIAS); + } + + @Override + public Path trustStorePath() { + return trustStore; + } + + @Override + public List getIdentityCertificate() { + return List.of(autoReloadingX509KeyManager.getCertificateChain(AutoReloadingX509KeyManager.CERTIFICATE_ALIAS)); + } + + @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 X509Certificate requestRoleCertificate(AthenzRole role) { + var credentials = autoReloadingX509KeyManager.getCurrentCertificateWithKey(); + var athenzUniqueInstanceId = VespaUniqueInstanceId.fromDottedString( + AthenzX509CertificateUtils.getInstanceId(credentials.certificate()) + .orElseThrow() + ); + var keyPair = KeyUtils.toKeyPair(credentials.privateKey()); + Pkcs10Csr csr = csrGenerator.generateRoleCsr( + identity, role, athenzUniqueInstanceId, null, keyPair); + 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", autoReloadingX509KeyManager.getCurrentCertificateWithKey().privateKey(), 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 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 Instant getExpirationTime(X509Certificate certificate) { + return certificate.getNotAfter().toInstant(); + } + + void reportMetrics() { + try { + Instant expirationTime = getExpirationTime(autoReloadingX509KeyManager.getCurrentCertificateWithKey().certificate()); + Duration remainingLifetime = Duration.between(clock.instant(), expirationTime); + metric.set(CERTIFICATE_EXPIRY_METRIC_NAME, remainingLifetime.getSeconds(), null); + } 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/AthenzIdentityProviderProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java new file mode 100644 index 00000000000..d53b68bffc9 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java @@ -0,0 +1,38 @@ +package com.yahoo.vespa.athenz.identityprovider.client; + +import com.yahoo.container.core.identity.IdentityConfig; +import com.yahoo.container.di.componentgraph.Provider; +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 { + + 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 AthenzIdentityProviderImplV2(config, metric); + else + athenzIdentityProvider = new AthenzIdentityProviderImpl(config, metric); + } + + @Override + public void deconstruct() { + athenzIdentityProvider.deconstruct(); + } + + @Override + public AthenzIdentityProvider get() { + return athenzIdentityProvider; + } +} -- cgit v1.2.3 From cf4c4d51f2853f094717e957a06eb409c1b1503e Mon Sep 17 00:00:00 2001 From: Ola Aunronning Date: Tue, 25 Apr 2023 11:37:55 +0200 Subject: Rename AthenzIdentityProviderImpl classes --- .../client/AthenzIdentityProviderImpl.java | 109 ++---- .../client/AthenzIdentityProviderImplV2.java | 349 ------------------ .../client/AthenzIdentityProviderProvider.java | 4 +- .../client/LegacyAthenzIdentityProviderImpl.java | 392 +++++++++++++++++++++ .../client/AthenzIdentityProviderImplTest.java | 160 --------- .../LegacyAthenzIdentityProviderImplTest.java | 160 +++++++++ 6 files changed, 586 insertions(+), 588 deletions(-) delete mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplV2.java create mode 100644 vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImpl.java delete mode 100644 vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java create mode 100644 vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImplTest.java 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 b9f9f3862c2..3d8f12e559d 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 @@ -11,7 +11,9 @@ 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.AutoReloadingX509KeyManager; import com.yahoo.security.KeyStoreBuilder; +import com.yahoo.security.KeyUtils; import com.yahoo.security.MutableX509KeyManager; import com.yahoo.security.Pkcs10Csr; import com.yahoo.security.SslContextBuilder; @@ -24,9 +26,9 @@ 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.identityprovider.api.VespaUniqueInstanceId; +import com.yahoo.vespa.athenz.tls.AthenzX509CertificateUtils; import com.yahoo.vespa.athenz.utils.SiaUtils; -import com.yahoo.vespa.defaults.Defaults; import javax.net.ssl.SSLContext; import javax.net.ssl.X509ExtendedKeyManager; @@ -38,7 +40,6 @@ import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,7 +66,6 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen // 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 @@ -73,20 +73,17 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen // 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 AutoReloadingX509KeyManager autoReloadingX509KeyManager; private final SSLContext identitySslContext; private final LoadingCache roleSslCertCache; private final Map roleKeyManagerCache; @@ -98,40 +95,31 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen @Inject public AthenzIdentityProviderImpl(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()); + this(config, metric, CLIENT_TRUST_STORE, new ScheduledThreadPoolExecutor(1), Clock.systemUTC()); } // Test only AthenzIdentityProviderImpl(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.roleSslCertCache = crateAutoReloadableCache(ROLE_SSL_CONTEXT_EXPIRY, this::requestRoleCertificate, this.scheduler); + this.roleKeyManagerCache = new HashMap<>(); + this.roleSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); + this.domainSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); + this.domainSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); + this.roleSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); this.csrGenerator = new CsrGenerator(config.athenzDnsSuffix(), config.configserverIdentityName()); - this.identitySslContext = createIdentitySslContext(identityKeyManager, trustStore); - registerInstance(); + this.autoReloadingX509KeyManager = AutoReloadingX509KeyManager.fromPemFiles(privateKeyPath(),certificatePath()); + this.identitySslContext = createIdentitySslContext(autoReloadingX509KeyManager, trustStore); + this.scheduler.scheduleAtFixedRate(this::reportMetrics, 0, 5, TimeUnit.MINUTES); } private static LoadingCache createCache(Duration expiry, Function cacheLoader) { @@ -165,16 +153,6 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen .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; @@ -197,13 +175,13 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen @Override public X509CertificateWithKey getIdentityCertificateWithKey() { - AthenzCredentials copy = this.credentials; - return new X509CertificateWithKey(copy.getCertificate(), copy.getKeyPair().getPrivate()); + var copy = this.autoReloadingX509KeyManager.getCurrentCertificateWithKey(); + return new X509CertificateWithKey(copy.certificate(), copy.privateKey()); } - @Override public Path certificatePath() { return athenzCredentialsService.certificatePath(); } + @Override public Path certificatePath() { return SiaUtils.getCertificateFile(identity); } - @Override public Path privateKeyPath() { return athenzCredentialsService.privateKeyPath(); } + @Override public Path privateKeyPath() { return SiaUtils.getPrivateKeyFile(identity); } @Override public SSLContext getRoleSslContext(String domain, String role) { @@ -262,7 +240,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen @Override public PrivateKey getPrivateKey() { - return credentials.getKeyPair().getPrivate(); + return autoReloadingX509KeyManager.getPrivateKey(AutoReloadingX509KeyManager.CERTIFICATE_ALIAS); } @Override @@ -272,7 +250,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen @Override public List getIdentityCertificate() { - return Collections.singletonList(credentials.getCertificate()); + return List.of(autoReloadingX509KeyManager.getCertificateChain(AutoReloadingX509KeyManager.CERTIFICATE_ALIAS)); } @Override @@ -288,19 +266,15 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen } } - 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(); + var credentials = autoReloadingX509KeyManager.getCurrentCertificateWithKey(); + var athenzUniqueInstanceId = VespaUniqueInstanceId.fromDottedString( + AthenzX509CertificateUtils.getInstanceId(credentials.certificate()) + .orElseThrow() + ); + var keyPair = KeyUtils.toKeyPair(credentials.privateKey()); Pkcs10Csr csr = csrGenerator.generateRoleCsr( - identity, role, doc.providerUniqueId(), doc.clusterType(), credentials.getKeyPair()); + identity, role, athenzUniqueInstanceId, null, keyPair); try (ZtsClient client = createZtsClient()) { X509Certificate roleCertificate = client.getRoleCertificate(role, csr); updateRoleKeyManager(role, roleCertificate); @@ -313,7 +287,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen MutableX509KeyManager keyManager = roleKeyManagerCache.computeIfAbsent(role, r -> new MutableX509KeyManager()); keyManager.updateKeystore( KeyStoreBuilder.withType(PKCS12) - .withKeyEntry("default", credentials.getKeyPair().getPrivate(), certificate) + .withKeyEntry("default", autoReloadingX509KeyManager.getCurrentCertificateWithKey().privateKey(), certificate) .build(), new char[0]); } @@ -356,32 +330,13 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen } } - 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); - } + private static Instant getExpirationTime(X509Certificate certificate) { + return certificate.getNotAfter().toInstant(); } void reportMetrics() { try { - Instant expirationTime = getExpirationTime(credentials); + Instant expirationTime = getExpirationTime(autoReloadingX509KeyManager.getCurrentCertificateWithKey().certificate()); Duration remainingLifetime = Duration.between(clock.instant(), expirationTime); metric.set(CERTIFICATE_EXPIRY_METRIC_NAME, remainingLifetime.getSeconds(), null); } catch (Throwable t) { diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplV2.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplV2.java deleted file mode 100644 index b03ad244ee4..00000000000 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplV2.java +++ /dev/null @@ -1,349 +0,0 @@ -// 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.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.metrics.ContainerMetrics; -import com.yahoo.security.AutoReloadingX509KeyManager; -import com.yahoo.security.KeyStoreBuilder; -import com.yahoo.security.KeyUtils; -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.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.vespa.athenz.tls.AthenzX509CertificateUtils; -import com.yahoo.vespa.athenz.utils.SiaUtils; - -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 - * @author olaa - */ -// 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 AthenzIdentityProviderImplV2 extends AbstractComponent implements AthenzIdentityProvider, ServiceIdentityProvider { - - private static final Logger log = Logger.getLogger(AthenzIdentityProviderImplV2.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 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"); - - public static final String CERTIFICATE_EXPIRY_METRIC_NAME = ContainerMetrics.ATHENZ_TENANT_CERT_EXPIRY_SECONDS.baseName(); - - private final Metric metric; - private final Path trustStore; - private final ScheduledExecutorService scheduler; - private final Clock clock; - private final AthenzService identity; - private final URI ztsEndpoint; - - private final AutoReloadingX509KeyManager autoReloadingX509KeyManager; - private final SSLContext identitySslContext; - private final LoadingCache roleSslCertCache; - private final Map roleKeyManagerCache; - private final LoadingCache roleSpecificRoleTokenCache; - private final LoadingCache domainSpecificRoleTokenCache; - private final LoadingCache domainSpecificAccessTokenCache; - private final LoadingCache, AthenzAccessToken> roleSpecificAccessTokenCache; - private final CsrGenerator csrGenerator; - - @Inject - public AthenzIdentityProviderImplV2(IdentityConfig config, Metric metric) { - this(config, metric, CLIENT_TRUST_STORE, new ScheduledThreadPoolExecutor(1), Clock.systemUTC()); - } - - // Test only - AthenzIdentityProviderImplV2(IdentityConfig config, - Metric metric, - Path trustStore, - ScheduledExecutorService scheduler, - Clock clock) { - this.metric = metric; - this.trustStore = trustStore; - this.scheduler = scheduler; - this.clock = clock; - this.identity = new AthenzService(config.domain(), config.service()); - this.ztsEndpoint = URI.create(config.ztsUrl()); - this.roleSslCertCache = crateAutoReloadableCache(ROLE_SSL_CONTEXT_EXPIRY, this::requestRoleCertificate, this.scheduler); - this.roleKeyManagerCache = new HashMap<>(); - this.roleSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); - this.domainSpecificRoleTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createRoleToken); - this.domainSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); - this.roleSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); - this.csrGenerator = new CsrGenerator(config.athenzDnsSuffix(), config.configserverIdentityName()); - this.autoReloadingX509KeyManager = AutoReloadingX509KeyManager.fromPemFiles(privateKeyPath(),certificatePath()); - this.identitySslContext = createIdentitySslContext(autoReloadingX509KeyManager, trustStore); - this.scheduler.scheduleAtFixedRate(this::reportMetrics, 0, 5, TimeUnit.MINUTES); - } - - private static LoadingCache createCache(Duration expiry, Function cacheLoader) { - return CacheBuilder.newBuilder() - .refreshAfterWrite(expiry.dividedBy(2).toMinutes(), TimeUnit.MINUTES) - .expireAfterWrite(expiry.toMinutes(), TimeUnit.MINUTES) - .build(new CacheLoader() { - @Override - public VALUE load(KEY key) { - return cacheLoader.apply(key); - } - }); - } - - private static LoadingCache crateAutoReloadableCache(Duration expiry, Function cacheLoader, ScheduledExecutorService scheduler) { - LoadingCache 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(); - } - - @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() { - var copy = this.autoReloadingX509KeyManager.getCurrentCertificateWithKey(); - return new X509CertificateWithKey(copy.certificate(), copy.privateKey()); - } - - @Override public Path certificatePath() { return SiaUtils.getCertificateFile(identity); } - - @Override public Path privateKeyPath() { return SiaUtils.getPrivateKeyFile(identity); } - - @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 roles) { - try { - List 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 PrivateKey getPrivateKey() { - return autoReloadingX509KeyManager.getPrivateKey(AutoReloadingX509KeyManager.CERTIFICATE_ALIAS); - } - - @Override - public Path trustStorePath() { - return trustStore; - } - - @Override - public List getIdentityCertificate() { - return List.of(autoReloadingX509KeyManager.getCertificateChain(AutoReloadingX509KeyManager.CERTIFICATE_ALIAS)); - } - - @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 X509Certificate requestRoleCertificate(AthenzRole role) { - var credentials = autoReloadingX509KeyManager.getCurrentCertificateWithKey(); - var athenzUniqueInstanceId = VespaUniqueInstanceId.fromDottedString( - AthenzX509CertificateUtils.getInstanceId(credentials.certificate()) - .orElseThrow() - ); - var keyPair = KeyUtils.toKeyPair(credentials.privateKey()); - Pkcs10Csr csr = csrGenerator.generateRoleCsr( - identity, role, athenzUniqueInstanceId, null, keyPair); - 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", autoReloadingX509KeyManager.getCurrentCertificateWithKey().privateKey(), 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 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 Instant getExpirationTime(X509Certificate certificate) { - return certificate.getNotAfter().toInstant(); - } - - void reportMetrics() { - try { - Instant expirationTime = getExpirationTime(autoReloadingX509KeyManager.getCurrentCertificateWithKey().certificate()); - Duration remainingLifetime = Duration.between(clock.instant(), expirationTime); - metric.set(CERTIFICATE_EXPIRY_METRIC_NAME, remainingLifetime.getSeconds(), null); - } 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/AthenzIdentityProviderProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java index d53b68bffc9..66dad931815 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 @@ -21,9 +21,9 @@ public class AthenzIdentityProviderProvider implements Provider roleSslCertCache; + private final Map roleKeyManagerCache; + private final LoadingCache roleSpecificRoleTokenCache; + private final LoadingCache domainSpecificRoleTokenCache; + private final LoadingCache domainSpecificAccessTokenCache; + private final LoadingCache, 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 LoadingCache createCache(Duration expiry, Function cacheLoader) { + return CacheBuilder.newBuilder() + .refreshAfterWrite(expiry.dividedBy(2).toMinutes(), TimeUnit.MINUTES) + .expireAfterWrite(expiry.toMinutes(), TimeUnit.MINUTES) + .build(new CacheLoader() { + @Override + public VALUE load(KEY key) { + return cacheLoader.apply(key); + } + }); + } + + private static LoadingCache crateAutoReloadableCache(Duration expiry, Function cacheLoader, ScheduledExecutorService scheduler) { + LoadingCache 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 roles) { + try { + List 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 PrivateKey getPrivateKey() { + return credentials.getKeyPair().getPrivate(); + } + + @Override + public Path trustStorePath() { + return trustStore; + } + + @Override + public List getIdentityCertificate() { + return Collections.singletonList(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(); + 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 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.set(CERTIFICATE_EXPIRY_METRIC_NAME, remainingLifetime.getSeconds(), null); + } catch (Throwable t) { + log.log(Level.WARNING, "Failed to update metrics: " + t.getMessage(), t); + } + } +} + diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java deleted file mode 100644 index c9d2ea581bb..00000000000 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java +++ /dev/null @@ -1,160 +0,0 @@ -// 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.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 AthenzIdentityProviderImplTest { - - @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 AthenzIdentityProviderImpl(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)); - - AthenzIdentityProviderImpl identityProvider = - new AthenzIdentityProviderImpl(IDENTITY_CONFIG, metric, trustStoreFile, athenzCredentialsService, mock(ScheduledExecutorService.class), clock); - - identityProvider.reportMetrics(); - verify(metric).set(eq(AthenzIdentityProviderImpl.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(AthenzIdentityProviderImpl.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(AthenzIdentityProviderImpl.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(AthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.getSeconds()), any()); - - } - - private Supplier getExpirationSupplier(ManualClock clock) { - return () -> new Date(clock.instant().plus(certificateValidity).toEpochMilli()); - } - - private X509Certificate getCertificate(KeyPair keyPair, Supplier 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(); - } - -} 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 new file mode 100644 index 00000000000..75dc42cd4a6 --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImplTest.java @@ -0,0 +1,160 @@ +// 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.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 getExpirationSupplier(ManualClock clock) { + return () -> new Date(clock.instant().plus(certificateValidity).toEpochMilli()); + } + + private X509Certificate getCertificate(KeyPair keyPair, Supplier 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(); + } + +} -- cgit v1.2.3 From d74583acb0e5087567f1b977a72e82e48b2ea763 Mon Sep 17 00:00:00 2001 From: Ola Aunronning Date: Tue, 25 Apr 2023 11:40:40 +0200 Subject: Replace AthenzIdentityProviderProvider --- .../yahoo/vespa/model/container/ApplicationContainerCluster.java | 1 + .../yahoo/vespa/model/container/xml/ContainerModelBuilder.java | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java index 62589857d22..d101cea428e 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java @@ -111,6 +111,7 @@ public final class ApplicationContainerCluster extends ContainerCluster { Zone zone, DeploymentSpec spec) { spec.athenzDomain() - .ifPresentOrElse(domain -> { + .ifPresent(domain -> { AthenzService service = spec.instance(app.getApplicationId().instance()) .flatMap(instanceSpec -> instanceSpec.athenzService(zone.environment(), zone.region())) .or(spec::athenzService) @@ -1169,13 +1170,16 @@ public class ContainerModelBuilder extends ConfigModelBuilder { ztsUrl, zoneDnsSuffix, zone); + + // Replace AthenzIdentityProviderProvider + cluster.removeComponent(ComponentId.fromString("com.yahoo.container.jdisc.AthenzIdentityProviderProvider")); cluster.addComponent(identityProvider); cluster.getContainers().forEach(container -> { container.setProp("identity.domain", domain.value()); container.setProp("identity.service", service.value()); }); - }, () -> cluster.addComponent(new SimpleComponent("com.yahoo.container.jdisc.AthenzIdentityProviderProvider"))); + }); } private HostName getLoadBalancerName(HostName loadbalancerName, List configServerSpecs) { -- cgit v1.2.3 From d46b56f3f8577d8c4cc4b08f5f13b3b983ef7d2a Mon Sep 17 00:00:00 2001 From: Ola Aunronning Date: Tue, 25 Apr 2023 12:46:30 +0200 Subject: Add test --- .../client/AthenzIdentityProviderImpl.java | 12 +- .../client/AthenzIdentityProviderImplTest.java | 122 +++++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java 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 3d8f12e559d..77aaf17419d 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 @@ -95,7 +95,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen @Inject public AthenzIdentityProviderImpl(IdentityConfig config, Metric metric) { - this(config, metric, CLIENT_TRUST_STORE, new ScheduledThreadPoolExecutor(1), Clock.systemUTC()); + this(config, metric, CLIENT_TRUST_STORE, new ScheduledThreadPoolExecutor(1), Clock.systemUTC(), createAutoReloadingX509KeyManager(config)); } // Test only @@ -103,7 +103,8 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen Metric metric, Path trustStore, ScheduledExecutorService scheduler, - Clock clock) { + Clock clock, + AutoReloadingX509KeyManager autoReloadingX509KeyManager) { this.metric = metric; this.trustStore = trustStore; this.scheduler = scheduler; @@ -117,7 +118,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen this.domainSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); this.roleSpecificAccessTokenCache = createCache(ROLE_TOKEN_EXPIRY, this::createAccessToken); this.csrGenerator = new CsrGenerator(config.athenzDnsSuffix(), config.configserverIdentityName()); - this.autoReloadingX509KeyManager = AutoReloadingX509KeyManager.fromPemFiles(privateKeyPath(),certificatePath()); + this.autoReloadingX509KeyManager = autoReloadingX509KeyManager; this.identitySslContext = createIdentitySslContext(autoReloadingX509KeyManager, trustStore); this.scheduler.scheduleAtFixedRate(this::reportMetrics, 0, 5, TimeUnit.MINUTES); } @@ -320,6 +321,11 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen return new DefaultZtsClient.Builder(ztsEndpoint).withSslContext(getIdentitySslContext()).build(); } + private static AutoReloadingX509KeyManager createAutoReloadingX509KeyManager(IdentityConfig config) { + var tenantIdentity = new AthenzService(config.domain(), config.service()); + return AutoReloadingX509KeyManager.fromPemFiles(SiaUtils.getPrivateKeyFile(tenantIdentity), SiaUtils.getCertificateFile(tenantIdentity)); + } + @Override public void deconstruct() { try { diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java new file mode 100644 index 00000000000..108da9e0136 --- /dev/null +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java @@ -0,0 +1,122 @@ +// 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.container.core.identity.IdentityConfig; +import com.yahoo.jdisc.Metric; +import com.yahoo.security.AutoReloadingX509KeyManager; +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.security.X509CertificateWithKey; +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.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; + +public class AthenzIdentityProviderImplTest { + + @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 certificate_expiry_metric_is_reported() { + ManualClock clock = new ManualClock(Instant.EPOCH); + Metric metric = mock(Metric.class); + AutoReloadingX509KeyManager keyManager = mock(AutoReloadingX509KeyManager.class); + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + X509Certificate certificate = getCertificate(keyPair, getExpirationSupplier(clock)); + when(keyManager.getCurrentCertificateWithKey()).thenReturn(new X509CertificateWithKey(certificate, keyPair.getPrivate())); + + AthenzIdentityProviderImpl identityProvider = new AthenzIdentityProviderImpl(IDENTITY_CONFIG, metric, trustStoreFile, mock(ScheduledExecutorService.class), clock, keyManager); + identityProvider.reportMetrics(); + verify(metric).set(eq(AthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.getSeconds()), any()); + + clock.advance(Duration.ofDays(1)); + identityProvider.reportMetrics(); + verify(metric).set(eq(AthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.minus(Duration.ofDays(1)).getSeconds()), any()); + + clock.advance(Duration.ofDays(1)); + identityProvider.reportMetrics(); + verify(metric).set(eq(AthenzIdentityProviderImpl.CERTIFICATE_EXPIRY_METRIC_NAME), eq(certificateValidity.minus(Duration.ofDays(2)).getSeconds()), any()); + } + + private Supplier getExpirationSupplier(ManualClock clock) { + return () -> new Date(clock.instant().plus(certificateValidity).toEpochMilli()); + } + + private X509Certificate getCertificate(KeyPair keyPair, Supplier 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(); + } + +} -- cgit v1.2.3