Commit 07d671c
fix(boj): bind Cowboy to 127.0.0.1 by default (HCG tier-2 E1 prereq) (#130)
## Summary
Code-enforces the ADR-0004 §1 invariant that BoJ's back-side bind is not
externally routable in deployments fronted by `http-capability-gateway`
(HCG tier-2).
Previously, `Plug.Cowboy` started with `port: 7700` and **no `ip:`
option**, which defaults to `0.0.0.0` — all interfaces. The
"loopback-only" property in the contract document
(`docs/integration/http-capability-gateway-boj-contract.md` §1) and the
Phase E rollout-runbook §1.4 prereq #6 (boj-server#128) were therefore
**operational assertions**, not code-enforced.
This is **action item #6** from the Phase E consumer-side audit posted
on
[`standards#100`](hyperpolymath/standards#100 (comment))
— pre-staged ahead of E1/E2 wiring so it ships independent of Phase D's
still-scaffolded benchmark gate.
`Refs hyperpolymath/standards#100` (NOT Closes — joint-close is
owner-only).
`Refs hyperpolymath/standards#91`.
## What's in
| File | Change |
|---|---|
| `elixir/lib/boj_rest/application.ex` | Read `bind_ip` config; parse
via `:inet.parse_address/1`; pass as `ip:` to `Plug.Cowboy.options`. New
`parse_bind_ip/1` public helper. Moduledoc updated to document the new
bind-default. |
| `elixir/config/config.exs` | `bind_ip: System.get_env("BOJ_BIND_IP")
|| "127.0.0.1"` |
| `elixir/test/application_test.exs` | New file — 7 tests on
`parse_bind_ip/1` (IPv4 loopback, IPv4 all-interfaces, IPv6 loopback,
IPv6 all-interfaces, invalid, empty, error-message-mentions-input). |
| `CHANGELOG.md` | New `### Changed` entry under `[Unreleased]`. |
## Behaviour
- **Default**: bind `127.0.0.1` (IPv4 loopback only).
- **Override**: set `BOJ_BIND_IP` to any IPv4/IPv6 address string
parseable by `:inet.parse_address/1`. Use `0.0.0.0` or `::` to opt back
into all-interfaces for legacy/standalone deployments.
- **Invalid `BOJ_BIND_IP`**: fails fast at boot with a descriptive
`ArgumentError` naming the offending value. Preferred to silently
degrading to `0.0.0.0` and exposing the back-side bind.
## Verification
```
$ mix test test/application_test.exs
Finished in 0.02 seconds (0.02s async, 0.00s sync)
7 tests, 0 failures
```
Full `mix test` shows the same 2 baseline-rot failures present on
`origin/main` (`whisper-mcp` cartridge has `auth.method:
"optional_api_key"`, which the catalog property test doesn't accept).
Verified by running `mix test` against `origin/main` HEAD `92f3b3a1` in
a clean detached worktree:
| Branch | properties | tests | failures |
|---|---|---|---|
| `origin/main` (`92f3b3a1`) | 10 | 188 | 2 |
| this branch | 10 | 195 | 2 |
The two failures are *the same* failures; my +7 tests all pass. **Not
introduced by this PR** — out of scope. (Per
`[[feedback_pr_triage_crosscheck_main]]` from session memory: red checks
that are also red on main are baseline rot, not PR defects.)
## Test plan
- [x] `mix test test/application_test.exs` — 7/7 pass locally.
- [x] Cross-check against `origin/main` — failures match (baseline rot,
not regression).
- [ ] CI green — governance / hypatia / a2ml / k9 / dogfooding all pass.
- [ ] Manual sanity: starting BoJ in a dev shell with the default config
binds on `127.0.0.1:7700`, and `curl http://127.0.0.1:7700/health`
succeeds while a connect from a non-loopback IP fails at the socket
layer (owner-verified pre-merge in any environment where this is
reachable).
## Risk
**Low.** The change is one Plug.Cowboy option and a small parser with
explicit fail-fast. The only behaviour difference is that a dev /
packaged deployment that *relied* on the implicit `0.0.0.0` default now
needs to opt in via `BOJ_BIND_IP`. Two known sites currently assume
all-interfaces and may need follow-up:
- `stapeln.toml` line 100: `APP_HOST = "[::]"` — used by the stapeln
packager. Audit item #7; out of scope for this PR (filing as a follow-up
if desired).
- `k8s/deployment.yaml` / `k8s/service.yaml`: containerPort 7700 exposed
via Service. Audit item #8; out of scope here, and this PR makes that
exposure safer (the BoJ process itself no longer binds all-interfaces),
but the Service should still be tightened to ClusterIP-only as part of
E1/E2.
If the owner wants those two folded in, happy to extend this PR or to
file as follow-ups.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent a3ae35f commit 07d671c
4 files changed
Lines changed: 92 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
32 | 44 | | |
33 | 45 | | |
34 | 46 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
10 | 23 | | |
11 | 24 | | |
12 | 25 | | |
13 | 26 | | |
14 | 27 | | |
15 | 28 | | |
| 29 | + | |
16 | 30 | | |
17 | 31 | | |
18 | 32 | | |
| |||
26 | 40 | | |
27 | 41 | | |
28 | 42 | | |
29 | | - | |
| 43 | + | |
30 | 44 | | |
31 | 45 | | |
32 | 46 | | |
33 | 47 | | |
34 | 48 | | |
35 | 49 | | |
36 | 50 | | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
37 | 71 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
0 commit comments