diff options
author | Morten Tokle <morten.tokle@gmail.com> | 2017-11-06 12:28:00 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-11-06 12:28:00 +0100 |
commit | dfcd99818b4b0151ad226df03548915f9dddd9fb (patch) | |
tree | b2a9e280d7e14fff88b0b1e0cdb0990b115b1e48 | |
parent | c8e3ec213f31627e34a3dc0fc28429aefeebaf6e (diff) | |
parent | 03e895730cea1257941f4790438dc347571d9d65 (diff) |
Merge pull request #3988 from vespa-engine/freva/instance-validator
Freva/instance validator
8 files changed, 415 insertions, 247 deletions
diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml index bb82e40f827..26e24be526c 100644 --- a/athenz-identity-provider-service/pom.xml +++ b/athenz-identity-provider-service/pom.xml @@ -82,6 +82,12 @@ <version>${project.version}</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-model-api</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> <!-- TEST --> <dependency> 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 index 74b697fb004..26a88896fb9 100644 --- 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 @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.athenz.instanceproviderservice; import com.google.inject.Inject; import com.yahoo.component.AbstractComponent; +import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.Zone; import com.yahoo.jdisc.http.SecretStore; @@ -48,25 +49,39 @@ public class AthenzInstanceProviderService extends AbstractComponent { private final Server jetty; @Inject - public AthenzInstanceProviderService(AthenzProviderServiceConfig config, NodeRepository nodeRepository, Zone zone, SecretStore secretStore) { + public AthenzInstanceProviderService(AthenzProviderServiceConfig config, SuperModelProvider superModelProvider, + NodeRepository nodeRepository, Zone zone, SecretStore secretStore) { this(config, new SecretStoreKeyProvider(secretStore, getZoneConfig(config, zone).secretName()), Executors.newSingleThreadScheduledExecutor(), - nodeRepository, zone, new AthenzCertificateClient(config, getZoneConfig(config, zone))); + superModelProvider, nodeRepository, zone, new AthenzCertificateClient(config, getZoneConfig(config, zone)), createSslContextFactory()); + } + + private AthenzInstanceProviderService(AthenzProviderServiceConfig config, + KeyProvider keyProvider, + ScheduledExecutorService scheduler, + SuperModelProvider superModelProvider, + NodeRepository nodeRepository, + Zone zone, + CertificateClient certificateClient, + SslContextFactory sslContextFactory) { + this(config, scheduler, zone, sslContextFactory, + new InstanceValidator(keyProvider, superModelProvider), + new IdentityDocumentGenerator(config, getZoneConfig(config, zone), nodeRepository, zone, keyProvider), + new AthenzCertificateUpdater( + certificateClient, sslContextFactory, keyProvider, config, getZoneConfig(config, zone))); } AthenzInstanceProviderService(AthenzProviderServiceConfig config, - KeyProvider keyProvider, ScheduledExecutorService scheduler, - NodeRepository nodeRepository, Zone zone, - CertificateClient certificateClient) { + SslContextFactory sslContextFactory, + InstanceValidator instanceValidator, + IdentityDocumentGenerator identityDocumentGenerator, + AthenzCertificateUpdater reloader) { // TODO: Enable for all systems. Currently enabled for CD system only if (SystemName.cd.equals(zone.system())) { this.scheduler = scheduler; - SslContextFactory sslContextFactory = createSslContextFactory(); - this.jetty = createJettyServer( - config, keyProvider, sslContextFactory, nodeRepository, zone); - AthenzCertificateUpdater reloader = - new AthenzCertificateUpdater(certificateClient, sslContextFactory, keyProvider, config, getZoneConfig(config, zone)); + this.jetty = createJettyServer(config, sslContextFactory, instanceValidator, identityDocumentGenerator); + // TODO Configurable update frequency scheduler.scheduleAtFixedRate(reloader, 0, 1, TimeUnit.DAYS); try { @@ -81,22 +96,19 @@ public class AthenzInstanceProviderService extends AbstractComponent { } private static Server createJettyServer(AthenzProviderServiceConfig config, - KeyProvider keyProvider, SslContextFactory sslContextFactory, - NodeRepository nodeRepository, - Zone zone) { + InstanceValidator instanceValidator, + IdentityDocumentGenerator identityDocumentGenerator) { Server server = new Server(); ServerConnector connector = new ServerConnector(server, sslContextFactory); connector.setPort(config.port()); server.addConnector(connector); ServletHandler handler = new ServletHandler(); - InstanceConfirmationServlet instanceConfirmationServlet = - new InstanceConfirmationServlet(new InstanceValidator(keyProvider)); + InstanceConfirmationServlet instanceConfirmationServlet = new InstanceConfirmationServlet(instanceValidator); handler.addServletWithMapping(new ServletHolder(instanceConfirmationServlet), config.apiPath() + "/instance"); - IdentityDocumentServlet identityDocumentServlet = - new IdentityDocumentServlet(new IdentityDocumentGenerator(config, getZoneConfig(config, zone), nodeRepository, zone, keyProvider)); + IdentityDocumentServlet identityDocumentServlet = new IdentityDocumentServlet(identityDocumentGenerator); handler.addServletWithMapping(new ServletHolder(identityDocumentServlet), config.apiPath() + "/identity-document"); handler.addServletWithMapping(StatusServlet.class, "/status.html"); @@ -110,7 +122,7 @@ public class AthenzInstanceProviderService extends AbstractComponent { return config.zones(key); } - private static SslContextFactory createSslContextFactory() { + static SslContextFactory createSslContextFactory() { try { SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setWantClientAuth(true); @@ -122,7 +134,7 @@ public class AthenzInstanceProviderService extends AbstractComponent { } } - private static class AthenzCertificateUpdater implements Runnable { + static class AthenzCertificateUpdater implements Runnable { // TODO Make expiry a configuration parameter private static final TemporalAmount EXPIRY_TIME = Duration.ofDays(30); @@ -134,11 +146,11 @@ public class AthenzInstanceProviderService extends AbstractComponent { private final AthenzProviderServiceConfig config; private final AthenzProviderServiceConfig.Zones zoneConfig; - private AthenzCertificateUpdater(CertificateClient certificateClient, - SslContextFactory sslContextFactory, - KeyProvider keyProvider, - AthenzProviderServiceConfig config, - AthenzProviderServiceConfig.Zones zoneConfig) { + AthenzCertificateUpdater(CertificateClient certificateClient, + SslContextFactory sslContextFactory, + KeyProvider keyProvider, + AthenzProviderServiceConfig config, + AthenzProviderServiceConfig.Zones zoneConfig) { this.certificateClient = certificateClient; this.sslContextFactory = sslContextFactory; this.keyProvider = keyProvider; @@ -179,7 +191,7 @@ public class AthenzInstanceProviderService extends AbstractComponent { log.log(LogLevel.INFO, "Deconstructing Athenz provider service"); if(scheduler != null) scheduler.shutdown(); - if(jetty !=null) + if(jetty != null) jetty.stop(); if (scheduler != null && !scheduler.awaitTermination(1, TimeUnit.MINUTES)) { log.log(LogLevel.ERROR, "Failed to stop certificate updater"); 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 index 8d76300c2bb..427f35c41d8 100644 --- 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 @@ -1,6 +1,10 @@ // 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.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.model.api.SuperModelProvider; +import com.yahoo.config.provision.ApplicationId; 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; @@ -12,6 +16,7 @@ import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.util.Base64; +import java.util.Optional; import java.util.logging.Logger; /** @@ -22,19 +27,29 @@ import java.util.logging.Logger; public class InstanceValidator { private static final Logger log = Logger.getLogger(InstanceValidator.class.getName()); + static final String SERVICE_PROPERTIES_DOMAIN_KEY = "identity.domain"; + static final String SERVICE_PROPERTIES_SERVICE_KEY = "identity.service"; private final KeyProvider keyProvider; + private final SuperModelProvider superModelProvider; - public InstanceValidator(KeyProvider keyProvider) { + public InstanceValidator(KeyProvider keyProvider, SuperModelProvider superModelProvider) { this.keyProvider = keyProvider; + this.superModelProvider = superModelProvider; } public boolean isValidInstance(InstanceConfirmation instanceConfirmation) { SignedIdentityDocument signedIdentityDocument = instanceConfirmation.signedIdentityDocument; ProviderUniqueId providerUniqueId = signedIdentityDocument.identityDocument.providerUniqueId; + ApplicationId applicationId = ApplicationId.from( + providerUniqueId.tenant, providerUniqueId.application, providerUniqueId.instance); + + if (! isSameIdentityAsInServicesXml(applicationId, instanceConfirmation.domain, instanceConfirmation.service)) { + return false; + } + log.log(LogLevel.INFO, () -> String.format("Validating instance %s.", providerUniqueId)); - PublicKey publicKey = keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion); - if (isSignatureValid(publicKey, signedIdentityDocument.rawIdentityDocument, signedIdentityDocument.signature)) { + if (isInstanceSignatureValid(instanceConfirmation)) { log.log(LogLevel.INFO, () -> String.format("Instance %s is valid.", providerUniqueId)); return true; } @@ -42,7 +57,14 @@ public class InstanceValidator { return false; } - public static boolean isSignatureValid(PublicKey publicKey, String rawIdentityDocument, String signature) { + boolean isInstanceSignatureValid(InstanceConfirmation instanceConfirmation) { + SignedIdentityDocument signedIdentityDocument = instanceConfirmation.signedIdentityDocument; + + PublicKey publicKey = keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion); + return isSignatureValid(publicKey, signedIdentityDocument.rawIdentityDocument, signedIdentityDocument.signature); + } + + static boolean isSignatureValid(PublicKey publicKey, String rawIdentityDocument, String signature) { try { Signature signatureVerifier = Signature.getInstance("SHA512withRSA"); signatureVerifier.initVerify(publicKey); @@ -52,4 +74,38 @@ public class InstanceValidator { throw new RuntimeException(e); } } + + // If/when we dont care about logging exactly whats wrong, this can be simplified + boolean isSameIdentityAsInServicesXml(ApplicationId applicationId, String domain, String service) { + Optional<ApplicationInfo> applicationInfo = superModelProvider.getSuperModel().getApplicationInfo(applicationId); + + if (!applicationInfo.isPresent()) { + log.info(String.format("Could not find application info for %s", applicationId.serializedForm())); + return false; + } + + Optional<ServiceInfo> matchingServiceInfo = applicationInfo.get() + .getModel() + .getHosts() + .stream() + .flatMap(hostInfo -> hostInfo.getServices().stream()) + .filter(serviceInfo -> serviceInfo.getProperty(SERVICE_PROPERTIES_DOMAIN_KEY).isPresent()) + .filter(serviceInfo -> serviceInfo.getProperty(SERVICE_PROPERTIES_SERVICE_KEY).isPresent()) + .findFirst(); + + if (!matchingServiceInfo.isPresent()) { + log.info(String.format("Application %s has not specified domain/service", applicationId.serializedForm())); + return false; + } + + String domainInConfig = matchingServiceInfo.get().getProperty(SERVICE_PROPERTIES_DOMAIN_KEY).get(); + String serviceInConfig = matchingServiceInfo.get().getProperty(SERVICE_PROPERTIES_SERVICE_KEY).get(); + if (!domainInConfig.equals(domain) || !serviceInConfig.equals(service)) { + log.warning(String.format("domain '%s' or service '%s' does not match the one in config for application %s", + domain, service, applicationId.serializedForm())); + return false; + } + + return true; + } } 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 index 20a56359eff..bf0746aee7e 100644 --- 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 @@ -1,22 +1,14 @@ // 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 athenz.shade.zts.jersey.repackaged.com.google.common.collect.ImmutableMap; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -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.InstanceName; -import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; -import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderService.AthenzCertificateUpdater; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.CertificateClient; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.IdentityDocumentGenerator; @@ -27,11 +19,6 @@ import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.Identity 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; @@ -53,13 +40,12 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.Test; import javax.net.ssl.SSLContext; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.math.BigInteger; -import java.security.InvalidKeyException; import java.security.KeyManagementException; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -68,7 +54,6 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; -import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Instant; @@ -76,16 +61,15 @@ import java.time.temporal.TemporalAmount; import java.util.Base64; import java.util.Calendar; import java.util.Date; -import java.util.HashSet; -import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; 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.Matchers.any; +import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -102,28 +86,35 @@ public class AthenzInstanceProviderServiceTest { public void provider_service_hosts_endpoint_secured_with_tls() throws Exception { String domain = "domain"; String service = "service"; + AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider(); PrivateKey privateKey = keyProvider.getPrivateKey(0); AthenzProviderServiceConfig config = getAthenzProviderConfig(domain, service, "vespa.dns.suffix", ZONE); - ScheduledExecutorServiceMock executor = new ScheduledExecutorServiceMock(); + SslContextFactory sslContextFactory = AthenzInstanceProviderService.createSslContextFactory(); + AthenzCertificateUpdater certificateUpdater = new AthenzCertificateUpdater( + new SelfSignedCertificateClient(keyProvider.getKeyPair(), config, getZoneConfig(config, ZONE)), + sslContextFactory, + keyProvider, + config, + getZoneConfig(config, ZONE)); + + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + when(executor.awaitTermination(anyLong(), any())).thenReturn(true); + + InstanceValidator instanceValidator = mock(InstanceValidator.class); + when(instanceValidator.isValidInstance(any())).thenReturn(true); - AthenzInstanceProviderService athenzInstanceProviderService = - new AthenzInstanceProviderService(config, - keyProvider, - executor, - mock(NodeRepository.class), - ZONE, - new SelfSignedCertificateClient(keyProvider.getKeyPair(), config, - getZoneConfig(config, ZONE))); + IdentityDocumentGenerator identityDocumentGenerator = mock(IdentityDocumentGenerator.class); + + AthenzInstanceProviderService athenzInstanceProviderService = new AthenzInstanceProviderService( + config, executor, ZONE, sslContextFactory, instanceValidator, identityDocumentGenerator, certificateUpdater); try (CloseableHttpClient client = createHttpClient(domain, service)) { - Runnable certificateRefreshCommand = executor.getCommand() - .orElseThrow(() -> new AssertionError("Command not present")); assertFalse(getStatus(client)); - certificateRefreshCommand.run(); + certificateUpdater.run(); assertTrue(getStatus(client)); assertInstanceConfirmationSucceeds(client, privateKey); - certificateRefreshCommand.run(); + certificateUpdater.run(); assertTrue(getStatus(client)); assertInstanceConfirmationSucceeds(client, privateKey); } finally { @@ -131,62 +122,7 @@ public class AthenzInstanceProviderServiceTest { } } - @Test - public void generates_valid_identity_document() throws Exception { - String hostname = "x.y.com"; - - 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); - Node n = Node.create("ostkid", - ImmutableSet.of("127.0.0.1"), - new HashSet<>(), - hostname, - Optional.empty(), - new MockNodeFlavors().getFlavorOrThrow("default"), - NodeType.tenant) - .with(allocation); - - NodeRepository nodeRepository = mock(NodeRepository.class); - when(nodeRepository.getNode(eq(hostname))).thenReturn(Optional.of(n)); - AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider(); - - String dnsSuffix = "vespa.dns.suffix"; - AthenzProviderServiceConfig athenzProviderConfig = getAthenzProviderConfig("domain", "service", dnsSuffix, ZONE); - IdentityDocumentGenerator identityDocumentGenerator = new IdentityDocumentGenerator( - athenzProviderConfig, - getZoneConfig(athenzProviderConfig, ZONE), - nodeRepository, - ZONE, - keyProvider); - String rawSignedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(hostname); - - - SignedIdentityDocument signedIdentityDocument = - Utils.getMapper().readValue(rawSignedIdentityDocument, SignedIdentityDocument.class); - - // Verify attributes - assertEquals(hostname, signedIdentityDocument.identityDocument.instanceHostname); - - String environment = "dev"; - String region = "us-north-1"; - String expectedZoneDnsSuffix = environment + "-" + region + "." + dnsSuffix; - assertEquals(expectedZoneDnsSuffix, signedIdentityDocument.dnsSuffix); - - ProviderUniqueId expectedProviderUniqueId = - new ProviderUniqueId("tenant", "application", environment, region, "default", "default", 0); - assertEquals(expectedProviderUniqueId, signedIdentityDocument.identityDocument.providerUniqueId); - - // Validate signature - assertTrue("Message", InstanceValidator.isSignatureValid(keyProvider.getPublicKey(0), - signedIdentityDocument.rawIdentityDocument, - signedIdentityDocument.signature)); - } - - private static AthenzProviderServiceConfig getAthenzProviderConfig(String domain, String service, String dnsSuffix, Zone zone) { + public static AthenzProviderServiceConfig getAthenzProviderConfig(String domain, String service, String dnsSuffix, Zone zone) { AthenzProviderServiceConfig.Zones.Builder zoneConfig = new AthenzProviderServiceConfig.Zones.Builder() .serviceName(service) @@ -205,7 +141,7 @@ public class AthenzInstanceProviderServiceTest { } - private AthenzProviderServiceConfig.Zones getZoneConfig(AthenzProviderServiceConfig config, Zone zone) { + public static AthenzProviderServiceConfig.Zones getZoneConfig(AthenzProviderServiceConfig config, Zone zone) { return config.zones(zone.environment().value() + "." + zone.region().value()); } @@ -264,23 +200,23 @@ public class AthenzInstanceProviderServiceTest { "localhost/zts", 1)); return new StringEntity(mapper.writeValueAsString(instanceConfirmation)); - } catch (JsonProcessingException - | NoSuchAlgorithmException - | UnsupportedEncodingException - | SignatureException - | InvalidKeyException e) { + } catch (Exception e) { throw new RuntimeException(e); } } - private static class AutoGeneratedKeyProvider implements KeyProvider { + public static class AutoGeneratedKeyProvider implements KeyProvider { private final KeyPair keyPair; - public AutoGeneratedKeyProvider() throws IOException, NoSuchAlgorithmException { - KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); - rsa.initialize(2048); - keyPair = rsa.genKeyPair(); + public AutoGeneratedKeyProvider() { + try { + KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); + rsa.initialize(2048); + keyPair = rsa.genKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } } @Override 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 deleted file mode 100644 index 45cb82a0c0a..00000000000 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java +++ /dev/null @@ -1,115 +0,0 @@ -// 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/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGeneratorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGeneratorTest.java new file mode 100644 index 00000000000..d77757374ce --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/IdentityDocumentGeneratorTest.java @@ -0,0 +1,98 @@ +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + + +import com.google.common.collect.ImmutableSet; +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.InstanceName; +import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.AutoGeneratedKeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; +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.junit.Test; + +import java.util.HashSet; +import java.util.Optional; + +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.getAthenzProviderConfig; +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.getZoneConfig; +import static org.junit.Assert.assertEquals; +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 valerijf + */ +public class IdentityDocumentGeneratorTest { + private static final Zone ZONE = new Zone(SystemName.cd, Environment.dev, RegionName.from("us-north-1")); + + @Test + public void generates_valid_identity_document() throws Exception { + String hostname = "x.y.com"; + + 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); + Node n = Node.create("ostkid", + ImmutableSet.of("127.0.0.1"), + new HashSet<>(), + hostname, + Optional.empty(), + new MockNodeFlavors().getFlavorOrThrow("default"), + NodeType.tenant) + .with(allocation); + + NodeRepository nodeRepository = mock(NodeRepository.class); + when(nodeRepository.getNode(eq(hostname))).thenReturn(Optional.of(n)); + AutoGeneratedKeyProvider keyProvider = new AutoGeneratedKeyProvider(); + + String dnsSuffix = "vespa.dns.suffix"; + AthenzProviderServiceConfig config = getAthenzProviderConfig("domain", "service", dnsSuffix, ZONE); + IdentityDocumentGenerator identityDocumentGenerator = new IdentityDocumentGenerator( + config, + getZoneConfig(config, ZONE), + nodeRepository, + ZONE, + keyProvider); + String rawSignedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(hostname); + + + SignedIdentityDocument signedIdentityDocument = + Utils.getMapper().readValue(rawSignedIdentityDocument, SignedIdentityDocument.class); + + // Verify attributes + assertEquals(hostname, signedIdentityDocument.identityDocument.instanceHostname); + + String environment = "dev"; + String region = "us-north-1"; + String expectedZoneDnsSuffix = environment + "-" + region + "." + dnsSuffix; + assertEquals(expectedZoneDnsSuffix, signedIdentityDocument.dnsSuffix); + + ProviderUniqueId expectedProviderUniqueId = + new ProviderUniqueId("tenant", "application", environment, region, "default", "default", 0); + assertEquals(expectedProviderUniqueId, signedIdentityDocument.identityDocument.providerUniqueId); + + // Validate signature + assertTrue("Message", InstanceValidator.isSignatureValid(keyProvider.getPublicKey(0), + signedIdentityDocument.rawIdentityDocument, + signedIdentityDocument.signature)); + } +}
\ No newline at end of file diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidatorTest.java new file mode 100644 index 00000000000..c1fab319ebf --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidatorTest.java @@ -0,0 +1,171 @@ +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.model.api.SuperModel; +import com.yahoo.config.model.api.SuperModelProvider; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.AthenzInstanceProviderServiceTest.AutoGeneratedKeyProvider; +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 org.junit.Test; + +import java.security.PrivateKey; +import java.security.Signature; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator.SERVICE_PROPERTIES_DOMAIN_KEY; +import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator.SERVICE_PROPERTIES_SERVICE_KEY; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author valerijf + */ +public class InstanceValidatorTest { + + private final ApplicationId applicationId = ApplicationId.from("tenant", "application", "instance"); + private final String domain = "domain"; + private final String service = "service"; + + @Test + public void valid_signature() throws Exception { + KeyProvider keyProvider = new AutoGeneratedKeyProvider(); + InstanceValidator instanceValidator = new InstanceValidator(keyProvider, null); + InstanceConfirmation instanceConfirmation = createInstanceConfirmation( + keyProvider.getPrivateKey(0), applicationId, domain, service); + + assertTrue(instanceValidator.isInstanceSignatureValid(instanceConfirmation)); + } + + @Test + public void invalid_signature() throws Exception { + KeyProvider keyProvider = new AutoGeneratedKeyProvider(); + InstanceValidator instanceValidator = new InstanceValidator(keyProvider, null); + + KeyProvider fakeKeyProvider = new AutoGeneratedKeyProvider(); + InstanceConfirmation instanceConfirmation = createInstanceConfirmation( + fakeKeyProvider.getPrivateKey(0), applicationId, domain, service); + + assertFalse(instanceValidator.isInstanceSignatureValid(instanceConfirmation)); + } + + @Test + public void application_does_not_exist() { + SuperModelProvider superModelProvider = mockSuperModelProvider(); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider); + + assertFalse(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service)); + } + + @Test + public void application_does_not_have_domain_set() { + SuperModelProvider superModelProvider = mockSuperModelProvider( + mockApplicationInfo(applicationId, 5, Collections.emptyList())); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider); + + assertFalse(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service)); + } + + @Test + public void application_has_wrong_domain() { + ServiceInfo serviceInfo = new ServiceInfo("serviceName", "type", Collections.emptyList(), + Collections.singletonMap(SERVICE_PROPERTIES_DOMAIN_KEY, "not-domain"), "confId", "hostName"); + + SuperModelProvider superModelProvider = mockSuperModelProvider( + mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo))); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider); + + assertFalse(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service)); + } + + @Test + public void application_has_same_domain_and_service() { + Map<String, String> properties = new HashMap<>(); + properties.put(SERVICE_PROPERTIES_DOMAIN_KEY, domain); + properties.put(SERVICE_PROPERTIES_SERVICE_KEY, service); + + ServiceInfo serviceInfo = new ServiceInfo("serviceName", "type", Collections.emptyList(), + properties, "confId", "hostName"); + + SuperModelProvider superModelProvider = mockSuperModelProvider( + mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo))); + InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider); + + assertTrue(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service)); + } + + private static InstanceConfirmation createInstanceConfirmation(PrivateKey privateKey, ApplicationId applicationId, + String domain, String service) { + IdentityDocument identityDocument = new IdentityDocument( + new ProviderUniqueId(applicationId.tenant().value(), applicationId.application().value(), + "environment", "region", applicationId.instance().value(), "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()); + + return new InstanceConfirmation( + "provider", domain, service, + new SignedIdentityDocument(encodedIdentityDocument, + Base64.getEncoder().encodeToString(sigGenerator.sign()), + 0, + identityDocument.providerUniqueId.asString(), + "dnssuffix", + "service", + "localhost/zts", + 1)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private SuperModelProvider mockSuperModelProvider(ApplicationInfo... appInfos) { + SuperModel superModel = new SuperModel(Stream.of(appInfos) + .collect(Collectors.groupingBy( + appInfo -> appInfo.getApplicationId().tenant(), + Collectors.toMap( + ApplicationInfo::getApplicationId, + Function.identity() + ) + ))); + + SuperModelProvider superModelProvider = mock(SuperModelProvider.class); + when(superModelProvider.getSuperModel()).thenReturn(superModel); + return superModelProvider; + } + + private ApplicationInfo mockApplicationInfo(ApplicationId appId, int numHosts, List<ServiceInfo> serviceInfo) { + List<HostInfo> hosts = IntStream.range(0, numHosts) + .mapToObj(i -> new HostInfo("host-" + i + "." + appId.toShortString() + ".yahoo.com", serviceInfo)) + .collect(Collectors.toList()); + + Model model = mock(Model.class); + when(model.getHosts()).thenReturn(hosts); + + return new ApplicationInfo(appId, 0, model); + } +}
\ No newline at end of file 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 fb7ad137c22..ce9d0ed27f1 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 @@ -162,6 +162,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { addLegacyFilters(spec, cluster); // TODO: Remove for Vespa 7 // Athenz copper argos + // NOTE: Must be done after addNodes() addIdentity(spec, cluster, context.getDeployState().getProperties().configServerSpecs()); //TODO: overview handler, see DomQrserverClusterBuilder @@ -703,7 +704,10 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { Identity identity = new Identity(domain.trim(), service.trim(), cfgHostName); cluster.addComponent(identity); - + cluster.getContainers().forEach(container -> { + container.setProp("identity.domain", domain); + container.setProp("identity.service", service); + }); } } |