Skip to content

Commit bb49def

Browse files
committed
proxy security
1 parent 397bde2 commit bb49def

File tree

4 files changed

+51
-4
lines changed

4 files changed

+51
-4
lines changed

colandr/app.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ def _create_app_v1_1(
3030
if config_overrides:
3131
app.config.update(config_overrides)
3232

33-
# Configure ProxyFix to trust X-Forwarded-Proto header from nginx
34-
# This ensures Flask detects HTTPS when behind a reverse proxy
35-
# ProxyFix returns a WSGI middleware object; cast keeps type-checkers happy.
33+
# Trust X-Forwarded-Proto/Host only when the sole client is our reverse proxy.
34+
# See docs/proxy-security.md: never expose port 5000 to the public.
3635
app.wsgi_app = t.cast(t.Any, ProxyFix(app.wsgi_app, x_proto=1, x_host=1))
3736

3837
@app.before_request

compose.prod.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ services:
3131
- BUILD_TARGET=prod
3232
- FLASK_ENV=production
3333
ports:
34-
- "5000:5000"
34+
# Bind to localhost only so only the reverse proxy (e.g. nginx on this host)
35+
# can reach the API. Required for ProxyFix security (see docs/proxy-security.md).
36+
- "127.0.0.1:5000:5000"
3537
restart: unless-stopped
3638
volumes:
3739
- ${COLANDR_DATA_DIR:-./colandr_data}:${COLANDR_FILESYSTEM_ROOT_DIR:-/app/data}

docs/for-developers.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Colandr's back-end system consists of multiple services defined and configured in `compose.yaml` and two `Dockerfile`s, including a PostgreSQL database, Flask API server, and Redis broker+worker.
44

5+
For production deployments behind a reverse proxy (nginx), see [Proxy security](proxy-security.md) for how to expose the API securely.
6+
57
## local setup
68

79
These instructions describe a one-time setup, started from scratch. They assume you're on a machine running macOS or Linux.

docs/proxy-security.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Proxy security (X-Forwarded-* and host-header poisoning)
2+
3+
The Flask app uses [Werkzeug ProxyFix](https://werkzeug.palletsprojects.com/en/stable/middleware/proxy_fix/) so that behind a reverse proxy (e.g. nginx) it correctly sees HTTPS and the public host (for `request.url`, OpenAPI "Try it out", redirects, etc.). ProxyFix trusts the `X-Forwarded-Proto` and `X-Forwarded-Host` headers.
4+
5+
**Security requirement:** Those headers are only safe to trust if the **only** client that can reach the Flask app is your reverse proxy. If the app is reachable directly by arbitrary clients (e.g. port 5000 exposed to the internet), a client could send spoofed `X-Forwarded-*` headers and influence generated URLs (host-header poisoning). So the app must **never** be exposed directly to the public.
6+
7+
## How to set it up
8+
9+
### Docker Compose (production)
10+
11+
- **Nginx on the same host:** Publish the API port only on localhost so only nginx on the host can connect:
12+
```yaml
13+
ports:
14+
- "127.0.0.1:5000:5000"
15+
```
16+
(This is already set in `compose.prod.yaml`.)
17+
18+
- **Nginx in Docker (same network):** Do **not** publish port 5000. Nginx should connect to the API using the service name, e.g. `http://colandr-api:5000`. Only nginx (and other containers on the same network) can reach the API.
19+
20+
### AWS
21+
22+
- **ECS/Fargate + ALB:** The API task should listen on 5000 only inside the task. The ALB target group points at the task’s port 5000. Ensure the API task’s security group allows inbound 5000 **only** from the ALB (or from the nginx task if nginx is in front of the ALB). Never allow 0.0.0.0/0 to port 5000 on the API.
23+
24+
- **EC2 + nginx on same instance:** Bind the API to 127.0.0.1:5000 (or run it in a container with `127.0.0.1:5000:5000`). Nginx on the instance proxies to localhost:5000. Port 5000 is not in the security group or is only allowed from localhost.
25+
26+
### Nginx
27+
28+
Have nginx **set** (overwrite) the headers instead of forwarding client values:
29+
30+
```nginx
31+
proxy_set_header X-Forwarded-Proto $scheme;
32+
proxy_set_header X-Forwarded-Host $host;
33+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
34+
```
35+
36+
That way the app only ever sees values from nginx, not from the client.
37+
38+
## Summary
39+
40+
| Requirement | Action |
41+
|------------|--------|
42+
| App only reachable by proxy | Do not expose 5000 to the public; use 127.0.0.1 or internal network only. |
43+
| AWS security groups | Allow inbound 5000 to the API only from ALB/nginx, never 0.0.0.0/0. |
44+
| Nginx | Set `X-Forwarded-Proto` and `X-Forwarded-Host` (overwrite client headers). |

0 commit comments

Comments
 (0)