* [PATCH] daemon: reload TLS certs and keys on SIGHUP
@ 2022-08-03 8:06 Eric Wong
2022-08-03 8:16 ` Eric Wong
0 siblings, 1 reply; 2+ messages in thread
From: Eric Wong @ 2022-08-03 8:06 UTC (permalink / raw)
To: meta
This allows new TLS certificates to be loaded for new clients
without having to timeout or drop existing clients with
established connections on the . This should benefit users with
admins who expire certificates frequently (as encouraged by
Let's Encrypt).
---
lib/PublicInbox/Daemon.pm | 54 +++++++++++++----------------------
lib/PublicInbox/IMAP.pm | 10 +++----
lib/PublicInbox/IMAPD.pm | 2 +-
lib/PublicInbox/NNTP.pm | 10 +++----
lib/PublicInbox/NNTPD.pm | 2 +-
lib/PublicInbox/POP3.pm | 10 +++----
lib/PublicInbox/POP3D.pm | 2 +-
lib/PublicInbox/TLS.pm | 28 +++++++++++++++++--
t/httpd-https.t | 59 +++++++++++++++++++++++++++++----------
9 files changed, 107 insertions(+), 70 deletions(-)
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index 20b07b83..67b26d2e 100644
--- a/lib/PublicInbox/Daemon.pm
+++ b/lib/PublicInbox/Daemon.pm
@@ -29,7 +29,7 @@ my (@cfg_listen, $stdout, $stderr, $group, $user, $pid_file, $daemonize);
my $worker_processes = 1;
my @listeners;
my (%pids, %logs);
-my %tls_opt; # scheme://sockname => args for IO::Socket::SSL->start_SSL
+my %tls_opt; # scheme://sockname => args for IO::Socket::SSL::SSL_Context->new
my $reexec_pid;
my ($uid, $gid);
my ($default_cert, $default_key);
@@ -55,43 +55,31 @@ sub listener_opt ($) {
$o;
}
+sub check_absolute ($$) {
+ my ($var, $val) = @_;
+ die <<EOM if index($val // '/', '/') != 0;
+$var must be an absolute path when using --daemonize: $val
+EOM
+}
+
sub accept_tls_opt ($) {
my ($opt) = @_;
my $o = ref($opt) eq 'HASH' ? $opt : listener_opt($opt);
return if !defined($o->{cert});
require PublicInbox::TLS;
- my %ctx_opt = (SSL_server => 1);
+ my @ctx_opt;
# parse out hostname:/path/to/ mappings:
for my $k (qw(cert key)) {
$o->{$k} // next;
- my $x = $ctx_opt{'SSL_'.$k.'_file'} = {};
+ push(@ctx_opt, "SSL_${k}_file", {});
foreach my $path (@{$o->{$k}}) {
my $host = '';
$path =~ s/\A([^:]+):// and $host = $1;
- $x->{$host} = $path;
+ $ctx_opt[-1]->{$host} = $path;
check_absolute($k, $path) if $daemonize;
}
}
- my $ctx = IO::Socket::SSL::SSL_Context->new(%ctx_opt) or
- die 'SSL_Context->new: '.PublicInbox::TLS::err();
-
- # save ~34K per idle connection (cf. SSL_CTX_set_mode(3ssl))
- # RSS goes from 346MB to 171MB with 10K idle NNTPS clients on amd64
- # cf. https://rt.cpan.org/Ticket/Display.html?id=129463
- my $mode = eval { Net::SSLeay::MODE_RELEASE_BUFFERS() };
- if ($mode && $ctx->{context}) {
- eval { Net::SSLeay::CTX_set_mode($ctx->{context}, $mode) };
- warn "W: $@ (setting SSL_MODE_RELEASE_BUFFERS)\n" if $@;
- }
-
- { SSL_server => 1, SSL_startHandshake => 0, SSL_reuse_ctx => $ctx };
-}
-
-sub check_absolute ($$) {
- my ($var, $val) = @_;
- die <<EOM if index($val // '/', '/') != 0;
-$var must be an absolute path when using --daemonize: $val
-EOM
+ \@ctx_opt;
}
sub do_chown ($) {
@@ -637,12 +625,11 @@ EOF
exit # never gets here, just for documentation
}
-sub tls_start_cb ($$) {
- my ($opt, $orig_post_accept) = @_;
+sub tls_cb {
+ my ($post_accept, $tlsd) = @_;
sub {
my ($io, $addr, $srv) = @_;
- my $ssl = IO::Socket::SSL->start_SSL($io, %$opt);
- $orig_post_accept->($ssl, $addr, $srv);
+ $post_accept->(PublicInbox::TLS::start($io, $tlsd), $addr, $srv)
}
}
@@ -669,21 +656,20 @@ sub daemon_loop ($) {
my $refresh = sub {
my ($sig) = @_;
for my $xn (values %$xnetd) {
+ delete $xn->{tlsd}->{ssl_ctx}; # PublicInbox::TLS::start
eval { $xn->{refresh}->($sig) };
warn "refresh $@\n" if $@;
}
};
my %post_accept;
- while (my ($k, $v) = each %tls_opt) {
+ while (my ($k, $ctx_opt) = each %tls_opt) {
my $l = $k;
$l =~ s!\A([^:]+)://!!;
my $scheme = $1 // '';
my $xn = $xnetd->{$l} // $xnetd->{''};
- if ($scheme =~ m!\A(?:https|imaps|nntps|pop3s)!) {
- $post_accept{$l} = tls_start_cb($v, $xn->{post_accept});
- } elsif ($xn->{tlsd}) { # STARTTLS, $k eq '' is OK
- $xn->{tlsd}->{accept_tls} = $v;
- }
+ $xn->{tlsd}->{ssl_ctx_opt} //= $ctx_opt;
+ $scheme =~ m!\A(?:https|imaps|nntps|pop3s)! and
+ $post_accept{$l} = tls_cb(@$xn{qw(post_accept tlsd)});
}
my $sig = {
HUP => $refresh,
diff --git a/lib/PublicInbox/IMAP.pm b/lib/PublicInbox/IMAP.pm
index 0f0f9b3a..19ead70c 100644
--- a/lib/PublicInbox/IMAP.pm
+++ b/lib/PublicInbox/IMAP.pm
@@ -121,7 +121,7 @@ sub capa ($) {
$capa .= ' COMPRESS=DEFLATE';
} else {
if (!($self->{sock} // $self)->can('accept_SSL') &&
- $self->{imapd}->{accept_tls}) {
+ $self->{imapd}->{ssl_ctx_opt}) {
$capa .= ' STARTTLS';
}
$capa .= ' AUTH=ANONYMOUS';
@@ -1230,14 +1230,12 @@ sub cmd_compress ($$$) {
sub cmd_starttls ($$) {
my ($self, $tag) = @_;
- my $sock = $self->{sock} or return;
- if ($sock->can('stop_SSL') || $self->compressed) {
+ (($self->{sock} // return)->can('stop_SSL') || $self->compressed) and
return "$tag BAD TLS or compression already enabled\r\n";
- }
- my $opt = $self->{imapd}->{accept_tls} or
+ $self->{imapd}->{ssl_ctx_opt} or
return "$tag BAD can not initiate TLS negotiation\r\n";
$self->write(\"$tag OK begin TLS negotiation now\r\n");
- $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
+ PublicInbox::TLS::start($self->{sock}, $self->{imapd});
$self->requeue if PublicInbox::DS::accept_tls_step($self);
undef;
}
diff --git a/lib/PublicInbox/IMAPD.pm b/lib/PublicInbox/IMAPD.pm
index 9a5bdcfe..6038fd88 100644
--- a/lib/PublicInbox/IMAPD.pm
+++ b/lib/PublicInbox/IMAPD.pm
@@ -18,7 +18,7 @@ sub new {
mailboxes => {},
err => \*STDERR,
out => \*STDOUT,
- # accept_tls => { SSL_server => 1, ..., SSL_reuse_ctx => ... }
+ # ssl_ctx_opt => { SSL_cert_file => ..., SSL_key_file => ... }
# pi_cfg => PublicInbox::Config
# idler => PublicInbox::InboxIdle
}, $class;
diff --git a/lib/PublicInbox/NNTP.pm b/lib/PublicInbox/NNTP.pm
index 791fe2a9..9ae1353a 100644
--- a/lib/PublicInbox/NNTP.pm
+++ b/lib/PublicInbox/NNTP.pm
@@ -85,7 +85,7 @@ sub cmd_capabilities ($;$) {
my ($self, undef) = @_;
my $res = $CAPABILITIES;
if (!$self->{sock}->can('accept_SSL') &&
- $self->{nntpd}->{accept_tls}) {
+ $self->{nntpd}->{ssl_ctx_opt}) {
$res .= "STARTTLS\r\n";
}
$res .= ".\r\n";
@@ -885,13 +885,13 @@ sub cmd_xover ($;$) {
sub cmd_starttls ($) {
my ($self) = @_;
- my $sock = $self->{sock} or return;
# RFC 4642 2.2.1
- return r502 if ($sock->can('accept_SSL') || $self->compressed);
- my $opt = $self->{nntpd}->{accept_tls} or
+ (($self->{sock} // return)->can('stop_SSL') || $self->compressed) and
+ return r502;
+ $self->{nntpd}->{ssl_ctx_opt} or
return \"580 can not initiate TLS negotiation\r\n";
$self->write(\"382 Continue with TLS negotiation\r\n");
- $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
+ PublicInbox::TLS::start($self->{sock}, $self->{nntpd});
$self->requeue if PublicInbox::DS::accept_tls_step($self);
undef;
}
diff --git a/lib/PublicInbox/NNTPD.pm b/lib/PublicInbox/NNTPD.pm
index 9e232ef6..15a72bac 100644
--- a/lib/PublicInbox/NNTPD.pm
+++ b/lib/PublicInbox/NNTPD.pm
@@ -17,7 +17,7 @@ sub new {
err => \*STDERR,
out => \*STDOUT,
# pi_cfg => $pi_cfg,
- # accept_tls => { SSL_server => 1, ..., SSL_reuse_ctx => ... }
+ # ssl_ctx_opt => { SSL_cert_file => ..., SSL_key_file => ... }
# idler => PublicInbox::InboxIdle
}, $class;
}
diff --git a/lib/PublicInbox/POP3.pm b/lib/PublicInbox/POP3.pm
index 203c91a6..7469922b 100644
--- a/lib/PublicInbox/POP3.pm
+++ b/lib/PublicInbox/POP3.pm
@@ -130,12 +130,12 @@ sub cmd_pass {
sub cmd_stls {
my ($self) = @_;
- my $sock = $self->{sock} or return;
- return \"-ERR TLS already enabled\r\n" if $sock->can('stop_SSL');
- my $opt = $self->{pop3d}->{accept_tls} or
+ ($self->{sock} // return)->can('stop_SSL') and
+ return \"-ERR TLS already enabled\r\n";
+ $self->{pop3d}->{ssl_ctx_opt} or
return \"-ERR can't start TLS negotiation\r\n";
$self->write(\"+OK begin TLS negotiation now\r\n");
- $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
+ PublicInbox::TLS::start($self->{sock}, $self->{pop3d});
$self->requeue if PublicInbox::DS::accept_tls_step($self);
undef;
}
@@ -281,7 +281,7 @@ sub cmd_dele {
sub cmd_capa {
my ($self) = @_;
my $STLS = !$self->{ibx} && !$self->{sock}->can('stop_SSL') &&
- $self->{pop3d}->{accept_tls} ? "\nSTLS\r" : '';
+ $self->{pop3d}->{ssl_ctx_opt} ? "\nSTLS\r" : '';
$self->{expire} = ''; # "EXPIRE 0" allows clients to avoid DELE commands
<<EOM;
+OK Capability list follows\r
diff --git a/lib/PublicInbox/POP3D.pm b/lib/PublicInbox/POP3D.pm
index 5cfe9613..764f9ffe 100644
--- a/lib/PublicInbox/POP3D.pm
+++ b/lib/PublicInbox/POP3D.pm
@@ -45,7 +45,7 @@ sub new {
# lock_path => ...
# interprocess lock is the $pop3state/txn.locks file
# txn_locks => {}, # intraworker locks
- # accept_tls => { SSL_server => 1, ..., SSL_reuse_ctx => ... }
+ # ssl_ctx_opt => { SSL_cert_file => ..., SSL_key_file => ... }
}, $cls;
}
diff --git a/lib/PublicInbox/TLS.pm b/lib/PublicInbox/TLS.pm
index 3fe16a62..3ce57f1b 100644
--- a/lib/PublicInbox/TLS.pm
+++ b/lib/PublicInbox/TLS.pm
@@ -1,4 +1,4 @@
-# Copyright (C) 2019-2021 all contributors <meta@public-inbox.org>
+# Copyright (C) all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
# IO::Socket::SSL support code
@@ -6,7 +6,7 @@ package PublicInbox::TLS;
use strict;
use IO::Socket::SSL;
use PublicInbox::Syscall qw(EPOLLIN EPOLLOUT);
-use Carp qw(carp);
+use Carp qw(carp croak);
sub err () { $SSL_ERROR }
@@ -18,4 +18,28 @@ sub epollbit () {
undef;
}
+sub _ctx_new ($) {
+ my ($tlsd) = @_;
+ my $ctx = IO::Socket::SSL::SSL_Context->new(
+ @{$tlsd->{ssl_ctx_opt}}, SSL_server => 1) or
+ croak "SSL_Context->new: $SSL_ERROR";
+
+ # save ~34K per idle connection (cf. SSL_CTX_set_mode(3ssl))
+ # RSS goes from 346MB to 171MB with 10K idle NNTPS clients on amd64
+ # cf. https://rt.cpan.org/Ticket/Display.html?id=129463
+ my $mode = eval { Net::SSLeay::MODE_RELEASE_BUFFERS() };
+ if ($mode && $ctx->{context}) {
+ eval { Net::SSLeay::CTX_set_mode($ctx->{context}, $mode) };
+ warn "W: $@ (setting SSL_MODE_RELEASE_BUFFERS)\n" if $@;
+ }
+ $ctx;
+}
+
+sub start {
+ my ($io, $tlsd) = @_;
+ IO::Socket::SSL->start_SSL($io, SSL_server => 1,
+ SSL_reuse_ctx => ($tlsd->{ssl_ctx} //= _ctx_new($tlsd)),
+ SSL_startHandshake => 0);
+}
+
1;
diff --git a/t/httpd-https.t b/t/httpd-https.t
index d42d7c50..b0cd7eab 100644
--- a/t/httpd-https.t
+++ b/t/httpd-https.t
@@ -1,15 +1,15 @@
-# Copyright (C) 2019-2021 all contributors <meta@public-inbox.org>
+#!perl -w
+# Copyright (C) all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
-use strict;
-use warnings;
-use Test::More;
+use v5.12;
use Socket qw(SOCK_STREAM IPPROTO_TCP SOL_SOCKET);
use PublicInbox::TestCommon;
+use File::Copy qw(cp);
# IO::Poll is part of the standard library, but distros may split them off...
require_mods(qw(IO::Socket::SSL IO::Poll Plack::Util));
-my $cert = 'certs/server-cert.pem';
-my $key = 'certs/server-key.pem';
-unless (-r $key && -r $cert) {
+my @certs = qw(certs/server-cert.pem certs/server-key.pem
+ certs/server2-cert.pem certs/server2-key.pem);
+if (scalar(grep { -r $_ } @certs) != scalar(@certs)) {
plan skip_all =>
"certs/ missing for $0, run $^X ./create-certs.perl in certs/";
}
@@ -22,6 +22,20 @@ my $out = "$tmpdir/stdout.log";
my $https = tcp_server();
my $td;
my $https_addr = tcp_host_port($https);
+my $cert = "$tmpdir/cert.pem";
+my $key = "$tmpdir/key.pem";
+cp('certs/server-cert.pem', $cert) or xbail $!;
+cp('certs/server-key.pem', $key) or xbail $!;
+
+my $check_url_scheme = sub {
+ my ($s, $line) = @_;
+ $s->print("GET /url_scheme HTTP/1.1\r\n\r\nHost: example.com\r\n\r\n")
+ or xbail "failed to write HTTP request: $! (line $line)";
+ my $buf = '';
+ sysread($s, $buf, 2007, length($buf)) until $buf =~ /\r\n\r\nhttps?/;
+ like($buf, qr!\AHTTP/1\.1 200!, "read HTTPS response (line $line)");
+ like($buf, qr!\r\nhttps\z!, "psgi.url_scheme is 'https' (line $line)");
+};
for my $args (
[ "-lhttps://$https_addr/?key=$key,cert=$cert" ],
@@ -53,12 +67,7 @@ for my $args (
# normal HTTPS
my $c = tcp_connect($https);
IO::Socket::SSL->start_SSL($c, %o);
- $c->print("GET /url_scheme HTTP/1.1\r\n\r\nHost: example.com\r\n\r\n")
- or xbail "failed to write HTTP request: $!";
- my $buf = '';
- sysread($c, $buf, 2007, length($buf)) until $buf =~ /\r\n\r\nhttps?/;
- like($buf, qr!\AHTTP/1\.1 200!, 'read HTTP response');
- like($buf, qr!\r\nhttps\z!, "psgi.url_scheme is 'https'");
+ $check_url_scheme->($c, __LINE__);
# HTTPS with bad hostname
$c = tcp_connect($https);
@@ -81,7 +90,7 @@ for my $args (
$slow->blocking(1);
ok($slow->print("GET /empty HTTP/1.1\r\n\r\nHost: example.com\r\n\r\n"),
'wrote HTTP request from slow');
- $buf = '';
+ my $buf = '';
sysread($slow, $buf, 666, length($buf)) until $buf =~ /\r\n\r\n/;
like($buf, qr!\AHTTP/1\.1 200!, 'read HTTP response from slow');
$slow = undef;
@@ -105,7 +114,27 @@ for my $args (
like($x, qr/\Adataready\0+\z/, 'got dataready accf for https');
};
- $c = undef;
+ # switch cert and key:
+ cp('certs/server2-cert.pem', $cert) or xbail $!;
+ cp('certs/server2-key.pem', $key) or xbail $!;
+ $td->kill('HUP') or xbail "kill: $!";
+ tick(); # wait for SIGHUP to take effect (hopefully :x)
+
+ my $d = tcp_connect($https);
+ $d = IO::Socket::SSL->start_SSL($d, %o);
+ is($d, undef, 'HTTPS fails with bad hostname after new cert on HUP');
+
+ $d = tcp_connect($https);
+ $o{SSL_hostname} = $o{SSL_verifycn_name} = 'server2.local';
+ is(IO::Socket::SSL->start_SSL($d, %o), $d,
+ 'new hostname to match cert works after HUP');
+ $check_url_scheme->($d, __LINE__);
+
+ # existing connection w/ old cert still works:
+ $check_url_scheme->($c, __LINE__);
+
+ undef $c;
+ undef $d;
$td->kill;
$td->join;
is($?, 0, 'no error in exited process');
^ permalink raw reply related [flat|nested] 2+ messages in thread
end of thread, other threads:[~2022-08-03 8:16 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2022-08-03 8:06 [PATCH] daemon: reload TLS certs and keys on SIGHUP Eric Wong
2022-08-03 8:16 ` Eric Wong
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).