From f43b079bb2f9b1da2e931dc6ce2c1984be7328db Mon Sep 17 00:00:00 2001 From: teidesu Date: Sat, 30 Nov 2024 20:11:32 +0300 Subject: [PATCH] feat(koi): s3-backed encrypted mount for navidrome --- hosts/koi/configuration.nix | 3 +- hosts/koi/containers/navidrome/default.nix | 40 +++++- hosts/koi/containers/navidrome/feishin.nix | 32 +++++ hosts/koi/containers/navidrome/navidrome.toml | 4 +- hosts/koi/containers/sftpgo/default.nix | 8 +- hosts/koi/services/geesefs.nix | 51 ++++++++ secrets/desu-priv-passphrase.age | 6 + secrets/geesefs-credentials.age | 5 + services/ecryptfs.nix | 122 ++++++++++++++++++ services/geesefs.nix | 58 +++++++++ 10 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 hosts/koi/containers/navidrome/feishin.nix create mode 100644 hosts/koi/services/geesefs.nix create mode 100644 secrets/desu-priv-passphrase.age create mode 100644 secrets/geesefs-credentials.age create mode 100644 services/ecryptfs.nix create mode 100644 services/geesefs.nix diff --git a/hosts/koi/configuration.nix b/hosts/koi/configuration.nix index 84885cf..fd7d8b2 100755 --- a/hosts/koi/configuration.nix +++ b/hosts/koi/configuration.nix @@ -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 diff --git a/hosts/koi/containers/navidrome/default.nix b/hosts/koi/containers/navidrome/default.nix index 2e27fc8..d80989d 100644 --- a/hosts/koi/containers/navidrome/default.nix +++ b/hosts/koi/containers/navidrome/default.nix @@ -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}'; + ''; + }; }; } \ No newline at end of file diff --git a/hosts/koi/containers/navidrome/feishin.nix b/hosts/koi/containers/navidrome/feishin.nix new file mode 100644 index 0000000..b494ff2 --- /dev/null +++ b/hosts/koi/containers/navidrome/feishin.nix @@ -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 + ''; +} \ No newline at end of file diff --git a/hosts/koi/containers/navidrome/navidrome.toml b/hosts/koi/containers/navidrome/navidrome.toml index 98ebd9f..3d7f80d 100644 --- a/hosts/koi/containers/navidrome/navidrome.toml +++ b/hosts/koi/containers/navidrome/navidrome.toml @@ -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= diff --git a/hosts/koi/containers/sftpgo/default.nix b/hosts/koi/containers/sftpgo/default.nix index 474b2f6..16c3c1f 100644 --- a/hosts/koi/containers/sftpgo/default.nix +++ b/hosts/koi/containers/sftpgo/default.nix @@ -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} -" diff --git a/hosts/koi/services/geesefs.nix b/hosts/koi/services/geesefs.nix new file mode 100644 index 0000000..5877003 --- /dev/null +++ b/hosts/koi/services/geesefs.nix @@ -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" ]; +} \ No newline at end of file diff --git a/secrets/desu-priv-passphrase.age b/secrets/desu-priv-passphrase.age new file mode 100644 index 0000000..d00f279 --- /dev/null +++ b/secrets/desu-priv-passphrase.age @@ -0,0 +1,6 @@ +age-encryption.org/v1 +-> ssh-ed25519 sj88Xw 0sQCy2mBALq5SnVPkTYpbKGohPZhP89Dps/rq50bAWg +kSFv9R4knHXKA/0UpCgZaDu58JBUt0yO2nbL/Aey2es +--- GYLN4fCb9cDTV+376HQ838V3Hm/OtjOh2AQZx+sOMpI +6F=J}r5ڧ3 +#tWf[ ,,ֲ#hvB&4Zk7mZ \ No newline at end of file diff --git a/secrets/geesefs-credentials.age b/secrets/geesefs-credentials.age new file mode 100644 index 0000000..90678e6 --- /dev/null +++ b/secrets/geesefs-credentials.age @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> ssh-ed25519 sj88Xw v96oTxfqbjTfA6mSKLKZmiZgXLTFKdUWC09Fa3iX/Bc +BUXqBEcNoKijYgBUm2K0SBlgZ9VNSgkQb8Y6LTyryCE +--- Cnivk9SCcqkOkHa1zZQ0cQScCtt6udQjJ+Kt02ekDy4 +0cqeCU2zm\7R段,zśwp5>{{-Za(Rzkfj9qs Dvikm P T:I~˒-#Pm%Ә~ۉ}L8ѥoPq{K% \ No newline at end of file diff --git a/services/ecryptfs.nix b/services/ecryptfs.nix new file mode 100644 index 0000000..e6ac4f1 --- /dev/null +++ b/services/ecryptfs.nix @@ -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}" + ''; + }; + }; +} \ No newline at end of file diff --git a/services/geesefs.nix b/services/geesefs.nix new file mode 100644 index 0000000..af90223 --- /dev/null +++ b/services/geesefs.nix @@ -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 -" + ]; + }; +} \ No newline at end of file