Skip to content

Commit

Permalink
add new task to (re)configure mail/ftp services with let's encrypt; r…
Browse files Browse the repository at this point in the history
…efs #1297

Signed-off-by: Michael Kaufmann <[email protected]>
  • Loading branch information
d00p committed Dec 24, 2024
1 parent c2d166c commit 3638dc0
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 49 deletions.
4 changes: 2 additions & 2 deletions actions/admin/settings/131.ssl.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
'dovecot' => 'dovecot (imap/pop3)',
'proftpd' => 'proftpd (ftp)',
],
'save_method' => 'storeSettingField',
'save_method' => 'storeSettingFieldInsertUpdateServicesTask',
'advanced_mode' => true
],
'system_le_renew_hook' => [
Expand All @@ -278,7 +278,7 @@
'type' => 'text',
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
'default' => 'systemctl restart postfix dovecot proftpd',
'save_method' => 'storeSettingField',
'save_method' => 'storeSettingFieldInsertUpdateServicesTask',
'advanced_mode' => true,
'required_otp' => true
],
Expand Down
1 change: 1 addition & 0 deletions lib/Froxlor/Cli/MasterCron.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
Cronjob::inserttask(TaskId::REBUILD_RSPAMD);
Cronjob::inserttask(TaskId::CREATE_QUOTA);
Cronjob::inserttask(TaskId::REBUILD_CRON);
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
$jobs[] = 'tasks';
}
define('CRON_IS_FORCED', 1);
Expand Down
123 changes: 76 additions & 47 deletions lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ class AcmeSh extends FroxlorCron
* run the task
*
* @param bool $internal
* @return number
* @return int
* @throws \Exception
*/
public static function run(bool $internal = false)
{
Expand All @@ -85,6 +86,9 @@ public static function run(bool $internal = false)
if ($issue_froxlor || !empty($issue_domains) || !empty($renew_froxlor) || $renew_domains) {
// insert task to generate certificates and vhost-configs
Cronjob::inserttask(TaskId::REBUILD_VHOST);
if ($renew_froxlor) {
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
}
}
return 0;
}
Expand Down Expand Up @@ -217,6 +221,7 @@ public static function run(bool $internal = false)
* check whether we need to issue a new certificate for froxlor itself
*
* @return boolean
* @throws \Exception
*/
private static function issueFroxlorVhost()
{
Expand Down Expand Up @@ -340,6 +345,7 @@ private static function issueDomains()
* check whether we need to renew-check the certificate for froxlor itself
*
* @return boolean
* @throws \Exception
*/
private static function renewFroxlorVhost()
{
Expand Down Expand Up @@ -539,6 +545,7 @@ private static function runIssueFor($certrows = [])
* @param array $domains
* @param int $domain_id
* @param FroxlorLogger $cronlog
* @throws \Exception
*/
private static function validateDns(array &$domains, $domain_id, &$cronlog)
{
Expand Down Expand Up @@ -619,61 +626,83 @@ private static function runAcmeSh(array $certrow, array $domains, &$cronlog = nu
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Successful exit-code returned - storing certificate");
$cert_stored = self::certToDb($certrow, $cronlog, $acme_result);

if ($cert_stored
&& $renew_hook
&& !empty(trim(Settings::Get('system.le_renew_services') ?? ""))
&& !empty(trim(Settings::Get('system.le_renew_hook') ?? ""))
) {
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Renew-hook is enabled - adjusting configurations");

$certificate_folder = self::getCertificateFolder(strtolower(Settings::Get('system.hostname')));
$fullchain = FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer');
$keyfile = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower(Settings::Get('system.hostname')) . '.key');
$ca_file = FileDir::makeCorrectFile($certificate_folder . '/ca.cer');

if (Settings::IsInList('system.le_renew_services', 'postfix')) {
// "postconf -e" for postfix
FileDir::safe_exec('postconf -e smtpd_tls_cert_file=' . escapeshellarg($fullchain));
FileDir::safe_exec('postconf -e smtpd_tls_key_file=' . escapeshellarg($keyfile));
}
if (Settings::IsInList('system.le_renew_services', 'dovecot')) {
// custom config for dovecot
$dovecot_conf = '/etc/dovecot/conf.d/99-froxlor.ssl.conf'; // @fixme setting?
$ssl_content = <<<EOSSL
if ($cert_stored && $renew_hook) {
self::renewHookConfigs($cronlog);
}
}
}
}

public static function renewHookConfigs($cronlog)
{
if (!empty(trim(Settings::Get('system.le_renew_services') ?? ""))
&& !empty(trim(Settings::Get('system.le_renew_hook') ?? ""))
) {

$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Renew-hook is enabled - adjusting configurations");

$certificate_folder = self::getCertificateFolder(strtolower(Settings::Get('system.hostname')));

if (empty($certificate_folder)) {
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "No certificate folder for '" . Settings::Get('system.hostname') . "' found");
return;
}

$fullchain = FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer');
$keyfile = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower(Settings::Get('system.hostname')) . '.key');
$ca_file = FileDir::makeCorrectFile($certificate_folder . '/ca.cer');

if (!file_exists($fullchain) || !file_exists($keyfile) || !file_exists($ca_file)) {
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "At least one of the required certificate files for '" . Settings::Get('system.hostname') . "' could not be found");
return;
}

$dovecot_conf = '/etc/dovecot/conf.d/99-froxlor.ssl.conf'; // @fixme setting?
if (Settings::IsInList('system.le_renew_services', 'postfix')) {
// "postconf -e" for postfix
FileDir::safe_exec('postconf -e smtpd_tls_cert_file=' . escapeshellarg($fullchain));
FileDir::safe_exec('postconf -e smtpd_tls_key_file=' . escapeshellarg($keyfile));
}
if (Settings::IsInList('system.le_renew_services', 'dovecot')) {
// custom config for dovecot
$ssl_content = <<<EOSSL
# Autogenerated configuration by froxlor.
# Do not manually edit this file as it will be overwritten.
ssl = yes
ssl_cert = <{$fullchain}
ssl_key = <{$keyfile}
EOSSL;
file_put_contents($dovecot_conf, $ssl_content);
}
if (Settings::IsInList('system.le_renew_services', 'proftpd')) {
$proftpd_conf = '/etc/proftpd/tls.conf'; // @fixme setting?
$rval = false;
// ECC certificate or not?
if (strpos($certificate_folder, '_ecc') === false) {
// comment out ECC related settings
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateFile|# TLSECCertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateKeyFile|# TLSECCertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add RSA directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateFile.*|TLSRSACertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateKeyFile.*|TLSRSACertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
} else {
// comment out RSA related settings
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateFile|# TLSRSACertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateKeyFile|# TLSRSACertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add ECC directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateFile.*|TLSECCertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateKeyFile.*|TLSECCertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSCACertificateFile.*|TLSCACertificateFile " . $ca_file . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}
// reload the services
FileDir::safe_exec(Settings::Get('system.le_renew_hook'));
file_put_contents($dovecot_conf, $ssl_content);
} elseif (file_exists($dovecot_conf)) {
// safely remove the autogenerated config file
unlink($dovecot_conf);
}
if (Settings::IsInList('system.le_renew_services', 'proftpd')) {
$proftpd_conf = '/etc/proftpd/tls.conf'; // @fixme setting?
$rval = false;
// ECC certificate or not?
if (strpos($certificate_folder, '_ecc') === false) {
// comment out ECC related settings
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateFile|# TLSECCertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateKeyFile|# TLSECCertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add RSA directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateFile.*|TLSRSACertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateKeyFile.*|TLSRSACertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
} else {
// comment out RSA related settings
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateFile|# TLSRSACertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateKeyFile|# TLSRSACertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add ECC directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateFile.*|TLSECCertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateKeyFile.*|TLSECCertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSCACertificateFile.*|TLSCACertificateFile " . $ca_file . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}

// reload the services
FileDir::safe_exec(Settings::Get('system.le_renew_hook'));
}
}

Expand Down
7 changes: 7 additions & 0 deletions lib/Froxlor/Cron/System/TasksCron.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Froxlor\Cron\FroxlorCron;
use Froxlor\Cron\Http\ConfigIO;
use Froxlor\Cron\Http\HttpConfigBase;
use Froxlor\Cron\Http\LetsEncrypt\AcmeSh;
use Froxlor\Cron\Mail\Rspamd;
use Froxlor\Cron\TaskId;
use Froxlor\Database\Database;
Expand Down Expand Up @@ -125,6 +126,12 @@ public static function run()
*/
FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing Let's Encrypt entries for domain " . $row['data']['domain']);
Domain::doLetsEncryptCleanUp($row['data']['domain']);
} elseif ($row['type'] == TaskId::UPDATE_LE_SERVICES) {
/**
* TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate
*/
FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Updating Let's Encrypt configuration for selected services");
AcmeSh::renewHookConfigs(FroxlorLogger::getInstanceOf());
}
}

Expand Down
5 changes: 5 additions & 0 deletions lib/Froxlor/Cron/TaskId.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ final class TaskId
*/
const DELETE_DOMAIN_SSL = 12;

/**
* TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate
*/
const UPDATE_LE_SERVICES = 13;

/**
* TYPE=20 CUSTUMER DATA DUMP
*/
Expand Down
11 changes: 11 additions & 0 deletions lib/Froxlor/Settings/Store.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ public static function storeSettingFieldInsertAntispamTask($fieldname, $fielddat
return $returnvalue;
}

public static function storeSettingFieldInsertUpdateServicesTask($fieldname, $fielddata, $newfieldvalue)
{
// first save the setting itself
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);

if ($returnvalue !== false) {
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
}
return $returnvalue;
}

public static function storeSettingHostname($fieldname, $fielddata, $newfieldvalue)
{
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);
Expand Down
1 change: 1 addition & 0 deletions lng/de.lng.php
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,7 @@
'CREATE_CUSTOMER_DATADUMP' => 'Daten-Export für Kunde %s',
'DELETE_DOMAIN_PDNS' => 'Lösche Domain %s von PowerDNS Datenbank',
'DELETE_DOMAIN_SSL' => 'Lösche SSL Dateien von Domain %s',
'UPDATE_LE_SERVICES' => 'Aktualisiere Systemdienste für Let\'s Encrypt',
],
'terms' => 'AGB',
'traffic' => [
Expand Down
1 change: 1 addition & 0 deletions lng/en.lng.php
Original file line number Diff line number Diff line change
Expand Up @@ -2326,6 +2326,7 @@
'CREATE_CUSTOMER_DATADUMP' => 'Data export job for customer %s',
'DELETE_DOMAIN_PDNS' => 'Delete domain %s from PowerDNS database',
'DELETE_DOMAIN_SSL' => 'Delete ssl files of domain %s',
'UPDATE_LE_SERVICES' => 'Updating system services for Let\'s Encrypt',
],
'terms' => 'Terms of use',
'traffic' => [
Expand Down

0 comments on commit 3638dc0

Please sign in to comment.