mirror of
https://github.com/getnamingo/registry.git
synced 2025-05-16 17:46:59 +02:00
DNSSEC key rollover updates
This commit is contained in:
parent
44e975bf6e
commit
d4a935055f
7 changed files with 70 additions and 247 deletions
|
@ -16,7 +16,6 @@
|
|||
// 'backup_upload' => false, // Enable or disable backup upload
|
||||
// 'gtld_mode' => false, // Enable or disable gTLD mode
|
||||
// 'spec11' => false, // Enable or disable Spec 11 checks
|
||||
// 'dnssec' => false, // Enable or disable DNSSEC
|
||||
// 'exchange_rates' => false, // Enable or disable exchange rate download
|
||||
// ];
|
||||
//
|
||||
|
@ -31,7 +30,6 @@ $defaultConfig = [
|
|||
'backup_upload' => false, // Set to true to enable
|
||||
'gtld_mode' => 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
|
||||
];
|
||||
|
||||
|
@ -74,10 +72,6 @@ if ($cronJobConfig['backup_upload']) {
|
|||
$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']) {
|
||||
$scheduler->php('/opt/registry/automation/abusemonitor.php')->at('30 * * * *');
|
||||
$scheduler->php('/opt/registry/automation/abusereport.php')->at('5 0 * * *');
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -1012,9 +1012,8 @@ class SystemController extends Controller
|
|||
|
||||
$secureTld = $tld['secure'];
|
||||
if ($secureTld === 1) {
|
||||
$tld_extension_cleaned = ltrim($tld['tld'], '.');
|
||||
$zone = escapeshellarg($tld_extension_cleaned);
|
||||
$statusOutput = shell_exec("rndc dnssec -status $zone");
|
||||
$zone = ltrim($tld['tld'], '.');
|
||||
$statusOutput = shell_exec("sudo rndc dnssec -status " . escapeshellarg($zone) . " 2>&1");
|
||||
|
||||
if (!$statusOutput) {
|
||||
$dnssecData = ['error' => "Unable to fetch DNSSEC status for $zone."];
|
||||
|
@ -1032,13 +1031,45 @@ class SystemController extends Controller
|
|||
$keyId = $match[1];
|
||||
$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
|
||||
$keyStatus = strpos($statusOutput, "key: $keyId") !== false
|
||||
? (strpos($statusOutput, "key signing: yes") !== false ? 'Active' : 'Pending Rollover')
|
||||
: 'Unknown';
|
||||
preg_match("/key: $keyId.*?(?=\\nkey:|\\z)/s", $statusOutput, $keyBlockMatch);
|
||||
$keyBlock = $keyBlockMatch[0] ?? '';
|
||||
|
||||
// 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
|
||||
$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';
|
||||
|
||||
// Append key details
|
||||
|
@ -1048,6 +1079,10 @@ class SystemController extends Controller
|
|||
'ds_record' => $dsRecord,
|
||||
'status' => $keyStatus,
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'next_rollover' => $nextRollover,
|
||||
'retirement_date' => $retirementDate,
|
||||
'published_date' => $publishedDate,
|
||||
'ds_status' => $dsStatus,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -105,18 +105,16 @@
|
|||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Key ID') }}</th>
|
||||
<th>{{ __('Algorithm') }}</th>
|
||||
<th>{{ __('DS Record') }}</th>
|
||||
<th>{{ __('Status') }}</th>
|
||||
<th>{{ __('Timestamp') }}</th>
|
||||
<th>{{ __('Published') }}</th>
|
||||
<th>{{ __('Next Rollover') }}</th>
|
||||
<th>{{ __('Parent') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key in dnssecData.keys %}
|
||||
<tr>
|
||||
<td>{{ key.key_id }}</td>
|
||||
<td>{{ key.algorithm }}</td>
|
||||
<td>
|
||||
{% if key.ds_record != 'N/A' %}
|
||||
<p class="user-select-all tracking-wide mb-0">
|
||||
|
@ -128,14 +126,30 @@
|
|||
</td>
|
||||
<td>
|
||||
{% 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' %}
|
||||
<span class="badge bg-warning">{{ __('Pending Rollover') }}</span>
|
||||
<span class="badge bg-warning text-warning-fg">{{ __('Pending Rollover') }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ __('Unknown') }}</span>
|
||||
<span class="badge bg-secondary text-secondary-fg">{{ __('Unknown') }}</span>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -309,7 +309,6 @@ return [
|
|||
'backup_upload' => false, // Enable or disable backup upload
|
||||
'gtld_mode' => false, // Enable or disable gTLD mode
|
||||
'spec11' => false, // Enable or disable Spec 11 checks
|
||||
'dnssec' => false, // Enable or disable DNSSEC
|
||||
'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
|
||||
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.
|
||||
|
||||
Initially, you will need to generate the DNSSEC ZSK and KSK manually:
|
||||
|
||||
```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:
|
||||
Finally, set correct permissions and restart BIND9 to apply changes:
|
||||
|
||||
```bash
|
||||
chown -R bind:bind /var/lib/bind
|
||||
systemctl restart bind9
|
||||
rndc loadkeys test.
|
||||
```
|
||||
|
||||
Configure the `Zone Writer` in Registry Automation and run it manually the first time.
|
||||
|
|
|
@ -152,6 +152,9 @@ done
|
|||
|
||||
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
|
||||
echo "Starting services..."
|
||||
systemctl start epp
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue