Skip to content

Security Model

xeri edited this page Jul 14, 2026 · 1 revision

Security Model

You are about to put a port on the public internet and point it at a machine inside your home network. It's fair to want to know exactly what's protecting it.

What is encrypted, and what is trusted

Agent ↔ gateway: TLS 1.3, and nothing else. Both sides refuse anything older. The tunnel between your machine and the gateway is encrypted and authenticated.

Players ↔ gateway: whatever Minecraft does. proxyforward doesn't change that. The player-facing leg is ordinary Minecraft traffic on an ordinary TCP port.

Trust comes from a pin, not a CA. On first start, the gateway generates a self-signed ECDSA P-256 certificate. There is no certificate authority, no Let's Encrypt, no third party in the trust path — and so nothing to leak, expire or get tricked into issuing. Instead:

  • the certificate's SHA-256 fingerprint is carried out of band, inside the pairing code;
  • the agent pins that exact fingerprint, and verifies the gateway against it on every connection;
  • a mismatch is a hard failure with a message telling you to re-pair.

The certificate is long-lived on purpose. Its expiry isn't what's protecting you — the pin is.

The pairing code

pf1://mc.example.com:8474/<token>#sha256:<64 hex>
        │              │      │            │
        └ gateway host ┘      │            └ the certificate to pin
                              └ the shared auth token

Two secrets, one string, delivered by a channel you already trust (you copy it between your own two machines).

Treat the pairing code like a password. Whoever holds it can authenticate to your gateway and register tunnels on it. Don't paste it into a public issue, a Discord channel, or a screenshot. If it leaks, rotate the token (Settings → Security) — every agent then has to re-pair.

Both the token and the fingerprint are compared in constant time, so an attacker can't recover them a byte at a time by measuring how long a rejection takes.

Surviving the public internet

Your gateway's control port will be found by scanners. It's built for that.

  • The unauthenticated prologue is on a short clock. Everything before authentication — the TCP accept, the TLS handshake, the first frame — must complete within a hard deadline or the connection is dropped. Nobody gets to hold a socket open doing nothing.
  • Pre-auth frames are capped far smaller than post-auth frames, and the size is checked before any memory is allocated. A scanner cannot make the gateway allocate a large buffer by claiming a huge frame.
  • Failed authentication is rate-limited per IP, fail2ban style. Successful authentications never count toward the limit — so an agent on a flaky connection that reconnects fifty times is never locked out, while a token brute-forcer is.
  • Connection gates, globally and per source IP, cap how many public connections can exist at once (defaults in Configuration).
  • A port allowlist (gateway.port_allowlist) can restrict which public ports an agent is allowed to ask for. Empty means any — set it if you don't fully trust the holder of the pairing code.

The exact deadlines, caps and limits are compile-time constants, listed in docs/agent/architecture.md → "The numbers". They're deliberately not restated here, so this page can't quietly go stale.

Every parser that an unauthenticated stranger can reach — the control frames, the Minecraft handshake, the login sniffer — is continuously fuzzed in CI.

Identity: one agent, no flapping

The agent has a persistent random identity, separate from the token.

  • The same agent reconnecting supersedes its old session. That's what makes a reconnect after a network blip clean, rather than leaving a zombie session holding your port.
  • A different agent presenting the same token is rejected, with a clear message. Without this, two machines sharing a pairing code would fight, each kicking the other off forever.

If you see "another agent is already connected", that's this rule doing its job — see Troubleshooting.

Failing safely

Authentication failures that can never succeed by retrying — a bad token, an agent conflict, an incompatible protocol version — stop the agent instead of making it hammer the gateway forever. The reason is surfaced in the UI rather than buried in a log.

On your machine

  • The named pipe the GUI uses to talk to the engine is local-only, and its permissions admit only Administrators, SYSTEM and the interactive user. It is not reachable from the network.
  • The Windows Firewall rule is scoped to the program, not to a port — so it doesn't quietly leave a port open after you change your config.
  • Diagnostics exports are redacted: secrets, hostnames, IP addresses and player identities are stripped, with peer IPs replaced by stable pseudonyms. There's an automated test that fails the build if anything leaks.

What this does not protect against

Be clear-eyed:

  • Your config file holds the auth token in plain text. Anyone who can read %APPDATA%\proxyforward\config.toml has it.
  • The gateway machine sees all player traffic in the clear as it relays it. It's a proxy — that's inherent. Run your gateway somewhere you actually control.
  • proxyforward does not authenticate players. That's Minecraft's job (online mode). An offline-mode server behind a tunnel is still an offline-mode server.
  • The binaries are unsigned. Verify a release's provenance attestation instead — see Installation.

Clone this wiki locally