add validation script to find configs to remove
This commit is contained in:
@@ -24,6 +24,7 @@ WriteMakefile(
|
||||
'bin/sync-unified-master',
|
||||
'bin/dump-config',
|
||||
'bin/update-config',
|
||||
'bin/validate-unified-files',
|
||||
],
|
||||
'AUTHOR' => 'Jorj Bauer <jorj@jorj.org>',
|
||||
);
|
||||
|
||||
239
bin/validate-unified-files
Executable file
239
bin/validate-unified-files
Executable file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/perl
|
||||
#
|
||||
# validate-unified-files
|
||||
#
|
||||
# Validate that the only files present in one or more BIND directories are
|
||||
# "related to" zones described in a unified zones config (typically generated by
|
||||
# sync-unified-master).
|
||||
#
|
||||
# - Reads unified config (default: /etc/bind/unified.zones.9)
|
||||
# - Extracts:
|
||||
# * zone names: zone "example.org" { ... }
|
||||
# * referenced paths in: file "..." ; journal "..." ; include "..."
|
||||
# - Builds an allow-set of basenames:
|
||||
# * the exact referenced basenames
|
||||
# * "related" siblings: .jnl, .signed, .jbk, backups of those
|
||||
# - Additionally allows DNSSEC-related artifacts for zones that exist in unified:
|
||||
# * K<zone>+*.key / K<zone>+*.private
|
||||
# * dsset-<zone>. / keyset-<zone>. (common dnssec-signzone outputs)
|
||||
# * <zone>.signed (already covered via zonefile sibling rules if zonefile is db.<zone>)
|
||||
# * *.ds / *.dnskey / *.keyset / *.dsset variants that are zone-specific
|
||||
#
|
||||
# Usage:
|
||||
# validate-unified-files [--unified /path] [--dir DIR --dir DIR ...] [--strict] [--quiet]
|
||||
#
|
||||
# Exit:
|
||||
# 0: OK (or warnings but not strict)
|
||||
# 2: warnings and --strict
|
||||
#
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Getopt::Long qw/GetOptions/;
|
||||
use File::Basename qw/basename/;
|
||||
|
||||
my $unified = '/etc/bind/unified.zones.9';
|
||||
my @dirs = ('/etc/bind/custom', '/etc/bind/vhost');
|
||||
my $strict = 0;
|
||||
my $quiet = 0;
|
||||
|
||||
GetOptions(
|
||||
'unified=s' => \$unified,
|
||||
'dir=s@' => \@dirs, # may be repeated
|
||||
'strict!' => \$strict, # exit nonzero if warnings
|
||||
'quiet|q!' => \$quiet,
|
||||
) or die "Usage: $0 [--unified /path/to/unified.zones.9] [--dir DIR --dir DIR] [--strict] [-q]\n";
|
||||
|
||||
sub _esc_re {
|
||||
my ($s) = @_;
|
||||
$s =~ s/([\\.^\$|(){}\[\]*+?])/\\$1/g;
|
||||
return $s;
|
||||
}
|
||||
|
||||
sub allow_related_by_base {
|
||||
my ($base, $allowed_ref) = @_;
|
||||
return if !defined $base || $base eq '';
|
||||
|
||||
# Always allow the thing itself
|
||||
$allowed_ref->{$base} = 1;
|
||||
|
||||
# Helper: add common backup suffixes for a given filename
|
||||
my $add_backups = sub {
|
||||
my ($fn) = @_;
|
||||
$allowed_ref->{$fn} = 1;
|
||||
$allowed_ref->{"$fn.jbk"} = 1;
|
||||
$allowed_ref->{"$fn.bak"} = 1;
|
||||
$allowed_ref->{"$fn.old"} = 1;
|
||||
$allowed_ref->{"$fn.tmp"} = 1;
|
||||
};
|
||||
|
||||
# If base is a zonefile (neither .jnl nor .signed nor include), allow common siblings.
|
||||
# We handle .jnl and .signed specially below.
|
||||
if ($base !~ /\.jnl$/ && $base !~ /\.signed$/) {
|
||||
# base zonefile variants
|
||||
$add_backups->($base);
|
||||
|
||||
# journal of base
|
||||
$add_backups->("$base.jnl");
|
||||
|
||||
# signed zonefile + its journal (THIS is the one you hit)
|
||||
$add_backups->("$base.signed");
|
||||
$add_backups->("$base.signed.jnl");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
# If base is a journal, allow the corresponding base zonefile + signed siblings
|
||||
if ($base =~ /\.jnl$/) {
|
||||
(my $zf = $base) =~ s/\.jnl$//;
|
||||
if ($zf ne $base) {
|
||||
$add_backups->($zf);
|
||||
$add_backups->("$zf.signed");
|
||||
$add_backups->("$zf.signed.jnl");
|
||||
$add_backups->($base); # the journal itself
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
# If base is a signed zonefile, allow its underlying zonefile + journals
|
||||
if ($base =~ /\.signed$/) {
|
||||
(my $zf = $base) =~ s/\.signed$//;
|
||||
if ($zf ne $base) {
|
||||
$add_backups->($zf);
|
||||
$add_backups->("$zf.jnl");
|
||||
$add_backups->($base);
|
||||
$add_backups->("$base.jnl");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
# Fallback: just allow the file itself (already done)
|
||||
}
|
||||
|
||||
sub allow_dnssec_artifacts_for_zone {
|
||||
my ($zone, $allowed_ref, $allowed_regex_ref) = @_;
|
||||
return if !defined $zone || $zone eq '';
|
||||
|
||||
# Normalize: ensure trailing dot for keyset/dsset patterns is optional
|
||||
# (tools vary in whether they include a trailing dot in filenames).
|
||||
my $z_no_dot = $zone;
|
||||
$z_no_dot =~ s/\.$//;
|
||||
|
||||
# 1) DNSSEC key files produced by dnssec-keygen:
|
||||
# Kexample.org.+013+12345.key / .private
|
||||
# Some setups also create "state" files; we allow those too if present.
|
||||
my $zre = _esc_re($z_no_dot);
|
||||
push @$allowed_regex_ref, qr/^K$zre\+\d+\+\d+\.(key|private)$/;
|
||||
push @$allowed_regex_ref, qr/^K$zre\+\d+\+\d+(\.state|\.signed|\.published|\.removed)?$/; # very conservative extras
|
||||
|
||||
# 2) dnssec-signzone outputs:
|
||||
# dsset-example.org. / keyset-example.org.
|
||||
# Sometimes with/without trailing dot.
|
||||
my $zre_dot_optional = _esc_re($z_no_dot) . '\.?';
|
||||
push @$allowed_regex_ref, qr/^(dsset|keyset)-$zre_dot_optional$/;
|
||||
|
||||
# 3) Occasionally you might keep DS or DNSKEY exports as separate files:
|
||||
# example.org.ds / example.org.dnskey (or .dnskeys)
|
||||
push @$allowed_regex_ref, qr/^$zre(\.ds|\.dnskey|\.dnskeys)$/;
|
||||
|
||||
# 4) Some workflows store per-zone keyset/dsset with suffixes/backups:
|
||||
push @$allowed_regex_ref, qr/^(dsset|keyset)-$zre_dot_optional(\.jbk|\.bak|\.old|\.tmp)$/;
|
||||
|
||||
# Note: we intentionally do NOT blanket-allow "any *.key" etc.
|
||||
# Everything remains tied to a zone that exists in the unified file.
|
||||
}
|
||||
|
||||
# --- Parse unified config, building allowed basenames and zone names ---
|
||||
open(my $fh, '<', $unified) or die "Can't open $unified: $!\n";
|
||||
|
||||
my %allowed; # exact basenames allowed
|
||||
my %zones_in_unified; # zone names encountered
|
||||
my @allowed_regex; # regex-based allows (DNSSEC artifacts, etc.)
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
chomp $line;
|
||||
|
||||
# Skip comment-only lines quickly
|
||||
next if $line =~ /^\s*#/;
|
||||
next if $line =~ /^\s*;/;
|
||||
|
||||
# Capture zone names: zone "k8s.jorj.org" { ... };
|
||||
# This works even if multiple zones are on one line.
|
||||
while ($line =~ /\bzone\s+"([^"]+)"\s*\{/g) {
|
||||
my $z = $1;
|
||||
$zones_in_unified{$z} = 1;
|
||||
allow_dnssec_artifacts_for_zone($z, \%allowed, \@allowed_regex);
|
||||
}
|
||||
|
||||
# Collect referenced paths: file/journal/include "..."
|
||||
while ($line =~ /\b(file|journal|include)\s+"([^"]+)"/g) {
|
||||
my $path = $2;
|
||||
my $base = basename($path);
|
||||
allow_related_by_base($base, \%allowed);
|
||||
}
|
||||
}
|
||||
close $fh;
|
||||
|
||||
# Extra: If you keep zonefiles named db.<zone> in the validated dirs, but unified points
|
||||
# to /var/lib/bind/custom/db.<zone>, then basenames already match.
|
||||
# If you ALSO keep DNSSEC-signed zonefiles as db.<zone>.signed, those are allowed via siblings.
|
||||
|
||||
# --- Scan target dirs and warn about anything not allowed ---
|
||||
my $warnings = 0;
|
||||
|
||||
DIR:
|
||||
for my $dir (@dirs) {
|
||||
if (!-d $dir) {
|
||||
print "WARNING: directory not found: $dir\n" unless $quiet;
|
||||
$warnings++;
|
||||
next DIR;
|
||||
}
|
||||
|
||||
opendir(my $dh, $dir) or do {
|
||||
print "WARNING: cannot open directory $dir: $!\n" unless $quiet;
|
||||
$warnings++;
|
||||
next DIR;
|
||||
};
|
||||
|
||||
while (my $name = readdir($dh)) {
|
||||
next if $name eq '.' || $name eq '..';
|
||||
|
||||
my $path = "$dir/$name";
|
||||
|
||||
# Only care about files and symlinks; skip subdirectories.
|
||||
next if -d $path;
|
||||
|
||||
my $ok = 0;
|
||||
|
||||
# Exact match allow
|
||||
if ($allowed{$name}) {
|
||||
$ok = 1;
|
||||
} else {
|
||||
# Regex allow (DNSSEC-related, zone-tied artifacts)
|
||||
for my $re (@allowed_regex) {
|
||||
if ($name =~ $re) {
|
||||
$ok = 1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next if $ok;
|
||||
|
||||
print "WARNING: stray file not referenced by $unified: $path\n";
|
||||
$warnings++;
|
||||
}
|
||||
|
||||
closedir $dh;
|
||||
}
|
||||
|
||||
if (!$quiet) {
|
||||
if ($warnings) {
|
||||
print "validate-unified-files: $warnings warning(s)\n";
|
||||
} else {
|
||||
print "validate-unified-files: OK (no stray files)\n";
|
||||
}
|
||||
}
|
||||
|
||||
exit(($strict && $warnings) ? 2 : 0);
|
||||
Reference in New Issue
Block a user