Reverse Proxy - Let’s Encrypt finalize bad status 403

I thought I would leave this here for anyone trying to get reverse proxying to a service hosted on a different server and getting a letsencrypt certificate issued for the service.

I have spent the last 6 hours trying to get this working and finally is working.

root@host:/usr/local/hestia/data/templates/web/nginx/php-fpm# cat archive.tpl
server {
listen      %ip%:%web_port%;
listen 80;                 # Probably not needed but had nat issues
listen [::]:80;            # Probably not needed but had nat issues
server_name %domain_idn% %alias_idn%;
return 301 https://$host$request_uri;
access_log  /var/log/nginx/domains/%domain%.log combined;
access_log  /var/log/nginx/domains/%domain%.bytes bytes;
error_log   /var/log/nginx/domains/%domain%.error.log error;

# ACME challenge (comment out the following lines or remove them)
#location ^~ /.well-known/acme-challenge/ {}
#include /home/ukpoliticsdecoded/conf/web/archive.ukpoliticsdecoded.uk/nginx.conf_*;

include %home%/%user%/conf/web/%domain%/nginx.conf_*;

# Static files (critical)
location /static/ {
    proxy_pass http://192.168.60.116:8000/static/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto https;
}

# Admin UI
location /admin/ {
    proxy_pass http://192.168.60.116:8000/admin/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto https;
}

# Public UI
location / {
    proxy_pass http://192.168.60.116:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto https;
}

location ~ /\.(?!well-known\/) {
    deny all;
    return 404;
}

}

If you are getting not secure on Chrome change:

proxy_set_header X-Forwarded-Proto http;

change it to:
proxy_set_header X-Forwarded-Proto https;

Thanks for sharing this! It’s a really useful config for anyone hitting that specific combination of issues — HestiaCP + reverse proxy to a remote backend + Let’s Encrypt.

A few things worth highlighting for anyone reading this:

Why the X-Forwarded-Proto https fix works

When your backend app (Django, Rails, etc.) sees X-Forwarded-Proto: http, it thinks the original request came in over plain HTTP, so it generates http:// URLs and may redirect back to HTTP — causing the “not secure” warning or a redirect loop. Setting it to https tells the app the client actually connected over HTTPS, so it behaves correctly.

The include line placement

Moving the include %home%/%user%/conf/web/%domain%/nginx.conf_*; before your proxy blocks is important — HestiaCP writes the ACME challenge handler into that conf snippet, so it needs to be processed before nginx hits your catch-all location / proxy block. If the proxy block is first, the ACME challenge requests get forwarded to your backend instead of being handled locally, and certificate issuance fails.

The extra listen 80 lines

Likely needed if your server is behind NAT and HestiaCP’s %ip%:%web_port% resolves to an internal IP that doesn’t match what nginx needs to bind on. Not harmful to leave in.

One thing to watch

Your return 301 redirect is in the HTTP server block but you have no separate server { listen 443 ssl; ... } block shown — presumably HestiaCP generates the SSL block via its own template (archive.stpl). Just worth making sure both templates are in sync if you customise further.

Good find — 6 hours is a very normal amount of time to lose to this particular combination of problems!

1 Like