Per-hostname certs still carry `*.<tld>` in their SAN, which Apple platforms reject
Summary
#18 moved .localhost hosts to per-hostname certificates, citing RFC 2606 §2: *.localhost sits at the public suffix boundary and cannot be honoured. The per-hostname certificate still adds that same wildcard as a second SAN entry, so a two-label host ends up with:
X509v3 Subject Alternative Name:
DNS:myapp.localhost, DNS:*.localhostgenerateHostCertAsync in packages/portless/src/certs.ts:
const sans = [`DNS:${hostname}`];
const parts = hostname.split(".");
if (parts.length >= 2) {
sans.push(`DNS:*.${parts.slice(1).join(".")}`);
}For myapp.localhost, parts.slice(1) is the bare TLD, so the wildcard lands back at the public suffix boundary.
Impact
Chrome ignores the unusable entry, which is why #18 looked complete. Apple platforms reject the entire certificate because of it. A native iOS app talking to a portless URL fails with:
The certificate for this server is invalid. You might be connecting to a server that is
pretending to be "myapp.localhost" which could put your confidential information at risk.This happens regardless of how the CA is trusted. In my case the CA was installed in the Simulator as a configuration profile with full trust enabled under Certificate Trust Settings, and the device was rebooted.
Reproduction
portless myapp <cmd>so the proxy serveshttps://myapp.localhost.- Trust
~/.portless/ca.pemon an iOS Simulator, as a profile, with full trust enabled. - From a native app (not Safari),
fetch("https://myapp.localhost/...").
Isolating the variable
I re-issued the leaf with the same key and the same CA, changing only the SAN to DNS:myapp.localhost. The identical request then succeeded, and the server logged the session with a CFNetwork/Darwin user agent. Putting the wildcard back reproduced the failure.
Two things that make this easy to misdiagnose:
- Safari in the Simulator is not a valid check. It loads the page either way, because it resolves trust through the host keychain. Only a native app exercises the Simulator's own trust evaluation.
xcrun simctl keychain booted add-root-certsilently does nothing on Xcode 26 / iOS 26.5. It exits 0 and leavesTrustStore.sqlite3empty, so it looks like the CA is trusted when it is not.
An ATS exception does not help either, since no ATS key disables certificate validation. It only changes the message from A TLS error caused the secure connection to fail to certificate is invalid.
Scope
Only two-label hostnames are affected. branch.myapp.localhost produces *.myapp.localhost, which is below the TLD and valid.
Fix
PR opened: only add the sibling wildcard when the parent domain has at least two labels. Verified against portless 0.15.6 on macOS 26.6 / Xcode 26 / iOS 26.5.
Source: vercel-labs/portless