|
| 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