Securing a Self-Hosted Server¶
When you run Inference on your own hardware, you own its security posture. A locally deployed server does not enforce authentication, encryption, or network restrictions by default — it is built to be easy to start, not to be safe to expose. Out of the box it will answer any request that reaches it, including requests to run models and execute Workflows.
This page covers the five controls every self-hosted deployment should review before it handles anything beyond local development traffic. They are complementary — apply as many as your environment allows.
This is your responsibility
Roboflow secures the managed Cloud, Serverless, and Dedicated Deployment offerings. For a server you run yourself, securing the host, the network around it, and the credentials it accepts is your responsibility. If your server is reachable from an untrusted network without the controls below, treat it as open to the world.
1. Restrict network access¶
The single most effective control is to not expose the server in the first
place. Inference listens on port 9001 by default and has no concept of a
"trusted" network — anything that can reach the port can use it.
- Bind to localhost when only processes on the same host need it
(e.g. publish the container port as
127.0.0.1:9001:9001instead of9001:9001). - Keep it on a private network / VPC and reach it through a VPN, SSH tunnel, or service mesh rather than a public IP.
- Use host and cloud firewalls / security groups to allow
9001only from the specific clients that need it. - Put a reverse proxy in front of it (nginx, Traefik, Caddy, a cloud load balancer) if you need to expose it more broadly — that gives you a single place to add TLS, rate limiting, and access logging.
Never publish the inference port directly to the public internet without authentication and TLS in place.
2. Enforce authentication¶
By default a self-hosted server does not require an API key to make requests — as a result,
beyond the auth that happens at the Roboflow API level when fetching data from the platform, there is
no additional security on the server itself. To turn on authentication, set
WORKSPACES_WHITELISTED_FOR_LOCAL_DEPLOYMENT to a comma-separated list of the Roboflow workspace slug
allowed to use the server:
docker run --rm -p 9001:9001 \
-e WORKSPACES_WHITELISTED_FOR_LOCAL_DEPLOYMENT=your-workspace-url-slug,another-workspace-url-slug \
roboflow/roboflow-inference-server-cpu:latest
With this set, the server installs an authorization middleware. Every
inference and Workflow request must carry an api_key (as a query parameter
or in the JSON body) that resolves — via Roboflow — to one of the whitelisted
workspaces. Requests with a missing, invalid, or non-whitelisted key are
rejected with 401 Unauthorized.
What is not covered by the API-key check
A small set of unauthenticated endpoints stay open so the server remains
usable and observable: /, /docs, /redoc, /info, /healthz,
/readiness, /metrics, /openapi.json, and static assets
(/static/..., /_next/...). Treat /info and /metrics as
information that anyone who can reach the server can read, and rely on
network restrictions (control #1) to limit who that is.
Bring your own auth. The built-in check ties authorization to Roboflow
workspaces. If you have your own identity model, you can instead place a
reverse proxy or authentication middleware in front of the server — enforcing
OAuth/OIDC, mTLS, signed headers, an API gateway, or whatever your
organization already uses — and let only authenticated traffic through to
port 9001. The two approaches can be combined.
3. Enable TLS when the network requires it¶
The built-in API-key check sends credentials in the request. If those requests travel over any network you do not fully control, the connection must be encrypted, otherwise keys and payloads are exposed in plaintext.
You have two options:
- Terminate TLS at a reverse proxy / load balancer in front of the server. This is the usual choice when you already run one.
- Serve HTTPS directly from the server by mounting a certificate and
key and setting
ENABLE_HTTPS=true. See Serving inference over HTTPS for the full guide, including mutual TLS (client certificates) viaSSL_CA_CERTS.
For purely local, loopback-only traffic (control #1, bound to 127.0.0.1)
TLS is optional. Any time requests leave the host over an untrusted network,
TLS is required.
4. Disable custom Python execution in Workflows¶
Workflows can contain Custom Python blocks — arbitrary Python that runs inside the server process. This is a powerful feature, but it means that anyone who can submit a Workflow to the server can run arbitrary code on your host. On a server reachable by untrusted clients, that is remote code execution.
This is controlled by ALLOW_CUSTOM_PYTHON_EXECUTION_IN_WORKFLOWS.
| Setting | Effect |
|---|---|
True (current default) |
Workflows may define and run custom Python blocks. |
False |
Custom Python blocks are rejected; all other Workflow features still work. |
If your Workflows do not rely on custom Python, set it to False:
docker run --rm -p 9001:9001 \
-e ALLOW_CUSTOM_PYTHON_EXECUTION_IN_WORKFLOWS=false \
roboflow/roboflow-inference-server-cpu:latest
The default is changing on 2026-06-19
Today this flag defaults to True for backward compatibility. On
2026-06-19 the default will change to False. If your Workflows
depend on custom Python blocks, set
ALLOW_CUSTOM_PYTHON_EXECUTION_IN_WORKFLOWS=true explicitly so they
keep working after that date. Otherwise, leave it disabled — and prefer
enabling it only on deployments where the network and authentication
controls above are already in place.
5. Restrict image fetching from URLs (SSRF)¶
Inference can load images straight from a URL supplied in the request
({"image": {"type": "url", "value": "https://..."}}). Any time a server
fetches a URL that a caller controls, the caller can try to steer it into
making requests on their behalf — a class of attack called Server-Side
Request Forgery (SSRF). Someone who cannot reach your internal network
directly can ask your server to fetch, for example:
http://169.254.169.254/latest/meta-data/— the cloud metadata service (AWS/GCP/Azure), which can hand back instance credentials.http://127.0.0.1:9001/...and otherlocalhostservices — admin panels, databases, or the inference server's own unauthenticated endpoints.http://10.0.0.5/,http://192.168.1.1/, and other private (RFC1918), link-local, CGNAT, or IPv6 ULA hosts that sit behind your perimeter.
A public-looking hostname is not proof of a public target: it may resolve to a private IP, redirect to one, or use DNS rebinding (resolve to a public IP for the validation check, then a private IP for the actual connection). Inference ships controls for all of these.
Turn URL input off if you don't need it¶
The strongest control is to not accept URL images at all. If your clients always send images as base64 or file uploads, disable URL fetching outright:
docker run --rm -p 9001:9001 \
-e ALLOW_URL_INPUT=false \
roboflow/roboflow-inference-server-cpu:latest
Harden URL input when you do need it¶
When URL images are required, these flags narrow what the server is allowed to fetch. Together they reject internal targets, pin the connection to the validated IP (defeating DNS rebinding), and re-check every redirect hop:
| Variable | Default | Effect |
|---|---|---|
ALLOW_URL_INPUT |
True |
Master switch for URL image input. False rejects all URL images. |
ALLOW_URL_TO_NON_GLOBAL_ADDRESSES |
True |
When False, a URL whose host resolves to a non-global address (loopback, private, link-local/metadata, CGNAT, IPv6 ULA, …) is rejected, and the connection is pinned to the validated IP so a second DNS answer cannot swap the target. |
VALIDATE_IMAGE_URL_REDIRECTS |
False |
When True, redirects are followed one hop at a time and each hop URL is re-validated, instead of being followed blindly. |
MAX_IMAGE_URL_REDIRECTS |
30 |
Hard cap on redirect hops, enforced regardless of the flag above. |
ALLOW_NON_HTTPS_URL_INPUT |
False |
When False, only https:// URLs are accepted. |
ALLOW_URL_INPUT_WITHOUT_FQDN |
False |
When False, URLs whose host is a bare IP or has no public suffix are rejected — callers must use a real domain name. |
WHITELISTED_DESTINATIONS_FOR_URL_INPUT |
(unset) | Comma-separated allow-list of destinations (subdomain.domain.suffix). When set, only these are permitted. |
BLACKLISTED_DESTINATIONS_FOR_URL_INPUT |
(unset) | Comma-separated block-list of destinations that are always rejected. |
A hardened configuration that still allows public HTTPS image URLs:
docker run --rm -p 9001:9001 \
-e ALLOW_URL_TO_NON_GLOBAL_ADDRESSES=false \
-e VALIDATE_IMAGE_URL_REDIRECTS=true \
roboflow/roboflow-inference-server-cpu:latest
For the tightest control, add an allow-list so the server can only reach the exact hosts you serve images from:
docker run --rm -p 9001:9001 \
-e ALLOW_URL_TO_NON_GLOBAL_ADDRESSES=false \
-e VALIDATE_IMAGE_URL_REDIRECTS=true \
-e WHITELISTED_DESTINATIONS_FOR_URL_INPUT=images.example.com,cdn.example.com \
roboflow/roboflow-inference-server-cpu:latest
Two defaults are changing in Q4 2026
ALLOW_URL_TO_NON_GLOBAL_ADDRESSES (→ False) and
VALIDATE_IMAGE_URL_REDIRECTS (→ True) currently default to the legacy,
permissive behaviour for backward compatibility. Both defaults are
scheduled to flip to the secure values in Q4 2026. Set them explicitly
now — to the secure values to opt in early, or to the legacy values if a
workflow genuinely depends on fetching internal URLs — so the change does
not surprise you.
Proxies bypass this protection
If an HTTP(S) proxy is configured for the server, the proxy — not Inference — resolves the destination, so non-global blocking and connection pinning cannot be enforced (the server emits a warning when it detects this). Restrict what the proxy itself can reach if you rely on these controls.
Same controls in the Python SDK
The inference-sdk client applies the same URL policy and SSRF protections
— and reads the same environment variables — when it loads images from
URLs, so a client that hydrates URL images before sending them is covered
too.
Recommended baseline¶
For any self-hosted server that is reachable beyond localhost:
- [ ] Network access restricted to known clients (firewall / private network / proxy).
- [ ]
WORKSPACES_WHITELISTED_FOR_LOCAL_DEPLOYMENTset, or your own auth in front. - [ ] TLS terminated at the server or an upstream proxy.
- [ ]
ALLOW_CUSTOM_PYTHON_EXECUTION_IN_WORKFLOWS=falseunless you genuinely need it. - [ ] URL image input disabled (
ALLOW_URL_INPUT=false), or hardened withALLOW_URL_TO_NON_GLOBAL_ADDRESSES=falseandVALIDATE_IMAGE_URL_REDIRECTS=true(plus an allow-list where you can).