diff --git a/www/nginx/src/etc/inc/plugins.inc.d/nginx.inc b/www/nginx/src/etc/inc/plugins.inc.d/nginx.inc new file mode 100644 index 000000000..439af7f44 --- /dev/null +++ b/www/nginx/src/etc/inc/plugins.inc.d/nginx.inc @@ -0,0 +1,52 @@ + array('/usr/local/opnsense/scripts/nginx/ngx_autoblock.php', '*') + ) + ); +} + +function nginx_services() +{ + + return array( + array( + 'description' => gettext('Reverse Proxy and Web Server'), + 'configd' => array( + 'restart' => array('nginx restart'), + 'start' => array('nginx start') + ), + 'name' => 'nginx', + 'pidfile' => '/var/run/nginx.pid' + ) + ); +} diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/BansController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/BansController.php new file mode 100644 index 000000000..9493ba51c --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/BansController.php @@ -0,0 +1,53 @@ +searchBase('ban', array('ip', 'time')); + } + public function delbanAction($uuid) + { + if ($this->request->isPost() || $this->request->isDelete()) { + $mdl = $this->getModel(); + $node = $mdl->getNodeByReference('ban.' . $uuid); + $backend = new Backend(); + $backend->configdRun('nginx unlock ' . (string)$node->ip); + return $this->delBase('ban', $uuid); + } else { + return array('result' => 'most be called via POST'); + } + } +} diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php index f0e426e36..00de73e5f 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php @@ -31,11 +31,11 @@ use OPNsense\Base\ApiControllerBase; use OPNsense\Core\Backend; use OPNsense\Nginx\Nginx; - class LogsController extends ApiControllerBase { private $nginx; - public function accessesAction($uuid = null) { + public function accessesAction($uuid = null) + { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /accesses delivers a list of servers with access logs @@ -46,7 +46,8 @@ class LogsController extends ApiControllerBase } } - public function errorsAction($uuid = null) { + public function errorsAction($uuid = null) + { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /errors delivers a list of servers with error logs @@ -57,7 +58,8 @@ class LogsController extends ApiControllerBase } } - private function call_configd($type, $uuid) { + private function call_configd($type, $uuid) + { if (!$this->vhost_exists($uuid)) { $this->response->setStatusCode(404, "Not Found"); } @@ -67,7 +69,8 @@ class LogsController extends ApiControllerBase return json_decode($data, true); } - private function list_vhosts() { + private function list_vhosts() + { $data = []; foreach ($this->nginx->http_server->__items as $item) { $data[] = array('id' => $item->getAttributes()['uuid'], 'server_name' => (string)$item->servername); @@ -75,7 +78,8 @@ class LogsController extends ApiControllerBase return $data; } - private function vhost_exists($uuid) { + private function vhost_exists($uuid) + { $data = $this->nginx->getNodeByReference('http_server.'. $uuid); return isset($data); } diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php index 99aeaedda..e675bf31c 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php @@ -309,8 +309,10 @@ class SettingsController extends ApiMutableModelControllerBase // access limit zone headers public function searchlimit_zoneAction() { - return $this->searchBase('limit_zone', - array('description', 'key', 'size', 'rate', 'rate_unit')); + return $this->searchBase( + 'limit_zone', + array('description', 'key', 'size', 'rate', 'rate_unit') + ); } public function getlimit_zoneAction($uuid = null) @@ -337,8 +339,10 @@ class SettingsController extends ApiMutableModelControllerBase // limit_request_connection public function searchlimit_request_connectionAction() { - return $this->searchBase('limit_request_connection', - array('description', 'limit_zone', 'nodelay', 'burst', 'connection_count')); + return $this->searchBase( + 'limit_request_connection', + array('description', 'limit_zone', 'nodelay', 'burst', 'connection_count') + ); } public function getlimit_request_connectionAction($uuid = null) @@ -361,4 +365,33 @@ class SettingsController extends ApiMutableModelControllerBase { return $this->setBase('limit_request_connection', 'limit_request_connection', $uuid); } + // cache path + public function searchcache_pathAction() + { + return $this->searchBase( + 'cache_path', + array('path', 'inactive', 'size', 'max_size') + ); + } + + public function getcache_pathAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('cache_path', 'cache_path', $uuid); + } + + public function addcache_pathAction() + { + return $this->addBase('cache_path', 'cache_path'); + } + + public function delcache_pathAction($uuid) + { + return $this->delBase('cache_path', $uuid); + } + + public function setcache_pathAction($uuid) + { + return $this->setBase('cache_path', 'cache_path', $uuid); + } } diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php index 5b8f8be01..f6cb0cb11 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php @@ -56,13 +56,23 @@ class IndexController extends \OPNsense\Base\IndexController $this->view->security_headers = $this->getForm("security_headers"); $this->view->limit_request_connection = $this->getForm("limit_request_connection"); $this->view->limit_zone = $this->getForm("limit_zone"); + $this->view->cache_path = $this->getForm("cache_path"); $this->view->pick('OPNsense/Nginx/index'); } /** * show the nginx logs page /ui/nginx/index/logs */ - public function logsAction() { + public function logsAction() + { $this->view->pick('OPNsense/Nginx/logs'); } + + /** + * display a viewer for banned IPs. + */ + public function banAction() + { + $this->view->pick('OPNsense/Nginx/ban'); + } } diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/cache_path.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/cache_path.xml new file mode 100644 index 000000000..6d4c51a1f --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/cache_path.xml @@ -0,0 +1,27 @@ +
+ + cache_path.path + + text + + + cache_path.size + + text + + + cache_path.inactive + + text + + + cache_path.use_temp_path + + checkbox + + + cache_path.max_size + + text + +
diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml index 4dcddc9b1..3a213f715 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml @@ -48,6 +48,7 @@ httpserver.verify_client dropdown + true
  • On: the certificate is requested and validated. Use this option to protect a service with TLS authentication.
  • Off: The certificate is not requested. Choose this option for a normal website.
  • Optional: The certificate is requested and validated if existing. Choose this option for websites, with TLS login support or mixed TLS protected API and web content.
  • Optional, don't verify: Do accept the certificate and let the application choose what to do. Choose this option, for the same reasons as optional but in this case, the request is passed to the backend without rejecting untrusted certificates.
  • ]]>
    @@ -75,18 +76,21 @@ httpserver.block_nonpublic_data checkbox + true Blocks files like .htaccess files or other files not intended for the public. httpserver.naxsi_extensive_log checkbox + true Provide a more verbose WAF log for fixing false positives before going live. httpserver.sendfile checkbox + true Allow the daemon to use the sendfile function. diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml index 3cbfc2bc5..f89c85182 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml @@ -61,11 +61,62 @@ Select an upstream to proxy to or connect via FastCGI if chosen. + + location.cache_path + + dropdown + Choose a cache directory if you want to cache responses. + + + location.cache_use_stale + + select_multiple + + true + If you enable this option, a stale response will be sent when in cases the original server is unable to (for example if it is down). + + + location.cache_min_uses + + text + true + Enter how often the resource must be hit before adding it to the cache. + + + location.cache_background_update + + checkbox + true + Return a stale response to the client and update the cache. + + + location.cache_lock + + checkbox + true + Only allow a single request to call the backend on the same URL at a single time. + + + location.cache_revalidate + + checkbox + true + Only request a new version, if the content has changed since the last cache update. + + + location.cache_methods + + select_multiple + + true + Select the HTTP verbs to cache. GET and HEAD will be always cached. + location.limit_request_connections select_multiple + true If you choose multiple limits, the strictest will be used. @@ -110,6 +161,12 @@ checkbox Force encrypted connections. + + location.http2_push_preload + + checkbox + If you check this box, you can use the link header to send resources to the client before they are requested. You can boost your performance with this setting. This requires that your application sets the "Link" header correctly. + location.php_enable @@ -122,4 +179,18 @@ text If you set this setting, all requests are sent to this script instead of the request path (URL). Not using this setting on a remote instance can be dangerous. + + location.honeypot + + checkbox + true + If you enable the honeypot, all requests to this location will go to a special temporary log which will be used to block the IP. This is dangerous because you may accidentally block legitimate users or search engines. The result is available as a special alias in the firewall section. For example you can trigger on locations of Wordpress for phpMyAdmin if you are not using it. + + + location.websocket + + checkbox + true + If you enable the WebSocket option, nginx will pass the upgrade header to the backed server. + diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml index 722bc6339..f2303d759 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml @@ -19,6 +19,7 @@ upstream.tls_client_certificate + true Authenticate on the Server using a client certificate. dropdown @@ -50,17 +51,20 @@ upstream.tls_verify checkbox + true Don't turn it off unless you really know what you are doing! Never do it because a random website tells you to do. upstream.tls_verify_depth text + true Choose how many sub-CAs can be between the server certificate and a trusted CA. 1 means the certificate has to be signed directly by a CA. upstream.store + true checkbox Store the response on the local storage. diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream_server.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream_server.xml index 655ad6c3d..15fd3c19f 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream_server.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream_server.xml @@ -37,6 +37,7 @@ upstream_server.no_use + true dropdown diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogLine.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogLine.php index 48c3915a6..b05789244 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogLine.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogLine.php @@ -28,7 +28,6 @@ namespace OPNsense\Nginx; - class AccessLogLine { public $remote_ip; diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php index 2e7d62fec..aa288caf9 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php @@ -42,7 +42,8 @@ class AccessLogParser $this->lines = file($this->file_name); $this->result = array_map([$this, 'parse_line'], $this->lines); } - private function parse_line($line) { + private function parse_line($line) + { $container = new AccessLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { $container->remote_ip = $data[1]; @@ -58,8 +59,8 @@ class AccessLogParser return $container; } - public function get_result() { + public function get_result() + { return $this->result; } - } diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php index be74908a3..f39a2ad7e 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php @@ -42,7 +42,8 @@ class ErrorLogParser $this->lines = file($this->file_name); $this->result = array_map([$this, 'parse_line'], $this->lines); } - private function parse_line($line) { + private function parse_line($line) + { $container = new ErrorLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { $container->date = $data[1]; @@ -54,7 +55,8 @@ class ErrorLogParser return $container; } - public function get_result() { + public function get_result() + { return $this->result; } } diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml index b7dc538bf..06050605c 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml @@ -3,6 +3,7 @@ + diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml index 91f166929..ee115c95e 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml @@ -1,6 +1,6 @@ //OPNsense/Nginx - 1.1.1 + 1.1.2 nginx web server, reverse proxy and waf @@ -205,6 +205,65 @@ N N + + + + + Selected cache directory not found + N + N + + + Y + + Error + Timeout + Invalid_header + Updating + HTTP Status Code 403 + HTTP Status Code 404 + HTTP Status Code 429 + HTTP Status Code 500 + HTTP Status Code 502 + HTTP Status Code 503 + HTTP Status Code 504 + + N + + + Y + + Post + + + N + + + Y + 1 + + + Y + 0 + + + Y + 0 + + + Y + 0 + N @@ -267,6 +326,18 @@ N Y + + Y + 0 + + + Y + 0 + + + Y + 0 + @@ -922,5 +993,36 @@ Y + + + Y + + + + + + Y + /\/(srv|var|tmp|mnt)[a-z0-9\-\._\:\,\/]+[a-z0-9\-\._\:\,]+/i + + + 10 + 10 + + + N + 1 + + + Y + 0 + + + N + 1 + + diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/ban.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/ban.volt new file mode 100644 index 000000000..e2071c5fb --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/ban.volt @@ -0,0 +1,60 @@ +{# + # Copyright (C) 2017-2018 Fabian Franz + # Copyright (C) 2014-2015 Deciso B.V. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + +
    + + + + + + + + + + +
    {{ lang._('IP Address / Network') }}{{ lang._('Time') }}{{ lang._('Unlock') }}
    +
    + \ No newline at end of file diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt index 1679d3924..8de244bc7 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt @@ -28,7 +28,7 @@ - - + + + diff --git a/www/nginx/src/opnsense/scripts/nginx/csp_report.php b/www/nginx/src/opnsense/scripts/nginx/csp_report.php index 1e8e0293b..303eed985 100644 --- a/www/nginx/src/opnsense/scripts/nginx/csp_report.php +++ b/www/nginx/src/opnsense/scripts/nginx/csp_report.php @@ -36,12 +36,12 @@ if (stristr($_SERVER['CONTENT_TYPE'], 'json') === false) { } if ($json_data = json_decode(file_get_contents('php://input'), true)) { - http_response_code(204); + http_response_code(204); // inject some data for a log viewer to get a relation with the server entry - $json_data['server_time'] = time(); - $json_data['server_uuid'] = $_SERVER['SERVER-UUID']; - $json_data = json_encode($json_data); - file_put_contents($log_file, $json_data . PHP_EOL, FILE_APPEND | LOCK_EX); + $json_data['server_time'] = time(); + $json_data['server_uuid'] = $_SERVER['SERVER-UUID']; + $json_data = json_encode($json_data); + file_put_contents($log_file, $json_data . PHP_EOL, FILE_APPEND | LOCK_EX); } else { http_response_code(400); echo "Your request data cannot be decoded. Please send compliant JSON data."; diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_auth.php b/www/nginx/src/opnsense/scripts/nginx/ngx_auth.php index a1e166a17..8927f9b86 100644 --- a/www/nginx/src/opnsense/scripts/nginx/ngx_auth.php +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_auth.php @@ -36,14 +36,18 @@ $method = $_SERVER['Original-METHOD']; $is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'; $server_uuid = $_SERVER['SERVER-UUID']; -function password_auth_test($username, $password, $auth_server) { +function password_auth_test($username, $password, $auth_server) +{ $authFactory = new OPNsense\Auth\AuthenticationFactory; $authenticator = $authFactory->get($auth_server); return $authenticator->authenticate($username, $password); } -function password_auth($auth_server = 'Local Database') { - if (!isset($_SERVER['PHP_AUTH_PW']) || !isset($_SERVER['PHP_AUTH_PW'])) return false; +function password_auth($auth_server = 'Local Database') +{ + if (!isset($_SERVER['PHP_AUTH_PW']) || !isset($_SERVER['PHP_AUTH_PW'])) { + return false; + } return password_auth_test($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $auth_server); } diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php new file mode 100755 index 000000000..c7b134c75 --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php @@ -0,0 +1,148 @@ +#!/usr/local/bin/php + 'error', 'message' => $msg) + ); +} +function exec_hidden($command): void +{ + $descriptorspec = array( + 1 => array('file', "/dev/null", 'w'), + 2 => array('file', "/dev/null", "w") + ); + + $process = proc_open($command, $descriptorspec, $pipes); + + if (is_resource($process)) { + proc_close($process); + } +} +function add_to_blocklist($tablename, $ip) +{ + $escaped = escapeshellarg($ip); + exec_hidden("/sbin/pfctl -t ${tablename} -T add ${escaped}"); +} + + +function reopen_logs() +{ + exec_hidden('/usr/local/sbin/nginx -s reopen'); +} + +$permanent_ban_file = '/var/log/nginx/permanentban.access.log'; +$permanent_ban_file_work = $permanent_ban_file . '.work'; +$autoblock_alias_name = 'nginx_autoblock'; + + +if (!file_exists($permanent_ban_file)) { + nginx_print_error('No Log exists - nothing to do'); + // let create it + reopen_logs(); + exit(0); +} + +// move the file, and inform nginx that we deleted the file +rename($permanent_ban_file, $permanent_ban_file_work); +reopen_logs(); + +$log_parser = new AccessLogParser($permanent_ban_file_work); + +$log_lines = $log_parser->get_result(); + +$model = new Alias(); + +$blacklist_element = null; +foreach ($model->aliases->alias->__items as $alias) { + if ((string)$alias->name == $autoblock_alias_name) { + if ((string)$alias->type != 'external') { + nginx_print_error('alias is misconfigured - exiting'); + exit(0); + } else { + $blacklist_element = $alias; + break; + } + } +} + +// does not exist yet, create it +if ($blacklist_element == null) { + $blacklist_element = $model->aliases->alias->Add(); + $blacklist_element->name = $autoblock_alias_name; + $blacklist_element->type = "external"; + $model->serializeToConfig(); +} + +$model = new Nginx(); +$alias_ips = []; +foreach ($model->ban->__items as $entry) { + $alias_ips[] = (string)$entry->ip; +} + +$new_ips = array_unique( + array_map(function ($row) { + if (stripos($row->remote_ip, '.') !== false) { + return $row->remote_ip; + } + // in case of IPv6, we have to use the network address instead + // danger of DoS because the attacker should have at least 2 ** 64 IPs + return Net_IPv6::getNetmask($row->remote_ip, 64) . '/64'; + }, $log_lines) +); + +foreach (array_diff($new_ips, $alias_ips) as $new_ip) { + $entry = $model->ban->Add(); + $entry->ip = $new_ip; + $entry->time = time(); +} +$val_result = $model->performValidation(false); +if (count($val_result) !== 0) { + print_r($val_result); + exit(1); +} +$model->serializeToConfig(); +Config::getInstance()->save(); +echo '{"status":"saved"}'; + +// all ips are used because the others may not be set for some reason +foreach ($model->ban->__items as $entry) { + add_to_blocklist($autoblock_alias_name, (string)$entry->ip); +} + +@unlink($permanent_ban_file_work); diff --git a/www/nginx/src/opnsense/scripts/nginx/read_log.php b/www/nginx/src/opnsense/scripts/nginx/read_log.php index 7ae3ba431..716a3f09f 100755 --- a/www/nginx/src/opnsense/scripts/nginx/read_log.php +++ b/www/nginx/src/opnsense/scripts/nginx/read_log.php @@ -30,11 +30,14 @@ require_once 'config.inc'; use OPNsense\Nginx\Nginx; use OPNsense\Nginx\ErrorLogParser; use OPNsense\Nginx\AccessLogParser; + $log_prefix = '/var/log/nginx/'; $log_suffix = '.log'; -if ($_SERVER['argc'] != 3) die('{"error": "Incorrect amount of parameters given"}'); +if ($_SERVER['argc'] != 3) { + die('{"error": "Incorrect amount of parameters given"}'); +} // first parameter: error|access $mode = $_SERVER['argv'][1]; @@ -51,24 +54,26 @@ if ($data = $nginx->getNodeByReference('http_server.'. $server)) { foreach (explode(',', $server_names) as $server_name) { $log_file_name = $log_prefix . basename($server_name) . '.' . $mode . $log_suffix; // this entry has no log file, ignore it - if (!file_exists($log_file_name)) continue; + if (!file_exists($log_file_name)) { + continue; + } $logparser = null; if ($mode == 'error') { $logparser = new ErrorLogParser($log_file_name); - } - elseif ($mode == 'access') { + } elseif ($mode == 'access') { $logparser = new AccessLogParser($log_file_name); } // we cannot parse the file - something went wrong - if ($logparser == null) continue; + if ($logparser == null) { + continue; + } $lines = array_merge($lines, $logparser->get_result()); } if (empty($lines)) { $lines['error'] = 'no lines found'; } echo json_encode($lines); -} -else { +} else { die('{"error": "UUID not found"}'); } diff --git a/www/nginx/src/opnsense/scripts/nginx/setup.php b/www/nginx/src/opnsense/scripts/nginx/setup.php index 3fe4e61e8..5a0500d46 100755 --- a/www/nginx/src/opnsense/scripts/nginx/setup.php +++ b/www/nginx/src/opnsense/scripts/nginx/setup.php @@ -31,78 +31,85 @@ require_once('config.inc'); require_once('certs.inc'); use \OPNsense\Nginx\Nginx; -function export_pem_file($filename, $data) { - $pem_content = trim(str_replace("\n\n", "\n", str_replace( - "\r", - "", - base64_decode((string)$data)) - )); - file_put_contents($filename, $pem_content); - chmod($filename, 0600); +function export_pem_file($filename, $data, $post_append = null) +{ + $pem_content = trim(str_replace("\n\n", "\n", str_replace( + "\r", + "", + base64_decode((string)$data) + ) . ($post_append == null ? '' : "\n" . $post_append))); + file_put_contents($filename, $pem_content); + chmod($filename, 0600); } -function find_cert($refid) { - global $config; - foreach($config['cert'] as $cert_entry) { - if ($cert_entry['refid'] == $refid) { - return $cert_entry; +function find_cert($refid) +{ + global $config; + foreach ($config['cert'] as $cert_entry) { + if ($cert_entry['refid'] == $refid) { + return $cert_entry; + } } - } } -function find_ca($refid) { - global $config; - foreach($config['ca'] as $cert_entry) { - if ($cert_entry['refid'] == $refid) { - return $cert_entry; +function find_ca($refid) +{ + global $config; + foreach ($config['ca'] as $cert_entry) { + if ($cert_entry['refid'] == $refid) { + return $cert_entry; + } } - } } // export server certificates if (!isset($config['OPNsense']['Nginx'])) { - die("nginx is not configured"); + die("nginx is not configured"); } $nginx = $config['OPNsense']['Nginx']; if (!isset($nginx['http_server'])) { - die("no http servers configured"); + die("no http servers configured"); } if (is_array($nginx['http_server']) && !isset($nginx['http_server']['servername'])) { - $http_servers = $nginx['http_server']; + $http_servers = $nginx['http_server']; } else { - $http_servers = array($nginx['http_server']); + $http_servers = array($nginx['http_server']); } @mkdir('/usr/local/etc/nginx/key', 0750, true); @mkdir("/var/db/nginx/auth", 0750, true); foreach ($http_servers as $http_server) { - if (!empty($http_server['listen_https_port']) && !empty($http_server['certificate'])) - { - // try to find the reference - $cert = find_cert($http_server['certificate']); - if (!isset($cert)) { - next; - } - $hostname = explode(',', $http_server['servername'])[0]; - export_pem_file( - '/usr/local/etc/nginx/key/' . $hostname . '.pem', - $cert['crt'] - ); - export_pem_file( - '/usr/local/etc/nginx/key/' . $hostname . '.key', - $cert['prv'] - ); - if (!empty($http_server['ca'])) { - foreach ($http_server['ca'] as $caref) { - $ca = find_ca($caref); - if (isset($ca)) { - export_pem_file( - '/usr/local/etc/nginx/key/' . $hostname . '_ca.pem', - $ca['crt'] - ); + if (!empty($http_server['listen_https_port']) && !empty($http_server['certificate'])) { + // try to find the reference + $cert = find_cert($http_server['certificate']); + if (!isset($cert)) { + next; + } + $chain = []; + foreach (ca_chain_array($cert) as $entry) { + $chain[] = base64_decode($entry['crt']); + } + $hostname = explode(',', $http_server['servername'])[0]; + export_pem_file( + '/usr/local/etc/nginx/key/' . $hostname . '.pem', + $cert['crt'], + implode("\n", $chain) + ); + export_pem_file( + '/usr/local/etc/nginx/key/' . $hostname . '.key', + $cert['prv'] + ); + if (!empty($http_server['ca'])) { + foreach ($http_server['ca'] as $caref) { + $ca = find_ca($caref); + if (isset($ca)) { + export_pem_file( + '/usr/local/etc/nginx/key/' . $hostname . '_ca.pem', + $ca['crt'] + ); + } + } } - } } - } } // end export server certificates @@ -116,16 +123,20 @@ if (isset($nginx['upstream'])) { foreach ($upstreams as $upstream) { $upstream_uuid = $upstream['@attributes']['uuid']; - if (!empty($upstream['tls_enable']) && $upstream['tls_enable'] == '1') - { + if (!empty($upstream['tls_enable']) && $upstream['tls_enable'] == '1') { // try to find the reference if (!empty($upstream['tls_client_certificate'])) { $cert = find_cert($upstream['tls_client_certificate']); if (isset($cert)) { + $chain = []; + foreach (ca_chain_array($cert) as $entry) { + $chain[] = base64_decode($entry['crt']); + } $hostname = explode(',', $http_server['servername'])[0]; export_pem_file( '/usr/local/etc/nginx/key/' . $upstream['tls_client_certificate'] . '.pem', - $cert['crt'] + $cert['crt'], + implode("\n", $chain) ); export_pem_file( '/usr/local/etc/nginx/key/' . $upstream['tls_client_certificate'] . '.key', @@ -159,18 +170,21 @@ foreach ($nginx->userlist->__items as $user_list) { $file = null; try { $file = fopen("/var/db/nginx/auth/" . $uuid, "wb"); - $users = explode(',',(string)$user_list->users); + $users = explode(',', (string)$user_list->users); foreach ($users as $user) { $user_node = $nginx->getNodeByReference("credential." . $user); $username = (string)$user_node->username; $password = crypt((string)$user_node->password); fwrite($file, $username . ':' . $password . "\n"); } - } - finally { + } finally { if (isset($file)) { fclose($file); } unset($file); } } +// create directories for cache +foreach ($nginx->cache_path->__items as $cache_path) { + @mkdir((string)$cache_path->path, 0755, true); +} diff --git a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf index 63297eec8..617e55bee 100644 --- a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf +++ b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf @@ -31,3 +31,22 @@ command:/usr/local/opnsense/scripts/nginx/read_log.php parameters: %s %s type:script_output message:restarting nginx + +[log] +command:/usr/local/opnsense/scripts/nginx/read_log.php +parameters: %s %s +type:script_output +message:restarting nginx + +[unlock] +command:/sbin/pfctl -t nginx_autoblock -T delete +parameters: %s +type:script +message:restarting nginx + +[autoblock] +command:/usr/local/opnsense/scripts/nginx/ngx_autoblock.php +parameters: +type:script_output +message:Ban bots and other attackers (nginx plugin) +description:Automatically ban attacking hosts (nginx plugin) diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf index 7734e90ee..9da79e97f 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf @@ -29,6 +29,11 @@ default_type application/octet-stream; keepalive_timeout {{ OPNsense.Nginx.http.keepalive_timeout }}; {% endif %} +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + # TODO add when core is ready for allowing nginx to serve the web interface # include nginx_web.conf; @@ -37,6 +42,15 @@ keepalive_timeout {{ OPNsense.Nginx.http.keepalive_timeout }}; limit_req_zone ${{ zone.key }} zone={{ zone['@uuid'].replace('-', '') }}:{{ zone.size }}m rate={{ zone.rate }}{{ zone.rate_unit }}; {% endfor %} +{% for cache_path in helpers.toList('OPNsense.Nginx.cache_path') %} +proxy_cache_path {{ cache_path.path }} levels=1:2 keys_zone={{ cache_path['@uuid'].replace('-', '') }}:{{ cache_path.size +}}m{% if cache_path.max_size is defined and cache_path.max_size != '' + %} max_size={{ cache_path.max_size }}g{% endif%}{% if cache_path.inactive is defined and cache_path.inactive != '' +%} inactive={{ cache_path.inactive }}m{% endif%} use_temp_path={% +if cache_path.use_temp_path is defined and cache_path.use_temp_path == '1' +%}on{% else %}off{%endif%}; +{% endfor %} + {% include "OPNsense/Nginx/upstream.conf" ignore missing with context %} {% set listen_list = [] %} @@ -81,7 +95,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - server_name {{ server.servername }}; + server_name {{ server.servername.replace(',', ' ') }}; charset {{ server.charset }}; access_log /var/log/nginx/{{ server.servername }}.access.log {{ server.access_log_format }}; error_log /var/log/nginx/{{ server.servername }}.error.log; @@ -130,10 +144,15 @@ server { } {% endif %} # block based on User Agents - stuff I have found over the years in my server log - if ($http_user_agent ~* Python-urllib|Nmap|python-requests|libwww-perl|MJ12bot|Jorgee|fasthttp|libwww|Telesphoreo|A6-Indexer|ltx71|okhttp|ZmEu) { + if ($http_user_agent ~* Python-urllib|Nmap|python-requests|libwww-perl|MJ12bot|Jorgee|fasthttp|libwww|Telesphoreo|A6-Indexer|ltx71|okhttp|ZmEu|sqlmap|LMAO/2.0|ltx71|zgrab|Ronin/2.0|Hakai/2.0) { return 418; } - if ($http_user_agent ~ "Indy\sLibrary|Morfeus Fucking Scanner") + {# MSIE 7 cannot be blocked - used for compatibility mode - https://blogs.msdn.microsoft.com/ieinternals/2013/09/21/internet-explorer-11s-many-user-agent-strings/ #} + if ($http_user_agent ~ "Indy\sLibrary|Morfeus Fucking Scanner|MSIE [0-6]\.\d+") + { + return 418; + } + if ($http_user_agent ~ ^Mozilla/[\d\.]+$) { return 418; } @@ -164,13 +183,7 @@ server { fastcgi_param TLS-SNI-Host $ssl_server_name; fastcgi_param Original-URI $request_uri; fastcgi_param Original-HOST $host; -{% if helpers._template_in_data['__uuid__'] is defined %} -{% for uuid in helpers._template_in_data['__uuid__'] %} -{% if helpers._template_in_data['__uuid__'][uuid] == server %} - fastcgi_param SERVER-UUID "{{ uuid }}"; -{% endif %} -{% endfor %} -{% endif %} + fastcgi_param SERVER-UUID "{{ server['@uuid'] }}"; fastcgi_param SCRIPT_FILENAME /usr/local/opnsense/scripts/nginx/ngx_auth.php; fastcgi_intercept_errors on; include fastcgi_params; diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf index 6fb180f97..06ca29f3c 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf @@ -1,5 +1,8 @@ location {{ location.matchtype }} {{ location.urlpattern }} { +{% if location.honeypot == '1' %} + return 418; +{% else %} {% if location.enable_secrules is defined and location.enable_secrules == '1' %} SecRulesEnabled; {% endif %} @@ -59,6 +62,7 @@ location {{ location.matchtype }} {{ location.urlpattern }} { auth_request /opnsense-auth-request; {% endif %} {% endif %} + http2_push_preload {% if location.http2_push_preload is defined and location.http2_push_preload == '1' %}on{% else %}off{% endif %}; {% if location.php_enable is defined and location.php_enable == '1' %} fastcgi_split_path_info ^(.+\.php)(/.+)$; include fastcgi_params; @@ -87,6 +91,23 @@ location {{ location.matchtype }} {{ location.urlpattern }} { {% if location.upstream is defined and (location.php_enable is not defined or location.php_enable != '1') %} {% set upstream = helpers.getUUID(location.upstream) %} proxy_set_header Host $host; +{% if location.websocket is defined and location.websocket == '1' %} + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; +{% endif %} +{% if location.cache_path is defined and location.cache_path != '' %} + proxy_cache {{ location.cache_path.replace('-', '') }}; +{% if location.cache_use_stale is defined and location.cache_use_stale != '' %} + proxy_cache_use_stale {{ location.cache_use_stale.replace(',', ' ') }}; +{% endif %} + proxy_cache_min_uses {{ location.cache_min_uses|default('1') }}; + proxy_cache_background_update {% if location.cache_background_update is defined and location.cache_background_update == '1' %}on{% else %}off{% endif %}; + proxy_cache_lock {% if location.cache_lock is defined and location.cache_lock == '1'%}on{% else %}off{% endif %}; + proxy_cache_revalidate {% if location.cache_revalidate is defined and location.cache_revalidate == '1' %}on{% else %}off{% endif %}; + proxy_cache_methods GET HEAD{% if location.cache_methods is defined and location.cache_methods != '' %} {{ location.cache_methods.replace(',', ' ') }}{% endif %}; +{% endif %} + proxy_pass http{% if upstream.tls_enable == '1' %}s{% endif %}://upstream{{ location.upstream.replace('-','') }}; {% if upstream.tls_enable == '1' %} {% if upstream.tls_client_certificate is defined and upstream.tls_client_certificate != '' %} @@ -115,5 +136,6 @@ location {{ location.matchtype }} {{ location.urlpattern }} { {% endif %} {% endif %} {% endif %} +{% endif %} } diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf index dd6649bf6..0990b5393 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf @@ -27,6 +27,7 @@ server { {% endif %} autoindex off; + http2_push_preload on; # gzip compression gzip_static on;