CentOS監控ssh免密登入

張京發表於2018-04-01

ssh免密登入在帶來方便的同時也帶來一些問題,那就是不知道什麼時間什麼人利用ssh免密通道登入伺服器了,為此我們需要在sshd的配置檔案裡設定好詳細日誌,以便日後回溯。

CentOS裡,sshd的日誌檔案是儲存在/var/log/secure裡的,如果不進行特殊設定的話,它只記錄一些最簡單的資訊,比如什麼時間哪個賬號被人用免密登入的方式登入了,如果這個賬號的authorized_keys裡有很多key的話,這樣的log沒有辦法告訴你客戶到底是用哪個key登入的。

所以,我們需要在sshd的配置檔案/etc/ssh/sshd_config檔案裡找到以下項LogLevel並把它改成LogLevel VERBOSE,這一項的預設值是INFO,不夠詳細,所以我們需要把它改成囉嗦模式,這樣可以記錄更多資訊。

改成VERBOSE並重啟sshd(service sshd restart)後,我們會在日誌檔案裡看到類似於這樣的記錄:

Apr  1 10:37:06 hostname sshd[5903]: Found matching RSA key: 83:67:b5:c7:bb:17:4d:06:ca:dc:8b:ca:85:cc:0c:b1

但這樣的資訊明顯不同於我們在authorized_keys裡存放的資訊,那該怎麼辦呢?實際上,在sshd的日誌檔案裡儲存的只是我們authorized_keys的指紋資訊fingerprint,不是真正的key,必須從authorized_keys反算出fingerprint來,才能做對比:

ssh-keygen -E md5 -lf /home/someuser/.ssh/authorized_keys

但是這樣依然很麻煩,有沒有辦法直接告訴我日誌裡到底是誰登入的呢?為此我們還需要引入一個用Perl寫的小程式。(原作者寫的略有問題,在新版的CentOS裡必須要求附加md5引數,為此我做了一些小的修改):

#!/usr/bin/perl -w
use strict;
use diagnostics;
use File::Temp;

# Matches Fingerprints from sshd logs (sshd on loglevel VERBOSE) against
# authorized_keys for the respective user.

die "Please specify input file!
" unless ($ARGV[0]);

my $fingerprints;

my $nav = File::Navigate->new($ARGV[0]);
# Store publickey login events
my @lines = @{$nav->find(qr/sshd[d+]: Accepted publickey for .+ from .+ port d+/)};

# Process all publickey login events
foreach(@lines){
    $nav->cursor($_);
    my $line = $nav->get();
    $line =~ /^(.{15}).+sshd[(d+)]: Accepted publickey for (.+) from (.+) port (d+)/;
    my $date = $1;
    my $pid  = $2;
    my $user = $3;
    my $ip   = $4;
    my $port = $5;
    my $fp   = "unknown"; # (yet)
    # Seek backwards to find matching fingerprint line
    my $sought = 0;
    while ((my $seekline = $nav->getprev()) and ($sought++ < 1000)){
        if ($seekline =~ /sshd[$pid]: Found matching .+ key: (.+)/){
            $fp = $1;
            last;
        }elsif($line =~ /sshd[$pid]: Connection from $ip port $port/){
            last;
        }
    }
    my $key = get_key($fp, $user);
    print ""$date";"$user";"$fp";"$key"
";
}

sub get_key{
    my $fp   = shift;
    $fp = "MD5:" . $fp;
    my $user = shift;

    # See if FP is cached
    if ($fingerprints->{$user}){
        if ($fingerprints->{$user}->{$fp}){
            return $fingerprints->{$user}->{$fp};
        }else{
            return "No matching key found.";
        }
    }

    # Else, generate fingerprints from users authorized_keys
    print STDERR "------> Reading keys for user $user
";
    my $home = (getpwnam($user))[7];
    open my $fh_in, "<$home/.ssh/authorized_keys" or warn "No such file: $home/.ssh/authorized_keys
";
    while (<$fh_in>){
        chomp;
        next unless (/^ssh-/);
        my $out_fh = File::Temp->new();
        print $out_fh "$_
";
        close $out_fh;
        my $fp_raw = `ssh-keygen -E md5 -lf $out_fh`;
        # Second field of output has the fingerpring
        my $fp = (split /s+/, $fp_raw)[1];
        $fingerprints->{$user}->{$fp} = $_;
    }
    if ($fingerprints->{$user}->{$fp}){
        return $fingerprints->{$user}->{$fp};
    }else{
        return "No matching key found.";
    }
}

package File::Navigate;
use strict;
use warnings;

=head1 NAME

File::Navigate - Navigate freely inside a text file

=head1 DESCRIPTION

The module is a glorified wrapper for tell() and seek().

It aims to simplify the creation of logfile analysis tools by
providing a facility to jump around freely inside the contents
of large files without creating the need to slurp excessive
amounts of data.

=head1 SYNOPSIS

  use File::Navigate;
  my $nav = File::Navigate->new(`/var/log/messages`);

  # Read what`s below the "cursor":
  my $first = $nav->get;

  # Advance the cursor before reading:
  my $second = $nav->getnext;
  my $third  = $nav->getnext;

  # Advance the cursor by hand:
  $nav->next;
  my $fourth = $nav->get;

  # Position the cursor onto an arbitrary line:
  $nav->cursor(10);
  my $tenth  = $nav->get;

  # Reverse the cursor one line backward:
  $nav->prev;
  my $ninth  = $nav->get;

  # Reverse the cursor before reading:
  my $eigth  = $nav->getprev;

  # Read an arbitrary line:
  my $sixth  = $nav->get(6);

=cut

our @ISA       = qw(Exporter);
our @EXPORT_OK = qw();
our $VERSION   = `1.0`;

=head1 CLASS METHODS

=head2 I<new()>

Open the file and create an index of the lines inside of it.

  my $mapper = File::Navigate->new($filename);

=cut

sub new($){
    my $class = shift;
    my $file;
    unless ($file = shift){
        die "No file specified
";
    }
    unless (-e $file){
        die "File not found: $file
";
    }
    unless (-r $file){
        die "File not readable: $file
";
    }
    my $self = {};
       $self->{`cursor`}         = 1;
       $self->{`lineindex`}      = {};
       $self->{`lineindex`}->{1} = 0;
    open my $fh, "$file"
        or die "Can`t open $file: $!
";
    while (<$fh>){
        my $thisline = $.;
        my $nextline = $thisline + 1;
        $self->{`lineindex`}->{$nextline} = tell $fh;
    }
    $self->{`length`} = scalar(keys %{$self->{`lineindex`}}) - 1 ;
    $self->{`fh`} = $fh;
    bless $self;
}

=head1 OBJECT METHODS

=head2 I<count()>

Returns the number of lines in the file ("wc -l")

  my $lines = $nav->count;

=cut

sub length(){
    my $self = shift;
    return $self->{`length`};
}

=head2 I<cursor()>

Returns the current cursor position and/or sets the cursor.

  my $cursor = $nav->cursor();   # Query cursor position.
  my $cursor = $nav->cursor(10); # Set cursor to line 10

=cut

sub cursor($){
    my $self = shift;
    if (my $goto = shift){
        $self->{`cursor`} = $goto;
    }
    return $self->{`cursor`};
}

=head2 I<get()>

Gets the line at the cursor position or at the given position.

  my $line = $nav->get();   # Get line at cursor
  my $line = $nav->get(10); # Get line 10

=cut

sub get($){
    my $self = shift;
    my $fh   = $self->{`fh`};

    my $getline;
    $getline = $self->{`cursor`} unless ($getline = shift);

    if ($getline < 1){
        warn "WARNING: Seek before first line.";
        return undef;
    }elsif($getline > $self->{`length`}){
        warn "WARNING: Seek beyond last line.";
        return undef;
    }
    seek ($fh, $self->{`lineindex`}->{$getline}, 0);
    my $gotline = <$fh>;
    chomp $gotline;
    return $gotline;
}

=head2 I<next()>

Advance the cursor position by one line. Returns the new cursor position.
Returns I<undef> if the cursor is already on the last line.

  my $newcursor = $nav->next();

=cut

sub next(){
    my $self = shift;
    if ($self->{`cursor`} == $self->{`length`}){
        return undef;
    }
    $self->{`cursor`}++;
    return $self->{`cursor`};
}

=head2 I<prev()>

Reverse the cursor position by one line. Returns the new cursor position.
Returns I<undef> if the cursor is already on line 1.

  my $newcursor = $nav->prev();

=cut

sub prev(){
    my $self = shift;
    if ($self->{`cursor`} == 1){
        return undef;
    }
    $self->{`cursor`}--;
    return $self->{`cursor`};
}

=head2 I<getnext()>

Advance to the next line and return it.
Returns I<undef> if the cursor is already on the last line.

  my $newcursor = $nav->getnext();

=cut

sub getnext(){
    my $self = shift;
    $self->next or return undef;
    return $self->get;
}

=head2 I<getprev()>

Reverse to the previous line and return it:
Returns I<undef> if the cursor is already on line 1.

  my $newcursor = $nav->getprev();

=cut

sub getprev(){
    my $self = shift;
    $self->prev or return undef;
    return $self->get;
}

=head2 I<find()>

Find lines containing given regex. Returns array with line numbers.

  my @lines = @{$nav->find(qr/foo/)};

=cut

sub find($){
    my $self = shift;
    my $regex = shift;

    my @results;
    for (my $lineno = 1; $lineno <= $self->{`length`}; $lineno++){
        my $line = $self->get($lineno);
            if ($line =~ $regex){
            push @results, $lineno;
        }
    }
    return @results;
}

sub DESTROY(){
    my $self = shift;
    close $self->{`fh`};
}

=head1 EXAMPLE

I<tac>, the opposite of I<cat>, in Perl using File::Navigate:

  #!/usr/bin/perl -w
  use strict;
  use File::Navigate;

  foreach my $file (reverse(@ARGV)){
          my $nav = File::Navigate->new($file);
          # Force cursor beyond last line
          $nav->cursor($nav->length()+1);
          print $nav->get()."
" while $nav->prev();
  }

=head1 BUGS

Seems to lack proper error handling.

=head1 LIMITATIONS

Works only on plain text files. Sockets, STDIO etc. are not supported.

=head1 PREREQUISITES

Tested on Perl 5.6.1.

=head1 STATUS

Mostly harmless.

=head1 AUTHOR

Martin Schmitt <mas at scsy dot de>

=cut

1;

為此我們給它起名叫match-ssh-keys,賦予它可執行許可權(chmod +x match-ssh-keys),然後把它搬到/usr/local/bin裡(mv match-ssh-keys /usr/local/bin/),這樣我們以後再想查誰通過sshd免密登入過伺服器就方便了,我們只需要執行:

match-ssh-keys /var/log/secure

就可以了。

相關文章