@caelo-cms/provisioning 0.11.8 → 0.11.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/dist/admin-domain.d.ts +10 -1
  2. package/dist/admin-domain.d.ts.map +1 -1
  3. package/dist/admin-domain.js +13 -3
  4. package/dist/admin-domain.js.map +1 -1
  5. package/dist/adopt-existing.d.ts +6 -1
  6. package/dist/adopt-existing.d.ts.map +1 -1
  7. package/dist/adopt-existing.js +86 -0
  8. package/dist/adopt-existing.js.map +1 -1
  9. package/dist/agent/command.d.ts +20 -0
  10. package/dist/agent/command.d.ts.map +1 -1
  11. package/dist/agent/command.js +32 -9
  12. package/dist/agent/command.js.map +1 -1
  13. package/dist/agent/run.d.ts +2 -0
  14. package/dist/agent/run.d.ts.map +1 -1
  15. package/dist/agent/run.js +5 -1
  16. package/dist/agent/run.js.map +1 -1
  17. package/dist/agent-db.js +65 -3
  18. package/dist/aws-task-roles.d.ts +4 -2
  19. package/dist/aws-task-roles.d.ts.map +1 -1
  20. package/dist/aws-task-roles.js +4 -1
  21. package/dist/aws-task-roles.js.map +1 -1
  22. package/dist/caddy.d.ts +21 -4
  23. package/dist/caddy.d.ts.map +1 -1
  24. package/dist/caddy.js +27 -3
  25. package/dist/caddy.js.map +1 -1
  26. package/dist/cli.js +12 -3
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloudsql-tls.d.ts +63 -0
  29. package/dist/cloudsql-tls.d.ts.map +1 -0
  30. package/dist/cloudsql-tls.js +289 -0
  31. package/dist/cloudsql-tls.js.map +1 -0
  32. package/dist/compose.d.ts +29 -6
  33. package/dist/compose.d.ts.map +1 -1
  34. package/dist/compose.js +95 -23
  35. package/dist/compose.js.map +1 -1
  36. package/dist/converge-checkpoint.d.ts +1 -0
  37. package/dist/converge-checkpoint.d.ts.map +1 -1
  38. package/dist/converge-checkpoint.js +8 -1
  39. package/dist/converge-checkpoint.js.map +1 -1
  40. package/dist/converge-job-gcp.d.ts.map +1 -1
  41. package/dist/converge-job-gcp.js +2 -11
  42. package/dist/converge-job-gcp.js.map +1 -1
  43. package/dist/deployed-release.d.ts +4 -1
  44. package/dist/deployed-release.d.ts.map +1 -1
  45. package/dist/deployed-release.js +18 -8
  46. package/dist/deployed-release.js.map +1 -1
  47. package/dist/gcp-names.d.ts +21 -0
  48. package/dist/gcp-names.d.ts.map +1 -1
  49. package/dist/gcp-names.js +24 -0
  50. package/dist/gcp-names.js.map +1 -1
  51. package/dist/gcp-url-map.d.ts.map +1 -1
  52. package/dist/gcp-url-map.js +6 -2
  53. package/dist/gcp-url-map.js.map +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/infra-contract.d.ts +1 -1
  59. package/dist/infra-contract.d.ts.map +1 -1
  60. package/dist/infra-contract.js +24 -1
  61. package/dist/infra-contract.js.map +1 -1
  62. package/dist/install-state.d.ts +6 -0
  63. package/dist/install-state.d.ts.map +1 -1
  64. package/dist/install-state.js +17 -4
  65. package/dist/install-state.js.map +1 -1
  66. package/dist/lifecycle.d.ts.map +1 -1
  67. package/dist/lifecycle.js +17 -8
  68. package/dist/lifecycle.js.map +1 -1
  69. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  70. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  71. package/dist/mcp-auth-proxy-setup.js +280 -0
  72. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  73. package/dist/release-manifest.d.ts +25 -1
  74. package/dist/release-manifest.d.ts.map +1 -1
  75. package/dist/release-manifest.js +36 -3
  76. package/dist/release-manifest.js.map +1 -1
  77. package/dist/remote-mcp.d.ts +55 -18
  78. package/dist/remote-mcp.d.ts.map +1 -1
  79. package/dist/remote-mcp.js +307 -77
  80. package/dist/remote-mcp.js.map +1 -1
  81. package/dist/runtime-secrets.d.ts +9 -0
  82. package/dist/runtime-secrets.d.ts.map +1 -1
  83. package/dist/runtime-secrets.js +21 -10
  84. package/dist/runtime-secrets.js.map +1 -1
  85. package/dist/seal-key-rotation.d.ts +7 -7
  86. package/dist/seal-key-rotation.d.ts.map +1 -1
  87. package/dist/seal-key-rotation.js +22 -22
  88. package/dist/seal-key-rotation.js.map +1 -1
  89. package/dist/self-hosted-db-tls.d.ts +37 -0
  90. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  91. package/dist/self-hosted-db-tls.js +220 -0
  92. package/dist/self-hosted-db-tls.js.map +1 -0
  93. package/dist/self-hosted-release.d.ts +3 -1
  94. package/dist/self-hosted-release.d.ts.map +1 -1
  95. package/dist/self-hosted-release.js +33 -9
  96. package/dist/self-hosted-release.js.map +1 -1
  97. package/dist/self-hosted-roles.d.ts +34 -0
  98. package/dist/self-hosted-roles.d.ts.map +1 -1
  99. package/dist/self-hosted-roles.js +52 -0
  100. package/dist/self-hosted-roles.js.map +1 -1
  101. package/dist/self-hosted-start.d.ts +0 -21
  102. package/dist/self-hosted-start.d.ts.map +1 -1
  103. package/dist/self-hosted-start.js +39 -1
  104. package/dist/self-hosted-start.js.map +1 -1
  105. package/dist/self-hosted-upgrade.d.ts +15 -6
  106. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  107. package/dist/self-hosted-upgrade.js +72 -23
  108. package/dist/self-hosted-upgrade.js.map +1 -1
  109. package/dist/stack-contract.d.ts +170 -25
  110. package/dist/stack-contract.d.ts.map +1 -1
  111. package/dist/stack-contract.js +314 -37
  112. package/dist/stack-contract.js.map +1 -1
  113. package/dist/stack-converge.d.ts +31 -0
  114. package/dist/stack-converge.d.ts.map +1 -1
  115. package/dist/stack-converge.js +71 -30
  116. package/dist/stack-converge.js.map +1 -1
  117. package/dist/upgrade/app-roll.d.ts +25 -4
  118. package/dist/upgrade/app-roll.d.ts.map +1 -1
  119. package/dist/upgrade/app-roll.js +63 -17
  120. package/dist/upgrade/app-roll.js.map +1 -1
  121. package/dist/upgrade/converge.d.ts.map +1 -1
  122. package/dist/upgrade/converge.js +99 -12
  123. package/dist/upgrade/converge.js.map +1 -1
  124. package/dist/upgrade/health.d.ts +31 -7
  125. package/dist/upgrade/health.d.ts.map +1 -1
  126. package/dist/upgrade/health.js +41 -17
  127. package/dist/upgrade/health.js.map +1 -1
  128. package/dist/upgrade/image-forms.d.ts +1 -1
  129. package/dist/upgrade/image-forms.d.ts.map +1 -1
  130. package/dist/upgrade/image-forms.js +24 -7
  131. package/dist/upgrade/image-forms.js.map +1 -1
  132. package/dist/upgrade/release-images.d.ts +12 -3
  133. package/dist/upgrade/release-images.d.ts.map +1 -1
  134. package/dist/upgrade/release-images.js +13 -4
  135. package/dist/upgrade/release-images.js.map +1 -1
  136. package/dist/upgrade/upgrade.d.ts +13 -3
  137. package/dist/upgrade/upgrade.d.ts.map +1 -1
  138. package/dist/upgrade/upgrade.js +34 -2
  139. package/dist/upgrade/upgrade.js.map +1 -1
  140. package/dist/upgrader-gcp.d.ts +12 -0
  141. package/dist/upgrader-gcp.d.ts.map +1 -1
  142. package/dist/upgrader-gcp.js +49 -1
  143. package/dist/upgrader-gcp.js.map +1 -1
  144. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  145. package/dist/upgrader-iam-preflight.js +6 -1
  146. package/dist/upgrader-iam-preflight.js.map +1 -1
  147. package/dist/upgrader-job.d.ts +1 -1
  148. package/dist/upgrader-job.d.ts.map +1 -1
  149. package/dist/upgrader-job.js +20 -14
  150. package/dist/upgrader-job.js.map +1 -1
  151. package/dist/upgrader-records.d.ts +1 -1
  152. package/dist/upgrader-records.d.ts.map +1 -1
  153. package/dist/upgrader-records.js +26 -7
  154. package/dist/upgrader-records.js.map +1 -1
  155. package/dist/upgrader-setup.d.ts.map +1 -1
  156. package/dist/upgrader-setup.js +10 -5
  157. package/dist/upgrader-setup.js.map +1 -1
  158. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  159. package/dist/wizards/gcp-cost.js +22 -7
  160. package/dist/wizards/gcp-cost.js.map +1 -1
  161. package/dist/wizards/gcp.d.ts.map +1 -1
  162. package/dist/wizards/gcp.js +43 -17
  163. package/dist/wizards/gcp.js.map +1 -1
  164. package/package.json +2 -2
  165. package/src/admin-domain.test.ts +9 -8
  166. package/src/admin-domain.ts +13 -3
  167. package/src/admin-memory.test.ts +1 -0
  168. package/src/adopt-existing.test.ts +39 -0
  169. package/src/adopt-existing.ts +83 -1
  170. package/src/agent/command.test.ts +49 -1
  171. package/src/agent/command.ts +36 -9
  172. package/src/agent/run.test.ts +19 -0
  173. package/src/agent/run.ts +11 -2
  174. package/src/aws-task-roles.ts +5 -2
  175. package/src/caddy-redirects.docker.test.ts +2 -0
  176. package/src/caddy-redirects.test.ts +3 -0
  177. package/src/caddy-validate.test.ts +7 -1
  178. package/src/caddy.test.ts +39 -1
  179. package/src/caddy.ts +36 -4
  180. package/src/checkpoint-secrets.test.ts +14 -6
  181. package/src/cli.ts +12 -1
  182. package/src/cloudsql-tls.test.ts +225 -0
  183. package/src/cloudsql-tls.ts +352 -0
  184. package/src/compose.ts +125 -24
  185. package/src/converge-checkpoint.ts +8 -1
  186. package/src/converge-job-gcp.ts +2 -11
  187. package/src/database-tls-builders.test.ts +55 -0
  188. package/src/deployed-release.test.ts +104 -40
  189. package/src/deployed-release.ts +30 -9
  190. package/src/gateway-env-isolation.test.ts +10 -5
  191. package/src/gcp-names.ts +31 -0
  192. package/src/gcp-url-map.test.ts +6 -8
  193. package/src/gcp-url-map.ts +6 -2
  194. package/src/index.test.ts +7 -1
  195. package/src/index.ts +1 -0
  196. package/src/infra-contract.test.ts +5 -3
  197. package/src/infra-contract.ts +24 -1
  198. package/src/install-state.ts +26 -4
  199. package/src/lifecycle.ts +20 -11
  200. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  201. package/src/mcp-auth-proxy-setup.ts +374 -0
  202. package/src/mcp-iap-stacks.test.ts +1 -1
  203. package/src/media-volume.test.ts +1 -1
  204. package/src/operator-access-stacks.test.ts +1 -1
  205. package/src/operator-access.test.ts +5 -4
  206. package/src/pool-budget-stacks.test.ts +1 -1
  207. package/src/release-manifest.test.ts +25 -0
  208. package/src/release-manifest.ts +59 -4
  209. package/src/remote-mcp-stack-config.test.ts +19 -9
  210. package/src/remote-mcp.test.ts +252 -57
  211. package/src/remote-mcp.ts +336 -77
  212. package/src/runtime-secrets.test.ts +20 -7
  213. package/src/runtime-secrets.ts +50 -9
  214. package/src/seal-key-rotation.test.ts +2 -2
  215. package/src/seal-key-rotation.ts +25 -25
  216. package/src/self-hosted-compose.test.ts +108 -5
  217. package/src/self-hosted-db-tls.test.ts +111 -0
  218. package/src/self-hosted-db-tls.ts +287 -0
  219. package/src/self-hosted-release-resolve.test.ts +71 -0
  220. package/src/self-hosted-release.ts +41 -11
  221. package/src/self-hosted-roles.test.ts +54 -0
  222. package/src/self-hosted-roles.ts +58 -0
  223. package/src/self-hosted-start.test.ts +34 -2
  224. package/src/self-hosted-start.ts +20 -2
  225. package/src/self-hosted-upgrade.test.ts +103 -14
  226. package/src/self-hosted-upgrade.ts +115 -29
  227. package/src/stack-contract-parity.test.ts +79 -12
  228. package/src/stack-contract.test.ts +169 -29
  229. package/src/stack-contract.ts +362 -42
  230. package/src/stack-converge-secrets.test.ts +26 -13
  231. package/src/stack-converge.test.ts +199 -26
  232. package/src/stack-converge.ts +92 -30
  233. package/src/state-store.test.ts +2 -1
  234. package/src/upgrade/app-roll.ts +97 -19
  235. package/src/upgrade/converge.ts +120 -11
  236. package/src/upgrade/health.test.ts +63 -25
  237. package/src/upgrade/health.ts +54 -22
  238. package/src/upgrade/image-forms.test.ts +34 -0
  239. package/src/upgrade/image-forms.ts +23 -8
  240. package/src/upgrade/release-images.test.ts +5 -1
  241. package/src/upgrade/release-images.ts +22 -6
  242. package/src/upgrade/upgrade.test.ts +353 -44
  243. package/src/upgrade/upgrade.ts +55 -6
  244. package/src/upgrader-gcp.ts +65 -3
  245. package/src/upgrader-iam-preflight.test.ts +19 -0
  246. package/src/upgrader-iam-preflight.ts +6 -0
  247. package/src/upgrader-job.test.ts +101 -27
  248. package/src/upgrader-job.ts +27 -14
  249. package/src/upgrader-records.test.ts +97 -12
  250. package/src/upgrader-records.ts +28 -11
  251. package/src/upgrader-setup.test.ts +2 -0
  252. package/src/upgrader-setup.ts +10 -4
  253. package/src/wizards/gcp-cost.test.ts +50 -0
  254. package/src/wizards/gcp-cost.ts +24 -7
  255. package/src/wizards/gcp.ts +55 -14
  256. package/stacks/aws/index.ts +32 -14
  257. package/stacks/azure/index.ts +57 -18
  258. package/stacks/gcp/index.ts +83 -23
  259. package/stacks/gcp-firebase/index.ts +162 -33
  260. package/stacks/self-hosted/README.md +2 -1
  261. package/stacks/self-hosted/index.ts +11 -1
package/src/caddy.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  * Per-vhost shape:
12
12
  * <hostname> {
13
13
  * # admin → reverse_proxy <adminUpstream>
14
+ * # mcp → reverse_proxy <mcpAuthProxyUpstream> (#1328: the
15
+ * # MCP-Auth-Proxy on its own host, never the site's origin)
14
16
  * # public → /api/* → <gatewayUpstream>; everything else is the
15
17
  * # static site (try_files + file_server, 404 → /404.html)
16
18
  * }
@@ -68,7 +70,12 @@ import {
68
70
 
69
71
  export interface CaddyDomainSpec {
70
72
  readonly hostname: string;
71
- readonly kind: "admin" | "public";
73
+ /**
74
+ * `mcp` (#1328) is the MCP-Auth-Proxy's own host: remote MCP's sign-in
75
+ * pages and its forwarder must never share an origin with the site, whose
76
+ * module JS could otherwise read them same-origin or toss their cookies.
77
+ */
78
+ readonly kind: "admin" | "public" | "mcp";
72
79
  readonly env: "production" | "staging";
73
80
  }
74
81
 
@@ -89,6 +96,8 @@ export interface CaddyfileSpec {
89
96
  readonly adminUpstream: string;
90
97
  /** `host:port` of the API gateway as Caddy reaches it (Compose: `caelo-gateway:8090`). */
91
98
  readonly gatewayUpstream: string;
99
+ /** `host:port` of the MCP-Auth-Proxy as Caddy reaches it (Compose: `caelo-mcp-auth-proxy:8095`). */
100
+ readonly mcpAuthProxyUpstream: string;
92
101
  readonly domains: ReadonlyArray<CaddyDomainSpec>;
93
102
  }
94
103
 
@@ -97,12 +106,25 @@ export function selfHostedAdminHost(domain: string): string {
97
106
  return `admin.${domain}`;
98
107
  }
99
108
 
109
+ /** #1328 — the host the MCP-Auth-Proxy of a self-hosted install answers on. */
110
+ export function selfHostedMcpHost(domain: string): string {
111
+ return `mcp.${domain}`;
112
+ }
113
+
100
114
  /**
101
115
  * The vhosts every self-hosted install serves before an Owner adds domains
102
116
  * in the admin: the admin on `admin.<domain>`, the live site on `<domain>`,
103
- * the staging site on its own host. The admin needs its own host — it
104
- * routes from `/` (`/setup`, `/login`, …), so it cannot share one with the
105
- * public site, and Caddy refuses two site blocks for the same address.
117
+ * the staging site on its own host. The admin needs its own host — it routes
118
+ * from `/` (`/setup`, `/login`, …), so it cannot share one with the public
119
+ * site, and Caddy refuses two site blocks for the same address.
120
+ *
121
+ * #1328 — the MCP-Auth-Proxy's vhost (`mcp.<domain>`, kind `mcp`) is not
122
+ * seeded: remote MCP needs an identity proxy in front of the admin (§11.B),
123
+ * which a self-hosted install does not have yet, so the proxy runs
124
+ * unexposed on the Compose network (E14 a: provisioned, fixed 404) — the
125
+ * same honest refusal as on aws and azure. Seeding it would make Caddy
126
+ * fetch and retry an ACME certificate for a host nobody has a DNS record
127
+ * for. The vhost comes with remote MCP on self-hosted.
106
128
  */
107
129
  export function selfHostedSeedDomains(domain: string, stagingDomain: string): CaddyDomainSpec[] {
108
130
  return [
@@ -154,6 +176,7 @@ export function generateCaddyfile(spec: CaddyfileSpec): string {
154
176
  assertSafeCaddyToken("stagingSiteRoot", spec.stagingSiteRoot, "token");
155
177
  assertSafeCaddyToken("adminUpstream", spec.adminUpstream, "token");
156
178
  assertSafeCaddyToken("gatewayUpstream", spec.gatewayUpstream, "token");
179
+ assertSafeCaddyToken("mcpAuthProxyUpstream", spec.mcpAuthProxyUpstream, "token");
157
180
 
158
181
  const hostnames = new Set<string>();
159
182
  for (const d of spec.domains) {
@@ -200,6 +223,15 @@ function vhost(d: CaddyDomainSpec, spec: CaddyfileSpec): string {
200
223
  return `${d.hostname} {${noindex}
201
224
  reverse_proxy ${spec.adminUpstream}
202
225
  }
226
+ `;
227
+ }
228
+ if (d.kind === "mcp") {
229
+ // Everything on this host goes to the proxy: it answers its own 404s.
230
+ return `${d.hostname} {
231
+ header X-Robots-Tag "noindex"
232
+ header X-Content-Type-Options "${NOSNIFF}"
233
+ reverse_proxy ${spec.mcpAuthProxyUpstream}
234
+ }
203
235
  `;
204
236
  }
205
237
  // public — API routes go to the gateway, the rest is the static site.
@@ -32,14 +32,20 @@ import {
32
32
  import { databaseUrls } from "./stack-contract.js";
33
33
 
34
34
  const FAKE_PW = "fakePw0rdForTests123";
35
- const legacyAdminUrl = `postgresql://admin_role:${FAKE_PW}@10.1.2.3:5432/cms_admin?sslmode=require`;
36
- const legacyPublicUrl = `postgresql://public_role:${FAKE_PW}@10.1.2.3:5432/cms_public?sslmode=require`;
35
+ const legacyAdminUrl = `postgresql://admin_role:${FAKE_PW}@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`;
36
+ const legacyPublicUrl = `postgresql://public_role:${FAKE_PW}@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`;
37
37
  const setupLink = "https://admin.acme.test/setup?token=fakeSetupToken0123456789";
38
38
 
39
39
  /** What `pulumi up` returns on the current stacks, secret flags included. */
40
40
  const stackOutputs = {
41
- adminDatabaseUrlOut: { value: databaseUrls("10.1.2.3").admin, secret: false },
42
- publicDatabaseUrlOut: { value: databaseUrls("10.1.2.3").public, secret: false },
41
+ adminDatabaseUrlOut: {
42
+ value: databaseUrls("db1.p-label.europe-west1.sql-psa.goog").admin,
43
+ secret: false,
44
+ },
45
+ publicDatabaseUrlOut: {
46
+ value: databaseUrls("db1.p-label.europe-west1.sql-psa.goog").public,
47
+ secret: false,
48
+ },
43
49
  bootstrapUrlOut: { value: setupLink, secret: false },
44
50
  bootstrapTokenExpiresAtOut: { value: "2026-10-13T00:00:00.000Z", secret: false },
45
51
  providerOut: { value: "gcp", secret: false },
@@ -97,7 +103,7 @@ describe("findSecrets", () => {
97
103
  });
98
104
 
99
105
  it("does not flag the password-less database URLs the stacks export today", () => {
100
- expect(findSecrets(databaseUrls("10.1.2.3"))).toEqual([]);
106
+ expect(findSecrets(databaseUrls("db1.p-label.europe-west1.sql-psa.goog"))).toEqual([]);
101
107
  });
102
108
 
103
109
  it("flags secret-named keys holding a value, not markers or look-alikes", () => {
@@ -314,7 +320,9 @@ describe("redactCredentials", () => {
314
320
  it("masks URL passwords and leaves the rest readable", () => {
315
321
  const out = redactCredentials(`connect failed: ${legacyAdminUrl} (timeout)`);
316
322
  expect(out).not.toContain(FAKE_PW);
317
- expect(out).toContain("postgresql://admin_role:***@10.1.2.3:5432/cms_admin");
323
+ expect(out).toContain(
324
+ "postgresql://admin_role:***@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
325
+ );
318
326
  expect(redactCredentials("https://admin.acme.test/x")).toBe("https://admin.acme.test/x");
319
327
  });
320
328
  });
package/src/cli.ts CHANGED
@@ -63,6 +63,7 @@ import {
63
63
  BOOTSTRAP_DIR,
64
64
  COMPOSE_ADMIN_UPSTREAM,
65
65
  COMPOSE_GATEWAY_UPSTREAM,
66
+ COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
66
67
  type ComposeImages,
67
68
  composeProjectName,
68
69
  generateDockerCompose,
@@ -75,6 +76,7 @@ import {
75
76
  } from "./compose.js";
76
77
  import { tightenPrivateTree, writePrivateFile } from "./private-file.js";
77
78
  import { selfCommand } from "./self-command.js";
79
+ import { ensureSelfHostedDbTls, selfHostedDbTlsServerCertSha256 } from "./self-hosted-db-tls.js";
78
80
  import { ensureInternalSecret, writeInternalSecret } from "./self-hosted-internal-secret.js";
79
81
  import {
80
82
  bunHostRunner,
@@ -84,6 +86,7 @@ import {
84
86
  resolveVerifiedRelease,
85
87
  } from "./self-hosted-release.js";
86
88
  import {
89
+ applyPublicRoleLimits,
87
90
  convergeSelfHostedRoles,
88
91
  decideUp,
89
92
  selfHostedBootstrapScript,
@@ -305,6 +308,7 @@ function writeCaddyfile(cfg: CaeloConfig, extraDomains: CaddyDomainSpec[]): void
305
308
  // localhost (#707).
306
309
  adminUpstream: COMPOSE_ADMIN_UPSTREAM,
307
310
  gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
311
+ mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
308
312
  domains: dedupeDomains([
309
313
  ...selfHostedSeedDomains(cfg.domain, cfg.stagingDomain),
310
314
  ...extraDomains,
@@ -344,6 +348,9 @@ function renderCompose(cfg: CaeloConfig, images: ComposeImages): string {
344
348
  "config has no ports / composeProject — they are back-filled on load; this is a bug",
345
349
  );
346
350
  }
351
+ // #1338 — the install's database CA + server certificate the Compose file
352
+ // mounts (created once, the server certificate renewed when due).
353
+ for (const line of ensureSelfHostedDbTls(CAELO_DIR)) console.log(` database TLS: ${line}`);
347
354
  return generateDockerCompose({
348
355
  domain: cfg.domain,
349
356
  postgresPassword: cfg.postgresPassword,
@@ -351,6 +358,7 @@ function renderCompose(cfg: CaeloConfig, images: ComposeImages): string {
351
358
  caeloSecretKek: cfg.caeloSecretKek,
352
359
  // #770 — every render (up, upgrade, agent apply) hands the admin the
353
360
  // secret host-side commands sign their /api/internal/* tokens with.
361
+ dbTlsServerCertSha256: selfHostedDbTlsServerCertSha256(CAELO_DIR),
354
362
  internalSecret: ensureInternalSecret(CAELO_DIR, {
355
363
  randomSecret: () => randomSecret(48),
356
364
  report: (line) => console.log(line),
@@ -397,6 +405,8 @@ function agentContext(cfg: CaeloConfig): AgentCliContext {
397
405
  ...(activeAgent(cfg) ? { agentPassword: activeAgent(cfg)?.dbPassword } : {}),
398
406
  });
399
407
  },
408
+ applyPublicRoleLimits: () =>
409
+ applyPublicRoleLimits({ probe: postgresIsUp, psql: superuserPsql }),
400
410
  renderCompose: (images) => renderCompose(cfg, images),
401
411
  refreshCaddy: () => refreshCaddyfile(cfg),
402
412
  randomSecret: () => randomSecret(32),
@@ -1019,7 +1029,7 @@ async function upgradeSelfHostedCommand(opts: {
1019
1029
  console.error(
1020
1030
  "This is a DEV MODE install: the admin + gateway run your Caelo checkout, so there are\n" +
1021
1031
  "no images to upgrade. Update the checkout (git pull), then `bun packages/provisioning/src/cli.ts migrate` and\n" +
1022
- `\`docker compose -f ${COMPOSE_PATH} restart caelo-admin caelo-gateway\`.`,
1032
+ `\`docker compose -f ${COMPOSE_PATH} restart caelo-admin caelo-gateway caelo-mcp-auth-proxy\`.`,
1023
1033
  );
1024
1034
  process.exit(2);
1025
1035
  }
@@ -1047,6 +1057,7 @@ async function upgradeSelfHostedCommand(opts: {
1047
1057
  runner: bunHostRunner,
1048
1058
  renderCompose: (images) => renderCompose(cfg, images),
1049
1059
  convergeRoles: agentContext(cfg).convergeRoles,
1060
+ applyPublicRoleLimits: agentContext(cfg).applyPublicRoleLimits,
1050
1061
  refreshCaddy: () => refreshCaddyfile(cfg),
1051
1062
  log: {
1052
1063
  step: (m) => console.log(`\n==> ${m}`),
@@ -0,0 +1,225 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import {
5
+ dialsIpAddress,
6
+ ensureCloudSqlVerifyFull,
7
+ moveServicesToDnsHost,
8
+ urlOnDnsHost,
9
+ } from "./cloudsql-tls.js";
10
+ import type { GcloudResult } from "./gcloud.js";
11
+ import type { HttpFetch } from "./runtime-secrets.js";
12
+
13
+ const DNS = "abc123.p-label.europe-west1.sql-psa.goog";
14
+ const TARGET = { projectId: "p", env: "production", sqlInstance: "caelo-production-pg-1" };
15
+
16
+ const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
17
+ const notFound = (): GcloudResult => ({
18
+ ok: false,
19
+ stdout: "",
20
+ stderr: "ERROR: (gcloud) NOT_FOUND",
21
+ exitCode: 1,
22
+ });
23
+
24
+ /** An instance as `gcloud sql instances describe --format=json` prints it. */
25
+ function instance(opts: { caMode: string; sslMode: string; dns: boolean }): string {
26
+ return JSON.stringify({
27
+ settings: {
28
+ ipConfiguration: {
29
+ serverCaMode: opts.caMode,
30
+ sslMode: opts.sslMode,
31
+ privateNetwork: "projects/p/global/networks/caelo-production-vpc-1",
32
+ },
33
+ },
34
+ ipAddresses: [{ type: "PRIVATE", ipAddress: "10.20.0.3" }],
35
+ dnsNames: opts.dns
36
+ ? [{ name: `${DNS}.`, connectionType: "PRIVATE_SERVICES_ACCESS", dnsScope: "INSTANCE" }]
37
+ : [],
38
+ });
39
+ }
40
+
41
+ /** A fake gcloud: `answer` per call, every call recorded. */
42
+ function fake(answer: (args: string[]) => GcloudResult) {
43
+ const calls: string[] = [];
44
+ return {
45
+ calls,
46
+ run: async (args: string[]) => {
47
+ calls.push(args.join(" "));
48
+ return answer(args);
49
+ },
50
+ };
51
+ }
52
+
53
+ /** The Cloud SQL Admin API listServerCertificates answer. */
54
+ function certs(body: object): HttpFetch {
55
+ return async () => ({ ok: true, status: 200, text: async () => JSON.stringify(body) });
56
+ }
57
+
58
+ describe("ensureCloudSqlVerifyFull", () => {
59
+ it("moves a per-instance-CA instance to the shared CA, rotates, and makes its name resolve", async () => {
60
+ let described = 0;
61
+ const g = fake((a) => {
62
+ const line = a.join(" ");
63
+ if (line.startsWith("sql instances describe")) {
64
+ described += 1;
65
+ return ok(
66
+ instance(
67
+ described === 1
68
+ ? {
69
+ caMode: "GOOGLE_MANAGED_INTERNAL_CA",
70
+ sslMode: "ALLOW_UNENCRYPTED_AND_ENCRYPTED",
71
+ dns: false,
72
+ }
73
+ : { caMode: "GOOGLE_MANAGED_CAS_CA", sslMode: "ENCRYPTED_ONLY", dns: true },
74
+ ),
75
+ );
76
+ }
77
+ if (line.startsWith("auth print-access-token")) return ok("token");
78
+ if (line.includes(" describe ")) return notFound();
79
+ return ok();
80
+ });
81
+ const r = await ensureCloudSqlVerifyFull(TARGET, {
82
+ run: g.run,
83
+ http: certs({ serverCerts: [] }),
84
+ sleep: async () => {},
85
+ });
86
+ expect(r).toMatchObject({ ok: true, host: DNS });
87
+ const writes = g.calls.filter((c) => !c.includes(" describe ") && !c.startsWith("auth"));
88
+ expect(writes).toEqual([
89
+ "sql instances patch caelo-production-pg-1 --project=p --server-ca-mode=GOOGLE_MANAGED_CAS_CA --ssl-mode=ENCRYPTED_ONLY --quiet",
90
+ "sql ssl server-certs create --instance=caelo-production-pg-1 --project=p",
91
+ "sql ssl server-certs rotate --instance=caelo-production-pg-1 --project=p --quiet",
92
+ `dns managed-zones create caelo-production-pg-dns --project=p --dns-name=${DNS}. --visibility=private --networks=caelo-production-vpc-1 --description=Caelo production: the Cloud SQL instance's DNS name inside the VPC (#1338)`,
93
+ `dns record-sets create ${DNS}. --zone=caelo-production-pg-dns --type=A --project=p --ttl=300 --rrdatas=10.20.0.3`,
94
+ ]);
95
+ });
96
+
97
+ it("changes nothing on an instance already in shape", async () => {
98
+ const g = fake((a) => {
99
+ const line = a.join(" ");
100
+ if (line.startsWith("sql instances describe")) {
101
+ return ok(
102
+ instance({ caMode: "GOOGLE_MANAGED_CAS_CA", sslMode: "ENCRYPTED_ONLY", dns: true }),
103
+ );
104
+ }
105
+ if (line.startsWith("auth")) return ok("token");
106
+ if (line.startsWith("dns managed-zones describe"))
107
+ return ok(JSON.stringify({ dnsName: `${DNS}.` }));
108
+ if (line.startsWith("dns record-sets describe"))
109
+ return ok(JSON.stringify({ rrdatas: ["10.20.0.3"] }));
110
+ return ok();
111
+ });
112
+ const r = await ensureCloudSqlVerifyFull(TARGET, {
113
+ run: g.run,
114
+ http: certs({ activeVersion: "f1", serverCerts: [{ sha1Fingerprint: "f1" }] }),
115
+ });
116
+ expect(r).toEqual({ ok: true, host: DNS, done: [] });
117
+ expect(g.calls.every((c) => c.includes(" describe ") || c.startsWith("auth"))).toBe(true);
118
+ });
119
+
120
+ it("finishes an interrupted rotation without creating a second certificate", async () => {
121
+ const g = fake((a) => {
122
+ const line = a.join(" ");
123
+ if (line.startsWith("sql instances describe")) {
124
+ return ok(
125
+ instance({ caMode: "GOOGLE_MANAGED_CAS_CA", sslMode: "ENCRYPTED_ONLY", dns: true }),
126
+ );
127
+ }
128
+ if (line.startsWith("auth")) return ok("token");
129
+ if (line.startsWith("dns managed-zones describe"))
130
+ return ok(JSON.stringify({ dnsName: `${DNS}.` }));
131
+ if (line.startsWith("dns record-sets describe"))
132
+ return ok(JSON.stringify({ rrdatas: ["10.9.9.9"] }));
133
+ return ok();
134
+ });
135
+ const r = await ensureCloudSqlVerifyFull(TARGET, {
136
+ run: g.run,
137
+ http: certs({
138
+ activeVersion: "old-per-instance",
139
+ serverCerts: [{ sha1Fingerprint: "upcoming" }],
140
+ }),
141
+ });
142
+ expect(r.ok).toBe(true);
143
+ expect(g.calls.some((c) => c.startsWith("sql ssl server-certs create"))).toBe(false);
144
+ expect(g.calls.some((c) => c.startsWith("sql ssl server-certs rotate"))).toBe(true);
145
+ expect(
146
+ g.calls.some(
147
+ (c) => c.startsWith("dns record-sets update") && c.endsWith("--rrdatas=10.20.0.3"),
148
+ ),
149
+ ).toBe(true);
150
+ });
151
+
152
+ it("refuses a zone that serves another name instead of repointing it", async () => {
153
+ const g = fake((a) => {
154
+ const line = a.join(" ");
155
+ if (line.startsWith("sql instances describe")) {
156
+ return ok(
157
+ instance({ caMode: "GOOGLE_MANAGED_CAS_CA", sslMode: "ENCRYPTED_ONLY", dns: true }),
158
+ );
159
+ }
160
+ if (line.startsWith("auth")) return ok("token");
161
+ if (line.startsWith("dns managed-zones describe")) {
162
+ return ok(JSON.stringify({ dnsName: "old.p-label.europe-west1.sql-psa.goog." }));
163
+ }
164
+ return ok();
165
+ });
166
+ const r = await ensureCloudSqlVerifyFull(TARGET, {
167
+ run: g.run,
168
+ http: certs({ activeVersion: "f1", serverCerts: [{ sha1Fingerprint: "f1" }] }),
169
+ });
170
+ expect(r).toMatchObject({ ok: false });
171
+ if (!r.ok) expect(r.error).toContain("serves old.p-label");
172
+ });
173
+
174
+ it("fails loudly when the instance names no private DNS name", async () => {
175
+ const g = fake((a) =>
176
+ a.join(" ").startsWith("sql instances describe")
177
+ ? ok(instance({ caMode: "GOOGLE_MANAGED_CAS_CA", sslMode: "ENCRYPTED_ONLY", dns: false }))
178
+ : ok("token"),
179
+ );
180
+ const r = await ensureCloudSqlVerifyFull(TARGET, {
181
+ run: g.run,
182
+ http: certs({ activeVersion: "f1", serverCerts: [{ sha1Fingerprint: "f1" }] }),
183
+ });
184
+ expect(r).toMatchObject({ ok: false });
185
+ if (!r.ok) expect(r.error).toContain("PRIVATE_SERVICES_ACCESS DNS name");
186
+ });
187
+ });
188
+
189
+ describe("moveServicesToDnsHost", () => {
190
+ it("switches only the URLs that dial an IP, keeping their sslmode", async () => {
191
+ const g = fake(() => ok());
192
+ const r = await moveServicesToDnsHost(
193
+ { projectId: "p", region: "europe-west1" },
194
+ [
195
+ {
196
+ serviceName: "caelo-production-admin-1",
197
+ liveUrls: {
198
+ ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
199
+ PUBLIC_ADMIN_DATABASE_URL:
200
+ "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
201
+ },
202
+ },
203
+ {
204
+ serviceName: "caelo-production-gateway-1",
205
+ liveUrls: { PUBLIC_DATABASE_URL: `postgresql://public_role@${DNS}:5432/cms_public` },
206
+ },
207
+ ],
208
+ DNS,
209
+ { run: g.run, sleep: async () => {} },
210
+ );
211
+ expect(r.ok).toBe(true);
212
+ expect(g.calls).toEqual([
213
+ `run services update caelo-production-admin-1 --region europe-west1 --project=p --update-env-vars=^|^ADMIN_DATABASE_URL=postgresql://admin_role@${DNS}:5432/cms_admin?sslmode=require|PUBLIC_ADMIN_DATABASE_URL=postgresql://admin_role@${DNS}:5432/cms_public?sslmode=require --quiet`,
214
+ ]);
215
+ });
216
+ });
217
+
218
+ describe("urlOnDnsHost / dialsIpAddress", () => {
219
+ it("tells IP-dialling URLs apart and moves only those", () => {
220
+ expect(dialsIpAddress("postgresql://a@10.0.0.3:5432/d")).toBe(true);
221
+ expect(dialsIpAddress(`postgresql://a@${DNS}:5432/d`)).toBe(false);
222
+ expect(dialsIpAddress("not a url")).toBe(false);
223
+ expect(urlOnDnsHost(`postgresql://a@${DNS}:5432/d`, "x")).toBeNull();
224
+ });
225
+ });