From b23317354c83b8adda516b505cfe234efdbb9032 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 14 Dec 2025 20:30:33 +0100 Subject: [PATCH] refactor: allow to run without the SYS_ADMIN cap for simple setups (#138) * refactor: allow to run without the SYS_ADMIN cap for simple setups * fix: pr feedback --- README.md | 28 ++++++++++ app/client/api-client/types.gen.ts | 1 + app/client/hooks/use-system-info.ts | 2 +- .../volumes/components/create-volume-form.tsx | 55 ++++++++++++++++--- app/server/core/capabilities.ts | 43 +++++++++++++++ app/server/modules/system/system.dto.ts | 1 + 6 files changed, 120 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8bf2b7bb..87289812 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,34 @@ docker compose up -d Once the container is running, you can access the web interface at `http://:4096`. +### Simplified setup (No remote mounts) + +If you only need to back up locally mounted folders and don't require remote share mounting capabilities, you can remove the `SYS_ADMIN` capability and FUSE device from your `docker-compose.yml`: + +```yaml +services: + zerobyte: + image: ghcr.io/nicotsx/zerobyte:v0.18 + container_name: zerobyte + restart: unless-stopped + ports: + - "4096:4096" + environment: + - TZ=Europe/Paris # Set your timezone here + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/lib/zerobyte:/var/lib/zerobyte + - /path/to/your/directory:/mydata +``` + +**Trade-offs:** +- ✅ Improved security by reducing container capabilities +- ✅ Support for local directories +- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone) +- ❌ Cannot mount NFS, SMB, or WebDAV shares directly from Zerobyte + +If you need remote mount capabilities, keep the original configuration with `cap_add: SYS_ADMIN` and `devices: /dev/fuse:/dev/fuse`. + ## Adding your first volume Zerobyte supports multiple volume backends including NFS, SMB, WebDAV, and local directories. A volume represents the source data you want to back up and monitor. diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index bc2b48ef..87b61d3e 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -2855,6 +2855,7 @@ export type GetSystemInfoResponses = { 200: { capabilities: { rclone: boolean; + sysAdmin: boolean; }; }; }; diff --git a/app/client/hooks/use-system-info.ts b/app/client/hooks/use-system-info.ts index 47046bae..762d228c 100644 --- a/app/client/hooks/use-system-info.ts +++ b/app/client/hooks/use-system-info.ts @@ -10,7 +10,7 @@ export function useSystemInfo() { }); return { - capabilities: data?.capabilities ?? { rclone: false }, + capabilities: data?.capabilities ?? { rclone: false, sysAdmin: false }, isLoading, error, systemInfo: data, diff --git a/app/client/modules/volumes/components/create-volume-form.tsx b/app/client/modules/volumes/components/create-volume-form.tsx index fd7f6176..e4f05125 100644 --- a/app/client/modules/volumes/components/create-volume-form.tsx +++ b/app/client/modules/volumes/components/create-volume-form.tsx @@ -64,7 +64,6 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for const { watch, getValues } = form; const { capabilities } = useSystemInfo(); - const watchedBackend = watch("backend"); useEffect(() => { @@ -142,16 +141,54 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for Directory - NFS - SMB - WebDAV - - - rclone (40+ cloud providers) - + +
+ + NFS + +
- + +

Remote mounts require SYS_ADMIN capability

+
+
+ + +
+ + SMB + +
+
+ +

Remote mounts require SYS_ADMIN capability

+
+
+ + +
+ + WebDAV + +
+
+ +

Remote mounts require SYS_ADMIN capability

+
+
+ + +
+ + rclone + +
+
+ +

Remote mounts require SYS_ADMIN capability

+
+

Setup rclone to use this backend

diff --git a/app/server/core/capabilities.ts b/app/server/core/capabilities.ts index 343e6d4a..2fc454c8 100644 --- a/app/server/core/capabilities.ts +++ b/app/server/core/capabilities.ts @@ -3,6 +3,7 @@ import { logger } from "../utils/logger"; export type SystemCapabilities = { rclone: boolean; + sysAdmin: boolean; }; let capabilitiesPromise: Promise | null = null; @@ -27,6 +28,7 @@ export async function getCapabilities(): Promise { async function detectCapabilities(): Promise { return { rclone: await detectRclone(), + sysAdmin: await detectSysAdmin(), }; } @@ -38,6 +40,12 @@ async function detectRclone(): Promise { try { await fs.access("/root/.config/rclone"); + // Make sure the folder is not empty + const files = await fs.readdir("/root/.config/rclone"); + if (files.length === 0) { + throw new Error("rclone config directory is empty"); + } + logger.info("rclone capability: enabled"); return true; } catch (_) { @@ -45,3 +53,38 @@ async function detectRclone(): Promise { return false; } } + +async function detectSysAdmin(): Promise { + try { + const procStatus = await fs.readFile("/proc/self/status", "utf-8"); + + const capEffLine = procStatus.split("\n").find((line) => line.startsWith("CapEff:")); + + if (!capEffLine) { + logger.warn("sysAdmin capability: disabled. Could not read CapEff from /proc/self/status"); + return false; + } + + // Extract the hex value (e.g., "00000000a80425fb") + const capEffHex = capEffLine.split(/\s+/)[1]; + + if (!capEffHex) { + logger.warn("sysAdmin capability: disabled. Could not parse CapEff value"); + return false; + } + + // Check if bit 21 (CAP_SYS_ADMIN) is set + const capValue = parseInt(capEffHex, 16) & (1 << 21); + + if (capValue !== 0) { + logger.info("sysAdmin capability: enabled (CAP_SYS_ADMIN detected)"); + return true; + } + + logger.warn("sysAdmin capability: disabled. " + "To enable: add 'cap_add: SYS_ADMIN' in docker-compose.yml"); + return false; + } catch (_error) { + logger.warn("sysAdmin capability: disabled. " + "To enable: add 'cap_add: SYS_ADMIN' in docker-compose.yml"); + return false; + } +} diff --git a/app/server/modules/system/system.dto.ts b/app/server/modules/system/system.dto.ts index b74f622f..03883460 100644 --- a/app/server/modules/system/system.dto.ts +++ b/app/server/modules/system/system.dto.ts @@ -3,6 +3,7 @@ import { describeRoute, resolver } from "hono-openapi"; export const capabilitiesSchema = type({ rclone: "boolean", + sysAdmin: "boolean", }); export const systemInfoResponse = type({