#!/usr/bin/perl use strict; use warnings; # complete rewrite of the sshkeys-lint program. Usage has changed, see # usage() function or run without arguments. use lib $ENV{GL_LIBDIR}; use Gitolite::Common; use Getopt::Long; my $admin = 0; my $quiet = 0; my $help = 0; GetOptions( 'admin|a=s' => \$admin, 'quiet|q' => \$quiet, 'help|h' => \$help ); use Data::Dumper; $Data::Dumper::Deepcopy = 1; $|++; my $in_gl_section = 0; my $warnings = 0; my $KEYTYPE_REGEX = qr/\b(?:ssh-(?:rsa|dss|ed25519)|ecdsa-sha2-nistp(?:256|384|521))\b/; sub msg { my $warning = shift; return if $quiet and not $warning; $warnings++ if $warning; print "sshkeys-lint: " . ( $warning ? "WARNING: " : "" ) . $_ for @_; } usage() if $help; our @pubkeyfiles = @ARGV; @ARGV = (); my $kd = "$ENV{HOME}/.gitolite/keydir"; if ( not @pubkeyfiles ) { chomp( @pubkeyfiles = `find $kd -type f -name "*.pub" | sort` ); } if ( -t STDIN ) { @ARGV = ("$ENV{HOME}/.ssh/authorized_keys"); } # ------------------------------------------------------------------------ my @authkeys; my %seen_fprints; my %pkf_by_fp; msg 0, "==== checking authkeys file:\n"; fill_authkeys(); # uses up STDIN if ($admin) { my $fp = fprint("$admin.pub"); my $fpu = ( $fp && $seen_fprints{$fp}{user} || 'no access' ); # dbg("fpu = $fpu, admin=$admin"); #<<< die "\t\t*** FATAL ***\n" . "$admin.pub maps to $fpu, not $admin.\n" . "You will not be able to access gitolite with this key.\n" . "Look for the 'ssh troubleshooting' link in http://gitolite.com/gitolite/ssh.html.\n" if $fpu ne "user $admin"; #>>> } msg 0, "==== checking pubkeys:\n" if @pubkeyfiles; for my $pkf (@pubkeyfiles) { # get the short name for the pubkey file ( my $pkfsn = $pkf ) =~ s(^$kd/)(); my $fp = fprint($pkf); next unless $fp; msg 1, "$pkfsn appears to be a COPY of $pkf_by_fp{$fp}\n" if $pkf_by_fp{$fp}; $pkf_by_fp{$fp} ||= $pkfsn; my $fpu = ( $seen_fprints{$fp}{user} || 'no access' ); msg 0, "$pkfsn maps to $fpu\n"; } if ($warnings) { print "\n$warnings warnings found\n"; } exit $warnings; # ------------------------------------------------------------------------ sub fill_authkeys { while (<>) { my $seq = $.; next if ak_comment($_); # also sets/clears $in_gl_section global my $fp = fprint($_); my $user = user($_); check( $seq, $fp, $user ); $authkeys[$seq]{fprint} = $fp; $authkeys[$seq]{ustatus} = $user; } } sub check { my ( $seq, $fp, $user ) = @_; msg 1, "line $seq, $user key found *outside* gitolite section!\n" if $user =~ /^user / and not $in_gl_section; msg 1, "line $seq, $user key found *inside* gitolite section!\n" if $user !~ /^user / and $in_gl_section; if ( $seen_fprints{$fp} ) { #<<< msg 1, "authkeys line $seq ($user) will be ignored by sshd; " . "same key found on line " . $seen_fprints{$fp}{seq} . " (" . $seen_fprints{$fp}{user} . ")\n"; return; #>>> } $seen_fprints{$fp}{seq} = $seq; $seen_fprints{$fp}{user} = $user; } sub user { my $user = ''; $user ||= "user $1" if /^command=.*gitolite-shell (.*?)"/; $user ||= "unknown command" if /^command/; $user ||= "shell access" if /$KEYTYPE_REGEX/; return $user; } sub ak_comment { local $_ = shift; $in_gl_section = 1 if /^# gitolite start/; $in_gl_section = 0 if /^# gitolite end/; die "gitosis? what's that?\n" if /^#.*gitosis/; return /^\s*(#|$)/; } sub fprint { local $_ = shift; my ($fp, $output); if ( /$KEYTYPE_REGEX/ ) { # an actual key was passed. ssh-keygen CAN correctly handle options on # the front of the key, so don't bother to strip them at all. ($fp, $output) = ssh_fingerprint_line($_); } else { # a filename was passed ($fp, $output) = ssh_fingerprint_file($_); # include the line of input as well, as it won't always be included by the ssh-keygen command warn "Bad line: $_\n" unless $fp; } # sshkeys-lint should only be run by a trusted admin, so we can give the output here. warn "$output\n" unless $fp; return $fp; } # ------------------------------------------------------------------------ =for usage Usage: gitolite sshkeys-lint [-q] [optional list of pubkey filenames] (optionally, STDIN can be a pipe or redirected from a file; see below) Look for potential problems in ssh keys. sshkeys-lint expects: - the contents of an authorized_keys file via STDIN, otherwise it uses \$HOME/.ssh/authorized_keys - one or more pubkey filenames as arguments, otherwise it uses all the keys found (recursively) in \$HOME/.gitolite/keydir The '-q' option will print only warnings instead of all mappings. Note that this runs ssh-keygen -l for each line in the authkeys file and each pubkey in the argument list, so be wary of running it on something huge. This is meant for troubleshooting. =cut