feat(koi): s3-backed encrypted mount for navidrome
This commit is contained in:
parent
f5acbe6637
commit
f43b079bb2
10 changed files with 318 additions and 11 deletions
|
@ -21,6 +21,7 @@
|
|||
./services/phpfront.nix
|
||||
./services/postgresql.nix
|
||||
./services/landing
|
||||
./services/geesefs.nix
|
||||
|
||||
./containers/torrent.nix
|
||||
./containers/vaultwarden.nix
|
||||
|
@ -28,7 +29,7 @@
|
|||
./containers/verdaccio
|
||||
./containers/sharkey
|
||||
./containers/pds
|
||||
# ./containers/navidrome
|
||||
./containers/navidrome
|
||||
./containers/conduwuit
|
||||
./containers/zond
|
||||
./containers/kanidm
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
{ config, ... }:
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
UID = 1102;
|
||||
|
||||
feishin = pkgs.callPackage ./feishin.nix {};
|
||||
feishinConfig = builtins.replaceStrings [ "\n" ] [ "" ] ''
|
||||
window.SERVER_URL="https://navi.stupid.fish";
|
||||
window.SERVER_NAME="stupid.fish";
|
||||
window.SERVER_TYPE="navidrome";
|
||||
window.SERVER_LOCK=true;
|
||||
'';
|
||||
in {
|
||||
desu.secrets.navidrome-env.owner = "navidrome";
|
||||
|
||||
users.groups.navidrome = {};
|
||||
users.users.navidrome = {
|
||||
isNormalUser = true;
|
||||
uid = UID;
|
||||
extraGroups = [ "geesefs" ];
|
||||
};
|
||||
|
||||
virtualisation.oci-containers.containers.navidrome = {
|
||||
image = "deluan/navidrome:0.52.5@sha256:b154aebe8b33bae82c500ad0a3eb743e31da54c3bfb4e7cc3054b9a919b685c7";
|
||||
image = "deluan/navidrome:0.53.3";
|
||||
volumes = [
|
||||
"${./navidrome.toml}:/navidrome.toml"
|
||||
"/mnt/puffer/Downloads/music:/music:ro"
|
||||
"/mnt/puffer/navidrome:/data"
|
||||
"/mnt/s3-desu-priv-encrypted/music:/music/s3:ro"
|
||||
"/srv/navidrome:/data"
|
||||
];
|
||||
environment = {
|
||||
ND_CONFIGFILE = "/navidrome.toml";
|
||||
|
@ -24,11 +32,15 @@ in {
|
|||
environmentFiles = [
|
||||
config.desu.secrets.navidrome-env.path
|
||||
];
|
||||
user = builtins.toString UID;
|
||||
user = "${builtins.toString UID}:${builtins.toString UID}";
|
||||
extraOptions = [
|
||||
"--group-add=${builtins.toString config.users.groups.geesefs.gid}"
|
||||
];
|
||||
};
|
||||
systemd.services.docker-navidrome.requires = [ "ecryptfs.service" ];
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /mnt/puffer/navidrome 0755 navidrome navidrome -"
|
||||
"d /srv/navidrome 0755 ${builtins.toString UID} ${builtins.toString UID} -"
|
||||
];
|
||||
|
||||
services.nginx.virtualHosts."navi.stupid.fish" = {
|
||||
|
@ -42,5 +54,19 @@ in {
|
|||
proxy_buffering off;
|
||||
'';
|
||||
};
|
||||
|
||||
locations."/feishin/" = {
|
||||
extraConfig = ''
|
||||
alias ${feishin}/;
|
||||
try_files $uri $uri/ /index.html;
|
||||
'';
|
||||
};
|
||||
|
||||
locations."/feishin/settings.js" = {
|
||||
extraConfig = ''
|
||||
add_header 'Content-Type' 'application/javascript';
|
||||
return 200 '${feishinConfig}';
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
32
hosts/koi/containers/navidrome/feishin.nix
Normal file
32
hosts/koi/containers/navidrome/feishin.nix
Normal file
|
@ -0,0 +1,32 @@
|
|||
{ buildNpmPackage, fetchFromGitHub, fetchNpmDeps }:
|
||||
|
||||
buildNpmPackage rec {
|
||||
pname = "feishin";
|
||||
version = "0.12.1";
|
||||
src = fetchFromGitHub {
|
||||
owner = "jeffvli";
|
||||
repo = "feishin";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-UpNtRZhAqRq/sRVkgg/RbLUWNXvHkAyGhu29zWE6Lk0=";
|
||||
};
|
||||
|
||||
npmFlags = [ "--legacy-peer-deps" "--ignore-scripts" ];
|
||||
npmBuildScript = "build:web";
|
||||
makeCacheWritable = true;
|
||||
|
||||
# i have NO idea why this doesnt work but calling it manually works
|
||||
# npmDepsHash = "sha256-0YfydhQZgxjMvZYosuS+rGA+9qzSYTLilQqMqlnR1oQ=";
|
||||
npmDeps = fetchNpmDeps {
|
||||
inherit src;
|
||||
name = "feishin-deps";
|
||||
hash = "sha256-0YfydhQZgxjMvZYosuS+rGA+9qzSYTLilQqMqlnR1oQ=";
|
||||
buildPhase = ''
|
||||
prefetch-npm-deps package-lock.json $out
|
||||
'';
|
||||
};
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r ./release/app/dist/web/* $out
|
||||
'';
|
||||
}
|
|
@ -6,8 +6,8 @@ BaseUrl = 'https://navi.stupid.fish'
|
|||
EnableSharing = true
|
||||
EnableTranscodingConfig = true
|
||||
|
||||
UILoginBackgroundUrl = 'https://upload.wikimedia.org/wikipedia/en/9/9a/Trollface_non-free.png' # idk why lol
|
||||
UIWelcomeMessage = 'mrrp meow!'
|
||||
UILoginBackgroundUrl = 'https://upload.wikimedia.org/wikipedia/en/7/73/Trollface.png' # idk why lol
|
||||
UIWelcomeMessage = 'meow!'
|
||||
|
||||
# values sourced from secret env:
|
||||
# ND_LASTFM_APIKEY=
|
||||
|
|
|
@ -13,6 +13,7 @@ in {
|
|||
users.users.sftpgo = {
|
||||
isNormalUser = true;
|
||||
uid = UID;
|
||||
extraGroups = [ "geesefs" ];
|
||||
};
|
||||
|
||||
virtualisation.oci-containers.containers.sftpgo = {
|
||||
|
@ -21,8 +22,12 @@ in {
|
|||
"/srv/sftpgo/data:/srv/sftpgo"
|
||||
"/srv/sftpgo/config:/var/lib/sftpgo"
|
||||
"/mnt/puffer:/mnt/puffer"
|
||||
"/mnt/s3-desu-priv-encrypted:/mnt/s3-desu-priv-encrypted"
|
||||
];
|
||||
user = "${builtins.toString UID}:${builtins.toString UID}";
|
||||
extraOptions = [
|
||||
"--group-add=${builtins.toString config.users.groups.geesefs.gid}"
|
||||
];
|
||||
user = builtins.toString UID;
|
||||
environment = {
|
||||
SFTPGO_SFTPD__BINDINGS__0__PORT = "22";
|
||||
SFTPGO_WEBDAVD__BINDINGS__0__PORT = "80";
|
||||
|
@ -46,6 +51,7 @@ in {
|
|||
"${builtins.toString WEBDAV_PORT}:80"
|
||||
];
|
||||
};
|
||||
systemd.services.docker-sftpgo.requires = [ "ecryptfs.service" ];
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /srv/sftpgo/data 0700 ${builtins.toString UID} ${builtins.toString UID} -"
|
||||
|
|
51
hosts/koi/services/geesefs.nix
Normal file
51
hosts/koi/services/geesefs.nix
Normal file
|
@ -0,0 +1,51 @@
|
|||
{ config, abs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(abs "services/geesefs.nix")
|
||||
(abs "services/ecryptfs.nix")
|
||||
];
|
||||
|
||||
desu.secrets.geesefs-credentials = {};
|
||||
desu.secrets.desu-priv-passphrase = {};
|
||||
|
||||
users.users.geesefs = {
|
||||
isNormalUser = true;
|
||||
uid = 1117;
|
||||
};
|
||||
users.groups.geesefs = {
|
||||
gid = 1117;
|
||||
};
|
||||
|
||||
services.geesefs = {
|
||||
enable = true;
|
||||
args = [
|
||||
"--endpoint" "https://storage.yandexcloud.net"
|
||||
"--region" "ru-central1"
|
||||
"--shared-config" config.desu.secrets.geesefs-credentials.path
|
||||
"-o" "allow_other"
|
||||
"-o" "rootmode=040771"
|
||||
"--dir-mode" "0770"
|
||||
"--file-mode" "0660"
|
||||
"--uid" "1117"
|
||||
"--gid" "1117"
|
||||
# performance tuning
|
||||
"--memory-limit" "4000"
|
||||
"--max-flushers" "32"
|
||||
"--max-parallel-parts" "32"
|
||||
"--part-sizes" "25"
|
||||
"--enable-patch"
|
||||
];
|
||||
bucket = "desu-priv";
|
||||
mountPoint = "/mnt/s3-desu-priv";
|
||||
};
|
||||
|
||||
services.ecryptfs = {
|
||||
enable = true;
|
||||
cipherDir = "/mnt/s3-desu-priv/encrypted";
|
||||
passphrasePath = config.desu.secrets.desu-priv-passphrase.path;
|
||||
masterKeyPath = "/mnt/s3-desu-priv/encrypted.key";
|
||||
mountPoint = "/mnt/s3-desu-priv-encrypted";
|
||||
};
|
||||
systemd.services.ecryptfs-setup.requires = [ "geesefs.service" ];
|
||||
}
|
6
secrets/desu-priv-passphrase.age
Normal file
6
secrets/desu-priv-passphrase.age
Normal file
|
@ -0,0 +1,6 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 sj88Xw 0sQCy2mBALq5SnVPkTYpbKGohPZhP89Dps/rq50bAWg
|
||||
kSFv9R4knHXKA/0UpCgZaDu58JBUt0yO2nbL/Aey2es
|
||||
--- GYLN4fCb9cDTV+376HQ838V3Hm/OtjOh2AQZx+sOMpI
|
||||
6F=ËÖïJð}…rœÆ5Ú§3
|
||||
#t¤†ÍÐW±f[ ãæ¨Å,Ñ,Ö²¥#™hìvBæ&¸4Z§kž7 ºî¹µ<7F>mZ
|
5
secrets/geesefs-credentials.age
Normal file
5
secrets/geesefs-credentials.age
Normal file
|
@ -0,0 +1,5 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 sj88Xw v96oTxfqbjTfA6mSKLKZmiZgXLTFKdUWC09Fa3iX/Bc
|
||||
BUXqBEcNoKijYgBUm2K0SBlgZ9VNSgkQb8Y6LTyryCE
|
||||
--- Cnivk9SCcqkOkHa1zZQ0cQScCtt6udQjJ+Kt02ekDy4
|
||||
0Žcq¸eCÜÉèU‹»ëŠ2z¯„m<EFBFBD>ñ\7R段Ú,zºÅ›¡wpÛ5>þ{{黆Ö-¨èïZÿa(RÞÝzê±kÎïf¢j<C2A2>9š£qÜsÄDávi‰k<E280B0>mæ½ØP T·Ý:Iñ~Ë’-#Pm%Ó˜~îÛ‰}‚¼Lß8וѥßoP˜“qÑ{KÝšìžææ%š
|
122
services/ecryptfs.nix
Normal file
122
services/ecryptfs.nix
Normal file
|
@ -0,0 +1,122 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
# todo: ideally we should support multiple encrypted directories
|
||||
options.services.ecryptfs = with lib; {
|
||||
enable = mkEnableOption "ecryptfs";
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.ecryptfs;
|
||||
defaultText = "pkgs.ecryptfs";
|
||||
description = "ecryptfs package";
|
||||
};
|
||||
serviceName = mkOption {
|
||||
type = types.str;
|
||||
default = "ecryptfs";
|
||||
description = "ecryptfs service name";
|
||||
};
|
||||
|
||||
cipherDir = mkOption {
|
||||
type = types.str;
|
||||
description = "path to the directory used as the underlying encrypted storage";
|
||||
};
|
||||
|
||||
passphrasePath = mkOption {
|
||||
type = types.str;
|
||||
description = "path to the file containing the passphrase (this can be rotated down the line without needing to re-encrypt the data)";
|
||||
};
|
||||
|
||||
masterKeyPath = mkOption {
|
||||
type = types.str;
|
||||
description = "path to the master key (i.e. the wrapped passphrase file)";
|
||||
};
|
||||
|
||||
mountPoint = mkOption {
|
||||
type = types.str;
|
||||
description = "ecryptfs mount point";
|
||||
};
|
||||
|
||||
encryptFilenames = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "whether to encrypt filenames";
|
||||
};
|
||||
|
||||
encryptionKeySize = mkOption {
|
||||
type = types.int;
|
||||
default = 32;
|
||||
description = "size of the encryption key in bytes";
|
||||
};
|
||||
|
||||
extraOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "additional options to pass to ecryptfs";
|
||||
};
|
||||
};
|
||||
|
||||
config = let
|
||||
cfg = config.services.ecryptfs;
|
||||
|
||||
mountOptions =
|
||||
[
|
||||
"ecryptfs_sig=$sig"
|
||||
"ecryptfs_key_bytes=${toString cfg.encryptionKeySize}"
|
||||
"ecryptfs_cipher=aes"
|
||||
"ecryptfs_unlink_sigs"
|
||||
"no_sig_cache"
|
||||
] ++
|
||||
(lib.optional cfg.encryptFilenames "ecryptfs_fnek_sig=$sig") ++
|
||||
cfg.extraOptions;
|
||||
in {
|
||||
systemd.services.${cfg.serviceName} = {
|
||||
description = "${cfg.serviceName} setup";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [ cfg.package pkgs.keyutils ];
|
||||
serviceConfig = {
|
||||
User = "root";
|
||||
Group = "root";
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStop = "${pkgs.utillinux}/bin/umount ${lib.escapeShellArg cfg.mountPoint}";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
if [ ! -f ${lib.escapeShellArg cfg.masterKeyPath} ]; then
|
||||
echo "master key file ${cfg.masterKeyPath} does not exist, generating..."
|
||||
passphrase=$(${pkgs.coreutils}/bin/head -c 48 /dev/random | base64)
|
||||
wrapping_passphrase=$(cat ${lib.escapeShellArg cfg.passphrasePath})
|
||||
printf "%s\n%s" "$passphrase" "$wrapping_passphrase" | ecryptfs-wrap-passphrase ${lib.escapeShellArg cfg.masterKeyPath} -
|
||||
fi
|
||||
|
||||
if [ ! -d ${lib.escapeShellArg cfg.cipherDir} ]; then
|
||||
echo "ecryptfs: directory ${lib.escapeShellArg cfg.cipherDir} does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d ${lib.escapeShellArg cfg.mountPoint} ]; then
|
||||
mkdir -p ${lib.escapeShellArg cfg.mountPoint}
|
||||
fi
|
||||
|
||||
result=$(cat ${lib.escapeShellArg cfg.passphrasePath} | ecryptfs-insert-wrapped-passphrase-into-keyring ${lib.escapeShellArg cfg.masterKeyPath} -)
|
||||
sig=$(echo "$result" | sed -r 's/^.*sig \[(.*)\] into.*$/\1/')
|
||||
|
||||
if [ -z "$sig" ]; then
|
||||
echo "failed to extract signature"
|
||||
echo "$result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Keyring signature: $sig"
|
||||
|
||||
if ! keyctl show | grep -q "_uid.0"; then
|
||||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=870126
|
||||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=870335
|
||||
keyctl link @u @s
|
||||
fi
|
||||
|
||||
${pkgs.utillinux}/bin/mount -i -t ecryptfs ${lib.escapeShellArg cfg.cipherDir} ${lib.escapeShellArg cfg.mountPoint} -o "${lib.concatStringsSep "," mountOptions}"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
58
services/geesefs.nix
Normal file
58
services/geesefs.nix
Normal file
|
@ -0,0 +1,58 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
options.services.geesefs = with lib; {
|
||||
enable = mkEnableOption "geesefs";
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.geesefs;
|
||||
defaultText = "pkgs.geesefs";
|
||||
description = "geesefs package";
|
||||
};
|
||||
serviceName = mkOption {
|
||||
type = types.str;
|
||||
default = "geesefs";
|
||||
description = "geesefs service name";
|
||||
};
|
||||
args = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "geesefs arguments";
|
||||
};
|
||||
bucket = mkOption {
|
||||
type = types.str;
|
||||
description = "geesefs bucket name";
|
||||
};
|
||||
mountPoint = mkOption {
|
||||
type = types.str;
|
||||
description = "geesefs mount point";
|
||||
};
|
||||
};
|
||||
|
||||
config = let
|
||||
cfg = config.services.geesefs;
|
||||
|
||||
allArgs = cfg.args ++ [
|
||||
"-f" # foreground
|
||||
cfg.bucket
|
||||
cfg.mountPoint
|
||||
];
|
||||
in {
|
||||
systemd.services.${cfg.serviceName} = {
|
||||
description = "${cfg.serviceName} Daemon";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [ pkgs.fuse ];
|
||||
serviceConfig = {
|
||||
User = "root";
|
||||
Group = "root";
|
||||
ExecStart = "${cfg.package}/bin/geesefs ${builtins.concatStringsSep " " (map lib.escapeShellArg allArgs)}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.mountPoint} 0777 root root -"
|
||||
];
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue