[pve-devel] [PATCH manager 2/2] enable certificate pinning for proxied requests

Fabian Grünbichler f.gruenbichler at proxmox.com
Wed Nov 16 14:24:15 CET 2016


when forwarding an API request to the responsible node,
only accept the certificate that this node should have
according to the contents of the cluster file system.

to limit performance issues, cache certificate fingerprint
on first request for each node, and only regenerate cache
(once) if the actual encountered fingerprint does not match.
---
 PVE/HTTPServer.pm | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 70 insertions(+), 1 deletion(-)

diff --git a/PVE/HTTPServer.pm b/PVE/HTTPServer.pm
index 1c7d033..5ddbd07 100755
--- a/PVE/HTTPServer.pm
+++ b/PVE/HTTPServer.pm
@@ -105,6 +105,53 @@ sub get_login_formatter {
     return $info->{func};
 }
 
+my $cert_cache_nodes = {};
+my $cert_cache_fingerprints = {};
+sub update_cert_cache {
+    my ($node) = @_;
+
+    if (!defined($node)) {
+	for $node (keys %$cert_cache_nodes) {
+	    update_cert_cache($node);
+	}
+	return;
+    }
+
+    if (my $old_fp = $cert_cache_nodes->{$node}) {
+	delete $cert_cache_fingerprints->{$old_fp};
+    }
+
+    my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
+    my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
+    $cert_path = $custom_cert_path if -f $custom_cert_path;
+    my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r');
+    my $cert = Net::SSLeay::PEM_read_bio_X509($bio);
+    Net::SSLeay::BIO_free($bio);
+
+    my $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
+    return if !defined($fp);
+
+    $cert_cache_fingerprints->{$fp} = 1;
+    $cert_cache_nodes->{$node} = $fp;
+}
+
+sub check_cert_fp {
+    my ($fp) = @_;
+
+    my $check = sub {
+	for my $expected (keys %$cert_cache_fingerprints) {
+	    return 1 if $fp eq $expected;
+	}
+	return 0;
+    };
+
+    return 1 if &$check();
+
+    # refresh cache and retry once
+    update_cert_cache();
+    return &$check();
+}
+
 # server implementation
 
 sub log_request {
@@ -601,6 +648,28 @@ sub proxy_request {
 	    }
 	}
 
+	my $tls = {
+	    # TLS 1.x only, with certificate pinning
+	    method => 'any',
+	    sslv2 => 0,
+	    sslv3 => 0,
+	    verify => 1,
+	    verify_cb => sub {
+		my (undef, undef, undef, $depth, undef, undef, $cert) = @_;
+		# we don't care about intermediate or root certificates
+		return 0 if $depth != 0;
+
+		# get fingerprint of server certificate
+		my $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
+		return 0 if !defined($fp); # error
+		return check_cert_fp($fp); # check against cache of pinned FPs
+	    },
+	};
+
+	# load and cache cert fingerprint if first time we proxy to this node
+	update_cert_cache($node)
+	    if defined($node) && !defined($cert_cache_nodes->{$node});
+
 	my $w; $w = http_request(
 	    $method => $target,
 	    headers => $headers,
@@ -609,7 +678,7 @@ sub proxy_request {
 	    proxy => undef, # avoid use of $ENV{HTTP_PROXY}
 	    keepalive => $keep_alive,
 	    body => $content,
-	    tls_ctx => $self->{tls_ctx},
+	    tls_ctx => AnyEvent::TLS->new(%{$tls}),
 	    sub {
 		my ($body, $hdr) = @_;
 
-- 
2.1.4





More information about the pve-devel mailing list