diff options
author | Morten Tokle <morten.tokle@gmail.com> | 2018-05-02 11:08:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-02 11:08:00 +0200 |
commit | c435b77007282d32fa48960cdac19d7487654077 (patch) | |
tree | 490421a68bb5c967bdfadea87635b68cba4d7fb3 | |
parent | 3158c62af78e2d0b5870219beed5a4a10a4406b1 (diff) | |
parent | 43449b3468682c48536cda2698c52e9788712386 (diff) |
Merge pull request #5755 from vespa-engine/bjorncs/athenz-identity-provider
Bjorncs/athenz identity provider
-rw-r--r-- | container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java | 3 | ||||
-rw-r--r-- | vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java | 28 | ||||
-rw-r--r-- | vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java | 63 | ||||
-rw-r--r-- | vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentService.java | 3 | ||||
-rw-r--r-- | vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/ZtsClient.java (renamed from vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzService.java) | 93 | ||||
-rw-r--r-- | vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java | 9 |
6 files changed, 135 insertions, 64 deletions
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java index bb6a033a4ab..9762d69ec31 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/athenz/AthenzIdentityProvider.java @@ -10,4 +10,7 @@ public interface AthenzIdentityProvider { String domain(); String service(); SSLContext getIdentitySslContext(); + SSLContext getRoleSslContext(String domain, String role); + String getRoleToken(String domain); + String getRoleToken(String domain, String role); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java index 26fe0b6e930..4278e641166 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java @@ -35,16 +35,16 @@ class AthenzCredentialsService { private final IdentityConfig identityConfig; private final IdentityDocumentService identityDocumentService; - private final AthenzService athenzService; + private final ZtsClient ztsClient; private final File trustStoreJks; AthenzCredentialsService(IdentityConfig identityConfig, IdentityDocumentService identityDocumentService, - AthenzService athenzService, + ZtsClient ztsClient, File trustStoreJks) { this.identityConfig = identityConfig; this.identityDocumentService = identityDocumentService; - this.athenzService = athenzService; + this.ztsClient = ztsClient; this.trustStoreJks = trustStoreJks; } @@ -64,13 +64,12 @@ class AthenzCredentialsService { identityConfig.service(), rawDocument, Pkcs10CsrUtils.toPem(csr)); - InstanceIdentity instanceIdentity = athenzService.sendInstanceRegisterRequest(instanceRegisterInformation, - document.ztsEndpoint); + InstanceIdentity instanceIdentity = ztsClient.sendInstanceRegisterRequest(instanceRegisterInformation, + document.ztsEndpoint); return toAthenzCredentials(instanceIdentity, keyPair, document); } - AthenzCredentials updateCredentials(AthenzCredentials currentCredentials) { - SignedIdentityDocument document = currentCredentials.getIdentityDocument(); + AthenzCredentials updateCredentials(SignedIdentityDocument document, SSLContext sslContext) { KeyPair newKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); Pkcs10Csr csr = createCSR(identityConfig.domain(), identityConfig.service(), @@ -80,14 +79,13 @@ class AthenzCredentialsService { newKeyPair); InstanceRefreshInformation refreshInfo = new InstanceRefreshInformation(Pkcs10CsrUtils.toPem(csr)); InstanceIdentity instanceIdentity = - athenzService.sendInstanceRefreshRequest(document.providerService, - identityConfig.domain(), - identityConfig.service(), - document.providerUniqueId, - refreshInfo, - document.ztsEndpoint, - currentCredentials.getCertificate(), - currentCredentials.getKeyPair().getPrivate()); + ztsClient.sendInstanceRefreshRequest(document.providerService, + identityConfig.domain(), + identityConfig.service(), + document.providerUniqueId, + refreshInfo, + document.ztsEndpoint, + sslContext); return toAthenzCredentials(instanceIdentity, newKeyPair, document); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java index 594fa91e18f..eb0ae89fdcf 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImpl.java @@ -8,11 +8,17 @@ import com.yahoo.container.jdisc.athenz.AthenzIdentityProvider; import com.yahoo.container.jdisc.athenz.AthenzIdentityProviderException; import com.yahoo.jdisc.Metric; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; +import com.yahoo.vespa.athenz.tls.KeyStoreType; +import com.yahoo.vespa.athenz.tls.SslContextBuilder; import com.yahoo.vespa.defaults.Defaults; import javax.net.ssl.SSLContext; import java.io.File; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -37,20 +43,22 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen public static final String CERTIFICATE_EXPIRY_METRIC_NAME = "athenz-tenant-cert.expiry.seconds"; private volatile AthenzCredentials credentials; + private final ZtsClient ztsClient = new ZtsClient(); private final Metric metric; private final AthenzCredentialsService athenzCredentialsService; private final ScheduledExecutorService scheduler; private final Clock clock; - private final com.yahoo.vespa.athenz.api.AthenzService identity; + private final AthenzService identity; + // TODO IdentityConfig should contain ZTS uri and dns suffix @Inject public AthenzIdentityProviderImpl(IdentityConfig config, Metric metric) { this(config, metric, new AthenzCredentialsService(config, new IdentityDocumentService(config.loadBalancerAddress()), - new AthenzService(), - new File(Defaults.getDefaults().underVespaHome("share/ssl/certs/yahoo_certificate_bundle.jks"))), + new ZtsClient(), + getDefaultTrustStoreLocation()), new ScheduledThreadPoolExecutor(1), Clock.systemUTC()); } @@ -65,7 +73,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen this.athenzCredentialsService = athenzCredentialsService; this.scheduler = scheduler; this.clock = clock; - this.identity = new com.yahoo.vespa.athenz.api.AthenzService(config.domain(), config.service()); + this.identity = new AthenzService(config.domain(), config.service()); registerInstance(); } @@ -80,7 +88,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen } @Override - public com.yahoo.vespa.athenz.api.AthenzService identity() { + public AthenzService identity() { return identity; } @@ -100,6 +108,45 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen } @Override + public SSLContext getRoleSslContext(String domain, String role) { + // This ssl context should ideally be cached as it is quite expensive to create. + PrivateKey privateKey = credentials.getKeyPair().getPrivate(); + X509Certificate roleCertificate = ztsClient.getRoleCertificate( + new AthenzDomain(domain), + role, + credentials.getIdentityDocument().dnsSuffix, + credentials.getIdentityDocument().ztsEndpoint, + identity, + privateKey, + credentials.getIdentitySslContext()); + return new SslContextBuilder() + .withKeyStore(privateKey, roleCertificate) + .withTrustStore(getDefaultTrustStoreLocation(), KeyStoreType.JKS) + .build(); + } + + @Override + public String getRoleToken(String domain) { + return ztsClient + .getRoleToken( + new AthenzDomain(domain), + credentials.getIdentityDocument().ztsEndpoint, + credentials.getIdentitySslContext()) + .getRawToken(); + } + + @Override + public String getRoleToken(String domain, String role) { + return ztsClient + .getRoleToken( + new AthenzDomain(domain), + role, + credentials.getIdentityDocument().ztsEndpoint, + credentials.getIdentitySslContext()) + .getRawToken(); + } + + @Override public void deconstruct() { try { scheduler.shutdownNow(); @@ -109,6 +156,10 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen } } + private static File getDefaultTrustStoreLocation() { + return new File(Defaults.getDefaults().underVespaHome("share/ssl/certs/yahoo_certificate_bundle.jks")); + } + private boolean isExpired(AthenzCredentials credentials) { return clock.instant().isAfter(getExpirationTime(credentials)); } @@ -121,7 +172,7 @@ public final class AthenzIdentityProviderImpl extends AbstractComponent implemen try { AthenzCredentials newCredentials = isExpired(credentials) ? athenzCredentialsService.registerInstance() - : athenzCredentialsService.updateCredentials(credentials); + : athenzCredentialsService.updateCredentials(credentials.getIdentityDocument(), credentials.getIdentitySslContext()); credentials = newCredentials; } catch (Throwable t) { log.log(LogLevel.WARNING, "Failed to update credentials: " + t.getMessage(), t); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentService.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentService.java index 34b28e48914..0d82bb29edd 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentService.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/IdentityDocumentService.java @@ -74,8 +74,7 @@ public class IdentityDocumentService { .setScheme("https") .setHost(loadBalancerName) .setPort(4443) - .setPath("/athenz/v1/provider/identity-document") - .addParameter("hostname", Defaults.getDefaults().vespaHostname()) + .setPath("/athenz/v1/provider/identity-document/tenant/" + Defaults.getDefaults().vespaHostname()) .build(); } catch (URISyntaxException e) { throw new RuntimeException(e); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzService.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/ZtsClient.java index 98307a8a2d1..c995bfba791 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzService.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/ZtsClient.java @@ -1,13 +1,19 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.identityprovider.client; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.athenz.zts.RoleCertificateRequest; +import com.yahoo.athenz.zts.RoleToken; +import com.yahoo.athenz.zts.ZTSClient; +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.api.ZToken; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; import org.apache.http.client.HttpRequestRetryHandler; 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.conn.ssl.SSLContextBuilder; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -20,21 +26,15 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.UnrecoverableKeyException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.time.Duration; /** * @author mortent * @author bjorncs */ -public class AthenzService { +class ZtsClient { private static final String INSTANCE_API_PATH = "/zts/v1/instance"; @@ -44,7 +44,7 @@ public class AthenzService { /** * Send instance register request to ZTS, get InstanceIdentity */ - public InstanceIdentity sendInstanceRegisterRequest(InstanceRegisterInformation instanceRegisterInformation, + InstanceIdentity sendInstanceRegisterRequest(InstanceRegisterInformation instanceRegisterInformation, URI uri) { try(CloseableHttpClient client = HttpClientBuilder.create().setRetryHandler(retryHandler).build()) { HttpUriRequest postRequest = RequestBuilder.post() @@ -57,15 +57,14 @@ public class AthenzService { } } - public InstanceIdentity sendInstanceRefreshRequest(String providerService, - String instanceDomain, - String instanceServiceName, - String instanceId, - InstanceRefreshInformation instanceRefreshInformation, - URI ztsEndpoint, - X509Certificate certicate, - PrivateKey privateKey) { - try (CloseableHttpClient client = createHttpClientWithTlsAuth(certicate, privateKey, retryHandler)) { + InstanceIdentity sendInstanceRefreshRequest(String providerService, + String instanceDomain, + String instanceServiceName, + String instanceId, + InstanceRefreshInformation instanceRefreshInformation, + URI ztsEndpoint, + SSLContext sslContext) { + try (CloseableHttpClient client = createHttpClientWithTlsAuth(sslContext, retryHandler)) { URI uri = ztsEndpoint .resolve(INSTANCE_API_PATH + '/') .resolve(providerService + '/') @@ -82,6 +81,43 @@ public class AthenzService { } } + ZToken getRoleToken(AthenzDomain domain, + URI ztsEndpoint, + SSLContext sslContext) { + // TODO ztsEndpoint should contain '/zts/v1' as path + URI correctedZtsEndpoint = ztsEndpoint.resolve("/zts/v1"); + return new ZToken( + new ZTSClient(correctedZtsEndpoint.toString(), sslContext) + .getRoleToken(domain.getName()).getToken()); + } + + ZToken getRoleToken(AthenzDomain domain, + String roleName, + URI ztsEndpoint, + SSLContext sslContext) { + // TODO ztsEndpoint should contain '/zts/v1' as path + URI correctedZtsEndpoint = ztsEndpoint.resolve("/zts/v1"); + return new ZToken( + new ZTSClient(correctedZtsEndpoint.toString(), sslContext) + .getRoleToken(domain.getName(), roleName).getToken()); + } + + X509Certificate getRoleCertificate(AthenzDomain roleDomain, + String roleName, + String dnsSuffix, + URI ztsEndpoint, + AthenzService identity, + PrivateKey privateKey, + SSLContext sslContext) { + // TODO ztsEndpoint should contain '/zts/v1' as path + URI correctedZtsEndpoint = ztsEndpoint.resolve("/zts/v1"); + ZTSClient ztsClient = new ZTSClient(correctedZtsEndpoint.toString(), sslContext); + RoleCertificateRequest rcr = ZTSClient.generateRoleCertificateRequest( + identity.getDomain().getName(), identity.getName(), roleDomain.getName(), roleName, privateKey, dnsSuffix, (int) Duration.ofHours(1).getSeconds()); + RoleToken pemCert = ztsClient.postRoleCertificateRequest(roleDomain.getName(), roleName, rcr); + return X509CertificateUtils.fromPem(pemCert.token); + } + private InstanceIdentity getInstanceIdentity(CloseableHttpClient client, HttpUriRequest postRequest) throws IOException { try (CloseableHttpResponse response = client.execute(postRequest)) { @@ -99,26 +135,11 @@ public class AthenzService { return new StringEntity(objectMapper.writeValueAsString(value), ContentType.APPLICATION_JSON); } - private static CloseableHttpClient createHttpClientWithTlsAuth(X509Certificate certificate, - PrivateKey privateKey, + private static CloseableHttpClient createHttpClientWithTlsAuth(SSLContext sslContext, HttpRequestRetryHandler retryHandler) { - try { - String dummyPassword = "athenz"; - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null); - keyStore.setKeyEntry("athenz", privateKey, dummyPassword.toCharArray(), new Certificate[]{certificate}); - SSLContext sslContext = new SSLContextBuilder() - .loadKeyMaterial(keyStore, dummyPassword.toCharArray()) - .build(); return HttpClientBuilder.create() .setRetryHandler(retryHandler) .setSslcontext(sslContext) .build(); - } catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException | - KeyManagementException | CertificateException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new UncheckedIOException(e); - } } } diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java index a0ae6ca61db..91c68702e87 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java @@ -58,26 +58,25 @@ public class AthenzIdentityProviderImplTest { @Test public void metrics_updated_on_refresh() throws IOException { IdentityDocumentService identityDocumentService = mock(IdentityDocumentService.class); - AthenzService athenzService = mock(AthenzService.class); + ZtsClient ztsClient = mock(ZtsClient.class); ManualClock clock = new ManualClock(Instant.EPOCH); Metric metric = mock(Metric.class); when(identityDocumentService.getSignedIdentityDocument()).thenReturn(getIdentityDocument()); - when(athenzService.sendInstanceRegisterRequest(any(), any())).then(new Answer<InstanceIdentity>() { + when(ztsClient.sendInstanceRegisterRequest(any(), any())).then(new Answer<InstanceIdentity>() { @Override public InstanceIdentity answer(InvocationOnMock invocationOnMock) throws Throwable { return new InstanceIdentity(getCertificate(getExpirationSupplier(clock)), "TOKEN"); } }); - when(athenzService.sendInstanceRefreshRequest(anyString(), anyString(), anyString(), - anyString(), any(), any(), any(), any())) + when(ztsClient.sendInstanceRefreshRequest(anyString(), anyString(), anyString(), anyString(), any(), any(), any())) .thenThrow(new RuntimeException("#1")) .thenThrow(new RuntimeException("#2")) .thenReturn(new InstanceIdentity(getCertificate(getExpirationSupplier(clock)), "TOKEN")); AthenzCredentialsService credentialService = - new AthenzCredentialsService(IDENTITY_CONFIG, identityDocumentService, athenzService, createDummyTrustStore()); + new AthenzCredentialsService(IDENTITY_CONFIG, identityDocumentService, ztsClient, createDummyTrustStore()); AthenzIdentityProviderImpl identityProvider = new AthenzIdentityProviderImpl(IDENTITY_CONFIG, metric, credentialService, mock(ScheduledExecutorService.class), clock); |