diff --git a/backend/migrations/20260131163528_trust_forwarded_proto.js b/backend/migrations/20260131163528_trust_forwarded_proto.js
new file mode 100644
index 00000000..e982dbf7
--- /dev/null
+++ b/backend/migrations/20260131163528_trust_forwarded_proto.js
@@ -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 };
\ No newline at end of file
diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js
index b6ce6361..e8f447c8 100644
--- a/backend/models/proxy_host.js
+++ b/backend/models/proxy_host.js
@@ -21,6 +21,7 @@ const boolFields = [
"enabled",
"hsts_enabled",
"hsts_subdomains",
+ "trust_forwarded_proto",
];
class ProxyHost extends Model {
diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json
index 464b188e..cebbe0e6 100644
--- a/backend/schema/components/proxy-host-object.json
+++ b/backend/schema/components/proxy-host-object.json
@@ -22,7 +22,8 @@
"enabled",
"locations",
"hsts_enabled",
- "hsts_subdomains"
+ "hsts_subdomains",
+ "trust_forwarded_proto"
],
"properties": {
"id": {
@@ -141,6 +142,10 @@
"hsts_subdomains": {
"$ref": "../common.json#/properties/hsts_subdomains"
},
+ "trust_forwarded_proto":{
+ "type": "boolean",
+ "example": false
+ },
"certificate": {
"oneOf": [
{
diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
index 7ae60e1a..98b370bc 100644
--- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
+++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json
@@ -56,6 +56,9 @@
"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": {
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
},
diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json
index 77d772e9..e0763d9d 100644
--- a/backend/schema/paths/nginx/proxy-hosts/post.json
+++ b/backend/schema/paths/nginx/proxy-hosts/post.json
@@ -48,6 +48,9 @@
"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": {
"$ref": "../../../components/proxy-host-object.json#/properties/http2_support"
},
diff --git a/backend/templates/_forced_ssl.conf b/backend/templates/_forced_ssl.conf
index 7fade20c..886e866e 100644
--- a/backend/templates/_forced_ssl.conf
+++ b/backend/templates/_forced_ssl.conf
@@ -1,6 +1,11 @@
{% if certificate and certificate_id > 0 -%}
{% if ssl_forced == 1 or ssl_forced == true %}
# 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;
{% endif %}
{% endif %}
\ No newline at end of file
diff --git a/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf
index e43c2fc8..000bffe9 100644
--- a/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf
+++ b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf
@@ -5,9 +5,25 @@ if ($scheme = "http") {
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
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") {
+ 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";
}
+
if ($test = H) {
return 301 https://$host$request_uri;
}
diff --git a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf
index d346c4ef..fe2c2f21 100644
--- a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf
+++ b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf
@@ -1,7 +1,7 @@
add_header X-Served-By $host;
proxy_set_header Host $host;
-proxy_set_header X-Forwarded-Scheme $scheme;
-proxy_set_header X-Forwarded-Proto $scheme;
+proxy_set_header X-Forwarded-Scheme $x_forwarded_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-Real-IP $remote_addr;
proxy_pass $forward_scheme://$server:$port$request_uri;
diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf
index 0a83ef0c..892cf158 100644
--- a/docker/rootfs/etc/nginx/nginx.conf
+++ b/docker/rootfs/etc/nginx/nginx.conf
@@ -57,6 +57,18 @@ 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
# Local subnets:
diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts
index d63d47ae..2ae0b083 100644
--- a/frontend/src/api/backend/models.ts
+++ b/frontend/src/api/backend/models.ts
@@ -127,6 +127,7 @@ export interface ProxyHost {
locations?: ProxyLocation[];
hstsEnabled: boolean;
hstsSubdomains: boolean;
+ trustForwardedProto: boolean;
// Expansions:
owner?: User;
accessList?: AccessList;
diff --git a/frontend/src/components/Form/SSLOptionsFields.tsx b/frontend/src/components/Form/SSLOptionsFields.tsx
index 1c5b9f9a..a697c5f0 100644
--- a/frontend/src/components/Form/SSLOptionsFields.tsx
+++ b/frontend/src/components/Form/SSLOptionsFields.tsx
@@ -15,7 +15,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
const newCertificate = v?.certificateId === "new";
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 || {};
if (forceDNSForNew && newCertificate && !dnsChallenge) {
@@ -140,6 +140,31 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
{dnsChallenge ?