feat(koi): s3-backed encrypted mount for navidrome

This commit is contained in:
alina 🌸 2024-11-30 20:11:32 +03:00
parent f5acbe6637
commit f43b079bb2
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
10 changed files with 318 additions and 11 deletions

View file

@ -21,6 +21,7 @@
./services/phpfront.nix ./services/phpfront.nix
./services/postgresql.nix ./services/postgresql.nix
./services/landing ./services/landing
./services/geesefs.nix
./containers/torrent.nix ./containers/torrent.nix
./containers/vaultwarden.nix ./containers/vaultwarden.nix
@ -28,7 +29,7 @@
./containers/verdaccio ./containers/verdaccio
./containers/sharkey ./containers/sharkey
./containers/pds ./containers/pds
# ./containers/navidrome ./containers/navidrome
./containers/conduwuit ./containers/conduwuit
./containers/zond ./containers/zond
./containers/kanidm ./containers/kanidm

View file

@ -1,22 +1,30 @@
{ config, ... }: { config, pkgs, ... }:
let let
UID = 1102; 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 { in {
desu.secrets.navidrome-env.owner = "navidrome"; desu.secrets.navidrome-env.owner = "navidrome";
users.groups.navidrome = {};
users.users.navidrome = { users.users.navidrome = {
isNormalUser = true; isNormalUser = true;
uid = UID; uid = UID;
extraGroups = [ "geesefs" ];
}; };
virtualisation.oci-containers.containers.navidrome = { virtualisation.oci-containers.containers.navidrome = {
image = "deluan/navidrome:0.52.5@sha256:b154aebe8b33bae82c500ad0a3eb743e31da54c3bfb4e7cc3054b9a919b685c7"; image = "deluan/navidrome:0.53.3";
volumes = [ volumes = [
"${./navidrome.toml}:/navidrome.toml" "${./navidrome.toml}:/navidrome.toml"
"/mnt/puffer/Downloads/music:/music:ro" "/mnt/s3-desu-priv-encrypted/music:/music/s3:ro"
"/mnt/puffer/navidrome:/data" "/srv/navidrome:/data"
]; ];
environment = { environment = {
ND_CONFIGFILE = "/navidrome.toml"; ND_CONFIGFILE = "/navidrome.toml";
@ -24,11 +32,15 @@ in {
environmentFiles = [ environmentFiles = [
config.desu.secrets.navidrome-env.path 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 = [ 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" = { services.nginx.virtualHosts."navi.stupid.fish" = {
@ -42,5 +54,19 @@ in {
proxy_buffering off; 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}';
'';
};
}; };
} }

View 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
'';
}

View file

@ -6,8 +6,8 @@ BaseUrl = 'https://navi.stupid.fish'
EnableSharing = true EnableSharing = true
EnableTranscodingConfig = true EnableTranscodingConfig = true
UILoginBackgroundUrl = 'https://upload.wikimedia.org/wikipedia/en/9/9a/Trollface_non-free.png' # idk why lol UILoginBackgroundUrl = 'https://upload.wikimedia.org/wikipedia/en/7/73/Trollface.png' # idk why lol
UIWelcomeMessage = 'mrrp meow!' UIWelcomeMessage = 'meow!'
# values sourced from secret env: # values sourced from secret env:
# ND_LASTFM_APIKEY= # ND_LASTFM_APIKEY=

View file

@ -13,6 +13,7 @@ in {
users.users.sftpgo = { users.users.sftpgo = {
isNormalUser = true; isNormalUser = true;
uid = UID; uid = UID;
extraGroups = [ "geesefs" ];
}; };
virtualisation.oci-containers.containers.sftpgo = { virtualisation.oci-containers.containers.sftpgo = {
@ -21,8 +22,12 @@ in {
"/srv/sftpgo/data:/srv/sftpgo" "/srv/sftpgo/data:/srv/sftpgo"
"/srv/sftpgo/config:/var/lib/sftpgo" "/srv/sftpgo/config:/var/lib/sftpgo"
"/mnt/puffer:/mnt/puffer" "/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 = { environment = {
SFTPGO_SFTPD__BINDINGS__0__PORT = "22"; SFTPGO_SFTPD__BINDINGS__0__PORT = "22";
SFTPGO_WEBDAVD__BINDINGS__0__PORT = "80"; SFTPGO_WEBDAVD__BINDINGS__0__PORT = "80";
@ -46,6 +51,7 @@ in {
"${builtins.toString WEBDAV_PORT}:80" "${builtins.toString WEBDAV_PORT}:80"
]; ];
}; };
systemd.services.docker-sftpgo.requires = [ "ecryptfs.service" ];
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"d /srv/sftpgo/data 0700 ${builtins.toString UID} ${builtins.toString UID} -" "d /srv/sftpgo/data 0700 ${builtins.toString UID} ${builtins.toString UID} -"

View 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" ];
}

View 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

View 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
View 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
View 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 -"
];
};
}