Skip to content

Commit 5fd73ad

Browse files
gorecodesclaude
andcommitted
feat(server): allow plain HTTP on non-loopback via ARBOR_ALLOW_PLAINTEXT=1
ARBOR_ALLOW_PLAINTEXT=1 now bypasses the loopback-only check in addition to the cert-not-found check. This lets users bind to a VPN interface (e.g. WireGuard 10.x.x.x) with plain HTTP when the tunnel itself provides confidentiality, without needing a self-signed certificate. A WARNING is printed at startup; the error message for the default (unset) case now mentions ARBOR_ALLOW_PLAINTEXT=1 as the escape hatch. Documented in arbor.env.example under the VPN / private network section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f4598f6 commit 5fd73ad

3 files changed

Lines changed: 23 additions & 1 deletion

File tree

backend/arbor/server.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,19 @@ def _is_loopback_host(host: str) -> bool:
2929
def _enforce_loopback_or_tls(host: str, tls: bool) -> None:
3030
if tls or _is_loopback_host(host):
3131
return
32+
if env_value("ARBOR_ALLOW_PLAINTEXT") == "1":
33+
print(
34+
f"[arbor] WARNING: ARBOR_ALLOW_PLAINTEXT=1 — plain HTTP on {host!r}. "
35+
"Only safe behind a VPN or trusted private network.",
36+
flush=True,
37+
)
38+
return
3239
print(
3340
f"[arbor] ERROR: refusing to bind {host!r} without TLS. Plain HTTP is only\n"
3441
f"[arbor] permitted on loopback (127.0.0.1, ::1, localhost). Provide a\n"
3542
f"[arbor] TLS certificate (ARBOR_CERT, ARBOR_KEY) or place this instance\n"
36-
f"[arbor] behind a TLS-terminating reverse proxy and bind to 127.0.0.1.",
43+
f"[arbor] behind a TLS-terminating reverse proxy and bind to 127.0.0.1.\n"
44+
f"[arbor] To allow plain HTTP on a private/VPN interface set ARBOR_ALLOW_PLAINTEXT=1.",
3745
file=sys.stderr,
3846
)
3947
sys.exit(2)

backend/tests/test_pr1_web_edge.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Tests for PR 1 (hardening/web-edge): CSRF, HSTS, TLS bind, WS origin."""
22

3+
import os
34
import unittest
5+
import unittest.mock
46
from unittest.mock import patch
57

68
from fastapi.testclient import TestClient
@@ -146,6 +148,12 @@ def test_public_without_tls_exits_2(self):
146148
web_server._enforce_loopback_or_tls("192.168.1.10", tls=False)
147149
self.assertEqual(cm.exception.code, 2)
148150

151+
def test_public_without_tls_allowed_by_plaintext_override(self):
152+
with unittest.mock.patch.dict(os.environ, {"ARBOR_ALLOW_PLAINTEXT": "1"}):
153+
# No SystemExit expected — user explicitly opted in (e.g. behind VPN).
154+
web_server._enforce_loopback_or_tls("0.0.0.0", tls=False)
155+
web_server._enforce_loopback_or_tls("10.0.0.1", tls=False)
156+
149157

150158
if __name__ == "__main__":
151159
unittest.main()

config/arbor.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ ARBOR_TLS=0
1111
# ARBOR_CERT=/etc/arbor/cert.pem
1212
# ARBOR_KEY=/etc/arbor/key.pem
1313
# ARBOR_CORS_ORIGINS=https://arbor.lan
14+
#
15+
# VPN / private network: to allow plain HTTP on a non-loopback interface
16+
# (e.g. WireGuard 10.0.0.1) set both of the following:
17+
# ARBOR_HOST=0.0.0.0 (or the specific VPN interface IP)
18+
# ARBOR_ALLOW_PLAINTEXT=1
19+
# Only safe when the network itself provides confidentiality (VPN tunnel).
1420

1521
# --- Auth backend (do not change) ---
1622
ARBOR_AUTH_BACKEND=local

0 commit comments

Comments
 (0)