]> git.proxmox.com Git - pve-manager-legacy.git/commitdiff
enable certificate pinning for proxied requests
authorFabian Grünbichler <f.gruenbichler@proxmox.com>
Thu, 17 Nov 2016 10:36:32 +0000 (11:36 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Thu, 17 Nov 2016 14:55:17 +0000 (15:55 +0100)
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
(at most once per minute) if the actual encountered
fingerprint does not match or every 30 minutes (to clear out
old entries).

PVE/HTTPServer.pm

index 1c7d033fae8bd0fbea83a10fa048b4f396e9cae1..db1faae1ff7a2ca6c47db6e94d360342f974434a 100755 (executable)
@@ -105,6 +105,96 @@ sub get_login_formatter {
     return $info->{func};
 }
 
+my $cert_cache_nodes = {};
+my $cert_cache_timestamp = time();
+my $cert_cache_fingerprints = {};
+sub update_cert_cache {
+    my ($update_node, $clear) = @_;
+
+    syslog('info', "Clearing outdated entries from certificate cache")
+       if $clear;
+    $cert_cache_timestamp = time() if !defined($update_node);
+
+    my $node_list = defined($update_node) ? [ $update_node ] : [ keys %$cert_cache_nodes ];
+
+    foreach my $node (@$node_list) {
+       my $clear_old = sub {
+           if (my $old_fp = $cert_cache_nodes->{$node}) {
+               # distrust old fingerprint
+               delete $cert_cache_fingerprints->{$old_fp};
+               # ensure reload on next proxied request
+               delete $cert_cache_nodes->{$node};
+           }
+       };
+
+       my $err;
+       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 $cert;
+       eval {
+           my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r');
+           $cert = Net::SSLeay::PEM_read_bio_X509($bio);
+           Net::SSLeay::BIO_free($bio);
+       };
+       $err = $@;
+       if ($err || !defined($cert)) {
+           &$clear_old() if $clear;
+           next;
+       }
+
+       my $fp;
+       eval {
+           $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
+       };
+       $err = $@;
+       if ($err || !defined($fp) || $fp eq '') {
+           &$clear_old() if $clear;
+           next;
+       }
+
+       my $old_fp = $cert_cache_nodes->{$node};
+       $cert_cache_fingerprints->{$fp} = 1;
+       $cert_cache_nodes->{$node} = $fp;
+
+       if (defined($old_fp) && $fp ne $old_fp) {
+           delete $cert_cache_fingerprints->{$old_fp};
+       }
+    }
+}
+
+sub check_cert_fingerprint {
+    my ($cert) = @_;
+
+    # clear cache every 30 minutes at least
+    update_cert_cache(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
+
+    # get fingerprint of server certificate
+    my $fp;
+    eval {
+       $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
+    };
+    return 0 if $@ || !defined($fp) || $fp eq ''; # error
+
+    my $check = sub {
+       for my $expected (keys %$cert_cache_fingerprints) {
+           return 1 if $fp eq $expected;
+       }
+       return 0;
+    };
+
+    return 1 if &$check();
+
+    # clear cache and retry at most once every minute
+    if (time() - $cert_cache_timestamp >= 60) {
+       syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
+       update_cert_cache();
+       return &$check();
+    }
+
+    return 0;
+}
+
 # server implementation
 
 sub log_request {
@@ -601,6 +691,25 @@ 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;
+               # check server certificate against cache of pinned FPs
+               return check_cert_fingerprint($cert);
+           },
+       };
+
+       # 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 +718,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) = @_;