├── .mailmap ├── AUTHORS ├── LICENSE ├── README.md ├── application ├── clicommands │ ├── CheckCommand.php │ ├── CleanupCommand.php │ ├── ImportCommand.php │ ├── JobsCommand.php │ ├── MigrateCommand.php │ ├── ScanCommand.php │ └── VerifyCommand.php ├── controllers │ ├── CertificateController.php │ ├── CertificatesController.php │ ├── ChainController.php │ ├── ConfigController.php │ ├── DashboardController.php │ ├── JobController.php │ ├── JobsController.php │ ├── SniController.php │ └── UsageController.php ├── forms │ ├── Config │ │ ├── BackendConfigForm.php │ │ └── SniConfigForm.php │ └── Jobs │ │ ├── JobConfigForm.php │ │ └── ScheduleForm.php └── views │ └── scripts │ ├── certificate │ └── index.phtml │ ├── chain │ └── index.phtml │ ├── config │ └── backend.phtml │ ├── dashboard │ └── index.phtml │ ├── missing-resource.phtml │ ├── simple-form.phtml │ └── sni │ └── index.phtml ├── config └── systemd │ └── icinga-x509.service ├── configuration.php ├── doc ├── 01-About.md ├── 02-Installation.md ├── 02-Installation.md.d │ └── From-Source.md ├── 03-Configuration.md ├── 04-Scanning.md ├── 10-Monitoring.md ├── 11-Housekeeping.md ├── 80-Upgrading.md └── res │ ├── check-host-perf-data.png │ ├── host-check-multiple-services.png │ ├── host-check-single-service.png │ ├── host-template-fields.png │ ├── hosts-import-result.png │ ├── hosts-import-source.png │ ├── multiple-services-result.png │ ├── new-host-template.png │ ├── new-service-template.png │ ├── ports-property-modifier.png │ ├── service-template-fields.png │ ├── single-service-result.png │ ├── sync-rule-properties.png │ ├── weekly-schedules.png │ ├── x509-certificates.png │ ├── x509-dashboard.png │ └── x509-usage.png ├── library └── X509 │ ├── CertificateDetails.php │ ├── CertificateUtils.php │ ├── CertificatesTable.php │ ├── ChainDetails.php │ ├── ColorScheme.php │ ├── Command.php │ ├── Common │ ├── Database.php │ ├── JobOptions.php │ ├── JobUtils.php │ └── Links.php │ ├── Controller.php │ ├── DataTable.php │ ├── DbTool.php │ ├── Donut.php │ ├── ExpirationWidget.php │ ├── FilterAdapter.php │ ├── Hook │ └── SniHook.php │ ├── Job.php │ ├── Model │ ├── Behavior │ │ ├── DERBase64.php │ │ ├── ExpressionInjector.php │ │ └── Ip.php │ ├── Schema.php │ ├── X509Certificate.php │ ├── X509CertificateChain.php │ ├── X509CertificateChainLink.php │ ├── X509CertificateSubjectAltName.php │ ├── X509Dn.php │ ├── X509Job.php │ ├── X509JobRun.php │ ├── X509Schedule.php │ └── X509Target.php │ ├── ProvidedHook │ ├── DbMigration.php │ ├── HostsImportSource.php │ ├── ServicesImportSource.php │ └── X509ImportSource.php │ ├── React │ └── StreamOptsCaptureConnector.php │ ├── Schedule.php │ ├── SniIniRepository.php │ ├── Table.php │ ├── UsageTable.php │ ├── Web │ └── Control │ │ └── SearchBar │ │ └── ObjectSuggestions.php │ └── Widget │ ├── JobDetails.php │ ├── Jobs.php │ └── Schedules.php ├── module.info ├── phpstan-baseline-7x.neon ├── phpstan-baseline-8x.neon ├── phpstan-baseline-by-php-version.php ├── phpstan-baseline-common.neon ├── phpstan.neon ├── phpunit.xml ├── public └── css │ └── module.less ├── run.php ├── schema ├── mysql-upgrades │ ├── 1.0.0.sql │ ├── 1.1.0.sql │ ├── 1.2.0.sql │ └── 1.3.0.sql ├── mysql.schema.sql ├── pgsql-upgrades │ └── 1.3.0.sql └── pgsql.schema.sql └── test └── php ├── Lib └── TestModel.php └── library └── X509 ├── Common └── JobUtilsTest.php ├── JobTest.php └── Model └── Behavior ├── DERBase64Test.php ├── ExpressionInjectorTest.php └── IpTest.php /.mailmap: -------------------------------------------------------------------------------- 1 | Alexander A. Klimov 2 | Eric Lippmann 3 | Florian Strohmaier 4 | Gunnar Beutner 5 | Jan Wagner 6 | Jens Meißner 7 | Johannes Meyer 8 | Michael Friedrich 9 | Ravi Kumar Kempapura Srinivasa <33730024+raviks789@users.noreply.github.com> 10 | Robert Rettig 11 | Sukhwinder Dhillon 12 | Timm Ortloff 13 | Yonas Habteab 14 | lx 15 | moreamazingnick 16 | nmartinii <51709615+nmartinii@users.noreply.github.com> 17 | pgress 18 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Alexander A. Klimov 2 | Eric Lippmann 3 | Florian Strohmaier 4 | Gunnar Beutner 5 | Jan Wagner 6 | Jens Meißner 7 | Johannes Meyer 8 | Michael Friedrich 9 | Ravi Kumar Kempapura Srinivasa 10 | Robert Rettig 11 | Sukhwinder Dhillon 12 | Timm Ortloff 13 | Yonas Habteab 14 | lx 15 | moreamazingnick 16 | nmartinii <51709615+nmartinii@users.noreply.github.com> 17 | pgress 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Icinga Certificate Monitoring 2 | 3 | [![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/) 4 | ![Build Status](https://github.com/icinga/icingaweb2-module-x509/workflows/PHP%20Tests/badge.svg?branch=master) 5 | [![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-x509.svg)](https://github.com/Icinga/icingaweb2-module-x509) 6 | 7 | ![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png) 8 | 9 | The certificate monitoring module for Icinga keeps track of certificates as they are deployed in a network environment. 10 | It does this by scanning networks for TLS services and collects whatever certificates it finds along the way. 11 | The certificates are verified using its own trust store. 12 | 13 | The module’s web frontend can be used to view scan results, allowing you to drill down into detailed information 14 | about any discovered certificate of your landscape: 15 | 16 | ![X.509 Usage](doc/res/x509-usage.png "X.509 Usage") 17 | 18 | ![X.509 Certificates](doc/res/x509-certificates.png "X.509 Certificates") 19 | 20 | At a glance you see which CAs have issued your certificates and key counters of your environment: 21 | 22 | ![X.509 Dashboard](doc/res/x509-dashboard.png "X.509 Dashboard") 23 | 24 | ## Documentation 25 | 26 | * [Installation](doc/02-Installation.md) 27 | * [Configuration](doc/03-Configuration.md) 28 | * [Monitoring](doc/10-Monitoring.md) 29 | -------------------------------------------------------------------------------- /application/clicommands/CleanupCommand.php: -------------------------------------------------------------------------------- 1 | 38 | * Clean up targets whose last scan is older than the specified date/time, 39 | * which can also be an English textual datetime description like "2 days". 40 | * Defaults to "1 month". 41 | * 42 | * EXAMPLES 43 | * 44 | * Remove any targets that have not been scanned for at least two months and any certificates that are no longer 45 | * used. 46 | * 47 | * icingacli x509 cleanup --since-last-scan="2 months" 48 | * 49 | */ 50 | public function indexAction() 51 | { 52 | /** @var string $sinceLastScan */ 53 | $sinceLastScan = $this->params->get('since-last-scan', '-1 month'); 54 | $lastScan = $sinceLastScan; 55 | if ($lastScan[0] !== '-') { 56 | // When the user specified "2 days" as a threshold strtotime() will compute the 57 | // timestamp NOW() + 2 days, but it has to be NOW() + (-2 days) 58 | $lastScan = "-$lastScan"; 59 | } 60 | 61 | try { 62 | $sinceLastScan = new DateTime($lastScan); 63 | } catch (Exception $_) { 64 | throw new InvalidArgumentException(sprintf( 65 | 'The specified last scan time is in an unknown format: %s', 66 | $sinceLastScan 67 | )); 68 | } 69 | 70 | try { 71 | $conn = Database::get(); 72 | $query = $conn->delete( 73 | 'x509_target', 74 | ['last_scan < ?' => $sinceLastScan->format('Uv')] 75 | ); 76 | 77 | if ($query->rowCount() > 0) { 78 | Logger::info( 79 | 'Removed %d targets matching since last scan filter: %s', 80 | $query->rowCount(), 81 | $sinceLastScan->format('Y-m-d H:i:s') 82 | ); 83 | } 84 | 85 | $query = $conn->delete('x509_job_run', ['start_time < ?' => $sinceLastScan->getTimestamp() * 1000]); 86 | if ($query->rowCount() > 0) { 87 | Logger::info('Removed %d jobs activities', $query->rowCount()); 88 | } 89 | 90 | CertificateUtils::cleanupNoLongerUsedCertificates($conn); 91 | } catch (Throwable $err) { 92 | Logger::error($err); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /application/clicommands/ImportCommand.php: -------------------------------------------------------------------------------- 1 | 22 | * 23 | * EXAMPLES: 24 | * 25 | * icingacli x509 import --file /etc/ssl/certs/ca-bundle.crt 26 | */ 27 | public function indexAction() 28 | { 29 | $file = $this->params->getRequired('file'); 30 | 31 | if (! file_exists($file)) { 32 | Logger::warning('The specified certificate file does not exist.'); 33 | exit(1); 34 | } 35 | 36 | $bundle = CertificateUtils::parseBundle($file); 37 | 38 | $count = 0; 39 | 40 | Database::get()->transaction(function (Connection $db) use ($bundle, &$count) { 41 | foreach ($bundle as $data) { 42 | $cert = openssl_x509_read($data); 43 | 44 | list($id, $_) = CertificateUtils::findOrInsertCert($db, $cert); 45 | 46 | $db->update( 47 | 'x509_certificate', 48 | [ 49 | 'trusted' => 'y', 50 | 'mtime' => new Expression('UNIX_TIMESTAMP() * 1000') 51 | ], 52 | ['id = ?' => $id] 53 | ); 54 | 55 | $count++; 56 | } 57 | }); 58 | 59 | printf("Processed %d X.509 certificate%s.\n", $count, $count !== 1 ? 's' : ''); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /application/clicommands/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | 31 | * 32 | * OPTIONS 33 | * 34 | * --author= 35 | * An Icinga Web 2 user used to mark as an author for all the migrated jobs. 36 | */ 37 | public function jobsAction(): void 38 | { 39 | /** @var string $author */ 40 | $author = $this->params->getRequired('author'); 41 | /** @var User $user */ 42 | $user = Auth::getInstance()->getUser(); 43 | $user->setUsername($author); 44 | 45 | $this->migrateJobs(); 46 | 47 | Logger::info('Successfully applied all pending migrations'); 48 | } 49 | 50 | protected function migrateJobs(): void 51 | { 52 | $repo = new class () extends IniRepository { 53 | /** @var array> */ 54 | protected $queryColumns = [ 55 | 'jobs' => ['name', 'cidrs', 'ports', 'exclude_targets', 'schedule', 'frequencyType'] 56 | ]; 57 | 58 | /** @var array> */ 59 | protected $configs = [ 60 | 'jobs' => [ 61 | 'module' => 'x509', 62 | 'name' => 'jobs', 63 | 'keyColumn' => 'name' 64 | ] 65 | ]; 66 | }; 67 | 68 | $conn = Database::get(); 69 | $conn->transaction(function (Connection $conn) use ($repo) { 70 | /** @var User $user */ 71 | $user = Auth::getInstance()->getUser(); 72 | /** @var stdClass $data */ 73 | foreach ($repo->select() as $data) { 74 | $config = []; 75 | if (! isset($data->frequencyType) && ! empty($data->schedule)) { 76 | $frequency = new Cron($data->schedule); 77 | $config = [ 78 | 'type' => get_php_type($frequency), 79 | 'frequency' => Json::encode($frequency) 80 | ]; 81 | } elseif (! empty($data->schedule)) { 82 | $config = [ 83 | 'type' => $data->frequencyType, 84 | 'frequency' => $data->schedule // Is already json encoded 85 | ]; 86 | } 87 | 88 | $excludes = $data->exclude_targets; 89 | if (empty($excludes)) { 90 | $excludes = new Expression('NULL'); 91 | } 92 | 93 | $conn->insert('x509_job', [ 94 | 'name' => $data->name, 95 | 'author' => $user->getUsername(), 96 | 'cidrs' => $data->cidrs, 97 | 'ports' => $data->ports, 98 | 'exclude_targets' => $excludes, 99 | 'ctime' => (new DateTime())->getTimestamp() * 1000, 100 | 'mtime' => (new DateTime())->getTimestamp() * 1000 101 | ]); 102 | 103 | $jobId = (int) $conn->lastInsertId(); 104 | if (! empty($config)) { 105 | $config['rescan'] = 'n'; 106 | $config['full_scan'] = 'n'; 107 | $config['since_last_scan'] = Job::DEFAULT_SINCE_LAST_SCAN; 108 | 109 | $conn->insert('x509_schedule', [ 110 | 'job_id' => $jobId, 111 | 'name' => $data->name . ' Schedule', 112 | 'author' => $user->getUsername(), 113 | 'config' => Json::encode($config), 114 | 'ctime' => (new DateTime())->getTimestamp() * 1000, 115 | 'mtime' => (new DateTime())->getTimestamp() * 1000, 116 | ]); 117 | } 118 | } 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /application/clicommands/VerifyCommand.php: -------------------------------------------------------------------------------- 1 | addTitleTab($this->translate('X.509 Certificate')); 19 | $this->getTabs()->disableLegacyExtensions(); 20 | 21 | $certId = $this->params->getRequired('cert'); 22 | 23 | try { 24 | $conn = Database::get(); 25 | } catch (ConfigurationError $_) { 26 | $this->render('missing-resource', null, true); 27 | 28 | return; 29 | } 30 | 31 | /** @var ?X509Certificate $cert */ 32 | $cert = X509Certificate::on($conn) 33 | ->filter(Filter::equal('id', $certId)) 34 | ->first(); 35 | 36 | if (! $cert) { 37 | $this->httpNotFound($this->translate('Certificate not found.')); 38 | } 39 | 40 | $this->view->certificateDetails = (new CertificateDetails()) 41 | ->setCert($cert); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /application/controllers/CertificatesController.php: -------------------------------------------------------------------------------- 1 | addTitleTab($this->translate('Certificates')); 22 | $this->getTabs()->enableDataExports(); 23 | 24 | try { 25 | $conn = Database::get(); 26 | } catch (ConfigurationError $_) { 27 | $this->render('missing-resource', null, true); 28 | 29 | return; 30 | } 31 | 32 | $certificates = X509Certificate::on($conn); 33 | 34 | $sortColumns = [ 35 | 'subject' => $this->translate('Certificate'), 36 | 'issuer' => $this->translate('Issuer'), 37 | 'version' => $this->translate('Version'), 38 | 'self_signed' => $this->translate('Is Self-Signed'), 39 | 'ca' => $this->translate('Is Certificate Authority'), 40 | 'trusted' => $this->translate('Is Trusted'), 41 | 'pubkey_algo' => $this->translate('Public Key Algorithm'), 42 | 'pubkey_bits' => $this->translate('Public Key Strength'), 43 | 'signature_algo' => $this->translate('Signature Algorithm'), 44 | 'signature_hash_algo' => $this->translate('Signature Hash Algorithm'), 45 | 'valid_from' => $this->translate('Valid From'), 46 | 'valid_to' => $this->translate('Valid To'), 47 | 'duration' => $this->translate('Duration') 48 | ]; 49 | 50 | $limitControl = $this->createLimitControl(); 51 | $paginator = $this->createPaginationControl($certificates); 52 | $sortControl = $this->createSortControl($certificates, $sortColumns); 53 | 54 | $searchBar = $this->createSearchBar($certificates, [ 55 | $limitControl->getLimitParam(), 56 | $sortControl->getSortParam() 57 | ]); 58 | 59 | if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { 60 | if ($searchBar->hasBeenSubmitted()) { 61 | $filter = $this->getFilter(); 62 | } else { 63 | $this->addControl($searchBar); 64 | $this->sendMultipartUpdate(); 65 | 66 | return; 67 | } 68 | } else { 69 | $filter = $searchBar->getFilter(); 70 | } 71 | 72 | $certificates->peekAhead($this->view->compact); 73 | 74 | $certificates->filter($filter); 75 | 76 | $this->addControl($paginator); 77 | $this->addControl($sortControl); 78 | $this->addControl($limitControl); 79 | $this->addControl($searchBar); 80 | 81 | $this->handleFormatRequest($certificates, function (Query $certificates) { 82 | /** @var X509Certificate $cert */ 83 | foreach ($certificates as $cert) { 84 | $cert->valid_from = $cert->valid_from->format('l F jS, Y H:i:s e'); 85 | $cert->valid_to = $cert->valid_to->format('l F jS, Y H:i:s e'); 86 | 87 | yield array_intersect_key(iterator_to_array($cert), array_flip($cert->getExportableColumns())); 88 | } 89 | }); 90 | 91 | $this->addContent((new CertificatesTable())->setData($certificates)); 92 | 93 | if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { 94 | $this->sendMultipartUpdate(); // Updates the browser search bar 95 | } 96 | } 97 | 98 | public function completeAction() 99 | { 100 | $this->getDocument()->add( 101 | (new ObjectSuggestions()) 102 | ->setModel(X509Certificate::class) 103 | ->forRequest($this->getServerRequest()) 104 | ); 105 | } 106 | 107 | public function searchEditorAction() 108 | { 109 | $editor = $this->createSearchEditor(X509Certificate::on(Database::get()), [ 110 | LimitControl::DEFAULT_LIMIT_PARAM, 111 | SortControl::DEFAULT_SORT_PARAM 112 | ]); 113 | 114 | $this->getDocument()->add($editor); 115 | $this->setTitle(t('Adjust Filter')); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /application/controllers/ChainController.php: -------------------------------------------------------------------------------- 1 | addTitleTab($this->translate('X.509 Certificate Chain')); 22 | $this->getTabs()->disableLegacyExtensions(); 23 | 24 | $id = $this->params->getRequired('id'); 25 | 26 | try { 27 | $conn = Database::get(); 28 | } catch (ConfigurationError $_) { 29 | $this->render('missing-resource', null, true); 30 | return; 31 | } 32 | 33 | /** @var ?X509CertificateChain $chain */ 34 | $chain = X509CertificateChain::on($conn) 35 | ->with(['target']) 36 | ->filter(Filter::equal('id', $id)) 37 | ->first(); 38 | 39 | if (! $chain) { 40 | $this->httpNotFound($this->translate('Certificate not found.')); 41 | } 42 | 43 | $chainInfo = Html::tag('div'); 44 | $chainInfo->add(Html::tag('dl', [ 45 | Html::tag('dt', $this->translate('Host')), 46 | Html::tag('dd', $chain->target->hostname), 47 | Html::tag('dt', $this->translate('IP')), 48 | Html::tag('dd', $chain->target->ip), 49 | Html::tag('dt', $this->translate('Port')), 50 | Html::tag('dd', $chain->target->port) 51 | ])); 52 | 53 | $valid = Html::tag('div', ['class' => 'cert-chain']); 54 | 55 | if ($chain['valid']) { 56 | $valid->getAttributes()->add('class', '-valid'); 57 | $valid->add(Html::tag('p', $this->translate('Certificate chain is valid.'))); 58 | } else { 59 | $valid->getAttributes()->add('class', '-invalid'); 60 | $valid->add(Html::tag('p', sprintf( 61 | $this->translate('Certificate chain is invalid: %s.'), 62 | $chain['invalid_reason'] 63 | ))); 64 | } 65 | 66 | $certs = X509Certificate::on($conn)->with(['chain']); 67 | $certs 68 | ->filter(Filter::equal('chain.id', $id)) 69 | ->getSelectBase() 70 | ->orderBy('certificate_link.order'); 71 | 72 | $this->view->chain = (new HtmlDocument()) 73 | ->add($chainInfo) 74 | ->add($valid) 75 | ->add((new ChainDetails())->setData($certs)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /application/controllers/ConfigController.php: -------------------------------------------------------------------------------- 1 | assertPermission('config/modules'); 16 | 17 | parent::init(); 18 | } 19 | 20 | public function backendAction() 21 | { 22 | $form = (new BackendConfigForm()) 23 | ->setIniConfig(Config::module('x509')); 24 | 25 | $form->handleRequest(); 26 | 27 | $this->view->tabs = $this->Module()->getConfigTabs()->activate('backend'); 28 | $this->view->form = $form; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /application/controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | addTitleTab($this->translate('Certificate Dashboard')); 23 | $this->getTabs()->disableLegacyExtensions(); 24 | 25 | try { 26 | $db = Database::get(); 27 | } catch (ConfigurationError $_) { 28 | $this->render('missing-resource', null, true); 29 | return; 30 | } 31 | 32 | $byCa = X509Certificate::on($db) 33 | ->columns([ 34 | 'issuer_certificate.subject', 35 | 'cnt' => new Expression('COUNT(*)') 36 | ]) 37 | ->orderBy('cnt', SORT_DESC) 38 | ->orderBy('issuer_certificate.subject') 39 | ->filter(Filter::equal('issuer_certificate.ca', true)) 40 | ->limit(5); 41 | 42 | $byCa 43 | ->getSelectBase() 44 | ->groupBy('certificate_issuer_certificate.id'); 45 | 46 | $this->view->byCa = (new Donut()) 47 | ->setHeading($this->translate('Certificates by CA'), 2) 48 | ->setData($byCa) 49 | ->setLabelCallback(function ($data) { 50 | return Html::tag( 51 | 'a', 52 | [ 53 | 'href' => Url::fromPath('x509/certificates', [ 54 | 'issuer' => $data->issuer_certificate->subject 55 | ])->getAbsoluteUrl() 56 | ], 57 | $data->issuer_certificate->subject 58 | ); 59 | }); 60 | 61 | $duration = X509Certificate::on($db) 62 | ->columns([ 63 | 'duration', 64 | 'cnt' => new Expression('COUNT(*)') 65 | ]) 66 | ->filter(Filter::equal('ca', false)) 67 | ->orderBy('cnt', SORT_DESC) 68 | ->limit(5); 69 | 70 | $duration 71 | ->getSelectBase() 72 | ->groupBy('duration'); 73 | 74 | $this->view->duration = (new Donut()) 75 | ->setHeading($this->translate('Certificates by Duration'), 2) 76 | ->setData($duration) 77 | ->setLabelCallback(function ($data) { 78 | return Html::tag( 79 | 'a', 80 | [ 81 | 'href' => Url::fromPath( 82 | "x509/certificates?duration={$data->duration->getTimestamp()}&ca=n" 83 | )->getAbsoluteUrl() 84 | ], 85 | CertificateUtils::duration($data->duration->getTimestamp()) 86 | ); 87 | }); 88 | 89 | $keyStrength = X509Certificate::on($db) 90 | ->columns([ 91 | 'pubkey_algo', 92 | 'pubkey_bits', 93 | 'cnt' => new Expression('COUNT(*)') 94 | ]) 95 | ->orderBy('cnt', SORT_DESC) 96 | ->limit(5); 97 | 98 | $keyStrength 99 | ->getSelectBase() 100 | ->groupBy(['pubkey_algo', 'pubkey_bits']); 101 | 102 | $this->view->keyStrength = (new Donut()) 103 | ->setHeading($this->translate('Key Strength'), 2) 104 | ->setData($keyStrength) 105 | ->setLabelCallback(function ($data) { 106 | return Html::tag( 107 | 'a', 108 | [ 109 | 'href' => Url::fromPath( 110 | 'x509/certificates', 111 | [ 112 | 'pubkey_algo' => $data->pubkey_algo, 113 | 'pubkey_bits' => $data->pubkey_bits 114 | ] 115 | )->getAbsoluteUrl() 116 | ], 117 | "{$data->pubkey_algo} {$data->pubkey_bits} bits" 118 | ); 119 | }); 120 | 121 | $sigAlgos = X509Certificate::on($db) 122 | ->columns([ 123 | 'signature_algo', 124 | 'signature_hash_algo', 125 | 'cnt' => new Expression('COUNT(*)') 126 | ]) 127 | ->orderBy('cnt', SORT_DESC) 128 | ->limit(5); 129 | 130 | $sigAlgos 131 | ->getSelectBase() 132 | ->groupBy(['signature_algo', 'signature_hash_algo']); 133 | 134 | $this->view->sigAlgos = (new Donut()) 135 | ->setHeading($this->translate('Signature Algorithms'), 2) 136 | ->setData($sigAlgos) 137 | ->setLabelCallback(function ($data) { 138 | return Html::tag( 139 | 'a', 140 | [ 141 | 'href' => Url::fromPath( 142 | 'x509/certificates', 143 | [ 144 | 'signature_hash_algo' => $data->signature_hash_algo, 145 | 'signature_algo' => $data->signature_algo 146 | ] 147 | )->getAbsoluteUrl() 148 | ], 149 | "{$data->signature_hash_algo} with {$data->signature_algo}" 150 | ); 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /application/controllers/JobsController.php: -------------------------------------------------------------------------------- 1 | addTitleTab($this->translate('Jobs')); 23 | $this->getTabs()->add('sni', [ 24 | 'title' => $this->translate('Configure SNI'), 25 | 'label' => $this->translate('SNI'), 26 | 'url' => 'x509/sni', 27 | 'baseTarget' => '_main' 28 | ]); 29 | 30 | $jobs = X509Job::on(Database::get()); 31 | if ($this->hasPermission('config/x509')) { 32 | $this->addControl( 33 | (new ButtonLink($this->translate('New Job'), Url::fromPath('x509/jobs/new'), 'plus')) 34 | ->openInModal() 35 | ); 36 | } 37 | 38 | $sortControl = $this->createSortControl($jobs, [ 39 | 'name' => $this->translate('Name'), 40 | 'author' => $this->translate('Author'), 41 | 'ctime' => $this->translate('Date Created'), 42 | 'mtime' => $this->translate('Date Modified') 43 | ]); 44 | 45 | $this->controls->getAttributes()->add('class', 'default-layout'); 46 | $this->addControl($sortControl); 47 | 48 | $this->addContent(new Jobs($jobs)); 49 | } 50 | 51 | public function newAction() 52 | { 53 | $this->assertPermission('config/x509'); 54 | 55 | $this->addTitleTab($this->translate('New Job')); 56 | 57 | $form = (new JobConfigForm()) 58 | ->setAction((string) Url::fromRequest()) 59 | ->on(JobConfigForm::ON_SUCCESS, function () { 60 | $this->closeModalAndRefreshRelatedView(Url::fromPath('x509/jobs')); 61 | }) 62 | ->handleRequest($this->getServerRequest()); 63 | 64 | $this->addContent($form); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /application/controllers/SniController.php: -------------------------------------------------------------------------------- 1 | getTabs()->add('jobs', [ 23 | 'title' => $this->translate('Configure Jobs'), 24 | 'label' => $this->translate('Jobs'), 25 | 'url' => 'x509/jobs', 26 | 'baseTarget' => '_main' 27 | 28 | ]); 29 | $this->addTitleTab($this->translate('SNI')); 30 | 31 | $this->addControl( 32 | (new ButtonLink($this->translate('New SNI Map'), Url::fromPath('x509/sni/new'), 'plus')) 33 | ->openInModal() 34 | ); 35 | $this->controls->getAttributes()->add('class', 'default-layout'); 36 | 37 | $this->view->controls = $this->controls; 38 | 39 | $repo = new SniIniRepository(); 40 | 41 | $this->view->sni = $repo->select(array('ip')); 42 | } 43 | 44 | /** 45 | * Create a map 46 | */ 47 | public function newAction() 48 | { 49 | $this->addTitleTab($this->translate('New SNI Map')); 50 | 51 | $form = $this->prepareForm()->add(); 52 | 53 | $form->handleRequest(); 54 | 55 | $this->addContent(new HtmlString($form->render())); 56 | } 57 | 58 | /** 59 | * Update a map 60 | */ 61 | public function updateAction() 62 | { 63 | $form = $this->prepareForm()->edit($this->params->getRequired('ip')); 64 | 65 | try { 66 | $form->handleRequest(); 67 | } catch (NotFoundError $_) { 68 | $this->httpNotFound($this->translate('IP not found')); 69 | } 70 | 71 | $this->renderForm($form, $this->translate('Update SNI Map')); 72 | } 73 | 74 | /** 75 | * Remove a map 76 | */ 77 | public function removeAction() 78 | { 79 | $form = $this->prepareForm()->remove($this->params->getRequired('ip')); 80 | 81 | try { 82 | $form->handleRequest(); 83 | } catch (NotFoundError $_) { 84 | $this->httpNotFound($this->translate('IP not found')); 85 | } 86 | 87 | $this->renderForm($form, $this->translate('Remove SNI Map')); 88 | } 89 | 90 | /** 91 | * Assert config permission and return a prepared RepositoryForm 92 | * 93 | * @return SniConfigForm 94 | */ 95 | protected function prepareForm() 96 | { 97 | $this->assertPermission('config/x509'); 98 | 99 | return (new SniConfigForm()) 100 | ->setRepository(new SniIniRepository()) 101 | ->setRedirectUrl(Url::fromPath('x509/sni')); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /application/controllers/UsageController.php: -------------------------------------------------------------------------------- 1 | addTitleTab($this->translate('Certificate Usage')); 23 | $this->getTabs()->enableDataExports(); 24 | 25 | try { 26 | $conn = Database::get(); 27 | } catch (ConfigurationError $_) { 28 | $this->render('missing-resource', null, true); 29 | return; 30 | } 31 | 32 | $targets = X509Certificate::on($conn) 33 | ->with(['chain', 'chain.target']) 34 | ->withColumns([ 35 | 'chain.id', 36 | 'chain.valid', 37 | 'chain.target.ip', 38 | 'chain.target.port', 39 | 'chain.target.hostname', 40 | ]); 41 | 42 | $targets 43 | ->getSelectBase() 44 | ->where(new Expression('certificate_link.order = 0')); 45 | 46 | $sortColumns = [ 47 | 'chain.target.hostname' => $this->translate('Hostname'), 48 | 'chain.target.ip' => $this->translate('IP'), 49 | 'chain.target.port' => $this->translate('Port'), 50 | 'subject' => $this->translate('Certificate'), 51 | 'issuer' => $this->translate('Issuer'), 52 | 'version' => $this->translate('Version'), 53 | 'self_signed' => $this->translate('Is Self-Signed'), 54 | 'ca' => $this->translate('Is Certificate Authority'), 55 | 'trusted' => $this->translate('Is Trusted'), 56 | 'pubkey_algo' => $this->translate('Public Key Algorithm'), 57 | 'pubkey_bits' => $this->translate('Public Key Strength'), 58 | 'signature_algo' => $this->translate('Signature Algorithm'), 59 | 'signature_hash_algo' => $this->translate('Signature Hash Algorithm'), 60 | 'valid_from' => $this->translate('Valid From'), 61 | 'valid_to' => $this->translate('Valid To'), 62 | 'chain.valid' => $this->translate('Chain Is Valid'), 63 | 'duration' => $this->translate('Duration') 64 | ]; 65 | 66 | $limitControl = $this->createLimitControl(); 67 | $paginator = $this->createPaginationControl($targets); 68 | $sortControl = $this->createSortControl($targets, $sortColumns); 69 | 70 | $searchBar = $this->createSearchBar($targets, [ 71 | $limitControl->getLimitParam(), 72 | $sortControl->getSortParam() 73 | ]); 74 | 75 | if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { 76 | if ($searchBar->hasBeenSubmitted()) { 77 | $filter = $this->getFilter(); 78 | } else { 79 | $this->addControl($searchBar); 80 | $this->sendMultipartUpdate(); 81 | 82 | return; 83 | } 84 | } else { 85 | $filter = $searchBar->getFilter(); 86 | } 87 | 88 | $targets->peekAhead($this->view->compact); 89 | 90 | $targets->filter($filter); 91 | 92 | $this->addControl($paginator); 93 | $this->addControl($sortControl); 94 | $this->addControl($limitControl); 95 | $this->addControl($searchBar); 96 | 97 | $this->handleFormatRequest($targets, function (Query $targets) { 98 | /** @var X509Certificate $usage */ 99 | foreach ($targets as $usage) { 100 | $usage->valid_from = $usage->valid_from->format('l F jS, Y H:i:s e'); 101 | $usage->valid_to = $usage->valid_to->format('l F jS, Y H:i:s e'); 102 | 103 | $usage->ip = $usage->chain->target->ip; 104 | $usage->hostname = $usage->chain->target->hostname; 105 | $usage->port = $usage->chain->target->port; 106 | $usage->valid = $usage->chain->valid; 107 | 108 | yield array_intersect_key( 109 | iterator_to_array($usage), 110 | array_flip(array_merge(['valid', 'hostname', 'ip', 'port'], $usage->getExportableColumns())) 111 | ); 112 | } 113 | }); 114 | 115 | $this->addContent((new UsageTable())->setData($targets)); 116 | 117 | if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { 118 | $this->sendMultipartUpdate(); // Updates the browser search bar 119 | } 120 | } 121 | 122 | public function completeAction() 123 | { 124 | $this->getDocument()->add( 125 | (new ObjectSuggestions()) 126 | ->setModel(X509Certificate::class) 127 | ->forRequest($this->getServerRequest()) 128 | ); 129 | } 130 | 131 | public function searchEditorAction() 132 | { 133 | $editor = $this->createSearchEditor(X509Certificate::on(Database::get()), [ 134 | LimitControl::DEFAULT_LIMIT_PARAM, 135 | SortControl::DEFAULT_SORT_PARAM 136 | ]); 137 | 138 | $this->getDocument()->add($editor); 139 | $this->setTitle(t('Adjust Filter')); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /application/forms/Config/BackendConfigForm.php: -------------------------------------------------------------------------------- 1 | setName('x509_backend'); 15 | $this->setSubmitLabel($this->translate('Save Changes')); 16 | } 17 | 18 | public function createElements(array $formData) 19 | { 20 | $dbResources = ResourceFactory::getResourceConfigs('db')->keys(); 21 | 22 | $this->addElement('select', 'backend_resource', [ 23 | 'label' => $this->translate('Database'), 24 | 'description' => $this->translate('Database resource'), 25 | 'multiOptions' => array_combine($dbResources, $dbResources), 26 | 'required' => true 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /application/forms/Config/SniConfigForm.php: -------------------------------------------------------------------------------- 1 | addElements([ 18 | [ 19 | 'text', 20 | 'ip', 21 | [ 22 | 'description' => $this->translate('IP'), 23 | 'label' => $this->translate('IP'), 24 | 'required' => true 25 | ] 26 | ], 27 | [ 28 | 'textarea', 29 | 'hostnames', 30 | [ 31 | 'description' => $this->translate('Comma-separated list of hostnames'), 32 | 'label' => $this->translate('Hostnames'), 33 | 'required' => true 34 | ] 35 | ] 36 | ]); 37 | 38 | $this->setSubmitLabel($this->translate('Create')); 39 | } 40 | 41 | protected function createUpdateElements(array $formData) 42 | { 43 | $this->createInsertElements($formData); 44 | $this->setTitle(sprintf($this->translate('Edit map for %s'), $this->getIdentifier())); 45 | $this->setSubmitLabel($this->translate('Save')); 46 | } 47 | 48 | protected function createDeleteElements(array $formData) 49 | { 50 | $this->setTitle(sprintf($this->translate('Remove map for %s?'), $this->getIdentifier())); 51 | $this->setSubmitLabel($this->translate('Yes')); 52 | } 53 | 54 | protected function createFilter() 55 | { 56 | return Filter::where('ip', $this->getIdentifier()); 57 | } 58 | 59 | protected function getInsertMessage($success) 60 | { 61 | return $success 62 | ? $this->translate('Map created') 63 | : $this->translate('Failed to create map'); 64 | } 65 | 66 | protected function getUpdateMessage($success) 67 | { 68 | return $success 69 | ? $this->translate('Map updated') 70 | : $this->translate('Failed to update map'); 71 | } 72 | 73 | protected function getDeleteMessage($success) 74 | { 75 | return $success 76 | ? $this->translate('Map removed') 77 | : $this->translate('Failed to remove map'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /application/forms/Jobs/JobConfigForm.php: -------------------------------------------------------------------------------- 1 | job = $job; 32 | } 33 | 34 | protected function isUpdating(): bool 35 | { 36 | return $this->job !== null; 37 | } 38 | 39 | public function hasBeenSubmitted(): bool 40 | { 41 | if (! $this->hasBeenSent()) { 42 | return false; 43 | } 44 | 45 | $button = $this->getPressedSubmitElement(); 46 | 47 | return $button && ($button->getName() === 'btn_submit' || $button->getName() === 'btn_remove'); 48 | } 49 | 50 | protected function assemble(): void 51 | { 52 | $this->addElement('text', 'name', [ 53 | 'required' => true, 54 | 'label' => $this->translate('Name'), 55 | 'description' => $this->translate('Job name'), 56 | ]); 57 | 58 | $this->addElement('textarea', 'cidrs', [ 59 | 'required' => true, 60 | 'label' => $this->translate('CIDRs'), 61 | 'description' => $this->translate('Comma-separated list of CIDR addresses to scan'), 62 | 'validators' => [ 63 | new CallbackValidator(function ($value, CallbackValidator $validator): bool { 64 | $cidrValidator = new CidrValidator(); 65 | $cidrs = Str::trimSplit($value); 66 | 67 | foreach ($cidrs as $cidr) { 68 | if (! $cidrValidator->isValid($cidr)) { 69 | $validator->addMessage(...$cidrValidator->getMessages()); 70 | 71 | return false; 72 | } 73 | } 74 | 75 | return true; 76 | }) 77 | ] 78 | ]); 79 | 80 | $this->addElement('textarea', 'ports', [ 81 | 'required' => true, 82 | 'label' => $this->translate('Ports'), 83 | 'description' => $this->translate('Comma-separated list of ports to scan'), 84 | ]); 85 | 86 | $this->addElement('textarea', 'exclude_targets', [ 87 | 'required' => false, 88 | 'label' => $this->translate('Exclude Targets'), 89 | 'description' => $this->translate('Comma-separated list of addresses/hostnames to exclude'), 90 | ]); 91 | 92 | $this->addElement('submit', 'btn_submit', [ 93 | 'label' => $this->isUpdating() ? $this->translate('Update') : $this->translate('Create') 94 | ]); 95 | 96 | if ($this->isUpdating()) { 97 | $removeButton = $this->createElement('submit', 'btn_remove', [ 98 | 'class' => 'btn-remove', 99 | 'label' => $this->translate('Remove Job'), 100 | ]); 101 | $this->registerElement($removeButton); 102 | 103 | /** @var HtmlDocument $wrapper */ 104 | $wrapper = $this->getElement('btn_submit')->getWrapper(); 105 | $wrapper->prepend($removeButton); 106 | } 107 | } 108 | 109 | protected function onSuccess(): void 110 | { 111 | $conn = Database::get(); 112 | /** @var FormSubmitElement $submitElement */ 113 | $submitElement = $this->getPressedSubmitElement(); 114 | if ($submitElement->getName() === 'btn_remove') { 115 | try { 116 | /** @var X509Job $job */ 117 | $job = $this->job; 118 | $conn->delete('x509_job', ['id = ?' => $job->id]); 119 | 120 | Notification::success($this->translate('Removed job successfully')); 121 | } catch (Exception $err) { 122 | Notification::error($this->translate('Failed to remove job') . ': ' . $err->getMessage()); 123 | } 124 | } else { 125 | $values = $this->getValues(); 126 | 127 | try { 128 | /** @var User $user */ 129 | $user = Auth::getInstance()->getUser(); 130 | if ($this->job === null) { 131 | $values['author'] = $user->getUsername(); 132 | $values['ctime'] = (new DateTime())->getTimestamp() * 1000.0; 133 | $values['mtime'] = (new DateTime())->getTimestamp() * 1000.0; 134 | 135 | $conn->insert('x509_job', $values); 136 | $message = $this->translate('Created job successfully'); 137 | } else { 138 | $values['mtime'] = (new DateTime())->getTimestamp() * 1000.0; 139 | 140 | $conn->update('x509_job', $values, ['id = ?' => $this->job->id]); 141 | $message = $this->translate('Updated job successfully'); 142 | } 143 | 144 | Notification::success($message); 145 | } catch (Exception $err) { 146 | $message = $this->isUpdating() 147 | ? $this->translate('Failed to update job') 148 | : $this->translate('Failed to create job'); 149 | 150 | Notification::error($message . ': ' . $err->getMessage()); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /application/views/scripts/certificate/index.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | render() ?> 6 |
7 | -------------------------------------------------------------------------------- /application/views/scripts/chain/index.phtml: -------------------------------------------------------------------------------- 1 | compact): ?> 2 |
3 | tabs ?> 4 |
5 | 6 |
7 | render() ?> 8 |
9 | -------------------------------------------------------------------------------- /application/views/scripts/config/backend.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /application/views/scripts/dashboard/index.phtml: -------------------------------------------------------------------------------- 1 | compact): ?> 2 |
3 | tabs ?> 4 |
5 | 6 |
7 |
8 | render() ?> 9 | render() ?> 10 | render() ?> 11 | render() ?> 12 |
13 |
14 | -------------------------------------------------------------------------------- /application/views/scripts/missing-resource.phtml: -------------------------------------------------------------------------------- 1 |
2 | tabs ?> 3 |
4 |
5 |

translate('Database not configured') ?>

6 |

translate('You seem to not have configured a database resource yet. Please create one %1$shere%3$s and then set it in this %2$smodule\'s configuration%3$s.'), 8 | '', 9 | '', 10 | '' 11 | ) ?>

12 |
13 | -------------------------------------------------------------------------------- /application/views/scripts/simple-form.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | create()->setTitle(null) // @TODO(el): create() has to be called because the UserForm is setting the title there ?> 6 |
7 | -------------------------------------------------------------------------------- /application/views/scripts/sni/index.phtml: -------------------------------------------------------------------------------- 1 | controls->render() ?> 2 |
3 | hasResult()): ?> 4 |

escape($this->translate('No SNI maps configured yet.')) ?>

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 29 |
escape($this->translate('IP')) ?>
qlink($data->ip, 'x509/sni/update', ['ip' => $data->ip]) ?>qlink( 17 | null, 18 | 'x509/sni/remove', 19 | array('ip' => $data->ip), 20 | array( 21 | 'class' => 'action-link', 22 | 'icon' => 'cancel', 23 | 'title' => $this->translate('Remove this SNI map') 24 | ) 25 | ) ?>
30 | 31 |
32 | -------------------------------------------------------------------------------- /config/systemd/icinga-x509.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Icinga Certificate Monitoring Module Jobs Runner 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=/usr/bin/icingacli x509 jobs run 7 | Restart=on-success 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /configuration.php: -------------------------------------------------------------------------------- 1 | menuSection(N_('Certificate Monitoring'), array( 8 | 'icon' => 'check', 9 | 'url' => 'x509/dashboard', 10 | 'priority' => 40 11 | )); 12 | 13 | $section->add(N_('Certificate Overview'), array( 14 | 'url' => 'x509/certificates', 15 | 'priority' => 10 16 | )); 17 | 18 | $section->add(N_('Certificate Usage'), array( 19 | 'url' => 'x509/usage', 20 | 'priority' => 20 21 | )); 22 | 23 | $section->add(N_('Configuration'), [ 24 | 'url' => 'x509/jobs', 25 | 'priority' => 100, 26 | 'description' => $this->translate('Configure the scan jobs and SNI map') 27 | ]); 28 | 29 | $this->provideConfigTab('backend', array( 30 | 'title' => $this->translate('Configure the database backend'), 31 | 'label' => $this->translate('Backend'), 32 | 'url' => 'config/backend' 33 | )); 34 | -------------------------------------------------------------------------------- /doc/01-About.md: -------------------------------------------------------------------------------- 1 | # Icinga Certificate Monitoring 2 | 3 | The certificate monitoring module for Icinga keeps track of certificates as they are deployed in a network environment. 4 | It does this by scanning networks for TLS services and collects whatever certificates it finds along the way. 5 | The certificates are verified using its own trust store. 6 | 7 | The module’s web frontend can be used to view scan results, allowing you to drill down into detailed information 8 | about any discovered certificate of your landscape: 9 | 10 | ![X.509 Usage](res/x509-usage.png "X.509 Usage") 11 | 12 | ![X.509 Certificates](res/x509-certificates.png "X.509 Certificates") 13 | 14 | At a glance you see which CAs have issued your certificates and key counters of your environment: 15 | 16 | ![X.509 Dashboard](res/x509-dashboard.png "X.509 Dashboard") 17 | 18 | ## Documentation 19 | 20 | * [Installation](02-Installation.md) 21 | * [Configuration](03-Configuration.md) 22 | * [Monitoring](10-Monitoring.md) 23 | -------------------------------------------------------------------------------- /doc/02-Installation.md: -------------------------------------------------------------------------------- 1 | 2 | # Installing Icinga Certificate Monitoring 3 | 4 | The recommended way to install Icinga Certificate Monitoring 5 | and its dependencies is to use prebuilt packages for 6 | all supported platforms from our official release repository. 7 | Please note that [Icinga Web](https://icinga.com/docs/icinga-web) is required 8 | and if it is not already set up, it is best to do this first. 9 | 10 | The following steps will guide you through installing and setting up Icinga Certificate Monitoring. 11 | 12 | 13 | 14 | ## Installing the Package 15 | 16 | If the [repository](https://packages.icinga.com) is not configured yet, please add it first. 17 | Then use your distribution's package manager to install the `icinga-x509` package 18 | or install [from source](02-Installation.md.d/From-Source.md). 19 | 20 | 21 | ## Setting up the Database 22 | 23 | ### Setting up a MySQL or MariaDB Database 24 | 25 | The module needs a MySQL/MariaDB database with the schema that's provided in the `/usr/share/icingaweb2/modules/x509/schema/mysql.schema.sql` file. 26 | 27 | 28 | **Note:** If you haven't installed this module from packages, then please adapt the schema path to the correct installation path. 29 | 30 | 31 | 32 | You can use the following sample command for creating the MySQL/MariaDB database. Please change the password: 33 | 34 | ``` 35 | CREATE DATABASE x509; 36 | GRANT CREATE, SELECT, INSERT, UPDATE, DELETE, DROP, ALTER, CREATE VIEW, INDEX, EXECUTE ON x509.* TO x509@localhost IDENTIFIED BY 'secret'; 37 | ``` 38 | 39 | After, you can import the schema using the following command: 40 | 41 | ``` 42 | mysql -p -u root x509 < /usr/share/icingaweb2/modules/x509/schema/mysql.schema.sql 43 | ``` 44 | 45 | ### Setting up a PostgreSQL Database 46 | 47 | The module needs a PostgreSQL database with the schema that's provided in the `/usr/share/icingaweb2/modules/x509/schema/pgsql.schema.sql` file. 48 | 49 | 50 | **Note:** If you haven't installed this module from packages, then please adapt the schema path to the correct installation path. 51 | 52 | 53 | 54 | You can use the following sample command for creating the PostgreSQL database. Please change the password: 55 | 56 | ```sql 57 | CREATE USER x509 WITH PASSWORD 'secret'; 58 | CREATE DATABASE x509 59 | WITH OWNER x509 60 | ENCODING 'UTF8' 61 | LC_COLLATE = 'en_US.UTF-8' 62 | LC_CTYPE = 'en_US.UTF-8'; 63 | ``` 64 | 65 | After, you can import the schema using the following command: 66 | 67 | ``` 68 | psql -U x509 x509 -a -f /usr/share/icingaweb2/modules/x509/schema/pgsql.schema.sql 69 | ``` 70 | 71 | This concludes the installation. You should now be able to import CA certificates and set up scan jobs. 72 | Please read the [Configuration](03-Configuration.md) section for details. 73 | 74 | -------------------------------------------------------------------------------- /doc/02-Installation.md.d/From-Source.md: -------------------------------------------------------------------------------- 1 | # Installing Icinga Certificate Monitoring from Source 2 | 3 | Please see the Icinga Web documentation on 4 | [how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source. 5 | Make sure you use `x509` as the module name. The following requirements must also be met. 6 | 7 | ## Requirements 8 | 9 | * PHP (≥7.2) 10 | * MySQL or PostgreSQL PDO PHP libraries 11 | * The following PHP modules must be installed: `gmp`, `pcntl`, `openssl` 12 | * [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.9) 13 | * [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.13.0) 14 | * [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥0.12.0) 15 | 16 | 17 | -------------------------------------------------------------------------------- /doc/03-Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Importing CA certificates 4 | 5 | The module tries to verify certificates using its own trust store. By default, this trust store is empty, and it 6 | is up to the Icinga Web 2 admin to import CA certificates into it. 7 | 8 | Using the `icingacli x509 import` command CA certificates can be imported. The certificate chain file that is specified 9 | with the `--file` option should contain a PEM-encoded list of X.509 certificates which should be added to the trust 10 | store: 11 | 12 | ``` 13 | icingacli x509 import --file /etc/ssl/certs/ca-certificates.crt 14 | ``` 15 | 16 | ## Configure Jobs 17 | 18 | Scan jobs have a name which uniquely identifies them, e.g. `lan`. These names are used by the CLI command to start 19 | scanning for specific jobs. 20 | 21 | Each scan job can have one or more IP address ranges and one or more port ranges. The module scans each port in 22 | a job's port ranges for all the individual IP addresses in the IP ranges. IP address ranges have to be specified using 23 | the CIDR format. Multiple IP address ranges can be separated with commas, e.g.: 24 | 25 | `192.0.2.0/24,10.0.10.0/24` 26 | 27 | Port ranges are separated with dashes (`-`). If you only want to scan a single port you don't need to specify the second 28 | port: 29 | 30 | `443,5665-5669` 31 | 32 | Additionally, each job may also exclude specific **hosts** and **IP** addresses from scan. These hosts won't be scanned 33 | when you run the [scan](04-Scanning.md#scan-command) or [jobs](04-Scanning.md#scheduling-jobs) command. Excluding an entire network and specifying IP addresses in CIDR 34 | format will not work. You must specify concrete **IP**s and **host CN**s separated with commas, e.g: 35 | 36 | `192.0.2.2,192.0.2.5,icinga.com` 37 | 38 | ### Job Schedules 39 | 40 | Schedules are [`cron`](https://crontab.guru) and rule based configs used to run jobs periodically at the given interval. 41 | Every job is allowed to have multiple schedules that can be run independently of each other. Each job schedule provides 42 | different options that you can use to control the scheduling behavior of the [jobs command](04-Scanning.md#scheduling-jobs). 43 | 44 | #### Examples 45 | 46 | A schedule that runs weekly on **Friday** and scans all targets that have not yet been scanned, or 47 | whose last scan is older than `1 week`. 48 | 49 | ![Weekly Schedules](res/weekly-schedules.png "Weekly Schedules") 50 | 51 | ## Server Name Indication 52 | 53 | In case you are serving multiple virtual hosts under a single IP you can configure those in 54 | `Configuration -> Modules -> x509 -> SNI`. 55 | 56 | Each entry defines an IP with multiple hostnames associated with it. These are then utilized when jobs run. 57 | 58 | Modules may also provide sources for SNI. At this time the module monitoring is the only one with known support. 59 | 60 | ## Icinga Certificate Monitoring Daemon 61 | 62 | The default `systemd` service of this module, shipped with package installations, uses the [jobs command](04-Scanning.md#scheduling-jobs) 63 | and runs all your configured jobs and schedules. 64 | 65 | 66 | 67 | > **Note** 68 | > 69 | > If you haven't installed this module from packages, you have to configure this as a `systemd` service yourself by just 70 | > copying the example service definition from `/usr/share/icingaweb2/modules/x509/config/systemd/icinga-x509.service` 71 | > to `/etc/systemd/system/icinga-x509.service`. 72 | 73 | 74 | You can run the following command to enable and start the daemon. 75 | ``` 76 | systemctl enable --now icinga-x509.service 77 | ``` 78 | -------------------------------------------------------------------------------- /doc/04-Scanning.md: -------------------------------------------------------------------------------- 1 | # Scanning 2 | 3 | The Icinga Certificate Monitoring provides CLI commands to scan **hosts** and **IPs** in various ways. 4 | These commands are listed below and can be used individually. It is necessary for all commands to know which IP address 5 | ranges and ports to scan. These can be configured as described [here](03-Configuration.md#configure-jobs). 6 | 7 | ## Scan Command 8 | 9 | The scan command, scans targets to find their X.509 certificates and track changes to them. 10 | A **target** is an **IP-port** combination that is generated from the job configuration, taking into account configured 11 | [**SNI**](03-Configuration.md#server-name-indication) maps, so that targets with multiple certificates are also properly 12 | scanned. 13 | 14 | By default, successive calls to the scan command perform partial scans, checking both targets not yet scanned and 15 | targets whose scan is older than 24 hours, to ensure that all targets are rescanned over time and new certificates 16 | are collected. This behavior can be customized through the command [options](#usage-1). 17 | 18 | > **Note** 19 | > 20 | > When rescanning due targets, they will be rescanned regardless of whether the target previously provided a certificate 21 | > or not, to collect new certificates, track changed certificates, and remove decommissioned certificates. 22 | 23 | ### Usage 24 | 25 | This scan command can be used like any other Icinga Web cli operations like this: `icingacli x509 scan [OPTIONS]` 26 | 27 | **Options:** 28 | 29 | ``` 30 | --job= Scan targets that belong to the specified job. (Required) 31 | --since-last-scan=