Threat Model
This page is the canonical threat model. It lists what envGo defends against, the exact mechanism used, and — just as importantly — the limits of each defence.
Threat model
| Threat | Mitigation | Where it lives |
|---|---|---|
| Inspect Element / DevTools | Secrets never enter the browser. JavaScript only ever holds placeholder strings | server.serveEnvJS, proxy.inject |
| Network tab sniffing | The browser talks to your own origin; the key is injected and the request is forwarded server-side | proxy.Handler.ServeHTTP |
| Cross-tab / XSS request forgery | Session token plus Host plus Origin validation on /proxy | server.authorized |
| DNS rebinding | Host header must match the bound address, localhost, or the bound host | server.hostAllowed |
| SSRF via arbitrary targets | Proxy is disabled when --allow is empty; otherwise only allowlisted hosts | proxy.validateTarget |
| Abuse of fixed routes | Per-route vars allow-set, per-route rate limit, optional bearer auth | gateway.inject, ratelimit.Limiter |
| Upstream secret leak | scrub_response redacts known secret values from responses | logger.Redact |
| LAN / network exposure | Defaults to binding 127.0.0.1 only; --qr is the one opt-in that widens it to 0.0.0.0 | main.go |
| TLS downgrade | MinVersion is TLS 1.2 when using --tls | main.go |
| PHP code execution | 30-second execution timeout per request | server.servePHP |
| Accidental secret logging | Every log line passes through a redacting logger | logger.Logger.Redact |
| Secrets committed to git | envgo init writes a .gitignore covering .env | main.go doInit |
| Config file disclosure over HTTP | envgo.routes.json, routes.json are always blocked from static serving, and dotfiles are never served | server.serveStatic |
| Tampered self-update | envgo run update refuses to install a download whose SHA256 is not proven to match the release manifest | verifyChecksum, verifyDownload |
Defences in detail
Browser isolation
The browser receives variable names, never values. /__env.js fetches
/__envgo_dashboard/data, reads the vars array, and compares it against element
IDs on the page. Only booleans are stored (window.EnvLoaded[name] = true).
Three-guard handshake on /proxy
Local mode rejects a proxy call unless all three checks pass:
Hostheader matches a known address (anti DNS-rebinding)Originheader is absent or same-origin (anti cross-tab)X-EnvGo-Tokenequals the per-process session token, compared withsubtle.ConstantTimeCompare
The token is 256 bits from crypto/rand, generated once per process start and
served only by /__envgo_token. It has no expiry — it is valid for the life
of the process, so restarting envGo rotates it.
Deny by default
- No
--allow→ the proxy refuses every target - No
default_rate_limitand norate_limit→ unlimited (this is the one place envGo does not deny by default; you must opt in to limiting) - No matching route →
404 - Placeholder not in the route’s
vars→ hard400, never a silent drop
Redaction
The logger is taught every secret value loaded from .env (excluding HOST and
PORT). Every log line, the dashboard’s request history, and scrubbed response
bodies have occurrences replaced with [REDACTED]. Longest values are replaced
first so overlapping secrets are handled deterministically.
Self-update integrity
envgo run update is the only part of envGo that fetches an executable from the
network, so it is the only part that can be attacked by tampering with a
download. It refuses to install anything it cannot prove is the released binary:
- The platform binary is downloaded to a temporary file next to the running
executable, so the final
os.Renamehappens on the same filesystem and is atomic. There is no window where a half-written binary sits at the target path. - The SHA256 of that download is computed and compared against the release’s
SHA256SUMSmanifest. - Only a proven match prints
checksum OKand lets the install continue. - The downloaded binary is then executed with
-has a smoke test, and only then swapped in.
The gate covers the fallback path too. When the GitHub API is unreachable,
envgo still downloads directly from /releases/latest/download/, and that
download goes through the same checksum check — an earlier version skipped
verification entirely on that path.
A digest mismatch always aborts and cannot be overridden. The bytes you received are not the bytes that were published, and only the release author can explain that.
Being unable to verify is also a failure. If the manifest cannot be fetched,
cannot be read, or does not list your platform’s asset, the update stops. These
outcomes are all reported as “unverified” rather than treated as success, because
an attacker who can substitute a binary can equally serve a manifest that omits
it. The startup -h check is not a substitute: any binary can answer -h.
The escape hatch for a network that cannot reach the manifest at all — a corporate proxy, or an air-gapped mirror — is an explicit opt-out:
envgo run update --skip-checksum # or -kIt prints a warning and installs an unverified binary. Use it only when you control the mirror and have verified it yourself.
Known limitations
Be aware of these before trusting envGo with a high-value secret:
--qrdeliberately widens the default exposure. Without it envGo binds127.0.0.1and nothing on the network can reach it.--qrbinds0.0.0.0instead, so any device on the same Wi-Fi can load your static files and read variable names from the dashboard. The/proxyguards above still apply, and secret values are still never sent to the browser — but treat--qras public-facing for the files it serves, and do not use it on an untrusted network. See Sharing with your phone.- Streaming responses are not scrubbed. SSE/streaming passthrough skips redaction entirely to avoid buffering. See Configuration.
- The dashboard exposes variable names. Variable names and request metadata are readable by anyone who can reach the dashboard. It is enabled by default in local mode and opt-in in public mode. Do not enable it on a publicly reachable instance.
/__env.jsdepends on the dashboard data endpoint. Because it reads/__envgo_dashboard/datato learn the variable names, the helper only works where that endpoint is enabled.- Rate limiting is per-process, per-IP. Multiple instances multiply the
effective limit, and behind a proxy without
trust_proxy: trueevery request shares a single bucket. - A failed or missing PHP interpreter falls through to serving the source.
If
phpis not installed, or a script exits non-zero or times out, envGo serves the.phpfile as a static download. Add PHP files only when a PHP interpreter is present. See PHP Support. - The session token is not a user authentication system. It stops a random cross-origin page from using your proxy. It does not authenticate users, and any page served from your own origin can obtain it.
- Static files are served as-is. Anything in your web root — except the
config files and dotfiles — is public, including a
.envthat happens to sit in the served directory. Do not serve the directory that holds your.env. --tlsuses a self-signed certificate. Browsers will warn. Use a reverse proxy with real certificates for anything public.- The self-updater trusts GitHub’s CDN, not a signature. The checksum proves
the download matches the manifest that was served next to it; it does not
protect against a GitHub account or CDN compromise that serves both a
substituted binary and a substituted manifest. For a high-assurance setup,
download the release yourself, verify it out of band, and replace the binary
by hand instead of using
envgo run update.
Security checklist for production
-
.envis in.gitignoreand is not inside the served web root -
--allowis set (local mode) or every route uses an explicitvarsallow-set -
default_rate_limitor per-routerate_limitis set — empty means unlimited - Running behind Caddy/nginx for TLS termination
- envGo bound to
127.0.0.1, not0.0.0.0— and--qrnot passed -
trust_proxy: trueset only when actually behind a proxy you control - Dashboard disabled (no
--dashboard) in public mode -
scrub_responseenabled if upstreams might echo secrets -
.env.examplecontains placeholder values only - Secrets rotated if they were ever committed or logged
-
--skip-checksumis not passed toenvgo run update(it installs an unverified binary)
Reporting a vulnerability
Please report suspected vulnerabilities privately rather than in a public issue, and include reproduction steps plus a description of the impact.
Next steps
- Security Model — how the layers are composed
- Configuration — the settings referenced above
- PHP Support — details of the PHP caveat