[project @ peter@palfrader.org-20081125220925-7e5fhpc5e4jmjc88]
[mirror/dsa-nagios.git] / dsa-nagios-nrpe-config / weak-ssh-keys-check
1 #!/usr/bin/perl
2
3 # This cheak is based on code from the Debian/OpenSSL Weak Key Detector
4 # written by Florian Weimer <fw@deneb.enyo.de>. 
5 # The code has been modified and enhanced by Alexander Wirt 
6 # <formorer@debian.org> to use it as a nagios check. 
7 #
8 # Copyright (c) 2008, Florian Weimer <fw@deneb.enyo.de> for the original 
9 # Debian/OpenSSL Weak Key Detector 
10 # (http://security.debian.org/project/extra/dowkd/dowkd.pl.gz)
11 #
12 # Copyright (c) 2008, Alexander Wirt <formorer@debian.org> for check_weakkeys
13 #
14 # Permission to use, copy, modify, and/or distribute this software for any
15 # purpose with or without fee is hereby granted, provided that the above
16 # copyright notice and this permission notice appear in all copies.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
19 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
20 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
21 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
23 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
24 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
25 #
26
27 =pod
28
29 =head1 NAME
30
31 B<check_weakkeys> - checks system for weak ssh keys 
32
33 =cut
34
35 =head1 SYNOPSIS
36
37 B<check_weakkeys> [options]
38
39 =cut
40
41 =head1 DESCRIPTION
42
43 B<check_weakkeys> checks for all users if there id_rsa, id_dsa or
44 authorized_key files if they contain weak ssh keys created by a Debian with a
45 broken libssl (see DSA-1571 for more informations). Optionally <check_weakkeys>
46 can spit out a warning of there are any DSA keys left in key or authorized_key
47 files. To work it needs a database of precomputed hashes of known weak keys.
48 This file is expected as an bdb database with the hash (like
49 03:a2:f0:46:7f:13:9f:5f:96:71:a9:b8:a0:1c:01:05) as key. See <gen_fprdb> for
50 such a database generator.  <check_weakkeys> outputs his data to STDOUT or to a
51 file. It meaned to be picked up by an nagios check like B<dsa-check-statusfile>
52 from Peter Palfrader. 
53
54 =cut
55
56 =head1 OPTIONS
57
58 =over 4
59
60 =item B<-h, --help>
61
62 Prints out a brief help
63
64 =item B<-s, --statusfile> "statusfile"
65
66 Use 'F<statusfile>' instead of 'F<STDOUT>'. 
67
68 =item B<-f, --fprdb> "database" (default: /var/lib/dsa/ssh-weak-keys.db)
69
70 Use 'F<database>' instead of 'F</var/lib/dsa/ssh-weak-keys.db>'
71 as fingerprint database. 
72
73 =item B<-n, --dsa_nowarn> 
74
75 Don't warn for DSA keys
76
77 =back 
78
79 =cut
80
81 use strict;
82 use warnings;
83
84 use File::Temp;
85 use BerkeleyDB;
86 use Pod::Usage;
87 use Getopt::Long;
88 use IPC::Open3;
89
90 my $fprdb_fname = "/var/lib/dsa/ssh-weak-keys.db" ;
91 my ($outfile, $help);
92 my $dsa_nowarn = 0;
93
94 GetOptions(     'help|h' => \$help, #Help function
95                 'statusfile|s=s' => \$outfile, 
96                 'fprdb|f=s' => \$fprdb_fname,
97                 'n|dsa_nowarn' => \$dsa_nowarn,  
98 );
99
100 pod2usage(1) if $help;
101
102 my $fh; 
103 if ($outfile) {
104         open ($fh, '>', $outfile) 
105                 or die "Could not open statusfile '$outfile' for writing: $!";
106 } else {
107         $fh = *STDOUT; 
108 }
109
110 my %fpr_hash;
111 tie %fpr_hash, 'BerkeleyDB::Btree',
112         -Filename   => $fprdb_fname,
113         -Flags      => DB_RDONLY
114                 or die "Cannot open fingerprint db $fprdb_fname: $! $BerkeleyDB::Error\n";
115
116
117 my ($weak_keys,$checked_keys) = 0;
118 my $dsa_keys = 0;
119 my $text = '';
120 my %key_sizes;
121
122
123
124 #&from_user_all;
125 &from_debianorg_places;
126 &from_ssh_host(qw(localhost));
127
128 my $status="OK";
129 if ($weak_keys) {
130         $status = "CRITICAL";
131 } elsif ($dsa_keys && ! $dsa_nowarn) {
132         $status = "WARNING";
133 }
134
135 print $fh "$status\n";
136 print $fh "Checked $checked_keys keys - $weak_keys weak - $dsa_keys dsa keys\n";
137 print $fh "Sizes: ";
138 foreach my $size (sort(keys(%key_sizes))) {
139         print $fh "$size:$key_sizes{$size} ";
140 }
141
142 print $fh "\n";
143 print $fh "$text" if $text;
144
145
146
147 sub safe_backtick (@) {
148     my @args = @_;
149
150     my ($wtr, $fh, $err);
151
152     open3($wtr,$fh,$err, @args)
153         or die "error: failed to spawn $args[0]: $!\n";
154     my @result;
155     if (wantarray) {
156         @result = <$fh>;
157     } else {
158         local $/;
159         @result = scalar(<$fh>);
160     }
161     close $fh;
162     $? == 0 or return undef;
163     if (wantarray) {
164         return @result;
165     } else {
166         return $result[0];
167     }
168 }
169
170 sub ssh_fprint_file ($) {
171     my $name = shift;
172     my $data = safe_backtick qw/ssh-keygen -l -f/, $name;
173     defined $data or return ();
174     my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
175     return @data if @data == 2;
176     return ();
177 }
178
179 sub ssh_fprint_check ($$$) {
180     my ($name, $length, $hash) = @_;
181     if (exists $key_sizes{$length}) {
182             $key_sizes{$length}++;
183     } else {
184             $key_sizes{$length}=1;
185     }
186     $checked_keys++;
187     if (exists $fpr_hash{$hash}) {
188         $weak_keys++;
189         $text .= "$name weak ($hash)\n";
190     }
191 }
192
193
194 sub from_ssh_key_file ($) {
195     my $name = shift;
196     if (open (my $FH, '<', $name)) {
197         my $key = <$FH>; 
198         if ($key =~ m/ssh-dss/) {
199                 $dsa_keys++;
200                 $text .= "$name is a DSA key\n";
201         }
202     } else {
203         $text .= "Could not open $name: $!";
204     }
205     my ($length, $hash) = ssh_fprint_file $name;
206     if ($length && $hash) {
207         ssh_fprint_check "$name:1", $length, $hash;
208     } else {
209         $text .= "$name:1: warning: failed to parse SSH key file\n";
210     }
211 }
212
213 sub clear_tmp ($) {
214     my $tmp = shift;
215     seek $tmp, 0, 0 or die "seek: $!";
216     truncate $tmp, 0 or die "truncate: $!";
217 }
218
219 sub from_ssh_auth_file ($) {
220     my $name = shift;
221     my $auth;
222     unless (open $auth, '<', $name) {
223         warn "$name:0: error: open failed: $!\n";
224         return;
225     }
226     my $tmp = new File::Temp;
227     while (my $line = <$auth>) {
228         chomp $line;
229         my $lineno = $.;
230         clear_tmp $tmp;
231         next if $line =~ m/^$/; # ignore empty lines
232         next if $line =~ m/^#/; # ignore comments
233         if ($line =~ m/ssh-dss/) {
234                 $dsa_keys++;
235                 $text .= "$name:$lineno is a DSA key\n";
236         }
237         print $tmp "$line\n" or die "print: $!";
238         $tmp->flush;
239         my ($length, $hash) = ssh_fprint_file "$tmp";
240         if ($length && $hash) {
241             ssh_fprint_check "$name:$lineno", $length, $hash;
242         } else {
243             $text .= "$name:$lineno: warning: unparsable line\n";
244         }
245     }
246 }
247
248 sub from_ssh_host (@) {
249     my @names = @_;
250     my @lines;
251     push @lines, safe_backtick qw|ssh-keyscan -t rsa|, @names;
252     push @lines, safe_backtick qw|ssh-keyscan -t dsa|, @names;
253
254     my $tmp = new File::Temp;
255     for my $line (@lines) {
256         next if $line =~ /^#/;
257         next if $line =~ /^no hostkey alg/;
258         my ($host, $data) = $line =~ /^(\S+) (.*)$/;
259         clear_tmp $tmp;
260         print $tmp "$data\n" or die "print: $!";
261         $tmp->flush;
262         my ($length, $hash) = ssh_fprint_file "$tmp";
263         if ($length && $hash) {
264             ssh_fprint_check "$host", $length, $hash;
265         } else {
266             $text .= "$host: warning: unparsable line\n";
267         }
268     }
269 }
270
271 sub from_user ($) {
272     my $user = shift;
273     my ($name,$passwd,$uid,$gid,
274         $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
275     my $file = "$dir/.ssh/authorized_keys";
276     from_ssh_auth_file $file if -r $file;
277     $file = "$dir/.ssh/authorized_keys2";
278     from_ssh_auth_file $file if -r $file;
279     $file = "$dir/.ssh/id_rsa.pub";
280     from_ssh_key_file $file if -r $file;
281     $file = "$dir/.ssh/id_dsa.pub";
282     from_ssh_key_file $file if -r $file;
283 }
284
285 sub from_user_all () {
286     setpwent;
287     while (my $name = getpwent) {
288         from_user $name;
289     }
290     endpwent;
291 }
292
293
294 sub from_debianorg_places () {
295     open(F, "/etc/ssh/sshd_config") or die ("Cannot open /etc/ssh/sshd_config: $!\n");
296     my @lines = <F>;
297     close(F);
298
299     my @ak = grep { /^AuthorizedKeysFile\s/i } @lines;
300     my @ak2 = grep { /^AuthorizedKeysFile2\s/i } @lines;
301
302     if (scalar @ak != 1) {
303         print $fh "UNKNOWN\n";
304         print $fh "There is more than one AuthorizedKeysFile definition in sshd_config\n";
305         exit
306     }
307     if (scalar @ak2 != 1) {
308         print $fh "UNKNOWN\n";
309         print $fh "There is more than one AuthorizedKeysFile2 definition in sshd_config\n";
310         exit
311     }
312     unless ($ak[0] =~ m#^((?i)AuthorizedKeysFile)\s+/etc/ssh/userkeys/%u$# ) {
313         print $fh "UNKNOWN\n";
314         print $fh "The AuthorizedKeysFile definition has an unexpected value.  Should be /etc/ssh/userkeys/%u\n";
315         exit
316     }
317     unless ($ak2[0] =~ m#^((?i)AuthorizedKeysFile2)\s+/var/lib/misc/userkeys/%u$# ) {
318         print $fh "UNKNOWN\n";
319         print $fh "The AuthorizedKeysFile2 definition has an unexpected value.  Should be /var/lib/misc/userkeys/%u\n";
320         exit
321     }
322
323     for my $d (qw{/etc/ssh/userkeys /var/lib/misc/userkeys}) {
324         next unless (-d $d);
325         opendir(D, $d) or die "Cannot opendir $d: $!\n";
326         for my $file (grep { $_ ne "." && $_ ne ".."  } readdir(D)) {
327             my $f = $d.'/'.$file;
328             from_ssh_key_file $f if -r $f;
329         };
330     };
331 }
332
333