Skip to content

Commit 3638dc0

Browse files
committed
add new task to (re)configure mail/ftp services with let's encrypt; refs #1297
Signed-off-by: Michael Kaufmann <[email protected]>
1 parent c2d166c commit 3638dc0

File tree

8 files changed

+104
-49
lines changed

8 files changed

+104
-49
lines changed

actions/admin/settings/131.ssl.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@
268268
'dovecot' => 'dovecot (imap/pop3)',
269269
'proftpd' => 'proftpd (ftp)',
270270
],
271-
'save_method' => 'storeSettingField',
271+
'save_method' => 'storeSettingFieldInsertUpdateServicesTask',
272272
'advanced_mode' => true
273273
],
274274
'system_le_renew_hook' => [
@@ -278,7 +278,7 @@
278278
'type' => 'text',
279279
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
280280
'default' => 'systemctl restart postfix dovecot proftpd',
281-
'save_method' => 'storeSettingField',
281+
'save_method' => 'storeSettingFieldInsertUpdateServicesTask',
282282
'advanced_mode' => true,
283283
'required_otp' => true
284284
],

lib/Froxlor/Cli/MasterCron.php

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8080
Cronjob::inserttask(TaskId::REBUILD_RSPAMD);
8181
Cronjob::inserttask(TaskId::CREATE_QUOTA);
8282
Cronjob::inserttask(TaskId::REBUILD_CRON);
83+
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
8384
$jobs[] = 'tasks';
8485
}
8586
define('CRON_IS_FORCED', 1);

lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php

+76-47
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ class AcmeSh extends FroxlorCron
6969
* run the task
7070
*
7171
* @param bool $internal
72-
* @return number
72+
* @return int
73+
* @throws \Exception
7374
*/
7475
public static function run(bool $internal = false)
7576
{
@@ -85,6 +86,9 @@ public static function run(bool $internal = false)
8586
if ($issue_froxlor || !empty($issue_domains) || !empty($renew_froxlor) || $renew_domains) {
8687
// insert task to generate certificates and vhost-configs
8788
Cronjob::inserttask(TaskId::REBUILD_VHOST);
89+
if ($renew_froxlor) {
90+
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
91+
}
8892
}
8993
return 0;
9094
}
@@ -217,6 +221,7 @@ public static function run(bool $internal = false)
217221
* check whether we need to issue a new certificate for froxlor itself
218222
*
219223
* @return boolean
224+
* @throws \Exception
220225
*/
221226
private static function issueFroxlorVhost()
222227
{
@@ -340,6 +345,7 @@ private static function issueDomains()
340345
* check whether we need to renew-check the certificate for froxlor itself
341346
*
342347
* @return boolean
348+
* @throws \Exception
343349
*/
344350
private static function renewFroxlorVhost()
345351
{
@@ -539,6 +545,7 @@ private static function runIssueFor($certrows = [])
539545
* @param array $domains
540546
* @param int $domain_id
541547
* @param FroxlorLogger $cronlog
548+
* @throws \Exception
542549
*/
543550
private static function validateDns(array &$domains, $domain_id, &$cronlog)
544551
{
@@ -619,61 +626,83 @@ private static function runAcmeSh(array $certrow, array $domains, &$cronlog = nu
619626
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Successful exit-code returned - storing certificate");
620627
$cert_stored = self::certToDb($certrow, $cronlog, $acme_result);
621628

622-
if ($cert_stored
623-
&& $renew_hook
624-
&& !empty(trim(Settings::Get('system.le_renew_services') ?? ""))
625-
&& !empty(trim(Settings::Get('system.le_renew_hook') ?? ""))
626-
) {
627-
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Renew-hook is enabled - adjusting configurations");
628-
629-
$certificate_folder = self::getCertificateFolder(strtolower(Settings::Get('system.hostname')));
630-
$fullchain = FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer');
631-
$keyfile = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower(Settings::Get('system.hostname')) . '.key');
632-
$ca_file = FileDir::makeCorrectFile($certificate_folder . '/ca.cer');
633-
634-
if (Settings::IsInList('system.le_renew_services', 'postfix')) {
635-
// "postconf -e" for postfix
636-
FileDir::safe_exec('postconf -e smtpd_tls_cert_file=' . escapeshellarg($fullchain));
637-
FileDir::safe_exec('postconf -e smtpd_tls_key_file=' . escapeshellarg($keyfile));
638-
}
639-
if (Settings::IsInList('system.le_renew_services', 'dovecot')) {
640-
// custom config for dovecot
641-
$dovecot_conf = '/etc/dovecot/conf.d/99-froxlor.ssl.conf'; // @fixme setting?
642-
$ssl_content = <<<EOSSL
629+
if ($cert_stored && $renew_hook) {
630+
self::renewHookConfigs($cronlog);
631+
}
632+
}
633+
}
634+
}
635+
636+
public static function renewHookConfigs($cronlog)
637+
{
638+
if (!empty(trim(Settings::Get('system.le_renew_services') ?? ""))
639+
&& !empty(trim(Settings::Get('system.le_renew_hook') ?? ""))
640+
) {
641+
642+
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Renew-hook is enabled - adjusting configurations");
643+
644+
$certificate_folder = self::getCertificateFolder(strtolower(Settings::Get('system.hostname')));
645+
646+
if (empty($certificate_folder)) {
647+
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "No certificate folder for '" . Settings::Get('system.hostname') . "' found");
648+
return;
649+
}
650+
651+
$fullchain = FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer');
652+
$keyfile = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower(Settings::Get('system.hostname')) . '.key');
653+
$ca_file = FileDir::makeCorrectFile($certificate_folder . '/ca.cer');
654+
655+
if (!file_exists($fullchain) || !file_exists($keyfile) || !file_exists($ca_file)) {
656+
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "At least one of the required certificate files for '" . Settings::Get('system.hostname') . "' could not be found");
657+
return;
658+
}
659+
660+
$dovecot_conf = '/etc/dovecot/conf.d/99-froxlor.ssl.conf'; // @fixme setting?
661+
662+
if (Settings::IsInList('system.le_renew_services', 'postfix')) {
663+
// "postconf -e" for postfix
664+
FileDir::safe_exec('postconf -e smtpd_tls_cert_file=' . escapeshellarg($fullchain));
665+
FileDir::safe_exec('postconf -e smtpd_tls_key_file=' . escapeshellarg($keyfile));
666+
}
667+
if (Settings::IsInList('system.le_renew_services', 'dovecot')) {
668+
// custom config for dovecot
669+
$ssl_content = <<<EOSSL
643670
# Autogenerated configuration by froxlor.
644671
# Do not manually edit this file as it will be overwritten.
645672
646673
ssl = yes
647674
ssl_cert = <{$fullchain}
648675
ssl_key = <{$keyfile}
649676
EOSSL;
650-
file_put_contents($dovecot_conf, $ssl_content);
651-
}
652-
if (Settings::IsInList('system.le_renew_services', 'proftpd')) {
653-
$proftpd_conf = '/etc/proftpd/tls.conf'; // @fixme setting?
654-
$rval = false;
655-
// ECC certificate or not?
656-
if (strpos($certificate_folder, '_ecc') === false) {
657-
// comment out ECC related settings
658-
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateFile|# TLSECCertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
659-
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateKeyFile|# TLSECCertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
660-
// add RSA directives
661-
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateFile.*|TLSRSACertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
662-
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateKeyFile.*|TLSRSACertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
663-
} else {
664-
// comment out RSA related settings
665-
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateFile|# TLSRSACertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
666-
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateKeyFile|# TLSRSACertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
667-
// add ECC directives
668-
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateFile.*|TLSECCertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
669-
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateKeyFile.*|TLSECCertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
670-
}
671-
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSCACertificateFile.*|TLSCACertificateFile " . $ca_file . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
672-
}
673-
// reload the services
674-
FileDir::safe_exec(Settings::Get('system.le_renew_hook'));
677+
file_put_contents($dovecot_conf, $ssl_content);
678+
} elseif (file_exists($dovecot_conf)) {
679+
// safely remove the autogenerated config file
680+
unlink($dovecot_conf);
681+
}
682+
if (Settings::IsInList('system.le_renew_services', 'proftpd')) {
683+
$proftpd_conf = '/etc/proftpd/tls.conf'; // @fixme setting?
684+
$rval = false;
685+
// ECC certificate or not?
686+
if (strpos($certificate_folder, '_ecc') === false) {
687+
// comment out ECC related settings
688+
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateFile|# TLSECCertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
689+
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateKeyFile|# TLSECCertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
690+
// add RSA directives
691+
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateFile.*|TLSRSACertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
692+
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateKeyFile.*|TLSRSACertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
693+
} else {
694+
// comment out RSA related settings
695+
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateFile|# TLSRSACertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
696+
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateKeyFile|# TLSRSACertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
697+
// add ECC directives
698+
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateFile.*|TLSECCertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
699+
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateKeyFile.*|TLSECCertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
675700
}
701+
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSCACertificateFile.*|TLSCACertificateFile " . $ca_file . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
676702
}
703+
704+
// reload the services
705+
FileDir::safe_exec(Settings::Get('system.le_renew_hook'));
677706
}
678707
}
679708

lib/Froxlor/Cron/System/TasksCron.php

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Froxlor\Cron\FroxlorCron;
3030
use Froxlor\Cron\Http\ConfigIO;
3131
use Froxlor\Cron\Http\HttpConfigBase;
32+
use Froxlor\Cron\Http\LetsEncrypt\AcmeSh;
3233
use Froxlor\Cron\Mail\Rspamd;
3334
use Froxlor\Cron\TaskId;
3435
use Froxlor\Database\Database;
@@ -125,6 +126,12 @@ public static function run()
125126
*/
126127
FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing Let's Encrypt entries for domain " . $row['data']['domain']);
127128
Domain::doLetsEncryptCleanUp($row['data']['domain']);
129+
} elseif ($row['type'] == TaskId::UPDATE_LE_SERVICES) {
130+
/**
131+
* TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate
132+
*/
133+
FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Updating Let's Encrypt configuration for selected services");
134+
AcmeSh::renewHookConfigs(FroxlorLogger::getInstanceOf());
128135
}
129136
}
130137

lib/Froxlor/Cron/TaskId.php

+5
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ final class TaskId
8787
*/
8888
const DELETE_DOMAIN_SSL = 12;
8989

90+
/**
91+
* TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate
92+
*/
93+
const UPDATE_LE_SERVICES = 13;
94+
9095
/**
9196
* TYPE=20 CUSTUMER DATA DUMP
9297
*/

lib/Froxlor/Settings/Store.php

+11
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,17 @@ public static function storeSettingFieldInsertAntispamTask($fieldname, $fielddat
237237
return $returnvalue;
238238
}
239239

240+
public static function storeSettingFieldInsertUpdateServicesTask($fieldname, $fielddata, $newfieldvalue)
241+
{
242+
// first save the setting itself
243+
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);
244+
245+
if ($returnvalue !== false) {
246+
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
247+
}
248+
return $returnvalue;
249+
}
250+
240251
public static function storeSettingHostname($fieldname, $fielddata, $newfieldvalue)
241252
{
242253
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);

lng/de.lng.php

+1
Original file line numberDiff line numberDiff line change
@@ -2192,6 +2192,7 @@
21922192
'CREATE_CUSTOMER_DATADUMP' => 'Daten-Export für Kunde %s',
21932193
'DELETE_DOMAIN_PDNS' => 'Lösche Domain %s von PowerDNS Datenbank',
21942194
'DELETE_DOMAIN_SSL' => 'Lösche SSL Dateien von Domain %s',
2195+
'UPDATE_LE_SERVICES' => 'Aktualisiere Systemdienste für Let\'s Encrypt',
21952196
],
21962197
'terms' => 'AGB',
21972198
'traffic' => [

lng/en.lng.php

+1
Original file line numberDiff line numberDiff line change
@@ -2326,6 +2326,7 @@
23262326
'CREATE_CUSTOMER_DATADUMP' => 'Data export job for customer %s',
23272327
'DELETE_DOMAIN_PDNS' => 'Delete domain %s from PowerDNS database',
23282328
'DELETE_DOMAIN_SSL' => 'Delete ssl files of domain %s',
2329+
'UPDATE_LE_SERVICES' => 'Updating system services for Let\'s Encrypt',
23292330
],
23302331
'terms' => 'Terms of use',
23312332
'traffic' => [

0 commit comments

Comments
 (0)