Remote Access via WireGuard (iPhone → Mac Mini)
This guide documents how to set up secure remote access to the outheis WebUI from an iPhone using WireGuard, including all pitfalls encountered.
Overview
Overview
Goal: access http://10.10.0.1:8080 from an iPhone over WireGuard, where the Mac Mini is the WireGuard server and the iPhone is the peer.
1. Install WireGuard on macOS
1. Install WireGuard on macOS
brew install wireguard-go wireguard-tools
2. Create keys
2. Create keys
On the Mac Mini:
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
On the iPhone: generate keys inside the WireGuard iOS app (Add Tunnel → Create from scratch).
3. Server config (/etc/wireguard/wg0.conf)
3. Server config (/etc/wireguard/wg0.conf)
[Interface]
PrivateKey = <server private key>
Address = 10.10.0.1/24
ListenPort = 51820
[Peer]
PublicKey = <iphone public key>
AllowedIPs = 10.10.0.2/32
Note: The server's
Addresswill appear asinet 10.10.0.1 --> 10.10.0.1(a P2P self-loop on theutuninterface) — this is normal for macOS WireGuard.
4. iPhone config (in WireGuard app)
4. iPhone config (in WireGuard app)
[Interface]
PrivateKey = <iphone private key>
Address = 10.10.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <server public key>
Endpoint = <mac-mini-public-ip>:51820
AllowedIPs = 10.10.0.0/24
PersistentKeepalive = 25
AllowedIPs = 10.10.0.0/24 routes only WireGuard traffic through the tunnel (not all internet traffic).
5. Start WireGuard
5. Start WireGuard
sudo wg-quick up wg0
Verify:
sudo wg show
6. outheis WebUI: bind to all interfaces
6. outheis WebUI: bind to all interfaces
In the outheis WebUI config, set Host to 0.0.0.0 (with the warning acknowledged). This makes uvicorn listen on all interfaces, including the WireGuard utun interface.
Restart the daemon after changing the host.
Pitfalls & Solutions
Pitfalls & Solutions
Pitfall 1: Can't ping 10.10.0.1 — no ICMP reply
Symptom: ping 10.10.0.1 from iPhone gets no reply. WireGuard handshake succeeds (wg show shows a recent handshake).
Cause: macOS Stealth Mode silently drops all uninitiated incoming packets — including ICMP echo requests.
Fix:
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode off
Verify:
/usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode
# → Stealth mode is off
Pitfall 2: TCP handshake succeeds but HTTP never responds
Symptom: tcpdump on utun4 shows SYN → SYN/ACK → ACK → HTTP GET → server ACK, but uvicorn never logs the request and sends no HTTP response. Browser shows ERR_CONNECTION_CLOSED.
Cause: macOS Application Firewall was blocking incoming connections to the Homebrew Python binary. The firewall allows /usr/bin/python3 but not /opt/homebrew/.../Python. Loopback traffic (SSH tunnel) bypasses the firewall, so local access works while WireGuard traffic is silently blocked at the application layer.
Diagnosis:
lsof -i :8080 -n -P # find the PID of uvicorn
lsof -p <pid> | grep txt # find the actual Python binary path
/usr/libexec/ApplicationFirewall/socketfilterfw --listapps # check allowlist
Fix:
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add \
/opt/homebrew/Cellar/python@3.12/3.12.13/Frameworks/Python.framework/Versions/3.12/Resources/Python.app/Contents/MacOS/Python
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp \
/opt/homebrew/Cellar/python@3.12/3.12.13/Frameworks/Python.framework/Versions/3.12/Resources/Python.app/Contents/MacOS/Python
Note: The path contains the exact Python version. If you upgrade Python via Homebrew, repeat this step for the new version.
Pitfall 3: IP forwarding is irrelevant
Symptom/Confusion: sysctl net.inet.ip.forwarding shows 0. Might seem like a routing problem.
Reality: IP forwarding is only needed when the Mac acts as a router between two different networks. For WireGuard delivering packets destined for the Mac itself (10.10.0.1), forwarding is not involved. Leave it at 0.
Pitfall 4: Brave browser blocks requests to private IPs (Private Network Access)
Symptom: Requests from a web page to http://10.x.x.x/ fail with ERR_FAILED. Affects Chromium-based browsers (Brave, Chrome).
Cause: Chrome's Private Network Access (PNA) policy requires a preflight OPTIONS request with Access-Control-Request-Private-Network: true header, and the server must respond with Access-Control-Allow-Private-Network: true.
Fix: Add this to the FastAPI/Starlette middleware:
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
if request.method == "OPTIONS" and request.headers.get("Access-Control-Request-Private-Network"):
return JSONResponse({}, headers={
"Access-Control-Allow-Origin": request.headers.get("Origin", "*"),
"Access-Control-Allow-Private-Network": "true",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
})
...
Pitfall 5: SSH tunnel conflicts with direct WireGuard access
Symptom: ssh -fNL 8080:localhost:8080 <host> fails with bind [127.0.0.1]:8080: Address already in use if outheis is already running locally.
Cause: Port 8080 is already bound by the local outheis instance.
Solution: Use a different local port for the tunnel, e.g.:
ssh -fNL 8081:localhost:8080 <host>
# then access http://localhost:8081/
Or stop the local outheis instance before tunneling.
Normal Behavior to Expect
Normal Behavior to Expect
ifconfig utun4showsinet 10.10.0.1 --> 10.10.0.1— the P2P self-loop is correct for macOS WireGuard (not Linux).wg showshows the peer with a handshake timestamp — this confirms the tunnel is active.lsof -i :8080showsTCP *:8080 (LISTEN)— uvicorn is bound to all interfaces.