diff --git a/.changeset/fuse-privileged-containers.md b/.changeset/fuse-privileged-containers.md new file mode 100644 index 0000000000..fe089a6979 --- /dev/null +++ b/.changeset/fuse-privileged-containers.md @@ -0,0 +1,8 @@ +--- +"wrangler": minor +"miniflare": minor +--- + +Add `dev.privileged_containers` config and `--privileged-containers` CLI flag for FUSE in local dev + +When set, miniflare launches local containers with the elevated permissions FUSE requires (`CAP_SYS_ADMIN`, `/dev/fuse`, AppArmor unconfined). Off by default. diff --git a/packages/miniflare/src/plugins/do/index.ts b/packages/miniflare/src/plugins/do/index.ts index 67cfbf964f..f3f91fd03c 100644 --- a/packages/miniflare/src/plugins/do/index.ts +++ b/packages/miniflare/src/plugins/do/index.ts @@ -17,6 +17,9 @@ import type { // Options for a container attached to the DO export const DOContainerOptionsSchema = z.object({ imageName: z.string(), + // When true, workerd creates the container with the elevated privileges + // needed for FUSE (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined). + allowPrivileged: z.boolean().optional(), }); export type DOContainerOptions = z.infer; diff --git a/packages/miniflare/src/runtime/config/workerd.ts b/packages/miniflare/src/runtime/config/workerd.ts index 4ee0396b22..568b8dd75a 100644 --- a/packages/miniflare/src/runtime/config/workerd.ts +++ b/packages/miniflare/src/runtime/config/workerd.ts @@ -195,8 +195,14 @@ export type Worker_DurableObjectNamespace = { className?: string; preventEviction?: boolean; enableSql?: boolean; + container?: Worker_ContainerOptions; } & ({ uniqueKey?: string } | { ephemeralLocal?: Void }); +export type Worker_ContainerOptions = { + imageName?: string; + allowPrivileged?: boolean; +}; + export type ExternalServer = { address?: string } & ( | { http: HttpOptions } | { https: ExternalServer_Https } diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index b7f36973ff..d8cfec1df9 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -254,6 +254,15 @@ export interface DevConfig { */ enable_containers: boolean; + /** + * When developing, whether to launch containers with the elevated privileges + * required for FUSE mounts (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined). + * Off by default — only enable when your container needs FUSE locally. + * + * @default false + */ + privileged_containers: boolean; + /** * Either the Docker unix socket i.e. `unix:///var/run/docker.sock` or a full configuration. * Note that windows is only supported via WSL at the moment @@ -312,6 +321,7 @@ export const defaultWranglerConfig: Config = { host: undefined, // Note this one is also workers only enable_containers: true, + privileged_containers: false, container_engine: undefined, generate_types: false, }, diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 83969a0e56..1481001781 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -237,6 +237,7 @@ export type NormalizeAndValidateConfigArgs = { upstreamProtocol?: string; script?: string; enableContainers?: boolean; + privilegedContainers?: boolean; generateTypes?: boolean; }; @@ -692,6 +693,7 @@ function normalizeAndValidateDev( upstreamProtocol: upstreamProtocolArg, remote: remoteArg, enableContainers: enableContainersArg, + privilegedContainers: privilegedContainersArg, generateTypes: generateTypesArg, } = args; assert( @@ -709,6 +711,10 @@ function normalizeAndValidateDev( enableContainersArg === undefined || typeof enableContainersArg === "boolean" ); + assert( + privilegedContainersArg === undefined || + typeof privilegedContainersArg === "boolean" + ); assert( generateTypesArg === undefined || typeof generateTypesArg === "boolean" ); @@ -731,6 +737,7 @@ function normalizeAndValidateDev( : local_protocol, host, enable_containers = enableContainersArg ?? true, + privileged_containers = privilegedContainersArg ?? false, container_engine, generate_types = generateTypesArg ?? false, ...rest @@ -778,6 +785,14 @@ function normalizeAndValidateDev( "boolean" ); + validateOptionalProperty( + diagnostics, + "dev", + "privileged_containers", + privileged_containers, + "boolean" + ); + validateOptionalProperty( diagnostics, "dev", @@ -803,6 +818,7 @@ function normalizeAndValidateDev( upstream_protocol, host, enable_containers, + privileged_containers, container_engine, generate_types, }; diff --git a/packages/wrangler/src/__tests__/config-validation-pages.test.ts b/packages/wrangler/src/__tests__/config-validation-pages.test.ts index 898f150738..a3858758f0 100644 --- a/packages/wrangler/src/__tests__/config-validation-pages.test.ts +++ b/packages/wrangler/src/__tests__/config-validation-pages.test.ts @@ -194,6 +194,7 @@ describe("validatePagesConfig()", () => { upstream_protocol: "https", host: "test-host", enable_containers: false, + privileged_containers: false, container_engine: undefined, generate_types: false, }, diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index 83bdf82eca..4396a14d99 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -1679,6 +1679,44 @@ describe.sequential("wrangler dev", () => { }); }); + describe("privileged containers", () => { + it("should default to false", async ({ expect }) => { + writeWranglerConfig({ + main: "index.js", + }); + fs.writeFileSync("index.js", `export default {};`); + const config = await runWranglerUntilConfig("dev"); + expect(config.dev.privilegedContainers).toBe(false); + }); + + it("should be true when `dev.privileged_containers` is set in the Wrangler config", async ({ + expect, + }) => { + writeWranglerConfig({ + main: "index.js", + dev: { + privileged_containers: true, + }, + }); + fs.writeFileSync("index.js", `export default {};`); + const config = await runWranglerUntilConfig("dev"); + expect(config.dev.privilegedContainers).toBe(true); + }); + + it("should be true when `--privileged-containers` is passed on the CLI", async ({ + expect, + }) => { + writeWranglerConfig({ + main: "index.js", + }); + fs.writeFileSync("index.js", `export default {};`); + const config = await runWranglerUntilConfig( + "dev --privileged-containers" + ); + expect(config.dev.privilegedContainers).toBe(true); + }); + }); + describe("durable_objects", () => { it("should warn if there are remote Durable Objects, or missing migrations for local Durable Objects", async ({ expect, diff --git a/packages/wrangler/src/api/dev.ts b/packages/wrangler/src/api/dev.ts index 848c79accc..3a900b8126 100644 --- a/packages/wrangler/src/api/dev.ts +++ b/packages/wrangler/src/api/dev.ts @@ -164,6 +164,7 @@ export async function unstable_dev( d1Databases, disableDevRegistry, testScheduled: testScheduled ?? false, + privilegedContainers: false, enablePagesAssetsServiceBinding, forceLocal, liveReload, diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index 0a2da465de..5089d1e037 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -296,6 +296,8 @@ async function getMiniflareOptionsFromConfig(args: { ), containerBuildId: undefined, enableContainers: config.dev.enable_containers, + privilegedContainers: + config.dev.privileged_containers, }, remoteProxyConnectionString ); @@ -467,6 +469,8 @@ export function unstable_getMiniflareWorkerOptions( containerDOClassNames, containerBuildId: options?.containerBuildId, enableContainers, + privilegedContainers: + config.dev.privileged_containers, }, options?.remoteProxyConnectionString ); diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index 28371ac7c3..155b9f8d38 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -166,6 +166,9 @@ async function resolveDevConfig( inferOriginFromRoutes: input.dev?.inferOriginFromRoutes ?? true, enableContainers: input.dev?.enableContainers ?? config.dev.enable_containers, + privilegedContainers: + input.dev?.privilegedContainers ?? + config.dev.privileged_containers, dockerPath: input.dev?.dockerPath ?? getDockerPath(), containerEngine: useContainers ? (input.dev?.containerEngine ?? diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index f1d4bcb0d6..78dced7616 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -211,6 +211,8 @@ export async function convertToConfigBundle( containerBuildId: event.config.dev?.containerBuildId, containerEngine: event.config.dev.containerEngine, enableContainers: event.config.dev.enableContainers ?? true, + privilegedContainers: + event.config.dev.privilegedContainers ?? false, zone: getZoneForCfWorkerHeader(event.config), sendMetrics: event.config.sendMetrics, publicUrl: event.config.dev?.server?.port diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index d3a4725b28..ad038ff37d 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -180,6 +180,12 @@ export interface StartDevWorkerInput { /** Whether to build and connect to containers during local dev. Requires Docker daemon to be running. Defaults to true. */ enableContainers?: boolean; + /** + * Whether to launch local containers with the elevated privileges needed for FUSE + * (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined). Defaults to false. + */ + privilegedContainers?: boolean; + /** Path to the dev registry directory */ registry?: string; diff --git a/packages/wrangler/src/dev.ts b/packages/wrangler/src/dev.ts index 86c6f82ac1..6cf3797282 100644 --- a/packages/wrangler/src/dev.ts +++ b/packages/wrangler/src/dev.ts @@ -142,6 +142,12 @@ export const dev = createCommand({ describe: "Whether to build and enable containers during development", hidden: true, }, + "privileged-containers": { + type: "boolean", + describe: + "Launch local containers with the elevated privileges required for FUSE (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined)", + hidden: true, + }, site: { describe: "Root folder of static assets for Workers Sites", type: "string", diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 53f845e887..bc1a70e5e4 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -102,6 +102,7 @@ export interface ConfigBundle { containerBuildId: string | undefined; containerEngine: ContainerEngine | undefined; enableContainers: boolean; + privilegedContainers: boolean; // Zone to use for the CF-Worker header in outbound fetches zone: string | undefined; sendMetrics: boolean | undefined; @@ -478,6 +479,7 @@ type MiniflareBindingsConfig = Pick< | "containerDOClassNames" | "containerBuildId" | "enableContainers" + | "privilegedContainers" > & Partial< Pick @@ -734,6 +736,7 @@ export function buildMiniflareBindingOptions( doClassName: className, containerDOClassNames: config.containerDOClassNames, containerBuildId: config.containerBuildId, + allowPrivileged: config.privilegedContainers, }) : undefined, }); @@ -1007,6 +1010,8 @@ export function buildMiniflareBindingOptions( doClassName: className, containerDOClassNames: config.containerDOClassNames, containerBuildId: config.containerBuildId, + allowPrivileged: + config.privilegedContainers, }) : undefined, }, @@ -1161,6 +1166,7 @@ export function getImageNameFromDOClassName(options: { doClassName: string; containerDOClassNames: Set; containerBuildId: string | undefined; + allowPrivileged: boolean; }): DOContainerOptions | undefined { assert( options.containerBuildId, @@ -1173,6 +1179,7 @@ export function getImageNameFromDOClassName(options: { options.doClassName, options.containerBuildId ), + allowPrivileged: options.allowPrivileged, }; } } diff --git a/packages/wrangler/src/dev/start-dev.ts b/packages/wrangler/src/dev/start-dev.ts index 8067fb18e9..e5d2710c5f 100644 --- a/packages/wrangler/src/dev/start-dev.ts +++ b/packages/wrangler/src/dev/start-dev.ts @@ -266,6 +266,7 @@ async function setupDevEnv( registry: args.disableDevRegistry ? undefined : getRegistryPath(), multiworkerPrimary: args.multiworkerPrimary, enableContainers: args.enableContainers, + privilegedContainers: args.privilegedContainers, dockerPath: args.dockerPath, // initialise with a random id containerBuildId: generateContainerBuildId(), diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index c6b56ad373..04ee378b69 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -1034,6 +1034,7 @@ export const pagesDevCommand = createCommand({ siteInclude: undefined, siteExclude: undefined, enableContainers: false, + privilegedContainers: false, types: false, tunnel: undefined, })