DNSSEC key rollover updates

This commit is contained in:
Pinga 2025-03-11 18:32:05 +02:00
parent 44e975bf6e
commit d4a935055f
7 changed files with 70 additions and 247 deletions

View file

@ -16,7 +16,6 @@
// 'backup_upload' => false, // Enable or disable backup upload // 'backup_upload' => false, // Enable or disable backup upload
// 'gtld_mode' => false, // Enable or disable gTLD mode // 'gtld_mode' => false, // Enable or disable gTLD mode
// 'spec11' => false, // Enable or disable Spec 11 checks // 'spec11' => false, // Enable or disable Spec 11 checks
// 'dnssec' => false, // Enable or disable DNSSEC
// 'exchange_rates' => false, // Enable or disable exchange rate download // 'exchange_rates' => false, // Enable or disable exchange rate download
// ]; // ];
// //
@ -31,7 +30,6 @@ $defaultConfig = [
'backup_upload' => false, // Set to true to enable 'backup_upload' => false, // Set to true to enable
'gtld_mode' => false, // Set to true to enable 'gtld_mode' => false, // Set to true to enable
'spec11' => false, // Set to true to enable 'spec11' => false, // Set to true to enable
'dnssec' => false, // Set to true to enable
'exchange_rates' => false, // Set to true to enable 'exchange_rates' => false, // Set to true to enable
]; ];
@ -74,10 +72,6 @@ if ($cronJobConfig['backup_upload']) {
$scheduler->php('/opt/registry/automation/backup-upload.php')->at('30 * * * *'); $scheduler->php('/opt/registry/automation/backup-upload.php')->at('30 * * * *');
} }
if ($cronJobConfig['dnssec']) {
$scheduler->php('/opt/registry/automation/dnssec-ds-rotator.php')->at('0 0 * * *');
}
if ($cronJobConfig['spec11']) { if ($cronJobConfig['spec11']) {
$scheduler->php('/opt/registry/automation/abusemonitor.php')->at('30 * * * *'); $scheduler->php('/opt/registry/automation/abusemonitor.php')->at('30 * * * *');
$scheduler->php('/opt/registry/automation/abusereport.php')->at('5 0 * * *'); $scheduler->php('/opt/registry/automation/abusereport.php')->at('5 0 * * *');

View file

@ -1,168 +0,0 @@
<?php
require_once 'vendor/autoload.php';
$c = require_once 'config.php';
require_once 'helpers.php';
// Configuration
$keyDir = $c['dns_server'] === 'bind' ? '/var/lib/bind' : '/etc/knot/keys'; // Directory containing key files
$adminEmail = isset($c['iana_email']) && !empty($c['iana_email']) ? $c['iana_email'] : 'admin@example.com'; // Email for IANA submission logs
$dnssecTool = $c['dns_server'] === 'bind' ? '/usr/bin/dnssec-dsfromkey' : '/usr/bin/keymgr'; // Tool path
$logFilePath = '/var/log/namingo/dnssec-ds-rotator.log';
$log = setupLogger($logFilePath, 'DNSSEC_DS_Rotator');
$log->info("Starting DS record handling for " . strtoupper($c['dns_server']) . ".");
try {
// Connect to the database
$dsn = "{$c['db_type']}:host={$c['db_host']};dbname={$c['db_database']};port={$c['db_port']}";
$dbh = new PDO($dsn, $c['db_username'], $c['db_password']);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Query the domain_tld table
$query = "SELECT tld FROM domain_tld";
$stmt = $dbh->query($query);
// Loop through all rows
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$zoneName = ltrim($row['tld'], '.'); // Remove leading dots
// Process the zone name
$log->info("Processing zone: $zoneName");
if ($c['dns_server'] === 'bind') {
// Locate all keys for the zone (BIND)
$keyFiles = glob("$keyDir/K$zoneName.+*.key");
if (empty($keyFiles)) {
$log->error("No keys found for $zoneName in $keyDir.");
continue;
}
// Filter for KSKs (flag 257)
$kskFiles = [];
foreach ($keyFiles as $keyFile) {
$keyContent = file_get_contents($keyFile);
if (strpos($keyContent, '257') !== false) {
$kskFiles[] = $keyFile;
}
}
if (empty($kskFiles)) {
$log->error("No KSKs found for $zoneName in $keyDir.");
continue;
}
// Process each KSK and generate DS records
$keys = [];
foreach ($kskFiles as $kskFile) {
exec("$dnssecTool -a SHA-256 $kskFile", $output, $returnCode);
if ($returnCode !== 0 || empty($output)) {
$log->error("Failed to generate DS record for $zoneName (key file: $kskFile).");
continue;
}
$dsRecord = implode("\n", $output);
$lastModified = filemtime($kskFile);
$keyData = [
'keyFile' => $kskFile,
'dsRecord' => $dsRecord,
'timestamp' => $lastModified ? date('Y-m-d H:i:s', $lastModified) : 'unknown',
];
$keys[] = $keyData;
$log->info("DS Record Generated for KSK file $kskFile: $dsRecord");
}
} elseif ($c['dns_server'] === 'knot') {
// **Knot DNS: Use keymgr to manage keys and DS records**
$keys = [];
exec("$dnssecTool ds $zoneName", $output, $returnCode);
if ($returnCode !== 0 || empty($output)) {
$log->error("Failed to generate DS record for $zoneName using Knot DNS.");
continue;
}
$dsRecord = implode("\n", $output);
$keyData = [
'dsRecord' => $dsRecord,
'timestamp' => date('Y-m-d H:i:s'),
];
$keys[] = $keyData;
$log->info("DS Record Generated for zone $zoneName using Knot DNS: $dsRecord");
}
// Prepare data to save
$data = [
'zoneName' => $zoneName,
'timestamp' => date('Y-m-d H:i:s'),
'keys' => $keys,
];
// Save to /tmp as JSON
$filePath = "/tmp/{$zoneName}.json";
file_put_contents($filePath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
$log->info("Saved zone data for $zoneName to $filePath");
// Determine zone type and handle DS submission
$levelCount = substr_count($zoneName, '.') + 1;
if ($levelCount === 1) {
$log->info("Logging DS record details for manual submission to IANA...");
$ianaDetails = [
'Zone' => $zoneName,
'DS Records' => array_column($keys, 'dsRecord'),
'Admin Contact' => $adminEmail,
];
$log->info(json_encode($ianaDetails, JSON_PRETTY_PRINT));
foreach ($keys as $key) {
$log->info($key['dsRecord']);
}
} elseif ($levelCount >= 2) {
$log->info("DS record for $zoneName should be submitted to the parent registry.");
foreach ($keys as $key) {
$log->info($key['dsRecord']);
}
// You must create the script at the specified path: /opt/registry/automation/ds-update.php.
// This script is responsible for submitting the DS record for your zone to the top-level domain registrar.
// The implementation of this script will depend on the registrar's API or the registry's EPP system.
// If you are using EPP for your registry communication, you can refer to our Tembo project for a sample EPP client.
// Tembo provides a flexible and customizable way to interact with EPP-based registries, which can simplify your implementation.
// Ensure your script handles all necessary authentication, logging, and error handling when interacting with the registrar.
$dsUpdateScript = '/opt/registry/automation/ds-update.php';
if (!file_exists($dsUpdateScript)) {
$log->error("The DS record submission script ($dsUpdateScript) does not exist. Please create it to enable submission to the parent registry.");
continue;
}
$log->info("Submitting DS record to the parent zone using the local PHP script...");
$response = shell_exec("php /opt/registry/automation/ds-update.php $zoneName '" . json_encode($keys) . "'");
// Check the response for success
if (str_contains($response, 'success')) {
$log->info("DS record successfully submitted to the parent zone for $zoneName.");
} else {
$log->error("Failed to submit DS record to the parent zone for $zoneName.");
$log->error("Response from PHP script: $response");
continue;
}
} else {
$log->error("Unsupported zone type for $zoneName.");
continue;
}
$log->info("DS record handling completed successfully for $zoneName.");
}
$log->info('Job finished successfully.');
} catch (PDOException $e) {
$log->error('DB Connection failed: ' . $e->getMessage());
exit(1);
} catch (Exception $e) {
$log->error('An unexpected error occurred: ' . $e->getMessage());
exit(1);
}

View file

@ -1,44 +0,0 @@
#!/bin/bash
ZONE="example.tld"
ROOT_SERVER="a.root-servers.net"
KEYDIR="/var/lib/bind" # Adjust if needed
LOGFILE="/var/log/namingo/dnssec-rollover.log"
echo "[$(date)] Checking IANA root zone for DS update..." | tee -a $LOGFILE
# Fetch DS records from IANA root zone
root_ds=$(dig +short DS $ZONE @$ROOT_SERVER | awk '{print $1, $2, $3, $4}')
if [[ -z "$root_ds" ]]; then
echo "[$(date)] ERROR: Unable to fetch DS records from IANA. Exiting." | tee -a $LOGFILE
exit 1
fi
# Fetch DS records from BIND (local KSKs)
local_ds=$(dnssec-dsfromkey -2 $KEYDIR/K${ZONE}*.key | awk '{print $1, $2, $3, $4}')
if [[ -z "$local_ds" ]]; then
echo "[$(date)] ERROR: Unable to fetch DS records from BIND. Exiting." | tee -a $LOGFILE
exit 1
fi
echo "[$(date)] DS records at IANA:" | tee -a $LOGFILE
echo "$root_ds" | tee -a $LOGFILE
echo "[$(date)] DS records in BIND:" | tee -a $LOGFILE
echo "$local_ds" | tee -a $LOGFILE
# Step 1: Check if IANA DS matches BIND DS (safe transition condition)
if [[ "$root_ds" == "$local_ds" ]]; then
echo "[$(date)] IANA has updated DS record. Safe to retire old KSK." | tee -a $LOGFILE
# Step 2: Identify and remove old KSKs from BIND
for key in $(ls $KEYDIR/K${ZONE}*.key); do
key_ds=$(dnssec-dsfromkey -2 $key | awk '{print $1, $2, $3, $4}')
if [[ ! "$root_ds" == *"$key_ds"* ]]; then
echo "[$(date)] Removing old KSK: $key" | tee -a $LOGFILE
rndc dnssec -clear key $ZONE
echo "[$(date)] Old KSK $key removed successfully." | tee -a $LOGFILE
fi
done
else
echo "[$(date)] DS record mismatch! Keeping old KSK active. Retrying later..." | tee -a $LOGFILE
fi

View file

@ -1012,9 +1012,8 @@ class SystemController extends Controller
$secureTld = $tld['secure']; $secureTld = $tld['secure'];
if ($secureTld === 1) { if ($secureTld === 1) {
$tld_extension_cleaned = ltrim($tld['tld'], '.'); $zone = ltrim($tld['tld'], '.');
$zone = escapeshellarg($tld_extension_cleaned); $statusOutput = shell_exec("sudo rndc dnssec -status " . escapeshellarg($zone) . " 2>&1");
$statusOutput = shell_exec("rndc dnssec -status $zone");
if (!$statusOutput) { if (!$statusOutput) {
$dnssecData = ['error' => "Unable to fetch DNSSEC status for $zone."]; $dnssecData = ['error' => "Unable to fetch DNSSEC status for $zone."];
@ -1031,14 +1030,46 @@ class SystemController extends Controller
foreach ($matches as $match) { foreach ($matches as $match) {
$keyId = $match[1]; $keyId = $match[1];
$algorithm = $match[2]; $algorithm = $match[2];
// Convert algorithm name to corresponding number for dnssec-dsfromkey
$algoMap = [
'RSASHA1' => '005', 'RSASHA1-NSEC3-SHA1' => '007',
'RSASHA256' => '008', 'RSASHA512' => '010',
'ECDSAP256SHA256' => '013', 'ECDSAP384SHA384' => '014',
'ED25519' => '015', 'ED448' => '016'
];
$algoNum = $algoMap[$algorithm] ?? '000'; // Default to unknown if missing
// Determine if key is active or in rollover state // Determine if key is active or in rollover state
$keyStatus = strpos($statusOutput, "key: $keyId") !== false preg_match("/key: $keyId.*?(?=\\nkey:|\\z)/s", $statusOutput, $keyBlockMatch);
? (strpos($statusOutput, "key signing: yes") !== false ? 'Active' : 'Pending Rollover') $keyBlock = $keyBlockMatch[0] ?? '';
: 'Unknown';
// Skip keys explicitly removed from the zone
if (strpos($keyBlock, 'Key has been removed from the zone') !== false) {
continue;
}
// Determine key status accurately
$keyStatus = strpos($keyBlock, 'key signing: yes') !== false ? 'Active' : 'Pending Rollover';
// Extract next rollover date
preg_match('/Next rollover scheduled on ([^\\n]+)/', $keyBlock, $rolloverMatch);
$nextRollover = $rolloverMatch[1] ?? null;
// Extract retirement date, if present
preg_match('/Key.*removed.*on ([^\\n]+)/', $keyBlock, $retirementMatch);
$retirementDate = $retirementMatch[1] ?? null;
// Extract published date
preg_match('/published:\s+yes\s+-\s+since\s+([^\n]+)/', $keyBlock, $publishedMatch);
$publishedDate = isset($publishedMatch[1]) ? trim($publishedMatch[1]) : null;
// Extract DS status ("rumoured" or "omnipresent")
preg_match('/- ds:\s+(\w+)/', $keyBlock, $dsMatch);
$dsStatus = $dsMatch[1] ?? null;
// Extract DS record for this key // Extract DS record for this key
$dsRecord = shell_exec("dnssec-dsfromkey -2 /var/lib/bind/K{$tld_extension_cleaned}.+008+{$keyId}.key"); $dsRecord = shell_exec("dnssec-dsfromkey -2 /var/lib/bind/K{$zone}.+{$algoNum}+{$keyId}.key");
$dsRecord = $dsRecord ? trim($dsRecord) : 'N/A'; $dsRecord = $dsRecord ? trim($dsRecord) : 'N/A';
// Append key details // Append key details
@ -1048,6 +1079,10 @@ class SystemController extends Controller
'ds_record' => $dsRecord, 'ds_record' => $dsRecord,
'status' => $keyStatus, 'status' => $keyStatus,
'timestamp' => date('Y-m-d H:i:s'), 'timestamp' => date('Y-m-d H:i:s'),
'next_rollover' => $nextRollover,
'retirement_date' => $retirementDate,
'published_date' => $publishedDate,
'ds_status' => $dsStatus,
]; ];
} }

View file

@ -105,18 +105,16 @@
<table class="table table-vcenter card-table"> <table class="table table-vcenter card-table">
<thead> <thead>
<tr> <tr>
<th>{{ __('Key ID') }}</th>
<th>{{ __('Algorithm') }}</th>
<th>{{ __('DS Record') }}</th> <th>{{ __('DS Record') }}</th>
<th>{{ __('Status') }}</th> <th>{{ __('Status') }}</th>
<th>{{ __('Timestamp') }}</th> <th>{{ __('Published') }}</th>
<th>{{ __('Next Rollover') }}</th>
<th>{{ __('Parent') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for key in dnssecData.keys %} {% for key in dnssecData.keys %}
<tr> <tr>
<td>{{ key.key_id }}</td>
<td>{{ key.algorithm }}</td>
<td> <td>
{% if key.ds_record != 'N/A' %} {% if key.ds_record != 'N/A' %}
<p class="user-select-all tracking-wide mb-0"> <p class="user-select-all tracking-wide mb-0">
@ -128,14 +126,30 @@
</td> </td>
<td> <td>
{% if key.status == 'Active' %} {% if key.status == 'Active' %}
<span class="badge bg-success">{{ __('Active') }}</span> <span class="badge bg-success text-success-fg">{{ __('Active') }}</span>
{% elseif key.status == 'Pending Rollover' %} {% elseif key.status == 'Pending Rollover' %}
<span class="badge bg-warning">{{ __('Pending Rollover') }}</span> <span class="badge bg-warning text-warning-fg">{{ __('Pending Rollover') }}</span>
{% else %} {% else %}
<span class="badge bg-secondary">{{ __('Unknown') }}</span> <span class="badge bg-secondary text-secondary-fg">{{ __('Unknown') }}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ key.timestamp }}</td> <td>{{ key.published_date }}</td>
<td>{{ key.next_rollover }}</td>
<td>
{% if key.ds_status == 'omnipresent' %}
<span class="status-indicator status-green" title="DS record submitted and active at parent zone">
<span class="status-indicator-circle"></span>
</span>
{% elseif key.ds_status == 'rumoured' %}
<span class="status-indicator status-orange status-indicator-animated" title="DS record needs submission to parent zone">
<span class="status-indicator-circle"></span>
</span>
{% else %}
<span class="status-indicator status-gray" title="DS record status unknown or unavailable">
<span class="status-indicator-circle"></span>
</span>
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -309,7 +309,6 @@ return [
'backup_upload' => false, // Enable or disable backup upload 'backup_upload' => false, // Enable or disable backup upload
'gtld_mode' => false, // Enable or disable gTLD mode 'gtld_mode' => false, // Enable or disable gTLD mode
'spec11' => false, // Enable or disable Spec 11 checks 'spec11' => false, // Enable or disable Spec 11 checks
'dnssec' => false, // Enable or disable DNSSEC
'exchange_rates' => false, // Enable or disable exchange rate download 'exchange_rates' => false, // Enable or disable exchange rate download
]; ];
``` ```
@ -514,7 +513,7 @@ dnssec-policy "namingo-policy" {
}; };
``` ```
Add the following zone definition: Then, add the zone definition:
```bash ```bash
zone "test." { zone "test." {
@ -530,21 +529,11 @@ zone "test." {
Replace ```<slave-server-IP>``` with the actual IP address of your slave server. Replace ```test``` with your TLD. Replace ```<slave-server-IP>``` with the actual IP address of your slave server. Replace ```test``` with your TLD.
Initially, you will need to generate the DNSSEC ZSK and KSK manually: Finally, set correct permissions and restart BIND9 to apply changes:
```bash
dnssec-keygen -a Ed25519 -n ZONE test.
dnssec-keygen -a Ed25519 -n ZONE -f KSK test.
```
After generating the keys, place them in ```/var/lib/bind```. Run ```dnssec-dsfromkey Ktest.EXAMPLE.key``` on the KSK key you just generated, and the DS record must be submitted to IANA once setup is complete.
Use rndc to tell BIND to load and use the new keys:
```bash ```bash
chown -R bind:bind /var/lib/bind chown -R bind:bind /var/lib/bind
systemctl restart bind9 systemctl restart bind9
rndc loadkeys test.
``` ```
Configure the `Zone Writer` in Registry Automation and run it manually the first time. Configure the `Zone Writer` in Registry Automation and run it manually the first time.

View file

@ -152,6 +152,9 @@ done
wget "http://www.adminer.org/latest.php" -O /usr/share/adminer/latest.php wget "http://www.adminer.org/latest.php" -O /usr/share/adminer/latest.php
echo 'www-data ALL=(ALL) NOPASSWD: /usr/sbin/rndc' > /etc/sudoers.d/namingo-rndc
chmod 440 /etc/sudoers.d/namingo-rndc
# Start services # Start services
echo "Starting services..." echo "Starting services..."
systemctl start epp systemctl start epp