diff --git a/README.md b/README.md index 1d02f812e..c2ee0de3a 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ sysutils/vmware -- VMware tools sysutils/xen -- Xen guest utilities www/c-icap -- c-icap connects the web proxy with a virus scanner www/cache -- Webserver cache +www/nginx -- Nginx HTTP server and reverse proxy www/web-proxy-sso -- Kerberos authentication module www/web-proxy-useracl -- Group and user ACL for the web proxy ``` diff --git a/www/nginx/Makefile b/www/nginx/Makefile new file mode 100644 index 000000000..849242b50 --- /dev/null +++ b/www/nginx/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= nginx +PLUGIN_VERSION= 0.2 +PLUGIN_COMMENT= Nginx HTTP server and reverse proxy +PLUGIN_DEPENDS= nginx +PLUGIN_MAINTAINER= franz.fabian.94@gmail.com +PLUGIN_DEVEL= yes + +.include "../../Mk/plugins.mk" diff --git a/www/nginx/pkg-descr b/www/nginx/pkg-descr new file mode 100644 index 000000000..3f1a716e4 --- /dev/null +++ b/www/nginx/pkg-descr @@ -0,0 +1,8 @@ +NGINX is a high performance edge web server with the lowest memory footprint +and the key features to build modern and efficient web infrastructure. + +NGINX functionality includes HTTP server, HTTP and mail reverse proxy, caching, +load balancing, compression, request throttling, connection multiplexing and +reuse, SSL offload and HTTP media streaming. + +WWW: https://nginx.org/ diff --git a/www/nginx/src/etc/nginx/views/opnsense_error_404.html b/www/nginx/src/etc/nginx/views/opnsense_error_404.html new file mode 100644 index 000000000..54200472c --- /dev/null +++ b/www/nginx/src/etc/nginx/views/opnsense_error_404.html @@ -0,0 +1,23 @@ + + + + + Request Denied + + + + + +

Not Found

+

The resource you want to access is not available.

+

Please contact the webmaster if you think this is an error.

+ + diff --git a/www/nginx/src/etc/nginx/views/opnsense_server_error.html b/www/nginx/src/etc/nginx/views/opnsense_server_error.html new file mode 100644 index 000000000..6a3267f4a --- /dev/null +++ b/www/nginx/src/etc/nginx/views/opnsense_server_error.html @@ -0,0 +1,23 @@ + + + + + Request Denied + + + + + +

Server Error

+

Sorry, but something went wrong on our side.

+

There is nothing you can do except waiting until we fix the issue.

+ + diff --git a/www/nginx/src/etc/nginx/views/waf_denied.html b/www/nginx/src/etc/nginx/views/waf_denied.html new file mode 100644 index 000000000..b5e96d2c9 --- /dev/null +++ b/www/nginx/src/etc/nginx/views/waf_denied.html @@ -0,0 +1,60 @@ + + + + + Request Denied + + + + + +

Request Denied For Security Reasons

+

The request has been denied by the web application firewall due to a security policy violation.

+

Request information have been logged to investigate the incident.

+

If you think this is an error on our side, please contact us.

+
+ + + diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/ServiceController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/ServiceController.php new file mode 100644 index 000000000..77c0ef122 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/ServiceController.php @@ -0,0 +1,103 @@ + + * Copyright (C) 2016 IT-assistans Sverige AB + * Copyright (C) 2015-2016 Deciso B.V. + * Copyright (C) 2018 Fabian Franz + * 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. + */ + +namespace OPNsense\Nginx\Api; + +use OPNsense\Base\ApiMutableServiceControllerBase; +use OPNsense\Core\Backend; + +class ServiceController extends ApiMutableServiceControllerBase +{ + static protected $internalServiceClass = '\OPNsense\Nginx\Nginx'; + static protected $internalServiceTemplate = 'OPNsense/Nginx'; + static protected $internalServiceEnabled = 'general.enabled'; + static protected $internalServiceName = 'nginx'; + + /** + * override parent method - stopping nginx is not allowed because otherwise you would loose + * access to the web interface + */ + public function stopAction() + { + return array('status' => 'failed'); + } + + + /** + * reconfigure with optional stop, generate config and start / reload + * @return array response message + * @throws \Exception when configd action fails + * @throws \ReflectionException when model can't be instantiated + */ + public function reconfigureAction() + { + if ($this->request->isPost()) { + $this->sessionClose(); + $model = $this->getModel(); + $backend = new Backend(); + if ($this->reconfigureForceRestart()) { + $backend->configdRun('nginx stop'); + } + $backend->configdRun('template reload OPNsense/Nginx'); + $runStatus = $this->statusAction(); + if ($runStatus['status'] != 'running') { + $backend->configdRun('nginx start'); + } else { + $backend->configdRun('nginx reload'); + } + return array('status' => 'ok'); + } else { + return array('status' => 'failed'); + } + } + + /** + * retrieve status of service + * @return array response message + * @throws \Exception when configd action fails + */ + public function statusAction() + { + $backend = new Backend(); + $model = $this->getModel(); + $response = $backend->configdRun('nginx status'); + + if (strpos($response, 'not running') > 0) { + $status = 'stopped'; + } elseif (strpos($response, 'is running') > 0) { + $status = 'running'; + } else { + $status = 'unknown'; + } + + return array('status' => $status); + } +} 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 new file mode 100644 index 000000000..28f5eb716 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php @@ -0,0 +1,281 @@ +searchBase('userlist', array('name', 'users')); + } + + public function getuserlistAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('userlist', 'userlist', $uuid); + } + + public function adduserlistAction() + { + return $this->addBase('userlist', 'userlist'); + } + + public function deluserlistAction($uuid) + { + return $this->delBase('userlist', $uuid); + } + + public function setuserlistAction($uuid) + { + return $this->setBase('userlist', 'userlist', $uuid); + } + + // Credential + public function searchcredentialAction() + { + return $this->searchBase('credential', array('username')); + } + + public function getcredentialAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('credential', 'credential', $uuid); + } + + public function addcredentialAction() + { + return $this->addBase('credential', 'credential'); + } + + public function delcredentialAction($uuid) + { + return $this->delBase('credential', $uuid); + } + + public function setcredentialAction($uuid) + { + return $this->setBase('credential', 'credential', $uuid); + } + + // Upstream + public function searchupstreamAction() + { + return $this->searchBase('upstream', array('description', 'serverentries')); + } + + public function getupstreamAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('upstream', 'upstream', $uuid); + } + + public function addupstreamAction() + { + return $this->addBase('upstream', 'upstream'); + } + + public function delupstreamAction($uuid) + { + return $this->delBase('upstream', $uuid); + } + + public function setupstreamAction($uuid) + { + return $this->setBase('upstream', 'upstream', $uuid); + } + + // Upstream Server + public function searchupstreamserverAction() + { + return $this->searchBase('upstream_server', array('description', 'server', 'priority')); + } + + public function getupstreamserverAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('upstream_server', 'upstream_server', $uuid); + } + + public function addupstreamserverAction() + { + return $this->addBase('upstream_server', 'upstream_server'); + } + + public function delupstreamserverAction($uuid) + { + return $this->delBase('upstream_server', $uuid); + } + + public function setupstreamserverAction($uuid) + { + return $this->setBase('upstream_server', 'upstream_server', $uuid); + } + + // Location + public function searchlocationAction() + { + return $this->searchBase('location', array('description','urlpattern', 'matchtype', 'enable_secrules', 'force_https')); + } + + public function getlocationAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('location', 'location', $uuid); + } + + public function addlocationAction() + { + return $this->addBase('location', 'location'); + } + + public function dellocationAction($uuid) + { + return $this->delBase('location', $uuid); + } + + public function setlocationAction($uuid) + { + return $this->setBase('location', 'location', $uuid); + } + + // Custom Policy + public function searchcustompolicyAction() + { + return $this->searchBase('custom_policy', array('name', 'operator', 'value', 'action')); + } + + public function getcustompolicyAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('custompolicy', 'custom_policy', $uuid); + } + + public function addcustompolicyAction() + { + return $this->addBase('custompolicy', 'custom_policy'); + } + + public function delcustompolicyAction($uuid) + { + return $this->delBase('custom_policy', $uuid); + } + + public function setcustompolicyAction($uuid) + { + return $this->setBase('custompolicy', 'custom_policy', $uuid); + } + + // http server + public function searchhttpserverAction() + { + return $this->searchBase('http_server', array('servername', 'https_only', 'certificate', 'listen_http_port', 'listen_https_port')); + } + + public function gethttpserverAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('httpserver', 'http_server', $uuid); + } + + public function addhttpserverAction() + { + return $this->addBase('httpserver', 'http_server'); + } + + public function delhttpserverAction($uuid) + { + return $this->delBase('http_server', $uuid); + } + + public function sethttpserverAction($uuid) + { + return $this->setBase('httpserver', 'http_server', $uuid); + } + + // naxsi rules + public function searchnaxsiruleAction() + { + return $this->searchBase('naxsi_rule', array('description', 'ruletype', 'message')); + } + + public function getnaxsiruleAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('naxsi_rule', 'naxsi_rule', $uuid); + } + + public function addnaxsiruleAction() + { + return $this->addBase('naxsi_rule', 'naxsi_rule'); + } + + public function delnaxsiruleAction($uuid) + { + return $this->delBase('naxsi_rule', $uuid); + } + + public function setnaxsiruleAction($uuid) + { + return $this->setBase('naxsi_rule', 'naxsi_rule', $uuid); + } + + // http url rewriting + public function searchhttprewriteAction() + { + return $this->searchBase('http_rewrite', array('description', 'source', 'destination', 'flag')); + } + + public function gethttprewriteAction($uuid = null) + { + $this->sessionClose(); + return $this->getBase('httprewrite', 'http_rewrite', $uuid); + } + + public function addhttprewriteAction() + { + return $this->addBase('httprewrite', 'http_rewrite'); + } + + public function delhttprewriteAction($uuid) + { + return $this->delBase('http_rewrite', $uuid); + } + + public function sethttprewriteAction($uuid) + { + return $this->setBase('httprewrite', 'http_rewrite', $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 new file mode 100644 index 000000000..d6a445284 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php @@ -0,0 +1,54 @@ +view->settings = $this->getForm("settings"); + $this->view->upstream_server = $this->getForm("upstream_server"); + $this->view->upstream = $this->getForm("upstream"); + $this->view->location = $this->getForm("location"); + $this->view->credential = $this->getForm("credential"); + $this->view->userlist = $this->getForm("userlist"); + $this->view->httpserver = $this->getForm("httpserver"); + $this->view->httprewrite = $this->getForm("httprewrite"); + $this->view->naxsi_rule = $this->getForm("naxsi_rule"); + $this->view->naxsi_custom_policy = $this->getForm("naxsi_custom_policy"); + $this->view->pick('OPNsense/Nginx/index'); + } +} diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/credential.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/credential.xml new file mode 100644 index 000000000..a014c5708 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/credential.xml @@ -0,0 +1,12 @@ +
+ + credential.username + + text + + + credential.password + + text + +
diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httprewrite.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httprewrite.xml new file mode 100644 index 000000000..eda6fc20c --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httprewrite.xml @@ -0,0 +1,27 @@ +
+ + httprewrite.description + + text + Enter a short description like a name for this redirect. + + + httprewrite.source + + text + Enter a regular expression to match the URL + + + httprewrite.destination + + text + You may use replacement strings like $1 for the first match group from the source here. + + + httprewrite.flag + + dropdown + + Stop rule processing (break) and perform (internal) redirect (last). Return a moved permanently status code (301) or a teporary redirect (302). + +
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 new file mode 100644 index 000000000..1a455d735 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml @@ -0,0 +1,92 @@ +
+ + httpserver.listen_http_port + + text + + + httpserver.listen_https_port + + text + + + httpserver.servername + + true + + select_multiple + + + httpserver.locations + + + select_multiple + + + httpserver.rewrites + + select_multiple + + Choose URL rewriting rules. + + + httpserver.root + + text + + + httpserver.certificate + + dropdown + + + httpserver.ca + + dropdown + + + httpserver.verify_client + + dropdown +
  • 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.
  • ]]>
    +
    + + httpserver.access_log_format + + dropdown + + + httpserver.enable_acme_support + + checkbox + + + httpserver.charset + + dropdown + + + httpserver.https_only + + checkbox + If you check this box, a TLS encrypted connection is enforced. + + + httpserver.block_nonpublic_data + + checkbox + Blocks files like .htaccess files or other files not intended for the public. + + + httpserver.naxsi_extensive_log + + checkbox + Provide a more verbose WAF log for fixing false positives before going live. + + + httpserver.sendfile + + checkbox + 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 new file mode 100644 index 000000000..cc41d9c8a --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml @@ -0,0 +1,106 @@ +
    + + location.description + + text + + + location.urlpattern + + text + The URL pattern to match. + + + location.matchtype + + dropdown + Choose how to match on "URL Pattern". + + + location.rewrites + + select_multiple + + Choose URL rewriting rules. + + + location.enable_secrules + + checkbox + Enable WAF + + + location.enable_learning_mode + + checkbox + Enable learning mode means nothing is blocked but logged. + + + location.xss_block_score + + text + Block XSS with a score above the specified value using generic detection. + + + location.sqli_block_score + + text + Block SQL injection with a score above the specified value using generic detection. + + + location.custom_policy + + select_multiple + + Select custom security policies. + + + location.upstream + + dropdown + + Select an upstream to proxy to. + + + location.root + + text + Enter the file system root from which the files are served. + + + location.index + + select_multiple + Enter a list of file extensions, which are served instead of a directory. It is common to use index.html or index.php here. + + + location.autoindex + + checkbox + Check this to serve a file list when a directory is requested. This is useful for serving multiple files with someone without a web application. It is recommended to use this option only with authentication. + + + location.authbasic + + text + Enter the realm for this directory and enable authentication. + + + location.authbasicuserfile + + dropdown + Select a credential list to use. + + + location.advanced_acl + + checkbox + Send an authentication request to the OPNsense backend for advanced access control. + + + location.force_https + + checkbox + Force encrypted connections. + +
    diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/naxsi_custom_policy.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/naxsi_custom_policy.xml new file mode 100644 index 000000000..26f6fe026 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/naxsi_custom_policy.xml @@ -0,0 +1,31 @@ +
    + + custompolicy.name + text + + + + custompolicy.naxsi_rules + select_multiple + true + + + + + custompolicy.value + text + + + + custompolicy.operator + dropdown + + + + + custompolicy.action + dropdown + + + +
    diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/naxsi_rule.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/naxsi_rule.xml new file mode 100644 index 000000000..cdaec08a4 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/naxsi_rule.xml @@ -0,0 +1,110 @@ +
    + + naxsi_rule.description + + text + + + naxsi_rule.message + + text + + + naxsi_rule.negate + + checkbox + If this is box is checked, the score will be added if the rule does not match. Use with care. + + + naxsi_rule.identifier + + text + + + naxsi_rule.ruletype + + dropdown + + + naxsi_rule.regex + + checkbox + + + naxsi_rule.match_value + + text + + + naxsi_rule.match_type + + dropdown + + + + naxsi_rule.args + + checkbox + + + naxsi_rule.url + + checkbox + + + naxsi_rule.headers + + checkbox + + + naxsi_rule.body + + checkbox + + + naxsi_rule.name + + checkbox + Check this box to match the variable name and not its content when matching any of the above checkboxes. + + + naxsi_rule.file_extension + + checkbox + + + naxsi_rule.raw_body + + checkbox + + + naxsi_rule.dollar_url + + text + Enter the name of a parameter to match. + + + naxsi_rule.dollar_args_var + + text + Enter the name of a parameter to match. + + + naxsi_rule.dollar_body_var + + text + Enter the name of a variable in the HTTP body to match. + + + naxsi_rule.dollar_headers_var + + text + Enter the name of an HTTP header to match. + + + naxsi_rule.score + + text + + +
    diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml new file mode 100644 index 000000000..94100b302 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml @@ -0,0 +1,61 @@ +
    + + + + nginx.general.enabled + + checkbox + Enable or disable the nginx service. + + + + + nginx.http.enabled + + checkbox + Enable sendfile support (faster). + + + nginx.http.keepalive_timeout + + text + After this idle time, the client gets disconnected. + + + nginx.http.default_type + + text + This content type is sent if the file extension is unknown. + + + + + nginx.webgui.limitnetworks + + checkbox + Limit networks to directly connected networks. Enabling this option is recommended if your nginx instance is reachable via the internet to prevent remote access to the web interface for security reasons. Please note that you can lock yourself out if you are accessing OPNsense via a router without SNAT. + + + + + nginx-general-settings +
    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 new file mode 100644 index 000000000..4b8092077 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml @@ -0,0 +1,13 @@ +
    + + upstream.description + + text + + + upstream.serverentries + + + select_multiple + +
    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 new file mode 100644 index 000000000..655ad6c3d --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream_server.xml @@ -0,0 +1,42 @@ +
    + + upstream_server.description + + text + + + upstream_server.server + + text + + + upstream_server.port + + text + + + upstream_server.priority + + text + + + upstream_server.max_conns + + text + + + upstream_server.max_fails + + text + + + upstream_server.fail_timeout + + text + + + upstream_server.no_use + + dropdown + +
    diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/userlist.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/userlist.xml new file mode 100644 index 000000000..32dc6d074 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/userlist.xml @@ -0,0 +1,12 @@ +
    + + userlist.name + + text + + + userlist.users + + select_multiple + +
    diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/ACL/ACL.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/ACL/ACL.xml new file mode 100644 index 000000000..8793dd4bd --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + nginx + + ui/nginx/* + api/nginx/* + + + 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 new file mode 100644 index 000000000..3c4c31b12 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.php new file mode 100644 index 000000000..cfed5580e --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.php @@ -0,0 +1,34 @@ + + //OPNsense/Nginx + nginx web server, reverse proxy and waf + + + + 0 + Y + + + + + + 0 + Y + + + + + + 0 + N + + + 60 + N + + + N + + + + + + Y + + + + + + Selected user not found + Y + Y + + + + + + Y + + + Y + + + + + + Y + + + + + + Selected server not found + Y + Y + + + + + + Y + + + Y + + + Y + + + 0 + Y + + + N + + + N + + + N + + + + Permanently Unreachable + Backup Server + + N + + + + + + Y + + + Y + + + + Exact Match ("=") + Case Sensitive Match ("~") + Case Insensitive Match ("~*") + Don't check regular expressions on logest prefix match ("^~") + + N + + + 0 + Y + + + 0 + Y + + + N + + + N + + + + + + Selected server not found + N + Y + + + + + + Selected upstream not found + N + N + + + N + + + + + + Selected rewrite(s) not found + N + Y + + + N + + + N + + + N + + + + + + Selected server not found + N + Y + + + 0 + Y + + + N + + + + + + Y + + + + + + Selected rule not found + Y + Y + + + Y + + + Y + >= + + Bigger or Equal + Bigger + Lesser + Lesser or Equal + Equal + + + + Y + BLOCK + + Block Request + Allow Request + Drop The Connection + Log Request + + + + + + + Y + + + +
    Main Rule
    + Basic Rule +
    + Y +
    + + Y + /^[^"]+$/ + + + Y + 1000 + + + N + /^[^"]+$/ + + + N + /^[^"]+$/ + + + Y + /^[^"]+$/ + + + Y + id + + Blacklist + Whitelist + + + + Y + + + Y + 8 + + + Y + + + Y + + + Y + + + /^[^"]+$/ + + + /^[^"]+$/ + + + /^[^"]+$/ + + + Y + + + Y + + + Y + +
    + + + + Y + + + N + 80 + + + N + 443 + + + + + + Selected location(s) not found + N + Y + + + + + + Selected rewrite(s) not found + N + Y + + + N + + + cert + N + + + ca + N + + + Off + + Off + On + Optional + Optional, don't verify + + Y + + + main + +
    Default
    + Anonymized + Disabled +
    + Y +
    + + Y + 1 + + + utf-8 + + utf-8 + + N + + + 0 + Y + + + 0 + Y + + + 0 + Y + + + 1 + Y + +
    + + + + Y + /^[^" \t]+$/i + + + Y + + + Y + /^[^" \t]+$/i + + + + Stop processing rules + Stop processing rules and find location + Redirect + Permanent + + N + + + +
    + 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 new file mode 100644 index 000000000..b1d8109f9 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt @@ -0,0 +1,382 @@ +{# + # 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. + #} + + + + + + +
    + {{ partial("layout_partials/base_tabs_content",['formData':settings]) }} +
    + + + + + + + + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('URL Pattern') }}{{ lang._('Match Type') }}{{ lang._('WAF Enabled') }}{{ lang._('Force HTTPS') }}{{ lang._('Commands') }}
    + + +
    +
    + +
    + + + + + + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('Server') }}{{ lang._('Priority') }}{{ lang._('Commands') }}
    + + +
    +
    + + +
    + + + + + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('Servers') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + +
    {{ lang._('Username') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + + +
    {{ lang._('Name') }}{{ lang._('Users') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + +
    {{ lang._('Servername') }}{{ lang._('HTTPS Only') }}{{ lang._('Certificate') }}{{ lang._('HTTP Port') }}{{ lang._('HTTPS Port') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('Source URL') }}{{ lang._('Destination URL') }}{{ lang._('Flag') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + +
    {{ lang._('Name') }}{{ lang._('Operator') }}{{ lang._('Value') }}{{ lang._('Action') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('Rule Type') }}{{ lang._('Message') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + +{{ partial("layout_partials/base_dialog",['fields': upstream,'id':'upstreamdlg', 'label':lang._('Edit Upstream')]) }} +{{ partial("layout_partials/base_dialog",['fields': upstream_server,'id':'upstreamserverdlg', 'label':lang._('Edit Upstream')]) }} +{{ partial("layout_partials/base_dialog",['fields': location,'id':'locationdlg', 'label':lang._('Edit Location')]) }} +{{ partial("layout_partials/base_dialog",['fields': credential,'id':'credentialdlg', 'label':lang._('Edit Credential')]) }} +{{ partial("layout_partials/base_dialog",['fields': userlist,'id':'userlistdlg', 'label':lang._('Edit User List')]) }} +{{ partial("layout_partials/base_dialog",['fields': httpserver,'id':'httpserverdlg', 'label':lang._('Edit HTTP Server')]) }} +{{ partial("layout_partials/base_dialog",['fields': httprewrite,'id':'httprewritedlg', 'label':lang._('Edit URL Rewrite')]) }} +{{ partial("layout_partials/base_dialog",['fields': naxsi_custom_policy,'id':'custompolicydlg', 'label':lang._('Edit WAF Policy')]) }} +{{ partial("layout_partials/base_dialog",['fields': naxsi_rule,'id':'naxsiruledlg', 'label':lang._('Edit Naxsi Rule')]) }} diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_auth.php b/www/nginx/src/opnsense/scripts/nginx/ngx_auth.php new file mode 100644 index 000000000..a1e166a17 --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_auth.php @@ -0,0 +1,55 @@ +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; + return password_auth_test($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $auth_server); +} + +if (password_auth()) { + header("HTTP/1.1 200 OK"); +} else { + header("HTTP/1.1 401 Authorization Required"); + header('WWW-Authenticate: Basic realm="OPNsense Protected Area - Authentication Required"'); +} diff --git a/www/nginx/src/opnsense/scripts/nginx/setup.php b/www/nginx/src/opnsense/scripts/nginx/setup.php new file mode 100755 index 000000000..7cb8592ae --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/setup.php @@ -0,0 +1,105 @@ +#!/usr/local/bin/php +userlist->__items as $user_list) { + $attributes = $user_list->getAttributes(); + $uuid = $attributes['uuid']; + $file = null; + try { + $file = fopen("/var/db/nginx/auth/" . $uuid, "wb"); + $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 { + if (isset($file)) { + fclose($file); + } + unset($file); + } +} 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 new file mode 100644 index 000000000..afe983676 --- /dev/null +++ b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf @@ -0,0 +1,27 @@ +[start] +command:/usr/local/opnsense/scripts/nginx/setup.php;/usr/local/etc/rc.d/php-fpm start;/usr/local/etc/rc.d/nginx start +parameters: +type:script +message:starting nginx + +[stop] +command:/usr/local/etc/rc.d/nginx stop +parameters: +type:script +message:stopping nginx + +[restart] +command:/usr/local/opnsense/scripts/nginx/setup.php;/usr/local/etc/rc.d/php-fpm restart;/usr/local/etc/rc.d/nginx restart +parameters: +type:script +message:restarting nginx + +[status] +command:/usr/local/etc/rc.d/nginx status;exit 0 +parameters: +type:script_output + +[phpstatus] +command:/usr/local/etc/rc.d/php-fpm status; exit 0 +parameters: +type:script_output diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/+TARGETS b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/+TARGETS new file mode 100644 index 000000000..320f535db --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/+TARGETS @@ -0,0 +1,7 @@ +nginx:/etc/rc.conf.d/nginx +nginx.conf:/usr/local/etc/nginx/nginx.conf +webgui.conf:/usr/local/etc/nginx/nginx_web.conf +mime.types:/usr/local/etc/nginx/mime.types +php_fpm:/etc/rc.conf.d/php_fpm +php-www.conf:/usr/local/etc/php-fpm.d/www.conf +php-webgui.conf:/usr/local/etc/php-fpm.d/webgui.conf diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf new file mode 100644 index 000000000..7cf830c59 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf @@ -0,0 +1,177 @@ +include mime.types; + +{% include "OPNsense/Nginx/ruleset.conf" ignore missing with context %} + + +log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; +log_format anonymized ':: - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + +#tcp_nopush on; + +# 200M should be big enough for file servers etc. +client_max_body_size 200M; +brotli_static on; +brotli on; +gzip_static on; +gzip on; +server_tokens off; +sendfile {% if OPNsense.Nginx.http.sendfile is defined and OPNsense.Nginx.http.sendfile == '1' %}On{% else %}Off{% endif %}; +{% if OPNsense.Nginx.http.default_type is defined and OPNsense.Nginx.http.default_type != '' %} +default_type {{ OPNsense.Nginx.http.default_type }}; +{% else %} +default_type application/octet-stream; +{% endif %} +{% if OPNsense.Nginx.http.keepalive_timeout is defined and OPNsense.Nginx.http.keepalive_timeout != '' %} +keepalive_timeout {{ OPNsense.Nginx.http.keepalive_timeout }}; +{% endif %} + +#add_header X-Frame-Options SAMEORIGIN; +#add_header X-Content-Type-Options nosniff; +#add_header X-XSS-Protection "1; mode=block"; +#add_header Referrer-Policy "same-origin"; + +# TODO add when core is ready for allowing nginx to serve the web interface +# include nginx_web.conf; + +{% include "OPNsense/Nginx/upstream.conf" ignore missing with context %} + +{% set listen_list = [] %} +{% for server in helpers.toList('OPNsense.Nginx.http_server') %} +{% set single_servername = server.servername.split(",")[0] %} +server { +{% if server.listen_http_port is defined %} + listen [::]:{{ server.listen_http_port }}{% if server.listen_https_port not in listen_list%} ipv6only=off{% endif %}; +{% do listen_list.append(server.listen_http_port) %} +{% endif %} +{% if server.listen_https_port is defined and server.certificate is defined %} + listen [::]:{{ server.listen_https_port }}{% if server.listen_https_port not in listen_list%} ipv6only=off{% endif %} http2 ssl; +{% do listen_list.append(server.listen_https_port) %} +{% if server.ca is defined %} + ssl_client_certificate /usr/local/etc/nginx/key/{{ single_servername }}_ca.pem; + ssl_verify_client {{ server.verify_client }}; +{% endif %} + ssl_certificate_key /usr/local/etc/nginx/key/{{ single_servername }}.key; + ssl_certificate /usr/local/etc/nginx/key/{{ single_servername }}.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_dhparam /usr/local/etc/dh-parameters.4096; + ssl_ciphers 'ECDHE-ECDSA-CAMELLIA256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CAMELLIA256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CAMELLIA128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CAMELLIA128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:ECDHE-RSA-AES128-SHA256'; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_session_tickets off; + ssl_prefer_server_ciphers on; + add_header Strict-Transport-Security max-age=15768000; + sendfile {% if server.sendfile is defined and server.sendfile == '1' %}On{% else %}Off{% endif %}; + proxy_set_header X-TLS-Cipher $ssl_cipher; + proxy_set_header X-TLS-Protocol $ssl_protocol; + proxy_set_header X-TLS-SNI-Host $ssl_server_name; +{% endif %} + # proxy headers for backend server +{% if server.verify_client != 'off' %} + proxy_set_header X-Client-Dn $ssl_client_s_dn; + proxy_set_header X-Client-Verify $ssl_client_verify; +{% endif %} +{% if server.verify_client == 'optional_no_ca' %} + proxy_set_header X-Client-Certificate $ssl_client_escaped_cert; +{% endif %} + proxy_set_header Host $host; + 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 }}; + charset {{ server.charset }}; + access_log /var/log/nginx/{{ server.servername }}.access.log {{ server.access_log_format }}; + #include tls.conf; + error_page 404 /opnsense_error_404.html; + error_page 500 501 502 503 504 /opnsense_server_error.html; + # location to ban the host permanently + set $naxsi_extensive_log {% if server.naxsi_extensive_log is defined and server.naxsi_extensive_log == '1' %}1{% else %}0{% endif %}; + location @permanentban { + access_log /var/log/nginx/permanentban.access.log main; + internal; + add_header Content-Type text/plain; + add_header Charset utf-8; + return 403 "You got banned permanently from this server."; + } + error_page 418 = @permanentban; + location /opnsense_server_error.html { + internal; + root /usr/local/etc/nginx/views; + } + location /opnsense_error_404.html { + internal; + root /usr/local/etc/nginx/views; + } + location /waf_denied.html { + root /usr/local/etc/nginx/views; + access_log /var/log/nginx/waf_denied.access.log main; + } +{% if server.enable_acme_support is defined and server.enable_acme_support == '1' %} + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + root /var/etc/acme-client/challenges; + } +{% 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) { + return 418; + } + if ($http_user_agent ~ "Indy\sLibrary|Morfeus Fucking Scanner") + { + return 418; + } + location /opnsense-auth-request { + internal; + fastcgi_pass unix:/var/run/php-webgui.socket; + fastcgi_index index.php; + fastcgi_param TLS-Cipher $ssl_cipher; + fastcgi_param TLS-Protocol $ssl_protocol; + 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 SCRIPT_FILENAME /usr/local/opnsense/scripts/nginx/ngx_auth.php; + fastcgi_intercept_errors on; + include fastcgi_params; + } +{% if server.block_nonpublic_data is defined and server.block_nonpublic_data == '1' %} + # apache htpasswd and htaccess + location ~ /\.ht { + return 403; + } + # those files may expose file system stuff + location ~ \.DS_Store$ { + return 403; + } +{% endif %} +{% if server.https_only is defined and server.https_only == '1' %} + if ($scheme != "https") { + return 302 https://$host$request_uri; + } +{% endif %} +{% if server.rewrites is defined %} +{% for rewrite_uuid in server.rewrites.split(',') %} +{% set rewrite = helpers.getUUID(rewrite_uuid) %} + rewrite {{ rewrite.source }} {{ rewrite.destination }}{% if rewrite.flag is defined%} {{ rewrite.flag }}{% endif %}; +{% endfor %} +{% endif %} + +{% if server.locations is defined %} +{% for location_uuid in server.locations.split(',') %} +{% set location = helpers.getUUID(location_uuid) %} +{% include "OPNsense/Nginx/location.conf" ignore missing with context %} +{% endfor %} +{% endif %} + +} + +{% endfor %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf new file mode 100644 index 000000000..80401f2c7 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf @@ -0,0 +1,62 @@ + +location {{ location.matchtype }} {{ location.urlpattern }} { +{% if location.enable_secrules is defined and location.enable_secrules == '1' %} + SecRulesEnabled; +{% endif %} +{% if location.enable_learning_mode is defined and location.enable_learning_mode == '1' %} + LearningMode; +{% endif %} +{% if location.xss_block_score is defined %} + LibInjectionXss; + CheckRule "$LIBINJECTION_XSS >= {{ location.xss_block_score }}" BLOCK; +{% endif %} +{% set added_policies = [] %} +{% if location.custom_policy is defined %} +{% for custom_policy_uuid in location.custom_policy.split(',') %} +{% set custom_policy = helpers.getUUID(custom_policy_uuid) %} +{% set naxsi_ruletype = 'basic' %} +{% include "OPNsense/Nginx/naxsirule.conf" ignore missing with context %} + CheckRule "$policy{{ custom_policy_uuid.replace('-', '') }} {{ custom_policy.operator }} {{ custom_policy.value + }}" {{ custom_policy.action }}; +{% endfor %} +{% endif %} +{% if location.rewrites is defined %} +{% for rewrite_uuid in location.rewrites.split(',') %} +{% set rewrite = helpers.getUUID(rewrite_uuid) %} + rewrite {{ rewrite.source }} {{ rewrite.destination }}{% if rewrite.flag is defined%} {{ rewrite.flag }}{% endif %}; +{% endfor %} +{% endif %} +{% if location.sqli_block_score is defined %} + LibInjectionSql; + CheckRule "$LIBINJECTION_SQL >= {{ location.sqli_block_score }}" BLOCK; +{% endif %} + DeniedUrl "/waf_denied.html"; +{% if location.force_https is defined and location.force_https == '1' %} + if ($scheme != "https") { + return 302 https://$host$request_uri; + } +{% endif %} +{% if location.root is defined %} + root {{ location.root }}; +{% endif %} +{% if location.index is defined %} + index {{ location.index.replace(",", " ") }}; +{% endif %} +{% if location.autoindex is defined and location.autoindex == '1' %} + autoindex on; +{% else %} + autoindex off; +{% endif %} +{% if location.authbasic is defined and location.authbasicuserfile is defined %} + auth_basic "{{location.authbasic}}"; + auth_basic_user_file /var/db/nginx/auth/{{ location.authbasicuserfile }}; +{% else %} +{% if location.advanced_acl is defined and location.advanced_acl == '1' %} + auth_request /opnsense-auth-request; +{% endif %} +{% endif %} +{% if location.upstream is defined %} + proxy_pass http://upstream{{ location.upstream.replace('-','') }}; +{% endif %} + +} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/mime.types b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/mime.types new file mode 100644 index 000000000..bf1b4c4f9 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/mime.types @@ -0,0 +1,100 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + application/font-woff woff; + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + application/gzip gz; + application/xz xz; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg oga; + audio/opus opus + audio/speex spx; + audio/x-m4a m4a; + audio/x-realaudio ra; + audio/flac flac; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/ogg ogv; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/naxsirule.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/naxsirule.conf new file mode 100644 index 000000000..bc543f3fa --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/naxsirule.conf @@ -0,0 +1,62 @@ +{% macro naxsi_mzhelper(mz_helper_rule) -%} +{% set mz_matches = [] %} +{% set rx_suffix = '' %} +{% if mz_helper_rule.regex is defined and mz_helper_rule.regex == '1' %} +{% set rx_suffix = '_X' %} +{% endif %} +{% if mz_helper_rule.args == '1' %} +{% do mz_matches.append('ARGS') %} +{% endif %} +{% if mz_helper_rule.headers == '1' %} +{% do mz_matches.append('HEADERS') %} +{% endif %} +{% if mz_helper_rule.name == '1' %} +{% if mz_matches|length > 0 %} +{% do mz_matches.append('NAME') %} +{% endif %} +{% endif %} +{% if mz_helper_rule.raw_body == '1' %} +{% do mz_matches.append('RAW_BODY') %} +{% endif %} +{% if mz_helper_rule.file_extension == '1' %} +{% do mz_matches.append('FILE_EXT') %} +{% endif %} +{% if mz_helper_rule.dollar_body_var is defined and mz_helper_rule.dollar_body_var != '' %} +{% do mz_matches.append('$BODY_VAR' + rx_suffix + ':' + mz_helper_rule.dollar_body_var) %} +{% endif %} +{% if mz_helper_rule.dollar_args_var is defined and mz_helper_rule.dollar_args_var != '' %} +{# in case of regex, we cannot use args_var -> https://github.com/nbs-system/naxsi/wiki/matchzones-bnf#match-zone #} +{% if mz_helper_rule.url is defined and mz_helper_rule.url != '' %} +{% do mz_matches.append('$ARGS_VAR' + rx_suffix + ':' + mz_helper_rule.dollar_args_var) %} +{% endif %} +{% endif %} +{% if mz_helper_rule.dollar_headers_var is defined and mz_helper_rule.dollar_headers_var != '' %} +{% do mz_matches.append('$HEADERS_VAR' + rx_suffix +':' + mz_helper_rule.dollar_headers_var) %} +{% endif %} +{% if mz_helper_rule.dollar_url is defined and mz_helper_rule.dollar_url != '' %} +{% do mz_matches.insert(0,'$URL' + rx_suffix +':' + mz_helper_rule.dollar_url) %} +{% endif %} +{{ mz_matches|join('|') }} +{%- endmacro %} +{% macro naxsi_rule(uuid, rule, ruletype) -%} + {{ ruletype }}{% if rule.negate is defined and rule.negate == '1' %} negative{% endif + %} {{ rule.match_type }}:{{ rule.identifier }} "{% if rule.regex == '1' %}rx{% else %}str{% endif + %}:{{ rule.match_value }}" "msg:{{ rule.message }}" "mz:{{ naxsi_mzhelper(rule) + }}" "s:$policy{{ uuid.replace('-', '') }}:{{ rule.score }}"; +{%- endmacro %} + +{% if naxsi_ruletype == 'basic' %} +{# current policy in loop is available as custom_policy, the uuid as custom_policy_uuid #} +{% for naxsi_rule_uuid in custom_policy.naxsi_rules.split(',') %} +{% if naxsi_rule_uuid not in added_policies %} +{% set basic_rule = helpers.getUUID(naxsi_rule_uuid) %} +{% if basic_rule.ruletype == 'basic' %} +{{ naxsi_rule(custom_policy_uuid, basic_rule, "BasicRule") }} +{% do added_policies.append(naxsi_rule_uuid) %} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} +{% if naxsi_ruletype == 'main' %} +{{ naxsi_rule(custom_policy_uuid, main_rule, "MainRule") }} +{% endif %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx new file mode 100644 index 000000000..598e6444f --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx @@ -0,0 +1,2 @@ +nginx_enable="YES" +nginx_var_script="/usr/local/opnsense/scripts/nginx/setup.php" diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf new file mode 100644 index 000000000..480fe8fd6 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf @@ -0,0 +1,29 @@ +{# load naxsi WAF module #} +load_module /usr/local/libexec/nginx/ngx_stream_module.so; +load_module /usr/local/libexec/nginx/ngx_http_naxsi_module.so; +load_module /usr/local/libexec/nginx/ngx_mail_module.so; +load_module /usr/local/libexec/nginx/ngx_http_brotli_filter_module.so; +load_module /usr/local/libexec/nginx/ngx_http_brotli_static_module.so; + +# TODO enable root when running the web interface +#user root wheel; +worker_processes 1; + +error_log /var/log/nginx/error.log; + +events { + worker_connections 1024; +} + +http { +{% if helpers.exists('OPNsense.Nginx') %} +{# include http blocks partial #} +{% include "OPNsense/Nginx/http.conf" ignore missing with context %} +{% endif %} +} +{% if helpers.exists('OPNsense.Nginx') %} +# mail { +{# include http blocks partial #} +{% include "OPNsense/Nginx/mail.conf" ignore missing with context %} +# } +{% endif %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php-webgui.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php-webgui.conf new file mode 100644 index 000000000..3ed5154ac --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php-webgui.conf @@ -0,0 +1,16 @@ +{% raw %} +[webgui] +user = root +group = wheel +listen = /var/run/php-webgui.socket +listen.owner = root +listen.group = wheel +listen.mode = 0660 +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +php_admin_value[error_log] = /var/log/fpm-php.www.log +php_admin_flag[log_errors] = on +{% endraw %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php-www.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php-www.conf new file mode 100644 index 000000000..30547aacd --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php-www.conf @@ -0,0 +1,11 @@ +{% raw %} +[www] +user = www +group = www +listen = /var/run/php-www.socket +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +{% endraw %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php_fpm b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php_fpm new file mode 100644 index 000000000..1f644f9c3 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/php_fpm @@ -0,0 +1,2 @@ +php_fpm_enable="YES" +command_args="-R" diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/ruleset.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/ruleset.conf new file mode 100644 index 000000000..d08db7a21 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/ruleset.conf @@ -0,0 +1,29 @@ +{% set naxsi_ruletype = 'main' %} +{% set main_policies = [] %} +{% set main_rules = [] %} +{# collect custom policy UUIDs from locations so we know, which ones are probably in use #} +{% if OPNsense.Nginx.location is defined %} +{% for location in helpers.toList('OPNsense.Nginx.location') %} +{% if location.custom_policy is defined and location.custom_policy != ""%} +{% for custompolicy_uuid in location.custom_policy.split(',') %} +{% if not custompolicy_uuid in main_policies %} +{% do main_policies.append(custompolicy_uuid) %} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} +{% for custom_policy_uuid in main_policies %} +{% set custom_policy = helpers.getUUID(custom_policy_uuid) %} +{% if custom_policy.naxsi_rules is defined %} +{% for main_rule_uuid in custom_policy.naxsi_rules.split(',') %} +{% if main_rule_uuid not in main_rules %} +{% do main_rules.append(main_rule_uuid) %} +{% set main_rule = helpers.getUUID(main_rule_uuid) %} +{% if main_rule.ruletype == 'main' %} +{% include "OPNsense/Nginx/naxsirule.conf" ignore missing with context %} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/upstream.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/upstream.conf new file mode 100644 index 000000000..d1fb7db35 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/upstream.conf @@ -0,0 +1,22 @@ +# UPSTREAM SERVERS +{% set upstreamlist = {} %} +{% for location in helpers.toList('OPNsense.Nginx.location') %} +{% if location.upstream is defined %} +{% do upstreamlist.update({location.upstream: helpers.getUUID(location.upstream)}) %} +{% endif %} +{% endfor %} +{% for upstream_uuid, upstream in upstreamlist.items() %} +{% for upstream_serveruuid in upstream.serverentries.split(',') %} +upstream upstream{{ upstream_uuid.replace('-','') }} { +{% set upstream_server = helpers.getUUID(upstream_serveruuid) %} +server {% if ':' in upstream_server.server %}[{% endif %}{{ upstream_server.server }}{% if ':' in upstream_server.server %}]{% endif + %}{% if upstream_server.port is defined %}:{{ upstream_server.port }}{% endif + %}{% if upstream_server.priority is defined %} weight={{ upstream_server.priority }}{% endif + %}{% if upstream_server.max_conns is defined %} max_conns={{ upstream_server.max_conns }}{% endif + %}{% if upstream_server.max_fails is defined %} max_fails={{ upstream_server.max_fails }}{% endif + %}{% if upstream_server.fail_timeout is defined %} fail_timeout={{ upstream_server.fail_timeout }}{% endif + %}{% if upstream_server.no_use is defined %} {{ upstream_server.no_use }}{% endif %}; +{% endfor %} + +} +{% endfor %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf new file mode 100644 index 000000000..dd6649bf6 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf @@ -0,0 +1,205 @@ +server { + + keepalive_requests 15; + keepalive_timeout 30; + + root /usr/local/www/; +{% if system.webgui.protocol is defined and system.webgui.protocol == 'https' %} + if ($scheme != "https") { + return 302 https://$host$request_uri; + } + listen 80 default_server; # if redirect is enabled + listen {% if system.webgui.port is defined and system.webgui.port != '' %}{{ system.webgui.port }}{% else %}443{% endif %} ssl http2 default_server; + ## TLS configuration + ssl_dhparam /usr/local/etc/dh-parameters.4096; + ssl_ecdh_curve secp384r1; + ssl_certificate /var/etc/cert.pem; + ssl_certificate_key /var/etc/cert.pem; + ssl_client_certificate /var/etc/ca.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + {% if system.webgui['ssl-ciphers'] is defined and system.webgui['ssl-ciphers'] != '' %} + ssl_ciphers {{ system.webgui['ssl-ciphers'] }} + {% else %} + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA; + {% endif %} +{% else %} + listen {% if system.webgui.port is defined and system.webgui.port != '' %}{{ system.webgui.port }}{% else %}80{% endif %}; +{% endif %} + + autoindex off; + + # gzip compression + gzip_static on; +{% if system.webgui.compression is defined and system.webgui.compression != '' %} + gzip on; + gzip_comp_level {{ system.webgui.compression }}; +{% else %} + gzip off; +{% endif %} + #compress.cache-dir = "/tmp/lighttpdcompress/" + gzip_types text/plain text/css text/xml text/javascript; + + #server.upload-dirs = ( "/root/", "/tmp/", "/var/" ) + # server.max-request-size = 2097152 + + expires 50h; + + # Maximum idle time with nothing being written (php downloading) + #fastcgi_read_timeout = 999 + + ## where to send error/access-messages to + access_log syslog:server=127.0.0.1,facility=daemon; + access_log /var/log/nginx/webgui.access.log; + error_log syslog:server=127.0.0.1,facility=daemon; + error_log /var/log/nginx/webgui.error.log debug; + + index index.php index.html index.htm default.htm; + + # mimetype mapping + types { + application/x-ns-proxy-autoconfig pad.dat; + application/pdf pdf; + application/pgp-signature sig; + application/futuresplash spl; + application/octet-stream class; + application/postscript ps; + application/x-bittorrent torrent; + application/x-dvi dvi; + application/x-gzip gz; + application/x-ns-proxy-autoconfig pac; + application/x-shockwave-flash swf; + application/x-tgz tar.gz tgz; + application/x-tar tar; + application/zip zip; + audio/mpeg mp3; + audio/x-mpegurl m3u; + audio/x-ms-wma wma; + audio/x-ms-wax wax; + audio/x-wav ogg; + audio/x-wav wav; + image/gif gif; + image/jpeg jpg jpeg; + image/png png; + image/svg+xml svg; + image/x-xbitmap xbm; + image/x-xpixmap xpm; + image/x-xwindowdump xwd; + text/css css; + text/html html htm; + text/javascript js; + text/plain asc; + text/plain c; + text/plain conf; + text/plain text txt; + text/xml dtd; + text/xml xml; + video/mpeg mpeg; + video/mpeg mpg; + video/quicktime mov qt; + video/x-msvideo avi; + video/x-ms-asf asf asx; + video/x-ms-wmv wmv; + application/x-bzip bz2; + application/x-bzip-compressed-tar tbz tar.bz2; + } + + # Use the "Content-Type" extended attribute to obtain mime type if possible + #mimetypes.use-xattr = "enable" + + ## deny access the file-extensions + # + # ~ is for backupfiles from vi, emacs, joe, ... + # .inc is often used for code includes which should in general not be part + # of the document-root + location ~* "(~|.inc)$" { + return 403; + } + +{% if helpers.exists('OPNsense.Nginx.webgui.limitnetworks') and OPNsense.Nginx.webgui.limitnetworks == '1' %} +# whitelist only directly connected networks to prevent attacks over the internet to the web interface +# we cannot block everything except RFC 1918 because this does not work with IPv6 +{% set whitelisted_networks = [] %} +{% for interface_name in interfaces %} +{% set interface = interfaces[interface_name] %} +{% if interface.ipaddr is defined and interface.ipaddr != '' and '.' in interface.ipaddr %} +{% if interface.subnet is defined and interface.subnet != '' %} +{% set cidr = interface.ipaddr + '/' + interface.subnet %} +{% else %} +{% set cidr = interface.ipaddr %} +{% endif %} +{% if cidr not in whitelisted_networks %} +{% do whitelisted_networks.append(cidr) %} + allow {{ cidr }}; +{% endif %} +{% endif %} +{% if interface.ipaddrv6 is defined and interface.ipaddrv6 != '' and ':' in interface.ipaddrv6 %} +{% if interface.subnetv6 is defined and interface.subnetv6 != '' %} +{% set cidr = interface.ipaddrv6 + '/' + interface.subnetv6 %} +{% else %} +{% set cidr = interface.ipaddrv6 %} +{% endif %} +{% if cidr not in whitelisted_networks %} +{% do whitelisted_networks.append(cidr) %} + allow {{ cidr }}; +{% endif %} +{% endif %} +{% endfor %} +{% if helpers.exists('virtualip') %} +{% for intf_item in helpers.toList('virtualip.vip') %} +{% if intf_item.type == 'single' %} +{% set cidr = intf_item.subnet + '/' + intf_item.subnet_bits %} +{% if cidr not in whitelisted_networks %} + allow {{ cidr }}; +{% do whitelisted_networks.append(cidr) %} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} + deny all; +{% endif %} + + location ~ \.php$ { + fastcgi_pass unix:/var/run/php-webgui.socket; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_index index.php; + fastcgi_param TLS-Cipher $ssl_cipher; + fastcgi_param TLS-Protocol $ssl_protocol; + fastcgi_param TLS-SNI-Host $ssl_server_name; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_intercept_errors on; + include fastcgi_params; + } + + # Phalcon ui and api routing + + location @apirequest { + root /usr/local/opnsense/www; + include fastcgi_params; + fastcgi_param QUERY_STRING &$query_string; + fastcgi_param SCRIPT_FILENAME /usr/local/opnsense/www/api.php; + fastcgi_param TLS-Cipher $ssl_cipher; + fastcgi_param TLS-Protocol $ssl_protocol; + fastcgi_param TLS-SNI-Host $ssl_server_name; + fastcgi_intercept_errors off; + fastcgi_pass unix:/var/run/php-webgui.socket; + } + location @guirequest { + root /usr/local/opnsense/www; + include fastcgi_params; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param SCRIPT_FILENAME /usr/local/opnsense/www/index.php; + fastcgi_param TLS-Cipher $ssl_cipher; + fastcgi_param TLS-Protocol $ssl_protocol; + fastcgi_param TLS-SNI-Host $ssl_server_name; + fastcgi_intercept_errors off; + fastcgi_pass unix:/var/run/php-webgui.socket; + } + location ~ ^/ui/(?[^\?]+)(?\?(.*))? { + root /usr/local/opnsense/www; + try_files /$path @guirequest; + } + location ~ ^/api/(?[^\?]+)(?\?(.*))?{ + root /usr/local/opnsense/www; + try_files /$path @apirequest; + } +}