summaryrefslogtreecommitdiffstats
path: root/node-admin
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-05-09 11:37:53 +0200
committerBjørn Christian Seime <bjorncs@oath.com>2018-05-14 16:59:37 +0200
commit6c725b36dfb82345c902be0b8f0cf3fc9d86f376 (patch)
tree9f3c050d9b86444b4b996e9ff7a20e4998e88e1f /node-admin
parenta9d9ef3dc96791185de550702876d49b1fef9557 (diff)
Add initial implementation of AthenzCredentialsMaintainer
Diffstat (limited to 'node-admin')
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java265
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/package-info.java8
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