update-serial.pl
branchhs12
changeset 77 35905799cfd1
parent 76 7c48ae30987c
child 78 bb780a686eb6
equal deleted inserted replaced
76:7c48ae30987c 77:35905799cfd1
     1 #!/usr/bin/perl 
       
     2 
       
     3 use v5.10;
       
     4 use strict;
       
     5 use warnings;
       
     6 
       
     7 use FindBin;
       
     8 use File::Basename;
       
     9 use Pod::Usage;
       
    10 use Getopt::Long;
       
    11 use File::Temp;
       
    12 use IO::File;
       
    13 use POSIX qw(strftime);
       
    14 use if $ENV{DEBUG} => "Smart::Comments";
       
    15 use DNStools::Config qw(get_config);
       
    16 
       
    17 sub uniq(@);
       
    18 sub zones(@);
       
    19 sub changed_zones();
       
    20 sub update_index($);
       
    21 sub signature_expired($);
       
    22 sub need_rollover();
       
    23 sub done_rollover();
       
    24 sub begin_rollover(@);
       
    25 sub end_rollover(@);
       
    26 sub unlink_unused_keys($);
       
    27 sub include_keys($);
       
    28 sub sign($);
       
    29 sub update_serial($);
       
    30 
       
    31 sub mk_zone_conf;
       
    32 sub file_entry;
       
    33 sub server_reload;
       
    34 
       
    35 my %config;
       
    36 my %opt;
       
    37 
       
    38 MAIN: {
       
    39 
       
    40     GetOptions(
       
    41         "sign-alert-time=i" => \$opt{sign_alert_time},
       
    42         "key-counter-end=i" => \$opt{key_counter_end},
       
    43         "h|help"            => sub { pod2usage(-exit 0, -verbose => 1) },
       
    44         "m|man"             => sub {
       
    45             pod2usage(
       
    46                 -exit 0,
       
    47                 -verbose   => 2,
       
    48                 -noperldoc => system("perldoc -v &>/dev/null")
       
    49             );
       
    50         },
       
    51     ) or pod2usage;
       
    52 
       
    53     # merge the config and the defined options from commandline
       
    54     %config = get_config("$FindBin::Bin/dnstools.conf", "/etc/dnstools.conf", \%opt);
       
    55 
       
    56     our $bind_dir = $config{bind_dir};
       
    57     our $conf_dir = $config{zone_conf_dir};
       
    58 
       
    59     my @candidates = @ARGV ? zones(@ARGV) : changed_zones;
       
    60     push @candidates, update_index($config{indexzone});
       
    61     push @candidates, signature_expired($config{sign_alert_time});
       
    62 
       
    63     my @need_rollover = need_rollover;
       
    64     my @done_rollover = done_rollover;
       
    65 
       
    66     push @candidates, begin_rollover(@need_rollover);
       
    67     push @candidates, end_rollover(@done_rollover);
       
    68 
       
    69     foreach my $zone (uniq(@candidates)) {
       
    70         update_serial($zone);
       
    71         sign($zone);
       
    72     }
       
    73     say "Need to ... file_entry, mk_zone_conf, server_reload";
       
    74     exit;
       
    75 
       
    76     file_entry;       # bearbeitet die file-eintraege der konfigurations-datei
       
    77     mk_zone_conf;     # konfiguration zusammenfuegen
       
    78     server_reload;    # server neu laden
       
    79 
       
    80 }
       
    81 
       
    82 sub uniq(@) {
       
    83 
       
    84     # remove duplicate entries
       
    85     my %all;
       
    86     @all{@_} = ();
       
    87     keys %all;
       
    88 }
       
    89 
       
    90 sub zones(@) {
       
    91 
       
    92     # check whether the zones in argv are managed zones and
       
    93     # insert them into the list new_serial
       
    94 
       
    95     my @r;
       
    96 
       
    97     foreach (@_) {
       
    98         chomp(my $zone = `idn --quiet "$_"`);
       
    99         die "$zone is not managed\n"
       
   100           if not -e "$config{master_dir}/$zone/$zone";
       
   101         push @r, $zone;
       
   102     }
       
   103 
       
   104     return @r;
       
   105 }
       
   106 
       
   107 sub changed_zones() {
       
   108 
       
   109     # find candidates in our master dir
       
   110     my @r;
       
   111 
       
   112     while (glob "$config{master_dir}/*") {
       
   113         my $zone = basename($_);
       
   114 
       
   115         if (not -e "$_/.stamp") {
       
   116             say " * $zone: no .stamp file found";    # NOCH IN NEW_SERIAL PUSHEN
       
   117             push @r, $zone;
       
   118             next;
       
   119         }
       
   120 
       
   121         my $stamp_age = -M _;
       
   122         my $file_age  = -M "$_/$zone";
       
   123 
       
   124         next if $stamp_age <= $file_age;             # should be only <
       
   125 
       
   126         push @r, $zone;
       
   127         say " * $zone: zone file modified";
       
   128     }
       
   129     return @r;
       
   130 }
       
   131 
       
   132 sub signature_expired($) {
       
   133     my $sign_alert_time = shift;  # the time between the end and the new signing
       
   134                                   # (see external configuration)
       
   135     my @r;
       
   136 
       
   137 # erzeugt $time (die zeit ab der neu signiert werden soll)
       
   138 # ... warum eigentlich nur bis zu den Stunden und nicht auch Minuten und Sekunden?
       
   139     my $time = strftime("%Y%m%d%H" => localtime time + 3600 * $sign_alert_time);
       
   140 
       
   141     ## vergleicht fuer alle zonen im ordner $config{master_dir} mit einer
       
   142     ## <zone>.signed-datei den zeitpunkt in $time mit dem ablaufdatum der
       
   143     ## signatur, welcher aus der datei <zone>.signed ausgelesen wird.
       
   144   ZONE: while (my $dir = glob "$config{master_dir}/*") {
       
   145         my $zone = basename $dir;
       
   146 
       
   147         next if not -e "$dir/$zone.signed";
       
   148 
       
   149         open(my $fh, "$dir/$zone.signed")
       
   150           or die "Can't open $dir/$zone.signed: $!\n";
       
   151         push @r, $zone
       
   152           if /RRSIG\s+SOA[\d ]+(\d{10})\d{4}\s+\(/ ~~ [<$fh>]
       
   153               and $1 < $time;
       
   154     }
       
   155 
       
   156     return @r;
       
   157 }
       
   158 
       
   159 sub sign($) {
       
   160 
       
   161     my $zone = shift;
       
   162     my $dir  = "$config{master_dir}/$zone";
       
   163 
       
   164     my $pid = fork // die "Can't fork: $!";
       
   165 
       
   166     if ($pid == 0) {
       
   167         chdir $dir or die "Can't chdir to $dir: $!\n";
       
   168         exec "dnssec-signzone" => $zone;
       
   169         die "Can't exec: $!\n";
       
   170     }
       
   171 
       
   172     wait == $pid or die "Child is lost: $!";
       
   173     die "Can't sign zone!" if $?;
       
   174 
       
   175     say " * $zone neu signiert";
       
   176 
       
   177     open(my $fh, "+>>$dir/.keycounter")
       
   178       or die "Can't open $dir/.keycounter for update: $!\n";
       
   179     seek($fh, 0, 0);
       
   180     my $kc = <$fh>;
       
   181     truncate($fh, 0);
       
   182     say $fh ++$kc;
       
   183 }
       
   184 
       
   185 sub update_serial($) {
       
   186 
       
   187     my $zone = shift;
       
   188 
       
   189     my $file = "$config{master_dir}/$zone/$zone";
       
   190     my $in   = IO::File->new($file) or die "Can't open $file: $!\n";
       
   191     my $out  = File::Temp->new(DIR => dirname $file)
       
   192       or die "Can't open tmpfile: $!\n";
       
   193     my $_ = join "" => <$in>;
       
   194 
       
   195     my $serial;
       
   196     s/^(\s+)(\d{10})(?=\s*;\s*serial)/$1 . ($serial = new_serial($2))/emi
       
   197       or die "Serial number not found for replacement!";
       
   198 
       
   199     print $out $_;
       
   200 
       
   201     close($in);
       
   202     close($out);
       
   203 
       
   204     rename($out->filename => $file)
       
   205       or die "Can't rename tmp to $file: $!\n";
       
   206 
       
   207     $serial =~ s/\s*//g;
       
   208     say " * $zone: serial incremented to $serial";
       
   209 
       
   210     open(my $stamp, ">", dirname($file) . "/.stamp");
       
   211     print $stamp time() . " " . localtime() . "\n";
       
   212 
       
   213     say " * $zone: stamp aktualisiert";
       
   214 }
       
   215 
       
   216 sub new_serial($) {
       
   217 
       
   218     my ($date, $cnt) = $_[0] =~ /(\d{8})(\d\d)/;
       
   219 
       
   220     state $now = strftime("%4Y%02m%02d", localtime);
       
   221 
       
   222     return $date eq $now
       
   223       ? sprintf "%s%02d", $date, $cnt + 1
       
   224       : "${now}00";
       
   225 
       
   226 }
       
   227 
       
   228 sub mk_zone_conf {
       
   229 
       
   230     # erzeugt eine named.conf-datei aus den entsprechenden vorlagen.
       
   231     our $bind_dir;
       
   232     our $conf_dir;
       
   233 
       
   234     open(TO, ">$bind_dir/named.conf.zones")
       
   235       or die "$bind_dir/named.conf.zones: $!\n";
       
   236     while (<$conf_dir/*>) {
       
   237         open(FROM, "$_") or die "$_: $! \n";
       
   238         print TO <FROM>;
       
   239         close(FROM);
       
   240     }
       
   241     close(TO);
       
   242     print "** zonekonfiguration erzeugt\n";
       
   243 }
       
   244 
       
   245 sub update_index($) {
       
   246     my $indexzone = shift;
       
   247 
       
   248     my @iz;
       
   249 
       
   250     {
       
   251         open(my $fh, "$config{master_dir}/$indexzone/$indexzone")
       
   252           or die "$config{master_dir}/$indexzone/$indexzone: $!\n";
       
   253         chomp(@iz = grep !/ZONE::/ => <$fh>);
       
   254     }
       
   255 
       
   256     for my $dir (glob "$config{master_dir}/*") {
       
   257         my $zone = basename($dir);
       
   258         my $info = -e ("$dir/.keycounter") ? "sec-on" : "sec-off";
       
   259         push @iz, join "::", "\t\tIN TXT\t\t\"ZONE", $zone, $info;
       
   260     }
       
   261 
       
   262     {
       
   263         my $fh = File::Temp->new(DIR => "$config{master_dir}/$indexzone")
       
   264           or die "Can't create tmpdir: $!\n";
       
   265         print $fh join "\n" => @iz, "";
       
   266         rename($fh->filename => "$config{master_dir}/$indexzone/$indexzone")
       
   267           or die "Can't rename "
       
   268           . $fh->filename
       
   269           . " to $config{master_dir}/$indexzone/$indexzone: $!\n";
       
   270     }
       
   271 
       
   272     say "** index-zone aktualisiert";
       
   273     return $indexzone;
       
   274 }
       
   275 
       
   276 sub file_entry {
       
   277 
       
   278     # prueft jede domain, die ein verzeichnis in $config{master_dir} hat, ob sie
       
   279     # dnssec nutzt.
       
   280     # passt die eintraege in $config_file falls noetig an.
       
   281     our $conf_dir;
       
   282 
       
   283     while (glob "$config{master_dir}/*") {
       
   284         s#($config{master_dir}/)(.*)#$2#;
       
   285         my $zone      = $_;
       
   286         my $zone_file = "$config{master_dir}/$zone/$zone";
       
   287         my $conf_file = "$conf_dir/$zone";
       
   288         my @c_content;
       
   289 
       
   290         unless (-f "$conf_file") {
       
   291             die "$conf_file: $! \n";
       
   292         }
       
   293 
       
   294         if (-e "$config{master_dir}/$zone/.keycounter") {
       
   295             open(FILE, "<$conf_file") or die "$conf_file: $!\n";
       
   296             @c_content = <FILE>;
       
   297             close(FILE);
       
   298             for (@c_content) {
       
   299                 if (m{(.*)($zone_file)(";)}) {
       
   300                     print
       
   301                       " * zonekonfiguration aktualisiert ($2 ==> $2.signed)\n";
       
   302                     $_ = "$1$2.signed$3\n";
       
   303                 }
       
   304             }
       
   305             open(FILE, ">$conf_file") or die "$conf_file: $!\n";
       
   306             print FILE @c_content;
       
   307             close(FILE);
       
   308         }
       
   309         else {
       
   310             open(FILE, "<$conf_file") or die "$conf_file: $!\n";
       
   311             @c_content = <FILE>;
       
   312             close(FILE);
       
   313             for (@c_content) {
       
   314                 if (m{(.*)($zone_file)\.signed(.*)}) {
       
   315                     print
       
   316                       " * zonekonfiguration aktualisiert ($2.signed ==> $2)\n";
       
   317                     $_ = "$1$2$3\n";
       
   318                 }
       
   319             }
       
   320             open(FILE, ">$conf_file") or die "$conf_file: $!\n";
       
   321             print FILE @c_content;
       
   322             close(FILE);
       
   323         }
       
   324     }
       
   325 }
       
   326 
       
   327 sub server_reload {
       
   328     if (`rndc reload`) { print "** reload dns-server \n" }
       
   329 }
       
   330 
       
   331 sub need_rollover() {
       
   332 
       
   333     # gibt alle zonen mit abgelaufenen keycounter
       
   334     my @r;
       
   335 
       
   336     while (my $kc = glob "$config{master_dir}/*/.keycounter") {
       
   337         my $zone = basename dirname $kc;
       
   338         my $key;
       
   339 
       
   340         {
       
   341             open(my $fh, $kc) or die "$kc: $!\n";
       
   342             chomp($key = <$fh>);
       
   343         }
       
   344 
       
   345         push @r, $zone if $config{key_counter_end} <= $key;
       
   346     }
       
   347 
       
   348     return @r;
       
   349 }
       
   350 
       
   351 sub done_rollover() {
       
   352 
       
   353     # funktion ueberprueft ob ein keyrollover fertig ist
       
   354     # die bedingung dafuer ist das:
       
   355     # - eine datei .index.zsk vorhanden ist
       
   356     # - die datei .index.zsk älter ist, als die rollover-Zeit
       
   357     # - die datei .index.zsk ueber mehr als eine zeile gross ist
       
   358     #   (also mehr als einen Schlüssel enthält)
       
   359     my @r;
       
   360     my $now = time;
       
   361 
       
   362     while (my $dir = glob "$config{master_dir}/*") {
       
   363         my $zone = basename $dir;
       
   364 
       
   365         my @index = ();
       
   366         my $index_wc;
       
   367 
       
   368         # prueft nach der ".index.zsk"-datei und erstellt den zeitpunkt
       
   369         # an dem das key-rollover endet.
       
   370         # rollover is done when mtime of the .index.zsk + abl_zeit is
       
   371         # in the past
       
   372         next if not -e "$dir/.index.zsk";
       
   373         next if (stat _)[9] + 3600 * $config{abl_zeit} >= $now;
       
   374 
       
   375         # prueft die anzahl der schluessel in der .index.zsk
       
   376         open(my $fh, "$dir/.index.zsk") or die "$dir/.index.zsk: $!\n";
       
   377         (<$fh>);
       
   378         push @r, $zone if $. > 1;
       
   379     }
       
   380 
       
   381     return @r;
       
   382 }
       
   383 
       
   384 sub begin_rollover(@) {
       
   385     my @zones = @_;
       
   386     my @r;
       
   387 
       
   388     # anfang des key-rollovers
       
   389 
       
   390     foreach my $zone (@zones) {
       
   391 
       
   392         # erzeugt zsks
       
   393         my $dir = "$config{master_dir}/$zone";
       
   394         my ($keyname, @keys);
       
   395 
       
   396         # create a new key
       
   397         {    # need to change the direcoty, thus some more effort
       
   398                 # alternativly: $keyname = `cd $dir && dnssec-keygen ...`;
       
   399                 # would do, but is more fragile on shell meta characters
       
   400 
       
   401             open(my $keygen, "-|") or do {
       
   402                 chdir $dir or die "Can't chdir to $dir: $!\n";
       
   403                 exec "dnssec-keygen",
       
   404                   -a => "RSASHA1",
       
   405                   -b => 512,
       
   406                   -n => "ZONE",
       
   407                   $zone;
       
   408                 die "Can't exec: $!";
       
   409             };
       
   410             chomp($keyname = <$keygen>);
       
   411             close($keygen) or die "dnssec-keygen failed: $@";
       
   412         }
       
   413 
       
   414         open(my $fh, "+>>$dir/.index.zsk") or die "$dir/.index.zsk: $!\n";
       
   415         seek($fh, 0, 0);
       
   416         chomp(@keys = <$fh>);
       
   417 
       
   418         ### @keys
       
   419 
       
   420         push @keys, $keyname;
       
   421         shift @keys if @keys > 2;
       
   422 
       
   423         truncate($fh, 0) or die "truncate";
       
   424         print $fh join "\n" => @keys;
       
   425 
       
   426         print " * $zone: neuer ZSK $keyname erstellt\n";
       
   427 
       
   428         open($fh, ">$dir/.keycounter") or die "$dir/.keycounter: $!\n";
       
   429         say $fh 0;
       
   430         close($fh);
       
   431 
       
   432         unlink_unused_keys($zone);
       
   433         include_keys($zone);
       
   434         push @r, $zone;
       
   435     }
       
   436 
       
   437     return @r;
       
   438 }
       
   439 
       
   440 sub include_keys($) {
       
   441 
       
   442     # die funktion fugt alle schluessel in eine zonedatei
       
   443     my $zone = shift;
       
   444     my $dir  = "$config{master_dir}/$zone";
       
   445 
       
   446     my $in = IO::File->new("$dir/$zone") or die "Can't open $dir/$zone: $!\n";
       
   447     my $out = File::Temp->new(DIR => $dir) or die "Can't open tmpfile: $!\n";
       
   448 
       
   449     print $out grep { !/\$include\s+.*key/i } $in;
       
   450     print $out map  { "\$INCLUDE @{[basename $_]}\n" } glob "$dir/K*key";
       
   451 
       
   452     close $in;
       
   453     close $out;
       
   454     rename($out->filename => "$dir/$zone")
       
   455       or die "Can't rename tmp to $dir/$zone: $!\n";
       
   456 
       
   457 }
       
   458 
       
   459 sub unlink_unused_keys($) {
       
   460 
       
   461     # die funktion loescht alle schluessel die nicht in der index.zsk
       
   462     # der uebergebenen zone stehen
       
   463     my $zone = shift;
       
   464 
       
   465     my @keys;
       
   466     my $dir = "$config{master_dir}/$zone";
       
   467 
       
   468     {
       
   469 
       
   470         # collect the keys and cut everything except the key id
       
   471         # we cut the basenames (w/o the .private|.key suffix)
       
   472         open(my $zsk, "<$dir/.index.zsk") or die "$dir/.index.zsk: $!\n";
       
   473         open(my $ksk, "<$dir/.index.ksk") or die "$dir/.index.ksk: $!\n";
       
   474         @keys = (<$zsk>, <$ksk>);
       
   475     }
       
   476 
       
   477     # prueft alle schluesseldateien (ksk, zsk) ob sie in der jeweiligen
       
   478     # indexdatei beschrieben sind. wenn nicht werden sie geloescht.
       
   479     for my $file (glob "$dir/K*.key $dir/K*.private") {
       
   480         unlink $file if basename($file, ".key", ".private") ~~ @keys;
       
   481     }
       
   482 }
       
   483 
       
   484 sub end_rollover(@) {
       
   485 
       
   486     my @zones = @_;
       
   487     my @r;
       
   488 
       
   489     foreach my $zone (@zones) {
       
   490 
       
   491         my $dir = "$config{master_dir}/$zone";
       
   492 
       
   493         open(my $fh, "+>>$dir/.index.zsk")
       
   494           or die "Can't open $dir/.index.zsk: $!\n";
       
   495         seek($fh, 0, 0);
       
   496         chomp(my @keys = <$fh>);
       
   497 
       
   498         if (@keys > 1) {
       
   499             truncate($fh, 0);
       
   500             say $fh $keys[-1];
       
   501         }
       
   502         close($fh);
       
   503 
       
   504         unlink_unused_keys($zone);
       
   505         include_keys($zone);
       
   506         push @r => $zone;
       
   507     }
       
   508 
       
   509     return @r;
       
   510 }
       
   511 
       
   512 __END__
       
   513 
       
   514 =head1 NAME
       
   515  
       
   516  update-serial - updates the serial numbers and re-signs the zone files
       
   517 
       
   518 =head1 SYNOPSIS
       
   519 
       
   520  update-serial [options] [zone...]
       
   521 
       
   522 =head1 DESCRIPTION
       
   523 
       
   524 B<update-serial> scans the configured directories for modified zone files. On any
       
   525 file found it increments the serial number and signs the zone, if approbiate.
       
   526 
       
   527 =head1 OPTIONS
       
   528 
       
   529 =over
       
   530 
       
   531 =item B<--sign-alert-time> I<days>
       
   532 
       
   533 =item B<--key-counter-end> I<integer>
       
   534 
       
   535 Maximum number if key usages.
       
   536 
       
   537 
       
   538 =back
       
   539 
       
   540 The common options B<-h>|B<--help>|B<-m>|B<--man> are supported.
       
   541 
       
   542 =head1 AUTHOR
       
   543 
       
   544 L<andre.suess@pipkin.cc>
       
   545 
       
   546 =cut
       
   547 
       
   548 # vim:sts=4 sw=4 aw ai sm: