Merge branch 'tvarohohlavy-secrets-management'
This commit is contained in:
commit
f77fd86694
23 changed files with 295 additions and 69 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
*
|
**
|
||||||
|
|
||||||
!turbo.json
|
!turbo.json
|
||||||
!bun.lock
|
!bun.lock
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,3 +12,4 @@ CLAUDE.md
|
||||||
|
|
||||||
mutagen.yml.lock
|
mutagen.yml.lock
|
||||||
notes.md
|
notes.md
|
||||||
|
smb-password.txt
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -148,6 +148,25 @@ Repositories are optimized for storage efficiency and data integrity, leveraging
|
||||||
|
|
||||||
To create a repository, navigate to the "Repositories" section in the web interface and click on "Create repository". Fill in the required details such as repository name, type, and connection settings.
|
To create a repository, navigate to the "Repositories" section in the web interface and click on "Create repository". Fill in the required details such as repository name, type, and connection settings.
|
||||||
|
|
||||||
|
## Secret references (env:// and file://)
|
||||||
|
|
||||||
|
Any field that is normally stored encrypted in Zerobyte (passwords, tokens, access keys, etc.) also accepts secret references.
|
||||||
|
|
||||||
|
- `env://VAR_NAME` reads the value from `process.env.VAR_NAME` inside the Zerobyte container.
|
||||||
|
- `file://secret_name` reads the value from `/run/secrets/secret_name` (Docker secrets).
|
||||||
|
|
||||||
|
If you enter a normal value (not starting with `env://` or `file://`), Zerobyte will encrypt it before storing it in the database (values will look like `encv1:...`).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# SMB volume password from an env var
|
||||||
|
password: env://SMB_PASSWORD
|
||||||
|
|
||||||
|
# S3 secret access key from a Docker secret
|
||||||
|
secretAccessKey: file://s3-secret-access-key
|
||||||
|
```
|
||||||
|
|
||||||
### Using rclone for cloud storage
|
### Using rclone for cloud storage
|
||||||
|
|
||||||
Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage providers including Google Drive, Dropbox, OneDrive, Box, pCloud, Mega, and many more. This gives you the flexibility to store your backups on virtually any cloud storage service.
|
Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage providers including Google Drive, Dropbox, OneDrive, Box, pCloud, Mega, and many more. This gives you the flexibility to store your backups on virtually any cloud storage service.
|
||||||
|
|
|
||||||
|
|
@ -173,3 +173,9 @@ body {
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide built-in password reveal/clear controls (notably in Edge on Windows) */
|
||||||
|
[data-secret-input] input[type="password"]::-ms-reveal,
|
||||||
|
[data-secret-input] input[type="password"]::-ms-clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
59
app/client/components/ui/secret-input.tsx
Normal file
59
app/client/components/ui/secret-input.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
import type * as React from "react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
import { Button } from "./button";
|
||||||
|
import { Input } from "./input";
|
||||||
|
|
||||||
|
export const isStoredSecretValue = (value?: string): boolean => {
|
||||||
|
if (typeof value !== "string" || value.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.startsWith("env://") || value.startsWith("file://") || value.startsWith("encv1:");
|
||||||
|
};
|
||||||
|
|
||||||
|
type SecretInputProps = Omit<React.ComponentProps<typeof Input>, "type">
|
||||||
|
|
||||||
|
export const SecretInput = ({ className, value, ...props }: SecretInputProps) => {
|
||||||
|
const [revealed, setRevealed] = useState(false);
|
||||||
|
|
||||||
|
const showAsPlaintext = useMemo(() => {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isStoredSecretValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const type = useMemo(() => {
|
||||||
|
if (showAsPlaintext) {
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
return revealed ? "text" : "password";
|
||||||
|
}, [showAsPlaintext, revealed]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" data-secret-input>
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
type={type}
|
||||||
|
className={cn(!showAsPlaintext && "pr-10", className)}
|
||||||
|
/>
|
||||||
|
{!showAsPlaintext && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-1 top-1/2 size-7 -translate-y-1/2"
|
||||||
|
onClick={() => setRevealed((v) => !v)}
|
||||||
|
aria-label={revealed ? "Hide secret" : "Show secret"}
|
||||||
|
>
|
||||||
|
{revealed ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "~/client/components/ui/form";
|
} from "~/client/components/ui/form";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
import { notificationConfigSchema } from "~/schemas/notifications";
|
import { notificationConfigSchema } from "~/schemas/notifications";
|
||||||
|
|
@ -213,7 +214,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password (Optional)</FormLabel>
|
<FormLabel>Password (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" placeholder="••••••••" />
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -415,7 +416,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>App Token</FormLabel>
|
<FormLabel>App Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" placeholder="••••••••" />
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Application token from Gotify.</FormDescription>
|
<FormDescription>Application token from Gotify.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -510,7 +511,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password (Optional)</FormLabel>
|
<FormLabel>Password (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" placeholder="••••••••" />
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Password for server authentication, if required.</FormDescription>
|
<FormDescription>Password for server authentication, if required.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -567,7 +568,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>API Token</FormLabel>
|
<FormLabel>API Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" placeholder="••••••••" />
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Application API token from your Pushover application.</FormDescription>
|
<FormDescription>Application API token from your Pushover application.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -627,7 +628,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Bot Token</FormLabel>
|
<FormLabel>Bot Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" placeholder="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
|
<SecretInput {...field} placeholder="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Telegram bot token. Get this from BotFather when you create your bot.
|
Telegram bot token. Get this from BotFather when you create your bot.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "../../../components/ui/form";
|
} from "../../../components/ui/form";
|
||||||
import { Input } from "../../../components/ui/input";
|
import { Input } from "../../../components/ui/input";
|
||||||
|
import { SecretInput } from "../../../components/ui/secret-input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../components/ui/select";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip";
|
||||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||||
|
|
@ -241,7 +242,11 @@ export const CreateRepositoryForm = ({
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Repository Password</FormLabel>
|
<FormLabel>Repository Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="Enter repository password" {...field} />
|
<SecretInput
|
||||||
|
placeholder="Enter repository password"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The password used to encrypt this repository. It will be stored securely.
|
The password used to encrypt this repository. It will be stored securely.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "../../../../components/ui/form";
|
} from "../../../../components/ui/form";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import type { RepositoryFormValues } from "../create-repository-form";
|
import type { RepositoryFormValues } from "../create-repository-form";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -52,7 +53,7 @@ export const AzureRepositoryForm = ({ form }: Props) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Account Key</FormLabel>
|
<FormLabel>Account Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Azure Storage account key for authentication.</FormDescription>
|
<FormDescription>Azure Storage account key for authentication.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "../../../../components/ui/form";
|
} from "../../../../components/ui/form";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import type { RepositoryFormValues } from "../create-repository-form";
|
import type { RepositoryFormValues } from "../create-repository-form";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -68,7 +69,7 @@ export const R2RepositoryForm = ({ form }: Props) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Secret Access Key</FormLabel>
|
<FormLabel>Secret Access Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>R2 API token Secret Access Key (shown once when creating token).</FormDescription>
|
<FormDescription>R2 API token Secret Access Key (shown once when creating token).</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "../../../../components/ui/form";
|
} from "../../../../components/ui/form";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import type { RepositoryFormValues } from "../create-repository-form";
|
import type { RepositoryFormValues } from "../create-repository-form";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -66,7 +67,7 @@ export const RestRepositoryForm = ({ form }: Props) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password (Optional)</FormLabel>
|
<FormLabel>Password (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Password for REST server authentication.</FormDescription>
|
<FormDescription>Password for REST server authentication.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "../../../../components/ui/form";
|
} from "../../../../components/ui/form";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import type { RepositoryFormValues } from "../create-repository-form";
|
import type { RepositoryFormValues } from "../create-repository-form";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -66,7 +67,7 @@ export const S3RepositoryForm = ({ form }: Props) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Secret Access Key</FormLabel>
|
<FormLabel>Secret Access Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>S3 secret access key for authentication.</FormDescription>
|
<FormDescription>S3 secret access key for authentication.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "../../../../components/ui/form";
|
} from "../../../../components/ui/form";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -67,7 +68,7 @@ export const SMBForm = ({ form }: Props) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" value={field.value} onChange={field.onChange} />
|
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Password for SMB authentication.</FormDescription>
|
<FormDescription>Password for SMB authentication.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "../../../../components/ui/form";
|
} from "../../../../components/ui/form";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
form: UseFormReturn<FormValues>;
|
form: UseFormReturn<FormValues>;
|
||||||
|
|
@ -66,7 +67,7 @@ export const WebDAVForm = ({ form }: Props) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password (Optional)</FormLabel>
|
<FormLabel>Password (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Password for WebDAV authentication (optional).</FormDescription>
|
<FormDescription>Password for WebDAV authentication (optional).</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
await fs.mkdir(path, { recursive: true });
|
await fs.mkdir(path, { recursive: true });
|
||||||
|
|
||||||
const password = await cryptoUtils.decrypt(config.password);
|
const password = await cryptoUtils.resolveSecret(config.password);
|
||||||
|
|
||||||
const source = `//${config.server}/${config.share}`;
|
const source = `//${config.server}/${config.share}`;
|
||||||
const options = [
|
const options = [
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||||
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
||||||
|
|
||||||
if (config.username && config.password) {
|
if (config.username && config.password) {
|
||||||
const password = await cryptoUtils.decrypt(config.password);
|
const password = await cryptoUtils.resolveSecret(config.password);
|
||||||
const secretsFile = "/etc/davfs2/secrets";
|
const secretsFile = "/etc/davfs2/secrets";
|
||||||
const secretsContent = `${source} ${config.username} ${password}\n`;
|
const secretsContent = `${source} ${config.username} ${password}\n`;
|
||||||
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
|
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
|
||||||
|
|
|
||||||
|
|
@ -38,42 +38,42 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
||||||
case "email":
|
case "email":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
password: config.password ? await cryptoUtils.encrypt(config.password) : undefined,
|
password: config.password ? await cryptoUtils.sealSecret(config.password) : undefined,
|
||||||
};
|
};
|
||||||
case "slack":
|
case "slack":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
webhookUrl: await cryptoUtils.encrypt(config.webhookUrl),
|
webhookUrl: await cryptoUtils.sealSecret(config.webhookUrl),
|
||||||
};
|
};
|
||||||
case "discord":
|
case "discord":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
webhookUrl: await cryptoUtils.encrypt(config.webhookUrl),
|
webhookUrl: await cryptoUtils.sealSecret(config.webhookUrl),
|
||||||
};
|
};
|
||||||
case "gotify":
|
case "gotify":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
token: await cryptoUtils.encrypt(config.token),
|
token: await cryptoUtils.sealSecret(config.token),
|
||||||
};
|
};
|
||||||
case "ntfy":
|
case "ntfy":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
password: config.password ? await cryptoUtils.encrypt(config.password) : undefined,
|
password: config.password ? await cryptoUtils.sealSecret(config.password) : undefined,
|
||||||
};
|
};
|
||||||
case "pushover":
|
case "pushover":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
apiToken: await cryptoUtils.encrypt(config.apiToken),
|
apiToken: await cryptoUtils.sealSecret(config.apiToken),
|
||||||
};
|
};
|
||||||
case "telegram":
|
case "telegram":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
botToken: await cryptoUtils.encrypt(config.botToken),
|
botToken: await cryptoUtils.sealSecret(config.botToken),
|
||||||
};
|
};
|
||||||
case "custom":
|
case "custom":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
shoutrrrUrl: await cryptoUtils.encrypt(config.shoutrrrUrl),
|
shoutrrrUrl: await cryptoUtils.sealSecret(config.shoutrrrUrl),
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return config;
|
return config;
|
||||||
|
|
@ -85,42 +85,42 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
||||||
case "email":
|
case "email":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
password: config.password ? await cryptoUtils.decrypt(config.password) : undefined,
|
password: config.password ? await cryptoUtils.resolveSecret(config.password) : undefined,
|
||||||
};
|
};
|
||||||
case "slack":
|
case "slack":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
webhookUrl: await cryptoUtils.decrypt(config.webhookUrl),
|
webhookUrl: await cryptoUtils.resolveSecret(config.webhookUrl),
|
||||||
};
|
};
|
||||||
case "discord":
|
case "discord":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
webhookUrl: await cryptoUtils.decrypt(config.webhookUrl),
|
webhookUrl: await cryptoUtils.resolveSecret(config.webhookUrl),
|
||||||
};
|
};
|
||||||
case "gotify":
|
case "gotify":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
token: await cryptoUtils.decrypt(config.token),
|
token: await cryptoUtils.resolveSecret(config.token),
|
||||||
};
|
};
|
||||||
case "ntfy":
|
case "ntfy":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
password: config.password ? await cryptoUtils.decrypt(config.password) : undefined,
|
password: config.password ? await cryptoUtils.resolveSecret(config.password) : undefined,
|
||||||
};
|
};
|
||||||
case "pushover":
|
case "pushover":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
apiToken: await cryptoUtils.decrypt(config.apiToken),
|
apiToken: await cryptoUtils.resolveSecret(config.apiToken),
|
||||||
};
|
};
|
||||||
case "telegram":
|
case "telegram":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
botToken: await cryptoUtils.decrypt(config.botToken),
|
botToken: await cryptoUtils.resolveSecret(config.botToken),
|
||||||
};
|
};
|
||||||
case "custom":
|
case "custom":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
shoutrrrUrl: await cryptoUtils.decrypt(config.shoutrrrUrl),
|
shoutrrrUrl: await cryptoUtils.resolveSecret(config.shoutrrrUrl),
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return config;
|
return config;
|
||||||
|
|
|
||||||
|
|
@ -20,31 +20,31 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
|
||||||
const encryptedConfig: Record<string, string | boolean | number> = { ...config };
|
const encryptedConfig: Record<string, string | boolean | number> = { ...config };
|
||||||
|
|
||||||
if (config.customPassword) {
|
if (config.customPassword) {
|
||||||
encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
|
encryptedConfig.customPassword = await cryptoUtils.sealSecret(config.customPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (config.backend) {
|
switch (config.backend) {
|
||||||
case "s3":
|
case "s3":
|
||||||
case "r2":
|
case "r2":
|
||||||
encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
|
encryptedConfig.accessKeyId = await cryptoUtils.sealSecret(config.accessKeyId);
|
||||||
encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
|
encryptedConfig.secretAccessKey = await cryptoUtils.sealSecret(config.secretAccessKey);
|
||||||
break;
|
break;
|
||||||
case "gcs":
|
case "gcs":
|
||||||
encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
|
encryptedConfig.credentialsJson = await cryptoUtils.sealSecret(config.credentialsJson);
|
||||||
break;
|
break;
|
||||||
case "azure":
|
case "azure":
|
||||||
encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
|
encryptedConfig.accountKey = await cryptoUtils.sealSecret(config.accountKey);
|
||||||
break;
|
break;
|
||||||
case "rest":
|
case "rest":
|
||||||
if (config.username) {
|
if (config.username) {
|
||||||
encryptedConfig.username = await cryptoUtils.encrypt(config.username);
|
encryptedConfig.username = await cryptoUtils.sealSecret(config.username);
|
||||||
}
|
}
|
||||||
if (config.password) {
|
if (config.password) {
|
||||||
encryptedConfig.password = await cryptoUtils.encrypt(config.password);
|
encryptedConfig.password = await cryptoUtils.sealSecret(config.password);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "sftp":
|
case "sftp":
|
||||||
encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
|
encryptedConfig.privateKey = await cryptoUtils.sealSecret(config.privateKey);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,12 @@ async function encryptSensitiveFields(config: BackendConfig): Promise<BackendCon
|
||||||
case "smb":
|
case "smb":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
password: await cryptoUtils.encrypt(config.password),
|
password: await cryptoUtils.sealSecret(config.password),
|
||||||
};
|
};
|
||||||
case "webdav":
|
case "webdav":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
password: config.password ? await cryptoUtils.encrypt(config.password) : undefined,
|
password: config.password ? await cryptoUtils.sealSecret(config.password) : undefined,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return config;
|
return config;
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,11 @@ export const hasCompatibleCredentials = async (
|
||||||
(config1.backend === "s3" || config1.backend === "r2") &&
|
(config1.backend === "s3" || config1.backend === "r2") &&
|
||||||
(config2.backend === "s3" || config2.backend === "r2")
|
(config2.backend === "s3" || config2.backend === "r2")
|
||||||
) {
|
) {
|
||||||
const accessKey1 = await cryptoUtils.decrypt(config1.accessKeyId);
|
const accessKey1 = await cryptoUtils.resolveSecret(config1.accessKeyId);
|
||||||
const secretKey1 = await cryptoUtils.decrypt(config1.secretAccessKey);
|
const secretKey1 = await cryptoUtils.resolveSecret(config1.secretAccessKey);
|
||||||
|
|
||||||
const accessKey2 = await cryptoUtils.decrypt(config2.accessKeyId);
|
const accessKey2 = await cryptoUtils.resolveSecret(config2.accessKeyId);
|
||||||
const secretKey2 = await cryptoUtils.decrypt(config2.secretAccessKey);
|
const secretKey2 = await cryptoUtils.resolveSecret(config2.secretAccessKey);
|
||||||
|
|
||||||
return accessKey1 === accessKey2 && secretKey1 === secretKey2;
|
return accessKey1 === accessKey2 && secretKey1 === secretKey2;
|
||||||
}
|
}
|
||||||
|
|
@ -53,8 +53,8 @@ export const hasCompatibleCredentials = async (
|
||||||
}
|
}
|
||||||
case "gcs": {
|
case "gcs": {
|
||||||
if (config1.backend === "gcs" && config2.backend === "gcs") {
|
if (config1.backend === "gcs" && config2.backend === "gcs") {
|
||||||
const credentials1 = await cryptoUtils.decrypt(config1.credentialsJson);
|
const credentials1 = await cryptoUtils.resolveSecret(config1.credentialsJson);
|
||||||
const credentials2 = await cryptoUtils.decrypt(config2.credentialsJson);
|
const credentials2 = await cryptoUtils.resolveSecret(config2.credentialsJson);
|
||||||
|
|
||||||
return credentials1 === credentials2 && config1.projectId === config2.projectId;
|
return credentials1 === credentials2 && config1.projectId === config2.projectId;
|
||||||
}
|
}
|
||||||
|
|
@ -62,8 +62,8 @@ export const hasCompatibleCredentials = async (
|
||||||
}
|
}
|
||||||
case "azure": {
|
case "azure": {
|
||||||
if (config1.backend === "azure" && config2.backend === "azure") {
|
if (config1.backend === "azure" && config2.backend === "azure") {
|
||||||
const config1Accountkey = await cryptoUtils.decrypt(config1.accountKey);
|
const config1Accountkey = await cryptoUtils.resolveSecret(config1.accountKey);
|
||||||
const config2Accountkey = await cryptoUtils.decrypt(config2.accountKey);
|
const config2Accountkey = await cryptoUtils.resolveSecret(config2.accountKey);
|
||||||
|
|
||||||
return config1.accountName === config2.accountName && config1Accountkey === config2Accountkey;
|
return config1.accountName === config2.accountName && config1Accountkey === config2Accountkey;
|
||||||
}
|
}
|
||||||
|
|
@ -75,10 +75,10 @@ export const hasCompatibleCredentials = async (
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config1Username = await cryptoUtils.decrypt(config1.username || "");
|
const config1Username = await cryptoUtils.resolveSecret(config1.username || "");
|
||||||
const config1Password = await cryptoUtils.decrypt(config1.password || "");
|
const config1Password = await cryptoUtils.resolveSecret(config1.password || "");
|
||||||
const config2Username = await cryptoUtils.decrypt(config2.username || "");
|
const config2Username = await cryptoUtils.resolveSecret(config2.username || "");
|
||||||
const config2Password = await cryptoUtils.decrypt(config2.password || "");
|
const config2Password = await cryptoUtils.resolveSecret(config2.password || "");
|
||||||
|
|
||||||
return config1Username === config2Username && config1Password === config2Password;
|
return config1Username === config2Username && config1Password === config2Password;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
import { RESTIC_PASS_FILE } from "../core/constants";
|
import { RESTIC_PASS_FILE } from "../core/constants";
|
||||||
|
import { isNodeJSErrnoException } from "./fs";
|
||||||
|
|
||||||
const algorithm = "aes-256-gcm" as const;
|
const algorithm = "aes-256-gcm" as const;
|
||||||
const keyLength = 32;
|
const keyLength = 32;
|
||||||
const encryptionPrefix = "encv1";
|
const encryptionPrefix = "encv1:";
|
||||||
|
|
||||||
|
const envSecretPrefix = "env://";
|
||||||
|
const fileSecretPrefix = "file://";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given string is encrypted by looking for the encryption prefix.
|
* Checks if a given string is encrypted by looking for the encryption prefix.
|
||||||
|
|
@ -12,6 +18,68 @@ const isEncrypted = (val?: string): boolean => {
|
||||||
return typeof val === "string" && val.startsWith(encryptionPrefix);
|
return typeof val === "string" && val.startsWith(encryptionPrefix);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string looks like a supported secret reference.
|
||||||
|
* - env://VAR_NAME -> reads process.env.VAR_NAME
|
||||||
|
* - file://name -> reads a file /run/secrets/name
|
||||||
|
*/
|
||||||
|
const isSecretReference = (val?: string): boolean => {
|
||||||
|
return typeof val === "string" && (val.startsWith(envSecretPrefix) || val.startsWith(fileSecretPrefix));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves an environment variable secret reference.
|
||||||
|
*/
|
||||||
|
const resolveEnvSecret = (ref: string): string => {
|
||||||
|
const name = ref.slice(envSecretPrefix.length);
|
||||||
|
if (!name) {
|
||||||
|
throw new Error("env:// reference is missing variable name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = process.env[name];
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`Environment variable not set: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a file-based secret reference.
|
||||||
|
* Reads the secret from /run/secrets/{name}
|
||||||
|
*/
|
||||||
|
const resolveFileSecret = async (ref: string): Promise<string> => {
|
||||||
|
const secretName = ref.slice(fileSecretPrefix.length);
|
||||||
|
if (!secretName) {
|
||||||
|
throw new Error("file:// reference is missing secret name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedName = secretName.replace(/^\/+/, "");
|
||||||
|
if (!normalizedName) {
|
||||||
|
throw new Error("file:// reference is missing secret name");
|
||||||
|
}
|
||||||
|
if (normalizedName.includes("\0") || normalizedName.includes("/") || normalizedName.includes("\\")) {
|
||||||
|
throw new Error("Invalid secret reference: secret name must be a single path segment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = path.join("/run/secrets", normalizedName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(resolvedPath, "utf-8");
|
||||||
|
return content.trimEnd();
|
||||||
|
} catch (error) {
|
||||||
|
if (isNodeJSErrnoException(error)) {
|
||||||
|
if (error.code === "ENOENT") {
|
||||||
|
throw new Error(`Secret file not found: ${resolvedPath}`);
|
||||||
|
}
|
||||||
|
if (error.code === "EACCES") {
|
||||||
|
throw new Error(`Permission denied reading secret file: ${resolvedPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to read secret file ${resolvedPath}: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a string, encrypts it using a randomly generated salt.
|
* Given a string, encrypts it using a randomly generated salt.
|
||||||
* Returns the input unchanged if it's empty or already encrypted.
|
* Returns the input unchanged if it's empty or already encrypted.
|
||||||
|
|
@ -35,7 +103,7 @@ const encrypt = async (data: string) => {
|
||||||
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||||
|
|
||||||
const tag = cipher.getAuthTag();
|
const tag = cipher.getAuthTag();
|
||||||
return `${encryptionPrefix}:${salt.toString("hex")}:${iv.toString("hex")}:${encrypted.toString("hex")}:${tag.toString("hex")}`;
|
return `${encryptionPrefix}${salt.toString("hex")}:${iv.toString("hex")}:${encrypted.toString("hex")}:${tag.toString("hex")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -68,8 +136,54 @@ const decrypt = async (encryptedData: string) => {
|
||||||
return decrypted.toString();
|
return decrypted.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cryptoUtils = {
|
/**
|
||||||
encrypt,
|
* Resolves secret references and encrypted database values.
|
||||||
decrypt,
|
*
|
||||||
isEncrypted,
|
* - encv1:... -> decrypt
|
||||||
|
* - env://VAR -> read process.env.VAR
|
||||||
|
* - file://name -> read /run/secrets/name
|
||||||
|
* - otherwise returns value unchanged
|
||||||
|
*/
|
||||||
|
const resolveSecret = async (value: string): Promise<string> => {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEncrypted(value)) {
|
||||||
|
return decrypt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith(envSecretPrefix)) {
|
||||||
|
return resolveEnvSecret(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith(fileSecretPrefix)) {
|
||||||
|
return resolveFileSecret(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a secret value for storage.
|
||||||
|
*
|
||||||
|
* - env://... and file://... are stored as-is (references)
|
||||||
|
* - encv1:... is stored as-is (already encrypted)
|
||||||
|
* - otherwise encrypt before storing
|
||||||
|
*/
|
||||||
|
const sealSecret = async (value: string): Promise<string> => {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEncrypted(value) || isSecretReference(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return encrypt(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cryptoUtils = {
|
||||||
|
resolveSecret,
|
||||||
|
sealSecret,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
8
app/server/utils/fs.ts
Normal file
8
app/server/utils/fs.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const isNodeJSErrnoException = (error: unknown): error is NodeJS.ErrnoException => {
|
||||||
|
return (
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"code" in error &&
|
||||||
|
typeof (error as NodeJS.ErrnoException).code === "string"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -106,7 +106,7 @@ const buildEnv = async (config: RepositoryConfig) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.isExistingRepository && config.customPassword) {
|
if (config.isExistingRepository && config.customPassword) {
|
||||||
const decryptedPassword = await cryptoUtils.decrypt(config.customPassword);
|
const decryptedPassword = await cryptoUtils.resolveSecret(config.customPassword);
|
||||||
const passwordFilePath = path.join("/tmp", `zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
|
const passwordFilePath = path.join("/tmp", `zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
|
||||||
|
|
||||||
await fs.writeFile(passwordFilePath, decryptedPassword, { mode: 0o600 });
|
await fs.writeFile(passwordFilePath, decryptedPassword, { mode: 0o600 });
|
||||||
|
|
@ -117,17 +117,17 @@ const buildEnv = async (config: RepositoryConfig) => {
|
||||||
|
|
||||||
switch (config.backend) {
|
switch (config.backend) {
|
||||||
case "s3":
|
case "s3":
|
||||||
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
|
env.AWS_ACCESS_KEY_ID = await cryptoUtils.resolveSecret(config.accessKeyId);
|
||||||
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
|
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.resolveSecret(config.secretAccessKey);
|
||||||
break;
|
break;
|
||||||
case "r2":
|
case "r2":
|
||||||
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
|
env.AWS_ACCESS_KEY_ID = await cryptoUtils.resolveSecret(config.accessKeyId);
|
||||||
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
|
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.resolveSecret(config.secretAccessKey);
|
||||||
env.AWS_REGION = "auto";
|
env.AWS_REGION = "auto";
|
||||||
env.AWS_S3_FORCE_PATH_STYLE = "true";
|
env.AWS_S3_FORCE_PATH_STYLE = "true";
|
||||||
break;
|
break;
|
||||||
case "gcs": {
|
case "gcs": {
|
||||||
const decryptedCredentials = await cryptoUtils.decrypt(config.credentialsJson);
|
const decryptedCredentials = await cryptoUtils.resolveSecret(config.credentialsJson);
|
||||||
const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`);
|
const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`);
|
||||||
await fs.writeFile(credentialsPath, decryptedCredentials, { mode: 0o600 });
|
await fs.writeFile(credentialsPath, decryptedCredentials, { mode: 0o600 });
|
||||||
env.GOOGLE_PROJECT_ID = config.projectId;
|
env.GOOGLE_PROJECT_ID = config.projectId;
|
||||||
|
|
@ -136,7 +136,7 @@ const buildEnv = async (config: RepositoryConfig) => {
|
||||||
}
|
}
|
||||||
case "azure": {
|
case "azure": {
|
||||||
env.AZURE_ACCOUNT_NAME = config.accountName;
|
env.AZURE_ACCOUNT_NAME = config.accountName;
|
||||||
env.AZURE_ACCOUNT_KEY = await cryptoUtils.decrypt(config.accountKey);
|
env.AZURE_ACCOUNT_KEY = await cryptoUtils.resolveSecret(config.accountKey);
|
||||||
if (config.endpointSuffix) {
|
if (config.endpointSuffix) {
|
||||||
env.AZURE_ENDPOINT_SUFFIX = config.endpointSuffix;
|
env.AZURE_ENDPOINT_SUFFIX = config.endpointSuffix;
|
||||||
}
|
}
|
||||||
|
|
@ -144,15 +144,15 @@ const buildEnv = async (config: RepositoryConfig) => {
|
||||||
}
|
}
|
||||||
case "rest": {
|
case "rest": {
|
||||||
if (config.username) {
|
if (config.username) {
|
||||||
env.RESTIC_REST_USERNAME = await cryptoUtils.decrypt(config.username);
|
env.RESTIC_REST_USERNAME = await cryptoUtils.resolveSecret(config.username);
|
||||||
}
|
}
|
||||||
if (config.password) {
|
if (config.password) {
|
||||||
env.RESTIC_REST_PASSWORD = await cryptoUtils.decrypt(config.password);
|
env.RESTIC_REST_PASSWORD = await cryptoUtils.resolveSecret(config.password);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sftp": {
|
case "sftp": {
|
||||||
const decryptedKey = await cryptoUtils.decrypt(config.privateKey);
|
const decryptedKey = await cryptoUtils.resolveSecret(config.privateKey);
|
||||||
const keyPath = path.join("/tmp", `ironmount-ssh-${crypto.randomBytes(8).toString("hex")}`);
|
const keyPath = path.join("/tmp", `ironmount-ssh-${crypto.randomBytes(8).toString("hex")}`);
|
||||||
|
|
||||||
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
|
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ services:
|
||||||
- SYS_ADMIN
|
- SYS_ADMIN
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
|
# - SMB_PASSWORD=secret
|
||||||
ports:
|
ports:
|
||||||
- "4096:4096"
|
- "4096:4096"
|
||||||
|
# secrets:
|
||||||
|
# - smb-password
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||||
|
|
@ -42,3 +45,7 @@ services:
|
||||||
- /run/docker/plugins:/run/docker/plugins
|
- /run/docker/plugins:/run/docker/plugins
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ~/.config/rclone:/root/.config/rclone
|
- ~/.config/rclone:/root/.config/rclone
|
||||||
|
|
||||||
|
# secrets:
|
||||||
|
# smb-password:
|
||||||
|
# file: ./smb-password.txt
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue