mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-02-11 21:22:57 +00:00
feat: add trust_forwarded_proto option for SSL redirect handling in reverse proxy scenarios
When Nginx is behind another proxy server (like CloudFlare or AWS ALB), the force-SSL feature can cause redirect loops because Nginx sees the connection as plain HTTP while SSL is already handled upstream. This adds a new boolean option to trust the X-Forwarded-Proto header from upstream proxies. Changes: - Add `trust_forwarded_proto` column to proxy_host table (migration) - Update model and API schema to support the new boolean field - Modify force-ssl Nginx template to check X-Forwarded-Proto/X-Forwarded-Scheme - Add map directives in nginx.conf to validate and sanitize forwarded headers - Add advanced option toggle in frontend UI with i18n support (EN/ZH) - Set proxy headers from validated map variables instead of $scheme This allows administrators to control SSL redirect behavior when Nginx is deployed behind a TLS-terminating proxy.
This commit is contained in:
31
backend/migrations/20260131163528_trust_forwarded_proto.js
Normal file
31
backend/migrations/20260131163528_trust_forwarded_proto.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { migrate as logger } from "../logger.js";
|
||||||
|
|
||||||
|
const migrateName = "redirect_auto_scheme";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate
|
||||||
|
*
|
||||||
|
* @see http://knexjs.org/#Schema
|
||||||
|
*
|
||||||
|
* @param {Object} knex
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const up = function (knex) {
|
||||||
|
return knex.schema.alterTable('proxy_host', (table) => {
|
||||||
|
table.tinyint('trust_forwarded_proto').notNullable().defaultTo(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo Migrate
|
||||||
|
*
|
||||||
|
* @param {Object} knex
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const down = function (knex) {
|
||||||
|
return knex.schema.alterTable('proxy_host', (table) => {
|
||||||
|
table.dropColumn('trust_forwarded_proto');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { up, down };
|
||||||
@@ -21,6 +21,7 @@ const boolFields = [
|
|||||||
"enabled",
|
"enabled",
|
||||||
"hsts_enabled",
|
"hsts_enabled",
|
||||||
"hsts_subdomains",
|
"hsts_subdomains",
|
||||||
|
"trust_forwarded_proto",
|
||||||
];
|
];
|
||||||
|
|
||||||
class ProxyHost extends Model {
|
class ProxyHost extends Model {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"enabled",
|
"enabled",
|
||||||
"locations",
|
"locations",
|
||||||
"hsts_enabled",
|
"hsts_enabled",
|
||||||
"hsts_subdomains"
|
"hsts_subdomains",
|
||||||
|
"trust_forwarded_proto"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -141,6 +142,10 @@
|
|||||||
"hsts_subdomains": {
|
"hsts_subdomains": {
|
||||||
"$ref": "../common.json#/properties/hsts_subdomains"
|
"$ref": "../common.json#/properties/hsts_subdomains"
|
||||||
},
|
},
|
||||||
|
"trust_forwarded_proto":{
|
||||||
|
"type": "boolean",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
"certificate": {
|
"certificate": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,6 +56,9 @@
|
|||||||
"hsts_subdomains": {
|
"hsts_subdomains": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
|
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
|
||||||
},
|
},
|
||||||
|
"trust_forwarded_proto": {
|
||||||
|
"$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
|
||||||
|
},
|
||||||
"http2_support": {
|
"http2_support": {
|
||||||
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
|
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,6 +48,9 @@
|
|||||||
"hsts_subdomains": {
|
"hsts_subdomains": {
|
||||||
"$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains"
|
"$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains"
|
||||||
},
|
},
|
||||||
|
"trust_forwarded_proto": {
|
||||||
|
"$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
|
||||||
|
},
|
||||||
"http2_support": {
|
"http2_support": {
|
||||||
"$ref": "../../../components/proxy-host-object.json#/properties/http2_support"
|
"$ref": "../../../components/proxy-host-object.json#/properties/http2_support"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
{% if certificate and certificate_id > 0 -%}
|
{% if certificate and certificate_id > 0 -%}
|
||||||
{% if ssl_forced == 1 or ssl_forced == true %}
|
{% if ssl_forced == 1 or ssl_forced == true %}
|
||||||
# Force SSL
|
# Force SSL
|
||||||
|
{% if trust_forwarded_proto == true %}
|
||||||
|
set $trust_forwarded_proto "T";
|
||||||
|
{% else %}
|
||||||
|
set $trust_forwarded_proto "F";
|
||||||
|
{% endif %}
|
||||||
include conf.d/include/force-ssl.conf;
|
include conf.d/include/force-ssl.conf;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -5,9 +5,25 @@ if ($scheme = "http") {
|
|||||||
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
|
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
|
||||||
set $test "${test}T";
|
set $test "${test}T";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if the ssl staff has been handled
|
||||||
|
set $test_proto "";
|
||||||
|
if ($trust_forwarded_proto = T){
|
||||||
|
set $test_proto "${test_proto}T";
|
||||||
|
}
|
||||||
if ($http_x_forwarded_proto = "https") {
|
if ($http_x_forwarded_proto = "https") {
|
||||||
|
set $test_proto "${test_proto}S";
|
||||||
|
}
|
||||||
|
if ($http_x_forwarded_scheme = "https") {
|
||||||
|
set $test_proto "${test_proto}S";
|
||||||
|
}
|
||||||
|
if ($test_proto = "TSS") {
|
||||||
|
set $test_proto "TS";
|
||||||
|
}
|
||||||
|
if ($test_proto = "TS") {
|
||||||
set $test "${test}S";
|
set $test "${test}S";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($test = H) {
|
if ($test = H) {
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
add_header X-Served-By $host;
|
add_header X-Served-By $host;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Scheme $scheme;
|
proxy_set_header X-Forwarded-Scheme $x_forwarded_scheme;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $x_forwarded_proto;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_pass $forward_scheme://$server:$port$request_uri;
|
proxy_pass $forward_scheme://$server:$port$request_uri;
|
||||||
|
|||||||
@@ -57,6 +57,18 @@ http {
|
|||||||
default http;
|
default http;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Handle upstream X-Forwarded-Proto and X-Forwarded-Scheme header
|
||||||
|
map $http_x_forwarded_proto $x_forwarded_proto {
|
||||||
|
"http" "http";
|
||||||
|
"https" "https";
|
||||||
|
default $scheme;
|
||||||
|
}
|
||||||
|
map $http_x_forwarded_scheme $x_forwarded_scheme {
|
||||||
|
"http" "http";
|
||||||
|
"https" "https";
|
||||||
|
default $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
# Real IP Determination
|
# Real IP Determination
|
||||||
|
|
||||||
# Local subnets:
|
# Local subnets:
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export interface ProxyHost {
|
|||||||
locations?: ProxyLocation[];
|
locations?: ProxyLocation[];
|
||||||
hstsEnabled: boolean;
|
hstsEnabled: boolean;
|
||||||
hstsSubdomains: boolean;
|
hstsSubdomains: boolean;
|
||||||
|
trustForwardedProto: boolean;
|
||||||
// Expansions:
|
// Expansions:
|
||||||
owner?: User;
|
owner?: User;
|
||||||
accessList?: AccessList;
|
accessList?: AccessList;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
|
|||||||
|
|
||||||
const newCertificate = v?.certificateId === "new";
|
const newCertificate = v?.certificateId === "new";
|
||||||
const hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0);
|
const hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0);
|
||||||
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, meta } = v;
|
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, trustForwardedProto, meta } = v;
|
||||||
const { dnsChallenge } = meta || {};
|
const { dnsChallenge } = meta || {};
|
||||||
|
|
||||||
if (forceDNSForNew && newCertificate && !dnsChallenge) {
|
if (forceDNSForNew && newCertificate && !dnsChallenge) {
|
||||||
@@ -140,6 +140,31 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
|
|||||||
{dnsChallenge ? <DNSProviderFields showBoundaryBox /> : null}
|
{dnsChallenge ? <DNSProviderFields showBoundaryBox /> : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{<div>
|
||||||
|
<details>
|
||||||
|
<summary className="mb-1"><T id="domains.advanced" /></summary>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12">
|
||||||
|
<Field name="trustForwardedProto">
|
||||||
|
{({ field }: any) => (
|
||||||
|
<label className="form-check form-switch mt-1">
|
||||||
|
<input
|
||||||
|
className={trustForwardedProto ? toggleEnabled : toggleClasses}
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!trustForwardedProto}
|
||||||
|
onChange={(e) => handleToggleChange(e, field.name)}
|
||||||
|
disabled={!hasCertificate || !sslForced}
|
||||||
|
/>
|
||||||
|
<span className="form-check-label">
|
||||||
|
<T id="domains.trust-forwarded-proto" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const fetchProxyHost = (id: number | "new") => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
hstsEnabled: false,
|
hstsEnabled: false,
|
||||||
hstsSubdomains: false,
|
hstsSubdomains: false,
|
||||||
|
trustForwardedProto: false,
|
||||||
} as ProxyHost);
|
} as ProxyHost);
|
||||||
}
|
}
|
||||||
return getProxyHost(id, ["owner"]);
|
return getProxyHost(id, ["owner"]);
|
||||||
|
|||||||
@@ -347,6 +347,9 @@
|
|||||||
"domain-names.wildcards-not-supported": {
|
"domain-names.wildcards-not-supported": {
|
||||||
"defaultMessage": "Wildcards not supported for this CA"
|
"defaultMessage": "Wildcards not supported for this CA"
|
||||||
},
|
},
|
||||||
|
"domains.advanced": {
|
||||||
|
"defaultMessage": "Advanced"
|
||||||
|
},
|
||||||
"domains.force-ssl": {
|
"domains.force-ssl": {
|
||||||
"defaultMessage": "Force SSL"
|
"defaultMessage": "Force SSL"
|
||||||
},
|
},
|
||||||
@@ -359,6 +362,9 @@
|
|||||||
"domains.http2-support": {
|
"domains.http2-support": {
|
||||||
"defaultMessage": "HTTP/2 Support"
|
"defaultMessage": "HTTP/2 Support"
|
||||||
},
|
},
|
||||||
|
"domains.trust-forwarded-proto": {
|
||||||
|
"defaultMessage": "Trust Upstream Forwarded Proto Headers"
|
||||||
|
},
|
||||||
"domains.use-dns": {
|
"domains.use-dns": {
|
||||||
"defaultMessage": "Use DNS Challenge"
|
"defaultMessage": "Use DNS Challenge"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -275,6 +275,9 @@
|
|||||||
"domain-names.wildcards-not-supported": {
|
"domain-names.wildcards-not-supported": {
|
||||||
"defaultMessage": "此 CA 不支持通配符"
|
"defaultMessage": "此 CA 不支持通配符"
|
||||||
},
|
},
|
||||||
|
"domains.advanced": {
|
||||||
|
"defaultMessage": "高级选项"
|
||||||
|
},
|
||||||
"domains.force-ssl": {
|
"domains.force-ssl": {
|
||||||
"defaultMessage": "强制 SSL"
|
"defaultMessage": "强制 SSL"
|
||||||
},
|
},
|
||||||
@@ -287,6 +290,9 @@
|
|||||||
"domains.http2-support": {
|
"domains.http2-support": {
|
||||||
"defaultMessage": "HTTP/2 支持"
|
"defaultMessage": "HTTP/2 支持"
|
||||||
},
|
},
|
||||||
|
"domains.trust-forwarded-proto": {
|
||||||
|
"defaultMessage": "信任上游代理传递的协议类型头"
|
||||||
|
},
|
||||||
"domains.use-dns": {
|
"domains.use-dns": {
|
||||||
"defaultMessage": "使用DNS验证"
|
"defaultMessage": "使用DNS验证"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
|||||||
http2Support: data?.http2Support || false,
|
http2Support: data?.http2Support || false,
|
||||||
hstsEnabled: data?.hstsEnabled || false,
|
hstsEnabled: data?.hstsEnabled || false,
|
||||||
hstsSubdomains: data?.hstsSubdomains || false,
|
hstsSubdomains: data?.hstsSubdomains || false,
|
||||||
|
trustForwardedProto: data?.trustForwardedProto || false,
|
||||||
// Advanced tab
|
// Advanced tab
|
||||||
advancedConfig: data?.advancedConfig || "",
|
advancedConfig: data?.advancedConfig || "",
|
||||||
meta: data?.meta || {},
|
meta: data?.meta || {},
|
||||||
|
|||||||
Reference in New Issue
Block a user