Admin access & split container¶
The admin area (everything under /admin) can be isolated from the user portal so a stolen super-admin account alone is not enough — the attacker also needs network access to the admin topology.
Three modes¶
| Mode | Who gets in | Effort | When to use |
|---|---|---|---|
unrestricted (default) |
Anyone with a login | none | Single-person setup, no elevated threat |
ip_allowlist |
Only IPs in your CIDR list | 1 setting | Office IP / VPN subnet — sufficient for most self-hosters |
split_container |
Only callers reaching the admin hostname / VPN / SSH tunnel | high | IT firms with many tenants |
unrestricted and ip_allowlist are toggled directly under Admin → System → Access. split_container requires Compose work — that's what the setup wizard is for.
Split container — the three setup profiles¶
When you enable split_container, Vesana runs two API containers (public + admin). How traffic reaches them depends on who terminates TLS in your setup. There are three profiles:
stack-only — stack-internal nginx terminates TLS¶
Standard self-hoster. The nginx container in the Compose stack listens directly on host port 443 and does TLS itself.
How to recognise it:
# There's an nginx container in the Compose stack:
docker ps | grep nginx
# → vesana-nginx-1 ... 0.0.0.0:443->443/tcp ...
# No additional nginx running on the host:
systemctl status nginx 2>/dev/null | head -1
# → Unit nginx.service could not be found. OR inactive (dead)
What the wizard does: writes the admin vhost to the volume /etc/vesana/nginx-overrides/admin-vhost.conf, the stack-nginx loads it on restart via the include. The compose script also publishes the admin port if needed and runs restart nginx itself.
What you do: paste the script, done.
vor-nginx — nginx (or Caddy / Traefik) on the host terminates TLS¶
You run a reverse proxy outside the Compose stack. It listens on port 443 and forwards HTTP into the stack — either to the stack-internal nginx or directly to the API. Common when several services run on one host (Vesana plus other apps), or when the reverse proxy already existed before Vesana.
How to recognise it:
# An nginx (or Caddy / Traefik) is running as a host service:
systemctl status nginx
# → active (running)
# It listens on port 443:
ss -tlnp | grep ':443 '
# → LISTEN ... users:(("nginx", ...))
# In the Compose stack, nginx is NOT bound to host :443:
docker ps | grep nginx
# → 0.0.0.0:8181->443/tcp OR no 443 mapping at all
Caddy / Traefik / HAProxy count the same as "vor-nginx" — the wizard generates nginx vhost snippets, but the concept (reverse proxy on the host, backend in the container) is identical. With Caddy / Traefik you must translate the generated nginx vhost into your own config.
What the wizard does: generates a script that places the vhost file in /etc/nginx/sites-available/<host>-admin, symlinks into sites-enabled and reloads nginx. Plus a compose script that publishes the api-admin container on 127.0.0.1:8082.
What you do: paste the vhost script as root, then paste the compose script. With Caddy / Traefik translate the vhost manually.
bare-metal — no Compose stack¶
You run Vesana directly via systemd on the host (no Docker). Rare, but it happens. The wizard can't automate much here — you start the second API process yourself.
How to recognise it:
# No api container:
docker ps | grep vesana-api
# Instead: a systemd service:
systemctl status vesana-api
# → active (running)
What you do: create a second systemd unit for api-admin with VESANA_API_MODE=admin on a second port, switch the public API to VESANA_API_MODE=public, route the reverse proxy to the admin port. Plan reference: docs/admin-isolation-plan.md §6.8 variant A.
Which one for me?¶
Q1 — Is Vesana running in Docker Compose?
No → bare-metal
Yes → Q2
Q2 — Do you have a reverse proxy (nginx / Caddy / Traefik)
OUTSIDE the Compose stack listening on port 443?
No → stack-only
Yes → vor-nginx
The wizard in the admin portal has auto-detection that gets it right in most cases. If it misdetects, you can override the profile in step 1.
Conflicts & security notes¶
- Cookie domain: the admin hostname and the user hostname should share the same parent domain, otherwise the session cookie is lost on switch. The wizard suggests
.example.comautomatically when both live under*.example.com. - Custom port ≠ subdomain: browsers do not isolate cookies by port. Port mode (e.g.
:8443) only secures the admin area through permission checks, not through browser origin. If origin isolation matters → subdomain mode + own cert. - Emergency bypass: if you lock yourself out (setting
split_containerbut compose override not active yet), the drift fallback kicks in: ADMIN routes are reachable from localhost only. PlusVESANA_ADMIN_BYPASS=trueas env var (30 min override) orpython -m api.app.cli.admin_recovery reset-mode unrestricted.
Further reading¶
- Operator runbook (in repo):
docs/split-container-setup.md— step-by-step for variant-A activation, including nginx templates and recovery paths. - Plan:
docs/admin-isolation-plan.md§6.8 — architecture decisions (variant A/B/C, cookie scope, drift fallback). - Permissions: Roles & permissions — how
audit_log.view_*andtrash.*_*_scopeinteract with the modes.