Skip to content

Commit a3ae35f

Browse files
fix(k8s): Service for BoJ to ClusterIP (HCG tier-2 E1 prereq) (#131)
## Summary Per ADR-0004 §1 (http-capability-gateway tier-2 placement) and the Phase E rollout-runbook §1.4 prereq #8, BoJ **MUST NOT be externally addressable** when fronted by HCG. Previously `k8s/service.yaml` declared `type: LoadBalancer`, exposing all four BoJ ports (REST 7700, gRPC 7701, GraphQL 7702, SSE 7703) externally — defeating the runbook §3.4 decommission invariant before Phase E even starts. This is **action item #8** from the [Phase E consumer-side audit](hyperpolymath/standards#100 (comment)) — companion to action #6 (`boj-server#130` Cowboy bind tightening). The two layers together give defence in depth: BoJ binds loopback (#130) **AND** the k8s Service does not expose it externally (this PR). `Refs hyperpolymath/standards#100` (NOT Closes — joint-close is owner-only). `Refs hyperpolymath/standards#91`. ## What's in | File | Change | |---|---| | `k8s/service.yaml` | `type: LoadBalancer` → `type: ClusterIP`. Header comment explaining the Phase E posture. New annotations: `hyperpolymath.dev/exposure: "internal-only"`, `hyperpolymath.dev/external-via: "http-capability-gateway (tier-2)"`. Ports 7700–7703 retained forward-compatibly. | | `CHANGELOG.md` | New `### Changed` entry under `[Unreleased]`. | Estate cross-check: `hypatia/*`, `rsr-certifier`, and `opsm-service` all use `ClusterIP` for backend Services. The estate's gateway-fronted-backend pattern is `Service: ClusterIP`. The only `LoadBalancer` in the estate's k8s manifests today (besides the BoJ Service this PR fixes) is `svalinn-gateway` — i.e. the *gateway* tier, not a backend. This PR brings BoJ's Service in line with the estate pattern. ## Why DRAFT **Breaking for anyone running this manifest as-is with a LoadBalancer in production.** I don't know whether a live LoadBalancer-fronted BoJ deployment exists. Marking draft so the owner gates merge on confirming no such deployment will break. If one exists, the migration path is HCG-in-front (per the rollout-runbook §2). For ad-hoc / dev / non-HCG-fronted deployments that need BoJ exposed externally, the header comment in `service.yaml` shows the kustomize/helm overlay recipe rather than editing the canonical manifest: ```yaml - op: replace path: /spec/type value: LoadBalancer # only valid for non-HCG-fronted deployments ``` ## Test plan - [x] YAML lints clean (manual review — single-document Service spec, valid v1 API). - [ ] CI green — governance / a2ml / k9 / hypatia all pass. - [ ] Owner: confirm no live LoadBalancer-fronted BoJ deployment will break, then flip from DRAFT to ready. - [ ] Post-merge sanity: `kubectl apply -f k8s/service.yaml --dry-run=client` accepts the spec; `kubectl describe svc/boj-server` shows the new annotations. ## Risk **Medium for the manifest, low for the codebase.** The codebase is untouched — only k8s manifest. Breakage scope is limited to k8s operators relying on the LoadBalancer external IP for BoJ; they get a fixable Service-type override. No mix.exs / Elixir / Zig / Idris2 / cartridge changes; CI should not show any logic regressions. ## Follow-ups not in this PR (audit residue) - Action #7 — `stapeln.toml` `APP_HOST = "[::]"` → loopback (separate concern: packager). Can pick up next. - Optional: add a `NetworkPolicy` restricting BoJ pod ingress to only the HCG pod. Defence-in-depth beyond ClusterIP. Not strictly required for Phase E acceptance — Service-type is the controlling lever — but worth filing as a follow-up issue. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 08703bd commit a3ae35f

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ All notable changes to Bundle of Joy Server are documented here.
2727

2828
## [Unreleased]
2929

30+
### Changed
31+
32+
- **k8s Service for BoJ is now `type: ClusterIP`** (was: `LoadBalancer`).
33+
Per ADR-0004 §1 and the Phase E rollout-runbook §1.4 prereq #8, BoJ
34+
must not be externally addressable when fronted by
35+
`http-capability-gateway` (HCG tier-2). External clients reach HCG;
36+
HCG forwards to BoJ over the pod-network loopback. Legacy/standalone
37+
deployments that need BoJ exposed externally should override `type`
38+
in a kustomize/helm overlay rather than reverting the canonical
39+
manifest (see header comment in `k8s/service.yaml`). Adds
40+
`hyperpolymath.dev/exposure: "internal-only"` and
41+
`hyperpolymath.dev/external-via: "http-capability-gateway (tier-2)"`
42+
annotations so the posture is discoverable from `kubectl describe`.
43+
Refs
44+
[`hyperpolymath/standards#100`](https://github.com/hyperpolymath/standards/issues/100),
45+
[`#91`](https://github.com/hyperpolymath/standards/issues/91).
46+
3047
### Added
3148

3249
- **ADR-0014 — cross-cartridge composition safety (RFC)** — frames the

k8s/service.yaml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,42 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
#
4+
# BoJ Service — ClusterIP only.
5+
#
6+
# Per ADR-0004 §1 (http-capability-gateway tier-2 placement) and the
7+
# Phase E rollout-runbook (`docs/integration/hcg-tier2-rollout-runbook.md`
8+
# §1.4 prereq #8), BoJ MUST NOT be externally addressable. Only the HCG
9+
# (tier-2) gateway pod is permitted to reach BoJ. External clients reach
10+
# HCG, which forwards to BoJ over the pod-network loopback.
11+
#
12+
# Legacy/standalone deployments that need BoJ exposed externally (no
13+
# HCG in front) should override `type` in their kustomize/helm overlay
14+
# rather than editing this canonical manifest:
15+
#
16+
# - op: replace
17+
# path: /spec/type
18+
# value: LoadBalancer # only valid for non-HCG-fronted deployments
19+
#
20+
# Ports 7700–7703 are declared forward-compatibly (current BoJ binds
21+
# 7700 only; 7701/7702/7703 reserved for gRPC/GraphQL/SSE per
22+
# `docs/ARCHITECTURE.md`). When those bindings land, no Service edit is
23+
# required — they are already declared here.
24+
#
25+
# Refs:
26+
# - hyperpolymath/standards#100 (Phase E parent)
27+
# - hyperpolymath/standards#91 (ADR-0004 epic)
28+
# - docs/decisions/0004-adopt-http-capability-gateway.md
29+
# - docs/integration/hcg-tier2-rollout-runbook.md
30+
# - docs/integration/http-capability-gateway-boj-contract.md (§1 transport)
131
apiVersion: v1
232
kind: Service
333
metadata:
434
name: boj-server
535
labels:
636
app: boj-server
37+
annotations:
38+
hyperpolymath.dev/exposure: "internal-only"
39+
hyperpolymath.dev/external-via: "http-capability-gateway (tier-2)"
740
spec:
841
selector:
942
app: boj-server
@@ -20,4 +53,4 @@ spec:
2053
- name: sse
2154
port: 7703
2255
targetPort: 7703
23-
type: LoadBalancer
56+
type: ClusterIP

0 commit comments

Comments
 (0)