summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--athenz-identity-provider-service/OWNERS1
-rw-r--r--athenz-identity-provider-service/pom.xml144
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java182
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java40
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGenerator.java89
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java56
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java11
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java81
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java21
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java23
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java59
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java99
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java79
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java87
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java8
-rw-r--r--athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def29
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java260
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java115
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/Container.java16
-rwxr-xr-xconfig-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java21
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java22
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java3
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java1
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java28
-rw-r--r--config-model/src/main/resources/schema/containercluster.rnc7
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java4
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java3
-rw-r--r--config-model/src/test/schema-test-files/services.xml4
-rw-r--r--configdefinitions/src/vespa/configserver.def3
-rw-r--r--configserver/src/main/resources/configserver-app/services.xml2
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/identity/package-info.java7
-rw-r--r--container-core/src/main/resources/configdefinitions/identity.def4
-rw-r--r--container-dev/pom.xml8
-rw-r--r--container-disc/pom.xml11
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java121
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzService.java46
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceIdentity.java55
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceRegisterInformation.java38
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/athenz/ServiceProviderApi.java47
-rw-r--r--container-disc/src/test/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProviderTest.java47
-rw-r--r--pom.xml6
-rw-r--r--standalone-container/src/main/scala/com/yahoo/container/standalone/CloudConfigYinstVariables.scala1
42 files changed, 1877 insertions, 12 deletions
diff --git a/athenz-identity-provider-service/OWNERS b/athenz-identity-provider-service/OWNERS
new file mode 100644
index 00000000000..569bf1cc3a1
--- /dev/null
+++ b/athenz-identity-provider-service/OWNERS
@@ -0,0 +1 @@
+bjorncs
diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml
new file mode 100644
index 00000000000..074d4aa54f4
--- /dev/null
+++ b/athenz-identity-provider-service/pom.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>athenz-identity-provider-service</artifactId>
+ <packaging>container-plugin</packaging>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ </parent>
+ <dependencies>
+ <!-- COMPILE -->
+ <dependency>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-zms-java-client</artifactId>
+ <scope>compile</scope>
+ <exclusions>
+ <!--Exclude all Jersey bundles provided by JDisc-->
+ <exclusion>
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-client</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ </exclusion>
+ <!-- BouncyCastle is not bundled due to class loading issues
+ when security provider is registered from inside a OSGi bundle -->
+ <exclusion>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-zts-java-client</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <scope>compile</scope>
+ </dependency>
+
+ <!-- PROVIDED -->
+ <!-- BouncyCastle should be available through jdisc_http_service at runtime -->
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>node-repository</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-provisioning</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-disc</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- TEST -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-test</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.4.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ <version>4.4.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java
new file mode 100644
index 00000000000..301d6250b31
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java
@@ -0,0 +1,182 @@
+// Copyright 2017 Yahoo Holdings. 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.google.inject.Inject;
+import com.yahoo.athenz.auth.impl.PrincipalAuthority;
+import com.yahoo.athenz.auth.impl.SimpleServiceIdentityProvider;
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.athenz.zts.InstanceRefreshRequest;
+import com.yahoo.athenz.zts.ZTSClient;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.jdisc.http.ssl.ReaderForPath;
+import com.yahoo.jdisc.http.ssl.pem.PemKeyStore;
+import com.yahoo.jdisc.http.ssl.pem.PemSslKeyStore;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.athenz.identityproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.FileBackedKeyProvider;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.IdentityDocumentGenerator;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.KeyProvider;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.ProviderServiceServlet;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.StatusServlet;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import java.io.StringReader;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * A component acting as both SIA for configserver and provides a lightweight Jetty instance hosting the InstanceConfirmation API
+ *
+ * @author bjorncs
+ */
+public class AthenzInstanceProviderService extends AbstractComponent {
+
+ private static final Logger log = Logger.getLogger(AthenzInstanceProviderService.class.getName());
+
+ private final ScheduledExecutorService scheduler;
+ private final Server jetty;
+
+ @Inject
+ public AthenzInstanceProviderService(AthenzProviderServiceConfig config, NodeRepository nodeRepository, Zone zone) {
+ this(config, new FileBackedKeyProvider(config.keyPathPrefix()), Executors.newSingleThreadScheduledExecutor(),
+ nodeRepository, zone);
+ }
+
+ AthenzInstanceProviderService(AthenzProviderServiceConfig config,
+ KeyProvider keyProvider,
+ ScheduledExecutorService scheduler, NodeRepository nodeRepository, Zone zone) {
+ this.scheduler = scheduler;
+ SslContextFactory sslContextFactory = createSslContextFactory();
+ this.jetty = createJettyServer(config, keyProvider, sslContextFactory,
+ nodeRepository, zone);
+ AthenzCertificateUpdater reloader = new AthenzCertificateUpdater(
+ sslContextFactory, keyProvider, config);
+ scheduler.scheduleAtFixedRate(reloader, 0, 1, TimeUnit.DAYS);
+ try {
+ jetty.start();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Server createJettyServer(AthenzProviderServiceConfig config,
+ KeyProvider keyProvider,
+ SslContextFactory sslContextFactory,
+ NodeRepository nodeRepository,
+ Zone zone) {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server, sslContextFactory);
+ connector.setPort(config.port());
+ server.addConnector(connector);
+
+ ServletHandler handler = new ServletHandler();
+ ProviderServiceServlet providerServiceServlet =
+ new ProviderServiceServlet(new InstanceValidator(keyProvider), new IdentityDocumentGenerator(config, nodeRepository, zone, keyProvider));
+ handler.addServletWithMapping(new ServletHolder(providerServiceServlet), config.apiPath());
+ handler.addServletWithMapping(StatusServlet.class, "/status.html");
+ server.setHandler(handler);
+ return server;
+
+ }
+
+ private static SslContextFactory createSslContextFactory() {
+ try {
+ SslContextFactory sslContextFactory = new SslContextFactory();
+ sslContextFactory.setWantClientAuth(true);
+ sslContextFactory.setProtocol("TLS");
+ sslContextFactory.setKeyManagerFactoryAlgorithm("SunX509");
+ return sslContextFactory;
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to create SSL context factory: " + e.getMessage(), e);
+ }
+ }
+
+ private static class AthenzCertificateUpdater implements Runnable {
+
+ private static final Logger log = Logger.getLogger(AthenzCertificateUpdater.class.getName());
+
+ private final SslContextFactory sslContextFactory;
+ private final KeyProvider keyProvider;
+ private final AthenzProviderServiceConfig config;
+
+ private AthenzCertificateUpdater(SslContextFactory sslContextFactory,
+ KeyProvider keyProvider,
+ AthenzProviderServiceConfig config) {
+ this.sslContextFactory = sslContextFactory;
+ this.keyProvider = keyProvider;
+ this.config = config;
+ }
+
+ @Override
+ public void run() {
+ try {
+ log.log(LogLevel.INFO, "Updating Athenz certificate through ZTS");
+ String privateKey = keyProvider.getPrivateKey(config.keyVersion());
+ String certificate = getCertificateFromZTS(Crypto.loadPrivateKey(privateKey));
+ final KeyStore keyStore =
+ new PemSslKeyStore(
+ new PemKeyStore.KeyStoreLoadParameter(
+ new ReaderForPath(new StringReader(certificate), null),
+ new ReaderForPath(new StringReader(privateKey), null)))
+ .loadJavaKeyStore();
+ sslContextFactory.reload(sslContextFactory -> sslContextFactory.setKeyStore(keyStore));
+ log.log(LogLevel.INFO, "Athenz certificate reload successfully completed");
+ } catch (Exception e) {
+ log.log(LogLevel.ERROR, "Failed to update certificate from ZTS: " + e.getMessage(), e);
+ }
+ }
+
+ private String getCertificateFromZTS(PrivateKey privateKey) {
+ SimpleServiceIdentityProvider identityProvider = new SimpleServiceIdentityProvider(
+ new AthenzPrincipalAuthority(config.athenzPrincipalHeaderName()), config.domain(), config.serviceName(),
+ privateKey, Integer.toString(config.keyVersion()), TimeUnit.MINUTES.toSeconds(10));
+ ZTSClient ztsClient = new ZTSClient(
+ config.ztsUrl(), config.domain(), config.serviceName(), identityProvider);
+ InstanceRefreshRequest req = ZTSClient.generateInstanceRefreshRequest(
+ config.domain(), config.serviceName(), privateKey, config.certDnsSuffix(), (int)TimeUnit.DAYS.toSeconds(30));
+ return ztsClient.postInstanceRefreshRequest(config.domain(), config.serviceName(), req).getCertificate();
+ }
+
+ private static class AthenzPrincipalAuthority extends PrincipalAuthority {
+ private final String headerName;
+
+ public AthenzPrincipalAuthority(String headerName) {
+ this.headerName = headerName;
+ }
+
+ @Override
+ public String getHeader() {
+ return headerName;
+ }
+ }
+ }
+
+ @Override
+ public void deconstruct() {
+ try {
+ log.log(LogLevel.INFO, "Deconstructing Athenz provider service");
+ scheduler.shutdown();
+ jetty.stop();
+ if (!scheduler.awaitTermination(1, TimeUnit.MINUTES)) {
+ log.log(LogLevel.ERROR, "Failed to stop certificate updater");
+ }
+ } catch (InterruptedException e) {
+ log.log(LogLevel.ERROR, "Failed to stop certificate updater: " + e.getMessage(), e);
+ } catch (Exception e) {
+ log.log(LogLevel.ERROR, "Failed to stop Jetty: " + e.getMessage(), e);
+ } finally {
+ super.deconstruct();
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java
new file mode 100644
index 00000000000..f03f8415586
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java
@@ -0,0 +1,40 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+
+/**
+ * @author bjorncs
+ */
+public class FileBackedKeyProvider implements KeyProvider {
+
+ private final String keyPathPrefix;
+
+ public FileBackedKeyProvider(String keyPathPrefix) {
+ this.keyPathPrefix = keyPathPrefix;
+ }
+
+ @Override
+ public String getPrivateKey(int version) {
+ return loadKey(new File(keyPathPrefix + ".priv." + version));
+ }
+
+ @Override
+ public String getPublicKey(int version) {
+ return loadKey(new File(keyPathPrefix + ".pub." + version));
+ }
+
+ private static String loadKey(File file) {
+ try {
+ if (!file.exists() || !file.isFile()) {
+ throw new IllegalArgumentException("Key missing: " + file.getAbsolutePath());
+ }
+ return new String(Files.readAllBytes(file.toPath()));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGenerator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGenerator.java
new file mode 100644
index 00000000000..6a8e3c01b55
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGenerator.java
@@ -0,0 +1,89 @@
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl;
+
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.hosted.athenz.identityproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.IdentityDocument;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.node.Allocation;
+
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.time.Instant;
+import java.util.Base64;
+
+/**
+ * @author mortent
+ */
+public class IdentityDocumentGenerator {
+
+ private final NodeRepository nodeRepository;
+ private final Zone zone;
+ private final KeyProvider keyProvider;
+ private final String dnsSuffix;
+ private final String providerService;
+ private final String ztsUrl;
+
+ public IdentityDocumentGenerator(AthenzProviderServiceConfig config, NodeRepository nodeRepository, Zone zone, KeyProvider keyProvider) {
+ this.nodeRepository = nodeRepository;
+ this.zone = zone;
+ this.keyProvider = keyProvider;
+ this.dnsSuffix = config.certDnsSuffix();
+ this.providerService = config.serviceName();
+ this.ztsUrl = config.ztsUrl();
+ }
+
+ public String generateSignedIdentityDocument(String hostname) {
+ Node node = nodeRepository.getNode(hostname).orElseThrow(() -> new RuntimeException("Unable to find node " + hostname));
+ try {
+ IdentityDocument identityDocument = generateIdDocument(node);
+ String identityDocumentString = Utils.getMapper().writeValueAsString(identityDocument);
+
+ String encodedIdentityDocument =
+ Base64.getEncoder().encodeToString(identityDocumentString.getBytes());
+ Signature sigGenerator = Signature.getInstance("SHA512withRSA");
+
+ // TODO: Get the correct version 0 ok for now
+ PrivateKey privateKey = Crypto.loadPrivateKey(keyProvider.getPrivateKey(0));
+ sigGenerator.initSign(privateKey);
+ sigGenerator.update(encodedIdentityDocument.getBytes());
+ String signature = Base64.getEncoder().encodeToString(sigGenerator.sign());
+
+ SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument(
+ encodedIdentityDocument,
+ signature,
+ SignedIdentityDocument.DEFAULT_KEY_VERSION,
+ identityDocument.providerUniqueId.asString(),
+ dnsSuffix,
+ providerService,
+ ztsUrl,
+ SignedIdentityDocument.DEFAILT_DOCUMENT_VERSION
+ );
+ return Utils.getMapper().writeValueAsString(signedIdentityDocument);
+ } catch (Exception e) {
+ throw new RuntimeException("Exception generating identity document: " + e.getMessage(), e);
+ }
+ }
+
+ private IdentityDocument generateIdDocument(Node node) {
+ Allocation allocation = node.allocation().orElseThrow(() -> new RuntimeException("No allocation for node " + node.hostname()));
+ ProviderUniqueId providerUniqueId = new ProviderUniqueId(
+ allocation.owner().tenant().value(),
+ allocation.owner().application().value(),
+ zone.environment().value(),
+ zone.region().value(),
+ allocation.owner().instance().value(),
+ allocation.membership().cluster().id().value(),
+ allocation.membership().index());
+
+ return new IdentityDocument(
+ providerUniqueId,
+ "localhost", // TODO: Add configserver hostname
+ node.hostname(),
+ Instant.now());
+ }
+}
+
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java
new file mode 100644
index 00000000000..f5c2c319041
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java
@@ -0,0 +1,56 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl;
+
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.Base64;
+import java.util.logging.Logger;
+
+/**
+ * Verifies that the instance's identity document is valid
+ *
+ * @author bjorncs
+ */
+public class InstanceValidator {
+
+ private static final Logger log = Logger.getLogger(InstanceValidator.class.getName());
+
+ private final KeyProvider keyProvider;
+
+ public InstanceValidator(KeyProvider keyProvider) {
+ this.keyProvider = keyProvider;
+ }
+
+ public boolean isValidInstance(InstanceConfirmation instanceConfirmation) {
+ SignedIdentityDocument signedIdentityDocument = instanceConfirmation.signedIdentityDocument;
+ ProviderUniqueId providerUniqueId = signedIdentityDocument.identityDocument.providerUniqueId;
+ log.log(LogLevel.INFO, () -> String.format("Validating instance %s.", providerUniqueId));
+ PublicKey publicKey = Crypto.loadPublicKey(keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion));
+ if (isSignatureValid(publicKey, signedIdentityDocument.rawIdentityDocument, signedIdentityDocument.signature)) {
+ log.log(LogLevel.INFO, () -> String.format("Instance %s is valid.", providerUniqueId));
+ return true;
+ }
+ log.log(LogLevel.ERROR, () -> String.format("Instance %s has invalid signature.", providerUniqueId));
+ return false;
+ }
+
+ public static boolean isSignatureValid(PublicKey publicKey, String rawIdentityDocument, String signature) {
+ try {
+ Signature signatureVerifier = Signature.getInstance("SHA512withRSA");
+ signatureVerifier.initVerify(publicKey);
+ signatureVerifier.update(rawIdentityDocument.getBytes());
+ return signatureVerifier.verify(Base64.getDecoder().decode(signature));
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java
new file mode 100644
index 00000000000..8c807405693
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java
@@ -0,0 +1,11 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl;
+
+/**
+ * @author bjorncs
+ */
+public interface KeyProvider {
+ String getPrivateKey(int version);
+
+ String getPublicKey(int version);
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java
new file mode 100644
index 00000000000..7766dc9cc3c
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java
@@ -0,0 +1,81 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * A Servlet implementing the Athenz Service Provider InstanceConfirmation API
+ *
+ * @author bjorncs
+ */
+public class ProviderServiceServlet extends HttpServlet {
+
+ private static final Logger log = Logger.getLogger(ProviderServiceServlet.class.getName());
+
+ private final InstanceValidator instanceValidator;
+ private final IdentityDocumentGenerator identityDocumentGenerator;
+
+ public ProviderServiceServlet(InstanceValidator instanceValidator, IdentityDocumentGenerator identityDocumentGenerator) {
+ this.instanceValidator = instanceValidator;
+ this.identityDocumentGenerator = identityDocumentGenerator;
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ // TODO Validate that request originates from ZTS
+ try {
+ String confirmationContent = toString(req.getReader());
+ log.log(LogLevel.DEBUG, () -> "Confirmation content: " + confirmationContent);
+ InstanceConfirmation instanceConfirmation =
+ Utils.getMapper().readValue(confirmationContent, InstanceConfirmation.class);
+ log.log(LogLevel.DEBUG, () -> "Parsed confirmation content: " + instanceConfirmation.toString());
+ if (!instanceValidator.isValidInstance(instanceConfirmation)) {
+ log.log(LogLevel.ERROR, "Invalid instance: " + instanceConfirmation);
+ resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ } else {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType("application/json");
+ resp.getWriter().write(Utils.getMapper().writeValueAsString(instanceConfirmation));
+ }
+ } catch (JsonParseException | JsonMappingException e) {
+ log.log(LogLevel.ERROR, "InstanceConfirmation is not valid JSON", e);
+ resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ // TODO verify tls client cert
+ String hostname = req.getParameter("hostname");
+ try {
+ String signedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(hostname);
+ resp.setContentType("application/json");
+ PrintWriter writer = resp.getWriter();
+ writer.print(signedIdentityDocument);
+ writer.flush();
+ } catch (Exception e) {
+ resp.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("Unable to generate identity doument [%s]", e.getMessage()));
+ }
+ }
+
+ private static String toString(Reader reader) throws IOException {
+ try (BufferedReader bufferedReader = new BufferedReader(reader)) {
+ return bufferedReader.lines().collect(Collectors.joining("\n"));
+ }
+ }
+
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java
new file mode 100644
index 00000000000..fd5ba5843aa
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java
@@ -0,0 +1,21 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * A simple status servlet that should return status code 200 as long as the provider service servlet is up.
+ *
+ * @author bjorncs
+ */
+public class StatusServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java
new file mode 100644
index 00000000000..d81ec183fd4
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java
@@ -0,0 +1,23 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+/**
+ * @author bjorncs
+ */
+public class Utils {
+
+ private static final ObjectMapper mapper = createObjectMapper();
+
+ public static ObjectMapper getMapper() {
+ return mapper;
+ }
+
+ private static ObjectMapper createObjectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ return mapper;
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java
new file mode 100644
index 00000000000..41ce5d969a7
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java
@@ -0,0 +1,59 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * @author bjorncs
+ */
+public class IdentityDocument {
+
+ @JsonProperty("provider-unique-id")
+ public final ProviderUniqueId providerUniqueId;
+ @JsonProperty("configserver-hostname")
+ public final String configServerHostname;
+ @JsonProperty("instance-hostname")
+ public final String instanceHostname;
+ @JsonProperty("created-at")
+ public final Instant createdAt;
+
+ public IdentityDocument(
+ @JsonProperty("provider-unique-id") ProviderUniqueId providerUniqueId,
+ @JsonProperty("configserver-hostname") String configServerHostname,
+ @JsonProperty("instance-hostname") String instanceHostname,
+ @JsonProperty("created-at") Instant createdAt) {
+ this.providerUniqueId = providerUniqueId;
+ this.configServerHostname = configServerHostname;
+ this.instanceHostname = instanceHostname;
+ this.createdAt = createdAt;
+ }
+
+ @Override
+ public String toString() {
+ return "IdentityDocument{" +
+ "providerUniqueId=" + providerUniqueId +
+ ", configServerHostname='" + configServerHostname + '\'' +
+ ", instanceHostname='" + instanceHostname + '\'' +
+ ", createdAt=" + createdAt +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IdentityDocument that = (IdentityDocument) o;
+ return Objects.equals(providerUniqueId, that.providerUniqueId) &&
+ Objects.equals(configServerHostname, that.configServerHostname) &&
+ Objects.equals(instanceHostname, that.instanceHostname) &&
+ Objects.equals(createdAt, that.createdAt);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(providerUniqueId, configServerHostname, instanceHostname, createdAt);
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java
new file mode 100644
index 00000000000..ade42968e58
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java
@@ -0,0 +1,99 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * InstanceConfirmation object as per Athenz InstanceConfirmation API.
+ *
+ * @author bjorncs
+ */
+public class InstanceConfirmation {
+
+ @JsonProperty("provider") public final String provider;
+ @JsonProperty("domain") public final String domain;
+ @JsonProperty("service") public final String service;
+
+ @JsonProperty("attestationData") @JsonSerialize(using = SignedIdentitySerializer.class)
+ public final SignedIdentityDocument signedIdentityDocument;
+ @JsonUnwrapped public final Map<String, Object> attributes = new HashMap<>(); // optional attributes that Athenz may provide
+
+ @JsonCreator
+ public InstanceConfirmation(@JsonProperty("provider") String provider,
+ @JsonProperty("domain") String domain,
+ @JsonProperty("service") String service,
+ @JsonProperty("attestationData") @JsonDeserialize(using = SignedIdentityDeserializer.class)
+ SignedIdentityDocument signedIdentityDocument) {
+ this.provider = provider;
+ this.domain = domain;
+ this.service = service;
+ this.signedIdentityDocument = signedIdentityDocument;
+ }
+
+ @JsonAnySetter
+ public void set(String name, Object value) {
+ attributes.put(name, value);
+ }
+
+ @Override
+ public String toString() {
+ return "InstanceConfirmation{" +
+ "provider='" + provider + '\'' +
+ ", domain='" + domain + '\'' +
+ ", service='" + service + '\'' +
+ ", signedIdentityDocument='" + signedIdentityDocument + '\'' +
+ ", attributes=" + attributes +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ InstanceConfirmation that = (InstanceConfirmation) o;
+ return Objects.equals(provider, that.provider) &&
+ Objects.equals(domain, that.domain) &&
+ Objects.equals(service, that.service) &&
+ Objects.equals(signedIdentityDocument, that.signedIdentityDocument) &&
+ Objects.equals(attributes, that.attributes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(provider, domain, service, signedIdentityDocument, attributes);
+ }
+
+ public static class SignedIdentityDeserializer extends JsonDeserializer<SignedIdentityDocument> {
+ @Override
+ public SignedIdentityDocument deserialize(
+ JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+ String value = jsonParser.getValueAsString();
+ return Utils.getMapper().readValue(value, SignedIdentityDocument.class);
+ }
+ }
+
+ public static class SignedIdentitySerializer extends JsonSerializer<SignedIdentityDocument> {
+ @Override
+ public void serialize(
+ SignedIdentityDocument document, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ gen.writeString(Utils.getMapper().writeValueAsString(document));
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java
new file mode 100644
index 00000000000..810c75ef0c5
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java
@@ -0,0 +1,79 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
+/**
+ * @author bjorncs
+ */
+public class ProviderUniqueId {
+
+ @JsonProperty("tenant")
+ public final String tenant;
+ @JsonProperty("application")
+ public final String application;
+ @JsonProperty("environment")
+ public final String environment;
+ @JsonProperty("region")
+ public final String region;
+ @JsonProperty("instance")
+ public final String instance;
+ @JsonProperty("cluster-id")
+ public final String clusterId;
+ @JsonProperty("cluster-index")
+ public final int clusterIndex;
+
+ public ProviderUniqueId(@JsonProperty("tenant") String tenant,
+ @JsonProperty("application") String application,
+ @JsonProperty("environment") String environment,
+ @JsonProperty("region") String region,
+ @JsonProperty("instance") String instance,
+ @JsonProperty("cluster-id") String clusterId,
+ @JsonProperty("cluster-index") int clusterIndex) {
+ this.tenant = tenant;
+ this.application = application;
+ this.environment = environment;
+ this.region = region;
+ this.instance = instance;
+ this.clusterId = clusterId;
+ this.clusterIndex = clusterIndex;
+ }
+
+ public String asString() {
+ return String.format("%s.%s.%s.%s.%s.%s.%d", tenant, application, environment, region, instance, clusterId, clusterIndex);
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderUniqueId{" +
+ "tenant='" + tenant + '\'' +
+ ", application='" + application + '\'' +
+ ", environment='" + environment + '\'' +
+ ", region='" + region + '\'' +
+ ", instance='" + instance + '\'' +
+ ", clusterId='" + clusterId + '\'' +
+ ", clusterIndex=" + clusterIndex +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ProviderUniqueId that = (ProviderUniqueId) o;
+ return clusterIndex == that.clusterIndex &&
+ Objects.equals(tenant, that.tenant) &&
+ Objects.equals(application, that.application) &&
+ Objects.equals(environment, that.environment) &&
+ Objects.equals(region, that.region) &&
+ Objects.equals(instance, that.instance) &&
+ Objects.equals(clusterId, that.clusterId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tenant, application, environment, region, instance, clusterId, clusterIndex);
+ }
+} \ No newline at end of file
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java
new file mode 100644
index 00000000000..37f94d48a95
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java
@@ -0,0 +1,87 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Base64;
+import java.util.Objects;
+
+/**
+ * @author bjorncs
+ */
+public class SignedIdentityDocument {
+
+ public static final int DEFAULT_KEY_VERSION = 0;
+ public static final int DEFAILT_DOCUMENT_VERSION = 1;
+
+ @JsonProperty("identity-document")public final String rawIdentityDocument;
+ @JsonIgnore public final IdentityDocument identityDocument;
+ @JsonProperty("signature") public final String signature;
+ @JsonProperty("signing-key-version") public final int signingKeyVersion;
+ @JsonProperty("provider-unique-id") public final String providerUniqueId; // String representation
+ @JsonProperty("dns-suffix") public final String dnsSuffix;
+ @JsonProperty("provider-service") public final String providerService;
+ @JsonProperty("zts-endpoint") public final String ztsEndpoint;
+ @JsonProperty("document-version") public final int documentVersion;
+
+ @JsonCreator
+ public SignedIdentityDocument(@JsonProperty("identity-document") String rawIdentityDocument,
+ @JsonProperty("signature") String signature,
+ @JsonProperty("signing-key-version") int signingKeyVersion,
+ @JsonProperty("provider-unique-id") String providerUniqueId,
+ @JsonProperty("dns-suffix") String dnsSuffix,
+ @JsonProperty("provider-service") String providerService,
+ @JsonProperty("zts-endpoint") String ztsEndpoint,
+ @JsonProperty("document-version") int documentVersion) {
+ this.rawIdentityDocument = rawIdentityDocument;
+ this.identityDocument = parseIdentityDocument(rawIdentityDocument);
+ this.signature = signature;
+ this.signingKeyVersion = signingKeyVersion;
+ this.providerUniqueId = providerUniqueId;
+ this.dnsSuffix = dnsSuffix;
+ this.providerService = providerService;
+ this.ztsEndpoint = ztsEndpoint;
+ this.documentVersion = documentVersion;
+ }
+
+ private static IdentityDocument parseIdentityDocument(String rawIdentityDocument) {
+ try {
+ return Utils.getMapper().readValue(Base64.getDecoder().decode(rawIdentityDocument), IdentityDocument.class);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SignedIdentityDocument{" +
+ "rawIdentityDocument='" + rawIdentityDocument + '\'' +
+ ", identityDocument=" + identityDocument +
+ ", signature='" + signature + '\'' +
+ ", signingKeyVersion=" + signingKeyVersion +
+ ", documentVersion=" + documentVersion +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SignedIdentityDocument that = (SignedIdentityDocument) o;
+ return signingKeyVersion == that.signingKeyVersion &&
+ documentVersion == that.documentVersion &&
+ Objects.equals(rawIdentityDocument, that.rawIdentityDocument) &&
+ Objects.equals(identityDocument, that.identityDocument) &&
+ Objects.equals(signature, that.signature);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rawIdentityDocument, identityDocument, signature, signingKeyVersion, documentVersion);
+ }
+}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java
new file mode 100644
index 00000000000..3024d1e0115
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author bjorncs
+ */
+@ExportPackage
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def b/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def
new file mode 100644
index 00000000000..3a2ef9c3092
--- /dev/null
+++ b/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def
@@ -0,0 +1,29 @@
+# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.hosted.athenz.identityproviderservice.config
+
+# Athenz domain
+domain string
+
+# Athenz service name
+serviceName string
+
+# Current key version
+keyVersion int default=0
+
+# HTTPS port for Athenz Provider Service endpoint
+port int default=8443
+
+# File name prefix for private and public key. Component assumes suffix .[priv|pub].<version>.
+keyPathPrefix string
+
+# InstanceConfirmation API path
+apiPath string default="/athenz/v1/provider/instance"
+
+# Athenz principal authority header name
+athenzPrincipalHeaderName string default="Athenz-Principal-Auth"
+
+# Athenz ZTS server url
+ztsUrl string
+
+# Certificate DNS suffix
+certDnsSuffix string
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java
new file mode 100644
index 00000000000..125f8a3cb0f
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java
@@ -0,0 +1,260 @@
+// Copyright 2017 Yahoo Holdings. 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.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableSet;
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.component.Version;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.Flavor;
+import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.NodeType;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.athenz.identityproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.IdentityDocumentGenerator;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.KeyProvider;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.IdentityDocument;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.node.Allocation;
+import com.yahoo.vespa.hosted.provision.node.Generation;
+import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyManagementException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author bjorncs
+ */
+public class AthenzInstanceProviderServiceTest {
+
+ private static final Logger log = Logger.getLogger(AthenzInstanceProviderServiceTest.class.getName());
+ private static final int PORT = 12345;
+
+ @Test
+ @Ignore("Requires private key for Athenz service")
+ public void provider_service_hosts_endpoint_secured_with_tls() throws Exception {
+ String domain = "INSERT DOMAIN HERE";
+ String service = "INSERT SERVICE NAME HERE";
+ DummyKeyProvider keyProvider = new DummyKeyProvider();
+ PrivateKey privateKey = Crypto.loadPrivateKey(keyProvider.getPrivateKey(0));
+
+ AthenzProviderServiceConfig config = getAthenzProviderConfig(domain, service, "INSERT ZTS URL HERE", "INSERT DNS SUFFIX HERE");
+
+ ScheduledExecutorServiceMock executor = new ScheduledExecutorServiceMock();
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ Zone zone = new Zone(Environment.dev, RegionName.from("us-north-1"));
+ AthenzInstanceProviderService athenzInstanceProviderService = new AthenzInstanceProviderService(config, keyProvider, executor, nodeRepository, zone);
+
+ try (CloseableHttpClient client = createHttpClient(domain, service)) {
+ Runnable certificateRefreshCommand = executor.getCommand().orElseThrow(() -> new AssertionError("Command not present"));
+ assertFalse(getStatus(client));
+ certificateRefreshCommand.run();
+ assertTrue(getStatus(client));
+ assertInstanceConfirmationSucceeds(client, privateKey);
+ certificateRefreshCommand.run();
+ assertTrue(getStatus(client));
+ assertInstanceConfirmationSucceeds(client, privateKey);
+ } finally {
+ athenzInstanceProviderService.deconstruct();
+ }
+ }
+
+ @Test
+ public void generates_valid_identity_document() throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
+ String hostname = "x.y.com";
+ AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider();
+ AthenzProviderServiceConfig config = getAthenzProviderConfig("domain", "service", "localhost/zts", "dnsSuffix");
+
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ MockNodeFlavors nodeFlavors = new MockNodeFlavors();
+ ApplicationId appid = ApplicationId.from(TenantName.from("tenant"), ApplicationName.from("application"), InstanceName.from("default"));
+ Allocation allocation = new Allocation(appid, ClusterMembership.from("container/default/0/0", Version.fromString("1.2.3")), Generation.inital(), false); Flavor flavor = nodeFlavors.getFlavorOrThrow("default");
+ Node n = Node.create("ostkid", ImmutableSet.of("127.0.0.1"), new HashSet<>(), hostname, Optional.empty(), flavor, NodeType.tenant).with(allocation);
+ when(nodeRepository.getNode(eq(hostname))).thenReturn(Optional.of(n));
+ Zone zone = new Zone(Environment.dev, RegionName.from("us-north-1"));
+
+ IdentityDocumentGenerator identityDocumentGenerator = new IdentityDocumentGenerator(config, nodeRepository, zone, keyProvider);
+ String rawSignedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(hostname);
+
+
+ SignedIdentityDocument signedIdentityDocument = Utils.getMapper().readValue(rawSignedIdentityDocument, SignedIdentityDocument.class);
+
+ // Verify attributes
+ assertEquals(hostname, signedIdentityDocument.identityDocument.instanceHostname);
+ ProviderUniqueId expectedProviderUniqueId = new ProviderUniqueId("tenant", "application", "dev", "us-north-1", "default", "default", 0);
+ assertEquals(expectedProviderUniqueId, signedIdentityDocument.identityDocument.providerUniqueId);
+
+ // Validate signature
+ assertTrue("Message", InstanceValidator.isSignatureValid(Crypto.loadPublicKey(keyProvider.getPublicKey(0)), signedIdentityDocument.rawIdentityDocument, signedIdentityDocument.signature));
+
+ }
+
+ private AthenzProviderServiceConfig getAthenzProviderConfig(String domain, String service, String ztsUrl, String dnsSuffix) {
+ return new AthenzProviderServiceConfig(
+ new AthenzProviderServiceConfig.Builder()
+ .domain(domain)
+ .serviceName(service)
+ .port(PORT)
+ .keyPathPrefix("dummy-path")
+ .certDnsSuffix(dnsSuffix)
+ .ztsUrl(ztsUrl)
+ .athenzPrincipalHeaderName("INSERT PRINCIPAL HEADER NAME HERE")
+ .apiPath("/"));
+
+ }
+ private static boolean getStatus(HttpClient client) {
+ try {
+ HttpResponse response = client.execute(new HttpGet("https://localhost:" + PORT + "/status.html"));
+ return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
+ } catch (Exception e) {
+ log.log(LogLevel.INFO, "Status.html failed: " + e);
+ return false;
+ }
+ }
+
+ private static void assertInstanceConfirmationSucceeds(HttpClient client, PrivateKey privateKey) throws IOException {
+ HttpPost httpPost = new HttpPost("https://localhost:" + PORT + "/");
+ httpPost.setEntity(createInstanceConfirmation(privateKey));
+ HttpResponse response = client.execute(httpPost);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(200));
+ }
+
+ private static CloseableHttpClient createHttpClient(String domain, String service)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
+ SSLContext sslContext = new SSLContextBuilder()
+ .loadTrustMaterial(null, (certificateChain, ignoredAuthType) ->
+ certificateChain[0].getSubjectX500Principal().getName().equals("CN=" + domain + "." + service))
+ .build();
+
+ return HttpClients.custom()
+ .setSslcontext(sslContext)
+ .setSSLHostnameVerifier(new NoopHostnameVerifier())
+ .build();
+ }
+
+ private static HttpEntity createInstanceConfirmation(PrivateKey privateKey) {
+ IdentityDocument identityDocument = new IdentityDocument(
+ new ProviderUniqueId(
+ "tenant", "application", "environment", "region", "instance", "cluster-id", 0),
+ "hostname", "instance-hostname", Instant.now());
+ try {
+ ObjectMapper mapper = Utils.getMapper();
+ String encodedIdentityDocument =
+ Base64.getEncoder().encodeToString(mapper.writeValueAsString(identityDocument).getBytes());
+ Signature sigGenerator = Signature.getInstance("SHA512withRSA");
+ sigGenerator.initSign(privateKey);
+ sigGenerator.update(encodedIdentityDocument.getBytes());
+ String signature = Base64.getEncoder().encodeToString(sigGenerator.sign());
+
+ InstanceConfirmation instanceConfirmation = new InstanceConfirmation(
+ "provider", "domain", "service",
+ new SignedIdentityDocument(encodedIdentityDocument, signature, 0, identityDocument.providerUniqueId.asString(), "dnssuffix", "service", "localhost/zts",1));
+ return new StringEntity(mapper.writeValueAsString(instanceConfirmation));
+ } catch (JsonProcessingException
+ | NoSuchAlgorithmException
+ | UnsupportedEncodingException
+ | SignatureException
+ | InvalidKeyException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class DummyKeyProvider implements KeyProvider {
+
+ @Override
+ public String getPrivateKey(int version) {
+ return "INSERT PRIV KEY";
+ }
+
+ @Override
+ public String getPublicKey(int version) {
+ return "INSERT PUB KEY";
+ }
+ }
+
+ private static class AutoGeneratedKeyProvider implements KeyProvider {
+
+ private final String publicKey;
+ private final String privateKey;
+
+ public AutoGeneratedKeyProvider() throws IOException, NoSuchAlgorithmException {
+ KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
+ rsa.initialize(2048);
+ KeyPair keyPair = rsa.genKeyPair();
+ publicKey = pemEncode("RSA PUBLIC KEY", keyPair.getPublic());
+ privateKey = pemEncode("RSA PRIVATE KEY", keyPair.getPrivate());
+ }
+
+ private String pemEncode(String description, Key key) throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
+ pemWriter.writeObject(key);
+ pemWriter.flush();
+ return stringWriter.toString();
+
+ }
+
+ @Override
+ public String getPrivateKey(int version) {
+ return privateKey;
+ }
+
+ @Override
+ public String getPublicKey(int version) {
+ return publicKey;
+ }
+ }
+}
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java
new file mode 100644
index 00000000000..45cb82a0c0a
--- /dev/null
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java
@@ -0,0 +1,115 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.athenz.instanceproviderservice;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * @author bjorncs
+ */
+public class ScheduledExecutorServiceMock implements ScheduledExecutorService {
+
+ private Runnable runnable;
+
+ public Optional<Runnable> getCommand() {
+ return Optional.ofNullable(runnable);
+ }
+
+ @Override
+ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
+ if (runnable != null) {
+ throw new IllegalStateException("Can only register single command");
+ }
+ runnable = Objects.requireNonNull(command);
+ return null;
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void shutdown() {
+ // do nothing
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+ return true;
+ }
+
+ @Override
+ public <T> Future<T> submit(Callable<T> task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> Future<T> submit(Runnable task, T result) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Future<?> submit(Runnable task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java
index 8e2c6b8532d..ddd998c8816 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java
@@ -7,6 +7,7 @@ import com.yahoo.config.model.producer.AbstractConfigProducer;
import com.yahoo.container.ComponentsConfig;
import com.yahoo.container.QrConfig;
import com.yahoo.container.core.ContainerHttpConfig;
+import com.yahoo.container.core.identity.IdentityConfig;
import com.yahoo.container.jdisc.ContainerMbusConfig;
import com.yahoo.container.jdisc.JdiscBindingsConfig;
import com.yahoo.search.config.QrStartConfig;
@@ -48,7 +49,8 @@ public class Container extends AbstractService implements
ComponentsConfig.Producer,
JdiscBindingsConfig.Producer,
ContainerHttpConfig.Producer,
- ContainerMbusConfig.Producer {
+ ContainerMbusConfig.Producer,
+ IdentityConfig.Producer {
public static final int BASEPORT = Defaults.getDefaults().vespaWebServicePort();
public static final String SINGLENODE_CONTAINER_SERVICESPEC = "default_singlenode_container";
@@ -394,6 +396,18 @@ public class Container extends AbstractService implements
this.httpServerEnabled = httpServerEnabled;
}
+ @Override
+ public void getConfig(IdentityConfig.Builder builder) {
+ if(parent instanceof ContainerCluster) {
+ ContainerCluster containerCluster = (ContainerCluster) parent;
+ Optional<Identity> identity = containerCluster.getIdentity();
+ identity.ifPresent(id -> {
+ builder.serviceName(id.getService());
+ builder.domain(id.getDomain());
+ });
+ }
+ }
+
public static final class PortOverride {
public final ComponentSpecification serverId;
public final int port;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java
index 4eb611a1dbe..45a50af8f3b 100755
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java
@@ -7,11 +7,11 @@ import com.yahoo.cloud.config.RoutingProviderConfig;
import com.yahoo.component.ComponentId;
import com.yahoo.component.ComponentSpecification;
import com.yahoo.config.FileReference;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ComponentInfo;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.docproc.DocprocConfig;
import com.yahoo.config.docproc.SchemamappingConfig;
-import com.yahoo.config.application.api.ApplicationMetaData;
-import com.yahoo.config.application.api.ComponentInfo;
import com.yahoo.config.model.ApplicationConfigProducerRoot;
import com.yahoo.config.model.producer.AbstractConfigProducer;
import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
@@ -65,8 +65,8 @@ import com.yahoo.vespa.model.container.component.ConfigProducerGroup;
import com.yahoo.vespa.model.container.component.DiscBindingsConfigGenerator;
import com.yahoo.vespa.model.container.component.FileStatusHandlerComponent;
import com.yahoo.vespa.model.container.component.Handler;
-import com.yahoo.vespa.model.container.component.SimpleComponent;
import com.yahoo.vespa.model.container.component.Servlet;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
import com.yahoo.vespa.model.container.component.StatisticsComponent;
import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
@@ -82,7 +82,6 @@ import com.yahoo.vespa.model.content.Content;
import com.yahoo.vespa.model.search.AbstractSearchCluster;
import com.yahoo.vespa.model.utils.FileSender;
import com.yahoo.vespaclient.config.FeederConfig;
-
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
@@ -187,6 +186,8 @@ public final class ContainerCluster
private Optional<String> hostClusterId = Optional.empty();
private Optional<Integer> memoryPercentage = Optional.empty();
+ private Identity identity;
+
private static class AcceptAllVerifier implements ContainerClusterVerifier {
@Override
public boolean acceptComponent(Component component) { return true; }
@@ -238,6 +239,9 @@ public final class ContainerCluster
public void setZone(Zone zone) {
this.zone = zone;
}
+ public Zone getZone() {
+ return zone;
+ }
public void addMetricStateHandler() {
Handler<AbstractConfigProducer<?>> stateHandler = new Handler<>(
@@ -836,6 +840,15 @@ public final class ContainerCluster
*/
public Optional<Integer> getMemoryPercentage() { return memoryPercentage; }
+ public Optional<Identity> getIdentity() {
+ return Optional.ofNullable(identity);
+ }
+
+ public void setIdentity(Identity identity) {
+ this.identity = identity;
+ addSimpleComponent("com.yahoo.container.jdisc.athenz.AthenzIdentityProvider");
+ }
+
@Override
public String toString() {
return "container cluster '" + getName() + "'";
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java b/config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java
new file mode 100644
index 00000000000..e872cb7a73b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java
@@ -0,0 +1,22 @@
+package com.yahoo.vespa.model.container;
+
+/**
+ * @author mortent
+ */
+public class Identity {
+ private final String domain;
+ private final String service;
+
+ public Identity(String domain, String service) {
+ this.domain = domain;
+ this.service = service;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public String getService() {
+ return service;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java
index c3361426d64..f7caa0c734c 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java
@@ -141,6 +141,9 @@ public class ConfigserverCluster extends AbstractConfigProducer
if (options.dockerVespaBaseImage().isPresent()) {
builder.dockerVespaBaseImage(options.dockerVespaBaseImage().get());
}
+ if (options.serviceProviderEndpoint().isPresent()) {
+ builder.serviceProviderEndpoint(options.serviceProviderEndpoint().get());
+ }
}
private String[] getConfigModelPluginDirs() {
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java
index 8acab12414d..ae29a912c11 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java
@@ -43,4 +43,5 @@ public interface CloudConfigOptions {
Optional<Integer> numParallelTenantLoaders();
Optional<String> dockerRegistry();
Optional<String> dockerVespaBaseImage();
+ Optional<String> serviceProviderEndpoint();
}
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 49229fcc87a..0d221b57748 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
@@ -4,16 +4,16 @@ package com.yahoo.vespa.model.container.xml;
import com.google.common.collect.ImmutableList;
import com.yahoo.component.Version;
import com.yahoo.config.application.Xml;
-import com.yahoo.config.model.ConfigModelContext;
import com.yahoo.config.application.api.ApplicationPackage;
import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.ConfigModelContext;
import com.yahoo.config.model.application.provider.IncludeDirs;
import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
import com.yahoo.config.model.builder.xml.ConfigModelId;
import com.yahoo.config.model.producer.AbstractConfigProducer;
import com.yahoo.config.provision.Capacity;
-import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.NodeType;
import com.yahoo.container.jdisc.config.MetricDefaultsConfig;
@@ -22,14 +22,15 @@ import com.yahoo.text.XML;
import com.yahoo.vespa.defaults.Defaults;
import com.yahoo.vespa.model.AbstractService;
import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.container.Identity;
import com.yahoo.vespa.model.builder.xml.dom.DomClientProviderBuilder;
import com.yahoo.vespa.model.builder.xml.dom.DomComponentBuilder;
import com.yahoo.vespa.model.builder.xml.dom.DomFilterBuilder;
import com.yahoo.vespa.model.builder.xml.dom.DomHandlerBuilder;
import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
import com.yahoo.vespa.model.builder.xml.dom.NodesSpecification;
-import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
import com.yahoo.vespa.model.builder.xml.dom.ServletBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
import com.yahoo.vespa.model.builder.xml.dom.chains.docproc.DomDocprocChainsBuilder;
import com.yahoo.vespa.model.builder.xml.dom.chains.processing.DomProcessingBuilder;
import com.yahoo.vespa.model.builder.xml.dom.chains.search.DomSearchChainsBuilder;
@@ -52,12 +53,15 @@ import com.yahoo.vespa.model.container.search.QueryProfiles;
import com.yahoo.vespa.model.container.search.SemanticRules;
import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
import com.yahoo.vespa.model.container.xml.document.DocumentFactoryBuilder;
-
import com.yahoo.vespa.model.content.StorageGroup;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -108,7 +112,6 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
ContainerCluster cluster = createContainerCluster(spec, modelContext);
addClusterContent(cluster, spec, modelContext);
addBundlesForPlatformComponents(cluster);
-
model.setCluster(cluster);
}
@@ -157,6 +160,9 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
addServerProviders(spec, cluster);
addLegacyFilters(spec, cluster); // TODO: Remove for Vespa 7
+ // Athenz copper argos
+ addIdentity(spec, cluster);
+
//TODO: overview handler, see DomQrserverClusterBuilder
}
@@ -683,6 +689,16 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
}
}
+ private void addIdentity(Element element, ContainerCluster cluster) {
+ Element identityElement = XML.getChild(element, "identity");
+ if(identityElement != null) {
+ String domain = XML.getValue(XML.getChild(identityElement, "domain"));
+ String service = XML.getValue(XML.getChild(identityElement, "service"));
+ Identity identity = new Identity(domain.trim(), service.trim());
+ cluster.setIdentity(identity);
+ }
+ }
+
/**
* Disallow renderers named "DefaultRenderer" or "JsonRenderer"
*/
diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc
index 85219bdb46a..af9b89b8553 100644
--- a/config-model/src/main/resources/schema/containercluster.rnc
+++ b/config-model/src/main/resources/schema/containercluster.rnc
@@ -7,7 +7,8 @@ ContainerCluster = element container | jdisc {
ContainerServices &
DocumentBinding* &
Aliases? &
- NodesOfContainerCluster?
+ NodesOfContainerCluster? &
+ Identity?
}
ContainerServices =
@@ -225,3 +226,7 @@ DocumentBinding = element document {
attribute bundle { xsd:NCName }
}
+Identity = element identity {
+ element domain { xsd:NCName } &
+ element service { xsd:NCName }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java
index e2ed57251d9..4c09fd8d713 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java
@@ -62,6 +62,10 @@ public class DedicatedAdminV4Test {
" <metric name='nginx.upstreams.down.last' output-name='nginx.upstreams.down'/>" +
" </consumer>" +
" </metric-consumers>" +
+ " <identity>" +
+ " <domain>mydomain</domain>" +
+ " <service>myservice</service>" +
+ " </identity>" +
" </admin>" +
"</services>";
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java b/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java
index 663a656347d..fe5cd3dc029 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java
@@ -115,6 +115,9 @@ public class TestOptions implements CloudConfigOptions {
@Override
public Optional<String> dockerVespaBaseImage() { return Optional.empty(); }
+ @Override
+ public Optional<String> serviceProviderEndpoint() { return Optional.empty(); }
+
public TestOptions numParallelTenantLoaders(int numLoaders) {
this.numParallelTenantLoaders = Optional.of(numLoaders);
return this;
diff --git a/config-model/src/test/schema-test-files/services.xml b/config-model/src/test/schema-test-files/services.xml
index bf2a924e62d..380ce7f5a3d 100644
--- a/config-model/src/test/schema-test-files/services.xml
+++ b/config-model/src/test/schema-test-files/services.xml
@@ -36,6 +36,10 @@
</config>
<jdisc id='qrsCluster_1' version='1.0'>
+ <identity>
+ <domain>mydomain</domain>
+ <service>myservice</service>
+ </identity>
<rest-api path="jersey1">
<components bundle="my-bundle" />
<components bundle="other-bundle">
diff --git a/configdefinitions/src/vespa/configserver.def b/configdefinitions/src/vespa/configserver.def
index e3997805c3e..fe71e33bb03 100644
--- a/configdefinitions/src/vespa/configserver.def
+++ b/configdefinitions/src/vespa/configserver.def
@@ -41,3 +41,6 @@ useVespaVersionInRequest bool default=false
# Docker config
dockerRegistry string default=""
dockerVespaBaseImage string default=""
+
+# Athenz config
+serviceProviderEndpoint string default="" \ No newline at end of file
diff --git a/configserver/src/main/resources/configserver-app/services.xml b/configserver/src/main/resources/configserver-app/services.xml
index 7aec27a96f5..db95cb17530 100644
--- a/configserver/src/main/resources/configserver-app/services.xml
+++ b/configserver/src/main/resources/configserver-app/services.xml
@@ -134,5 +134,7 @@
<server port="19071" id="configserver" />
<preprocess:include file='hosted-vespa/http-server.xml' required='false' />
</http>
+
+ <preprocess:include file='athenz-identity-provider.xml' required='false' />
</jdisc>
</services>
diff --git a/container-core/src/main/java/com/yahoo/container/core/identity/package-info.java b/container-core/src/main/java/com/yahoo/container/core/identity/package-info.java
new file mode 100644
index 00000000000..f0d45f003c2
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/core/identity/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * @author mortent
+ */
+@ExportPackage
+package com.yahoo.container.core.identity;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/container-core/src/main/resources/configdefinitions/identity.def b/container-core/src/main/resources/configdefinitions/identity.def
new file mode 100644
index 00000000000..0f5080632b2
--- /dev/null
+++ b/container-core/src/main/resources/configdefinitions/identity.def
@@ -0,0 +1,4 @@
+namespace=container.core.identity
+
+domain string
+serviceName string
diff --git a/container-dev/pom.xml b/container-dev/pom.xml
index 8bb759bf867..c6cbeacd2c0 100644
--- a/container-dev/pom.xml
+++ b/container-dev/pom.xml
@@ -111,6 +111,14 @@
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</exclusion>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-auth-core</artifactId>
+ </exclusion>
</exclusions>
</dependency>
<dependency>
diff --git a/container-disc/pom.xml b/container-disc/pom.xml
index 44afe74459d..5409d9b1a40 100644
--- a/container-disc/pom.xml
+++ b/container-disc/pom.xml
@@ -130,6 +130,17 @@
<scope>provided</scope>
</dependency>
<!-- end WARNING -->
+ <!-- Athenz -->
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.athenz</groupId>
+ <artifactId>athenz-auth-core</artifactId>
+ <version>${athenz.version}</version>
+ </dependency>
</dependencies>
<properties>
<!-- These versions must be the ones used by the current jersey version (see vespa_jersey2/target/dependency).
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
new file mode 100644
index 00000000000..f67afdfc458
--- /dev/null
+++ b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java
@@ -0,0 +1,121 @@
+package com.yahoo.container.jdisc.athenz;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.Inject;
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.container.core.identity.IdentityConfig;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.operator.OperatorCreationException;
+
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * @author mortent
+ */
+public final class AthenzIdentityProvider extends AbstractComponent {
+
+ private InstanceIdentity instanceIdentity;
+
+ private final String athenzUrl;
+
+ private final String dnsSuffix;
+ private final String providerUniqueId;
+ private final String providerServiceName;
+
+ @Inject
+ public AthenzIdentityProvider(IdentityConfig config, ConfigserverConfig configserverConfig) throws IOException {
+ this(config, new ServiceProviderApi(configserverConfig.serviceProviderEndpoint()), new AthenzService());
+ }
+
+ // Test only
+ public AthenzIdentityProvider(IdentityConfig config, ServiceProviderApi serviceProviderApi, AthenzService athenzService) throws IOException {
+ KeyPair keyPair = createKeyPair();
+ String signedIdentityDocument = serviceProviderApi.getSignedIdentityDocument();
+ this.athenzUrl = getZtsEndpoint(signedIdentityDocument);
+ dnsSuffix = getDnsSuffix(signedIdentityDocument);
+ providerUniqueId = getProviderUniqueId(signedIdentityDocument);
+ providerServiceName = getProviderServiceName(signedIdentityDocument);
+
+ InstanceRegisterInformation instanceRegisterInformation = new InstanceRegisterInformation(
+ providerServiceName,
+ config.domain(),
+ config.serviceName(),
+ signedIdentityDocument,
+ createCSR(keyPair, config),
+ true
+ );
+ instanceIdentity = athenzService.sendInstanceRegisterRequest(instanceRegisterInformation, athenzUrl);
+ }
+
+ private String getProviderUniqueId(String signedIdentityDocument) throws IOException {
+ return getJsonNode(signedIdentityDocument, "provider-unique-id");
+ }
+
+ private String getDnsSuffix(String signedIdentityDocument) throws IOException {
+ return getJsonNode(signedIdentityDocument, "dns-suffix");
+ }
+
+ private String getProviderServiceName(String signedIdentityDocument) throws IOException {
+ return getJsonNode(signedIdentityDocument, "provider-service");
+ }
+
+ private String getZtsEndpoint(String signedIdentityDocument) throws IOException {
+ return getJsonNode(signedIdentityDocument, "zts-endpoint");
+ }
+
+ private String getJsonNode(String jsonString, String path) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonString);
+ return jsonNode.get(path).asText();
+ }
+
+ private KeyPair createKeyPair() {
+ try {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ return kpg.generateKeyPair();
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String createCSR(KeyPair keyPair, IdentityConfig identityConfig) throws IOException {
+
+ try {
+ // Add SAN dnsname <service>.<domain-with-dashes>.<provider-dnsname-suffix>
+ // and SAN dnsname <provider-unique-instance-id>.instanceid.athenz.<provider-dnsname-suffix>
+ GeneralName[] sanDnsNames = new GeneralName[]{
+ new GeneralName(GeneralName.dNSName, String.format("%s.%s.%s",
+ identityConfig.serviceName(),
+ identityConfig.domain().replace(".", "-"),
+ dnsSuffix)),
+ new GeneralName(GeneralName.dNSName, String.format("%s.instanceid.athenz.%s",
+ providerUniqueId,
+ dnsSuffix))
+ };
+
+ return Crypto.generateX509CSR(keyPair.getPrivate(),
+ keyPair.getPublic(),
+ String.format("CN=%s.%s", identityConfig.domain(), identityConfig.serviceName()),
+ sanDnsNames);
+ } catch (OperatorCreationException e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String getNToken() {
+ return instanceIdentity.getServiceToken();
+ }
+
+ public String getX509Cert() {
+ return instanceIdentity.getX509Certificate();
+ }
+}
+
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzService.java b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzService.java
new file mode 100644
index 00000000000..cc5fa6a889b
--- /dev/null
+++ b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzService.java
@@ -0,0 +1,46 @@
+package com.yahoo.container.jdisc.athenz;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.jetty.http.HttpStatus;
+
+import java.io.IOException;
+
+/**
+ * @author mortent
+ */
+public class AthenzService {
+
+ /**
+ * Send instance register request to ZTS, get InstanceIdentity
+ *
+ * @param instanceRegisterInformation
+ */
+ InstanceIdentity sendInstanceRegisterRequest(InstanceRegisterInformation instanceRegisterInformation, String athenzUrl) {
+ try(CloseableHttpClient client = HttpClientBuilder.create().build()) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ System.out.println(objectMapper.writeValueAsString(instanceRegisterInformation));
+ HttpUriRequest postRequest = RequestBuilder.post()
+ .setUri(athenzUrl + "/instance")
+ .setEntity(new StringEntity(objectMapper.writeValueAsString(instanceRegisterInformation), ContentType.APPLICATION_JSON))
+ .build();
+ CloseableHttpResponse response = client.execute(postRequest);
+ if(HttpStatus.isSuccess(response.getStatusLine().getStatusCode())) {
+ return objectMapper.readValue(response.getEntity().getContent(), InstanceIdentity.class);
+ } else {
+ String s = EntityUtils.toString(response.getEntity());
+ System.out.println("s = " + s);
+ throw new RuntimeException(response.toString());
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceIdentity.java b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceIdentity.java
new file mode 100644
index 00000000000..45ef4c68d8e
--- /dev/null
+++ b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceIdentity.java
@@ -0,0 +1,55 @@
+package com.yahoo.container.jdisc.athenz;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Map;
+
+/**
+ * Used for deserializing response from ZTS
+ *
+ * @author mortent
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class InstanceIdentity {
+ @JsonProperty("attributes") private final Map<String, String> attributes;
+ @JsonProperty("provider") private final String provider;
+ @JsonProperty("name") private final String name;
+ @JsonProperty("instanceId") private final String instanceId;
+ @JsonProperty("x509Certificate") private final String x509Certificate;
+ @JsonProperty("x509CertificateSigner") private final String x509CertificateSigner;
+ @JsonProperty("sshCertificate") private final String sshCertificate;
+ @JsonProperty("sshCertificateSigner") private final String sshCertificateSigner;
+ @JsonProperty("serviceToken") private final String serviceToken;
+
+ public InstanceIdentity(
+ @JsonProperty("attributes") Map<String, String> attributes,
+ @JsonProperty("provider") String provider,
+ @JsonProperty("name") String name,
+ @JsonProperty("instanceId") String instanceId,
+ @JsonProperty("x509Certificate") String x509Certificate,
+ @JsonProperty("x509CertificateSigner") String x509CertificateSigner,
+ @JsonProperty("sshCertificate") String sshCertificate,
+ @JsonProperty("sshCertificateSigner") String sshCertificateSigner,
+ @JsonProperty("serviceToken") String serviceToken) {
+ this.attributes = attributes;
+ this.provider = provider;
+ this.name = name;
+ this.instanceId = instanceId;
+ this.x509Certificate = x509Certificate;
+ this.x509CertificateSigner = x509CertificateSigner;
+ this.sshCertificate = sshCertificate;
+ this.sshCertificateSigner = sshCertificateSigner;
+ this.serviceToken = serviceToken;
+ }
+
+ String getX509Certificate() {
+ return x509Certificate;
+ }
+
+ String getServiceToken() {
+ return serviceToken;
+ }
+}
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceRegisterInformation.java b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceRegisterInformation.java
new file mode 100644
index 00000000000..2fa3c598c58
--- /dev/null
+++ b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/InstanceRegisterInformation.java
@@ -0,0 +1,38 @@
+package com.yahoo.container.jdisc.athenz;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Used for serializing request to ZTS
+ *
+ * @author mortent
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class InstanceRegisterInformation {
+ @JsonProperty("provider")
+ private final String provider;
+ @JsonProperty("domain")
+ private final String domain;
+ @JsonProperty("service")
+ private final String service;
+ @JsonProperty("attestationData")
+ private final String attestationData;
+ @JsonProperty("ssh")
+ private final String ssh = null; // Not needed
+ @JsonProperty("csr")
+ private final String csr;
+ @JsonProperty("token")
+ private final boolean token;
+
+ public InstanceRegisterInformation(String provider, String domain, String service, String attestationData, String csr, boolean token) {
+ this.provider = provider;
+ this.domain = domain;
+ this.service = service;
+ this.attestationData = attestationData;
+ this.csr = csr;
+ this.token = token;
+ }
+}
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/ServiceProviderApi.java b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/ServiceProviderApi.java
new file mode 100644
index 00000000000..0b417a4d440
--- /dev/null
+++ b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/ServiceProviderApi.java
@@ -0,0 +1,47 @@
+package com.yahoo.container.jdisc.athenz;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.jetty.http.HttpStatus;
+
+import java.io.IOException;
+
+/**
+ * @author mortent
+ */
+public class ServiceProviderApi {
+
+ private final String providerEndpoint;
+
+ public ServiceProviderApi(String providerEndpoint) {
+ this.providerEndpoint = providerEndpoint;
+ }
+
+
+ /**
+ * Get signed identity document from config server
+ *
+ * @return
+ */
+ String getSignedIdentityDocument() {
+
+ // TODO Use client side auth to establish trusted secure channel
+ try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
+
+ CloseableHttpResponse idDocResponse = httpClient.execute(RequestBuilder.get().setUri(providerEndpoint + "/identity-document").build());
+ if (HttpStatus.isSuccess(idDocResponse.getStatusLine().getStatusCode())) {
+ return EntityUtils.toString(idDocResponse.getEntity());
+ } else {
+ // make sure we have retried a few times (AND logged) before giving up
+ throw new RuntimeException("Failed to initialize Athenz instance provider");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/container-disc/src/test/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProviderTest.java b/container-disc/src/test/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProviderTest.java
new file mode 100644
index 00000000000..4b351f1d2c0
--- /dev/null
+++ b/container-disc/src/test/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProviderTest.java
@@ -0,0 +1,47 @@
+package com.yahoo.container.jdisc.athenz;
+
+import com.yahoo.container.core.identity.IdentityConfig;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author mortent
+ */
+public class AthenzIdentityProviderTest {
+
+ @Test
+ public void ntoken_fetched_on_init() throws IOException {
+ IdentityConfig config = new IdentityConfig(new IdentityConfig.Builder().serviceName("tenantService").domain("tenantDomain"));
+ ServiceProviderApi serviceProviderApi = mock(ServiceProviderApi.class);
+ AthenzService athenzService = mock(AthenzService.class);
+
+ when(serviceProviderApi.getSignedIdentityDocument()).thenReturn(getIdentityDocument());
+ when(athenzService.sendInstanceRegisterRequest(any(), anyString())).thenReturn(
+ new InstanceIdentity(null,null,null,null,null,null, null, null, "TOKEN"));
+
+ AthenzIdentityProvider identityProvider = new AthenzIdentityProvider(config, serviceProviderApi, athenzService);
+
+ Assert.assertEquals("TOKEN", identityProvider.getNToken());
+ }
+
+ private String getIdentityDocument() {
+ return "{\n" +
+ " \"identity-document\": \"eyJwcm92aWRlci11bmlxdWUtaWQiOnsidGVuYW50IjoidGVuYW50IiwiYXBwbGljYXRpb24iOiJhcHBsaWNhdGlvbiIsImVudmlyb25tZW50IjoiZGV2IiwicmVnaW9uIjoidXMtbm9ydGgtMSIsImluc3RhbmNlIjoiZGVmYXVsdCIsImNsdXN0ZXItaWQiOiJkZWZhdWx0IiwiY2x1c3Rlci1pbmRleCI6MH0sImNvbmZpZ3NlcnZlci1ob3N0bmFtZSI6ImxvY2FsaG9zdCIsImluc3RhbmNlLWhvc3RuYW1lIjoieC55LmNvbSIsImNyZWF0ZWQtYXQiOjE1MDg3NDgyODUuNzQyMDAwMDAwfQ==\",\n" +
+ " \"signature\": \"kkEJB/98cy1FeXxzSjtvGH2a6BFgZu/9/kzCcAqRMZjENxnw5jyO1/bjZVzw2Sz4YHPsWSx2uxb32hiQ0U8rMP0zfA9nERIalSP0jB/hMU8laezGhdpk6VKZPJRC6YKAB9Bsv2qUIfMsSxkMqf66GUvjZAGaYsnNa2yHc1jIYHOGMeJO+HNPYJjGv26xPfAOPIKQzs3RmKrc3FoweTCsIwm5oblqekdJvVWYe0obwlOSB5uwc1zpq3Ie1QBFtJRuCGMVHg1pDPxXKBHLClGIrEvzLmICy6IRdHszSO5qiwujUD7sbrbM0sB/u0cYucxbcsGRUmBvme3UAw2mW9POVQ==\",\n" +
+ " \"signing-key-version\": 0,\n" +
+ " \"provider-unique-id\": \"tenant.application.dev.us-north-1.default.default.0\",\n" +
+ " \"dns-suffix\": \"dnsSuffix\",\n" +
+ " \"provider-service\": \"service\",\n" +
+ " \"zts-endpoint\": \"localhost/zts\", \n" +
+ " \"document-version\": 1\n" +
+ "}";
+
+ }
+}
diff --git a/pom.xml b/pom.xml
index a61c8f0425e..c6e7168904a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -499,6 +499,11 @@
<version>${jackson2.version}</version>
</dependency>
<dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <version>${jackson2.version}</version>
+ </dependency>
+ <dependency>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-annotation</artifactId>
<version>1.4</version>
@@ -953,6 +958,7 @@
<module>application-deploy-plugin</module>
<module>application-model</module>
<module>application-preprocessor</module>
+ <module>athenz-identity-provider-service</module>
<module>bundle-plugin-test</module>
<module>chain</module>
<module>clustercontroller-apps</module>
diff --git a/standalone-container/src/main/scala/com/yahoo/container/standalone/CloudConfigYinstVariables.scala b/standalone-container/src/main/scala/com/yahoo/container/standalone/CloudConfigYinstVariables.scala
index 0a888dc1208..009de6fd541 100644
--- a/standalone-container/src/main/scala/com/yahoo/container/standalone/CloudConfigYinstVariables.scala
+++ b/standalone-container/src/main/scala/com/yahoo/container/standalone/CloudConfigYinstVariables.scala
@@ -38,6 +38,7 @@ class CloudConfigYinstVariables extends CloudConfigOptions {
override val numParallelTenantLoaders = optionalYinstVar[java.lang.Integer]("num_parallel_tenant_loaders")
override val dockerRegistry = optionalYinstVar[java.lang.String]("docker_registry")
override val dockerVespaBaseImage = optionalYinstVar[java.lang.String]("docker_vespa_base_image")
+ override val serviceProviderEndpoint = optionalYinstVar[java.lang.String]("service_provider_endpoint")
}
object CloudConfigYinstVariables {