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.
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)
12 # Copyright (c) 2008, Alexander Wirt <formorer@debian.org> for check_weakkeys
14 # Copyright (c) 2008 Peter Palfrader <peter@palfrader.org>
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.
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.
33 B<check_weakkeys> - checks system for weak ssh keys
39 B<check_weakkeys> [options]
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>
64 Prints out a brief help
66 =item B<-s, --statusfile> "statusfile"
68 Use 'F<statusfile>' instead of 'F<STDOUT>'.
70 =item B<-f, --fprdb> "database" (default: /var/lib/dsa/ssh-weak-keys.db)
72 Use 'F<database>' instead of 'F</var/lib/dsa/ssh-weak-keys.db>'
73 as fingerprint database.
75 =item B<-n, --dsa_nowarn>
77 Don't warn for DSA keys
92 my $fprdb_fname = "/var/lib/dsa/ssh-weak-keys.db" ;
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,
104 pod2usage(1) if $help;
108 open ($fh, '>', $outfile)
109 or die "Could not open statusfile '$outfile' for writing: $!";
115 tie %fpr_hash, 'BerkeleyDB::Btree',
116 -Filename => $fprdb_fname,
118 or die "Cannot open fingerprint db $fprdb_fname: $! $BerkeleyDB::Error\n";
121 my ($weak_keys,$checked_keys) = 0;
123 my $weird_keyfiles = 0;
129 &from_debianorg_places;
133 &from_ssh_host(qw(localhost));
137 $status = "CRITICAL";
138 } elsif ($dsa_keys && ! $dsa_nowarn || $weird_keyfiles) {
142 print $fh "$status\n";
143 print $fh "Checked $checked_keys keys - $weak_keys weak - $dsa_keys dsa keys\n";
145 foreach my $size (sort(keys(%key_sizes))) {
146 print $fh "$size:$key_sizes{$size} ";
150 print $fh "$text" if $text;
154 sub safe_backtick (@) {
157 my ($wtr, $fh, $err);
159 open3($wtr,$fh,$err, @args)
160 or die "error: failed to spawn $args[0]: $!\n";
166 @result = scalar(<$fh>);
169 $? == 0 or return undef;
177 sub ssh_fprint_file ($) {
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;
186 sub ssh_fprint_check ($$$) {
187 my ($name, $length, $hash) = @_;
188 if (exists $key_sizes{$length}) {
189 $key_sizes{$length}++;
191 $key_sizes{$length}=1;
194 if (exists $fpr_hash{$hash}) {
196 $text .= "$name weak ($hash)\n";
201 sub from_ssh_key_file ($) {
203 if (open (my $FH, '<', $name)) {
206 if (! defined $key) {
208 $text .= "cannot read $name properly - empty?\n";
209 } elsif ($key =~ m/ssh-dss/) {
211 $text .= "$name is a DSA key\n";
214 $text .= "Could not open $name: $!";
216 my ($length, $hash) = ssh_fprint_file $name;
217 if ($length && $hash) {
218 ssh_fprint_check "$name:1", $length, $hash;
220 $text .= "$name:1: warning: failed to parse SSH key file\n";
226 seek $tmp, 0, 0 or die "seek: $!";
227 truncate $tmp, 0 or die "truncate: $!";
230 sub from_ssh_auth_file ($) {
233 unless (open $auth, '<', $name) {
234 warn "$name:0: error: open failed: $!\n";
237 my $tmp = new File::Temp;
238 while (my $line = <$auth>) {
242 next if $line =~ m/^$/; # ignore empty lines
243 next if $line =~ m/^#/; # ignore comments
244 if ($line =~ m/ssh-dss/) {
246 $text .= "$name:$lineno is a DSA key\n";
248 print $tmp "$line\n" or die "print: $!";
250 my ($length, $hash) = ssh_fprint_file "$tmp";
251 if ($length && $hash) {
252 ssh_fprint_check "$name:$lineno", $length, $hash;
254 $text .= "$name:$lineno: warning: unparsable line\n";
259 sub from_ssh_host (@) {
262 push @lines, safe_backtick qw|ssh-keyscan -t rsa|, @names;
263 push @lines, safe_backtick qw|ssh-keyscan -t dsa|, @names;
265 my $tmp = new File::Temp;
266 for my $line (@lines) {
267 next if $line =~ /^#/;
268 next if $line =~ /^no hostkey alg/;
269 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
271 print $tmp "$data\n" or die "print: $!";
273 my ($length, $hash) = ssh_fprint_file "$tmp";
274 if ($length && $hash) {
275 ssh_fprint_check "$host", $length, $hash;
277 $text .= "$host: warning: unparsable line\n";
284 my ($name,$passwd,$uid,$gid,
285 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
286 my $file = "$dir/.ssh/authorized_keys";
287 from_ssh_auth_file $file if -r $file;
288 $file = "$dir/.ssh/authorized_keys2";
289 from_ssh_auth_file $file if -r $file;
290 $file = "$dir/.ssh/id_rsa.pub";
291 from_ssh_key_file $file if -r $file;
292 $file = "$dir/.ssh/id_dsa.pub";
293 from_ssh_key_file $file if -r $file;
296 sub from_user_all () {
298 while (my $name = getpwent) {
305 sub from_debianorg_places () {
306 open(F, "/etc/ssh/sshd_config") or die ("Cannot open /etc/ssh/sshd_config: $!\n");
310 my @ak = grep { /^AuthorizedKeysFile\s/i } @lines;
311 my @ak2 = grep { /^AuthorizedKeysFile2\s/i } @lines;
314 for my $line ((@ak, @ak2)) {
315 my @file_locations = split /\s+/, $line;
316 shift @file_locations;
317 push @ak_files, @file_locations;
320 if (scalar @ak_files != 2) {
321 print $fh "UNKNOWN\n";
322 print $fh "There should be two locations for User AuthorizedKeysFile defined in sshd_config\n";
326 unless (grep { m#^/etc/ssh/userkeys/%u$# } @ak_files) {
327 print $fh "UNKNOWN\n";
328 print $fh "The AuthorizedKeysFile definition has an unexpected value. Should be /etc/ssh/userkeys/%u\n";
331 unless (grep { m#^/var/lib/misc/userkeys/%u$# } @ak_files) {
332 print $fh "UNKNOWN\n";
333 print $fh "The AuthorizedKeysFile2 definition has an unexpected value. Should be /var/lib/misc/userkeys/%u\n";
337 for my $d (qw{/etc/ssh/userkeys /var/lib/misc/userkeys}) {
339 opendir(D, $d) or die "Cannot opendir $d: $!\n";
340 for my $file (grep { ! -d $d.'/'.$_ } readdir(D)) {
341 next if ($file eq 'README-DSA-BUILDD');
342 my $f = $d.'/'.$file;
343 from_ssh_auth_file $f if -r $f;