Überwachung von aktiven Zertifikaten

Mit Letsencrypt hat sich das Problem verschärft, die aktiv genutzten Zertifikate eines Servers überwachen zu müssen. Im Rahmen einer sowieso schon erfolgenden Überwachung von shared Webhosting (zeigt die Kundendomain überhaupt noch auf die Maschine?), um den Rückbau nicht zu vergessen, bietet sich auch die Kontrolle der Zertifikate an.

Bei uns kommt dazu ein Perlscript zum Einsatz, das die wichtigsten Eigenschaften von Apache und PHP-Konfig überprüft.

Konfig

#! /usr/bin/perl

use strict;

my $http_config = 'path/to/httpd.conf';
my @php_configs = glob 'path/to/php/*.conf';
my $logdir = 'path/to/logfiles';
my $tempfile = "path/to/a/temporary-unique-name";
my $certwarn = 30*3600*24; # openssl needs seconds 

my (%ip, $current_ip, %crt);
my %log = (
#  'php.log' =>  'Hardcoded in mod_php',
     'status_log' => 'Hardcoded in log_server_status',
);
my ($altlast, @altlast) = ('regexp.on?IPs);

Dies ist die Konfiguration. Wo liegen die entsprechenden Dateien, die auszuwerten sind?

Darüber hinaus werden einige Logfiles, die nicht auf Konfigurationen ersichtlich sind, explizit angegeben, damit dieses Script darüber Bescheid weiß.

open(IPS, "/sbin/ip addr|") || die "ip: $!\n";
while(<IPS>) {
  next unless /inet (\d+\.\d+\.\d+\.\d+)\//;
  $ip{$1} = 1;
}
close(IPS) || die "ip: $!\n";

Welche IPs ein Server zur Verfügung hat, weiß er selbst am besten. Also lesen wir die direkt dort aus.

Warum kein IPv6? Nun, das ist akut nicht notwendig. Wir überwachen hier, ob der Kunde ungeplant von dem Server wegzieht. Und das geschieht i.d.R. nur mit IPv4. IPv6 auch zu überwachen kann man mal nachrüsten, es drückt aber nicht.

Daten sammeln

foreach my $conf (@php_configs) {
    my $pool;
    open(PHP,"<$conf") || next;
    while(<PHP>) {
        $pool = $1 if /^\[([\w-]+)\]/;
        s/\$pool/$pool/g;
        $log{$1} = "$conf:$." if /^[a-z._[]+log\]?\s*=\s*$logdir\/(\S+)/;
    }
    close(PHP);
}

open(HTTP, "<$http_config") || die "$http_config: $!\n";
while(<HTTP>) {
  $current_ip = $1, next if /^\s*<VirtualHost ([\d.]*)/i;
  $log{$1} = "$http_config:$.", next if /^\s*\w+Log\s+$logdir\/(\S+)/;
  $log{$1} = "$http_config:$.", next if /^\s*php_admin_value\s+\w+log\s+.$logdir\/(\S+)\S/;
  $crt{$1} = "$http_config:$.", next if /^\s*SSLCertificateFile\s+(\S+)/;
  next unless /^\s*Server(Name|Alias)\s+([^\s:]+)/;
  
  my $entry = $2;
  $entry =~ s/^[*]/ALL/;
  my ($name, $aliases, $a, $b, @ips) = gethostbyname($entry);
  print "$entry existiert nicht im DNS.\n" unless @ips;
  foreach my $ip (@ips) {
      $ip = join('.', unpack('C4', $ip));
      push(@altlast, $entry), next if $ip =~ /$altlast/o;
      print ("$entry -> $ip ist nicht lokal.\n"), next unless defined $ip{$ip};
      if($current_ip) {
          print "$entry -> $ip ist nicht $current_ip.\n" unless $current_ip eq $ip;
      }
  }
}
close(HTTP) || die "$http_config: $!\n"; 

print "Altlasten: ",join(" ",@altlast),"\n" if @altlast; 

Interessant für uns ist, welche Logfiles die PHP/Apache Konfiguration generieren könnte. Wenn wir welche haben, die nicht in der Konfig auftauchen, hat etwas mit dem Rückbau nicht funktioniert. Also überwachen wir das.

Des weiteren interessiert uns, ob das DNS noch auf den richtigen Server zeigt. Also schauen wir im DNS nach und vergleichen mit den lokalen IPs (legacy only).

Bei der IP-Prüfung zählen Umzüge von anderen Systemen nicht als Fehler, sondern als Altlast. Diese kennzeichnen, dass hier noch ein Transfer des Webs von der Altlast zu uns im Gange ist und mahnt, doch mal beim Kunden nachzufragen, wie weit er denn damit sei.

Und da wir gerade an der Stelle sind, sammeln wir noch die aktiv genutzten Zertifikate ein.

Auswertung

opendir(LOGS, $logdir) || die "$logdir: $!\n";
foreach (readdir(LOGS)) {
  next if /^\.{1,2}$/;
  next unless -f "$logdir/$_";
  if(defined $log{$_}) {
    delete $log{$_};
  } else {
    print "Logfile $_ ist nicht konfiguriert.\n";
  }
}
closedir(LOGS);

foreach (keys %log) {
  print "$log{$_}: Logfile $_ existiert nicht.\n" unless /\.php(-\w+)?\.log$/;
}

Für jedes existierende Logfile sollte es einen Grund geben. Wenn nicht: Alarm!

Und wenn Logfiles angegeben wurden, dann sollten die auch existieren. Im Zweifel sind sie halt leer. Da wir aber (sehr) alte Logfiles automatisch wegräumen, ist dieser Test eine versteckte Prüfung, ob die Funktion überhaupt noch genutzt wird. Wir bekommen so mit, ob wir z.B. noch eine PHP-FPM Version aktiviert haben, die keine Nutzer mehr hat. Dann kann sie abgeräumt werden.

while(my ($certfile, $line) = each %crt) {
    open(CERTS,"<$certfile") || warn "$line: $certfile $!\n";
    my $cert;
    while(<CERTS>) {
        $cert .= $_ if $cert;
        $cert  = $_ if /^-----BEGIN /;
        if(/^-----END /) {
            open(CERT, ">$tempfile") || warn "$tempfile: $!\n";
            print CERT $cert;
            close(CERT);
            undef $cert;

            open(OPENSSL,"openssl x509 -checkend $certwarn -subject -enddate -in $tempfile |")
              || warn "openssl: $!";
            my $warn = join("", <OPENSSL>);
            unlink $tempfile;
            close(OPENSSL) || print "\n$warn";
        }
    }
}

Und zu guter Letzt schauen wir, ob wir langsam an ein neues Zertifikat denken sollten. In den meisten Fällen dauert der Prozess mit dem Kunden und seinen Dienstleistern mehrere Wochen. Der übliche Angebots-Auftrags-Zyklus halt.

Das Anzeigeformat von openssl ist inkonsistent und maschinell schlecht weiterzubearbeiten. Deswegen nutzen wir die Option -checkend, die openssl mit einem Fehler enden lässt, wenn das Zertifikat abläuft. Dieser Fehlercode spart das unzuverlässige Parsen der Ausgabe.

Und nun noch das Script jeden Tag laufen lassen …

Avatar
Michael Renner 04.02.2016 10:03
next unless /inet (\d+\.\d+\.\d+\.\d+)\//

schmerzt im Jahr 2016 - dafür hat doch der liebe Herrgott

http://search.cpan.org/~drolsky/Data-Validate-IP-0.25/lib/Data/Validate/IP.pm

erfunden!

1 Kommentare

Post a comment

Verwandter Inhalt