diff --git a/bind8/bind8-lib.pl b/bind8/bind8-lib.pl index ddbc4c40c..608fd30b3 100644 --- a/bind8/bind8-lib.pl +++ b/bind8/bind8-lib.pl @@ -27,6 +27,8 @@ else { $bind_version = &get_bind_version(); } +$dnssec_cron_cmd = "$module_config_directory/renew.pl"; + # get_bind_version() # Returns the BIND verison number, or undef if unknown sub get_bind_version @@ -2627,11 +2629,20 @@ if (!$pid) { exit(1); } +# Work out zone key size +local $zonesize; +if ($single) { + (undef, $zonesize) = &compute_dnssec_key_size($alg, 1); + } +else { + $zonesize = $size; + } + # Create the zone key local $dom = $z->{'members'} ? $z->{'values'}->[0] : $z->{'name'}; local $out = &backquote_logged( "cd ".quotemeta($fn)." && ". - "$config{'keygen'} -a ".quotemeta($alg)." -b ".quotemeta($size). + "$config{'keygen'} -a ".quotemeta($alg)." -b ".quotemeta($zonesize). " -n ZONE $dom 2>&1"); if ($?) { kill('KILL', $pid); @@ -2681,6 +2692,73 @@ foreach my $key (@keys) { return undef; } +# resign_dnssec_key(&zone|&zone-name) +# Re-generate the zone key, and re-sign everything. Returns undef on success or +# an error message on failure. +sub resign_dnssec_key +{ +local ($z) = @_; +local $fn = &get_zone_file($z); +$fn || return "Could not work out records file!"; +local $dir = $fn; +$dir =~ s/\/[^\/]+$//; +local $dom = $z->{'members'} ? $z->{'values'}->[0] : $z->{'name'}; + +# Get the old zone key record +local @recs = &read_zone_file($fn, $dom); +locla $zonerec; +foreach my $r (@recs) { + if ($r->{'type'} eq 'DNSKEY' && $r->{'values'}->[0] % 2 == 0) { + $zonerec = $r; + } + } +$zonerec || return "Could not find DNSSEC zone key record"; +local @keys = &get_dnssec_keys($z); +@keys == 2 || return "Expected to find 2 keys, but found ".scalar(@keys); +local ($zonekey) = grep { !$_->{'ksk'} } @keys; +$zonekey || return "Could not find DNSSEC zone key"; + +# Fork a background job to do lots of IO, to generate entropy +local $pid = fork(); +if (!$pid) { + exec("find / -type f >/dev/null 2>&1"); + exit(1); + } + +# Work out zone key size +local $zonesize; +(undef, $zonesize) = &compute_dnssec_key_size($alg, 1); +local $alg = $zonekey->{'algorithm'}; + +# Generate a new zone key +local $out = &backquote_logged( + "cd ".quotemeta($dir)." && ". + "$config{'keygen'} -a ".quotemeta($alg)." -b ".quotemeta($zonesize). + " -n ZONE $dom 2>&1"); +kill('KILL', $pid); +if ($?) { + return "Failed to generate new zone key : $out"; + } + +# Delete the old key file +&unlink_file($zonekey->{'privatefile'}); +&unlink_file($zonekey->{'publicfile'}); + +# Update the zone file with the new key +@keys = &get_dnssec_keys($z); +local ($newzonekey) = grep { !$_->{'ksk'} } @keys; +$newzonekey || return "Could not find new DNSSEC zone key"; +&modify_record($fn, $dom.".", undef, "IN", "DNSKEY", + join(" ", @{$newzonekey->{'values'}})); +&bump_soa_record($fn, \@recs); + +# Re-sign everything +local $err = &sign_dnssec_zone($z); +return "Re-signing failed : $err" if ($err); + +return undef; +} + # delete_dnssec_key(&zone|&zone-name) # Deletes the key for a zone, and all DNSSEC records sub delete_dnssec_key diff --git a/bind8/lang/en b/bind8/lang/en index 648b5e3b1..15a437a39 100644 --- a/bind8/lang/en +++ b/bind8/lang/en @@ -753,6 +753,7 @@ log_thaw=Un-froze zone $1 log_zonekeyon=Enabled DNSSEC for zone $1 log_zonekeyoff=Disabled DNSSEC for zone $1 log_sign=Updated DNSSEC signatures for zone $1 +log_resign=Re-signed DNSSEC key for zone $1 convert_err=Failed to convert zone convert_efile=A records file must be specified before a slave zone can be converted to a master. @@ -999,6 +1000,8 @@ dnssec_enabled=Automatic key re-signing enabled? dnssec_period=Period between re-signs? dnssec_days=days dnssec_desc=Zones signed with DNSSEC typically have two keys - a zone key which must be re-generated and signed regularly, and a key signing key which remains constant. This page allows you to configure Webmin to perform this re-signing automatically. +dnssec_err=Failed to save DNSSEC key re-signing +dnssec_eperiod=Missing or invalid number of days between re-signs zonekey_title=Setup DNSSEC Key zonekey_desc=This zone does not have a DNSSEC signing key yet. You can use this form to have Webmin create one, so that clients resolving this zone are protected against DNS spoofing attacks. @@ -1031,3 +1034,6 @@ zonekey_signdesc=Immediately re-sign this zone, so that any changes to records m sign_err=Failed to sign zone sign_emsg=DNSSEC signing after records change failed : $1 + +resign_err=Failed to re-sign zone + diff --git a/bind8/resign.pl b/bind8/resign.pl new file mode 100644 index 000000000..47e3221bc --- /dev/null +++ b/bind8/resign.pl @@ -0,0 +1,46 @@ +#!/usr/local/bin/perl +# Called from cron to re-sign all zones that are too old + +$no_acl_check++; +require './bind8-lib.pl'; + +if ($ARGV[0] eq "--debug") { + $debug = 1; + } +if (!$config{'dnssec_period'}) { + print STDERR "Maximum age not set\n" if ($debug); + exit(0); + } + +@zones = &list_zone_names(); +$errcount = 0; +foreach $z (@zones) { + # Get the key + next if ($z->{'type'} ne 'master'); + print STDERR "Considering zone $z->{'name'}\n" if ($debug); + @keys = &get_dnssec_keys($z); + print STDERR " Key count ",scalar(@keys),"\n" if ($debug); + next if (@keys != 2); + ($zonekey) = grep { !$_->{'ksk'} } @keys; + next if (!$zonekey); + print STDERR " Zone key in ",$zonekey->{'privatefile'},"\n" + if ($debug); + + # Check if old enough + @st = stat($key->{'privatefile'}); + $old = (time() - $st[9]) / (24*60*60) + print STDERR " Age in days $old\n" if ($debug); + if ($old > $config{'dnssec_period'}) { + # Too old .. signing + $err = &resign_dnssec_key($z); + if ($err) { + print STDERR " Re-signing failed : $err\n"; + $errcount++; + } + elsif ($debug) { + print STDERR " Re-signed OK\n"; + } + } + } +exit($errcount); + diff --git a/bind8/resign_zone.cgi b/bind8/resign_zone.cgi new file mode 100755 index 000000000..21cfc70e8 --- /dev/null +++ b/bind8/resign_zone.cgi @@ -0,0 +1,21 @@ +#!/usr/local/bin/perl +# Re-generate the zone key and re-sign a zone + +require './bind8-lib.pl'; +&error_setup($text{'resign_err'}); +&ReadParse(); +$zone = &get_zone_name($in{'index'}, $in{'view'}); +$dom = $zone->{'name'}; +&can_edit_zone($zone) || + &error($text{'master_ecannot'}); + +# Do the signing +&lock_file(&make_chroot(&absolute_path($zone->{'file'}))); +$err = &resign_dnssec_zone($zone); +&error($err) if ($err); +&unlock_file(&make_chroot(&absolute_path($zone->{'file'}))); + +# Return to master page +&webmin_log("resign", undef, $dom); +&redirect("edit_master.cgi?index=$in{'index'}&view=$in{'view'}"); + diff --git a/bind8/save_dnssec.cgi b/bind8/save_dnssec.cgi new file mode 100644 index 000000000..fe7053b35 --- /dev/null +++ b/bind8/save_dnssec.cgi @@ -0,0 +1,43 @@ +#!/usr/local/bin/perl +# Turn on or off the DNSSEC key rotation cron job + +require './bind8-lib.pl'; +&foreign_require("cron", "cron-lib.pl"); +&ReadParse(); +&error_setup($text{'dnssec_err'}); +$access{'defaults'} || &error($text{'dnssec_ecannot'}); + +$in{'period'} =~ /^[1-9]\d*$/ || &error($text{'dnssec_eperiod'}); + +# Create or delete the cron job +$job = &get_dnssec_cron_job(); +if ($job && !$in{'enabled'}) { + # Turn off cron job + &lock_file(&cron::cron_file($job)); + &cron::delete_cron_job($job); + &unlock_file(&cron::cron_file($job)); + } +elsif (!$job && $in{'enabled'}) { + # Turn on cron job + $job = { 'user' => 'root', + 'active' => 1, + 'command' => $dnssec_cron_cmd, + 'mins' => int(rand()*60), + 'hours' => int(rand()*24), + 'days' => '*', + 'months' => '*', + 'weekdays' => '*' }; + &lock_file(&cron::cron_file($job)); + &cron::create_cron_job($job); + &unlock_file(&cron::cron_file($job)); + } +&cron::create_wrapper($dnssec_cron_cmd, $module_name, "renew.pl"); + +&lock_file($module_config_file); +$config{'dnssec_period'} = $in{'period'}; +&save_module_config(); +&unlock_file($module_config_file); + +&webmin_log("dnssec"); +&redirect(""); +