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:
parent
ebca72f1ee
commit
9e3cc90ba1
6 changed files with 120 additions and 10 deletions
28
README.md
28
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -2855,6 +2855,7 @@ export type GetSystemInfoResponses = {
|
||||||
200: {
|
200: {
|
||||||
capabilities: {
|
capabilities: {
|
||||||
rclone: boolean;
|
rclone: boolean;
|
||||||
|
sysAdmin: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue