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:
What you must protect
Section titled “What you must protect”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.
What is designed to face the internet
Section titled “What is designed to face the internet”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
Originare 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.
Example: nginx in front of the stack
Section titled “Example: nginx in front of the stack”# Public runtime API — internet-facinglocation /public/ { proxy_pass http://backend:8080; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}
# Management API — authenticatedlocation /api/ { auth_basic "Forms-Engine management"; auth_basic_user_file /etc/nginx/htpasswd; proxy_pass http://backend:8080;}
# Editor — authenticatedlocation / { 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).
⚠️ Rate limiting behind a proxy
Section titled “⚠️ Rate limiting behind a proxy”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=nativeon 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).
Other exposure notes
Section titled “Other exposure notes”- 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.