@caelo-cms/provisioning 0.11.9 → 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.
- package/dist/admin-domain.d.ts +10 -1
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +13 -3
- package/dist/admin-domain.js.map +1 -1
- package/dist/adopt-existing.d.ts +6 -1
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +86 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +20 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +32 -9
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/run.d.ts +2 -0
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +5 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +44 -2
- package/dist/aws-task-roles.d.ts +4 -2
- package/dist/aws-task-roles.d.ts.map +1 -1
- package/dist/aws-task-roles.js +4 -1
- package/dist/aws-task-roles.js.map +1 -1
- package/dist/caddy.d.ts +21 -4
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +27 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +63 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +289 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +29 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +95 -23
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +8 -1
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +2 -11
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/deployed-release.d.ts +4 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +18 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +21 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +24 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +6 -2
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +24 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-state.d.ts +6 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +17 -4
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +17 -8
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +48 -0
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
- package/dist/mcp-auth-proxy-setup.js +280 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -0
- package/dist/release-manifest.d.ts +25 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +36 -3
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts +55 -18
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +307 -77
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +9 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +21 -10
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/seal-key-rotation.d.ts +7 -7
- package/dist/seal-key-rotation.d.ts.map +1 -1
- package/dist/seal-key-rotation.js +22 -22
- package/dist/seal-key-rotation.js.map +1 -1
- package/dist/self-hosted-db-tls.d.ts +37 -0
- package/dist/self-hosted-db-tls.d.ts.map +1 -0
- package/dist/self-hosted-db-tls.js +220 -0
- package/dist/self-hosted-db-tls.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +33 -9
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +34 -0
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +52 -0
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-start.d.ts +0 -21
- package/dist/self-hosted-start.d.ts.map +1 -1
- package/dist/self-hosted-start.js +39 -1
- package/dist/self-hosted-start.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +15 -6
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +72 -23
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +170 -25
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +314 -37
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +31 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +71 -30
- package/dist/stack-converge.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +25 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +63 -17
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +99 -12
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -7
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +41 -17
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +1 -1
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +24 -7
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -3
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +13 -4
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +13 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +34 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +12 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +49 -1
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +6 -1
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +20 -14
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +1 -1
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +26 -7
- package/dist/upgrader-records.js.map +1 -1
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +10 -5
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +22 -7
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +43 -17
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +1 -0
- package/src/adopt-existing.test.ts +39 -0
- package/src/adopt-existing.ts +83 -1
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +36 -9
- package/src/agent/run.test.ts +19 -0
- package/src/agent/run.ts +11 -2
- package/src/aws-task-roles.ts +5 -2
- package/src/caddy-redirects.docker.test.ts +2 -0
- package/src/caddy-redirects.test.ts +3 -0
- package/src/caddy-validate.test.ts +7 -1
- package/src/caddy.test.ts +39 -1
- package/src/caddy.ts +36 -4
- package/src/checkpoint-secrets.test.ts +14 -6
- package/src/cli.ts +12 -1
- package/src/cloudsql-tls.test.ts +225 -0
- package/src/cloudsql-tls.ts +352 -0
- package/src/compose.ts +125 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-job-gcp.ts +2 -11
- package/src/database-tls-builders.test.ts +55 -0
- package/src/deployed-release.test.ts +104 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +10 -5
- package/src/gcp-names.ts +31 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +7 -1
- package/src/index.ts +1 -0
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +24 -1
- package/src/install-state.ts +26 -4
- package/src/lifecycle.ts +20 -11
- package/src/mcp-auth-proxy-setup.test.ts +305 -0
- package/src/mcp-auth-proxy-setup.ts +374 -0
- package/src/mcp-iap-stacks.test.ts +1 -1
- package/src/media-volume.test.ts +1 -1
- package/src/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +5 -4
- package/src/pool-budget-stacks.test.ts +1 -1
- package/src/release-manifest.test.ts +25 -0
- package/src/release-manifest.ts +59 -4
- package/src/remote-mcp-stack-config.test.ts +19 -9
- package/src/remote-mcp.test.ts +252 -57
- package/src/remote-mcp.ts +336 -77
- package/src/runtime-secrets.test.ts +20 -7
- package/src/runtime-secrets.ts +50 -9
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-compose.test.ts +108 -5
- package/src/self-hosted-db-tls.test.ts +111 -0
- package/src/self-hosted-db-tls.ts +287 -0
- package/src/self-hosted-release-resolve.test.ts +71 -0
- package/src/self-hosted-release.ts +41 -11
- package/src/self-hosted-roles.test.ts +54 -0
- package/src/self-hosted-roles.ts +58 -0
- package/src/self-hosted-start.test.ts +34 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +103 -14
- package/src/self-hosted-upgrade.ts +115 -29
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +169 -29
- package/src/stack-contract.ts +362 -42
- package/src/stack-converge-secrets.test.ts +26 -13
- package/src/stack-converge.test.ts +199 -26
- package/src/stack-converge.ts +92 -30
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +120 -11
- package/src/upgrade/health.test.ts +63 -25
- package/src/upgrade/health.ts +54 -22
- package/src/upgrade/image-forms.test.ts +34 -0
- package/src/upgrade/image-forms.ts +23 -8
- package/src/upgrade/release-images.test.ts +5 -1
- package/src/upgrade/release-images.ts +22 -6
- package/src/upgrade/upgrade.test.ts +353 -44
- package/src/upgrade/upgrade.ts +55 -6
- package/src/upgrader-gcp.ts +65 -3
- package/src/upgrader-iam-preflight.test.ts +19 -0
- package/src/upgrader-iam-preflight.ts +6 -0
- package/src/upgrader-job.test.ts +101 -27
- package/src/upgrader-job.ts +27 -14
- package/src/upgrader-records.test.ts +97 -12
- package/src/upgrader-records.ts +28 -11
- package/src/upgrader-setup.test.ts +2 -0
- package/src/upgrader-setup.ts +10 -4
- package/src/wizards/gcp-cost.test.ts +50 -0
- package/src/wizards/gcp-cost.ts +24 -7
- package/src/wizards/gcp.ts +55 -14
- package/stacks/aws/index.ts +32 -14
- package/stacks/azure/index.ts +57 -18
- package/stacks/gcp/index.ts +83 -23
- package/stacks/gcp-firebase/index.ts +162 -33
- package/stacks/self-hosted/README.md +2 -1
- 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
|
-
|
|
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
|
-
*
|
|
105
|
-
*
|
|
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}@
|
|
36
|
-
const legacyPublicUrl = `postgresql://public_role:${FAKE_PW}@
|
|
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: {
|
|
42
|
-
|
|
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("
|
|
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(
|
|
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
|
+
});
|