diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-05-09 11:37:53 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-05-14 16:59:37 +0200 |
commit | 6c725b36dfb82345c902be0b8f0cf3fc9d86f376 (patch) | |
tree | 9f3c050d9b86444b4b996e9ff7a20e4998e88e1f /node-admin | |
parent | a9d9ef3dc96791185de550702876d49b1fef9557 (diff) |
Add initial implementation of AthenzCredentialsMaintainer
Diffstat (limited to 'node-admin')
2 files changed, 273 insertions, 0 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java new file mode 100644 index 00000000000..dcc34d0306e --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java @@ -0,0 +1,265 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.maintenance.identity; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient; +import com.yahoo.vespa.athenz.client.zts.InstanceIdentity; +import com.yahoo.vespa.athenz.client.zts.ZtsClient; +import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; +import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocumentClient; +import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; +import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; +import com.yahoo.vespa.athenz.identityprovider.api.bindings.ProviderUniqueId; +import com.yahoo.vespa.athenz.identityprovider.client.InstanceCsrGenerator; +import com.yahoo.vespa.athenz.tls.KeyAlgorithm; +import com.yahoo.vespa.athenz.tls.KeyStoreType; +import com.yahoo.vespa.athenz.tls.KeyUtils; +import com.yahoo.vespa.athenz.tls.Pkcs10Csr; +import com.yahoo.vespa.athenz.tls.SslContextBuilder; +import com.yahoo.vespa.athenz.tls.X509CertificateUtils; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.node.admin.component.Environment; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeSpec; +import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Set; + +/** + * A maintainer that is responsible for providing and refreshing Athenz credentials for a container. + * + * @author bjorncs + */ +@SuppressWarnings("deprecation") // TODO Use new entity response types +public class AthenzCredentialsMaintainer { + + private static final Duration EXPIRY_MARGIN = Duration.ofDays(1); + private static final Duration REFRESH_PERIOD = Duration.ofDays(1); + + private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + private final PrefixLogger log; + private final String hostname; + private final Environment environment; + private final Path trustStorePath; + private final Path privateKeyFile; + private final Path certificateFile; + private final AthenzService containerIdentity; + private final URI ztsEndpoint; + private final Clock clock; + private final ServiceIdentityProvider hostIdentityProvider; + private final IdentityDocumentClient identityDocumentClient; + private final InstanceCsrGenerator csrGenerator; + private final AthenzService configserverIdentity; + + AthenzCredentialsMaintainer(String hostname, + Path trustStorePath, + Environment environment, + Path containerSiaDirectory, + URI ztsEndpoint, + String dnsSuffix, + AthenzService configserverIdentity, + AthenzService containerIdentity, + ServiceIdentityProvider hostIdentityProvider, + IdentityDocumentClient identityDocumentClient) { + this.log = PrefixLogger.getNodeAgentLogger(AthenzCredentialsMaintainer.class, ContainerName.fromHostname(hostname)); + this.hostname = hostname; + this.environment = environment; + this.containerIdentity = containerIdentity; + this.ztsEndpoint = ztsEndpoint; + this.configserverIdentity = configserverIdentity; + this.csrGenerator = new InstanceCsrGenerator(dnsSuffix); + this.trustStorePath = trustStorePath; + this.privateKeyFile = getPrivateKeyFile(containerSiaDirectory, containerIdentity); + this.certificateFile = getCertificateFile(containerSiaDirectory, containerIdentity); + this.hostIdentityProvider = hostIdentityProvider; + this.identityDocumentClient = identityDocumentClient; + this.clock = Clock.systemUTC(); + } + + /** + * @param nodeSpec Node specification + * @return Returns true if credentials were updated + */ + public boolean converge(NodeSpec nodeSpec) { + try { + log.debug("Checking certificate"); + Instant now = clock.instant(); + VespaUniqueInstanceId instanceId = getVespaUniqueInstanceId(nodeSpec); + Set<String> ipAddresses = nodeSpec.getIpAddresses(); + if (!privateKeyFile.toFile().exists() || !certificateFile.toFile().exists()) { + log.info("Certificate and/or private key file does not exist"); + Files.createDirectories(privateKeyFile.getParent()); + Files.createDirectories(certificateFile.getParent()); + registerIdentity(instanceId, ipAddresses); + return true; + } + X509Certificate certificate = readCertificateFromFile(); + Instant expiry = certificate.getNotAfter().toInstant(); + if (isCertificateExpired(expiry, now)) { + log.info(String.format("Certificate has expired (expiry=%s)", expiry.toString())); + registerIdentity(instanceId, ipAddresses); + return true; + } + Duration age = Duration.between(certificate.getNotBefore().toInstant(), now); + if (shouldRefreshCredentials(age)) { + log.info(String.format("Certificate is ready to be refreshed (age=%s)", age.toString())); + refreshIdentity(instanceId, ipAddresses); + return true; + } + log.debug("Certificate is still valid"); + return false; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void clearCredentials() { + boolean privateKeyDeleteResult = privateKeyFile.toFile().delete(); + log.info(String.format("Deleted private key file (path=%s, result=%s)", privateKeyFile, privateKeyDeleteResult)); + boolean certificateDeleteResult = certificateFile.toFile().delete(); + log.info(String.format("Deleted certificate file (path=%s, result=%s)", certificateFile, certificateDeleteResult)); + } + + private VespaUniqueInstanceId getVespaUniqueInstanceId(NodeSpec nodeSpec) { + NodeSpec.Membership membership = nodeSpec.getMembership().get(); + NodeSpec.Owner owner = nodeSpec.getOwner().get(); + return new VespaUniqueInstanceId( + membership.getIndex(), + membership.getClusterId(), + owner.getInstance(), + owner.getApplication(), + owner.getTenant(), + environment.getRegion(), + environment.getEnvironment()); + } + + private boolean shouldRefreshCredentials(Duration age) { + return age.compareTo(REFRESH_PERIOD) >= 0; + } + + private X509Certificate readCertificateFromFile() throws IOException { + String pemEncodedCertificate = new String(Files.readAllBytes(certificateFile)); + return X509CertificateUtils.fromPem(pemEncodedCertificate); + } + + private boolean isCertificateExpired(Instant expiry, Instant now) { + return expiry.minus(EXPIRY_MARGIN).isAfter(now); + } + + private void registerIdentity(VespaUniqueInstanceId instanceId, Set<String> ipAddresses) { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); + Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, instanceId, ipAddresses, keyPair); + SignedIdentityDocument signedIdentityDocument = identityDocumentClient.getNodeIdentityDocument(hostname); + try (ZtsClient ztsClient = new DefaultZtsClient(ztsEndpoint, hostIdentityProvider)) { + InstanceIdentity instanceIdentity = + ztsClient.registerInstance( + configserverIdentity, + containerIdentity, + instanceId.asDottedString(), + toAttestationDataString(signedIdentityDocument), + false, + csr); + writePrivateKeyAndCertificate(keyPair.getPrivate(), instanceIdentity.certificate()); + log.info("Instance successfully registered and credentials written to file"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (Exception e) { + // TODO Change close() in ZtsClient to not throw checked exception + } + } + + private void refreshIdentity(VespaUniqueInstanceId instanceId, Set<String> ipAddresses) { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); + Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, instanceId, ipAddresses, keyPair); + SSLContext containerIdentitySslContext = + new SslContextBuilder() + .withKeyStore(privateKeyFile.toFile(), certificateFile.toFile()) + .withTrustStore(trustStorePath.toFile(), KeyStoreType.JKS) + .build(); + try (ZtsClient ztsClient = new DefaultZtsClient(ztsEndpoint, containerIdentity, containerIdentitySslContext)) { + InstanceIdentity instanceIdentity = + ztsClient.refreshInstance( + configserverIdentity, + containerIdentity, + instanceId.asDottedString(), + false, + csr); + writePrivateKeyAndCertificate(keyPair.getPrivate(), instanceIdentity.certificate()); + log.info("Instance successfully refreshed and credentials written to file"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (Exception e) { + // TODO Change close() in ZtsClient to not throw checked exception + } + } + + private void writePrivateKeyAndCertificate(PrivateKey privateKey, X509Certificate certificate) throws IOException { + Path tempPrivateKeyFile = toTempPath(privateKeyFile); + Files.write(tempPrivateKeyFile, KeyUtils.toPem(privateKey).getBytes()); + Path tempCertificateFile = toTempPath(certificateFile); + Files.write(tempCertificateFile, X509CertificateUtils.toPem(certificate).getBytes()); + + Files.move(tempPrivateKeyFile, privateKeyFile, StandardCopyOption.ATOMIC_MOVE); + Files.move(tempCertificateFile, certificateFile, StandardCopyOption.ATOMIC_MOVE); + } + + private static Path toTempPath(Path file) { + return Paths.get(file.toAbsolutePath().toString() + ".tmp"); + } + + // TODO Move to vespa-athenz + private String toAttestationDataString(SignedIdentityDocument signedIdDoc) throws JsonProcessingException { + com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument idDoc = signedIdDoc.identityDocument(); + com.yahoo.vespa.athenz.identityprovider.api.bindings.IdentityDocument identityDocumentPayload = + new com.yahoo.vespa.athenz.identityprovider.api.bindings.IdentityDocument( + ProviderUniqueId.fromVespaUniqueInstanceId(idDoc.providerUniqueId()), + idDoc.configServerHostname(), + idDoc.instanceHostname(), + idDoc.createdAt(), + idDoc.ipAddresses()); + String rawIdentityDocument = objectMapper.writeValueAsString(identityDocumentPayload); + com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocument payload = + new com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocument( + rawIdentityDocument, + signedIdDoc.signature(), + signedIdDoc.signingKeyVersion(), + signedIdDoc.providerUniqueId().asDottedString(), + signedIdDoc.dnsSuffix(), + signedIdDoc.providerService().getFullName(), + signedIdDoc.ztsEndpoint(), + signedIdDoc.documentVersion()); + return objectMapper.writeValueAsString(payload); + } + + // TODO Move to vespa-athenz + private static Path getPrivateKeyFile(Path root, AthenzService service) { + return root + .resolve("keys") + .resolve(String.format("%s.%s.key.pem", service.getDomain().getName(), service.getName())); + } + + // TODO Move to vespa-athenz + private static Path getCertificateFile(Path root, AthenzService service) { + return root + .resolve("certs") + .resolve(String.format("%s.%s.cert.pem", service.getDomain().getName(), service.getName())); + } + +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/package-info.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/package-info.java new file mode 100644 index 00000000000..7ee04a33b05 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2018 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.node.admin.maintenance.identity; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file |