Skip to content

Reverse proxy & exposure

Forms-Engine ships with no authentication. That is a deliberate v1 posture, not an accident, and it splits the deployment into two zones:

The editor UI and the management API (/api/v1/**). Anyone who can reach them can read every response, edit every questionnaire, and delete anything. Protect them with whatever your infrastructure already does well:

  • a reverse proxy with authentication (basic auth, OAuth proxy, mTLS),
  • a VPN or private network,
  • network policy / firewall rules.

The management API even sends permissive CORS headers — it assumes the network layer is the boundary. Do not expose it and hope obscurity holds.

The public runtime API (/public/v1/**) — what embedded questionnaires call. Its defenses:

  • Unguessable ids. A response id is r_ plus 32 random hex characters (UUIDv4-derived); it is the only credential a respondent session holds, and nothing enumerates them.
  • Per-questionnaire allowed origins. Mutating requests from a disallowed Origin are rejected with 403; reads withhold the CORS header so browsers block them. Configure origins in the editor’s Embed panel.
  • Input caps on everything: answers map ≤ 500 keys / 256 KB total, 10 KB per answer, external refs ≤ 128 chars, definitions ≤ 1 MB.
  • Per-IP rate limits on mutating endpoints, uploads, geocoding, and the ref-status check.
# Public runtime API — internet-facing
location /public/ {
proxy_pass http://backend:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Management API — authenticated
location /api/ {
auth_basic "Forms-Engine management";
auth_basic_user_file /etc/nginx/htpasswd;
proxy_pass http://backend:8080;
}
# Editor — authenticated
location / {
auth_basic "Forms-Engine editor";
auth_basic_user_file /etc/nginx/htpasswd;
proxy_pass http://editor:80;
}

Adjust to taste — the shape that matters is public runtime open, everything else authenticated. Remember to set FE_API_BASE to the public URL so the editor’s embed snippets point at the right place, and terminate TLS at the proxy (embeds on HTTPS pages must call an HTTPS api-base, or browsers block the mixed content).

The backend’s rate limiter keys on the direct TCP peer address, and it does not read X-Forwarded-For out of the box. Behind a reverse proxy, every visitor therefore appears to be the proxy — they all share one bucket, and a burst of legitimate traffic can rate-limit everyone at once.

Fix it with Spring Boot’s standard forwarded-headers support: set

SERVER_FORWARD_HEADERS_STRATEGY=native

on the backend, which makes embedded Tomcat resolve the client address from X-Forwarded-For for requests arriving from private-network proxies (make sure your proxy actually sets that header, as in the nginx example above).

  • MinIO (ports 9000/9001 in the compose stack) has no reason to be publicly reachable — only the backend talks to it. Keep it internal and change the default credentials.
  • MongoDB likewise. The compose file maps its port to the host for convenience during evaluation; remove that mapping in production.
  • There is no CAPTCHA or proof-of-work on public endpoints — rate limiting and origin checks are the current bot story. Single-tenant self-hosted deployments are a modest target, but know what you’re exposing; a pluggable challenge is on the roadmap.