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