Skip to content

Commit 3da65d0

Browse files
authored
Merge pull request #132 from Icinga/feature/mange-expired-certificates
Introduce `CleanupCommand`
2 parents 798ffe9 + 7587d8f commit 3da65d0

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/* Icinga Web 2 X.509 Module | (c) 2023 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\X509\Clicommands;
6+
7+
use DateTime;
8+
use Exception;
9+
use Icinga\Application\Logger;
10+
use Icinga\Module\X509\CertificateUtils;
11+
use Icinga\Module\X509\Command;
12+
use InvalidArgumentException;
13+
use Throwable;
14+
15+
class CleanupCommand extends Command
16+
{
17+
/**
18+
* Remove targets whose last scan is older than a certain date/time and certificates that are no longer used.
19+
*
20+
* By default, any targets whose last scan is older than 1 month are removed. The last scan information is
21+
* always updated when scanning a target, regardless of whether a successful connection is made or not.
22+
* Therefore, targets that have been decommissioned or are no longer part of a job configuration are removed
23+
* after the specified period. Any certificates that are no longer used are also removed. This can either be
24+
* because the associated target has been removed or because it is presenting a new certificate chain.
25+
*
26+
* USAGE
27+
*
28+
* icingacli x509 cleanup [OPTIONS]
29+
*
30+
* OPTIONS
31+
*
32+
* --since-last-scan=<datetime>
33+
* Clean up targets whose last scan is older than the specified date/time,
34+
* which can also be an English textual datetime description like "2 days".
35+
* Defaults to "1 month".
36+
*
37+
* EXAMPLES
38+
*
39+
* Remove any targets that have not been scanned for at least two months and any certificates that are no longer
40+
* used.
41+
*
42+
* icingacli x509 cleanup --since-last-scan="2 months"
43+
*
44+
*/
45+
public function indexAction()
46+
{
47+
$sinceLastScan = $this->params->get('since-last-scan', '-1 month');
48+
$lastScan = $sinceLastScan;
49+
if ($lastScan[0] !== '-') {
50+
// When the user specified "2 days" as a threshold strtotime() will compute the
51+
// timestamp NOW() + 2 days, but it has to be NOW() + (-2 days)
52+
$lastScan = "-$lastScan";
53+
}
54+
55+
try {
56+
$sinceLastScan = new DateTime($lastScan);
57+
} catch (Exception $_) {
58+
throw new InvalidArgumentException(sprintf(
59+
'The specified last scan time is in an unknown format: %s',
60+
$sinceLastScan
61+
));
62+
}
63+
64+
try {
65+
$conn = $this->getDb();
66+
$query = $conn->delete(
67+
'x509_target',
68+
['last_scan < ?' => $sinceLastScan->format('Uv')]
69+
);
70+
71+
if ($query->rowCount() > 0) {
72+
Logger::info(
73+
'Removed %d targets matching since last scan filter: %s',
74+
$query->rowCount(),
75+
$sinceLastScan->format('Y-m-d H:i:s')
76+
);
77+
}
78+
79+
CertificateUtils::cleanupNoLongerUsedCertificates($conn);
80+
} catch (Throwable $err) {
81+
Logger::error($err);
82+
}
83+
}
84+
}

library/X509/CertificateUtils.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
use Icinga\Module\X509\Model\X509CertificateChain;
1212
use Icinga\Module\X509\Model\X509CertificateSubjectAltName;
1313
use Icinga\Module\X509\Model\X509Dn;
14+
use Icinga\Module\X509\Model\X509Target;
1415
use ipl\Sql\Connection;
1516
use ipl\Sql\Expression;
17+
use ipl\Sql\Select;
1618
use ipl\Stdlib\Filter;
1719

1820
class CertificateUtils
@@ -365,6 +367,49 @@ private static function findOrInsertDn($db, $certInfo, $type)
365367
return $hash;
366368
}
367369

370+
/**
371+
* Remove certificates that are no longer in use
372+
*
373+
* Remove chains that aren't used by any target, certificates that aren't part of any chain, and DNs
374+
* that aren't used anywhere.
375+
*
376+
* @param Connection $conn
377+
*/
378+
public static function cleanupNoLongerUsedCertificates(Connection $conn)
379+
{
380+
$chainQuery = $conn->delete(
381+
'x509_certificate_chain',
382+
['id NOT IN ?' => X509Target::on($conn)->columns('latest_certificate_chain_id')->assembleSelect()]
383+
);
384+
385+
$rows = $chainQuery->rowCount();
386+
if ($rows > 0) {
387+
Logger::info('Removed %d certificate chains that are not used by any targets', $rows);
388+
}
389+
390+
$certsQuery = $conn->delete('x509_certificate', [
391+
'id NOT IN ?' => (new Select())
392+
->from('x509_certificate_chain_link ccl')
393+
->columns(['ccl.certificate_id'])
394+
->distinct(),
395+
'trusted = ?' => 'n',
396+
]);
397+
398+
$rows = $certsQuery->rowCount();
399+
if ($rows > 0) {
400+
Logger::info('Removed %d certificates that are not part of any chains', $rows);
401+
}
402+
403+
$dnQuery = $conn->delete('x509_dn', [
404+
'hash NOT IN ?' => X509Certificate::on($conn)->columns('subject_hash')->assembleSelect()
405+
]);
406+
407+
$rows = $dnQuery->rowCount();
408+
if ($rows > 0) {
409+
Logger::info('Removed %d DNs that are not used anywhere', $rows);
410+
}
411+
}
412+
368413
/**
369414
* Verify certificates
370415
*

0 commit comments

Comments
 (0)