mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Add DKIM auto-provisioning, OS/panel self-update with self-healing
- AccountManager: auto-generate DKIM keypair + inject SPF/DKIM/DMARC DNS records on account create - AccountManager: rotateDKIM() method for key rotation with new selector - New dkim.php endpoint: list/view/rotate/provision DKIM keys per domain - schema.sql: add dkim_keys table - install.sh: install opendkim, wire into Postfix milter, fix dotfile copy (. vs *), fix config.ini permissions (root:www-data 640), copy VERSION to web root, add opendkim to service restart - api/index.php: fix NOVACPX_ROOT path (was 2 levels too high), fix CORS ports (8880-8883), VERSION fallback to /opt/novacpx-src - api/.htaccess: route all /api/* requests through index.php - system.php: check-os-update, apply-os-update (self-healing: auto-restart downed services, restore web root if panel ports go down), check-novacpx-update, apply-novacpx-update (PHP syntax validation before deploy, backup + restore on failure) - admin.js: Updates page now shows both NovaCPX panel updates and OS package upgrades in one section; sidebar badge shows combined count Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,9 @@ class AccountManager {
|
||||
// Create DNS zone
|
||||
DNSManager::createZone($acctId, $domain);
|
||||
|
||||
// Auto-provision SPF, DKIM, DMARC records
|
||||
self::provisionEmailDNS($acctId, $domain);
|
||||
|
||||
// Create PHP-FPM pool
|
||||
PHPManager::createPool($username, $phpVer);
|
||||
|
||||
@@ -110,6 +113,72 @@ class AccountManager {
|
||||
novacpx_log('info', "Account terminated: {$acct['username']}");
|
||||
}
|
||||
|
||||
public static function provisionEmailDNS(int $acctId, string $domain): void {
|
||||
// Generate DKIM keypair
|
||||
$keyDir = "/etc/opendkim/keys/{$domain}";
|
||||
self::shell("mkdir -p " . escapeshellarg($keyDir));
|
||||
self::shell("opendkim-genkey -b 2048 -s mail -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir));
|
||||
self::shell("chown -R opendkim:opendkim " . escapeshellarg($keyDir));
|
||||
|
||||
// Parse public key from .txt file
|
||||
$keyTxt = @file_get_contents("{$keyDir}/mail.txt") ?: '';
|
||||
preg_match('/p=([A-Za-z0-9+\/=]+)/', $keyTxt, $m);
|
||||
$pubKey = $m[1] ?? '';
|
||||
|
||||
if ($pubKey) {
|
||||
// Register domain/key in opendkim tables
|
||||
self::shell("grep -q " . escapeshellarg($domain) . " /etc/opendkim/signing.table 2>/dev/null || echo " . escapeshellarg("*@{$domain} {$domain}") . " >> /etc/opendkim/signing.table");
|
||||
self::shell("grep -q " . escapeshellarg($domain) . " /etc/opendkim/key.table 2>/dev/null || echo " . escapeshellarg("{$domain} {$domain}:mail:{$keyDir}/mail.private") . " >> /etc/opendkim/key.table");
|
||||
self::shell("systemctl reload opendkim 2>/dev/null || true");
|
||||
|
||||
// Store in DB
|
||||
$db = DB::getInstance();
|
||||
$db->execute(
|
||||
"INSERT INTO dkim_keys (account_id, domain, selector, public_key, private_key_path, created_at) VALUES (?,?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE public_key=VALUES(public_key)",
|
||||
[$acctId, $domain, 'mail', $pubKey, "{$keyDir}/mail.private"]
|
||||
);
|
||||
|
||||
// DKIM TXT record
|
||||
DNSManager::addRecord($acctId, $domain, 'TXT', "mail._domainkey", "v=DKIM1; k=rsa; p={$pubKey}", 300);
|
||||
}
|
||||
|
||||
// SPF
|
||||
DNSManager::addRecord($acctId, $domain, 'TXT', '@', "v=spf1 mx a ~all", 300);
|
||||
// DMARC
|
||||
DNSManager::addRecord($acctId, $domain, 'TXT', '_dmarc', "v=DMARC1; p=quarantine; rua=mailto:dmarc@{$domain}", 300);
|
||||
|
||||
novacpx_log('info', "Email DNS provisioned for $domain");
|
||||
}
|
||||
|
||||
public static function rotateDKIM(int $acctId, string $domain): string {
|
||||
$db = DB::getInstance();
|
||||
$selector = 'mail' . date('Ym');
|
||||
$keyDir = "/etc/opendkim/keys/{$domain}";
|
||||
self::shell("mkdir -p " . escapeshellarg($keyDir));
|
||||
self::shell("opendkim-genkey -b 2048 -s {$selector} -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir));
|
||||
self::shell("chown -R opendkim:opendkim " . escapeshellarg($keyDir));
|
||||
|
||||
$keyTxt = @file_get_contents("{$keyDir}/{$selector}.txt") ?: '';
|
||||
preg_match('/p=([A-Za-z0-9+\/=]+)/', $keyTxt, $m);
|
||||
$pubKey = $m[1] ?? '';
|
||||
if (!$pubKey) throw new RuntimeException("DKIM key generation failed");
|
||||
|
||||
// Update key.table
|
||||
$keyTableLine = "{$domain} {$domain}:{$selector}:{$keyDir}/{$selector}.private";
|
||||
self::shell("sed -i " . escapeshellarg("/^{$domain} /d") . " /etc/opendkim/key.table 2>/dev/null; echo " . escapeshellarg($keyTableLine) . " >> /etc/opendkim/key.table");
|
||||
self::shell("systemctl reload opendkim 2>/dev/null || true");
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO dkim_keys (account_id, domain, selector, public_key, private_key_path, created_at) VALUES (?,?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE selector=VALUES(selector), public_key=VALUES(public_key), private_key_path=VALUES(private_key_path)",
|
||||
[$acctId, $domain, $selector, $pubKey, "{$keyDir}/{$selector}.private"]
|
||||
);
|
||||
|
||||
// Add new TXT record, remove old mail._domainkey
|
||||
DNSManager::addRecord($acctId, $domain, 'TXT', "{$selector}._domainkey", "v=DKIM1; k=rsa; p={$pubKey}", 300);
|
||||
novacpx_log('info', "DKIM rotated for $domain, new selector: $selector");
|
||||
return $selector;
|
||||
}
|
||||
|
||||
public static function getDiskUsage(string $homeDir): int {
|
||||
$out = trim(shell_exec("du -sm " . escapeshellarg($homeDir) . " 2>/dev/null | awk '{print $1}'") ?: '0');
|
||||
return (int)$out;
|
||||
|
||||
Reference in New Issue
Block a user