mirror of
https://github.com/opnsense/plugins.git
synced 2026-06-04 22:33:07 -04:00
security/acme-client: merge version 1.2 from master
This commit is contained in:
parent
071ba27f3c
commit
2df92251f0
11 changed files with 146 additions and 46 deletions
|
|
@ -1,5 +1,5 @@
|
|||
PLUGIN_NAME= acme-client
|
||||
PLUGIN_VERSION= 1.1
|
||||
PLUGIN_VERSION= 1.2
|
||||
PLUGIN_COMMENT= Let's Encrypt client
|
||||
PLUGIN_MAINTAINER= opnsense@moov.de
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class CertificatesController extends ApiControllerBase
|
|||
$grid = new UIModelGrid($mdlAcme->certificates->certificate);
|
||||
return $grid->fetchBindRequest(
|
||||
$this->request,
|
||||
array("enabled", "name", "altNames", "description"),
|
||||
array("enabled", "name", "altNames", "description", "lastUpdate", "statusCode", "statusLastUpdate"),
|
||||
"name"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
|||
$mdlAcme = $this->getModel();
|
||||
|
||||
// Check if the required plugin is installed
|
||||
if ((string)$mdlAcme->isPluginInstalled('os-haproxy') != "1") {
|
||||
if ((string)$mdlAcme->isPluginInstalled('haproxy') != "1") {
|
||||
$this->getLogger()->error("LE check: HAProxy plugin is NOT installed, skipping integration");
|
||||
return($result);
|
||||
}
|
||||
|
|
@ -322,6 +322,11 @@ class SettingsController extends ApiMutableModelControllerBase
|
|||
(string)$validation->method == "http01" and
|
||||
(string)$validation->http_service == "haproxy") {
|
||||
//$this->getLogger()->error("LE HAProxy DEBUG: checking validation method: " . (string)$validation->name);
|
||||
// Check if HAProxy frontends were specified.
|
||||
if (empty((string)$validation->http_haproxyFrontends)) {
|
||||
// Skip item, no HAProxy frontends were specified.
|
||||
continue;
|
||||
}
|
||||
$_frontends = explode(',', $validation->http_haproxyFrontends);
|
||||
// Walk through all linked frontends.
|
||||
foreach ($_frontends as $_frontend) {
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@
|
|||
</field>
|
||||
<field>
|
||||
<id>certificate.account</id>
|
||||
<label>CA Account</label>
|
||||
<label>LE Account</label>
|
||||
<type>dropdown</type>
|
||||
<help><![CDATA[Set the CA account to use for this certificate.]]></help>
|
||||
<help><![CDATA[Set the Let's Encrypt account to use for this certificate.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<id>certificate.validationMethod</id>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
<field>
|
||||
<label>HTTP-01</label>
|
||||
<type>header</type>
|
||||
<style>method_table method_table_http01</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.http_service</id>
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
<field>
|
||||
<label>HTTP-01/OPNsense</label>
|
||||
<type>header</type>
|
||||
<style>method_table method_table_http01</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.http_opn_autodiscovery</id>
|
||||
|
|
@ -61,6 +63,7 @@
|
|||
<field>
|
||||
<label>HTTP-01/HAProxy</label>
|
||||
<type>header</type>
|
||||
<style>method_table method_table_http01</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.http_haproxyInject</id>
|
||||
|
|
@ -93,6 +96,7 @@
|
|||
<field>
|
||||
<label>DNS-01</label>
|
||||
<type>header</type>
|
||||
<style>method_table method_table_dns01</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_service</id>
|
||||
|
|
@ -109,6 +113,7 @@
|
|||
<field>
|
||||
<label>DNS-01/Alwaysdata</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_ad</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_ad_key</id>
|
||||
|
|
@ -119,6 +124,7 @@
|
|||
<field>
|
||||
<label>DNS-01/aliyun</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_ali</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_ali_key</id>
|
||||
|
|
@ -135,6 +141,7 @@
|
|||
<field>
|
||||
<label>DNS-01/AWS Route53</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_aws</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_aws_id</id>
|
||||
|
|
@ -151,6 +158,7 @@
|
|||
<field>
|
||||
<label>DNS-01/Cloudflare</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_cf</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_cf_email</id>
|
||||
|
|
@ -167,6 +175,7 @@
|
|||
<field>
|
||||
<label>DNS-01/CloudXNS</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_cx</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_cx_key</id>
|
||||
|
|
@ -183,6 +192,7 @@
|
|||
<field>
|
||||
<label>DNS-01/DNSPod</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_dp</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_dp_id</id>
|
||||
|
|
@ -199,6 +209,7 @@
|
|||
<field>
|
||||
<label>DNS-01/GoDaddy</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_gd</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_gd_key</id>
|
||||
|
|
@ -215,6 +226,7 @@
|
|||
<field>
|
||||
<label>DNS-01/IPSConfig</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_ispconfig</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_ispconfig_user</id>
|
||||
|
|
@ -243,6 +255,7 @@
|
|||
<field>
|
||||
<label>DNS-01/lexicon</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_lexicon</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_lexicon_provider</id>
|
||||
|
|
@ -265,6 +278,7 @@
|
|||
<field>
|
||||
<label>DNS-01/LuaDNS</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_lua</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_lua_email</id>
|
||||
|
|
@ -281,6 +295,7 @@
|
|||
<field>
|
||||
<label>DNS-01/DNSMadeEasy</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_me</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_me_key</id>
|
||||
|
|
@ -297,6 +312,7 @@
|
|||
<field>
|
||||
<label>DNS-01/nsupdate</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_nsupdate</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_nsupdate_server</id>
|
||||
|
|
@ -313,6 +329,7 @@
|
|||
<field>
|
||||
<label>DNS-01/OVH</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_ovh</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_ovh_app_key</id>
|
||||
|
|
@ -341,6 +358,7 @@
|
|||
<field>
|
||||
<label>DNS-01/PowerDNS</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_pdns</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_pdns_url</id>
|
||||
|
|
|
|||
|
|
@ -96,45 +96,7 @@ class AcmeClient extends BaseModel
|
|||
*/
|
||||
public function isPluginInstalled($name)
|
||||
{
|
||||
// NOTE: Based on infoAction() from Core/Api/FirmwareController.php
|
||||
// FIXME: Should be replaced by a Core function sooner or later.
|
||||
|
||||
$backend = new Backend();
|
||||
$keys = array('name', 'version', 'comment', 'flatsize', 'locked', 'license');
|
||||
$plugins = array();
|
||||
|
||||
// Only check local package data for performance reasons
|
||||
$current = $backend->configdRun("firmware local");
|
||||
$current = explode("\n", trim($current));
|
||||
|
||||
foreach ($current as $line) {
|
||||
/* package infos are flat lists with 3 pipes as delimiter */
|
||||
$expanded = explode('|||', $line);
|
||||
$translated = array();
|
||||
$index = 0;
|
||||
if (count($expanded) != count($keys)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($keys as $key) {
|
||||
$translated[$key] = $expanded[$index++];
|
||||
}
|
||||
|
||||
/* mark local packages as "installed" */
|
||||
$translated['installed'] = "1";
|
||||
|
||||
/* figure out local and remote plugins */
|
||||
$plugin = explode('-', $translated['name']);
|
||||
if (count($plugin)) {
|
||||
if ($plugin[0] == 'os' || $plugin[0] == 'ospriv') {
|
||||
$plugins[$translated['name']] = $translated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($plugins[$name]) and $plugins[$name]['installed'] == "1") {
|
||||
return 1; // TRUE, is installed
|
||||
} else {
|
||||
return 0; // FALSE, is not installed
|
||||
}
|
||||
return trim($backend->configdRun('firmware plugin ' . escapeshellarg($name)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,6 +224,18 @@
|
|||
<lastUpdate type="IntegerField">
|
||||
<Required>N</Required>
|
||||
</lastUpdate>
|
||||
<!-- hidden field; status of last operation -->
|
||||
<statusCode type="IntegerField">
|
||||
<Required>N</Required>
|
||||
<!-- XXX: enable once data migration is working -->
|
||||
<!-- <default>100</default> -->
|
||||
<MinimumValue>100</MinimumValue>
|
||||
<MaximumValue>1000</MaximumValue>
|
||||
</statusCode>
|
||||
<!-- hidden field; timestamp for statusCode -->
|
||||
<statusLastUpdate type="IntegerField">
|
||||
<Required>N</Required>
|
||||
</statusLastUpdate>
|
||||
</certificate>
|
||||
</certificates>
|
||||
<validations>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,52 @@ POSSIBILITY OF SUCH DAMAGE.
|
|||
} else {
|
||||
return "<span style=\"cursor: pointer;\" class=\"fa fa-square-o command-toggle\" data-value=\"0\" data-row-id=\"" + row.uuid + "\"></span>";
|
||||
}
|
||||
},
|
||||
"certdate": function (column, row) {
|
||||
if (row.lastUpdate == "" || row.lastUpdate == undefined) {
|
||||
return "{{ lang._('pending') }}";
|
||||
} else {
|
||||
var certdate = new Date(row.lastUpdate*1000);
|
||||
return certdate.toLocaleString();
|
||||
}
|
||||
},
|
||||
"acmestatus": function (column, row) {
|
||||
if (row.statusCode == "" || row.statusCode == undefined) {
|
||||
// fallback to lastUpdate value (unset if cert was never issued/imported)
|
||||
if (row.lastUpdate == "" || row.lastUpdate == undefined) {
|
||||
return "{{ lang._('unknown') }}";
|
||||
} else {
|
||||
return "{{ lang._('OK') }}";
|
||||
}
|
||||
} else if (row.statusCode == "100") {
|
||||
return "{{ lang._('unknown') }}";
|
||||
} else if (row.statusCode == "200") {
|
||||
return "{{ lang._('OK') }}";
|
||||
} else if (row.statusCode == "250") {
|
||||
return "{{ lang._('cert revoked') }}";
|
||||
} else if (row.statusCode == "300") {
|
||||
return "{{ lang._('configuration error') }}";
|
||||
} else if (row.statusCode == "400") {
|
||||
return "{{ lang._('validation failed') }}";
|
||||
} else if (row.statusCode == "500") {
|
||||
return "{{ lang._('internal error') }}";
|
||||
} else {
|
||||
return "{{ lang._('unknown') }}";
|
||||
}
|
||||
},
|
||||
"acmestatusdate": function (column, row) {
|
||||
if (row.statusLastUpdate == "" || row.statusCode == undefined) {
|
||||
// fallback to lastUpdate value
|
||||
if (row.lastUpdate == "" || row.lastUpdate == undefined) {
|
||||
return "{{ lang._('unknown') }}";
|
||||
} else {
|
||||
var legacydate = new Date(row.lastUpdate*1000);
|
||||
return legacydate.toLocaleString();
|
||||
}
|
||||
} else {
|
||||
var statusdate = new Date(row.statusLastUpdate*1000);
|
||||
return statusdate.toLocaleString();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -339,6 +385,9 @@ POSSIBILITY OF SUCH DAMAGE.
|
|||
<th data-column-id="name" data-type="string">{{ lang._('Certificate Name') }}</th>
|
||||
<th data-column-id="altNames" data-type="string">{{ lang._('Multi-Domain (SAN)') }}</th>
|
||||
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
|
||||
<th data-column-id="lastUpdate" data-type="string" data-formatter="certdate">{{ lang._('Issue/Renewal Date') }}</th>
|
||||
<th data-column-id="statusCode" data-type="string" data-formatter="acmestatus">{{ lang._('Last Acme Status') }}</th>
|
||||
<th data-column-id="statusLastUpdate" data-type="string" data-formatter="acmestatusdate">{{ lang._('Last Acme Run') }}</th>
|
||||
<th data-column-id="commands" data-width="11em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
|
||||
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
|||
});
|
||||
} else {
|
||||
BootstrapDialog.show({
|
||||
type: BootstrapDialog.TYPE_WARNING,
|
||||
type: BootstrapDialog.TYPE_INFO,
|
||||
title: "{{ lang._('acme-client config test result') }}",
|
||||
message: "{{ lang._('Your acme-client config contains no errors.') }}",
|
||||
draggable: true
|
||||
|
|
|
|||
|
|
@ -48,6 +48,22 @@ POSSIBILITY OF SUCH DAMAGE.
|
|||
}
|
||||
);
|
||||
|
||||
// hook into on-show event for dialog to extend layout.
|
||||
$('#DialogValidation').on('shown.bs.modal', function (e) {
|
||||
$("#validation\\.dns_service").change(function(){
|
||||
var service_id = 'table_' + $(this).val() ;
|
||||
$(".table_dns").hide();
|
||||
if ($("#validation\\.method").val() == 'dns01') {
|
||||
$("."+service_id).show();
|
||||
}
|
||||
});
|
||||
$("#validation\\.method").change(function(){
|
||||
$(".method_table").hide();
|
||||
$(".method_table_"+$(this).val()).show();
|
||||
$("#validation\\.dns_service").change();
|
||||
});
|
||||
$("#validation\\.method").change();
|
||||
})
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ function cert_action_validator($opt_cert_id)
|
|||
} else {
|
||||
//echo "DEBUG: account registration failed\n";
|
||||
log_error("AcmeClient: account registration failed");
|
||||
log_cert_acme_status($certObj, $modelObj, '400');
|
||||
if (isset($options["A"])) {
|
||||
continue; // skip to next item
|
||||
}
|
||||
|
|
@ -166,6 +167,7 @@ function cert_action_validator($opt_cert_id)
|
|||
} else {
|
||||
//echo "DEBUG: account not found\n";
|
||||
log_error("AcmeClient: account not found");
|
||||
log_cert_acme_status($certObj, $modelObj, '300');
|
||||
if (isset($options["A"])) {
|
||||
continue; // skip to next item
|
||||
}
|
||||
|
|
@ -193,10 +195,12 @@ function cert_action_validator($opt_cert_id)
|
|||
// Start acme client to revoke the certificate
|
||||
$rev_result = revoke_cert($certObj, $valObj, $acctObj);
|
||||
if (!$rev_result) {
|
||||
log_cert_acme_status($certObj, $modelObj, '250');
|
||||
return(0); // Success!
|
||||
} else {
|
||||
// Revocation failure
|
||||
log_error("AcmeClient: revocation for certificate failed");
|
||||
log_cert_acme_status($certObj, $modelObj, '400');
|
||||
if (isset($options["A"])) {
|
||||
continue; // skip to next item
|
||||
}
|
||||
|
|
@ -215,8 +219,10 @@ function cert_action_validator($opt_cert_id)
|
|||
//echo "DEBUG: cert import done\n";
|
||||
// Prepare certificate for restart action
|
||||
$restart_certs[] = $certObj;
|
||||
log_cert_acme_status($certObj, $modelObj, '200');
|
||||
} else {
|
||||
log_error("AcmeClient: unable to import certificate: " . (string)$certObj->name);
|
||||
log_cert_acme_status($certObj, $modelObj, '500');
|
||||
if (isset($options["A"])) {
|
||||
continue; // skip to next item
|
||||
}
|
||||
|
|
@ -227,6 +233,7 @@ function cert_action_validator($opt_cert_id)
|
|||
} else {
|
||||
// validation failure
|
||||
log_error("AcmeClient: validation for certificate failed: " . (string)$certObj->name);
|
||||
log_cert_acme_status($certObj, $modelObj, '400');
|
||||
if (isset($options["A"])) {
|
||||
continue; // skip to next item
|
||||
}
|
||||
|
|
@ -234,6 +241,7 @@ function cert_action_validator($opt_cert_id)
|
|||
}
|
||||
} else {
|
||||
log_error("AcmeClient: invalid validation method specified: " . (string)$valObj->method);
|
||||
log_cert_acme_status($certObj, $modelObj, '300');
|
||||
if (isset($options["A"])) {
|
||||
continue; // skip to next item
|
||||
}
|
||||
|
|
@ -241,6 +249,7 @@ function cert_action_validator($opt_cert_id)
|
|||
}
|
||||
} else {
|
||||
log_error("AcmeClient: validation method not found for cert " . $certObj->name);
|
||||
log_cert_acme_status($certObj, $modelObj, '300');
|
||||
if (isset($options["A"])) {
|
||||
continue; // skip to next item
|
||||
}
|
||||
|
|
@ -887,8 +896,11 @@ function run_restart_actions($certlist, $modelObj)
|
|||
}
|
||||
// Extract restart actions
|
||||
$_actions = explode(',', $certObj->restartActions);
|
||||
if (empty($_actions)) {
|
||||
// No restart actions configured.
|
||||
continue;
|
||||
}
|
||||
// Walk through all linked restart actions.
|
||||
$_actions = explode(',', $certObj->restartActions);
|
||||
foreach ($_actions as $_action) {
|
||||
// Extract restart action
|
||||
$action = $modelObj->getByActionID($_action);
|
||||
|
|
@ -1007,6 +1019,32 @@ function run_restart_actions($certlist, $modelObj)
|
|||
return($return);
|
||||
}
|
||||
|
||||
/* Update certificate object to log the status of the current acme run.
|
||||
* Supported status codes are:
|
||||
* 100 pending
|
||||
* 200 issue/renew OK
|
||||
* 250 certificate revoked
|
||||
* 300 configuration error (validation method, account, ...)
|
||||
* 400 issue/renew failed
|
||||
* 500 internal error (code issues, bad luck, unexpected errors, ...)
|
||||
* Feel free to add more status codes to make it more useful.
|
||||
*/
|
||||
function log_cert_acme_status($certObj, $modelObj, $statusCode)
|
||||
{
|
||||
$uuid = $certObj->attributes()->uuid;
|
||||
$node = $modelObj->getNodeByReference('certificates.certificate.' . $uuid);
|
||||
if ($node != null) {
|
||||
$node->statusCode = $statusCode;
|
||||
$node->statusLastUpdate = time();
|
||||
// serialize to config and save
|
||||
$modelObj->serializeToConfig();
|
||||
Config::getInstance()->save();
|
||||
} else {
|
||||
log_error("AcmeClient: unable to update acme status for certificate " . (string)$certObj->name);
|
||||
return(1);
|
||||
}
|
||||
}
|
||||
|
||||
// taken from certs.inc
|
||||
function local_cert_get_subject_array($str_crt, $decode = true)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue