1
0
Fork 0
mirror of synced 2024-06-11 07:14:51 +12:00

PR review changes

This commit is contained in:
Matej Bačo 2022-04-11 07:56:58 +00:00
parent 752079c874
commit 50412c7e80
4 changed files with 64 additions and 69 deletions

View file

@ -105,7 +105,7 @@ $cli
$certificates = $dbForConsole->find('certificates', [
new Query('attempts', Query::TYPE_LESSER, [5]), // Maximum 5 attempts
new Query('renewDate', Query::TYPE_LESSEREQUAL, [\time()]) // includes 60 days cooldown (we have 30 days to renew)
], 300); // Limit 300 comes from LetsEncrypt (orders per 3 hours)
], 200); // Limit 200 comes from LetsEncrypt (300 orders per 3 hours, keeping some for new domains)
if(\count($certificates) > 0) {
Console::info("[{$time}] Found " . \count($certificates) . " certificates for renewal, scheduling jobs.");
@ -134,7 +134,7 @@ $cli
[$database, $returnDatabase] = getDatabase($register, '_console');
$time = date('d-m-Y H:i:s', time());
Console::info("[{$time}] Notifying deletes workers every {$interval} seconds");
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
notifyDeleteExecutionLogs($executionLogsRetention);
notifyDeleteAbuseLogs($abuseLogsRetention);
notifyDeleteAuditLogs($auditLogRetention);

View file

@ -2,24 +2,35 @@
global $cli;
use Appwrite\Event\Event;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Validator\Hostname;
$cli
->task('ssl')
->desc('Validate server certificates')
->action(function () {
$domain = App::getEnv('_APP_DOMAIN', '');
->param('domain', App::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
->action(function ($domain) {
// HTTTP ping to check if domain is Appwrite server
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, 'http://appwrite/manifest.json');
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, 5);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
\curl_exec($ch);
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = \curl_error($ch);
\curl_close($ch);
// TODO: Instead of waiting, let's ping Traefik. If responds, we can schedule instantly
// TODO: Add support for argument (domain)
if($statusCode < 100) {
return Console::error('Appwrite connection refused with message: ' . $error);
}
Console::log('Issue a TLS certificate for master domain ('.$domain.') in 2 seconds.
Make sure your domain points to your server or restart to try again.');
Console::success('Schedule a job to issue a TLS certificate for domain:' . $domain);
// Const for types not available here
ResqueScheduler::enqueueAt(\time() + 2, 'v1-certificates', 'CertificatesV1', [
// Scheduje a job
Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [
'domain' => $domain,
'skipRenewCheck' => true // TODO: Discuss this behabiour. true? false? parameter? How do we document it?
]);
});

View file

@ -34,21 +34,38 @@ class CertificatesV1 extends Worker
$certificate = new Document();
/**
* 1. Read arguments and validate domain
* 2. Get main domain
* 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain)
* 4. Validate renew date with certificate file, unless requested to skip by parameter
* 5. Validate security email. Cannot be empty, required by LetsEncrypt
* 6. Issue a certificate using certbot CLI
* 7. Update 'log' attribute on certificate document with Certbot message
* 8. Create storage folder for certificate, if not ready already
* 9. Move certificates from Certbot location to our Storage
* 10. Create/Update our Storage with new Traefik config with new certificate paths
* 11. Read certificate file and update 'renewDate' on certificate document
* 12. Update 'issueDate' and 'attempts' on certificate
*
* If at any point unexpected error occurs, program stops without applying changes to document, and error is thrown into worker
*
* If code stops with expected error:
* 1. 'log' attribute on document is updated with error message
* 2. 'attempts' amount is increased
* 3. Console log is shown
* 4. Email is sent to security email
*
* Unless unexpected error occurs, at the end, we:
* 1. Update 'updated' attribute on document
* 2. Save document to database
* 3. Update all domains documents with current certificate ID
*
* Note: Renewals are checked and scheduled from maintenence worker
*/
try {
/**
* TODO: Update
* 1. Get new domain document - DONE
* 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE
* 2. Check if a certificate already exists - DONE
* 3. Check if certificate is about to expire, if not - skip it
* 3.1. Create / renew certificate
* 3.2. Update loadblancer
* 3.3. Update database (domains, change date, expiry)
* 3.4. Set retry on failure
* 3.5. Schedule to renew certificate in 60 days
*/
// Get attributes
$domain = $this->args['domain']; // String of domain (hostname)
$domain = new Domain((!empty($domain)) ? $domain : '');
@ -108,7 +125,7 @@ class CertificatesV1 extends Worker
try {
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'];
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
throw new Exception('Invalid expiry date.');
@ -146,7 +163,7 @@ class CertificatesV1 extends Worker
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new ExceptionCertificate('Failed to issue a certificate with message: ' . $stderr, true);
throw new ExceptionCertificate('Failed to issue a certificate with message: ' . $stderr);
}
// Command succeeded, store all data into document
@ -161,25 +178,25 @@ class CertificatesV1 extends Worker
$path = APP_STORAGE_CERTIFICATES . '/' . $domain->get();
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new ExceptionCertificate('Failed to create path for certificate.', true);
throw new ExceptionCertificate('Failed to create path for certificate.');
}
}
// Move generated files from certbot into our storage
if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) {
throw new ExceptionCertificate('Failed to rename certificate cert.pem: '.\json_encode($stdout), true);
throw new ExceptionCertificate('Failed to rename certificate cert.pem: '.\json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/chain.pem')) {
throw new ExceptionCertificate('Failed to rename certificate chain.pem: ' . \json_encode($stdout), true);
throw new ExceptionCertificate('Failed to rename certificate chain.pem: ' . \json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/fullchain.pem')) {
throw new ExceptionCertificate('Failed to rename certificate fullchain.pem: ' . \json_encode($stdout), true);
throw new ExceptionCertificate('Failed to rename certificate fullchain.pem: ' . \json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/privkey.pem')) {
throw new ExceptionCertificate('Failed to rename certificate privkey.pem: ' . \json_encode($stdout), true);
throw new ExceptionCertificate('Failed to rename certificate privkey.pem: ' . \json_encode($stdout));
}
// This multi-line syntax helps IDE
@ -191,14 +208,14 @@ class CertificatesV1 extends Worker
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain->get() . '.yml', $config)) {
throw new ExceptionCertificate('Failed to save Traefik configuration.', true);
throw new ExceptionCertificate('Failed to save Traefik configuration.');
}
// Read new renew date from cert file
// TODO: This might not be required, we could calculate it. But this feels safer
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'];
$validTo = $certData['validTo_time_t'] ?? 0;
$expiryInAdvance = (60*60*24*30);
$certificate->setAttribute('renewDate', $validTo - $expiryInAdvance);
@ -218,10 +235,8 @@ class CertificatesV1 extends Worker
$attempt = $certificate->getAttribute('attempts', 0);
$attempt++;
// Save increased attempts count if requested by exception
if($e->getIncrementAttempts()) {
$certificate->setAttribute('attempts', $attempt);
}
// Save increased attempts count
$certificate->setAttribute('attempts', $attempt);
Console::warning('Cannot renew domain (' . $domain->get() . ') on attempt no. ' . $attempt . ' certificate: ' . $e->getMessage());
} finally {

View file

@ -4,35 +4,4 @@ namespace Appwrite\Exception;
class Certificate extends \Exception
{
private $incrementAttempts = false;
// Only set incrementAttempts to TRUE if exception occured AFTER certbot command execution
public function __construct(string $message, bool $incrementAttempts = false)
{
$this->incrementAttempts = $incrementAttempts;
parent::__construct($message);
}
/**
* Get value if attempts should be incremented.
*
* @return boolean
*/
public function getIncrementAttempts(): bool
{
return $this->incrementAttempts;
}
/**
* Set if attempts should be incremented
*
* @param boolean $value
*
* @return void
*/
public function setIncrementAttempts(bool $value): void
{
$this->incrementAttempts = $value;
}
}