From 58368a77fefd7c00d06e30b4830b6afb06dc40d7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 2 Jul 2026 21:38:54 +0100 Subject: [PATCH] fix(worker): restrict Azure lease selectors --- CHANGELOG.md | 4 ++++ docs/providers/azure.md | 6 ++++-- worker/src/fleet.ts | 6 ++++++ worker/test/fleet.test.ts | 45 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e759ec9e6..ed579bb4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixed + +- Restricted brokered Azure image and OS-disk selectors to admin-authenticated requests while preserving user-selectable Azure placement. Thanks @coygeek. + ## 0.34.0 - 2026-07-02 ### Added diff --git a/docs/providers/azure.md b/docs/providers/azure.md index b74b83d58..28cbde852 100644 --- a/docs/providers/azure.md +++ b/docs/providers/azure.md @@ -183,8 +183,10 @@ Brokered leases reuse the same Azure service-principal secrets on the coordinato `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, and `AZURE_SUBSCRIPTION_ID`. Operators own the resource group, vnet, subnet, NSG, OS disk mode, and SSH CIDR defaults through the `CRABBOX_AZURE_*` env vars on the -Worker. A lease request may override only `azureLocation`, `azureImage`, and -`azureOSDisk`. +Worker. Explicit broker requests for `azureImage` and `azureOSDisk` require +admin-token authentication. Normal broker users receive coordinator-managed +image and OS-disk values; `azureLocation` remains user-selectable for capacity +routing. Direct mode keeps these local overrides. Set `CRABBOX_AZURE_WINDOWS_ARM64_IMAGE` on the coordinator when brokered Azure Windows ARM64 leases need a default ARM64 Windows image without changing the global `CRABBOX_AZURE_IMAGE` fallback used by existing custom-image leases. diff --git a/worker/src/fleet.ts b/worker/src/fleet.ts index 808d05748..56957672e 100644 --- a/worker/src/fleet.ts +++ b/worker/src/fleet.ts @@ -15840,6 +15840,12 @@ class AzureProvider implements CloudProvider { return this.clientValue; } + restrictedLeaseRequestFields(input: LeaseRequest): string[] { + return [input.azureImage ? "azureImage" : "", input.azureOSDisk ? "azureOSDisk" : ""].filter( + Boolean, + ); + } + listCrabboxServers(): Promise { return this.client.listCrabboxServers(); } diff --git a/worker/test/fleet.test.ts b/worker/test/fleet.test.ts index 52c97f8c4..87fd7d854 100644 --- a/worker/test/fleet.test.ts +++ b/worker/test/fleet.test.ts @@ -129,6 +129,12 @@ const restrictedBrokerSelectorCases = [ field: "gcpServiceAccount", value: "runner@other-project.iam.gserviceaccount.com", }, + { + provider: "azure" as const, + field: "azureImage", + value: "Canonical:0001-com-ubuntu-server-noble:24_04-lts-gen2:latest", + }, + { provider: "azure" as const, field: "azureOSDisk", value: "ephemeral" }, ]; class FakeWebSocket { @@ -3653,6 +3659,14 @@ describe("fleet lease identity and idle", () => { }, body: { os: "ubuntu:24.04" }, }, + { + provider: "azure" as const, + env: { + CRABBOX_AZURE_IMAGE: "Canonical:0001-com-ubuntu-server-noble:24_04-lts-gen2:latest", + CRABBOX_AZURE_OS_DISK: "ephemeral", + }, + body: {}, + }, ])( "does not treat coordinator $provider defaults as caller-supplied selectors", async ({ provider, env, body }) => { @@ -3682,6 +3696,37 @@ describe("fleet lease identity and idle", () => { }, ); + it("allows brokered Azure location selection without admin auth", async () => { + const storage = new MemoryStorage(); + const providerFetch = vi.fn(); + vi.stubGlobal("fetch", providerFetch); + const fleet = testFleet(storage); + + const response = await fleet.fetch( + request("POST", "/v1/leases", { + headers: { + "x-crabbox-owner": "alice@example.com", + "x-crabbox-org": "example-org", + }, + body: { + leaseID: "cbx_abcdef123456", + provider: "azure", + azureLocation: "westus3", + sshPublicKey: "ssh-ed25519 azure-location-test", + }, + }), + ); + + expect(response.status).toBe(424); + await expect(response.json()).resolves.toMatchObject({ + error: "provider_not_configured", + provider: "azure", + }); + expect(providerFetch).not.toHaveBeenCalled(); + expect((await storage.list()).size).toBe(0); + expect(storage.alarm()).toBeUndefined(); + }); + it("skips broker selector restrictions for internal workspace provisioning", async () => { let created = false; let restrictionChecks = 0;