diff options
| author | Guilhem Moulin <guilhem@fripost.org> | 2016-12-01 00:16:18 +0100 | 
|---|---|---|
| committer | Guilhem Moulin <guilhem@fripost.org> | 2016-12-01 00:16:18 +0100 | 
| commit | 27788fd4a399642eddbdb1934ccaa13f7fd00124 (patch) | |
| tree | 80ac319fdaec806a82dfc04e1453054ca199683a | |
| parent | dd1da2ac44a7eab89e9a17135367aa0915efad0b (diff) | |
Make lacme able to spawn lacme-accountd.
| -rw-r--r-- | config/lacme.conf | 41 | ||||
| -rwxr-xr-x | lacme | 77 | ||||
| -rwxr-xr-x | lacme-accountd | 37 | ||||
| -rw-r--r-- | lacme.md | 42 | 
4 files changed, 159 insertions, 38 deletions
| diff --git a/config/lacme.conf b/config/lacme.conf index d64276c..23313c7 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -4,12 +4,15 @@  #config-certs = /etc/lacme/lacme-certs.conf  [client] -# The value of "socket" specifies the lacme-accountd(1) UNIX-domain -# socket to connect to for signature requests from the ACME client. -# lacme(1) aborts if the socket is readable or writable by other users, -# or if its parent directory is writable by other users. +# The value of "socket" specifies the path to the lacme-accountd(1) +# UNIX-domain socket to connect to for signature requests from the ACME +# client.  lacme(1) aborts if the socket is readable or writable by +# other users, or if its parent directory is writable by other users.  # Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR environment  # variable is set. +# This option is ignored when lacme-accountd(1) is spawned by lacme(1), +# since the two processes communicate through a socket pair.  See the +# "accountd" section below for details.  #  #socket = /run/user/1000/S.lacme @@ -83,4 +86,34 @@  #  #iptables = Yes + +# lacme-accound(1) section.  Comment out the following section to make +# lacme(1) connect to an existing UNIX-domain socket bound by a running +# acme-accountd(1). +[accountd] + +# username to drop privileges to (setting both effective and real uid). +# Preserve root privileges if the value is empty. +# +#user = root + +# groupname to drop privileges to (setting both effective and real gid, +# and also setting the list of supplementary gids to that single group). +# Preserve root privileges if the value is empty. +# +#group = root + +# Path to the lacme-accountd(1) executable. +#command = /usr/bin/lacme-accountd + +# Path to the lacme-accountd(1) configuration file. +#config = /etc/lacme/lacme-accountd.conf + +# The (private) account key to use for signing requests.  See +# lacme-accountd(1) for details. +#privkey = file:/path/to/account.key + +# Be quiet. +#quiet = Yes +  ; vim:ft=dosini @@ -30,7 +30,7 @@ use File::Temp ();  use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/;  use List::Util 'first';  use POSIX (); -use Socket qw/PF_INET PF_INET6 PF_UNIX INADDR_ANY IN6ADDR_ANY +use Socket qw/AF_UNIX PF_INET PF_INET6 PF_UNIX PF_UNSPEC INADDR_ANY IN6ADDR_ANY                SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/;  use Config::Tiny (); @@ -82,6 +82,7 @@ do {      my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n";      my $defaults = delete $h->{_} // {}; +    my $accountd = exists $h->{accountd} ? 1 : 0;      my %valid = (          client => {              socket  => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef), @@ -99,6 +100,14 @@ do {              command               => '/usr/lib/lacme/webserver',              iptables              => 'Yes' +        }, +        accountd => { +            user    => '', +            group   => '', +            command => '/usr/bin/lacme-accountd', +            config  => '/etc/lacme/lacme-accountd.conf', +            privkey => undef, +            quiet   => 'Yes',          }      );      foreach my $s (keys %valid) { @@ -110,6 +119,7 @@ do {      }      die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h;      $CONFIG->{_} = $defaults; +    delete $CONFIG->{accountd} unless $accountd;  };  # Regular expressions for domain validation @@ -388,31 +398,58 @@ sub acme_client($@) {      my $args = shift;      my @args = @_; -    my @stat; +    my $client;      my $conf = $CONFIG->{client}; -    my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n"; -    $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname - -    # ensure we're the only user with write access to the parent dir -    my $dirname = $sockname =~ s/[^\/]+$//r; -    @stat = stat($dirname) or die "Can't stat $dirname: $!"; -    die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; - -    # ensure we're the only user with read/write access to the socket -    @stat = stat($sockname) or die "Can't stat $sockname: $!  (Is lacme-accountd running?)\n"; -    die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; - -    # connect(2) to the socket -    socket(my $client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; -    my $sockaddr = Socket::sockaddr_un($sockname) // die "Invalid address $sockname\n"; -    until (connect($client, $sockaddr)) { -        next if $! == EINTR; # try again if connect(2) was interrupted by a signal -        die "connect: $!"; +    if (defined (my $accountd = $CONFIG->{accountd})) { +        socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; +        my $pid = fork() // "fork: $!"; +        unless ($pid) { +            drop_privileges($accountd->{user}, $accountd->{group}, '/'); +            set_FD_CLOEXEC($s, 0); +            $client->close() or die "Can't close: $!"; +            my @cmd = ($accountd->{command}, '--fdopen='.fileno($s)); +            push @cmd, '--config='.$accountd->{config}   if defined $accountd->{config}; +            push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; +            push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no'; +            push @cmd, '--debug' if $OPTS{debug}; +            exec { $cmd[0] } @cmd or die; +        } +        print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; +        $s->close() or die "Can't close: $!"; +        push @CLEANUP, sub() { +            print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug}; +            shutdown($client, SHUT_RDWR) or warn "shutdown: $!"; +            kill 15 => $pid; +            waitpid $pid => 0; +        }; +    } +    else { +        my @stat; +        my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n"; +        $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname + +        # ensure we're the only user with write access to the parent dir +        my $dirname = $sockname =~ s/[^\/]+$//r; +        @stat = stat($dirname) or die "Can't stat $dirname: $!"; +        die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; + +        # ensure we're the only user with read/write access to the socket +        @stat = stat($sockname) or die "Can't stat $sockname: $!  (Is lacme-accountd running?)\n"; +        die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; + +        # connect(2) to the socket +        socket($client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; +        my $sockaddr = Socket::sockaddr_un($sockname) // die "Invalid address $sockname\n"; +        until (connect($client, $sockaddr)) { +            next if $! == EINTR; # try again if connect(2) was interrupted by a signal +            die "connect: $!"; +        }      }      # use execve(2) rather than a Perl pseudo-process to ensure that the      # child doesn't have access to the parent's memory      my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($CONFFILE, $client); # untaint fileno +    set_FD_CLOEXEC($client, 1);      spawn({%$args{qw/in out/}, child => sub() {          drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/');          set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); diff --git a/lacme-accountd b/lacme-accountd index fbf1bcb..411538d 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -59,7 +59,7 @@ sub usage(;$$) {      }      exit $rv;  } -usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s quiet|q debug help|h/); +usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s fdopen=i quiet|q debug help|h/);  usage(0) if $OPTS{help};  do { @@ -137,7 +137,10 @@ $JWK = JSON::->new->encode($JWK);  # to support the abstract namespace.)  The downside is that we have to  # delete the file manually.  # -do { +if (defined $OPTS{fdopen}) { +    die "Invalid file descriptor" unless $OPTS{fdopen} =~ /\A(\d+)\z/; +    open $S, '+<&=', $1 or die "fdopen $1: $!"; +} else {      my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef);      die "Missing socket option\n" unless defined $sockname;      $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname @@ -165,26 +168,34 @@ do {  # For each new connection, send the protocol version and the account key's  # public parameters, then sign whatever comes in  # -$SIG{PIPE} = 'IGNORE'; # ignore broken pipes -for (my $count = 0;; $count++) { -    accept(my $conn, $S) or do { -        next if $! == EINTR; # try again if accept(2) was interrupted by a signal -        die "accept: $!"; -    }; -    print STDERR "[$count]>> Accepted new connection\n" unless $OPTS{quiet}; - +sub conn($;$) { +    my $conn = shift; +    my $count = shift;      $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK, "\r\n" );      # sign whatever comes in      while (defined (my $data = $conn->getline())) {          $data =~ s/\r\n\z// or die; -        print STDERR "[$count]>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; +        print STDERR "[$count] >>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet};          my $sig = $SIGN->($data);          $conn->printflush( encode_base64url($sig), "\r\n" );      } +} -    print STDERR "[$count]>> Connection terminated\n" unless $OPTS{quiet}; -    close $conn or warn "Can't close: $!"; +if (defined $OPTS{fdopen}) { +    conn($S, $$); +} else { +    $SIG{PIPE} = 'IGNORE'; # ignore broken pipes +    for (my $count = 0;; $count++) { +        accept(my $conn, $S) or do { +            next if $! == EINTR; # try again if accept(2) was interrupted by a signal +            die "accept: $!"; +        }; +        print STDERR "[$count] >>> Accepted new connection\n" unless $OPTS{quiet}; +        conn($conn, $count); +        print STDERR "[$count] >>> Connection terminated\n" unless $OPTS{quiet}; +        close $conn or warn "Can't close: $!"; +    }  } @@ -26,7 +26,9 @@ with its own executable:      the [ACME] client.)      One can use the UNIX-domain socket forwarding facility of OpenSSH      6.7 and later to run [`lacme-accountd`(1)] and `lacme` on different -    hosts. +    hosts.  Alternatively, the [`lacme-accountd`(1)] process can be +    spawned by the “master” `lacme` process below; the communication +    between the two then goes through a socket pair.   2. A “master” `lacme` process, which runs as root and is the only      component with access to the private key material of the server @@ -249,6 +251,44 @@ This section is used for configuring the [ACME] webserver.      automatically removed once `lacme` exits.      Default: `Yes`. +`[accountd]` section +--------------------- + +This section is used for configuring the [`lacme-accountd`(1)] process. +If the section (including its header) is absent or commented out, +`lacme` connects to an existing UNIX-domain socket bound by a running +[`lacme-accountd`(1)]. + +*user* + +:   The username to drop privileges to (setting both effective and real +    uid).  Preserve root privileges if the value is empty. + +*group* + +:   The groupname to drop privileges to (setting both effective and real +    gid, and also setting the list of supplementary gids to that single +    group). + +*command* + +:   Path to the [`lacme-accountd`(1)] executable. +    Default: `/usr/bin/lacme-accountd`. + +*config* + +:   Path to the [`lacme-accountd`(1)] configuration file. +    Default: `/etc/lacme/lacme-accountd.conf`. + +*privkey* + +:   The (private) account key to use for signing requests.  See +    [`lacme-accountd`(1)] for details. + +*quiet* + +:   Be quiet. Possible values: `Yes`/`No`. +  Certificate configuration file  ============================== | 
