aboutsummaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java')
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java182
1 files changed, 182 insertions, 0 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java
new file mode 100644
index 00000000000..61a4a0fe41f
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java
@@ -0,0 +1,182 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice;
+
+import com.yahoo.component.annotation.Inject;
+import com.yahoo.jdisc.http.ssl.impl.TlsContextBasedProvider;
+import com.yahoo.security.KeyStoreBuilder;
+import com.yahoo.security.KeyStoreType;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SslContextBuilder;
+import com.yahoo.security.tls.DefaultTlsContext;
+import com.yahoo.security.MutableX509KeyManager;
+import com.yahoo.security.tls.PeerAuthentication;
+import com.yahoo.security.tls.TlsContext;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient;
+import com.yahoo.vespa.athenz.client.zts.Identity;
+import com.yahoo.vespa.athenz.client.zts.ZtsClient;
+import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
+import com.yahoo.vespa.athenz.utils.SiaUtils;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+
+import javax.net.ssl.SSLContext;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Configures the JDisc https connector with the configserver's Athenz provider certificate and private key.
+ *
+ * @author bjorncs
+ */
+public class ConfigserverSslContextFactoryProvider extends TlsContextBasedProvider {
+
+ private static final String CERTIFICATE_ALIAS = "athenz";
+ private static final Duration EXPIRATION_MARGIN = Duration.ofHours(6);
+ private static final Path VESPA_SIA_DIRECTORY = Paths.get(Defaults.getDefaults().underVespaHome("var/vespa/sia"));
+
+ private static final Logger log = Logger.getLogger(ConfigserverSslContextFactoryProvider.class.getName());
+
+ private final TlsContext tlsContext;
+ private final MutableX509KeyManager keyManager = new MutableX509KeyManager();
+ private final ScheduledExecutorService scheduler =
+ Executors.newSingleThreadScheduledExecutor(runnable -> new Thread(runnable, "configserver-ssl-context-factory-provider"));
+ private final ZtsClient ztsClient;
+ private final KeyProvider keyProvider;
+ private final AthenzProviderServiceConfig athenzProviderServiceConfig;
+ private final AthenzService configserverIdentity;
+
+ @Inject
+ public ConfigserverSslContextFactoryProvider(ServiceIdentityProvider bootstrapIdentity,
+ KeyProvider keyProvider,
+ AthenzProviderServiceConfig config) {
+ this.athenzProviderServiceConfig = config;
+ this.ztsClient = new DefaultZtsClient.Builder(URI.create(athenzProviderServiceConfig.ztsUrl()))
+ .withIdentityProvider(bootstrapIdentity).build();
+ this.keyProvider = keyProvider;
+ this.configserverIdentity = new AthenzService(athenzProviderServiceConfig.domain(), athenzProviderServiceConfig.serviceName());
+
+ Duration updatePeriod = Duration.ofDays(config.updatePeriodDays());
+ Path trustStoreFile = Paths.get(config.athenzCaTrustStore());
+ this.tlsContext = createTlsContext(keyProvider, keyManager, trustStoreFile, updatePeriod, configserverIdentity, ztsClient, athenzProviderServiceConfig);
+ scheduler.scheduleAtFixedRate(new KeystoreUpdater(keyManager),
+ updatePeriod.toDays()/*initial delay*/,
+ updatePeriod.toDays(),
+ TimeUnit.DAYS);
+ }
+
+ @Override
+ protected TlsContext getTlsContext(String containerId, int port) {
+ return tlsContext;
+ }
+
+ Instant getCertificateNotAfter() {
+ return keyManager.currentManager().getCertificateChain(CERTIFICATE_ALIAS)[0].getNotAfter().toInstant();
+ }
+
+ @Override
+ public void deconstruct() {
+ try {
+ scheduler.shutdownNow();
+ scheduler.awaitTermination(30, TimeUnit.SECONDS);
+ ztsClient.close();
+ super.deconstruct();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to shutdown Athenz certificate updater on time", e);
+ }
+ }
+
+ private static TlsContext createTlsContext(KeyProvider keyProvider,
+ MutableX509KeyManager keyManager,
+ Path trustStoreFile,
+ Duration updatePeriod,
+ AthenzService configserverIdentity,
+ ZtsClient ztsClient,
+ AthenzProviderServiceConfig zoneConfig) {
+ KeyStore keyStore =
+ tryReadKeystoreFile(configserverIdentity, updatePeriod)
+ .orElseGet(() -> updateKeystore(configserverIdentity, generateKeystorePassword(), keyProvider, ztsClient, zoneConfig));
+ keyManager.updateKeystore(keyStore, new char[0]);
+ SSLContext sslContext = new SslContextBuilder()
+ .withTrustStore(trustStoreFile, KeyStoreType.JKS)
+ .withKeyManager(keyManager)
+ .build();
+ return new DefaultTlsContext(sslContext, PeerAuthentication.WANT);
+ }
+
+ private static Optional<KeyStore> tryReadKeystoreFile(AthenzService configserverIdentity, Duration updatePeriod) {
+ Optional<X509Certificate> certificate = SiaUtils.readCertificateFile(VESPA_SIA_DIRECTORY, configserverIdentity);
+ if (!certificate.isPresent()) return Optional.empty();
+ Optional<PrivateKey> privateKey = SiaUtils.readPrivateKeyFile(VESPA_SIA_DIRECTORY, configserverIdentity);
+ if (!privateKey.isPresent()) return Optional.empty();
+ Instant minimumExpiration = Instant.now().plus(updatePeriod).plus(EXPIRATION_MARGIN);
+ boolean isExpired = certificate.get().getNotAfter().toInstant().isBefore(minimumExpiration);
+ if (isExpired) return Optional.empty();
+ KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry(CERTIFICATE_ALIAS, privateKey.get(), certificate.get())
+ .build();
+ return Optional.of(keyStore);
+ }
+
+ private static KeyStore updateKeystore(AthenzService configserverIdentity,
+ char[] keystorePwd,
+ KeyProvider keyProvider,
+ ZtsClient ztsClient,
+ AthenzProviderServiceConfig zoneConfig) {
+ PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion());
+ PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
+ Identity serviceIdentity = ztsClient.getServiceIdentity(configserverIdentity,
+ Integer.toString(zoneConfig.secretVersion()),
+ new KeyPair(publicKey, privateKey),
+ zoneConfig.certDnsSuffix());
+ X509Certificate certificate = serviceIdentity.certificate();
+ SiaUtils.writeCertificateFile(VESPA_SIA_DIRECTORY, configserverIdentity, certificate);
+ SiaUtils.writePrivateKeyFile(VESPA_SIA_DIRECTORY, configserverIdentity, privateKey);
+ Instant expirationTime = certificate.getNotAfter().toInstant();
+ Duration expiry = Duration.between(certificate.getNotBefore().toInstant(), expirationTime);
+ log.log(Level.INFO, String.format("Got Athenz x509 certificate with expiry %s (expires %s)", expiry, expirationTime));
+ return KeyStoreBuilder.withType(KeyStoreType.JKS)
+ .withKeyEntry(CERTIFICATE_ALIAS, privateKey, keystorePwd, certificate)
+ .build();
+ }
+
+ private static char[] generateKeystorePassword() {
+ return UUID.randomUUID().toString().toCharArray();
+ }
+
+ private class KeystoreUpdater implements Runnable {
+ final MutableX509KeyManager keyManager;
+
+ KeystoreUpdater(MutableX509KeyManager keyManager) {
+ this.keyManager = keyManager;
+ }
+
+ @Override
+ public void run() {
+ try {
+ log.log(Level.INFO, "Updating configserver provider certificate from ZTS");
+ char[] keystorePwd = generateKeystorePassword();
+ KeyStore keyStore = updateKeystore(configserverIdentity, keystorePwd, keyProvider, ztsClient, athenzProviderServiceConfig);
+ keyManager.updateKeystore(keyStore, keystorePwd);
+ log.log(Level.INFO, "Certificate successfully updated");
+ } catch (Throwable t) {
+ log.log(Level.SEVERE, "Failed to update certificate from ZTS: " + t.getMessage(), t);
+ }
+ }
+ }
+}