Security model

Port Zero runs a privileged daemon on your machine and, optionally, connects it to a cloud edge. This page states what the daemon reads, what it sends, why it needs elevated privileges, and how the local certificate authority is constrained.

What the daemon reads

To map a running process or container to a *.portzero.local name, the daemon scans local processes. For each one it reads two things.

Process environment variables

The daemon looks for PZ_TUNNEL and its optional companions (PZ_TUNNEL_HTTP_PORT, PZ_TUNNEL_PORTS, PZ_HEALTH_PATH, plus PWD for the working directory), reading each process’s environment block the same way on every platform: the kernel’s frozen copy of the environment captured at process start.

The scan reads the whole environment block into memory transiently but keeps only the value of the specific PZ_TUNNEL* variables it’s looking for; the rest is discarded, never stored, never logged, never transmitted. A process with no PZ_TUNNEL variable is ignored.

Listening TCP ports

To learn which port a discovered process or container listens on, the daemon reads its listening TCP sockets. Only the port numbers are used, to build the local reverse-proxy route. No socket payload is read.

What leaves the machine

Logged out (no cloud account)

Nothing leaves the machine from the daemon. There is no analytics, no telemetry, and no phone-home in the long-running daemon:

  • Tunnel requests are forwarded to http://127.0.0.1:<port> — loopback only.
  • The local dashboard is probed over the overlay — local only.
  • Authentication refreshes a stored token against the cloud API, but only when a token is already stored; logged out, this never runs.

The cloud connection itself is gated on an auth token loaded from disk — with no token, no connector is created and no bytes are sent to any Port Zero server.

One honest exception, and it’s the CLI front-end, not the daemon: running the portzero command spawns a once-daily version check, a plain HTTP GET of a public GitHub release asset. It sends no machine data beyond what any HTTP request reveals (your IP and a User-Agent), runs at most once per 24 hours, and is disabled by setting PZ_TUNNEL_NO_UPDATE_CHECK.

Logged in with cloud tunnels

When you log in and expose a Cloud tunnel, the daemon opens a WebSocket to edge.portzero.net and transmits:

  • Authentication — a message with your auth token, the client version, an OS string, and a machine identifier (your hostname plus a random suffix). The token is obtained at login and stored locally; it’s refreshed automatically.
  • Tunnel registration metadata — for each exposed route: the tunnel domain, the local port, the protocol, and process metadata (working directory, executable path, process name, full command line). If you expose a Cloud tunnel, this process metadata leaves your machine. Local-only tunnels never register with the edge.
  • The tunneled traffic itself — requests arriving from the internet are relayed through the edge to your daemon, which forwards them to 127.0.0.1:<port> and relays the response back. This is the point of a tunnel: the traffic transits Port Zero’s edge.

Be clear about what that transit means: the edge terminates TLS and sees your request and response plaintext. Cloud tunnels are not end-to-end encrypted. A visitor’s HTTPS connection ends at the edge; the edge relays the decoded request to your daemon over a separate encrypted WebSocket. This is deliberate — per-tunnel authentication, IP allow-listing, and path routing all require reading the request. When feedback comments are enabled on your plan, the edge also modifies HTML responses: it injects the comment widget’s script tag before </body> (and strips accept-encoding so it can parse the page). For custom domains, the edge itself holds the certificate’s private key and terminates TLS directly (certificates are issued via ACME DNS-01). The edge does not log or store request or response bodies; it keeps only aggregate request counts and byte totals per domain. If you need nobody-in-the-middle guarantees, serve your own TLS behind the tunnel or keep secrets out of shared dev tunnels.

Why the daemon needs elevated privileges

The Local overlay creates a virtual network interface, installs a route, binds a DNS server on a privileged port, and writes OS resolver configuration — all of which require elevated privileges. See Platform privileges for the exact operations, the per-platform requirements, and what degrades when privileges are missing.

The local certificate authority

To serve https://*.portzero.local without browser warnings, Port Zero generates a local certificate authority (CA) and installs its certificate into the OS trust store (and into Firefox/Chromium NSS databases). This is the part of the design that most deserves scrutiny, because a CA in your trust store is a powerful thing.

The CA private key is never written to disk

The CA private key is generated in memory, used once to self-sign the CA certificate and to sign the *.portzero.local wildcard certificate, and then dropped. It is never serialized to disk. The only private key persisted is the wildcard leaf key, which can impersonate only *.portzero.local names in the first place.

Persisted files live in your platform’s application data directory:

FileContentsPermissions
ca.crtCA certificate (public)default
wildcard.crtWildcard certificate (public)default
wildcard.keyWildcard private key0600 on Unix (owner read/write only)

Name constraints

The CA certificate carries a critical X.509 Name Constraints extension (RFC 5280 §4.2.1.10), so a conforming verifier will reject any certificate the CA signs that steps outside its lane:

  • Permitted DNS subtree portzero.local. Per RFC 5280 this matches the apex name and every subdomain (portzero.local, app.portzero.local, a.b.portzero.local), and nothing else. The CA cannot mint a trusted certificate for example.com.
  • Excluded IP address space 0.0.0.0/0 and ::/0. A DNS permitted subtree alone doesn’t constrain a leaf that presents only an IP-address SAN, because RFC 5280 applies constraints per name-form; excluding the entire IP space closes that bypass. Port Zero never issues IP certificates, so this costs nothing.

You can verify it yourself:

openssl x509 -in "$DATA_DIR/ca.crt" -noout -text | grep -A4 "Name Constraints"

Expect X509v3 Name Constraints: critical, a Permitted DNS:portzero.local, and an Excluded IP:0.0.0.0/0.0.0.0.

Honest caveats

  • Not every trust store enforces name constraints. Modern browsers (Chrome/Chromium, Firefox, Safari) and the OpenSSL and macOS/Windows platform verifiers do enforce them. But enforcement lives in the verifier, not the certificate: a client that ignores the extension gets no protection from it. Name constraints are defense-in-depth, not a substitute for keeping the trust store honest.
  • Constraints bound DNS and IP identities only. A verifier doing TLS server authentication matches a site against DNS and IP SANs, which is exactly what we constrain. Other SAN forms (email, URI) aren’t name-forms a browser uses for server identity, so they aren’t an HTTPS MITM vector.
  • The CA key would still matter if extracted at generation time. Because the key exists in daemon memory only during generation, an attacker would need to read that memory in that window. If they did, the name constraints still limit what the stolen key can forge.

Migration from earlier, unconstrained CAs

Port Zero originally shipped an unconstrained CA. If you installed one:

  • portzero trust generate detects a CA that lacks name constraints, regenerates the whole bundle in place, and tells you to re-run sudo portzero trust install. The old, unconstrained anchor stays trusted until you do.
  • portzero trust install removes any previously installed “PortZero Local CA” anchor before adding the new one, so you don’t accumulate stale roots.
  • The daemon never regenerates the CA on its own. If it loads an unconstrained CA it logs a one-line warning pointing at portzero trust generate && sudo portzero trust install, because silently swapping the CA would break trusted HTTPS until you reinstalled it.

Reporting a problem

If you find a way for the daemon to send data it shouldn’t, or a way for the constrained CA to sign something outside *.portzero.local, please report it via the GitHub repository.