|
| 1 | +K9! |
| 2 | +# SPDX-License-Identifier: MPL-2.0 |
| 3 | +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 4 | +# |
| 5 | +# container/gateway-deploy.k9.ncl |
| 6 | +# http-capability-gateway — k9-svc deployment component (Hunt level) |
| 7 | +# |
| 8 | +# Phase E E1 deliverable for the BoJ tier-2 wiring channel |
| 9 | +# (hyperpolymath/standards#91; Phase E = standards#100). |
| 10 | +# |
| 11 | +# The wider integration plan (boj-server `docs/integration/ |
| 12 | +# http-capability-gateway-plan.md` § Phase E, E1) prescribes this exact |
| 13 | +# path. Until this file existed, the gateway-side §1.5 prereq in the |
| 14 | +# rollout runbook (boj-server `docs/integration/hcg-tier2-rollout-runbook.md`) |
| 15 | +# was an open checkbox; landing it flips that single prereq, and only |
| 16 | +# that prereq — staging soak (§2), production split (§3) and the |
| 17 | +# Trustfile flip (§6.4) remain owner-driven follow-ups gated on real |
| 18 | +# infrastructure. |
| 19 | +# |
| 20 | +# Security Level: 'Hunt (requires cryptographic handshake for execution). |
| 21 | +# |
| 22 | +# WARNING: This component can execute shell commands when invoked |
| 23 | +# through k9-svc. It declares intent only; the Hunt-handshake gate |
| 24 | +# enforces authorisation at deploy time. |
| 25 | +# |
| 26 | +# Usage: |
| 27 | +# nickel typecheck container/gateway-deploy.k9.ncl |
| 28 | +# k9-svc validate container/gateway-deploy.k9.ncl |
| 29 | +# k9-svc deploy container/gateway-deploy.k9.ncl --env staging |
| 30 | + |
| 31 | +# ───────────────────────────────────────────────────────────────────── |
| 32 | +# Deployment configuration |
| 33 | +# ───────────────────────────────────────────────────────────────────── |
| 34 | +# |
| 35 | +# Backend URL forms are sourced from the contract document |
| 36 | +# (boj-server `docs/integration/http-capability-gateway-boj-contract.md` |
| 37 | +# § 1, Transport): staging uses TCP loopback to BoJ on :7700, |
| 38 | +# production uses a loopback Unix domain socket. The gateway proxy |
| 39 | +# module currently supports a single `backend_url`; both forms below |
| 40 | +# are within that single-backend capability. |
| 41 | +# |
| 42 | +# Trust source defaults to `"header"` per plan § E2 ("Trust level |
| 43 | +# source: \"header\" initially, switched to \"mtls\" after Phase B |
| 44 | +# cert rotation runbook is exercised"). The switch to `"mtls"` is a |
| 45 | +# config flip, not a code change; it happens at the §2.4 rehearsal. |
| 46 | +let deployment = { |
| 47 | + environments = { |
| 48 | + staging = { |
| 49 | + replicas = 1, |
| 50 | + memory = "512Mi", |
| 51 | + cpu = "250m", |
| 52 | + image_tag = "staging", |
| 53 | + # Phase A contract § 1: staging gateway → BoJ over TCP loopback. |
| 54 | + backend_url = "http://127.0.0.1:7700", |
| 55 | + # Plan § E2: external port is gateway-owned, TLS for direct, or |
| 56 | + # 8080 when fronted by a Cloudflare Tunnel. |
| 57 | + port = 8443, |
| 58 | + trust_level_source = "header", |
| 59 | + }, |
| 60 | + production = { |
| 61 | + replicas = 1, |
| 62 | + memory = "1Gi", |
| 63 | + cpu = "500m", |
| 64 | + image_tag = "latest", |
| 65 | + # Phase A contract § 1: production gateway → BoJ over a loopback |
| 66 | + # Unix domain socket; the back-side TCP port is not opened. |
| 67 | + backend_url = "http://unix:/run/boj/gnosis.sock:/", |
| 68 | + port = 8443, |
| 69 | + # Production trust source: Phase B mTLS (http-capability-gateway#10). |
| 70 | + # Flipped from `"header"` after the §2.4 staging cert-rotation |
| 71 | + # rehearsal completes per the rollout runbook. |
| 72 | + trust_level_source = "mtls", |
| 73 | + }, |
| 74 | + }, |
| 75 | + |
| 76 | + container = { |
| 77 | + # Image is built from the in-tree Containerfile (multi-stage |
| 78 | + # release build). Cerro-torre signing into a `.ctp` bundle is the |
| 79 | + # remaining § 1.5 prereq tracked on standards#100; the bundle name |
| 80 | + # is `http-capability-gateway` regardless of registry destination. |
| 81 | + image = "ghcr.io/hyperpolymath/http-capability-gateway", |
| 82 | + # Container listens on 4000 internally (Containerfile EXPOSE 4000); |
| 83 | + # the environment's `port` above is the externally bound port. |
| 84 | + internal_port = 4000, |
| 85 | + health_check = "/health", |
| 86 | + readiness_check = "/ready", |
| 87 | + metrics_path = "/metrics", |
| 88 | + }, |
| 89 | + |
| 90 | + # Environment variables consumed by `config/runtime.exs`. Paths |
| 91 | + # marked `!OWNER:` in the rollout runbook § 1.3 are supplied at |
| 92 | + # deploy time, not committed here. |
| 93 | + env = { |
| 94 | + POLICY_PATH = "/etc/http-capability-gateway/policy.yaml", |
| 95 | + BACKEND_URL = "<from environments[<env>].backend_url>", |
| 96 | + PORT = "<from environments[<env>].port>", |
| 97 | + TRUST_LEVEL_SOURCE = "<from environments[<env>].trust_level_source>", |
| 98 | + # mTLS material — owner-provided at deploy time per rollout |
| 99 | + # runbook § 1.3. The keys are declared here so a missing value |
| 100 | + # fails the deploy precheck rather than silently disabling mTLS. |
| 101 | + MTLS_CA_CERT_PATH = "<owner-provided>", |
| 102 | + GATEWAY_CERT_PATH = "<owner-provided>", |
| 103 | + GATEWAY_KEY_PATH = "<owner-provided>", |
| 104 | + }, |
| 105 | + |
| 106 | + # Mounts: the policy YAML is delivered into the container read-only |
| 107 | + # per the policy-authoring workflow (boj-server `docs/integration/ |
| 108 | + # http-capability-gateway-policy-authoring.md` § 3). |
| 109 | + mounts = [ |
| 110 | + { |
| 111 | + name = "gateway-policy", |
| 112 | + source = "boj-server:config/gateway-policy-boj.yaml", |
| 113 | + target = "/etc/http-capability-gateway/policy.yaml", |
| 114 | + read_only = true, |
| 115 | + }, |
| 116 | + { |
| 117 | + name = "mtls-material", |
| 118 | + source = "<owner-provided secret>", |
| 119 | + target = "/etc/http-capability-gateway/tls", |
| 120 | + read_only = true, |
| 121 | + }, |
| 122 | + ], |
| 123 | + |
| 124 | + # Rolling strategy with max_unavailable = 0 preserves the |
| 125 | + # gateway's atomic policy-swap guarantee across replica churn: |
| 126 | + # a new replica with a new policy comes up before an old replica |
| 127 | + # is drained. |
| 128 | + strategy = { |
| 129 | + type = "rolling", |
| 130 | + max_surge = 1, |
| 131 | + max_unavailable = 0, |
| 132 | + }, |
| 133 | + |
| 134 | + # Failure mode contract: the gateway↔BoJ seam declares |
| 135 | + # `failure_mode: "fail-closed (circuit breaker)"` in the BoJ |
| 136 | + # `Trustfile.a2ml [SEAMS]` entry. If the BoJ backend becomes |
| 137 | + # unreachable the gateway's circuit breaker trips and returns 503; |
| 138 | + # this deployment spec does not paper over that with a retry loop. |
| 139 | + failure_mode = "fail-closed", |
| 140 | +} in |
| 141 | + |
| 142 | +# ───────────────────────────────────────────────────────────────────── |
| 143 | +# Deployment scripts (executed at Hunt level) |
| 144 | +# ───────────────────────────────────────────────────────────────────── |
| 145 | +# |
| 146 | +# Shell-level recipes. The substantive logic lives in `Justfile` |
| 147 | +# (`just container-build`, `just container-up`) so these scripts are |
| 148 | +# thin orchestration. The pre-deploy script enforces that the policy |
| 149 | +# file mounted into the container loads + validates cleanly before |
| 150 | +# any traffic shift — the same `PolicyLoader.load_policy/1` + |
| 151 | +# `PolicyValidator.validate/1` pair the gateway runs at startup, so a |
| 152 | +# malformed policy never reaches a running container. |
| 153 | +let scripts = { |
| 154 | + pre_deploy = m%" |
| 155 | +#!/bin/sh |
| 156 | +set -eu |
| 157 | +echo "K9: Pre-deployment validation for http-capability-gateway..." |
| 158 | +just container-verify |
| 159 | +just policy-validate "${POLICY_FILE:-config/policy.yaml}" |
| 160 | +echo "K9: Validation passed." |
| 161 | +"%, |
| 162 | + |
| 163 | + deploy = m%" |
| 164 | +#!/bin/sh |
| 165 | +set -eu |
| 166 | +ENV="${1:-staging}" |
| 167 | +echo "K9: Deploying http-capability-gateway to $ENV environment..." |
| 168 | +just container-build |
| 169 | +just container-up |
| 170 | +echo "K9: Deployment to $ENV complete." |
| 171 | +"%, |
| 172 | + |
| 173 | + rollback = m%" |
| 174 | +#!/bin/sh |
| 175 | +set -eu |
| 176 | +echo "K9: Rolling back http-capability-gateway deployment..." |
| 177 | +just container-down |
| 178 | +echo "K9: Rollback complete (immediate-bypass)." |
| 179 | +echo "K9: See boj-server docs/integration/hcg-tier2-rollout-runbook.md § 5" |
| 180 | +echo "K9: for the full rollback procedure (immediate vs permanent)." |
| 181 | +"%, |
| 182 | +} in |
| 183 | + |
| 184 | +# ───────────────────────────────────────────────────────────────────── |
| 185 | +# Top-level k9-svc component |
| 186 | +# ───────────────────────────────────────────────────────────────────── |
| 187 | +# |
| 188 | +# `pedigree` is inlined at the top level so the canonical |
| 189 | +# hyperpolymath/k9-validate-action regex finds |
| 190 | +# pedigree = { … metadata = { name = …, version = … } … } |
| 191 | +# without traversing `let` bindings — same shape as the boj-server |
| 192 | +# reference (`container/deploy.k9.ncl`). |
| 193 | +{ |
| 194 | + pedigree = { |
| 195 | + schema_version = "1.0.0", |
| 196 | + component_type = "deployment-component", |
| 197 | + # L1: Snout — identity |
| 198 | + metadata = { |
| 199 | + name = "http-capability-gateway-deploy", |
| 200 | + version = "0.1.0", |
| 201 | + breed = "application/vnd.k9+nickel", |
| 202 | + magic_number = "K9!", |
| 203 | + description = "http-capability-gateway tier-2 deployment component (Hunt level)", |
| 204 | + }, |
| 205 | + # L2: Scent — target environment |
| 206 | + target = { |
| 207 | + os = 'Linux, |
| 208 | + is_edge = false, |
| 209 | + requires_podman = true, |
| 210 | + min_memory_mb = 512, |
| 211 | + }, |
| 212 | + # L3: Leash — security |
| 213 | + security = { |
| 214 | + leash = 'Hunt, |
| 215 | + trust_level = 'Hunt, |
| 216 | + allow_network = true, |
| 217 | + allow_filesystem_write = true, |
| 218 | + allow_subprocess = true, |
| 219 | + # Real Ed25519 signature is added at bundle-signing time |
| 220 | + # (cerro-torre `.ctp` step, rollout runbook § 1.5). |
| 221 | + signature = "PLACEHOLDER-SIGNATURE-REQUIRED-FOR-HUNT", |
| 222 | + }, |
| 223 | + # L4: Gut — self-validation |
| 224 | + validation = { |
| 225 | + checksum = "sha256:placeholder", |
| 226 | + pedigree_version = "1.0.0", |
| 227 | + hunt_authorized = false, |
| 228 | + }, |
| 229 | + # L5: Muscle — deployment recipes |
| 230 | + recipes = { |
| 231 | + install = "just container-build", |
| 232 | + validate = "just container-verify", |
| 233 | + deploy = "just container-up", |
| 234 | + migrate = "just container-build && just container-up", |
| 235 | + }, |
| 236 | + }, |
| 237 | + |
| 238 | + deployment = deployment, |
| 239 | + scripts = scripts, |
| 240 | + |
| 241 | + required_level = 'Hunt, |
| 242 | + |
| 243 | + warning = m%" |
| 244 | +WARNING: This is a Hunt-level component. |
| 245 | +
|
| 246 | +It can execute shell commands and modify your system. |
| 247 | +Before running, ensure you have: |
| 248 | +
|
| 249 | +1. Reviewed the deployment scripts above. |
| 250 | +2. Verified the signature (added at cerro-torre `.ctp` signing). |
| 251 | +3. Explicitly authorised Hunt-level execution. |
| 252 | +
|
| 253 | +Run with: |
| 254 | + k9-svc authorize container/gateway-deploy.k9.ncl && |
| 255 | + k9-svc deploy container/gateway-deploy.k9.ncl |
| 256 | +
|
| 257 | +The rollback procedure is in boj-server docs/integration/ |
| 258 | +hcg-tier2-rollout-runbook.md § 5; the script in this file only |
| 259 | +covers the immediate-bypass step. |
| 260 | +"%, |
| 261 | +} |
0 commit comments