aboutsummaryrefslogtreecommitdiffstats
path: root/node-repository/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'node-repository/src/main/java/com')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java54
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/HostAuthenticator.java112
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodePrincipal.java33
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java3
4 files changed, 179 insertions, 23 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java
index 737308d004d..ccc09aad24a 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java
@@ -8,7 +8,6 @@ import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.SecurityRequestFilter;
import com.yahoo.net.HostName;
-import com.yahoo.vespa.athenz.tls.X509CertificateUtils;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.restapi.v2.Authorizer;
import com.yahoo.vespa.hosted.provision.restapi.v2.ErrorResponse;
@@ -16,6 +15,7 @@ import com.yahoo.vespa.hosted.provision.restapi.v2.ErrorResponse;
import java.net.URI;
import java.security.Principal;
import java.security.cert.X509Certificate;
+import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
@@ -34,6 +34,7 @@ public class AuthorizationFilter implements SecurityRequestFilter {
private final BiPredicate<Principal, URI> authorizer;
private final BiConsumer<ErrorResponse, ResponseHandler> rejectAction;
+ private final HostAuthenticator hostAuthenticator;
@Inject
public AuthorizationFilter(Zone zone, NodeRepository nodeRepository, NodeRepositoryConfig nodeRepositoryConfig) {
@@ -45,44 +46,51 @@ public class AuthorizationFilter implements SecurityRequestFilter {
Stream.of(HostName.getLocalhost()),
Stream.of(nodeRepositoryConfig.hostnameWhitelist().split(","))
).filter(hostname -> !hostname.isEmpty()).collect(Collectors.toSet())),
- AuthorizationFilter::logAndReject
+ AuthorizationFilter::logAndReject,
+ new HostAuthenticator(zone, nodeRepository)
);
}
AuthorizationFilter(BiPredicate<Principal, URI> authorizer,
- BiConsumer<ErrorResponse, ResponseHandler> rejectAction) {
+ BiConsumer<ErrorResponse, ResponseHandler> rejectAction,
+ HostAuthenticator hostAuthenticator) {
this.authorizer = authorizer;
this.rejectAction = rejectAction;
+ this.hostAuthenticator = hostAuthenticator;
}
@Override
public void filter(DiscFilterRequest request, ResponseHandler handler) {
- Optional<String> commonName = request.getClientCertificateChain().stream()
- .findFirst()
- .flatMap(AuthorizationFilter::commonName);
- if (commonName.isPresent()) {
- if (!authorizer.test(commonName::get, request.getUri())) {
- rejectAction.accept(ErrorResponse.forbidden(
- String.format("%s %s denied for %s: Invalid credentials", request.getMethod(),
- request.getUri().getPath(), request.getRemoteAddr())), handler
- );
- }
- } else {
- rejectAction.accept(ErrorResponse.unauthorized(
- String.format("%s %s denied for %s: Missing credentials", request.getMethod(),
- request.getUri().getPath(), request.getRemoteAddr())), handler
- );
+ validateAccess(request)
+ .ifPresent(errorResponse -> rejectAction.accept(errorResponse, handler));
+ }
+
+ private Optional<ErrorResponse> validateAccess(DiscFilterRequest request) {
+ try {
+ List<X509Certificate> clientCertificateChain = request.getClientCertificateChain();
+ if (clientCertificateChain.isEmpty())
+ return Optional.of(ErrorResponse.unauthorized(createErrorMessage(request, "Missing credentials")));
+ NodePrincipal hostIdentity = hostAuthenticator.authenticate(clientCertificateChain);
+ if (!authorizer.test(hostIdentity, request.getUri()))
+ return Optional.of(ErrorResponse.forbidden(createErrorMessage(request, "Invalid credentials")));
+ request.setUserPrincipal(hostIdentity);
+ return Optional.empty();
+ } catch (HostAuthenticator.AuthenticationException e) {
+ return Optional.of(ErrorResponse.forbidden(createErrorMessage(request, "Invalid credentials: " + e.getMessage())));
}
}
+ private static String createErrorMessage(DiscFilterRequest request, String message) {
+ return String.format("%s %s denied for %s: %s",
+ request.getMethod(),
+ request.getUri().getPath(),
+ request.getRemoteAddr(),
+ message);
+ }
+
private static void logAndReject(ErrorResponse response, ResponseHandler handler) {
log.warning(response.message());
FilterUtils.write(response, handler);
}
- /** Read common name (CN) from certificate */
- private static Optional<String> commonName(X509Certificate certificate) {
- return X509CertificateUtils.getSubjectCommonNames(certificate).stream().findFirst();
- }
-
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/HostAuthenticator.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/HostAuthenticator.java
new file mode 100644
index 00000000000..de8d117de11
--- /dev/null
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/HostAuthenticator.java
@@ -0,0 +1,112 @@
+// 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.provision.restapi.v2.filter;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.athenz.tls.SubjectAlternativeName;
+import com.yahoo.vespa.athenz.tls.X509CertificateUtils;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import static com.yahoo.vespa.athenz.tls.SubjectAlternativeName.Type.DNS_NAME;
+
+/**
+ * Resolve node from various types of x509 identity certificates.
+ *
+ * @author bjorncs
+ */
+class HostAuthenticator {
+
+ private static final String TENANT_DOCKER_HOST_IDENTITY = "vespa.vespa.tenant-host";
+ private static final String TENANT_DOCKER_CONTAINER_IDENTITY = "vespa.vespa.tenant";
+ private static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz.";
+
+ private final Zone zone;
+ private final NodeRepository nodeRepository;
+
+ HostAuthenticator(Zone zone, NodeRepository nodeRepository) {
+ this.zone = zone;
+ this.nodeRepository = nodeRepository;
+ }
+
+ NodePrincipal authenticate(List<X509Certificate> certificateChain) throws AuthenticationException {
+ X509Certificate clientCertificate = certificateChain.get(0);
+ String subjectCommonName = X509CertificateUtils.getSubjectCommonNames(clientCertificate).stream()
+ .findFirst()
+ .orElseThrow(() -> new AuthenticationException("Certificate subject common name is missing!"));
+ if (isAthenzIssued(clientCertificate)) {
+ String hostname;
+ List<SubjectAlternativeName> sans = X509CertificateUtils.getSubjectAlternativeNames(clientCertificate);
+ switch (subjectCommonName) {
+ case TENANT_DOCKER_HOST_IDENTITY:
+ hostname = getHostFromCalypsoCertificate(sans);
+ break;
+ case TENANT_DOCKER_CONTAINER_IDENTITY:
+ hostname = getHostFromVespaCertificate(sans);
+ break;
+ default:
+ throw new AuthenticationException("Untrusted common name in subject: " + subjectCommonName);
+ }
+ return new NodePrincipal(hostname, certificateChain);
+ } else { // self-signed where common name is hostname
+ // TODO Remove this branch once self-signed certificates are gone
+ return new NodePrincipal(subjectCommonName, certificateChain);
+ }
+ }
+
+ private boolean isAthenzIssued(X509Certificate certificate) {
+ String issuerCommonName = X509CertificateUtils.getIssuerCommonNames(certificate).stream()
+ .findFirst()
+ .orElseThrow(() -> new AuthenticationException("Certificate issuer common name is missing!"));
+ return issuerCommonName.equals("Yahoo Athenz CA") || issuerCommonName.equals("Athenz AWS CA");
+ }
+
+ private String getHostFromCalypsoCertificate(List<SubjectAlternativeName> sans) {
+ String openstackId = getUniqueInstanceId(sans);
+ return nodeRepository.getNodes().stream()
+ .filter(node -> node.openStackId().equals(openstackId))
+ .map(Node::hostname)
+ .findFirst()
+ .orElseThrow(() -> new AuthenticationException(String.format("Cannot find node with openstack-id '%s' in node repository", openstackId)));
+ }
+
+ private String getHostFromVespaCertificate(List<SubjectAlternativeName> sans) {
+ VespaUniqueInstanceId instanceId = VespaUniqueInstanceId.fromDottedString(getUniqueInstanceId(sans));
+ if (!zone.environment().value().equals(instanceId.environment()))
+ throw new AuthenticationException("Invalid environment: " + instanceId.environment());
+ if (!zone.region().value().equals(instanceId.region()))
+ throw new AuthenticationException("Invalid region(): " + instanceId.region());
+ List<Node> applicationNodes =
+ nodeRepository.getNodes(ApplicationId.from(instanceId.tenant(), instanceId.application(), instanceId.instance()));
+ return applicationNodes.stream()
+ .filter(
+ node -> node.allocation()
+ .map(allocation -> allocation.membership().index() == instanceId.clusterIndex()
+ && allocation.membership().cluster().id().value().equals(instanceId.clusterId()))
+ .orElse(false))
+ .map(Node::hostname)
+ .findFirst()
+ .orElseThrow(() -> new AuthenticationException("Could not find any node with instance id: " + instanceId.asDottedString()));
+ }
+
+ private static String getUniqueInstanceId(List<SubjectAlternativeName> sans) {
+ return sans.stream()
+ .filter(san -> san.getType() == DNS_NAME)
+ .map(SubjectAlternativeName::getValue)
+ .filter(dnsName -> (dnsName.endsWith("yahoo.cloud") || dnsName.endsWith("oath.cloud")) && dnsName.contains(INSTANCE_ID_DELIMITER))
+ .map(dnsName -> dnsName.substring(0, dnsName.indexOf(INSTANCE_ID_DELIMITER)))
+ .findFirst()
+ .orElseThrow(() -> new AuthenticationException("Could not find unique instance id from SAN addresses: " + sans));
+ }
+
+ static class AuthenticationException extends RuntimeException {
+ AuthenticationException(String message) {
+ super(message);
+ }
+ }
+
+}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodePrincipal.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodePrincipal.java
new file mode 100644
index 00000000000..dbff2b0da34
--- /dev/null
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodePrincipal.java
@@ -0,0 +1,33 @@
+// 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.provision.restapi.v2.filter;
+
+import java.security.Principal;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+/**
+ * @author bjorncs
+ */
+public class NodePrincipal implements Principal {
+ private final String hostIdentity;
+ private final List<X509Certificate> clientCertificateChain;
+
+ public NodePrincipal(String hostIdentity, List<X509Certificate> clientCertificateChain) {
+ this.hostIdentity = hostIdentity;
+ this.clientCertificateChain = clientCertificateChain;
+ }
+
+ public String getHostIdentityName() {
+ return hostIdentity;
+ }
+
+ public List<X509Certificate> getClientCertificateChain() {
+ return clientCertificateChain;
+ }
+
+ @Override
+ public String getName() {
+ return hostIdentity;
+ }
+
+}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java
index 0e68629ef5e..e2a40f398a9 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java
@@ -2,4 +2,7 @@
/**
* @author mpolden
*/
+@ExportPackage
package com.yahoo.vespa.hosted.provision.restapi.v2.filter;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file