Skip to content

Add trust_forwarded_proto option for SSL redirect handling in r…#5260

Merged
jc21 merged 7 commits intoNginxProxyManager:developfrom
jerry-yuan:develop
Feb 11, 2026
Merged

Add trust_forwarded_proto option for SSL redirect handling in r…#5260
jc21 merged 7 commits intoNginxProxyManager:developfrom
jerry-yuan:develop

Conversation

@jerry-yuan
Copy link
Contributor

@jerry-yuan jerry-yuan commented Jan 31, 2026

…everse proxy scenarios

As mentioned in #5216, 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.

…everse 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.
@jerry-yuan jerry-yuan changed the title Add trust_forwarded_proto option for SSL redirect handling in r… WIP: Add trust_forwarded_proto option for SSL redirect handling in r… Feb 1, 2026
@jerry-yuan jerry-yuan changed the title WIP: Add trust_forwarded_proto option for SSL redirect handling in r… Add trust_forwarded_proto option for SSL redirect handling in r… Feb 1, 2026
@jerry-yuan jerry-yuan marked this pull request as draft February 3, 2026 07:07
@jerry-yuan jerry-yuan closed this Feb 3, 2026
@jerry-yuan jerry-yuan reopened this Feb 3, 2026
@jerry-yuan jerry-yuan marked this pull request as ready for review February 3, 2026 07:09
@jerry-yuan
Copy link
Contributor Author

@jc21 May I ask you for a code review?

@jc21 jc21 added the requires-verification Waiting for one or more people to confirm the fix label Feb 4, 2026
@jc21
Copy link
Member

jc21 commented Feb 4, 2026

Yep I'll see how I go today

@MansourM
Copy link

MansourM commented Feb 9, 2026

will this also fix
#4262
?
i've been manually fixing my configs so far

@jerry-yuan
Copy link
Contributor Author

will this also fix
#4262
?
i've been manually fixing my configs so far

Yes, we are working on a similar fix. My changes primarily address the loop redirect issue when a proxied NPM instance has Force SSL enabled. To ensure compatibility with multi-layer nested scenarios, I've also incidentally fixed the propagation of the X-Forwarded-Scheme and X-Forwarded-Proto headers.

@jc21 Hey, looks like I'm not the only one with this kind of issue 😁

@jc21
Copy link
Member

jc21 commented Feb 10, 2026

Ok so I've been testing and

  • 👍 works for a fresh instance
  • 👎 not working when used on an existing instance with existing proxy hosts
❯ Starting nginx ...
nginx: [emerg] unknown "trust_forwarded_proto" variable

My comment about quoting the comparision should eleviate this error, just has to be tested.

@jerry-yuan
Copy link
Contributor Author

@jc21 Thank you for your Code Review. Following your feedback, I have fixed the conditional statement for trust_forwarded_proto. Additionally, I added a check to verify if the trust_forwarded_proto variable is defined before using it. If it is not defined, I now assign it a default value of "F". I also locally validated the compatibility between older configuration files (those missing the trust_forwarded_proto definition) and the new software version:

  1. Started a fresh NPM instance.
  2. Created two virtual hosts with Force SSL enabled.
  3. Entered the Docker container and manually removed the trust_forwarded_proto definition from /data/nginx/1.conf to simulate an older configuration.
  4. Reopened the configuration window for the second virtual host and saved it to trigger an Nginx reload.
  5. Observed any NPM errors and checked the logs of the npm2dev.core container.

I found that even after changing if ($trust_forwarded_proto = T) to if ($trust_forwarded_proto = "T"), I still couldn’t prevent Nginx from throwing errors. Therefore, I decided to check whether the $trust_forwarded_proto variable is defined and initialize it before using it (as implemented in the diff here: https://github.com/NginxProxyManager/nginx-proxy-manager/pull/5260/changes#diff-25174e53424655eff165a58345aedd0fb61a56c4c01bd22a47e889b20caa7cb4R11). After adding this initialization check, I observed that Nginx no longer reported errors.

I am curious about how you set up existing instances with proxied hosts in your development environment. If possible, I would appreciate learning your method so I can more comprehensively test such scenarios.

@nginxproxymanagerci
Copy link

Docker Image for build 8 is available on DockerHub:

nginxproxymanager/nginx-proxy-manager-dev:pr-5260

Note

Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
This is a different docker image namespace than the official image.

Warning

Changes and additions to DNS Providers require verification by at least 2 members of the community!

@jc21
Copy link
Member

jc21 commented Feb 11, 2026

I am curious about how you set up existing instances with proxied hosts in your development environment. If possible, I would appreciate learning your method so I can more comprehensively test such scenarios.

I simply deploy the image in question to one of my production instances, after making a backup of the data, of course.

@jc21
Copy link
Member

jc21 commented Feb 11, 2026

However before that last step I would get the PR, run ./scripts/start-dev

This is good enought for most dev testing but if I need to test SSL there are other tweaks required depending on the case.

@jc21
Copy link
Member

jc21 commented Feb 11, 2026

ok so I've got this latest image working. However..

  • https://yum.jc21.com/
  • Hosted with Cloudflare, proxied through to my server
  • This is forced ssl, but without your new advanced option enabled

I expect to see the redirect loop? Instead, it's working as if the advanced option was enabled.

Screenshot 2026-02-11 at 10 49 32 Screenshot 2026-02-11 at 10 46 18

Generated proxy config:

# ------------------------------------------------------------
# mantine.jc21.com, public.jc21.com, yum.jc21.com
# ------------------------------------------------------------

map $scheme $hsts_header {
    https   "max-age=63072000;includeSubDomains; preload";
}

server {
  set $forward_scheme http;
  set $server         "172.17.0.1";
  set $port           8800;

  listen 80;
  listen [::]:80;
  listen 443 ssl;
  listen [::]:443 ssl;

  server_name mantine.jc21.com public.jc21.com yum.jc21.com;

  http2 on;

  # Let's Encrypt SSL
  include conf.d/include/letsencrypt-acme-challenge.conf;
  include conf.d/include/ssl-cache.conf;
  include conf.d/include/ssl-ciphers.conf;
  ssl_certificate /etc/letsencrypt/live/npm-13/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/npm-13/privkey.pem;

  # Block Exploits
  include conf.d/include/block-exploits.conf;

  # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years)
  add_header Strict-Transport-Security $hsts_header always;

  # Force SSL
  set $trust_forwarded_proto "F";
  include conf.d/include/force-ssl.conf;

  access_log /data/logs/proxy-host-44_access.log proxy;
  error_log /data/logs/proxy-host-44_error.log warn;

  location / {
    # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years)
    add_header Strict-Transport-Security $hsts_header always;

    # Proxy!
    include conf.d/include/proxy.conf;
  }

  # Custom
  include /data/nginx/custom/server_proxy[.]conf;
}

Cloudflare SSL Settings:

Screenshot 2026-02-11 at 10 51 52

@jerry-yuan
Copy link
Contributor Author

@jc21 Thank you for your reply. I think I understand what's happening now. As illustrated in your screenshot, when you select Full (strict) or Full mode, Cloudflare enables SSL encryption on both segments: between the Client and Cloudflare, and between Cloudflare and your origin server (NPM). In this case, the request received by the origin server is over HTTPS, which satisfies the condition for Force SSL, thus no redirect response is generated.

You could try changing Cloudflare's SSL setting to Flexible, where SSL is only enforced between the Client and Cloudflare, while HTTP is used between Cloudflare and your origin server. In this scenario, your origin server will receive an HTTP request carrying the X-Forwarded-Proto header. I believe this should trigger the redirect loop.

I'd like to emphasize that using HTTP to connect to your origin over the public internet (an untrusted network environment) is indeed not a good practice, so your choice of Full mode is reasonable. The newly added configuration primarily aims to support HTTP connections to the origin within internal networks (a controlled environment), while incidentally supporting Cloudflare's Flexible mode.

@jerry-yuan
Copy link
Contributor Author

I am curious about how you set up existing instances with proxied hosts in your development environment. If possible, I would appreciate learning your method so I can more comprehensively test such scenarios.

I simply deploy the image in question to one of my production instances, after making a backup of the data, of course.

Direct testing in a production environment is indeed a bit daunting. I understand now. I plan to clone my production environment configuration and data to test the PR image as well.

@jc21
Copy link
Member

jc21 commented Feb 11, 2026

Ok yeah of course...

Direct testing in a production environment is indeed a bit daunting.

Yes totally. These days, I need to have every confidence that things "work for me" before so many others also get the release. I've been burnt too much

@jc21
Copy link
Member

jc21 commented Feb 11, 2026

I've changed to flexible, I see the loop happening and enabling your flag is working as expected. Great!

@jc21 jc21 merged commit 011191f into NginxProxyManager:develop Feb 11, 2026
1 check passed
@jerry-yuan
Copy link
Contributor Author

I'm glad that my work has received your approval. I might consider adding an explanation of the new feature in the documentation later on.🥳🥳🥳

@toviszsolt
Copy link
Contributor

toviszsolt commented Feb 17, 2026

Hi @jerryzone / @jerry-yuan / @jc21 !

Thank you for implementing this feature - it addresses a real problem with SSL redirect loops in reverse proxy scenarios. The implementation looks well thought out, especially the header validation using map directives with whitelist values.

I wanted to share a couple of thoughts that might be worth considering for documentation and future enhancements:

1. Documentation for safe usage
Since this feature trusts upstream headers, it would be really helpful to add a note in the documentation or UI tooltip clarifying that trust_forwarded_proto should only be enabled when Nginx is deployed behind a trusted reverse proxy (like CloudFlare, AWS ALB, etc.). This would help administrators understand when it's safe to use this option.

2. Additional hardening possibility
For environments where this feature is used, it might be worth considering adding an optional IP allowlist for trusted proxies in a future enhancement. This would provide an additional layer of protection by only accepting X-Forwarded-Proto headers from known proxy IPs. This is just a thought for potential future improvements - not necessarily needed for the current implementation.

Overall, great work on solving the redirect loop issue! These are just some ideas that might help make the feature even more robust for production environments.

Thanks again for your contribution!

I also noticed the bot warnings regarding security regressions and custom locations. Since this is already merged, it might be critical to address the X-Forwarded-Proto spoofing possibility (CVE potential?) in a follow-up fix, especially for users not using Cloudflare/trusted proxies.

I'd like to emphasize that using HTTP to connect to your origin over the public internet (an untrusted network environment) is indeed not a good practice, so your choice of Full mode is reasonable. The newly added configuration primarily aims to support HTTP connections to the origin within internal networks (a controlled environment), while incidentally supporting Cloudflare's Flexible mode.

@jerry-yuan
Copy link
Contributor Author

@toviszsolt I'm glad that my work has received your recognition. Regarding the future plans you mentioned, here are my thoughts:

  1. More comprehensive documentation

After the code was merged into develop, I immediately looked at the current NPM documentation and started working on adding documentation for the new feature. However, I noticed that the existing Q&A and configuration documentation seem to be mostly introductory. Since my feature is only necessary in fairly complex use cases, I couldn't find a suitable section to place the description. If you think there's an appropriate place for it, I'd be happy to contribute the relevant documentation.

  1. Trusted upstream IP whitelist

I believe this feature is necessary and had initially designed something similar. However, @jc21 emphasized that the project's UI/UX philosophy is to keep things as simple and user-friendly as possible. I considered my feature itself to be an advanced, low-frequency-use feature, so I trimmed down the whitelist functionality and placed it within the collapsed advanced options menu by default. If a whitelist feature is to be added in the future, it may require a reassessment of its priority and design.

@toviszsolt
Copy link
Contributor

@jerry-yuan

  • By documentation, I meant a short text in the UI. I don't know how detailed the web-based documentation is, but I don't think it goes into that kind of detail. In my opinion, the UI should primarily contain some minimal information.
  • Regarding the Whitelist, it might be possible to make it mandatory to specify an Access List with an IP range. This can be set in the Access List menu.
  • Another possible option is a new tab in the modal window. There is also a helper icon option, which can also handle shorter texts.

Since @jc21 knows the database structure and UI, and what's connected to what, we'll ask him if he can comment and if he has any ideas about this.

@acenomad
Copy link

These changes appear to have broken existing configurations where a public-facing NPM instance that handle TLS termination then proxies requests to internal NPM instances over HTTP; headers that were previously trusted before are no longer, and it's rather unclear to me how these changes were intended to be addressed or accommodated by admins with existing configurations.

In my case the external NPM instance should not trust upstream headers as it is exposed online so I have left the new setting disabled. However that same setting is for some reason disabled in the UI of the internal NPM instance which is front of the application, it only deals with HTTP and instead trusts the X-Forwarded headers from the upstream proxy. Attempting to manually set set $trust_forwarded_proto "T"; in the internal NPM instance advanced settings does not resolve the issue either.

There are several comments towards the end here that mention writing documentation about how this feature works and was implemented; that would indeed be massively appreciated as it appears I was not the only admin who had issues after the implementation of this feature.

FWIW I am still debugging and attempting to resolve my header issues live. I am happy to share logs or more details about my set up, but will eventually issue a rollback if 2.14.0 is too broken to work with.

Finally,

Trusted upstream IP whitelist

I believe this feature is necessary and had initially designed something similar. However, @jc21 emphasized that the project's UI/UX philosophy is to keep things as simple and user-friendly as possible.

I would very enthusiastically support this feature, IMHO proxy-behind-a-proxy is not a particularly uncommon or user-unfriendly configuration.

@jerry-yuan
Copy link
Contributor Author

@acenomad Indeed, as you said, this change reverted a default feature introduced in the last version of 2.13. In that version, to quickly resolve my issue, @jc21 added the modification to trust upstream forwarded headers in the force‑SSL configuration and released a version before this change was merged. For security reasons, I reverted that change—more precisely, I added a toggle to make it opt‑in rather than enabled by default.

This change essentially deals with the behavior of the forced HTTPS redirect. When Force SSL is disabled, the entire file containing this change should be removed from the nginx configuration, so the toggle no longer has any effect—it’s reasonable that it appears disabled on HTTP virtual hosts.

From what I’ve seen so far, most of the reported issues stem from users mounting and modifying nginx.conf, which prevents it from being updated when the container is upgraded. In fact, NPM provides a way to customize the core configuration file—the relevant documentation is here: https://nginxproxymanager.com/advanced-config/#custom-nginx-configurations

Coming back to your problem: if you could describe your overall topology and the specific symptoms you’re experiencing in more detail, I’d be happy to help resolve the issues introduced by my change.

@acenomad
Copy link

acenomad commented Feb 28, 2026

Hi @jerry-yuan thanks for the extra context! Yesterday I spent a couple hours trying to debug why some of my apps suddenly stopped authenticating properly while all other traffic otherwise behaved normally. Initially I had thought it was a result of a recent Authentik update but I eventually narrowed it down to headers not being passed properly between proxies, in particular X-Forwarded-Proto which lead me here after some closer reading of recent changelogs.

After unsuccessfully troubleshooting for some time I rolled back to a previous host snapshot and called it a night. Picking things up again today I ran an update of the NPM containers and... nothing broke 😅 I think it's left me even more confused than previously because there are functionally no differences between the environment, nor I believe any container updates pulled between today and yesterday.

I don't directly modify nginx.conf, but I do have one snippet in /data/nginx/custom/server_proxy.conf. During my troubleshooting yesterday I tried a couple of times to completely down, prune images, and re-pull the containers, as well as tried rebooting the host and disabling+re-enabling the individual proxies among other things.

At this stage my best guess is, as you suggest, still probably related to something that was prevented from updating. I wouldn't yet be able to tell you what, unfortunately.

Either way, seems my issue is resolved. Thanks for your contribution and assistance :)

edit: typo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-verification Waiting for one or more people to confirm the fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants