mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
fix: account creation home dir permissions and duplicate SPF record
- Use sudo for mkdir/chown/chmod in home dir setup so www-data can execute - Set public_html to 775 (group-writable) so www-data can deploy index.html - Remove duplicate SPF from createZone defaults (provisionEmailDNS owns SPF/DMARC/DKIM) - sudo mkdir/chown in provisionEmailDNS for opendkim key directory
This commit is contained in:
@@ -28,9 +28,9 @@ class AccountManager {
|
|||||||
// Create Linux user
|
// Create Linux user
|
||||||
self::shell("useradd -m -d {$homeDir} -s /sbin/nologin -G www-data " . escapeshellarg($username));
|
self::shell("useradd -m -d {$homeDir} -s /sbin/nologin -G www-data " . escapeshellarg($username));
|
||||||
self::shell("echo " . escapeshellarg("{$username}:{$password}") . " | chpasswd");
|
self::shell("echo " . escapeshellarg("{$username}:{$password}") . " | chpasswd");
|
||||||
self::shell("mkdir -p {$docRoot} {$homeDir}/logs {$homeDir}/tmp");
|
self::shell("sudo mkdir -p {$docRoot} {$homeDir}/logs {$homeDir}/tmp");
|
||||||
self::shell("chown -R {$username}:www-data {$homeDir}");
|
self::shell("sudo chown -R {$username}:www-data {$homeDir}");
|
||||||
self::shell("chmod 750 {$homeDir}; chmod 755 {$docRoot}");
|
self::shell("sudo chmod 750 {$homeDir}"); self::shell("sudo chmod 775 {$docRoot}");
|
||||||
|
|
||||||
// Default index page
|
// Default index page
|
||||||
file_put_contents("{$docRoot}/index.html",
|
file_put_contents("{$docRoot}/index.html",
|
||||||
@@ -116,9 +116,9 @@ class AccountManager {
|
|||||||
public static function provisionEmailDNS(int $acctId, string $domain): void {
|
public static function provisionEmailDNS(int $acctId, string $domain): void {
|
||||||
// Generate DKIM keypair
|
// Generate DKIM keypair
|
||||||
$keyDir = "/etc/opendkim/keys/{$domain}";
|
$keyDir = "/etc/opendkim/keys/{$domain}";
|
||||||
self::shell("mkdir -p " . escapeshellarg($keyDir));
|
self::shell("sudo mkdir -p " . escapeshellarg($keyDir));
|
||||||
self::shell("opendkim-genkey -b 2048 -s mail -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir));
|
self::shell("opendkim-genkey -b 2048 -s mail -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir));
|
||||||
self::shell("chown -R opendkim:opendkim " . escapeshellarg($keyDir));
|
self::shell("sudo chown -R opendkim:opendkim " . escapeshellarg($keyDir));
|
||||||
|
|
||||||
// Parse public key from .txt file
|
// Parse public key from .txt file
|
||||||
$keyTxt = @file_get_contents("{$keyDir}/mail.txt") ?: '';
|
$keyTxt = @file_get_contents("{$keyDir}/mail.txt") ?: '';
|
||||||
@@ -139,13 +139,17 @@ class AccountManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// DKIM TXT record
|
// DKIM TXT record
|
||||||
DNSManager::addRecord($acctId, $domain, 'TXT', "mail._domainkey", "v=DKIM1; k=rsa; p={$pubKey}", 300);
|
$zoneRow = DB::getInstance()->fetchOne("SELECT id FROM dns_zones WHERE account_id=? AND domain=?", [$acctId, $domain]);
|
||||||
|
if ($zoneRow) DNSManager::addRecord((int)$zoneRow['id'], 'mail._domainkey', 'TXT', "v=DKIM1; k=rsa; p={$pubKey}", 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPF
|
// SPF + DMARC — look up zone once
|
||||||
DNSManager::addRecord($acctId, $domain, 'TXT', '@', "v=spf1 mx a ~all", 300);
|
$db2 = DB::getInstance();
|
||||||
// DMARC
|
$zoneRow = $zoneRow ?? $db2->fetchOne("SELECT id FROM dns_zones WHERE account_id=? AND domain=?", [$acctId, $domain]);
|
||||||
DNSManager::addRecord($acctId, $domain, 'TXT', '_dmarc', "v=DMARC1; p=quarantine; rua=mailto:dmarc@{$domain}", 300);
|
if ($zoneRow) {
|
||||||
|
DNSManager::addRecord((int)$zoneRow['id'], '@', 'TXT', "v=spf1 mx a ~all", 3600);
|
||||||
|
DNSManager::addRecord((int)$zoneRow['id'], '_dmarc', 'TXT', "v=DMARC1; p=quarantine; rua=mailto:dmarc@{$domain}", 3600);
|
||||||
|
}
|
||||||
|
|
||||||
novacpx_log('info', "Email DNS provisioned for $domain");
|
novacpx_log('info', "Email DNS provisioned for $domain");
|
||||||
}
|
}
|
||||||
@@ -154,9 +158,9 @@ class AccountManager {
|
|||||||
$db = DB::getInstance();
|
$db = DB::getInstance();
|
||||||
$selector = 'mail' . date('Ym');
|
$selector = 'mail' . date('Ym');
|
||||||
$keyDir = "/etc/opendkim/keys/{$domain}";
|
$keyDir = "/etc/opendkim/keys/{$domain}";
|
||||||
self::shell("mkdir -p " . escapeshellarg($keyDir));
|
self::shell("sudo mkdir -p " . escapeshellarg($keyDir));
|
||||||
self::shell("opendkim-genkey -b 2048 -s {$selector} -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir));
|
self::shell("opendkim-genkey -b 2048 -s {$selector} -d " . escapeshellarg($domain) . " -D " . escapeshellarg($keyDir));
|
||||||
self::shell("chown -R opendkim:opendkim " . escapeshellarg($keyDir));
|
self::shell("sudo chown -R opendkim:opendkim " . escapeshellarg($keyDir));
|
||||||
|
|
||||||
$keyTxt = @file_get_contents("{$keyDir}/{$selector}.txt") ?: '';
|
$keyTxt = @file_get_contents("{$keyDir}/{$selector}.txt") ?: '';
|
||||||
preg_match('/p=([A-Za-z0-9+\/=]+)/', $keyTxt, $m);
|
preg_match('/p=([A-Za-z0-9+\/=]+)/', $keyTxt, $m);
|
||||||
@@ -174,7 +178,8 @@ class AccountManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Add new TXT record, remove old mail._domainkey
|
// Add new TXT record, remove old mail._domainkey
|
||||||
DNSManager::addRecord($acctId, $domain, 'TXT', "{$selector}._domainkey", "v=DKIM1; k=rsa; p={$pubKey}", 300);
|
$zoneRow = $db->fetchOne("SELECT id FROM dns_zones WHERE account_id=? AND domain=?", [$acctId, $domain]);
|
||||||
|
if ($zoneRow) DNSManager::addRecord((int)$zoneRow['id'], "{$selector}._domainkey", 'TXT', "v=DKIM1; k=rsa; p={$pubKey}", 300);
|
||||||
novacpx_log('info', "DKIM rotated for $domain, new selector: $selector");
|
novacpx_log('info', "DKIM rotated for $domain, new selector: $selector");
|
||||||
return $selector;
|
return $selector;
|
||||||
}
|
}
|
||||||
@@ -185,6 +190,12 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static function shell(string $cmd): string {
|
private static function shell(string $cmd): string {
|
||||||
|
// Prefix privileged commands with sudo so www-data can run them
|
||||||
|
$privileged = ['useradd','userdel','usermod','chpasswd','a2ensite','a2dissite','apache2ctl','certbot','opendkim-genkey','rndc','named-checkzone','systemctl'];
|
||||||
|
$cmdBase = explode(' ', ltrim($cmd))[0];
|
||||||
|
foreach ($privileged as $p) {
|
||||||
|
if (str_ends_with($cmdBase, $p) || $cmdBase === $p) { $cmd = 'sudo ' . $cmd; break; }
|
||||||
|
}
|
||||||
$out = shell_exec($cmd . ' 2>&1');
|
$out = shell_exec($cmd . ' 2>&1');
|
||||||
novacpx_log('debug', "shell: $cmd");
|
novacpx_log('debug', "shell: $cmd");
|
||||||
return $out ?: '';
|
return $out ?: '';
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class DNSManager {
|
|||||||
['www', 'A', $ip, 3600, null],
|
['www', 'A', $ip, 3600, null],
|
||||||
['mail', 'A', $ip, 3600, null],
|
['mail', 'A', $ip, 3600, null],
|
||||||
['@', 'MX', "mail.{$domain}.", 3600, 10],
|
['@', 'MX', "mail.{$domain}.", 3600, 10],
|
||||||
['@', 'TXT', "v=spf1 a mx ~all", 3600, null],
|
|
||||||
];
|
];
|
||||||
foreach ($defaults as [$name, $type, $content, $ttl, $prio]) {
|
foreach ($defaults as [$name, $type, $content, $ttl, $prio]) {
|
||||||
$db->execute(
|
$db->execute(
|
||||||
@@ -127,13 +126,14 @@ class DNSManager {
|
|||||||
|
|
||||||
// Include in main named.conf if not already there
|
// Include in main named.conf if not already there
|
||||||
$mainConf = '/etc/bind/named.conf';
|
$mainConf = '/etc/bind/named.conf';
|
||||||
if (file_exists($mainConf) && !str_contains(file_get_contents($mainConf), 'named.conf.novacpx')) {
|
if (file_exists($mainConf) && !str_contains(file_get_contents($mainConf) ?: '', 'named.conf.novacpx')) {
|
||||||
file_put_contents($mainConf, "\ninclude \"" . self::$namedConf . "\";\n", FILE_APPEND);
|
$line = "\ninclude \"" . self::$namedConf . "\";\n";
|
||||||
|
shell_exec("echo " . escapeshellarg($line) . " | sudo tee -a {$mainConf} > /dev/null 2>&1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function reloadBind(): void {
|
private static function reloadBind(): void {
|
||||||
shell_exec("rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true");
|
shell_exec("sudo rndc reload 2>/dev/null || sudo systemctl reload named 2>/dev/null || sudo systemctl reload bind9 2>/dev/null || true");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function serverIp(): string {
|
private static function serverIp(): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user