diff --git a/backend/doc/api.swagger.json b/backend/doc/api.swagger.json index 3fa19fc4..c692cb1c 100644 --- a/backend/doc/api.swagger.json +++ b/backend/doc/api.swagger.json @@ -90,6 +90,8 @@ "nginx_err": "Command failed: /usr/sbin/nginx -t -g \"error_log off;\"\nnginx: [emerg] unknown directive \"sdfsdfsdf\" in /data/nginx/proxy_host/1.conf:37\nnginx: configuration file /etc/nginx/nginx.conf test failed\n" }, "allow_websocket_upgrade": 0, + "proxy_support_enabled": 0, + "load_balancer_ip": "", "http2_support": 0, "forward_scheme": "http", "enabled": 1, @@ -210,6 +212,8 @@ "dns_challenge": false }, "allow_websocket_upgrade": 0, + "proxy_support_enabled": 0, + "load_balancer_ip": "", "http2_support": 0, "forward_scheme": "http", "enabled": 1, diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 77933e73..84630938 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -155,6 +155,7 @@ const internalNginx = { let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id}, {ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits}, {allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support}, + {enable_proxy_protocol: host.enable_proxy_protocol}, {load_balancer_ip: host.load_balancer_ip}, {hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list}, {certificate: host.certificate}, host.locations[i]); diff --git a/backend/migrations/20240310085523_proxy_protocol.js b/backend/migrations/20240310085523_proxy_protocol.js new file mode 100644 index 00000000..3a65268f --- /dev/null +++ b/backend/migrations/20240310085523_proxy_protocol.js @@ -0,0 +1,41 @@ +const migrate_name = 'proxy_protocol'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.integer('enable_proxy_protocol').notNull().unsigned().defaultTo(0); + proxy_host.string('load_balancer_ip').notNull().defaultTo(''); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); + +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex, Promise) { + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.dropColumn('enable_proxy_protocol') + proxy_host.dropColumn('load_balancer_ip') + }) + .then(function () { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); +}; \ No newline at end of file diff --git a/backend/schema/endpoints/proxy-hosts.json b/backend/schema/endpoints/proxy-hosts.json index 9a3fff2f..27a8ec2a 100644 --- a/backend/schema/endpoints/proxy-hosts.json +++ b/backend/schema/endpoints/proxy-hosts.json @@ -58,6 +58,16 @@ "example": true, "type": "boolean" }, + "enable_proxy_protocol": { + "description": "Enable PROXY Protocol support", + "example": true, + "type": "boolean" + }, + "load_balancer_ip": { + "type": "string", + "minLength": 0, + "maxLength": 255 + }, "access_list_id": { "$ref": "../definitions.json#/definitions/access_list_id" }, @@ -155,6 +165,12 @@ "allow_websocket_upgrade": { "$ref": "#/definitions/allow_websocket_upgrade" }, + "enable_proxy_protocol": { + "$ref": "#/definitions/enable_proxy_protocol" + }, + "load_balancer_ip": { + "$ref": "#/definitions/load_balancer_ip" + }, "access_list_id": { "$ref": "#/definitions/access_list_id" }, @@ -245,6 +261,12 @@ "allow_websocket_upgrade": { "$ref": "#/definitions/allow_websocket_upgrade" }, + "enable_proxy_protocol": { + "$ref": "#/definitions/enable_proxy_protocol" + }, + "load_balancer_ip": { + "$ref": "#/definitions/load_balancer_ip" + }, "access_list_id": { "$ref": "#/definitions/access_list_id" }, @@ -318,6 +340,12 @@ "allow_websocket_upgrade": { "$ref": "#/definitions/allow_websocket_upgrade" }, + "enable_proxy_protocol": { + "$ref": "#/definitions/enable_proxy_protocol" + }, + "load_balancer_ip": { + "$ref": "#/definitions/load_balancer_ip" + }, "access_list_id": { "$ref": "#/definitions/access_list_id" }, diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf index ad1c96ba..6a4bd92a 100644 --- a/backend/templates/_listen.conf +++ b/backend/templates/_listen.conf @@ -1,15 +1,24 @@ - listen 80; -{% if ipv6 -%} - listen [::]:80; +{% if enable_proxy_protocol == 1 or enable_proxy_protocol == true -%} +{% assign port_number_http = "88" -%} +{% assign port_number_https = "444" -%} +{% assign listen_extra_args = "proxy_protocol" -%} {% else -%} - #listen [::]:80; -{% endif %} +{% assign port_number_http = "80" -%} +{% assign port_number_https = "443" -%} +{% assign listen_extra_args = "" -%} +{% endif -%} + + listen {{ port_number_http }} {{ listen_extra_args }}; +{% if ipv6 -%} + listen [::]:{{ port_number_http }} {{ listen_extra_args }}; +{% endif -%} + {% if certificate -%} - listen 443 ssl{% if http2_support == 1 or http2_support == true %} http2{% endif %}; + {% capture listen_extra_args_https %}ssl{% if http2_support %} http2{% endif %} {{ listen_extra_args }}{% endcapture -%} + listen {{ port_number_https }} {{ listen_extra_args_https }}; {% if ipv6 -%} - listen [::]:443 ssl{% if http2_support == 1 or http2_support == true %} http2{% endif %}; -{% else -%} - #listen [::]:443; -{% endif %} -{% endif %} + listen [::]:{{ port_number_https }} {{ listen_extra_args_https }}; +{% endif -%} +{% endif -%} + server_name {{ domain_names | join: " " }}; diff --git a/backend/templates/_proxy_protocol.conf b/backend/templates/_proxy_protocol.conf new file mode 100644 index 00000000..cba0424f --- /dev/null +++ b/backend/templates/_proxy_protocol.conf @@ -0,0 +1,6 @@ +{% if enable_proxy_protocol == 1 or enable_proxy_protocol == true %} +{% if load_balancer_ip != '' %} + set_real_ip_from {{ load_balancer_ip }}; + real_ip_header proxy_protocol; +{% endif %} +{% endif %} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index d23ca46f..e753b6dd 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -15,6 +15,7 @@ server { {% include "_exploits.conf" %} {% include "_hsts.conf" %} {% include "_forced_ssl.conf" %} +{% include "_proxy_protocol.conf" %} {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} proxy_set_header Upgrade $http_upgrade; diff --git a/docker/Dockerfile b/docker/Dockerfile index 799ee2a6..9ce4d5cc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -33,7 +33,7 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ COPY docker/scripts/install-s6 /tmp/install-s6 RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 -EXPOSE 80 81 443 +EXPOSE 80 81 88 443 444 COPY backend /app COPY frontend/dist /app/frontend diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 3c21849c..1936a788 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -29,5 +29,5 @@ RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager COPY scripts/install-s6 /tmp/install-s6 RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 -EXPOSE 80 81 443 +EXPOSE 80 81 88 443 444 ENTRYPOINT [ "/init" ] diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index 66770e53..0ae07b13 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -19,7 +19,9 @@ services: expose: - 81 - 80 + - 88 - 443 + - 444 depends_on: - db healthcheck: @@ -43,7 +45,9 @@ services: expose: - 81 - 80 + - 88 - 443 + - 444 healthcheck: test: ["CMD", "/usr/bin/check-health"] interval: 10s diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 6d8cf87c..3a056945 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -11,7 +11,9 @@ services: ports: - 3080:80 - 3081:81 + - 3088:88 - 3443:443 + - 3444:444 networks: - nginx_proxy_manager environment: diff --git a/docs/advanced-config/README.md b/docs/advanced-config/README.md index 472b8f12..9d5dbe18 100644 --- a/docs/advanced-config/README.md +++ b/docs/advanced-config/README.md @@ -208,3 +208,15 @@ You can customise the logrotate configuration through a mount (if your custom co ``` For reference, the default configuration can be found [here](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/docker/rootfs/etc/logrotate.d/nginx-proxy-manager). + +## Enabling Proxy Protocol for Proxy Hosts + +When running NPM behind a load balancer, you might want to use the [PROXY procotol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to receive client information such as the source IP address (useful for banning IPs). + +When configuring the PROXY protocol for proxy hosts, NPM uses the ports 88 for http and 444 for https traffic to allow you to decide on a per host basis whether to use the PROSY protocol. + +To enable the Proxy Protocol for your hosts you need to perform the following steps: + +1. Expose the ports `88` (and `444` is applicable) by adjusting your `docker-compose.yml` +2. Edit your proxy hosts to enable the PROXY protocol +3. Edit your upstream load balancer to redirect traffic to the port `88`/`444` and enable the PROXY protocol diff --git a/docs/setup/README.md b/docs/setup/README.md index d881d88d..bf307ddf 100644 --- a/docs/setup/README.md +++ b/docs/setup/README.md @@ -61,6 +61,8 @@ services: - '80:80' # Public HTTP Port - '443:443' # Public HTTPS Port - '81:81' # Admin Web Port + # - '88:88' # Public HTTP Port with proxy_protocol enabled + # - '444:444' # Public HTTPS Port with proxy_protocol enabled # Add any other Stream port you want to expose # - '21:21' # FTP environment: diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 8e7a2a2d..b45ef749 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -72,7 +72,7 @@ -
+
+
+
+ +
+
+ +
+
+ + > +
+
diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js index 4437a6dd..d5bdca79 100644 --- a/frontend/js/app/nginx/proxy/form.js +++ b/frontend/js/app/nginx/proxy/form.js @@ -43,6 +43,8 @@ module.exports = Mn.View.extend({ dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', propagation_seconds: 'input[name="meta[propagation_seconds]"]', forward_scheme: 'select[name="forward_scheme"]', + enable_proxy_protocol: 'input[name="enable_proxy_protocol"]', + load_balancer_ip: 'input[name="load_balancer_ip"]', letsencrypt: '.letsencrypt' }, @@ -51,6 +53,13 @@ module.exports = Mn.View.extend({ }, events: { + 'change @ui.enable_proxy_protocol': function () { + let checked = this.ui.enable_proxy_protocol.prop('checked'); + this.ui.load_balancer_ip + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + }, 'change @ui.certificate_select': function () { let id = this.ui.certificate_select.val(); if (id === 'new') { @@ -163,6 +172,7 @@ module.exports = Mn.View.extend({ data.block_exploits = !!data.block_exploits; data.caching_enabled = !!data.caching_enabled; data.allow_websocket_upgrade = !!data.allow_websocket_upgrade; + data.enable_proxy_protocol = !!data.enable_proxy_protocol; data.http2_support = !!data.http2_support; data.hsts_enabled = !!data.hsts_enabled; data.hsts_subdomains = !!data.hsts_subdomains; @@ -264,6 +274,7 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; + this.ui.enable_proxy_protocol.trigger('change'); this.ui.ssl_forced.trigger('change'); this.ui.hsts_enabled.trigger('change'); diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index 0bbde454..136e6ebd 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -131,6 +131,8 @@ "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.", "access-list": "Access List", "allow-websocket-upgrade": "Websockets Support", + "enable-proxy-protocol": "Enable Proxy Protocol", + "load-balancer-ip": "Load balancer or TCP proxy IP / CIDR range", "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", "custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path/", "search": "Search Host…" diff --git a/frontend/js/models/proxy-host.js b/frontend/js/models/proxy-host.js index b82d09fe..b1a80f54 100644 --- a/frontend/js/models/proxy-host.js +++ b/frontend/js/models/proxy-host.js @@ -19,6 +19,8 @@ const model = Backbone.Model.extend({ hsts_subdomains: false, caching_enabled: false, allow_websocket_upgrade: false, + enable_proxy_protocol: false, + load_balancer_ip: '', block_exploits: false, http2_support: false, advanced_config: '',