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
就可以了。