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
This commit is contained in:
Nico 2025-12-14 20:30:33 +01:00 committed by Nicolas Meienberger
parent ebca72f1ee
commit 9e3cc90ba1
6 changed files with 120 additions and 10 deletions

View file

@ -67,6 +67,34 @@ docker compose up -d
Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`. Once the container is running, you can access the web interface at `http://<your-server-ip>: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 ## 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. 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.

View file

@ -2855,6 +2855,7 @@ export type GetSystemInfoResponses = {
200: { 200: {
capabilities: { capabilities: {
rclone: boolean; rclone: boolean;
sysAdmin: boolean;
}; };
}; };
}; };

View file

@ -10,7 +10,7 @@ export function useSystemInfo() {
}); });
return { return {
capabilities: data?.capabilities ?? { rclone: false }, capabilities: data?.capabilities ?? { rclone: false, sysAdmin: false },
isLoading, isLoading,
error, error,
systemInfo: data, systemInfo: data,

View file

@ -64,7 +64,6 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
const { watch, getValues } = form; const { watch, getValues } = form;
const { capabilities } = useSystemInfo(); const { capabilities } = useSystemInfo();
const watchedBackend = watch("backend"); const watchedBackend = watch("backend");
useEffect(() => { useEffect(() => {
@ -142,16 +141,54 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="directory">Directory</SelectItem> <SelectItem value="directory">Directory</SelectItem>
<SelectItem value="nfs">NFS</SelectItem>
<SelectItem value="smb">SMB</SelectItem>
<SelectItem value="webdav">WebDAV</SelectItem>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger asChild>
<SelectItem disabled={!capabilities.rclone} value="rclone"> <div>
rclone (40+ cloud providers) <SelectItem disabled={!capabilities.sysAdmin} value="nfs">
</SelectItem> NFS
</SelectItem>
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className={cn({ hidden: capabilities.rclone })}> <TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
<p>Remote mounts require SYS_ADMIN capability</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div>
<SelectItem disabled={!capabilities.sysAdmin} value="smb">
SMB
</SelectItem>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
<p>Remote mounts require SYS_ADMIN capability</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div>
<SelectItem disabled={!capabilities.sysAdmin} value="webdav">
WebDAV
</SelectItem>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
<p>Remote mounts require SYS_ADMIN capability</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div>
<SelectItem disabled={!capabilities.rclone || !capabilities.sysAdmin} value="rclone">
rclone
</SelectItem>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
<p>Remote mounts require SYS_ADMIN capability</p>
</TooltipContent>
<TooltipContent className={cn({ hidden: !capabilities.sysAdmin || capabilities.rclone })}>
<p>Setup rclone to use this backend</p> <p>Setup rclone to use this backend</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View file

@ -3,6 +3,7 @@ import { logger } from "../utils/logger";
export type SystemCapabilities = { export type SystemCapabilities = {
rclone: boolean; rclone: boolean;
sysAdmin: boolean;
}; };
let capabilitiesPromise: Promise<SystemCapabilities> | null = null; let capabilitiesPromise: Promise<SystemCapabilities> | null = null;
@ -27,6 +28,7 @@ export async function getCapabilities(): Promise<SystemCapabilities> {
async function detectCapabilities(): Promise<SystemCapabilities> { async function detectCapabilities(): Promise<SystemCapabilities> {
return { return {
rclone: await detectRclone(), rclone: await detectRclone(),
sysAdmin: await detectSysAdmin(),
}; };
} }
@ -38,6 +40,12 @@ async function detectRclone(): Promise<boolean> {
try { try {
await fs.access("/root/.config/rclone"); 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"); logger.info("rclone capability: enabled");
return true; return true;
} catch (_) { } catch (_) {
@ -45,3 +53,38 @@ async function detectRclone(): Promise<boolean> {
return false; return false;
} }
} }
async function detectSysAdmin(): Promise<boolean> {
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;
}
}

View file

@ -3,6 +3,7 @@ import { describeRoute, resolver } from "hono-openapi";
export const capabilitiesSchema = type({ export const capabilitiesSchema = type({
rclone: "boolean", rclone: "boolean",
sysAdmin: "boolean",
}); });
export const systemInfoResponse = type({ export const systemInfoResponse = type({