An unqualified host port binds to 0.0.0.0, so the QA app answers directly on http://<nas-ip>:32751 from anywhere on the LAN, bypassing Nginx Proxy Manager and its TLS.
What this does and does not expose
It does not defeat the admin gate, and that is worth establishing before treating this as urgent. backend/src/middleware/adminGate.ts requires the x-admin-gate header to match ADMIN_GATE_SECRET, and the proxy is what injects it. A direct request to 32751 therefore arrives without the header and /api/admin refuses it. That path fails closed, which is the correct design.
What it does expose is everything else: the storefront, the customer API, login, registration, and the unauthenticated POST /api/client-errors, all over plain HTTP on the local network with none of the proxy in front of them. It also makes the QA hostname optional, so anything enforced only at the proxy is enforced only for people who come through the front door.
Fix, with a caveat that decides which fix
If Nginx Proxy Manager runs directly on the NAS host, bind the published port to loopback:
ports:
- 127.0.0.1:32751:3000
The proxy keeps working and direct LAN access stops.
If NPM runs as its own container, that change breaks QA rather than securing it. Container-to-container traffic does not arrive over the host's loopback interface, so the proxy would stop reaching the app. In that case the right fix is a shared Docker network between the proxy and this stack, with the ports: block removed altogether so nothing is published to the host at all. That is the stronger fix of the two, since an unpublished port cannot be reached by anything outside the network.
Establish which of the two NPM actually is before changing anything.
Also check production
Production is a separate Portainer stack that lives outside this repository, so this file says nothing about it. If it carries the same unqualified mapping, the same change matters considerably more there than it does in QA.
`docker-compose.qa.yml` maps the app with:
```
ports:
- 32751:3000
```
An unqualified host port binds to 0.0.0.0, so the QA app answers directly on `http://<nas-ip>:32751` from anywhere on the LAN, bypassing Nginx Proxy Manager and its TLS.
## What this does and does not expose
It does not defeat the admin gate, and that is worth establishing before treating this as urgent. `backend/src/middleware/adminGate.ts` requires the `x-admin-gate` header to match `ADMIN_GATE_SECRET`, and the proxy is what injects it. A direct request to 32751 therefore arrives without the header and `/api/admin` refuses it. That path fails closed, which is the correct design.
What it does expose is everything else: the storefront, the customer API, login, registration, and the unauthenticated `POST /api/client-errors`, all over plain HTTP on the local network with none of the proxy in front of them. It also makes the QA hostname optional, so anything enforced only at the proxy is enforced only for people who come through the front door.
## Fix, with a caveat that decides which fix
If Nginx Proxy Manager runs directly on the NAS host, bind the published port to loopback:
```
ports:
- 127.0.0.1:32751:3000
```
The proxy keeps working and direct LAN access stops.
If NPM runs as its own container, that change breaks QA rather than securing it. Container-to-container traffic does not arrive over the host's loopback interface, so the proxy would stop reaching the app. In that case the right fix is a shared Docker network between the proxy and this stack, with the `ports:` block removed altogether so nothing is published to the host at all. That is the stronger fix of the two, since an unpublished port cannot be reached by anything outside the network.
Establish which of the two NPM actually is before changing anything.
## Also check production
Production is a separate Portainer stack that lives outside this repository, so this file says nothing about it. If it carries the same unqualified mapping, the same change matters considerably more there than it does in QA.
bermudalamb
added this to the Code Quality and Hardening 2 project 2026-08-22 09:59:59 -05:00
bermudalamb
self-assigned this 2026-08-22 10:00:23 -05:00
Confirmed still present on main. docker-compose.qa.yml line 123 is unchanged:
ports:
# 32751, not production's 32750.
- 32751:3000
Also checked what the file says about networking, since that decides which of the two fixes is correct: it declares nonetworks: block at all, so the QA stack sits on the default bridge network compose creates for it. That rules out the two containers already sharing a network today, but it does not settle the question — a proxy container on a different network would still be reaching the app through the published host port, which is exactly the mapping this issue is about removing.
So the fix cannot be chosen from the repository alone. The deciding fact is only visible on the NAS: whether Nginx Proxy Manager runs as a Synology package or host service, or as its own container.
Host service → 127.0.0.1:32751:3000 is right, and is a one-line change.
Container → that same one-line change breaks QA rather than securing it, because container-to-container traffic does not arrive over the host's loopback interface. The fix is then a shared Docker network plus removing the ports: block entirely, which is the stronger outcome anyway since an unpublished port cannot be reached from outside the network at all.
Parked pending that answer rather than guessing, because guessing wrong here takes QA down.
The production note in the issue body still stands and is the more important half: production is a Portainer stack outside this repository, so if it carries the same unqualified mapping, the same change matters considerably more there than in QA.
Confirmed still present on `main`. `docker-compose.qa.yml` line 123 is unchanged:
```
ports:
# 32751, not production's 32750.
- 32751:3000
```
Also checked what the file says about networking, since that decides which of the two fixes is correct: it declares **no** `networks:` block at all, so the QA stack sits on the default bridge network compose creates for it. That rules out the two containers already sharing a network today, but it does not settle the question — a proxy container on a different network would still be reaching the app through the published host port, which is exactly the mapping this issue is about removing.
So the fix cannot be chosen from the repository alone. The deciding fact is only visible on the NAS: whether Nginx Proxy Manager runs as a Synology package or host service, or as its own container.
- **Host service** → `127.0.0.1:32751:3000` is right, and is a one-line change.
- **Container** → that same one-line change *breaks QA* rather than securing it, because container-to-container traffic does not arrive over the host's loopback interface. The fix is then a shared Docker network plus removing the `ports:` block entirely, which is the stronger outcome anyway since an unpublished port cannot be reached from outside the network at all.
Parked pending that answer rather than guessing, because guessing wrong here takes QA down.
The production note in the issue body still stands and is the more important half: production is a Portainer stack outside this repository, so if it carries the same unqualified mapping, the same change matters considerably more there than in QA.
Q: Does Nginx Proxy Manager run as its own container, or as a Synology package / host service?
A: Its own container.
That settles the fix, and it is the stronger of the two. 127.0.0.1:32751:3000 is wrong here and would break QA rather than secure it — container-to-container traffic does not arrive over the host's loopback interface, so the proxy would stop reaching the app.
The fix is a shared Docker network between the proxy and this stack, with the ports: block removed altogether. Nothing is then published to the host at all, and an unpublished port cannot be reached from outside that network — which is a better outcome than a loopback binding, since it does not depend on anything else on the NAS being trustworthy.
What that means in practice for docker-compose.qa.yml: declare the proxy's existing network as external: true, attach the app service to it, and delete the ports: mapping. The proxy then addresses the app by its container name and port 3000 rather than by host IP and 32751, so the proxy host entry needs updating in the same change — QA is reachable on its hostname throughout, and stops being reachable on <nas-ip>:32751 at all.
Two things to confirm on the NAS before the change, since both are invisible from here: the exact name of the network NPM is attached to, and the proxy host entry's current forward target so it can be repointed in the same pass.
**Q: Does Nginx Proxy Manager run as its own container, or as a Synology package / host service?**
A: Its own container.
That settles the fix, and it is the stronger of the two. `127.0.0.1:32751:3000` is **wrong** here and would break QA rather than secure it — container-to-container traffic does not arrive over the host's loopback interface, so the proxy would stop reaching the app.
The fix is a shared Docker network between the proxy and this stack, with the `ports:` block removed altogether. Nothing is then published to the host at all, and an unpublished port cannot be reached from outside that network — which is a better outcome than a loopback binding, since it does not depend on anything else on the NAS being trustworthy.
What that means in practice for `docker-compose.qa.yml`: declare the proxy's existing network as `external: true`, attach the app service to it, and delete the `ports:` mapping. The proxy then addresses the app by its container name and port 3000 rather than by host IP and 32751, so the proxy host entry needs updating in the same change — QA is reachable on its hostname throughout, and stops being reachable on `<nas-ip>:32751` at all.
Two things to confirm on the NAS before the change, since both are invisible from here: the exact name of the network NPM is attached to, and the proxy host entry's current forward target so it can be repointed in the same pass.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
docker-compose.qa.ymlmaps the app with:An unqualified host port binds to 0.0.0.0, so the QA app answers directly on
http://<nas-ip>:32751from anywhere on the LAN, bypassing Nginx Proxy Manager and its TLS.What this does and does not expose
It does not defeat the admin gate, and that is worth establishing before treating this as urgent.
backend/src/middleware/adminGate.tsrequires thex-admin-gateheader to matchADMIN_GATE_SECRET, and the proxy is what injects it. A direct request to 32751 therefore arrives without the header and/api/adminrefuses it. That path fails closed, which is the correct design.What it does expose is everything else: the storefront, the customer API, login, registration, and the unauthenticated
POST /api/client-errors, all over plain HTTP on the local network with none of the proxy in front of them. It also makes the QA hostname optional, so anything enforced only at the proxy is enforced only for people who come through the front door.Fix, with a caveat that decides which fix
If Nginx Proxy Manager runs directly on the NAS host, bind the published port to loopback:
The proxy keeps working and direct LAN access stops.
If NPM runs as its own container, that change breaks QA rather than securing it. Container-to-container traffic does not arrive over the host's loopback interface, so the proxy would stop reaching the app. In that case the right fix is a shared Docker network between the proxy and this stack, with the
ports:block removed altogether so nothing is published to the host at all. That is the stronger fix of the two, since an unpublished port cannot be reached by anything outside the network.Establish which of the two NPM actually is before changing anything.
Also check production
Production is a separate Portainer stack that lives outside this repository, so this file says nothing about it. If it carries the same unqualified mapping, the same change matters considerably more there than it does in QA.
Confirmed still present on
main.docker-compose.qa.ymlline 123 is unchanged:Also checked what the file says about networking, since that decides which of the two fixes is correct: it declares no
networks:block at all, so the QA stack sits on the default bridge network compose creates for it. That rules out the two containers already sharing a network today, but it does not settle the question — a proxy container on a different network would still be reaching the app through the published host port, which is exactly the mapping this issue is about removing.So the fix cannot be chosen from the repository alone. The deciding fact is only visible on the NAS: whether Nginx Proxy Manager runs as a Synology package or host service, or as its own container.
127.0.0.1:32751:3000is right, and is a one-line change.ports:block entirely, which is the stronger outcome anyway since an unpublished port cannot be reached from outside the network at all.Parked pending that answer rather than guessing, because guessing wrong here takes QA down.
The production note in the issue body still stands and is the more important half: production is a Portainer stack outside this repository, so if it carries the same unqualified mapping, the same change matters considerably more there than in QA.
Q: Does Nginx Proxy Manager run as its own container, or as a Synology package / host service?
A: Its own container.
That settles the fix, and it is the stronger of the two.
127.0.0.1:32751:3000is wrong here and would break QA rather than secure it — container-to-container traffic does not arrive over the host's loopback interface, so the proxy would stop reaching the app.The fix is a shared Docker network between the proxy and this stack, with the
ports:block removed altogether. Nothing is then published to the host at all, and an unpublished port cannot be reached from outside that network — which is a better outcome than a loopback binding, since it does not depend on anything else on the NAS being trustworthy.What that means in practice for
docker-compose.qa.yml: declare the proxy's existing network asexternal: true, attach the app service to it, and delete theports:mapping. The proxy then addresses the app by its container name and port 3000 rather than by host IP and 32751, so the proxy host entry needs updating in the same change — QA is reachable on its hostname throughout, and stops being reachable on<nas-ip>:32751at all.Two things to confirm on the NAS before the change, since both are invisible from here: the exact name of the network NPM is attached to, and the proxy host entry's current forward target so it can be repointed in the same pass.