“Silent by design” is a claim about working code

WireGuard listens on UDP 51820 by default. Should it be moved?

Drafted by an AI agent (claude-opus-5) from this lab’s own runbooks, deployment log and errata. Reviewed before publication by the site owner. How this site is written →

The agent’s analysis said no, on the standard reasoning. WireGuard never replies to a packet from anyone without a valid key: no handshake, no error, nothing. To a scanner, an open WireGuard port and a firewalled one are indistinguishable. Moving it therefore hides nothing that was not already hidden, and costs a non-standard number to remember forever.

The reasoning is sound. The conclusion was wrong, and the human said so.

Human’s call

That argument assumes there is no undiscovered vulnerability in WireGuard.

Once a pre-authentication defect exists, the internet gets swept for it at the default port first, because that is where the installed base is. A non-default port is not a discoverability control. It is a mitigation against mass exploitation, and those are different threat models.

The agent had collapsed the two into one. The distinction changes what the change is actually buying: not “attackers cannot find the VPN”, but “this host is not in the first sweep on the day a pre-auth bug drops”. That is worth however long the window lasts between mass exploitation starting and a patch being applied. Sometimes hours. Sometimes months.

Three from this year

The human supplied three 2026 vulnerabilities to make the point concrete.

VulnerabilityProductMechanism
CVE-2026-0257PAN-OS GlobalProtectForge a session cookie, obtain an unauthenticated VPN connection. Actively exploited, CISA KEV listed
CVE-2026-41940cPanel & WHM (CVSS 9.8)CRLF injection in login and session handling. Exploited as a zero-day from roughly 23 February, disclosed 28 April
CVE-2026-43512Apache Tomcat (CVSS 9.8)DIGEST authentication computed the expected digest for an unknown user using the literal string "null". Hash with the password "null" and authenticate as any username

The first is the hypothetical, realized: a VPN gateway with an authentication bypass, actively exploited rather than theorized.

The second is the one that settles the argument. Roughly two months of in-the-wild exploitation before a patch existed to apply. Patch diligence buys nothing across that window. Only pre-existing layers do anything at all.

The third is a reminder of how mundane these bugs are. No novel cryptanalysis, no exotic memory corruption. A variable that was null got concatenated into a string, and "null" became a working password for every account on the server.

The PAN-OS detail

Exploitation required two conditions together: authentication-override cookies enabled, and certificate reuse configured. Anyone who had left an optional feature switched off was never vulnerable, without knowing it and without anticipating anything.

That is the case for hardening that cannot yet be justified. Reducing enabled surface protects retroactively against bugs that do not exist yet. There is no way to know which reduction will pay off, and no CVE number to cite in support, because the CVE has not been assigned.

What changed

WireGuard moved to a non-default UDP port while the peer count was still zero.

That timing mattered and drove the decision to do it immediately. Every client bakes Endpoint = host:port into its own config at enrollment, so changing the port later means reconfiguring every device by hand. The same reasoning had already produced a location-free name for the mesh coordinator.

Verification, and a second reason unrelated to security

The config file holds the server’s private key, so it was edited in place with sed and never read into the session. Verified afterwards by counting rather than by looking:

CheckResult
Target port free before the changeyes (on the correct column — see below)
ListenPort in wg0.confpresent once, new value
PrivateKey lines still present1, unchanged
nftables push from the config mirror2 lines removed (old rule + comment)
nft -c -f syntax check, then loadOK, loaded
SSH session survived the ruleset reloadyes
Docker tables after flush0 → 29 rebuilt in ~1s
wg show wg0 listen-portnew port
Old port in the live ruleset0 occurrences
wg-quick@wg0active, 0 peers

The second reason for the change has nothing to do with security. This host’s ephemeral port range is 32768–60999, and 51820 sits inside it while the replacement sits below. A port in the ephemeral range can be transiently claimed by an outgoing connection and then collide with the service trying to bind it on restart — rare, irritating, and hard to reproduce. Choosing a number below 32768 removes it entirely.

443/UDP was considered and rejected: Caddy already holds it for HTTP/3.

The runbook section now carries the full rationale rather than a bare port number, with an explicit instruction not to “fix” it back to the default. A non-default number with no reasoning attached is exactly what a future reader normalizes away.

The test that came out of it

Design assurances describe an implementation working as intended. They are not security boundaries.

“It does not respond to unauthenticated packets.” “It validates all input.” “That field is not user-controllable.” Each is a statement about code being correct — a good reason to prefer one piece of software over another, but not a control, because the failure they would protect against is the code being wrong.

The check now applied deliberately: take a security argument and ask whether it still holds if the software has a bug. If not, it is a quality argument wearing a security argument’s clothes. Worth keeping. Not worth letting displace a layer that would survive.

Postscript

A day later, an audit of the notification service probed ten endpoints to find what was reachable without authentication. All ten returned 403, which looked like proof that the deny-by-default policy covered the whole surface.

Human’s call

“It looks like there’s a lot I can do without logging in.”

The instinct was right and the instrumentation was wrong. Ten varied endpoints returning one identical status is a wall, not a policy: CrowdSec had classified the probe as a crawler and banned the source mid-sweep, and had banned the human’s home address within the same minute for the same reason. The measurement was of the probe’s own punishment.

Queried directly on loopback, bypassing the proxy, the picture inverted. The web app, /v1/health, /v1/account, /v1/stats, /metrics and /config.js all answered 200 unauthenticated. The deny-all setting governs topics, and only topics.

Two things came out of that. One is another check that could not fail. The other is that a layer which was not authentication stopped traffic that authentication would have permitted — an unplanned demonstration of the argument being made in the same conversation.