mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-10-25 05:02:24 +00:00
Merge a92e485983 into 1aca775cb9
This commit is contained in:
commit
b0b4f6fdf4
16 changed files with 215 additions and 5 deletions
|
|
@ -162,6 +162,8 @@ const internalNginx = {
|
|||
{ caching_enabled: host.caching_enabled },
|
||||
{ block_exploits: host.block_exploits },
|
||||
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
|
||||
{ enable_proxy_protocol: host.enable_proxy_protocol },
|
||||
{ load_balancer_ip: host.load_balancer_ip },
|
||||
{ http2_support: host.http2_support },
|
||||
{ hsts_enabled: host.hsts_enabled },
|
||||
{ hsts_subdomains: host.hsts_subdomains },
|
||||
|
|
|
|||
41
backend/migrations/20260425014441_proxy_protocol.js
Normal file
41
backend/migrations/20260425014441_proxy_protocol.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const migrate_name = "proxy_protocol";
|
||||
import { migrate as logger } from "../logger.js";
|
||||
|
||||
/**
|
||||
* Migrate
|
||||
*
|
||||
* @see http://knexjs.org/#Schema
|
||||
*
|
||||
* @param {Object} knex
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const up = (knex) => {
|
||||
logger.info(`[${migrate_name}] Migrating Up...`);
|
||||
|
||||
return knex.schema.table("proxy_host", (table) => {
|
||||
table.integer("enable_proxy_protocol").notNull().unsigned().defaultTo(0);
|
||||
table.string("load_balancer_ip").notNull().defaultTo("");
|
||||
}).then(() => {
|
||||
logger.info(`[${migrate_name}] proxy_host Table altered`);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo Migrate
|
||||
*
|
||||
* @param {Object} knex
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const down = (knex) => {
|
||||
logger.info(`[${migrate_name}] Migrating Down...`);
|
||||
|
||||
return knex.schema.table("proxy_host", (table) => {
|
||||
table.dropColumn("enable_proxy_protocol");
|
||||
table.dropColumn("load_balancer_ip");
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`[${migrate_name}] proxy_host Table altered`);
|
||||
});
|
||||
};
|
||||
|
||||
export { up, down };
|
||||
|
|
@ -22,6 +22,7 @@ const boolFields = [
|
|||
"hsts_enabled",
|
||||
"hsts_subdomains",
|
||||
"trust_forwarded_proto",
|
||||
"enable_proxy_protocol",
|
||||
];
|
||||
|
||||
class ProxyHost extends Model {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
"advanced_config",
|
||||
"meta",
|
||||
"allow_websocket_upgrade",
|
||||
"enable_proxy_protocol",
|
||||
"load_balancer_ip",
|
||||
"http2_support",
|
||||
"forward_scheme",
|
||||
"enabled",
|
||||
|
|
@ -84,6 +86,16 @@
|
|||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"enable_proxy_protocol": {
|
||||
"description": "Enable PROXY Protocol support",
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"load_balancer_ip": {
|
||||
"type": "string",
|
||||
"minLength": 0,
|
||||
"maxLength": 255
|
||||
},
|
||||
"http2_support": {
|
||||
"$ref": "../common.json#/properties/http2_support"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@
|
|||
"nginx_err": null
|
||||
},
|
||||
"allow_websocket_upgrade": false,
|
||||
"enable_proxy_protocol": false,
|
||||
"load_balancer_ip": "",
|
||||
"http2_support": false,
|
||||
"forward_scheme": "http",
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@
|
|||
"nginx_err": null
|
||||
},
|
||||
"allow_websocket_upgrade": false,
|
||||
"enable_proxy_protocol": false,
|
||||
"load_balancer_ip": "",
|
||||
"http2_support": false,
|
||||
"forward_scheme": "http",
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@
|
|||
"allow_websocket_upgrade": {
|
||||
"$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
|
||||
},
|
||||
"enable_proxy_protocol": {
|
||||
"$ref": "../../../../components/proxy-host-object.json#/properties/enable_proxy_protocol"
|
||||
},
|
||||
"load_balancer_ip": {
|
||||
"$ref": "../../../../components/proxy-host-object.json#/properties/load_balancer_ip"
|
||||
},
|
||||
"access_list_id": {
|
||||
"$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id"
|
||||
},
|
||||
|
|
@ -119,6 +125,8 @@
|
|||
"nginx_err": null
|
||||
},
|
||||
"allow_websocket_upgrade": false,
|
||||
"enable_proxy_protocol": false,
|
||||
"load_balancer_ip": "",
|
||||
"http2_support": false,
|
||||
"forward_scheme": "http",
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@
|
|||
"allow_websocket_upgrade": {
|
||||
"$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
|
||||
},
|
||||
"enable_proxy_protocol": {
|
||||
"$ref": "../../../components/proxy-host-object.json#/properties/enable_proxy_protocol"
|
||||
},
|
||||
"load_balancer_ip": {
|
||||
"$ref": "../../../components/proxy-host-object.json#/properties/load_balancer_ip"
|
||||
},
|
||||
"access_list_id": {
|
||||
"$ref": "../../../components/proxy-host-object.json#/properties/access_list_id"
|
||||
},
|
||||
|
|
@ -116,6 +122,8 @@
|
|||
"advanced_config": "",
|
||||
"meta": {},
|
||||
"allow_websocket_upgrade": false,
|
||||
"enable_proxy_protocol": false,
|
||||
"load_balancer_ip": "",
|
||||
"http2_support": false,
|
||||
"forward_scheme": "http",
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,32 @@
|
|||
{% if enable_proxy_protocol == 1 or enable_proxy_protocol == true%}
|
||||
listen 88 proxy_protocol;
|
||||
listen 80;
|
||||
{% if ipv6 -%}
|
||||
listen [::]:88 proxy_protocol;
|
||||
listen [::]:80;
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
listen 80;
|
||||
{% if ipv6 -%}
|
||||
listen [::]:80;
|
||||
{% else -%}
|
||||
#listen [::]:80;
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if certificate -%}
|
||||
{% if enable_proxy_protocol == 1 or enable_proxy_protocol == true%}
|
||||
listen 444 ssl proxy_protocol;
|
||||
listen 443 ssl;
|
||||
{% if ipv6 -%}
|
||||
listen [::]:444 ssl proxy_protocol;
|
||||
listen [::]:443 ssl;
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
listen 443 ssl;
|
||||
{% if ipv6 -%}
|
||||
listen [::]:443 ssl;
|
||||
{% else -%}
|
||||
#listen [::]:443;
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
#listen [::]:443;
|
||||
{% endif %}
|
||||
server_name {{ domain_names | join: " " }};
|
||||
{% if http2_support == 1 or http2_support == true %}
|
||||
|
|
|
|||
6
backend/templates/_proxy_protocol.conf
Normal file
6
backend/templates/_proxy_protocol.conf
Normal file
|
|
@ -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 %}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -237,6 +237,40 @@ Setting these environment variables will create the default user on startup, ski
|
|||
INITIAL_ADMIN_PASSWORD: mypassword1
|
||||
```
|
||||
|
||||
## PROXY Protocol Support
|
||||
|
||||
Proxy hosts can opt-in to the PROXY protocol so the client's real IP is preserved when
|
||||
NPM sits behind an upstream load balancer (AWS NLB, HAProxy, another nginx, etc.) that
|
||||
speaks PROXY protocol. See nginx's [`ngx_http_realip_module`](https://nginx.org/en/docs/http/ngx_http_realip_module.html)
|
||||
for how `set_real_ip_from` and `real_ip_header proxy_protocol` interact.
|
||||
|
||||
When enabled on a proxy host, NPM listens for PROXY protocol traffic on additional ports:
|
||||
|
||||
| Port | Purpose |
|
||||
|------|-------------------------------|
|
||||
| 88 | HTTP with PROXY protocol |
|
||||
| 444 | HTTPS (TLS) with PROXY protocol |
|
||||
|
||||
These ports are **not** exposed by default. Publish them in your `docker-compose.yml`
|
||||
so your upstream load balancer can reach them:
|
||||
|
||||
```yml
|
||||
services:
|
||||
app:
|
||||
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
- '81:81'
|
||||
- '88:88' # PROXY protocol (HTTP)
|
||||
- '444:444' # PROXY protocol (HTTPS)
|
||||
# ...
|
||||
```
|
||||
|
||||
When editing a proxy host, enable **PROXY Protocol** and supply the **Load Balancer IP / CIDR**
|
||||
of the upstream that will be sending PROXY protocol headers. This value becomes the
|
||||
`set_real_ip_from` directive — without it, nginx will not trust the forwarded client IP.
|
||||
|
||||
## Disable Nginx Resolver
|
||||
|
||||
On startup, we generate a resolvers directive for Nginx unless this is defined:
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ export interface ProxyHost {
|
|||
hstsEnabled: boolean;
|
||||
hstsSubdomains: boolean;
|
||||
trustForwardedProto: boolean;
|
||||
enableProxyProtocol: boolean;
|
||||
loadBalancerIp: string;
|
||||
// Expansions:
|
||||
owner?: User;
|
||||
accessList?: AccessList;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ const fetchProxyHost = (id: number | "new") => {
|
|||
hstsEnabled: false,
|
||||
hstsSubdomains: false,
|
||||
trustForwardedProto: false,
|
||||
enableProxyProtocol: false,
|
||||
loadBalancerIp: "",
|
||||
} as ProxyHost);
|
||||
}
|
||||
return getProxyHost(id, ["owner"]);
|
||||
|
|
|
|||
|
|
@ -410,6 +410,9 @@
|
|||
"error.invalid-email": {
|
||||
"defaultMessage": "Invalid email address"
|
||||
},
|
||||
"error.invalid-ip-or-cidr": {
|
||||
"defaultMessage": "Must be a valid IPv4/IPv6 address or CIDR range"
|
||||
},
|
||||
"error.max-character-length": {
|
||||
"defaultMessage": "Maximum length is {max} character{max, plural, one {} other {s}}"
|
||||
},
|
||||
|
|
@ -449,9 +452,15 @@
|
|||
"host.flags.protocols": {
|
||||
"defaultMessage": "Protocols"
|
||||
},
|
||||
"host.flags.proxy-protocol": {
|
||||
"defaultMessage": "Enable PROXY Protocol"
|
||||
},
|
||||
"host.flags.websockets-upgrade": {
|
||||
"defaultMessage": "Websockets Support"
|
||||
},
|
||||
"host.load-balancer-ip": {
|
||||
"defaultMessage": "Load Balancer IP / CIDR (set_real_ip_from)"
|
||||
},
|
||||
"host.forward-port": {
|
||||
"defaultMessage": "Forward Port"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,11 +17,20 @@ import {
|
|||
SSLOptionsFields,
|
||||
} from "src/components";
|
||||
import { useProxyHost, useSetProxyHost, useUser } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { intl, T } from "src/locale";
|
||||
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
|
||||
import { validateNumber, validateString } from "src/modules/Validations";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
|
||||
const IP_OR_CIDR_RE =
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d)(?:\/(?:3[0-2]|[12]?\d))?$|^[0-9a-fA-F:]+(?:\/(?:12[0-8]|1[01]\d|\d?\d))?$/;
|
||||
|
||||
// Only invoked when enableProxyProtocol is true (Field is conditionally rendered).
|
||||
const validateLoadBalancerIp = (value: string): string | undefined => {
|
||||
if (!value || !value.length) return intl.formatMessage({ id: "error.required" });
|
||||
if (!IP_OR_CIDR_RE.test(value)) return intl.formatMessage({ id: "error.invalid-ip-or-cidr" });
|
||||
};
|
||||
|
||||
const showProxyHostModal = (id: number | "new") => {
|
||||
EasyModal.show(ProxyHostModal, { id });
|
||||
};
|
||||
|
|
@ -80,6 +89,8 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
|||
cachingEnabled: data?.cachingEnabled || false,
|
||||
blockExploits: data?.blockExploits || false,
|
||||
allowWebsocketUpgrade: data?.allowWebsocketUpgrade || false,
|
||||
enableProxyProtocol: data?.enableProxyProtocol || false,
|
||||
loadBalancerIp: data?.loadBalancerIp || "",
|
||||
// Locations tab
|
||||
locations: data?.locations || [],
|
||||
// SSL tab
|
||||
|
|
@ -328,7 +339,60 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
|||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="row" htmlFor="enableProxyProtocol">
|
||||
<span className="col">
|
||||
<T id="host.flags.proxy-protocol" />
|
||||
</span>
|
||||
<span className="col-auto">
|
||||
<Field name="enableProxyProtocol" type="checkbox">
|
||||
{({ field }: any) => (
|
||||
<label className="form-check form-check-single form-switch">
|
||||
<input
|
||||
{...field}
|
||||
id="enableProxyProtocol"
|
||||
className={cn("form-check-input", {
|
||||
"bg-lime": field.checked,
|
||||
})}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</Field>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Field name="enableProxyProtocol">
|
||||
{({ field: enableField }: any) =>
|
||||
enableField.value ? (
|
||||
<Field name="loadBalancerIp" validate={validateLoadBalancerIp}>
|
||||
{({ field, form }: any) => (
|
||||
<div className="mb-3">
|
||||
<label className="form-label" htmlFor="loadBalancerIp">
|
||||
<T id="host.load-balancer-ip" />
|
||||
</label>
|
||||
<input
|
||||
id="loadBalancerIp"
|
||||
type="text"
|
||||
className={`form-control text-monospace ${form.errors.loadBalancerIp && form.touched.loadBalancerIp ? "is-invalid" : ""}`}
|
||||
placeholder=""
|
||||
maxLength={255}
|
||||
autoComplete="off"
|
||||
required
|
||||
{...field}
|
||||
/>
|
||||
{form.errors.loadBalancerIp && form.touched.loadBalancerIp ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.loadBalancerIp}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
) : null
|
||||
}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tab-pane" id="tab-locations" role="tabpanel">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue