aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOla Aunrønning <olaa@yahooinc.com>2023-04-26 09:57:56 +0200
committerGitHub <noreply@github.com>2023-04-26 09:57:56 +0200
commit5f9d7d50216da4850c793a2a63f8da7eefb55056 (patch)
treeb41007ada3c803cc3282cc0556acbb13f3642e6a
parent9a4376dae10e986c7061633e5a02f18c24a951da (diff)
parent48b7ffe757ffa25bc3ca1eeaab8153db30623fa3 (diff)
Merge pull request #26805 from vespa-engine/olaa/versioned-feature-flag
Write tenant service identity at same location as AthenzCredentialsService
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/IdentityProvider.java2
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java4
-rw-r--r--container-disc/abi-spec.json3
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/AthenzIdentityProviderProvider.java3
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java2
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java2
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java25
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java117
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java38
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImpl.java392
-rw-r--r--vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java50
-rw-r--r--vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImplTest.java160
12 files changed, 671 insertions, 127 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/IdentityProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/IdentityProvider.java
index 2b5dadf5512..c791fea3a56 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/IdentityProvider.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/IdentityProvider.java
@@ -17,7 +17,7 @@ import java.net.URI;
* @author mortent
*/
public class IdentityProvider extends SimpleComponent implements IdentityConfig.Producer {
- public static final String CLASS = "com.yahoo.vespa.athenz.identityprovider.client.AthenzIdentityProviderImpl";
+ public static final String CLASS = "com.yahoo.vespa.athenz.identityprovider.client.AthenzIdentityProviderProvider";
public static final String BUNDLE = "vespa-athenz";
private final AthenzDomain domain;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
index 36d34b99223..f74c218a906 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.container.xml;
+import com.yahoo.component.ComponentId;
import com.yahoo.config.provision.ClusterInfo;
import com.yahoo.config.provision.IntRange;
import com.yahoo.component.ComponentSpecification;
@@ -1169,6 +1170,9 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
ztsUrl,
zoneDnsSuffix,
zone);
+
+ // Replace AthenzIdentityProviderProvider
+ cluster.removeComponent(ComponentId.fromString("com.yahoo.container.jdisc.AthenzIdentityProviderProvider"));
cluster.addComponent(identityProvider);
cluster.getContainers().forEach(container -> {
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<AthenzIdentityPr
public Path trustStorePath() {
throw new UnsupportedOperationException(message);
}
+
+ @Override
+ public void deconstruct() {}
}
}
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java
index af5133eceac..46803988b20 100644
--- a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java
+++ b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java
@@ -24,5 +24,5 @@ public interface AthenzIdentityProvider {
X509Certificate getRoleCertificate(String domain, String role);
PrivateKey getPrivateKey();
Path trustStorePath();
-
+ void deconstruct();
}
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 e89724dfb16..500adab72cf 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 c9c76e1edd3..9e295b6a8e6 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;
@@ -107,6 +109,8 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
modified |= maintain(context, NODE);
if (shouldWriteTenantServiceIdentity(context))
modified |= maintain(context, TENANT);
+ else
+ modified |= deleteTenantCredentials(context);
return modified;
}
@@ -195,6 +199,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;
}
@@ -321,8 +340,12 @@ 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();
}
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 b5579cbfcd8..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
@@ -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<AthenzRole, X509Certificate> roleSslCertCache;
private final Map<AthenzRole, MutableX509KeyManager> roleKeyManagerCache;
@@ -98,40 +95,32 @@ 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(), createAutoReloadingX509KeyManager(config));
}
// Test only
AthenzIdentityProviderImpl(IdentityConfig config,
Metric metric,
Path trustStore,
- AthenzCredentialsService athenzCredentialsService,
ScheduledExecutorService scheduler,
- Clock clock) {
+ Clock clock,
+ AutoReloadingX509KeyManager autoReloadingX509KeyManager) {
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;
+ this.identitySslContext = createIdentitySslContext(autoReloadingX509KeyManager, trustStore);
+ this.scheduler.scheduleAtFixedRate(this::reportMetrics, 0, 5, TimeUnit.MINUTES);
}
private static <KEY, VALUE> LoadingCache<KEY, VALUE> createCache(Duration expiry, Function<KEY, VALUE> cacheLoader) {
@@ -165,16 +154,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 +176,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 +241,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 +251,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen
@Override
public List<X509Certificate> getIdentityCertificate() {
- return Collections.singletonList(credentials.getCertificate());
+ return List.of(autoReloadingX509KeyManager.getCertificateChain(AutoReloadingX509KeyManager.CERTIFICATE_ALIAS));
}
@Override
@@ -288,19 +267,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().identityDocument();
+ 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 +288,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]);
}
@@ -346,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 {
@@ -356,32 +336,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/AthenzIdentityProviderProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderProvider.java
new file mode 100644
index 00000000000..66dad931815
--- /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<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);
+ }
+
+ @Override
+ public void deconstruct() {
+ athenzIdentityProvider.deconstruct();
+ }
+
+ @Override
+ public AthenzIdentityProvider get() {
+ return athenzIdentityProvider;
+ }
+}
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
new file mode 100644
index 00000000000..d699564a4ee
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/LegacyAthenzIdentityProviderImpl.java
@@ -0,0 +1,392 @@
+// 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.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.Collections;
+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 PrivateKey getPrivateKey() {
+ return credentials.getKeyPair().getPrivate();
+ }
+
+ @Override
+ public Path trustStorePath() {
+ return trustStore;
+ }
+
+ @Override
+ public List<X509Certificate> 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().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.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
index c9d2ea581bb..108da9e0136 100644
--- 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
@@ -2,8 +2,8 @@
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.AutoReloadingX509KeyManager;
import com.yahoo.security.KeyAlgorithm;
import com.yahoo.security.KeyStoreBuilder;
import com.yahoo.security.KeyStoreType;
@@ -13,13 +13,13 @@ 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;
@@ -33,17 +33,12 @@ 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
@@ -85,58 +80,25 @@ public class AthenzIdentityProviderImplTest {
}
@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() {
+ void certificate_expiry_metric_is_reported() {
ManualClock clock = new ManualClock(Instant.EPOCH);
Metric metric = mock(Metric.class);
-
- AthenzCredentialsService athenzCredentialsService = mock(AthenzCredentialsService.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()));
- 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);
-
+ 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());
- // 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<Date> getExpirationSupplier(ManualClock clock) {
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<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();
+ }
+
+}