Vedetta Core speaks plain HTTP and, by default, binds to loopback only
(127.0.0.1). That is the safe default: on a bare-metal / host-network install
nothing on your LAN can reach Core directly, so an unauthenticated device can't
poke the API before you've finished setup. To reach Core from another machine you
put a reverse proxy in front of it — one that terminates TLS and forwards
requests to Core.
Why not just bind Core to
0.0.0.0and expose it? Core serves the dashboard and API over HTTP with bearer-token auth. Exposing it directly means tokens and data travel in clear text on your LAN. A reverse proxy gives you HTTPS.
Core's bind address is controlled by one environment variable:
| Variable | Default | Meaning |
|---|---|---|
VEDETTA_LISTEN_ADDR |
127.0.0.1 |
Interface Core binds. Loopback by default. |
VEDETTA_PORT |
8080 |
TCP port Core listens on. |
VEDETTA_TRUSTED_PROXIES |
empty | Comma-separated IPs/CIDRs of controlled proxy hops whose forwarded client-IP headers Core may trust for per-source rate limits. |
So out of the box Core listens on 127.0.0.1:8080.
# Bare-metal / systemd: still loopback (recommended; proxy runs on the same host)
VEDETTA_LISTEN_ADDR=127.0.0.1 VEDETTA_PORT=8080 ./vedetta
# Bind a specific internal interface (only if the proxy is on another host).
# Replace 10.0.0.5 with an IP your host actually has on that interface.
VEDETTA_LISTEN_ADDR=10.0.0.5 ./vedetta
# Bind all interfaces — do this ONLY behind a firewall or trusted network
VEDETTA_LISTEN_ADDR=0.0.0.0 ./vedettaDocker is different — and already handled. Inside a container,
127.0.0.1refers to the container itself, so a loopback bind is unreachable through Docker's published port and from sibling containers.docker-compose.ymltherefore setsVEDETTA_LISTEN_ADDR=0.0.0.0for the backend service. That is safe only because the compose network is internal — see §4 for how to keep Core unpublished.
Core authenticates with a bearer token: every dashboard and API request carries
Authorization: Bearer <token>. The dashboard sends it automatically; sensors and
scripts send it explicitly.
⚠️ Do NOT put HTTP Basic auth in front of Core. Basic auth also uses theAuthorizationheader (Authorization: Basic …). A browser request can carry only oneAuthorizationheader, so a Basic-auth layer and Core's Bearer auth collide: the proxy either consumes/rewrites the Bearer (Core then rejects the request) or forwards Basic (also rejected). This guide previously suggested Basic auth — that was wrong; use one of the composable controls below instead.
The proxy's only job for auth is to preserve the Authorization: Bearer header
untouched and terminate TLS. If you want an additional outer control, pick one
that does not touch the Authorization header:
- Network policy (simplest, recommended). Only publish the proxy on a trusted surface — your LAN, a VPN, or a tailnet — and IP-allowlist at the proxy. Core's bearer auth is then your app-layer control and TLS is your transport control.
- A cookie/session auth proxy (advanced, browser only). A forward-auth proxy
(e.g. oauth2-proxy) that gates browser access with a cookie — never the
Authorizationheader — can sit in front. Ensure it passesAuthorizationthrough verbatim. This only gates the dashboard, not the native sensor.
Not available: sensor mTLS. The native
vedetta-sensorcannot present a client certificate today (there is no client-cert flag/config), so do not require mutual TLS at the proxy on any endpoint a sensor uses — it would lock the sensor out. mTLS as a proxy control is only workable for a browser/admin surface that never carries sensor traffic. Use network policy for remote sensors instead.
Sensor enrollment and installer credential checks are rate-limited per source IP.
Core deliberately ignores X-Forwarded-For and X-Real-IP by default; otherwise a
direct client could forge those headers to evade the limit. Without explicit proxy
trust, requests relayed through one proxy safely share that proxy's bucket. That is
usually harmless for a single-admin homelab, but it can let one allowed client consume
the bucket for other installers.
Set VEDETTA_TRUSTED_PROXIES only to the controlled proxy socket peers, never to
the client/LAN ranges being proxied. Include every controlled hop that appears at the
right side of the forwarded chain. Core then walks the chain from right to left,
skips only those trusted hops, and keys the limiter on the closest untrusted client.
For a same-host bare-metal proxy, the value is normally the exact loopback peer(s):
VEDETTA_TRUSTED_PROXIES=127.0.0.1/32,::1/128For Compose, docker-compose.yml passes the variable through to Core and the bundled
frontend now appends a standards-compatible X-Forwarded-For chain. Trust the exact
frontend container address plus any controlled outer TLS-proxy hop represented in that
chain. Container addresses can change on recreation, so either pin them in a dedicated
Compose override/network or update the value and restart Core when they change. Obtain
the current frontend address without treating an arbitrary client range as trusted:
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' vedetta-frontendIf you cannot keep the trusted-hop list exact, leave it empty and retain the safe
shared bucket. Never trust 0.0.0.0/0, ::/0, or a whole user/client subnet.
Proxy the FRONTEND, not the API-only backend. In the Docker deployment Core
(backend, port 8080) serves only the JSON API — it does not serve the dashboard
HTML. The frontend container both serves the dashboard and proxies /api/*
to Core on the same origin, so pointing your TLS proxy at the frontend gives you
the whole journey (dashboard + API) over HTTPS. Pointing it at 8080 would serve
the API but no dashboard.
Assumptions: the frontend container is published loopback-only at
127.0.0.1:3107 (the default — ${VEDETTA_FRONTEND_PORT:-3107}); your public
hostname is vedetta.example.com (replace with your own). If you followed §4 to
keep Core unpublished, the frontend is republished at 127.0.0.1:8088 instead —
use that port below.
With the default Compose mapping, use the effective
VEDETTA_FRONTEND_PORTin place of3107below when it differs from the default, whether it was selected automatically or configured explicitly. Retrieve the value without sourcing.env:./scripts/resolve-host-port.sh VEDETTA_FRONTEND_PORT 3107. If you use the §4 override, keep its fixed8088port instead; that override intentionally replaces the normalVEDETTA_FRONTEND_PORTmapping.
vedetta.example.com {
# Terminate TLS (Caddy provisions the cert automatically) and forward to the
# FRONTEND, which serves the dashboard and proxies /api/* to Core, preserving
# the Authorization: Bearer header. NO basic_auth here.
reverse_proxy 127.0.0.1:3107
# Optional outer control — network policy (allow only your LAN/VPN):
@untrusted not remote_ip 10.0.0.0/8 192.168.0.0/16 100.64.0.0/10
respond @untrusted 403
# NOTE: Do not add client-certificate (mTLS) enforcement here — the native
# sensor cannot present a client cert and would be locked out. Use network
# policy (above) instead.
}server {
listen 443 ssl;
server_name vedetta.example.com;
ssl_certificate /etc/ssl/certs/vedetta.example.com.crt;
ssl_certificate_key /etc/ssl/private/vedetta.example.com.key;
# NOTE: Do not enable client-certificate (mTLS) enforcement
# (ssl_verify_client on) on endpoints a sensor uses — the native sensor
# cannot present a client cert and would be locked out. Use network policy
# below as the outer control instead.
# Optional network policy:
# allow 10.0.0.0/8; allow 192.168.0.0/16; deny all;
location / {
# Forward to the FRONTEND (serves the dashboard + proxies /api/* to Core),
# NOT the API-only backend on 8080.
proxy_pass http://127.0.0.1:3107;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Preserve Core's bearer auth — do NOT add proxy_set_header Authorization.
}
}
# Redirect plain HTTP to HTTPS.
server { listen 80; server_name vedetta.example.com; return 301 https://$host$request_uri; }For a Docker deployment, do not publish Core's port on the host. Let the
frontend container (which already proxies /api to Core over the internal
network) and your TLS proxy be the only ingress.
# docker-compose.override.yml (requires Docker Compose v2.24.4+ for the !reset/!override tags)
services:
backend:
# Compose MERGES the `ports` list across files by APPENDING — an empty
# `ports: []` would leave the base "8080:8080" mapping in place, so Core would
# stay published. The `!reset` tag actually drops the base list, leaving Core
# with NO host port: reachable only on the internal network as http://backend:8080.
ports: !reset []
frontend:
# Your TLS proxy (Caddy/nginx) terminates HTTPS and forwards to the frontend,
# which serves the dashboard and proxies /api/* to backend:8080 same-origin.
# `!override` REPLACES the base mapping (which binds ${VEDETTA_FRONTEND_PORT:-3107}
# on ALL interfaces, i.e. the LAN) instead of appending to it. Republish
# loopback-only, and target the frontend's actual listen port 3000 — nginx in
# the frontend image listens on 3000 (see frontend/nginx.conf and Dockerfile),
# NOT 80.
ports: !override
- "127.0.0.1:8088:3000" # proxy connects here; not exposed to the LAN directly- Core is reachable only on the internal
vedettanetwork (http://backend:8080) — no host port at all. - The dashboard is published on
127.0.0.1:8088only; point your TLS proxy at it (e.g.reverse_proxy 127.0.0.1:8088/proxy_pass http://127.0.0.1:8088;), not the LAN. The frontend then proxies/api/*tobackend:8080over the internal network. - Sensors reach Core through the HTTPS proxy endpoint, not a plaintext port.
Why the
!reset/!overridetags? Compose does not replace list-valued keys likeportsin an override file — it concatenates them. Without these tags the base LAN-facing publications survive and the "keep Core unpublished" goal silently fails. If your Compose predates v2.24.4, instead edit the basedocker-compose.ymlto remove/loopback-bind thoseports:entries directly.
VITE_CORE_BASE lets the dashboard target a Core at a different base URL. Keep
the dashboard and API on the same origin (the default: the frontend serves the
UI and proxies /api to Core). A genuinely cross-origin Core does not work
today: attaching Authorization triggers a CORS preflight, and Core does not yet
implement CORS/OPTIONS. Until it does, treat VITE_CORE_BASE as a same-origin
proxy convenience, not a cross-origin split-deployment feature.
A native vedetta-sensor connecting over HTTPS validates the proxy's certificate
against the host system CA store — that is the only trust mechanism today.
There is no in-app --cacert flag and no way to point the sensor at a custom
CA bundle from the command line; trust is managed at the OS level.
- Public CA (Let's Encrypt via Caddy): trusted automatically by the system CA store — nothing to do.
- Internal CA / self-signed: install your CA into the host trust store
(e.g.
/usr/local/share/ca-certificates+update-ca-certificateson Linux, or the System keychain on macOS) so the sensor's system trust store accepts it. Do not disable verification in production.
Before trusting a deployment, confirm these checks end-to-end:
https://vedetta.example.comloads the dashboard (TLS valid).- An authenticated
GET /api/v1/statusthrough the proxy returns 200 with a bearer token and 401 without one (bearer preserved through the proxy). - An authenticated write (e.g. create a read token) succeeds through the proxy.
- A sensor registers through the proxy endpoint over HTTPS.
- No direct plaintext Core port is reachable from the LAN:
- With the default Compose mapping, obtain the actual host port on the Docker
host with
./scripts/resolve-host-port.sh VEDETTA_BACKEND_PORT 8080. From a different LAN machine,curl http://<host>:<resolved-port>/api/v1/statusmust fail or be refused. - With the §4 override,
docker compose port backend 8080on the Docker host must print no mapping; Core is intentionally unpublished in that topology.
- With the default Compose mapping, obtain the actual host port on the Docker
host with
The reverse proxy handles transport (TLS) and an optional outer control. Core's own API-token auth is independent and still applies:
- Create a first admin token during setup (the onboarding wizard requires the one-time setup code printed to Core's logs on first boot).
- Mint least-privilege read tokens for dashboards/scripts (
{"scope":"read"}) — they can query the read endpoints but never write or reach admin routes. - Once an active admin token exists, unauthenticated reads are rejected.
See Backup, Restore & Rollback for protecting those tokens.
Placeholders only. Every hostname, IP, and path above is a documentation placeholder (
vedetta.example.com,127.0.0.1,10.0.0.5). Substitute your own; never commit real ones.