Skip to content

Production deployment hardening: rootless Docker, single-origin Apache proxy, CSP - #1

Open
mantkiew wants to merge 8 commits into
mainfrom
server-setup
Open

Production deployment hardening: rootless Docker, single-origin Apache proxy, CSP#1
mantkiew wants to merge 8 commits into
mainfrom
server-setup

Conversation

@mantkiew

Copy link
Copy Markdown
Collaborator

Summary

Hardens the production deployment for a single-host Apache + rootless Docker setup, and gives the frontend its own explicit URL prefix so it coexists safely with other sites on the same Apache instance.

Changes

Remove CORS (no longer needed)

  • Both the frontend and API are served through Apache under the same origin (https://wiselab.uwaterloo.ca), so cross-origin requests never occur in production.
  • Removed CORSMiddleware and ALLOWED_ORIGINS from server/app.py, docker-compose.yml, and docker_setup.md.
  • In development, the frontend now routes API calls through the Next.js dev-server proxy (frontend/next.config.ts rewrites) instead of making direct cross-origin requests to localhost:8000.

Explicit path prefixes (avoid catch-all proxy)

  • Previously Apache proxied / (catch-all) to the Next.js container, which would have silently intercepted requests to other unrelated sites hosted on the same Apache instance (e.g. /t3-necsis/).
  • The frontend now gets its own explicit prefix, /DriverQ, via Next.js's basePath config (new BASE_PATH build arg).
  • The API prefix is /DriverQServer (was /DriverQ) so the two prefixes are visually distinct.
  • Apache strips the /DriverQServer prefix before forwarding to FastAPI (its routes are prefix-unaware and JSON responses have no self-referencing URLs), but preserves the /DriverQ prefix when forwarding to Next.js, since basePath makes the app itself prefix-aware (its HTML/JS/RSC payloads embed /DriverQ/... URLs that Apache cannot rewrite in response bodies). This asymmetry is documented in apache-setup.md.
  • NEXT_PUBLIC_API_BASE_URL and BASE_PATH are now path-only (no hostname baked into the build), making the Docker image portable across deployment hosts.

Apache hardening

  • ProxyRequests Off — disables forward-proxy behavior.
  • ServerTokens Prod / ServerSignature Off — suppress Apache version disclosure in headers and error pages (documented as global directives, not per-vhost).
  • Added a Content-Security-Policy header with a default-src 'none' baseline. connect-src 'self' is fully strict (blocks exfiltration to external origins from any injected script). script-src/style-src require 'unsafe-inline' due to Next.js RSC streaming hydration and React inline style={{}} props — documented as a known limitation with a follow-up path (nonce-based CSP via Next.js middleware).

Firewall / network exposure clarification

  • Removed misleading ufw deny 8000/tcp / ufw deny 3000/tcp rules from apache-setup.md. The real protection is the existing 127.0.0.1 loopback binding in docker-compose.yml, which makes those ports unreachable from any external network interface — there's nothing for UFW to block. (Also noted: on root Docker, UFW rules for published ports are ineffective anyway since Docker inserts its own iptables rules ahead of UFW's chains; this deployment uses rootless Docker where UFW rules would apply, but they're redundant given the loopback binding.)

Files changed

  • server/app.py — remove CORS middleware
  • frontend/app/lib/api.ts — use relative URLs in dev to route through Next.js proxy
  • frontend/next.config.ts — add basePath support
  • frontend/Dockerfile — add BASE_PATH build arg
  • docker-compose.yml — drop ALLOWED_ORIGINS, add BASE_PATH default, rename API prefix default
  • docker_setup.md — update setup instructions for new/removed env vars
  • apache-setup.md — single-origin architecture diagram, explicit per-service ProxyPass prefixes, CSP header, ServerTokens/ServerSignature/ProxyRequests hardening, corrected firewall guidance

Deployment notes

When deploying this branch, the Apache vhost config in apache-setup.md must be updated to match (new /DriverQServer API prefix, new /DriverQ frontend prefix with preserved path, new security headers). No ALLOWED_ORIGINS env var needs to be set anymore.

mantkiew added 3 commits May 15, 2026 11:11
Deployment improvements:
- Remove CORSMiddleware from FastAPI: CORS is unnecessary when Apache collapses
  both frontend (:3000) and API (:8000) into a single origin.
- Use Next.js dev proxy for API calls: frontend dev now routes through Next.js
  server instead of direct cross-origin requests, eliminating CORS in all
  environments.
- Switch NEXT_PUBLIC_API_BASE_URL to path-only prefix (/DriverQ): build is now
  host-independent and portable across deployment domains.
- Remove ALLOWED_ORIGINS env var and configuration.

Apache hardening:
- Add ProxyRequests Off to prevent forward-proxy abuse.
- Add ServerTokens Prod and ServerSignature Off to reduce information leakage
  in Server header and error pages.
- Add Content-Security-Policy header with strict defaults:
  - default-src 'none' baseline
  - script-src 'self' 'unsafe-inline' (Next.js RSC streaming limitation)
  - style-src 'self' 'unsafe-inline' (React inline styles)
  - connect-src 'self' (strict: no exfiltration to external origins)
  - All other resources (img, font, worker, object) locked to 'self' or 'none'.

Documentation:
- Clarify that loopback binding (127.0.0.1) is the real defense, not UFW rules.
- Explain that CORS is a browser check, not relevant when behind a reverse proxy.
Apache was proxying / as a catch-all to the Next.js container, which
would silently swallow requests for other unrelated sites hosted on the
same Apache instance. Replace it with an explicit
prefix for each service, matching the existing /DriverQ pattern already
used for the API.

- Add Next.js  support (frontend/next.config.ts) via a new
  BASE_PATH build arg (frontend/Dockerfile, docker-compose.yml),
  defaulting to /DriverQ for the frontend.
- Move the API to /DriverQServer (was /DriverQ) so the two prefixes are
  visually distinct; NEXT_PUBLIC_API_BASE_URL now defaults to
  /DriverQServer.
- Update Apache config: /DriverQServer strips its prefix before
  forwarding to FastAPI (routes are prefix-unaware, JSON has no
  self-referencing URLs). /DriverQ preserves its prefix when forwarding
  to Next.js, since basePath makes the app itself prefix-aware
  (HTML/JS/RSC payloads embed /DriverQ/... URLs that Apache cannot
  rewrite in response bodies). Document why this asymmetry is
  intentional, and fix a stale comment that incorrectly said the
  frontend prefix was stripped.
- Update architecture diagram to show both explicit prefixes plus
  other co-hosted sites passing through untouched.
- docker-compose.yml / docker_setup.md: give both env vars sensible
  defaults so  works without manual configuration;
  document how to override either prefix.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens the production deployment to run behind a single Apache origin with explicit per-service URL prefixes, reducing cross-site interference and tightening container/runtime security.

Changes:

  • Removed CORS middleware and shifted dev API access to same-origin proxying via Next.js rewrites.
  • Added explicit URL prefixes (/DriverQ frontend via basePath, /DriverQServer API) and updated Compose build args accordingly.
  • Added deployment hardening docs/config guidance (Apache proxy rules, CSP/security headers, loopback-only port binding, and container privilege reductions).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
server/Dockerfile Drops root by introducing an unprivileged runtime user for the API container.
server/app.py Removes CORS middleware now that production is same-origin behind Apache.
frontend/next.config.ts Adds basePath support and keeps dev rewrites for local proxying.
frontend/Dockerfile Adds BASE_PATH build arg and switches the runtime to a non-root user.
frontend/app/lib/api.ts Uses relative API URLs by default to support same-origin routing/proxying.
docker-compose.yml Binds services to 127.0.0.1 and adds container hardening options; updates build args for prefixes.
docker_setup.md Updates local setup docs for new/removed env vars and prefix defaults.
apache-setup.md New production Apache configuration guidance: explicit ProxyPass prefixes, CSP, and hardening notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/next.config.ts Outdated
Comment thread frontend/Dockerfile
Comment thread server/Dockerfile
Comment thread docker_setup.md
- next.config.ts: dev API proxy target now falls back to localhost:8000
  when NEXT_PUBLIC_API_BASE_URL is not an absolute URL (it's path-only
  in production), instead of producing an invalid rewrite destination.
- frontend/Dockerfile: set BASE_PATH in the runner stage too, since
  next.config.ts is reloaded at runtime and would otherwise serve at
  "/" instead of the built-in prefix. Replace RUN chown -R with
  COPY --chown on each layer to avoid a redundant, size-doubling copy.
- server/Dockerfile: chown /app to appuser before switching users, so
  runtime writes under the working directory don't fail.
- docker_setup.md: document that the app is served at
  http://localhost:3000/DriverQ/ (not /) given the BASE_PATH default.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread docker_setup.md
Comment thread frontend/next.config.ts
mantkiew added 3 commits July 14, 2026 11:55
Switch ObjectInspector from a hardcoded /api/... fetch to the shared apiUrl(...) helper so production requests are correctly prefixed (for example /DriverQServer/api/...) when served behind Apache path-based proxying.

Prevents object detail fetch failures caused by bypassing the configured API base path.
Connect to scene_data.db via a  URI with mode=ro&immutable=1 instead of a plain path. WAL mode is persisted in the database file header, so a normal connection attempts locking/WAL reconciliation on open — which fails with 'attempt to write a readonly database' when the file is mounted read-only in Docker.

immutable=1 tells SQLite the file will not change, skipping all locking and change detection, so read queries succeed against the read-only mount.
@mantkiew
mantkiew requested a review from bluebarryz July 14, 2026 16:58
@mantkiew

Copy link
Copy Markdown
Collaborator Author

@bluebarryz I have the deployment working: https://wiselab.uwaterloo.ca/DriverQ/
What do you think about the review comments suggested by copilot?

- next.config.ts: validate BASE_PATH eagerly (must be empty or start
  with '/' and not end with '/'), matching Next.js's own basePath
  constraints, so misconfiguration fails fast with a clear error
  instead of a confusing downstream routing issue.

- docker_setup.md: document that 'docker compose up' alone does not
  route /DriverQServer/* to the api container — that's Apache's job
  in production (apache-setup.md). Without a reverse proxy in front,
  API calls will 404 against the frontend container. Add guidance for
  testing locally without Apache (local reverse proxy, or `npm run dev`
  + 'docker compose up api').

Addresses the two still-open threads from the Copilot review on PR #1;
the other four (dev proxy fallback, BASE_PATH in Dockerfile runner
stage, COPY --chown, server/Dockerfile ownership) were already fixed
in earlier commits.
@mantkiew

Copy link
Copy Markdown
Collaborator Author

@bluebarryz I noticed that there are camera images missing for the cut-in scenarios (all others have images).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants