Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/providers/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions worker/src/fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15840,6 +15840,12 @@ class AzureProvider implements CloudProvider {
return this.clientValue;
}

restrictedLeaseRequestFields(input: LeaseRequest): string[] {
return [input.azureImage ? "azureImage" : "", input.azureOSDisk ? "azureOSDisk" : ""].filter(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect empty Azure OS-disk overrides

When the coordinator default is CRABBOX_AZURE_OS_DISK=ephemeral, a non-admin broker caller can send "azureOSDisk":""; this truthiness check reports no restricted fields, but leaseConfig still treats the present empty string as the selected value and normalizes it to managed, so the caller silently overrides the operator-managed OS-disk policy without admin auth. Check field presence (or trim/nullish semantics) rather than truthiness for azureOSDisk.

Useful? React with 👍 / 👎.

Boolean,
);
}

listCrabboxServers(): Promise<ProviderMachine[]> {
return this.client.listCrabboxServers();
}
Expand Down
45 changes: 45 additions & 0 deletions worker/test/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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<typeof fetch>();
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;
Expand Down