From 13e407847fe836ee8619bb37569fb84a9cb32a08 Mon Sep 17 00:00:00 2001 From: aloslider <53711835+aloslider@users.noreply.github.com> Date: Tue, 30 Jun 2026 04:26:40 +0300 Subject: [PATCH] Add selfhosted services --- .sops.yaml | 8 - deploy-homelab.sh | 75 ----- flake.lock | 36 +-- home/README.md | 1 - home/benq/README.md | 1 - home/benq/core/podman.nix | 6 - .../core/selfhosted/containers/common.nix | 18 -- .../core/selfhosted/containers/default.nix | 53 ---- .../selfhosted/containers/homarr/default.nix | 40 --- .../selfhosted/containers/netbird/config.nix | 195 ------------ .../selfhosted/containers/netbird/default.nix | 110 ------- .../containers/pocket-id/default.nix | 45 --- .../containers/vaultwarden/default.nix | 39 --- home/benq/core/selfhosted/default.nix | 7 - home/benq/core/selfhosted/network.nix | 11 - home/benq/homelab.nix | 4 +- home/{benq => common}/core/default.nix | 4 +- home/{benq => common}/core/git.nix | 0 home/{benq => common}/core/packages.nix | 0 home/{benq => common}/core/sops.nix | 2 +- hosts/common/core/README.md | 1 - hosts/common/core/default.nix | 1 - hosts/common/core/netbird.nix | 4 - hosts/common/core/nix.nix | 23 +- hosts/common/core/packages.nix | 4 + hosts/common/core/sops.nix | 5 - hosts/common/optional/README.md | 1 - hosts/common/optional/fonts.nix | 4 + hosts/common/optional/selfhosted/default.nix | 288 ++++++++++++++++++ .../optional/selfhosted/gitea/default.nix | 38 +++ .../selfhosted/gitea/gitea/config.nix | 53 ++++ .../selfhosted/gitea/gitea/default.nix | 111 +++++++ .../selfhosted/gitea/postgresql/config.nix | 11 + .../selfhosted/gitea/postgresql/default.nix | 48 +++ .../optional/selfhosted/homarr/config.nix | 30 ++ .../optional/selfhosted/homarr/default.nix | 59 ++++ .../optional/selfhosted/immich/default.nix | 38 +++ .../selfhosted/immich/immich/config.nix | 6 + .../selfhosted/immich/immich/default.nix | 71 +++++ .../optional/selfhosted/immich/ml/config.nix | 6 + .../optional/selfhosted/immich/ml/default.nix | 42 +++ .../selfhosted/immich/postgresql/config.nix | 6 + .../selfhosted/immich/postgresql/default.nix | 55 ++++ .../selfhosted/immich/redis/default.nix | 34 +++ .../optional/selfhosted/jellyfin/config.nix | 12 + .../optional/selfhosted/jellyfin/default.nix | 66 ++++ .../selfhosted/netbird/dashboard/config.nix | 23 ++ .../selfhosted/netbird/dashboard/default.nix | 43 +++ .../optional/selfhosted/netbird/default.nix | 43 +++ .../selfhosted/netbird/proxy/config.nix | 20 ++ .../selfhosted/netbird/proxy/default.nix | 60 ++++ .../selfhosted/netbird/server/config.nix | 43 +++ .../selfhosted/netbird/server/default.nix | 59 ++++ .../optional/selfhosted}/pocket-id/config.nix | 0 .../optional/selfhosted/pocket-id/default.nix | 61 ++++ .../optional/selfhosted/seafile/default.nix | 33 ++ .../selfhosted/seafile/mariadb/config.nix | 12 + .../selfhosted/seafile/mariadb/default.nix | 48 +++ .../seafile/notification/config.nix | 17 ++ .../seafile/notification/default.nix | 73 +++++ .../selfhosted/seafile/redis/config.nix | 10 + .../selfhosted/seafile/redis/default.nix | 37 +++ .../selfhosted/seafile/seadoc/config.nix | 17 ++ .../selfhosted/seafile/seadoc/default.nix | 57 ++++ .../selfhosted/seafile/server/config.nix | 103 +++++++ .../selfhosted/seafile/server/default.nix | 70 +++++ .../optional/selfhosted/traefik/config.nix | 118 +++++++ .../optional/selfhosted/traefik/default.nix | 72 +++++ .../selfhosted}/vaultwarden/config.nix | 19 +- .../selfhosted/vaultwarden/default.nix | 61 ++++ .../optional/selfhosted/zapret2/config.nix | 58 ++++ .../optional/selfhosted/zapret2/default.nix | 67 ++++ .../optional/selfhosted/zerobyte/config.nix | 24 ++ .../optional/selfhosted/zerobyte/default.nix | 61 ++++ hosts/common/users/benq.nix | 33 +- hosts/homelab/configuration.nix | 2 + hosts/homelab/network.nix | 4 +- secrets.yaml | 16 - 78 files changed, 2346 insertions(+), 690 deletions(-) delete mode 100644 .sops.yaml delete mode 100755 deploy-homelab.sh delete mode 100644 home/README.md delete mode 100644 home/benq/README.md delete mode 100644 home/benq/core/podman.nix delete mode 100644 home/benq/core/selfhosted/containers/common.nix delete mode 100644 home/benq/core/selfhosted/containers/default.nix delete mode 100644 home/benq/core/selfhosted/containers/homarr/default.nix delete mode 100644 home/benq/core/selfhosted/containers/netbird/config.nix delete mode 100644 home/benq/core/selfhosted/containers/netbird/default.nix delete mode 100644 home/benq/core/selfhosted/containers/pocket-id/default.nix delete mode 100644 home/benq/core/selfhosted/containers/vaultwarden/default.nix delete mode 100644 home/benq/core/selfhosted/default.nix delete mode 100644 home/benq/core/selfhosted/network.nix rename home/{benq => common}/core/default.nix (59%) rename home/{benq => common}/core/git.nix (100%) rename home/{benq => common}/core/packages.nix (100%) rename home/{benq => common}/core/sops.nix (84%) delete mode 100644 hosts/common/core/README.md delete mode 100644 hosts/common/core/netbird.nix delete mode 100644 hosts/common/optional/README.md create mode 100644 hosts/common/optional/fonts.nix create mode 100644 hosts/common/optional/selfhosted/default.nix create mode 100644 hosts/common/optional/selfhosted/gitea/default.nix create mode 100644 hosts/common/optional/selfhosted/gitea/gitea/config.nix create mode 100644 hosts/common/optional/selfhosted/gitea/gitea/default.nix create mode 100644 hosts/common/optional/selfhosted/gitea/postgresql/config.nix create mode 100644 hosts/common/optional/selfhosted/gitea/postgresql/default.nix create mode 100644 hosts/common/optional/selfhosted/homarr/config.nix create mode 100644 hosts/common/optional/selfhosted/homarr/default.nix create mode 100644 hosts/common/optional/selfhosted/immich/default.nix create mode 100644 hosts/common/optional/selfhosted/immich/immich/config.nix create mode 100644 hosts/common/optional/selfhosted/immich/immich/default.nix create mode 100644 hosts/common/optional/selfhosted/immich/ml/config.nix create mode 100644 hosts/common/optional/selfhosted/immich/ml/default.nix create mode 100644 hosts/common/optional/selfhosted/immich/postgresql/config.nix create mode 100644 hosts/common/optional/selfhosted/immich/postgresql/default.nix create mode 100644 hosts/common/optional/selfhosted/immich/redis/default.nix create mode 100644 hosts/common/optional/selfhosted/jellyfin/config.nix create mode 100644 hosts/common/optional/selfhosted/jellyfin/default.nix create mode 100644 hosts/common/optional/selfhosted/netbird/dashboard/config.nix create mode 100644 hosts/common/optional/selfhosted/netbird/dashboard/default.nix create mode 100644 hosts/common/optional/selfhosted/netbird/default.nix create mode 100644 hosts/common/optional/selfhosted/netbird/proxy/config.nix create mode 100644 hosts/common/optional/selfhosted/netbird/proxy/default.nix create mode 100644 hosts/common/optional/selfhosted/netbird/server/config.nix create mode 100644 hosts/common/optional/selfhosted/netbird/server/default.nix rename {home/benq/core/selfhosted/containers => hosts/common/optional/selfhosted}/pocket-id/config.nix (100%) create mode 100644 hosts/common/optional/selfhosted/pocket-id/default.nix create mode 100644 hosts/common/optional/selfhosted/seafile/default.nix create mode 100644 hosts/common/optional/selfhosted/seafile/mariadb/config.nix create mode 100644 hosts/common/optional/selfhosted/seafile/mariadb/default.nix create mode 100644 hosts/common/optional/selfhosted/seafile/notification/config.nix create mode 100644 hosts/common/optional/selfhosted/seafile/notification/default.nix create mode 100644 hosts/common/optional/selfhosted/seafile/redis/config.nix create mode 100644 hosts/common/optional/selfhosted/seafile/redis/default.nix create mode 100644 hosts/common/optional/selfhosted/seafile/seadoc/config.nix create mode 100644 hosts/common/optional/selfhosted/seafile/seadoc/default.nix create mode 100644 hosts/common/optional/selfhosted/seafile/server/config.nix create mode 100644 hosts/common/optional/selfhosted/seafile/server/default.nix create mode 100644 hosts/common/optional/selfhosted/traefik/config.nix create mode 100644 hosts/common/optional/selfhosted/traefik/default.nix rename {home/benq/core/selfhosted/containers => hosts/common/optional/selfhosted}/vaultwarden/config.nix (51%) create mode 100644 hosts/common/optional/selfhosted/vaultwarden/default.nix create mode 100644 hosts/common/optional/selfhosted/zapret2/config.nix create mode 100644 hosts/common/optional/selfhosted/zapret2/default.nix create mode 100644 hosts/common/optional/selfhosted/zerobyte/config.nix create mode 100644 hosts/common/optional/selfhosted/zerobyte/default.nix delete mode 100644 secrets.yaml diff --git a/.sops.yaml b/.sops.yaml deleted file mode 100644 index e8e1779..0000000 --- a/.sops.yaml +++ /dev/null @@ -1,8 +0,0 @@ -keys: - - &users: - - &benq age1tl5hp6v0t38fl8gxuqds40z503mwz885ev22au4jdy5r24aak3zsza23sx -creation_rules: - - path_regex: secrets.yaml$ - key_group: - - age: - - *benq diff --git a/deploy-homelab.sh b/deploy-homelab.sh deleted file mode 100755 index f6ea2be..0000000 --- a/deploy-homelab.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - - -STATUS=$(bw status | jq -r '.status') -case "$STATUS" in - "unauthenticated") - echo "Unauthenticated. Setting server config..." - - read -rp "Enter vault domain: " VAULT_URL - if [[ -z "$VAULT_URL" ]]; then - echo "Error: url cannot be empty" - exit 1 - fi - bw config server "https://$VAULT_URL" - - export BW_SESSION=$(bw login --raw) - ;; - "locked") - export BW_SESSION=$(bw unlock --raw) - ;; - "unlocked") - echo "Vault is already unlocked" - ;; - *) - echo "Unknown status: $STATUS" - exit 1 - ;; -esac - -echo "Syncing vault..." -bw sync - -FILES_DIR="$HOME/nixos-deploy" -rm -rf "$FILES_DIR" -mkdir -p "$FILES_DIR" - -AGE_DIR="$FILES_DIR/etc/age" -mkdir -p "$AGE_DIR" - -KEY_FILE=$AGE_DIR/key.txt - -echo "Writing age key..." -bw get item "age-keys" \ - | jq -r '.fields[] | select(.name == "private_key") | .value' \ - > "$KEY_FILE" - -chmod 700 "$AGE_DIR" -chmod 600 "$KEY_FILE" - -if [[ ! -s "$KEY_FILE" ]]; then - echo "Failed to create age key" - exit 1 -fi - -echo "Copying sops files..." -SOPS_DIR="$FILES_DIR/etc/sops" -mkdir -p "$SOPS_DIR" -cp .sops.yaml secrets.yaml "$SOPS_DIR" - -echo "Copying nixos config..." -NIXOS_CFG_DIR="$FILES_DIR/etc/nixos-config" -rsync -r --exclude ".sops.yaml" --exclude "secrets.yaml" ./* "$NIXOS_CFG_DIR" - -read -p "Enter IP of the target (user@ip): " TARGET -if [[ -z "$TARGET" ]]; then - echo "Error: TARGET cannot be empty" - exit 1 -fi - -nix run --extra-experimental-features "nix-command flakes" \ - github:nix-community/nixos-anywhere -- \ - --flake .#homelab \ - --extra-files "$FILES_DIR" \ - --target-host "$TARGET" diff --git a/flake.lock b/flake.lock index 739fa87..d7364f5 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1777713215, + "narHash": "sha256-8GzXDOXckDWwST8TY5DbwYFjdvQLlP7K9CLSVx6iTTo=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "63b4e7e6cf75307c1d26ac3762b886b5b0247267", "type": "github" }, "original": { @@ -21,11 +21,11 @@ "dotfiles": { "flake": false, "locked": { - "lastModified": 1778400209, - "narHash": "sha256-mEGIHZaTmQulmWLg3C8r0wdpEllFcKWeAt+3FLa4grM=", + "lastModified": 1778445660, + "narHash": "sha256-eiGc//3MdHJJSlIXcdtjT+Fdgg+xrK8eqYpRBALzFyw=", "owner": "aloslider", "repo": "dotfiles", - "rev": "e01f5de3b98f3c85997db571eeaef855bbda80a3", + "rev": "5cf0727f8aede69b350e46284cf7147d6185ef56", "type": "github" }, "original": { @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1772807318, - "narHash": "sha256-Qjw6ILt8cb2HQQpCmWNLMZZ63wEo1KjTQt+1BcQBr7k=", + "lastModified": 1778365864, + "narHash": "sha256-ImoT/wqmgMImf2dAC+E0MverAdA4QXsedOeES9B7Ezw=", "owner": "nix-community", "repo": "home-manager", - "rev": "daa2c221320809f5514edde74d0ad0193ad54ed8", + "rev": "2f419037039a152448c5f4ae9494154753d1b399", "type": "github" }, "original": { @@ -72,11 +72,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1772542754, - "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "type": "github" }, "original": { @@ -88,11 +88,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1775126147, - "narHash": "sha256-J0dZU4atgcfo4QvM9D92uQ0Oe1eLTxBVXjJzdEMQpD0=", + "lastModified": 1775888245, + "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8d8c1fa5b412c223ffa47410867813290cdedfef", + "rev": "13043924aaa7375ce482ebe2494338e058282925", "type": "github" }, "original": { @@ -116,11 +116,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1775682595, - "narHash": "sha256-0E9PohY/VuESLq0LR4doaH7hTag513sDDW5n5qmHd1Q=", + "lastModified": 1777944972, + "narHash": "sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=", "owner": "mic92", "repo": "sops-nix", - "rev": "d2e8438d5886e92bc5e7c40c035ab6cae0c41f76", + "rev": "c591bf665727040c6cc5cb409079acb22dcce33c", "type": "github" }, "original": { diff --git a/home/README.md b/home/README.md deleted file mode 100644 index a230cf6..0000000 --- a/home/README.md +++ /dev/null @@ -1 +0,0 @@ -Home configs per host. diff --git a/home/benq/README.md b/home/benq/README.md deleted file mode 100644 index 7546dd9..0000000 --- a/home/benq/README.md +++ /dev/null @@ -1 +0,0 @@ -Home config for benq user on homelab. diff --git a/home/benq/core/podman.nix b/home/benq/core/podman.nix deleted file mode 100644 index 426fd91..0000000 --- a/home/benq/core/podman.nix +++ /dev/null @@ -1,6 +0,0 @@ -{ config, ... }: -{ - services.podman = { - enable = true; - }; -} diff --git a/home/benq/core/selfhosted/containers/common.nix b/home/benq/core/selfhosted/containers/common.nix deleted file mode 100644 index 4937bb2..0000000 --- a/home/benq/core/selfhosted/containers/common.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ config, lib, ... }: -with lib; -{ - options.selfhosted.common = { - networkName = mkOption { - type = types.str; - }; - dataDir = mkOption { - type = types.str; - }; - }; - - config = { - sops.secrets = { - "domains/serv" = { }; - }; - }; -} diff --git a/home/benq/core/selfhosted/containers/default.nix b/home/benq/core/selfhosted/containers/default.nix deleted file mode 100644 index 7f89f89..0000000 --- a/home/benq/core/selfhosted/containers/default.nix +++ /dev/null @@ -1,53 +0,0 @@ -{ config, ... }: -let - prefix = "sh-"; - cfg = config.selfhosted; -in -{ - imports = [ - ./common.nix - - ./homarr - ./netbird - ./pocket-id - ./vaultwarden - ]; - - config.selfhosted = { - common = { - networkName = "selfhosted"; - dataDir = "/mnt/data"; - }; - homarr = { - name = "${prefix}homarr"; - subdomain = "dash"; - }; - netbird = - let - nbPrefix = "${prefix}nb-"; - in - { - subdomain = "nb"; - traefik = { - name = "${nbPrefix}traefik"; - }; - dashboard = { - name = "${nbPrefix}dashboard"; - }; - server = { - name = "${nbPrefix}server"; - }; - proxy = { - name = "${nbPrefix}proxy"; - }; - }; - pocket-id = { - name = "${prefix}pocket-id"; - subdomain = "id"; - }; - vaultwarden = { - name = "${prefix}vaultwarden"; - subdomain = "vault"; - }; - }; -} diff --git a/home/benq/core/selfhosted/containers/homarr/default.nix b/home/benq/core/selfhosted/containers/homarr/default.nix deleted file mode 100644 index 8fd6a0a..0000000 --- a/home/benq/core/selfhosted/containers/homarr/default.nix +++ /dev/null @@ -1,40 +0,0 @@ -{ config, lib, ... }: -with lib; -let - common = config.selfhosted.common; - cfg = config.selfhosted.homarr; - hostDataPath = "${common.dataDir}/homarr/data"; -in -{ - options.selfhosted.homarr = { - name = mkOption { - type = types.str; - }; - subdomain = mkOption { - type = types.str; - }; - }; - - config = { - sops.secrets = { - "homarr/enc_key" = { }; - }; - - services.podman.containers."${cfg.name}" = { - image = "ghcr.io/homarr-labs/homarr:v1.60.0"; - network = common.networkName; - environment = { - SECRET_ENCRYPTION_KEY = "${config.sops.secrets."homarr/enc_key".path}"; - }; - volumes = [ - "${hostDataPath}:/appdata" - ]; - autoStart = true; - extraConfig = { - Quadlet = { - DefaultDependencies = false; - }; - }; - }; - }; -} diff --git a/home/benq/core/selfhosted/containers/netbird/config.nix b/home/benq/core/selfhosted/containers/netbird/config.nix deleted file mode 100644 index c0212e8..0000000 --- a/home/benq/core/selfhosted/containers/netbird/config.nix +++ /dev/null @@ -1,195 +0,0 @@ -{ config, ... }: -let - cfg = config.selfhosted.netbird; - poCfg = config.selfhosted.pocket-id; - domain = config.sops.placeholder."domains/serv"; - fullDomain = "${cfg.subdomain}.${domain}"; -in -{ - sops.secrets = { - "netbird/traefik/letsEncryptEmail" = { }; - "netbird/auth/client_id" = { }; - "netbird/auth/client_secret" = { }; - "netbird/server/auth_secret" = { }; - "netbird/server/enc_key" = { }; - "netbird/proxy/token" = { }; - }; - - sops.templates."nb-traefik.yaml".content = '' - log: - level: INFO - - providers: - file: - filename: /etc/traefik/dynamic.yaml - - entryPoints: - web: - address: ":80" - http: - redirections: - entryPoint: - to: websecure - scheme: https - - websecure: - address: ":443" - - certificatesResolvers: - letsencrypt: - acme: - email: ${config.sops.placeholder."netbird/traefik/letsEncryptEmail"} - storage: /letsencrypt/acme.json - tlsChallenge: true - ''; - - sops.templates."nb-traefik-dynamic.yaml".content = - let - nbServerName = "nb-server"; - nbServerH2cName = "nb-server-h2c"; - nbDashboardName = "nb-dashboard"; - in - '' - http: - routers: - nb-relay: - rule: Host(`${fullDomain}`) && PathPrefix(`/relay`) - entryPoints: - - websecure - tls: - certResolver: letsencrypt - service: ${nbServerName} - priority: 110 - - nb-ws-signal: - rule: Host(`${fullDomain}`) && PathPrefix(`/ws-proxy/signal`) - entryPoints: - - websecure - tls: - certResolver: letsencrypt - service: ${nbServerName} - priority: 110 - - nb-ws-management: - rule: Host(`${fullDomain}`) && PathPrefix(`/ws-proxy/management`) - entryPoints: - - websecure - tls: - certResolver: letsencrypt - service: ${nbServerName} - priority: 110 - - nb-grpc-signal: - rule: Host(`${fullDomain}`) && PathPrefix(`/signalexchange.SignalExchange/`) - entryPoints: - - websecure - tls: - certResolver: letsencrypt - service: ${nbServerH2cName} - priority: 100 - - nb-grpc-management: - rule: Host(`${fullDomain}`) && PathPrefix(`/management.ManagementService/`) - entryPoints: - - websecure - tls: - certResolver: letsencrypt - service: ${nbServerH2cName} - priority: 100 - - nb-api: - rule: Host(`${fullDomain}`) && PathPrefix(`/api`) - entryPoints: - - websecure - tls: - certResolver: letsencrypt - service: ${nbServerName} - priority: 90 - - ${nbDashboardName}: - rule: Host(`${fullDomain}`) - entryPoints: - - websecure - tls: - certResolver: letsencrypt - service: ${nbDashboardName} - priority: 10 - - services: - ${nbServerName}: - loadBalancer: - servers: - - url: http://${cfg.server.name}:80 - - ${nbServerH2cName}: - loadBalancer: - servers: - - url: http://${cfg.server.name}:80 - scheme: h2c - - ${nbDashboardName}: - loadBalancer: - servers: - - url: http://${cfg.dashboard.name}:80 - tcp: - serversTransports: - pp-v2: - proxyProtocol: - version: 2 - ''; - - sops.templates."nb-dashboard.env".content = '' - NETBIRD_MGMT_API_ENDPOINT=https://${fullDomain} - NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${fullDomain} - - AUTH_AUDIENCE=${cfg.dashboard.name} - AUTH_CLIENT_ID=${config.sops.placeholder."netbird/auth/client_id"} - AUTH_CLIENT_SECRET=${config.sops.placeholder."netbird/auth/client_secret"} - AUTH_AUTHORITY=https://${poCfg.subdomain}.${domain} - USE_AUTH0=false - AUTH_SUPPORTED_SCOPES=openid profile email groups - AUTH_REDIRECT_URI=/nb-auth - AUTH_SILENT_REDIRECT_URI=/nb-silent-auth - - LETSENCRYPT_DOMAIN=none - LETSENCRYPT_EMAIL=${config.sops.placeholder."netbird/traefik/letsEncryptEmail"} - ''; - - sops.templates."nb-config.yaml".content = '' - server: - listenAddress: ":80" - exposedAddress: "https://${fullDomain}" - stunPorts: - - 3478 - metricsPort: 9090 - healthcheckAddress: ":9000" - logLevel: "info" - logFile: "console" - - authSecret: "${config.sops.placeholder."netbird/server/auth_secret"}" - - auth: - issuer: "https://${poCfg.subdomain}.${domain}" - clientID: "${config.sops.placeholder."netbird/auth/client_id"}" - clientSecret: "${config.sops.placeholder."netbird/auth/client_secret"}" - - reverseProxy: - trustedHTTPProxies: - - "127.0.0.1/32" - - store: - engine: "sqlite" - encryptionKey: "${config.sops.placeholder."netbird/server/enc_key"}" - ''; - - sops.templates."nb-proxy.env".content = '' - NB_PROXY_DOMAIN=${fullDomain} - NB_PROXY_TOKEN=${config.sops.placeholder."netbird/proxy/token"} - NB_PROXY_MANAGEMENT_ADDRESS=http://${cfg.server.name}:80 - NB_PROXY_ALLOW_INSECURE=true - NB_PROXY_ADDRESS=:8443 - NB_PROXY_ACME_CERTIFICATES=true - NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01 - NB_PROXY_CERTIFICATE_DIRECTORY=/certs - ''; -} diff --git a/home/benq/core/selfhosted/containers/netbird/default.nix b/home/benq/core/selfhosted/containers/netbird/default.nix deleted file mode 100644 index 8663fd0..0000000 --- a/home/benq/core/selfhosted/containers/netbird/default.nix +++ /dev/null @@ -1,110 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -with lib; -let - common = config.selfhosted.common; - cfg = config.selfhosted.netbird; - hostDataPath = "${common.dataDir}/netbird"; -in -{ - imports = [ - ./config.nix - ]; - - options.selfhosted.netbird = { - subdomain = mkOption { - type = types.str; - }; - traefik = { - name = mkOption { - type = types.str; - }; - }; - dashboard = { - name = mkOption { - type = types.str; - }; - }; - server = { - name = mkOption { - type = types.str; - }; - }; - proxy = { - name = mkOption { - type = types.str; - }; - }; - }; - - config = { - services.podman.containers.${cfg.traefik.name} = { - image = "traefik:v3.6"; - network = common.networkName; - ports = [ - "0.0.0.0:80:80" - "0.0.0.0:443:443" - ]; - autoStart = true; - volumes = [ - "${hostDataPath}/traefik/letsencrypt:/letsencrypt" - "${config.sops.templates."nb-traefik.yaml".path}:/etc/traefik/config.yaml" - "${config.sops.templates."nb-traefik-dynamic.yaml".path}:/etc/traefik/dynamic.yaml" - ]; - extraConfig = { - Quadlet = { - DefaultDependencies = false; - }; - }; - }; - - services.podman.containers.${cfg.dashboard.name} = { - image = "netbirdio/dashboard:v2.37.1"; - network = common.networkName; - environmentFile = [ "${config.sops.templates."nb-dashboard.env".path}" ]; - autoStart = true; - extraConfig = { - Quadlet = { - DefaultDependencies = false; - }; - }; - }; - - services.podman.containers.${cfg.server.name} = { - image = "netbirdio/netbird-server:0.70.5"; - network = common.networkName; - ports = [ - "0.0.0.0:3478:3478/udp" - ]; - autoStart = true; - volumes = [ - "${hostDataPath}/server/data:/var/lib/netbird" - "${config.sops.templates."nb-config.yaml".path}:/etc/netbird/config.yaml" - ]; - extraConfig = { - Quadlet = { - DefaultDependencies = false; - }; - }; - }; - - services.podman.containers.${cfg.proxy.name} = { - image = "netbirdio/reverse-proxy:0.70.5"; - network = common.networkName; - environmentFile = [ "${config.sops.templates."nb-proxy.env".path}" ]; - autoStart = true; - volumes = [ - "${hostDataPath}/proxy/certs:/certs" - ]; - extraConfig = { - Quadlet = { - DefaultDependencies = false; - }; - }; - }; - }; -} diff --git a/home/benq/core/selfhosted/containers/pocket-id/default.nix b/home/benq/core/selfhosted/containers/pocket-id/default.nix deleted file mode 100644 index f0079f5..0000000 --- a/home/benq/core/selfhosted/containers/pocket-id/default.nix +++ /dev/null @@ -1,45 +0,0 @@ -{ config, lib, ... }: -with lib; -let - common = config.selfhosted.common; - cfg = config.selfhosted.pocket-id; - hostDataPath = "${common.dataDir}/pocket-id/data"; -in -{ - imports = [ - ./config.nix - ]; - - options.selfhosted.pocket-id = { - name = mkOption { - type = types.str; - }; - subdomain = mkOption { - type = types.str; - }; - }; - - config = { - services.podman.containers.${cfg.name} = { - image = "ghcr.io/pocket-id/pocket-id:v2"; - network = common.networkName; - environmentFile = [ "${config.sops.templates."pocket-id.env".path}" ]; - volumes = [ - "${hostDataPath}:/app/data" - ]; - autoStart = true; - extraConfig = { - Quadlet = { - DefaultDependencies = false; - }; - Container = { - HealthCmd = ''"/app/pocket-id" "healthcheck"''; - HealthInterval = "1m30s"; - HealthTimeout = "5s"; - HealthRetries = "2"; - HealthStartPeriod = "10s"; - }; - }; - }; - }; -} diff --git a/home/benq/core/selfhosted/containers/vaultwarden/default.nix b/home/benq/core/selfhosted/containers/vaultwarden/default.nix deleted file mode 100644 index 9ede3cb..0000000 --- a/home/benq/core/selfhosted/containers/vaultwarden/default.nix +++ /dev/null @@ -1,39 +0,0 @@ -{ config, lib, ... }: -with lib; -let - common = config.selfhosted.common; - cfg = config.selfhosted.vaultwarden; - hostDataPath = "${common.dataDir}/vaultwarden/data"; -in -{ - imports = [ - ./config.nix - ]; - - options.selfhosted.vaultwarden = { - enable = mkEnableOption "Enable container"; - name = mkOption { - type = types.str; - }; - subdomain = mkOption { - type = types.str; - }; - }; - - config = { - services.podman.containers.${cfg.name} = { - image = "docker.io/vaultwarden/server:1.36.0"; - network = config.selfhosted.common.networkName; - environmentFile = [ "${config.sops.templates."vaultwarden.env".path}" ]; - volumes = [ - "${hostDataPath}:/data" - ]; - autoStart = true; - extraConfig = { - Quadlet = { - DefaultDependencies = false; - }; - }; - }; - }; -} diff --git a/home/benq/core/selfhosted/default.nix b/home/benq/core/selfhosted/default.nix deleted file mode 100644 index 38b8556..0000000 --- a/home/benq/core/selfhosted/default.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ config, ... }: -{ - imports = [ - ./network.nix - ./containers - ]; -} diff --git a/home/benq/core/selfhosted/network.nix b/home/benq/core/selfhosted/network.nix deleted file mode 100644 index 727bb85..0000000 --- a/home/benq/core/selfhosted/network.nix +++ /dev/null @@ -1,11 +0,0 @@ -{ config, ... }: -let - common = config.selfhosted.common; -in -{ - services.podman = { - networks.${common.networkName} = { - driver = "bridge"; - }; - }; -} diff --git a/home/benq/homelab.nix b/home/benq/homelab.nix index 7c69929..7949329 100644 --- a/home/benq/homelab.nix +++ b/home/benq/homelab.nix @@ -22,9 +22,9 @@ imports = lib.flatten [ (map lib.custom.relativeToRoot ( [ - "home/benq/core" + "home/common/core" ] - ++ (map (f: "home/benq/optional/${f}") [ + ++ (map (f: "home/common/optional/${f}") [ ]) )) diff --git a/home/benq/core/default.nix b/home/common/core/default.nix similarity index 59% rename from home/benq/core/default.nix rename to home/common/core/default.nix index 3951a21..c0e34f4 100644 --- a/home/benq/core/default.nix +++ b/home/common/core/default.nix @@ -3,8 +3,6 @@ imports = [ ./git.nix ./packages.nix - ./podman.nix - ./selfhosted - ./sops.nix + # ./sops.nix ]; } diff --git a/home/benq/core/git.nix b/home/common/core/git.nix similarity index 100% rename from home/benq/core/git.nix rename to home/common/core/git.nix diff --git a/home/benq/core/packages.nix b/home/common/core/packages.nix similarity index 100% rename from home/benq/core/packages.nix rename to home/common/core/packages.nix diff --git a/home/benq/core/sops.nix b/home/common/core/sops.nix similarity index 84% rename from home/benq/core/sops.nix rename to home/common/core/sops.nix index 2833169..06d53a7 100644 --- a/home/benq/core/sops.nix +++ b/home/common/core/sops.nix @@ -6,7 +6,7 @@ sops = { defaultSopsFile = "${dotfiles}/dot_config/sops/secrets.yaml"; - age.keyFile = "${config.xdg.configHome}/age/key.txt"; + age.keyFile = "${config.xdg.configHome}/sops/age/keys.txt"; }; home.file.".sops.yaml".source = "${dotfiles}/dot_sops.yaml"; diff --git a/hosts/common/core/README.md b/hosts/common/core/README.md deleted file mode 100644 index c043474..0000000 --- a/hosts/common/core/README.md +++ /dev/null @@ -1 +0,0 @@ -System config that is required for any host. diff --git a/hosts/common/core/default.nix b/hosts/common/core/default.nix index e4c5490..6ef2351 100644 --- a/hosts/common/core/default.nix +++ b/hosts/common/core/default.nix @@ -3,7 +3,6 @@ imports = [ ./disko.nix ./locale.nix - ./netbird.nix ./nix.nix ./packages.nix ./sops.nix diff --git a/hosts/common/core/netbird.nix b/hosts/common/core/netbird.nix deleted file mode 100644 index f7b3f32..0000000 --- a/hosts/common/core/netbird.nix +++ /dev/null @@ -1,4 +0,0 @@ -{ config, ... }: -{ - services.netbird.enable = true; -} diff --git a/hosts/common/core/nix.nix b/hosts/common/core/nix.nix index a41fd2c..5b75e50 100644 --- a/hosts/common/core/nix.nix +++ b/hosts/common/core/nix.nix @@ -1,14 +1,17 @@ { config, ... }: { - nix.settings.experimental-features = [ - "nix-command" - "flakes" - ]; - - nix.gc = { - automatic = true; - dates = "daily"; - options = "--delete-older-than +5"; + nix = { + gc = { + automatic = true; + dates = "daily"; + options = "--delete-older-than 7d"; + }; + settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + auto-optimise-store = true; + }; }; - nix.settings.auto-optimise-store = true; } diff --git a/hosts/common/core/packages.nix b/hosts/common/core/packages.nix index 4ce2c7a..37c18c3 100644 --- a/hosts/common/core/packages.nix +++ b/hosts/common/core/packages.nix @@ -2,12 +2,16 @@ { environment.systemPackages = with pkgs; [ age + bat bind btop git jq openssl + ripgrep tmux tree + vim + wget ]; } diff --git a/hosts/common/core/sops.nix b/hosts/common/core/sops.nix index 7835f45..ff9c8f1 100644 --- a/hosts/common/core/sops.nix +++ b/hosts/common/core/sops.nix @@ -8,10 +8,5 @@ defaultSopsFile = "/etc/sops/secrets.yaml"; age.keyFile = "/etc/age/key.txt"; validateSopsFiles = false; - secrets = { - benq-password = { - neededForUsers = true; - }; - }; }; } diff --git a/hosts/common/optional/README.md b/hosts/common/optional/README.md deleted file mode 100644 index 6e8d9c3..0000000 --- a/hosts/common/optional/README.md +++ /dev/null @@ -1 +0,0 @@ -Optional config that may or not exist on specific host. diff --git a/hosts/common/optional/fonts.nix b/hosts/common/optional/fonts.nix new file mode 100644 index 0000000..c5558c9 --- /dev/null +++ b/hosts/common/optional/fonts.nix @@ -0,0 +1,4 @@ +{ config, pkgs, ... }: +{ + fonts.fontDir.enable = true; +} diff --git a/hosts/common/optional/selfhosted/default.nix b/hosts/common/optional/selfhosted/default.nix new file mode 100644 index 0000000..a40c30c --- /dev/null +++ b/hosts/common/optional/selfhosted/default.nix @@ -0,0 +1,288 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted; +in +{ + imports = [ + ./gitea + ./homarr + ./immich + ./jellyfin + ./netbird + ./pocket-id + ./seafile + ./traefik + ./vaultwarden + ./zapret2 + ./zerobyte + ]; + + options.selfhosted.common = { + backend = mkOption { + type = types.str; + }; + rootTarget = { + name = mkOption { + type = types.str; + }; + }; + network = { + name = mkOption { + type = types.str; + }; + address = mkOption { + type = types.str; + }; + mask = mkOption { + type = types.str; + }; + gateway = mkOption { + type = types.str; + }; + }; + dataDir = mkOption { + type = types.str; + }; + user = { + name = mkOption { + type = types.str; + }; + uid = mkOption { + type = types.str; + }; + }; + group = { + name = mkOption { + type = types.str; + }; + gid = mkOption { + type = types.str; + }; + }; + }; + + config = { + selfhosted = { + common = { + backend = "docker"; + rootTarget.name = "sh-root"; + network = { + name = "selfhosted"; + address = "172.30.0.0"; + mask = "24"; + gateway = "172.30.0.1"; + }; + dataDir = "/mnt/data"; + user = { + name = config.users.users.benq.name; + uid = "1000"; + }; + group = { + name = "users"; + gid = "1000"; + }; + }; + jellyfin = { + name = "jellyfin"; + subdomain = "media"; + hostHttpPort = 5040; + hostUdpPort = 5041; + }; + gitea = { + targetName = "gitea"; + gitea = { + name = "gitea"; + subdomain = "gitea"; + hostWebPort = 5060; + hostSshPort = 5061; + }; + postgresql = { + name = "gitea-db"; + }; + }; + immich = { + targetName = "immich"; + version = "v2"; + immich = { + name = "immich"; + subdomain = "photos"; + hostPort = 5090; + }; + ml = { + name = "immich-ml"; + }; + redis = { + name = "immich-cache"; + }; + postgresql = { + name = "immich-db"; + db = "immich"; + user = "immich"; + }; + }; + netbird = { + targetName = "netbird"; + subdomain = "nb"; + dashboard = { + name = "nb-dashboard"; + }; + proxy = { + name = "nb-proxy"; + }; + server = { + name = "nb-server"; + }; + }; + homarr = { + name = "homarr"; + subdomain = "dash"; + hostPort = 5030; + }; + pocket-id = { + name = "pocket-id"; + subdomain = "id"; + hostPort = 5020; + }; + seafile = { + targetName = "seafile"; + server = { + name = "sf-server"; + subdomain = "cloud"; + hostHttpPort = 5050; + }; + mariadb = { + name = "sf-db"; + }; + redis = { + name = "sf-cache"; + }; + seadoc = { + name = "sf-seadoc"; + hostHttpPort = 5051; + }; + notification = { + name = "sf-notification"; + hostHttpPort = 5052; + }; + }; + traefik = { + name = "traefik"; + ip = "172.30.0.10"; + }; + vaultwarden = { + name = "vaultwarden"; + subdomain = "vault"; + hostPort = 5010; + }; + zapret2 = { + name = "zapret2"; + socksPort = 6000; + ssPort = 6001; + }; + zerobyte = { + name = "zerobyte"; + subdomain = "backup"; + hostPort = 5070; + }; + }; + + virtualisation = lib.mkMerge [ + { oci-containers.backend = config.selfhosted.common.backend; } + ( + if config.selfhosted.common.backend == "docker" then + { + docker = { + enable = true; + }; + } + else if config.selfhosted.common.backend == "podman" then + { + podman = { + enable = true; + dockerCompat = true; + }; + } + else + abort "virtualisation.backend is not set" + ) + ]; + + services.netbird.clients = { + homelab = { + port = 51820; + openFirewall = true; + openInternalFirewall = true; + login = { + enable = true; + setupKeyFile = config.sops.secrets."netbird/setup_keys/homelab".path; + }; + }; + }; + + sops.secrets = { + "netbird/setup_keys/homelab" = { }; + "netbird/setup_keys/site" = { }; + }; + + sops.templates."netbird-homelab.env".content = '' + NB_MANAGEMENT_URL=https://${cfg.netbird.subdomain}.${config.sops.placeholder."domains/serv"} + ''; + + # Override to pass env file + systemd.services = { + "netbird-homelab" = { + serviceConfig = { + EnvironmentFile = [ config.sops.templates."netbird-homelab.env".path ]; + }; + after = [ "sops-nix.service" ]; + wants = [ "sops-nix.service" ]; + }; + "netbird-homelab-login" = { + after = [ "sops-nix.service" ]; + wants = [ "sops-nix.service" ]; + }; + }; + + systemd.targets."${common.backend}-${common.rootTarget.name}" = { + unitConfig = { + Description = "Root target for homelab stack"; + }; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.services."${common.backend}-network-${common.network.name}" = { + path = [ pkgs.${common.backend} ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStop = "${common.backend} network rm -f ${common.network.name}"; + }; + script = '' + ${common.backend} network inspect ${common.network.name} \ + || ${common.backend} network create ${common.network.name} --driver=bridge --subnet=${common.network.address}/${common.network.mask} --gateway=${common.network.gateway} + ''; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + }; + + networking.firewall.interfaces = lib.mkMerge [ + (lib.mkIf (config.selfhosted.common.backend == "podman") ( + let + matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; + in + { + "${matchAll}" = { + allowedUDPPorts = [ 53 ]; + }; + } + )) + ]; + }; +} diff --git a/hosts/common/optional/selfhosted/gitea/default.nix b/hosts/common/optional/selfhosted/gitea/default.nix new file mode 100644 index 0000000..4333ea5 --- /dev/null +++ b/hosts/common/optional/selfhosted/gitea/default.nix @@ -0,0 +1,38 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.gitea; +in +{ + imports = [ + ./gitea + ./postgresql + ]; + + options.selfhosted.gitea = { + targetName = mkOption { + type = types.str; + }; + }; + + config = { + sops.secrets = { + "gitea/db_password" = { }; + "gitea/client_id" = { }; + "gitea/client_secret" = { }; + }; + + systemd.targets."${common.backend}-${cfg.targetName}" = { + unitConfig = { + Description = "Root target for ${cfg.targetName}"; + }; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/gitea/gitea/config.nix b/hosts/common/optional/selfhosted/gitea/gitea/config.nix new file mode 100644 index 0000000..ff41438 --- /dev/null +++ b/hosts/common/optional/selfhosted/gitea/gitea/config.nix @@ -0,0 +1,53 @@ +{ config, ... }: +let + common = config.selfhosted.common; + cfg = config.selfhosted.gitea; + giteaDomain = "${cfg.gitea.subdomain}.${config.sops.placeholder."domains/serv"}"; +in +{ + sops.templates."gitea.env".content = '' + USER_UID=${common.user.uid} + USER_GID=${common.group.gid} + + GITEA__database__DB_TYPE=postgres + GITEA__database__HOST=${cfg.postgresql.name}:5432 + GITEA__database__NAME=gitea + GITEA__database__USER=gitea + GITEA__database__PASSWD=${config.sops.placeholder."gitea/db_password"} + + TZ=Europe/Moscow + GITEA__time__DEFAULT_UI_LOCATION=Europe/Moscow + + GITEA__server__DOMAIN=${giteaDomain} + GITEA__server__ROOT_URL=https://${giteaDomain}/ + GITEA__server__HTTP_PORT=3000 + GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION=false + GITEA__service__DISABLE_REGISTRATION=true + GITEA__service__SHOW_REGISTRATION_BUTTON=false + GITEA__service__ENABLE_BASIC_AUTHENTICATION=false + + GITEA__service__ENABLE_PASSKEY_AUTHENTICATION=false + GITEA__server__START_SSH_SERVER=true + GITEA__server__SSH_DOMAIN=git.${config.sops.placeholder."domains/serv"} + GITEA__server__SSH_LISTEN_HOST=0.0.0.0 + GITEA__server__SSH_LISTEN_PORT=2222 + GITEA__server__SSH_PORT=2222 + + GITEA__security__INSTALL_LOCK=true + + GITEA__repository__MAX_CREATION_LIMIT=50 + + GITEA__server__ENABLE_PASSWORD_SIGNIN_FORM=false + GITEA__server__ENABLE_BASIC_AUTHENTICATION=false + GITEA__openid__ENABLE_OPENID_SIGNIN=false + GITEA__openid__ENABLE_OPENID_SIGNUP=false + + GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION=true + GITEA__oauth2_client__ACCOUNT_LINKING=auto + GITEA__oauth2_client__USERNAME=preferred_username + + GITEA__session__COOKIE_SECURE=true + GITEA__log__LEVEL=Info + ''; +} + diff --git a/hosts/common/optional/selfhosted/gitea/gitea/default.nix b/hosts/common/optional/selfhosted/gitea/gitea/default.nix new file mode 100644 index 0000000..15d7bf1 --- /dev/null +++ b/hosts/common/optional/selfhosted/gitea/gitea/default.nix @@ -0,0 +1,111 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.gitea; + hostDataPath = "${common.dataDir}/gitea"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.gitea.gitea = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostWebPort = mkOption { + type = types.port; + }; + hostSshPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.gitea.name}" = { + image = "gitea/gitea:latest"; + ports = [ + "0.0.0.0:${toString cfg.gitea.hostWebPort}:3000" + "0.0.0.0:${toString cfg.gitea.hostSshPort}:2222" + ]; + environmentFiles = [ config.sops.templates."gitea.env".path ]; + volumes = [ + "${hostDataPath}/data:/data" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.gitea.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-${cfg.postgresql.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-${cfg.postgresql.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/data + chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}/data + chmod -R 0775 ${hostDataPath}/data + ''; + postStart = + let + engine = + if common.backend == "podman" then + pkgs.podman + else if common.backend == "docker" then + pkgs.docker + else + abort "Unknown backend ${common.backend}"; + in + '' + set -euo pipefail + DOMAIN=$(cat ${config.sops.secrets."domains/serv".path}) + CLIENT_ID=$(cat ${config.sops.secrets."gitea/client_id".path}) + CLIENT_SECRET=$(cat ${config.sops.secrets."gitea/client_secret".path}) + + for i in $(seq 1 30); do + if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.gitea.hostWebPort}/api/v1/version >/dev/null 2>&1; then + break + fi + echo "Waiting for Gitea to be ready... ($i/20)" + sleep 5 + done + + if ${engine}/bin/${common.backend} exec --user ${common.user.uid} ${cfg.gitea.name} \ + gitea admin auth list 2>/dev/null | grep -q 'PocketID'; then + echo "PocketID auth source already registered" + exit 0 + fi + + ${engine}/bin/${common.backend} exec --user ${common.user.uid} ${cfg.gitea.name} \ + gitea admin auth add-oauth \ + --name "PocketID" \ + --provider "openidConnect" \ + --key "$CLIENT_ID" \ + --secret "$CLIENT_SECRET" \ + --auto-discover-url "https://${config.selfhosted.pocket-id.subdomain}.$DOMAIN/.well-known/openid-configuration" \ + --scopes "openid email profile" \ + --skip-local-2fa + + echo "PocketID auth source registered successfully" + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/gitea/postgresql/config.nix b/hosts/common/optional/selfhosted/gitea/postgresql/config.nix new file mode 100644 index 0000000..41e1996 --- /dev/null +++ b/hosts/common/optional/selfhosted/gitea/postgresql/config.nix @@ -0,0 +1,11 @@ +{ config, ... }: +let + cfg = config.selfhosted.gitea; +in +{ + sops.templates."gitea-db.env".content = '' + POSTGRES_DB=gitea + POSTGRES_USER=gitea + POSTGRES_PASSWORD=${config.sops.placeholder."gitea/db_password"} + ''; +} diff --git a/hosts/common/optional/selfhosted/gitea/postgresql/default.nix b/hosts/common/optional/selfhosted/gitea/postgresql/default.nix new file mode 100644 index 0000000..83ba7d0 --- /dev/null +++ b/hosts/common/optional/selfhosted/gitea/postgresql/default.nix @@ -0,0 +1,48 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.gitea; + hostDataPath = "${common.dataDir}/gitea"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.gitea.postgresql = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.postgresql.name}" = { + image = "postgres:16"; + environmentFiles = [ config.sops.templates."gitea-db.env".path ]; + volumes = [ + "${hostDataPath}/dbData:/var/lib/postgresql/data" + ]; + extraOptions = [ + "--network=${common.network.name}" + "--network-alias=${cfg.postgresql.name}" + "--health-cmd=pg_isready -U gitea" + "--health-interval=10s" + "--health-timeout=5s" + "--health-retries=5" + ]; + }; + + systemd.services."${common.backend}-${cfg.postgresql.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/dbData + chown 70:70 ${hostDataPath}/dbData + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/homarr/config.nix b/hosts/common/optional/selfhosted/homarr/config.nix new file mode 100644 index 0000000..ee1c35c --- /dev/null +++ b/hosts/common/optional/selfhosted/homarr/config.nix @@ -0,0 +1,30 @@ +{ config, ... }: +let + cfg = config.selfhosted.homarr; + poCfg = config.selfhosted.pocket-id; + oidpDomain = "https://${poCfg.subdomain}.${config.sops.placeholder."domains/serv"}"; +in +{ + sops.secrets = { + "homarr/enc_key" = { }; + "homarr/client_id" = { }; + "homarr/client_secret" = { }; + "homarr/nextauth_secret" = { }; + }; + + sops.templates."homarr.env" = { + content = '' + SECRET_ENCRYPTION_KEY=${config.sops.placeholder."homarr/enc_key"} + NEXTAUTH_SECRET=${config.sops.placeholder."homarr/nextauth_secret"} + AUTH_PROVIDERS=oidc + AUTH_OIDC_CLIENT_ID=${config.sops.placeholder."homarr/client_id"} + AUTH_OIDC_CLIENT_SECRET=${config.sops.placeholder."homarr/client_secret"} + AUTH_OIDC_ISSUER=${oidpDomain} + AUTH_OIDC_CLIENT_NAME="Pocket ID" + AUTH_OIDC_SCOPE_OVERWRITE=openid email profile groups + AUTH_OIDC_GROUPS_ATTRIBUTE=groups + AUTH_LOGOUT_REDIRECT_URL=${oidpDomain} + AUTH_OIDC_AUTO_LOGIN=true + ''; + }; +} diff --git a/hosts/common/optional/selfhosted/homarr/default.nix b/hosts/common/optional/selfhosted/homarr/default.nix new file mode 100644 index 0000000..c549212 --- /dev/null +++ b/hosts/common/optional/selfhosted/homarr/default.nix @@ -0,0 +1,59 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.homarr; + hostDataPath = "${common.dataDir}/homarr"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.homarr = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.name}" = { + image = "ghcr.io/homarr-labs/homarr:latest"; + ports = [ + "0.0.0.0:${toString cfg.hostPort}:7575" + ]; + environmentFiles = [ config.sops.templates."homarr.env".path ]; + volumes = [ + "${hostDataPath}/data:/appdata" + "/var/run/docker.sock:/var/run/docker.sock" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/data + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/immich/default.nix b/hosts/common/optional/selfhosted/immich/default.nix new file mode 100644 index 0000000..32c70d4 --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/default.nix @@ -0,0 +1,38 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.immich; +in +{ + imports = [ + ./immich + ./ml + ./postgresql + ./redis + ]; + + options.selfhosted.immich = { + targetName = mkOption { + type = types.str; + }; + version = mkOption { + type = types.str; + }; + }; + + config = { + sops.secrets = { + "immich/db_password" = { }; + "immich/client_id" = { }; + "immich/client_secret" = { }; + }; + + systemd.targets."${common.backend}-${cfg.targetName}" = { + unitConfig = { + Description = "Root target for ${cfg.targetName}"; + }; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/immich/immich/config.nix b/hosts/common/optional/selfhosted/immich/immich/config.nix new file mode 100644 index 0000000..42e94f1 --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/immich/config.nix @@ -0,0 +1,6 @@ +{ config, ... }: +{ + sops.templates."immich.env".content = '' + DB_PASSWORD=${config.sops.placeholder."immich/db_password"} + ''; +} diff --git a/hosts/common/optional/selfhosted/immich/immich/default.nix b/hosts/common/optional/selfhosted/immich/immich/default.nix new file mode 100644 index 0000000..c57d1cb --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/immich/default.nix @@ -0,0 +1,71 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.immich; + cfgPath = "/config/immich.json"; + hostDataPath = "${common.dataDir}/immich"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.immich.immich = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.immich.name}" = { + image = "ghcr.io/immich-app/immich-server:${cfg.version}"; + ports = [ + "0.0.0.0:${toString cfg.immich.hostPort}:2283" + ]; + environmentFiles = [ config.sops.templates."immich.env".path ]; + environment = { + IMMICH_VERSION = cfg.version; + DB_HOSTNAME = cfg.postgresql.name; + DB_DATABASE_NAME = cfg.postgresql.db; + DB_USERNAME = cfg.postgresql.user; + REDIS_HOSTNAME = cfg.redis.name; + }; + volumes = [ + "${hostDataPath}/data:/data" + "/etc/localtime:/etc/localtime:ro" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.immich.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${cfg.postgresql.name}.service" + "${common.backend}-${cfg.redis.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${cfg.postgresql.name}.service" + "${common.backend}-${cfg.redis.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/data + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/immich/ml/config.nix b/hosts/common/optional/selfhosted/immich/ml/config.nix new file mode 100644 index 0000000..f773803 --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/ml/config.nix @@ -0,0 +1,6 @@ +{ config, ... }: +{ + sops.templates."immich-db.env".content = '' + POSTGRES_PASSWORD=${config.sops.placeholder."immich/db_password"} + ''; +} diff --git a/hosts/common/optional/selfhosted/immich/ml/default.nix b/hosts/common/optional/selfhosted/immich/ml/default.nix new file mode 100644 index 0000000..5edfe18 --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/ml/default.nix @@ -0,0 +1,42 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.immich; + hostDataPath = "${common.dataDir}/immich"; +in +{ + options.selfhosted.immich.ml = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.ml.name}" = { + image = "ghcr.io/immich-app/immich-machine-learning:${cfg.version}"; + environment = { + IMMICH_VERSION = cfg.version; + }; + volumes = [ + "${hostDataPath}/cache:/cache" + ]; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.ml.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/cache + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/immich/postgresql/config.nix b/hosts/common/optional/selfhosted/immich/postgresql/config.nix new file mode 100644 index 0000000..f773803 --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/postgresql/config.nix @@ -0,0 +1,6 @@ +{ config, ... }: +{ + sops.templates."immich-db.env".content = '' + POSTGRES_PASSWORD=${config.sops.placeholder."immich/db_password"} + ''; +} diff --git a/hosts/common/optional/selfhosted/immich/postgresql/default.nix b/hosts/common/optional/selfhosted/immich/postgresql/default.nix new file mode 100644 index 0000000..9c260ee --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/postgresql/default.nix @@ -0,0 +1,55 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.immich; + hostDataPath = "${common.dataDir}/immich"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.immich.postgresql = { + name = mkOption { + type = types.str; + }; + db = mkOption { + type = types.str; + }; + user = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.postgresql.name}" = { + image = "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23"; + environmentFiles = [ config.sops.templates."immich-db.env".path ]; + environment = { + POSTGRES_DB = cfg.postgresql.db; + POSTGRES_USER = cfg.postgresql.user; + POSTGRES_INITDB_ARGS = "--data-checksums"; + }; + volumes = [ + "${hostDataPath}/db:/var/lib/postgresql/data" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + "--shm-size=128m" + ]; + }; + + systemd.services."${common.backend}-${cfg.postgresql.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/db + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/immich/redis/default.nix b/hosts/common/optional/selfhosted/immich/redis/default.nix new file mode 100644 index 0000000..7e8e336 --- /dev/null +++ b/hosts/common/optional/selfhosted/immich/redis/default.nix @@ -0,0 +1,34 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.immich; +in +{ + options.selfhosted.immich.redis = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.redis.name}" = { + image = "docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9"; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + "--health-cmd=redis-cli ping || exit 1" + ]; + }; + + systemd.services."${common.backend}-${cfg.redis.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/jellyfin/config.nix b/hosts/common/optional/selfhosted/jellyfin/config.nix new file mode 100644 index 0000000..d766702 --- /dev/null +++ b/hosts/common/optional/selfhosted/jellyfin/config.nix @@ -0,0 +1,12 @@ +{ config, ... }: +let + cfg = config.selfhosted.jellyfin; +in +{ + sops.secrets = { + }; + + sops.templates."jellyfin.env".content = '' + + ''; +} diff --git a/hosts/common/optional/selfhosted/jellyfin/default.nix b/hosts/common/optional/selfhosted/jellyfin/default.nix new file mode 100644 index 0000000..2f2a83a --- /dev/null +++ b/hosts/common/optional/selfhosted/jellyfin/default.nix @@ -0,0 +1,66 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.jellyfin; + hostDataPath = "${common.dataDir}/jellyfin"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.jellyfin = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostHttpPort = mkOption { + type = types.port; + }; + hostUdpPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.name} = { + image = "docker.io/jellyfin/jellyfin:10.11.11"; + ports = [ + "0.0.0.0:${toString cfg.hostHttpPort}:8096" + "0.0.0.0:${toString cfg.hostUdpPort}:7359" + ]; + user = "${common.user.uid}:${common.group.gid}"; + environmentFiles = [ config.sops.templates."jellyfin.env".path ]; + volumes = [ + "${hostDataPath}/config:/config" + "${hostDataPath}/cache:/cache" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/{config,cache} + chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath} + chmod -R 0775 ${hostDataPath} + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/netbird/dashboard/config.nix b/hosts/common/optional/selfhosted/netbird/dashboard/config.nix new file mode 100644 index 0000000..5fa7840 --- /dev/null +++ b/hosts/common/optional/selfhosted/netbird/dashboard/config.nix @@ -0,0 +1,23 @@ +{ config, ... }: +let + fullDomain = "${config.selfhosted.netbird.subdomain}.${config.sops.placeholder."domains/serv"}"; +in +{ + sops.templates."nb-dashboard.env".content = '' + NETBIRD_MGMT_API_ENDPOINT=https://${fullDomain} + NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${fullDomain} + + # Initial NB setup: custom OIDCP is added manually in panel later + AUTH_AUTHORITY=https://${fullDomain}/oauth2 + AUTH_AUDIENCE=netbird-dashboard + AUTH_CLIENT_ID=netbird-dashboard + AUTH_CLIENT_SECRET= + USE_AUTH0=false + AUTH_SUPPORTED_SCOPES=openid profile email groups + AUTH_REDIRECT_URI=/nb-auth + AUTH_SILENT_REDIRECT_URI=/nb-silent-auth + + LETSENCRYPT_DOMAIN=none + LETSENCRYPT_EMAIL=${config.sops.placeholder."letsEncrypt/email"} + ''; +} diff --git a/hosts/common/optional/selfhosted/netbird/dashboard/default.nix b/hosts/common/optional/selfhosted/netbird/dashboard/default.nix new file mode 100644 index 0000000..2224e70 --- /dev/null +++ b/hosts/common/optional/selfhosted/netbird/dashboard/default.nix @@ -0,0 +1,43 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.netbird; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.netbird.dashboard = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.dashboard.name} = { + image = "netbirdio/dashboard:latest"; + environmentFiles = [ config.sops.templates."nb-dashboard.env".path ]; + log-driver = "journald"; + extraOptions = [ + "--network-alias=${cfg.dashboard.name}" + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.dashboard.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-${cfg.server.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-${cfg.server.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/netbird/default.nix b/hosts/common/optional/selfhosted/netbird/default.nix new file mode 100644 index 0000000..938a4ae --- /dev/null +++ b/hosts/common/optional/selfhosted/netbird/default.nix @@ -0,0 +1,43 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.netbird; +in +{ + imports = [ + ./dashboard + ./proxy + ./server + ]; + + options.selfhosted.netbird = { + targetName = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + }; + + config = { + sops.secrets = { + "netbird/proxy/token" = { }; + }; + + systemd.targets."${common.backend}-${cfg.targetName}" = { + unitConfig = { + Description = "Root target for ${cfg.targetName}"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/netbird/proxy/config.nix b/hosts/common/optional/selfhosted/netbird/proxy/config.nix new file mode 100644 index 0000000..cd38ecd --- /dev/null +++ b/hosts/common/optional/selfhosted/netbird/proxy/config.nix @@ -0,0 +1,20 @@ +{ config, ... }: +let + cfg = config.selfhosted.netbird; +in +{ + sops.templates."nb-proxy.env".content = '' + NB_PROXY_DEBUG_LOGS=false + NB_PROXY_PROXY_PROTOCOL=true + NB_PROXY_MANAGEMENT_ADDRESS=http://${cfg.server.name}:80 + NB_PROXY_ALLOW_INSECURE=true + NB_PROXY_DOMAIN=${cfg.subdomain}.${config.sops.placeholder."domains/serv"} + NB_PROXY_ADDRESS=:8443 + NB_PROXY_TOKEN=${config.sops.placeholder."netbird/proxy/token"} + NB_PROXY_FORWARDED_PROTO=https + NB_PROXY_CERTIFICATE_DIRECTORY=/certs + NB_PROXY_ACME_CERTIFICATES=true + NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01 + NB_PROXY_TRUSTED_PROXIES=${config.selfhosted.traefik.ip} + ''; +} diff --git a/hosts/common/optional/selfhosted/netbird/proxy/default.nix b/hosts/common/optional/selfhosted/netbird/proxy/default.nix new file mode 100644 index 0000000..b318202 --- /dev/null +++ b/hosts/common/optional/selfhosted/netbird/proxy/default.nix @@ -0,0 +1,60 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.netbird; + hostDataPath = "${common.dataDir}/netbird/proxy"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.netbird.proxy = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.proxy.name} = { + image = "netbirdio/reverse-proxy:latest"; + environmentFiles = [ config.sops.templates."nb-proxy.env".path ]; + volumes = [ + "${hostDataPath}/certs:/certs:rw" + ]; + ports = [ + "0.0.0.0:2222:2222/tcp" # gitea + "0.0.0.0:7359:7359/udp" # jelly + "0.0.0.0:51820:51820/udp" + ]; + dependsOn = [ cfg.server.name ]; + log-driver = "journald"; + extraOptions = [ + "--network-alias=${cfg.proxy.name}" + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.proxy.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${cfg.server.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${cfg.server.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/certs + chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath} + chmod -R 0755 ${hostDataPath} + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/netbird/server/config.nix b/hosts/common/optional/selfhosted/netbird/server/config.nix new file mode 100644 index 0000000..a365e70 --- /dev/null +++ b/hosts/common/optional/selfhosted/netbird/server/config.nix @@ -0,0 +1,43 @@ +{ config, ... }: +let + common = config.selfhosted.common; + cfg = config.selfhosted.netbird; + fullDomain = "${cfg.subdomain}.${config.sops.placeholder."domains/serv"}"; +in +{ + sops.secrets = { + "netbird/server/auth_secret" = { }; + "netbird/server/enc_key" = { }; + }; + + sops.templates."nb-server.yaml".content = '' + server: + listenAddress: ":80" + exposedAddress: "https://${fullDomain}:443" + stunPorts: + - 3478 + metricsPort: 9090 + healthcheckAddress: ":9000" + logLevel: "info" + logFile: "console" + + authSecret: "${config.sops.placeholder."netbird/server/auth_secret"}" + + auth: + issuer: "https://${fullDomain}/oauth2" + dashboardRedirectURIs: + - "https://${fullDomain}/nb-auth" + - "https://${fullDomain}/nb-silent-auth" + cliRedirectURIs: + - "http://localhost:53000/" + localAuthDisabled: false + + store: + engine: "sqlite" + encryptionKey: "${config.sops.placeholder."netbird/server/enc_key"}" + + reverseProxy: + trustedHTTPProxies: + - "${config.selfhosted.traefik.ip}/32" + ''; +} diff --git a/hosts/common/optional/selfhosted/netbird/server/default.nix b/hosts/common/optional/selfhosted/netbird/server/default.nix new file mode 100644 index 0000000..c5d018d --- /dev/null +++ b/hosts/common/optional/selfhosted/netbird/server/default.nix @@ -0,0 +1,59 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.netbird; + hostDataPath = "${common.dataDir}/netbird/server"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.netbird.server = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.server.name} = { + image = "netbirdio/netbird-server:latest"; + volumes = [ + "${config.sops.templates."nb-server.yaml".path}:/etc/netbird/config.yaml:rw" + "${hostDataPath}/data:/var/lib/netbird:rw" + ]; + ports = [ + "0.0.0.0:3478:3478/udp" + ]; + cmd = [ + "--config" + "/etc/netbird/config.yaml" + ]; + log-driver = "journald"; + extraOptions = [ + "--network-alias=${cfg.server.name}" + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.server.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/data + chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath} + chmod -R 0755 ${hostDataPath} + ''; + }; + }; +} diff --git a/home/benq/core/selfhosted/containers/pocket-id/config.nix b/hosts/common/optional/selfhosted/pocket-id/config.nix similarity index 100% rename from home/benq/core/selfhosted/containers/pocket-id/config.nix rename to hosts/common/optional/selfhosted/pocket-id/config.nix diff --git a/hosts/common/optional/selfhosted/pocket-id/default.nix b/hosts/common/optional/selfhosted/pocket-id/default.nix new file mode 100644 index 0000000..6166ef4 --- /dev/null +++ b/hosts/common/optional/selfhosted/pocket-id/default.nix @@ -0,0 +1,61 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.pocket-id; + hostDataPath = "${common.dataDir}/pocket-id"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.pocket-id = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.name} = { + image = "ghcr.io/pocket-id/pocket-id:latest"; + ports = [ + "0.0.0.0:${toString cfg.hostPort}:1411" + ]; + user = "${common.user.uid}:${common.group.gid}"; + environmentFiles = [ config.sops.templates."pocket-id.env".path ]; + volumes = [ + "${hostDataPath}/data:/app/data" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/letsencrypt + chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath} + chmod -R 0775 ${hostDataPath} + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/seafile/default.nix b/hosts/common/optional/selfhosted/seafile/default.nix new file mode 100644 index 0000000..eeb4a91 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/default.nix @@ -0,0 +1,33 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.seafile; +in +{ + imports = [ + ./mariadb + ./notification + ./redis + ./seadoc + ./server + ]; + + options.selfhosted.seafile = { + targetName = mkOption { + type = types.str; + }; + }; + + config = { + systemd.targets."${common.backend}-${cfg.targetName}" = { + unitConfig = { + Description = "Root target for ${cfg.targetName}"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/seafile/mariadb/config.nix b/hosts/common/optional/selfhosted/seafile/mariadb/config.nix new file mode 100644 index 0000000..f59a733 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/mariadb/config.nix @@ -0,0 +1,12 @@ +{ config, ... }: +{ + sops.secrets = { + "seafile/mysql_password" = { }; + }; + + sops.templates."seafile-db.env".content = '' + MYSQL_ROOT_PASSWORD=${config.sops.placeholder."seafile/mysql_password"} + MYSQL_LOG_CONSOLE=true + MARIADB_AUTO_UPGRADE=1 + ''; +} diff --git a/hosts/common/optional/selfhosted/seafile/mariadb/default.nix b/hosts/common/optional/selfhosted/seafile/mariadb/default.nix new file mode 100644 index 0000000..6fbe5d5 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/mariadb/default.nix @@ -0,0 +1,48 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.seafile; + hostDataPath = "${common.dataDir}/seafile"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.seafile.mariadb = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.mariadb.name} = { + image = "docker.io/mariadb:10.11"; + environmentFiles = [ config.sops.templates."seafile-db.env".path ]; + volumes = [ + "${hostDataPath}/mysql:/var/lib/mysql" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + "--health-cmd=/usr/local/bin/healthcheck.sh --connect --mariadbupgrade --innodb_initialized" + "--health-interval=20s" + "--health-start-period=30s" + "--health-timeout=5s" + "--health-retries=10" + ]; + }; + + systemd.services."${common.backend}-${cfg.mariadb.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/mysql + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/seafile/notification/config.nix b/hosts/common/optional/selfhosted/seafile/notification/config.nix new file mode 100644 index 0000000..8569891 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/notification/config.nix @@ -0,0 +1,17 @@ +{ config, ... }: +let + cfg = config.selfhosted.seafile; +in +{ + sops.templates."seafile-notification.env".content = '' + SEAFILE_MYSQL_DB_HOST=${cfg.mariadb.name} + SEAFILE_MYSQL_DB_PORT=3306 + SEAFILE_MYSQL_DB_USER=seafile + SEAFILE_MYSQL_DB_PASSWORD=${config.sops.placeholder."seafile/mysql_password"} + SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db + SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db + JWT_PRIVATE_KEY=${config.sops.placeholder."seafile/jwt_private_key"} + SEAFILE_LOG_TO_STDOUT=false + NOTIFICATION_SERVER_LOG_LEVEL=info + ''; +} diff --git a/hosts/common/optional/selfhosted/seafile/notification/default.nix b/hosts/common/optional/selfhosted/seafile/notification/default.nix new file mode 100644 index 0000000..b3803eb --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/notification/default.nix @@ -0,0 +1,73 @@ +{ config, lib, pkgs, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.seafile; + hostDataPath = "${common.dataDir}/seafile"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.seafile.notification = { + name = mkOption { + type = types.str; + }; + hostHttpPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.notification.name} = { + image = "docker.io/seafileltd/notification-server:13.0-latest"; + ports = [ + "0.0.0.0:${toString cfg.notification.hostHttpPort}:8083" + ]; + environmentFiles = [ config.sops.templates."seafile-notification.env".path ]; + volumes = [ + "${hostDataPath}/notification/logs:/shared/seafile/logs" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.notification.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + "${common.backend}-${cfg.mariadb.name}.service" + "${common.backend}-${cfg.server.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + "${common.backend}-${cfg.mariadb.name}.service" + "${common.backend}-${cfg.server.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + path = with pkgs; [ curl gawk ]; + preStart = '' + echo "Waiting for Seafile server to be ready..." + for i in {1..60}; do + status=$(curl -sI --max-time 5 "http://localhost:${toString cfg.server.hostHttpPort}" | head -1 | awk '{print $2}') + + if [[ "$status" =~ ^(200|301|302)$ ]]; then + echo "Seafile server is up (HTTP $status, attempt $i)" + break + fi + + echo "Seafile not ready yet (status: $status) ... ($i/60)" + sleep 1 + done + + mkdir -p ${hostDataPath}/notification + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/seafile/redis/config.nix b/hosts/common/optional/selfhosted/seafile/redis/config.nix new file mode 100644 index 0000000..e81e257 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/redis/config.nix @@ -0,0 +1,10 @@ +{ config, ... }: +{ + sops.secrets = { + "seafile/redis_password" = { }; + }; + + sops.templates."seafile-redis.env".content = '' + REDIS_PASSWORD=${config.sops.placeholder."seafile/redis_password"} + ''; +} diff --git a/hosts/common/optional/selfhosted/seafile/redis/default.nix b/hosts/common/optional/selfhosted/seafile/redis/default.nix new file mode 100644 index 0000000..8bec426 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/redis/default.nix @@ -0,0 +1,37 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.seafile; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.seafile.redis = { + name = mkOption { + type = types.str; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.redis.name} = { + image = "docker.io/redis:7"; + environmentFiles = [ config.sops.templates."seafile-redis.env".path ]; + cmd = [ "/bin/sh" "-c" "exec redis-server --requirepass \"$REDIS_PASSWORD\"" ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.redis.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/seafile/seadoc/config.nix b/hosts/common/optional/selfhosted/seafile/seadoc/config.nix new file mode 100644 index 0000000..cdce5c9 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/seadoc/config.nix @@ -0,0 +1,17 @@ +{ config, ... }: +let + cfg = config.selfhosted.seafile; +in +{ + sops.templates."seadoc.env".content = '' + DB_HOST=${cfg.mariadb.name} + DB_PORT=3306 + DB_USER=seafile + DB_PASSWORD=${config.sops.placeholder."seafile/mysql_password"} + DB_NAME=seahub_db + TIME_ZONE=${config.time.timeZone} + JWT_PRIVATE_KEY=${config.sops.placeholder."seafile/jwt_private_key"} + NON_ROOT=false + SEAHUB_SERVICE_URL=http://${cfg.server.name} + ''; +} diff --git a/hosts/common/optional/selfhosted/seafile/seadoc/default.nix b/hosts/common/optional/selfhosted/seafile/seadoc/default.nix new file mode 100644 index 0000000..448379d --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/seadoc/default.nix @@ -0,0 +1,57 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.seafile; + hostDataPath = "${common.dataDir}/seafile"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.seafile.seadoc = { + name = mkOption { + type = types.str; + }; + hostHttpPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.seadoc.name} = { + image = "docker.io/seafileltd/sdoc-server:2.0-latest"; + ports = [ + "0.0.0.0:${toString cfg.seadoc.hostHttpPort}:80" + ]; + volumes = [ + "${hostDataPath}/seadoc/shared:/shared" + ]; + environmentFiles = [ config.sops.templates."seadoc.env".path ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.seadoc.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + "${common.backend}-${cfg.mariadb.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + "${common.backend}-${cfg.mariadb.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/seadoc/shared + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/seafile/server/config.nix b/hosts/common/optional/selfhosted/seafile/server/config.nix new file mode 100644 index 0000000..99ae677 --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/server/config.nix @@ -0,0 +1,103 @@ +{ config, ... }: +let + cfg = config.selfhosted.seafile; + poCfg = config.selfhosted.pocket-id; + domain = config.sops.placeholder."domains/serv"; +in +{ + sops.secrets = { + "seafile/admin_email" = { }; + "seafile/admin_password" = { }; + "seafile/client_id" = { }; + "seafile/client_secret" = { }; + "seafile/jwt_private_key" = { }; + }; + + sops.templates."seafile.env".content = '' + SEAFILE_MYSQL_DB_HOST=${cfg.mariadb.name} + SEAFILE_MYSQL_DB_PORT=3306 + SEAFILE_MYSQL_DB_USER=seafile + SEAFILE_MYSQL_DB_PASSWORD=${config.sops.placeholder."seafile/mysql_password"} + SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db + SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db + SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub_db + TIME_ZONE=${config.time.timeZone} + INIT_SEAFILE_MYSQL_ROOT_PASSWORD=${config.sops.placeholder."seafile/mysql_password"} + INIT_SEAFILE_ADMIN_EMAIL=${config.sops.placeholder."seafile/admin_email"} + INIT_SEAFILE_ADMIN_PASSWORD=${config.sops.placeholder."seafile/admin_password"} + SEAFILE_SERVER_HOSTNAME=${cfg.server.subdomain}.${domain} + SEAFILE_SERVER_PROTOCOL=https + SEAFILE_SERVER_LETSENCRYPT=false + SITE_ROOT=/ + NON_ROOT=false + JWT_PRIVATE_KEY=${config.sops.placeholder."seafile/jwt_private_key"} + SEAFILE_LOG_TO_STDOUT=false + ENABLE_GO_FILESERVER=true + ENABLE_SEADOC=true + SEADOC_SERVER_URL=https://${cfg.server.subdomain}.${domain}/sdoc-server + CACHE_PROVIDER=redis + REDIS_HOST=${cfg.redis.name} + REDIS_PORT=6379 + REDIS_PASSWORD=${config.sops.placeholder."seafile/redis_password"} + ENABLE_NOTIFICATION_SERVER=true + INNER_NOTIFICATION_SERVER_URL=http://${cfg.notification.name}:8083 + NOTIFICATION_SERVER_URL=https://${cfg.server.subdomain}.${domain}/notification + ENABLE_SEAFILE_AI=false + MD_FILE_COUNT_LIMIT=100000 + ''; + + sops.templates."seahub_settings.py".content = '' + ENABLE_VIDEO_THUMBNAIL = True + + ENABLE_OAUTH = True + OAUTH_CREATE_UNKNOWN_USER = True + OAUTH_ACTIVATE_USER_AFTER_CREATION = True + OAUTH_ENABLE_INSECURE_TRANSPORT = False + OAUTH_CLIENT_ID = "${config.sops.placeholder."seafile/client_id"}" + OAUTH_CLIENT_SECRET = "${config.sops.placeholder."seafile/client_secret"}" + OAUTH_REDIRECT_URL = "https://${cfg.server.subdomain}.${domain}/oauth/callback" + OAUTH_PROVIDER = "pocket-id" + OAUTH_PROVIDER_DOMAIN = "pocket-id" + OAUTH_AUTHORIZATION_URL = "https://${poCfg.subdomain}.${domain}/authorize" + OAUTH_TOKEN_URL = "https://${poCfg.subdomain}.${domain}/api/oidc/token" + OAUTH_USER_INFO_URL = "https://${poCfg.subdomain}.${domain}/api/oidc/userinfo" + OAUTH_SCOPE = [ + "openid", + "profile", + "email" + ] + OAUTH_ATTRIBUTE_MAP = { + "sub": (True, "uid"), + "name": (False, "name"), + "email": (False, "contact_email"), + } + CLIENT_SSO_VIA_LOCAL_BROWSER = True + + ENABLE_WEBDAV_SECRET = True + WEBDAV_SECRET_MIN_LENGTH = 8 + SERVICE_URL = "https://${cfg.server.subdomain}.${domain}" + USE_X_FORWARDED_HOST = True + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + ALLOWED_HOSTS = [".${domain}"] + CSRF_COOKIE_SECURE = True + CSRF_COOKIE_SAMESITE = "Lax" + CSRF_TRUSTED_ORIGINS = [ + "https://${cfg.server.subdomain}.${domain}" + ] + ''; + + sops.templates."seafdav.conf".content = '' + [WEBDAV] + enabled = true + port = 8080 + debug = true + share_name = /seafdav + workers = 5 + timeout = 1200 + ''; + + sops.templates."seafile.conf".content = '' + [quota] + default = 100 + ''; +} diff --git a/hosts/common/optional/selfhosted/seafile/server/default.nix b/hosts/common/optional/selfhosted/seafile/server/default.nix new file mode 100644 index 0000000..5350caf --- /dev/null +++ b/hosts/common/optional/selfhosted/seafile/server/default.nix @@ -0,0 +1,70 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.seafile; + hostDataPath = "${common.dataDir}/seafile"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.seafile.server = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostHttpPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.server.name} = { + image = "docker.io/seafileltd/seafile-mc:13.0-latest"; + ports = [ + "0.0.0.0:${toString cfg.server.hostHttpPort}:80" + ]; + environmentFiles = [ config.sops.templates."seafile.env".path ]; + volumes = [ + "${hostDataPath}/server/shared:/shared" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.server.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + "${common.backend}-${config.selfhosted.seafile.mariadb.name}.service" + "${common.backend}-${config.selfhosted.seafile.redis.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + "${common.backend}-${cfg.mariadb.name}.service" + "${common.backend}-${cfg.redis.name}.service" + ]; + partOf = [ "${common.backend}-${cfg.targetName}.target" ]; + wantedBy = [ "${common.backend}-${cfg.targetName}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/server/shared/seafile/conf + + cp -f ${config.sops.templates."seahub_settings.py".path} \ + ${hostDataPath}/server/shared/seafile/conf/seahub_settings.py + cp -f ${config.sops.templates."seafdav.conf".path} \ + ${hostDataPath}/server/shared/seafile/conf/seafdav.conf + cp -f ${config.sops.templates."seafile.conf".path} \ + ${hostDataPath}/server/shared/seafile/conf/seafile.conf + chmod 644 ${hostDataPath}/server/shared/seafile/conf/{seafile.conf,seafdav.conf,seahub_settings.py} + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/traefik/config.nix b/hosts/common/optional/selfhosted/traefik/config.nix new file mode 100644 index 0000000..6010351 --- /dev/null +++ b/hosts/common/optional/selfhosted/traefik/config.nix @@ -0,0 +1,118 @@ +{ config, ... }: +let + common = config.selfhosted.common; + cfg = config.selfhosted.netbird; +in +{ + sops.templates."traefik.env".content = '' + TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=${config.sops.placeholder."letsEncrypt/email"} + ''; + + sops.templates."traefik-static-config.yaml".content = '' + entryPoints: + web: + address: ":80" + http: + redirections: + entrypoint: + to: websecure + scheme: https + websecure: + address: ":443" + allowACMEByPass: true + transport: + respondingTimeouts: + readTimeout: 0 + writeTimeout: 0 + idleTimeout: 0 + proxyProtocol: + trustedIPs: + - "${common.network.address}/${common.network.mask}" + + serversTransport: + forwardingTimeouts: + responseHeaderTimeout: 0 + idleConnTimeout: 0 + + providers: + docker: + exposedByDefault: false + network: "${common.network.name}" + file: + filename: /etc/traefik/dynamic.yaml + watch: true + + certificatesResolvers: + letsencrypt: + acme: + storage: /letsencrypt/acme.json + tlsChallenge: true + ''; + + sops.templates."traefik-dynamic-config.yaml".content = '' + http: + routers: + netbird-dashboard: + entryPoints: + - websecure + rule: "Host(`${cfg.subdomain}.${config.sops.placeholder."domains/serv"}`)" + service: dashboard + priority: 1 + tls: + certResolver: letsencrypt + + netbird-backend: + entryPoints: + - websecure + rule: "Host(`${cfg.subdomain}.${config.sops.placeholder."domains/serv"}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))" + service: netbird-server + priority: 100 + tls: + certResolver: letsencrypt + + netbird-grpc: + entryPoints: + - websecure + rule: "Host(`${cfg.subdomain}.${config.sops.placeholder."domains/serv"}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`))" + service: netbird-server-h2c + priority: 100 + tls: + certResolver: letsencrypt + + services: + dashboard: + loadBalancer: + servers: + - url: "http://${cfg.dashboard.name}:80" + netbird-server: + loadBalancer: + servers: + - url: "http://${cfg.server.name}:80" + netbird-server-h2c: + loadBalancer: + servers: + - url: "h2c://${cfg.server.name}:80" + tcp: + serversTransports: + pp-v2: + proxyProtocol: + version: 2 + + routers: + proxy-passthrough: + entrypoints: + - websecure + rule: HostSNI(`*`) + service: proxy-tls + priority: 1 + tls: + passthrough: true + + services: + proxy-tls: + loadBalancer: + serversTransport: pp-v2@file + servers: + - address: "${cfg.proxy.name}:8443" + ''; +} diff --git a/hosts/common/optional/selfhosted/traefik/default.nix b/hosts/common/optional/selfhosted/traefik/default.nix new file mode 100644 index 0000000..82921c1 --- /dev/null +++ b/hosts/common/optional/selfhosted/traefik/default.nix @@ -0,0 +1,72 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.traefik; + hostDataPath = "${common.dataDir}/traefik"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.traefik = { + name = mkOption { + type = types.str; + }; + ip = mkOption { + type = types.str; + }; + }; + + config = { + sops.secrets = { + "domains/serv" = { }; + "letsEncrypt/email" = { }; + }; + + virtualisation.oci-containers.containers.${cfg.name} = { + image = "traefik:latest"; + environmentFiles = [ config.sops.templates."traefik.env".path ]; + volumes = [ + "/var/run/docker.sock:/var/run/docker.sock:ro" + "${config.sops.templates."traefik-static-config.yaml".path}:/etc/traefik/traefik.yaml:ro" + "${config.sops.templates."traefik-dynamic-config.yaml".path}:/etc/traefik/dynamic.yaml:ro" + "${hostDataPath}/letsencrypt:/letsencrypt:rw" + ]; + ports = [ + "0.0.0.0:80:80" + "0.0.0.0:443:443" + ]; + cmd = [ + "--log.level=INFO" + "--accesslog=true" + "--configfile=/etc/traefik/traefik.yaml" + ]; + log-driver = "journald"; + extraOptions = [ + "--ip=172.30.0.10" + "--network-alias=${cfg.name}" + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/letsencrypt + chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath} + chmod -R 0600 ${hostDataPath} + ''; + }; + }; +} diff --git a/home/benq/core/selfhosted/containers/vaultwarden/config.nix b/hosts/common/optional/selfhosted/vaultwarden/config.nix similarity index 51% rename from home/benq/core/selfhosted/containers/vaultwarden/config.nix rename to hosts/common/optional/selfhosted/vaultwarden/config.nix index 2259e03..a998dad 100644 --- a/home/benq/core/selfhosted/containers/vaultwarden/config.nix +++ b/hosts/common/optional/selfhosted/vaultwarden/config.nix @@ -1,26 +1,27 @@ { config, ... }: let - cfg = config.selfhosted.vaultwarden; - poCfg = config.selfhosted.pocket-id; - domain = config.sops.placeholder."domains/serv"; + cfg = config.selfhosted; + domain = "${config.sops.placeholder."domains/serv"}"; in { sops.secrets = { + "vaultwarden/admin_token" = { }; "vaultwarden/client_id" = { }; "vaultwarden/client_secret" = { }; }; sops.templates."vaultwarden.env".content = '' - DOMAIN=https://${cfg.subdomain}.${domain} - LOG_LEVEL=info,vaultwarden::sso=debug + DOMAIN=https://${cfg.vaultwarden.subdomain}.${domain} SIGNUPS_ALLOWED=false - SSO_ENABLED=false - SSO_DEBUG_TOKENS=true + ADMIN_TOKEN=${config.sops.placeholder."vaultwarden/admin_token"} + + # SSO + SSO_ENABLED=true SSO_ONLY=false - SSO_AUTHORITY=https://${poCfg.subdomain}.${domain} + SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION=true + SSO_AUTHORITY=https://${cfg.pocket-id.subdomain}.${domain} SSO_CLIENT_ID=${config.sops.placeholder."vaultwarden/client_id"} SSO_CLIENT_SECRET=${config.sops.placeholder."vaultwarden/client_secret"} SSO_AUTH_ONLY_NOT_SESSION=true - SSO_PKCE=true ''; } diff --git a/hosts/common/optional/selfhosted/vaultwarden/default.nix b/hosts/common/optional/selfhosted/vaultwarden/default.nix new file mode 100644 index 0000000..9f3919b --- /dev/null +++ b/hosts/common/optional/selfhosted/vaultwarden/default.nix @@ -0,0 +1,61 @@ +{ config, lib, ... }: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.vaultwarden; + hostDataPath = "${common.dataDir}/vaultwarden"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.vaultwarden = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers."${cfg.name}" = { + image = "vaultwarden/server:latest"; + ports = [ + "0.0.0.0:${toString cfg.hostPort}:80" + ]; + user = "${common.user.uid}:${common.group.gid}"; + environmentFiles = [ config.sops.templates."vaultwarden.env".path ]; + volumes = [ + "${hostDataPath}/data:/data" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=${common.network.name}" + ]; + }; + + systemd.services."${common.backend}-${cfg.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/data + chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath} + chmod -R 0775 ${hostDataPath} + ''; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/zapret2/config.nix b/hosts/common/optional/selfhosted/zapret2/config.nix new file mode 100644 index 0000000..8a0b963 --- /dev/null +++ b/hosts/common/optional/selfhosted/zapret2/config.nix @@ -0,0 +1,58 @@ +{ pkgs }: +let + configFiles = { + config = '' + FWTYPE=iptables + + SET_MAXELEM=522288 + + IPSET_OPT="hashsize 262144 maxelem $SET_MAXELEM" + + IP2NET_OPT4="--prefix-length=22-30 --v4-threshold=3/4" + IP2NET_OPT6="--prefix-length=56-64 --v6-threshold=5" + + AUTOHOSTLIST_INCOMING_MAXSEQ=4096 + AUTOHOSTLIST_RETRANS_MAXSEQ=32768 + AUTOHOSTLIST_RETRANS_RESET=1 + AUTOHOSTLIST_RETRANS_THRESHOLD=3 + AUTOHOSTLIST_FAIL_THRESHOLD=3 + AUTOHOSTLIST_FAIL_TIME=60 + AUTOHOSTLIST_UDP_IN=1 + AUTOHOSTLIST_UDP_OUT=4 + AUTOHOSTLIST_DEBUGLOG=0 + + MDIG_THREADS=30 + + GZIP_LISTS=1 + + DESYNC_MARK=0x40000000 + DESYNC_MARK_POSTNAT=0x20000000 + + NFQWS2_ENABLE=1 + NFQWS2_PORTS_TCP=80,443 + NFQWS2_PORTS_UDP=443 + NFQWS2_TCP_PKT_OUT=20 + NFQWS2_TCP_PKT_IN=10 + NFQWS2_UDP_PKT_OUT=5 + NFQWS2_UDP_PKT_IN=3 + NFQWS2_OPT=" + --filter-tcp=80 --filter-l7=http --payload http_req --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=1 --new + --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --hostlist-domains=x.com --lua-desync=hostfakesplit:nofake1:midhost=midsld:ip_ttl=6:repeats=1 --new + --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=1 --new + --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=1 + " + + MODE_FILTER=none + + FLOWOFFLOAD=donttouch + + INIT_APPLY_FW=1 + + DISABLE_IPV6=1 + + FILTER_TTL_EXPIRED_ICMP=1 + ''; + }; + +in +pkgs.lib.mapAttrs (name: content: pkgs.writeText name content) configFiles diff --git a/hosts/common/optional/selfhosted/zapret2/default.nix b/hosts/common/optional/selfhosted/zapret2/default.nix new file mode 100644 index 0000000..d05907a --- /dev/null +++ b/hosts/common/optional/selfhosted/zapret2/default.nix @@ -0,0 +1,67 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.zapret2; +in +{ + options.selfhosted.zapret2 = { + name = mkOption { + type = types.str; + }; + socksPort = mkOption { + type = types.port; + }; + ssPort = mkOption { + type = types.port; + }; + }; + + config = + let + socksPort = "${toString cfg.socksPort}"; + ssPort = "${toString cfg.ssPort}"; + configFiles = import ./config.nix { inherit pkgs; }; + in + { + virtualisation.oci-containers.containers.${cfg.name} = { + image = "vernette/ss-zapret2:latest"; + volumes = [ + "${configFiles.config}:/opt/zapret2/config" + ]; + environment = { + SOCKS_PORT = socksPort; + SS_PORT = ssPort; + SS_PASSWORD = "test"; + SS_ENCRYPT_METHOD = "chacha20-ietf-poly1305"; + SS_TIMEOUT = "300"; + }; + ports = [ + "127.0.0.1:${socksPort}:${socksPort}" + "127.0.0.1:${ssPort}:${ssPort}" + ]; + extraOptions = [ + "--cap-add=NET_ADMIN" + "--network=host" + ]; + }; + + systemd.services."${common.backend}-${cfg.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + postStart = '' + mv /opt/zapret2/lua.dist /opt/zapret2/lua || true + ''; + after = [ "${common.backend}-network-${common.network.name}.service" ]; + requires = [ "${common.backend}-network-${common.network.name}.service" ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + }; + }; +} diff --git a/hosts/common/optional/selfhosted/zerobyte/config.nix b/hosts/common/optional/selfhosted/zerobyte/config.nix new file mode 100644 index 0000000..e49b882 --- /dev/null +++ b/hosts/common/optional/selfhosted/zerobyte/config.nix @@ -0,0 +1,24 @@ +{ config, lib, ... }: +with lib; +let + cfg = config.selfhosted; + domain = "${config.sops.placeholder."domains/serv"}"; + zerobyteDomain = "${cfg.zerobyte.subdomain}.${domain}"; + trustedOrigins = [ + "https://${zerobyteDomain}" + "https://${cfg.pocket-id.subdomain}.${domain}" + ]; +in +{ + sops.secrets = { + "zerobyte/app_secret" = { }; + }; + + sops.templates."zerobyte.env".content = '' + TZ=${config.time.timeZone} + BASE_URL=https://${zerobyteDomain} + APP_SECRET=${config.sops.placeholder."zerobyte/app_secret"} + TRUST_PROXY=true + TRUSTED_ORIGINS=${(concatStringsSep "," trustedOrigins)} + ''; +} diff --git a/hosts/common/optional/selfhosted/zerobyte/default.nix b/hosts/common/optional/selfhosted/zerobyte/default.nix new file mode 100644 index 0000000..d4bdb2f --- /dev/null +++ b/hosts/common/optional/selfhosted/zerobyte/default.nix @@ -0,0 +1,61 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; +let + common = config.selfhosted.common; + cfg = config.selfhosted.zerobyte; + hostDataPath = "${common.dataDir}/zerobyte"; +in +{ + imports = [ ./config.nix ]; + + options.selfhosted.zerobyte = { + name = mkOption { + type = types.str; + }; + subdomain = mkOption { + type = types.str; + }; + hostPort = mkOption { + type = types.port; + }; + }; + + config = { + virtualisation.oci-containers.containers.${cfg.name} = { + image = "ghcr.io/nicotsx/zerobyte:latest"; + environmentFiles = [ config.sops.templates."zerobyte.env".path ]; + ports = [ + "0.0.0.0:${toString cfg.hostPort}:4096" + ]; + volumes = [ + "/etc/localtime:/etc/localtime:ro" + "${hostDataPath}/data:/var/lib/zerobyte" + "${hostDataPath}/backups:/mydata" + ]; + }; + + systemd.services."${common.backend}-${cfg.name}" = { + serviceConfig = { + Restart = lib.mkOverride 90 "always"; + }; + after = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + requires = [ + "${common.backend}-network-${common.network.name}.service" + "${common.backend}-${config.selfhosted.traefik.name}.service" + ]; + partOf = [ "${common.backend}-${common.rootTarget.name}.target" ]; + wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ]; + preStart = '' + mkdir -p ${hostDataPath}/{data,backups} + ''; + }; + }; +} diff --git a/hosts/common/users/benq.nix b/hosts/common/users/benq.nix index 608c2ed..f7d51e3 100644 --- a/hosts/common/users/benq.nix +++ b/hosts/common/users/benq.nix @@ -6,6 +6,12 @@ }: with lib; { + sops.secrets = { + benq-password = { + neededForUsers = true; + }; + }; + users = { mutableUsers = false; users.benq = { @@ -17,18 +23,29 @@ with lib; "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeIuJzR68xA4ugJjtWbwvaWEU852Hg9FAAhXNw8ou43 benq" ]; linger = true; - subUidRanges = [ { startUid = 100000; count = 65536; } ]; - subGidRanges = [ { startGid = 100000; count = 65536; } ]; + subUidRanges = [ + { + startUid = 100000; + count = 65536; + } + ]; + subGidRanges = [ + { + startGid = 100000; + count = 65536; + } + ]; extraGroups = let - ifTheyExist = groups: filter (group: hasAttr group config.users.groups) groups; - in + ifTheyExist = groups: filter (group: hasAttr group config.users.groups) groups; + in flatten [ - "wheel" + "wheel" (ifTheyExist [ - "git" - "networkmanager" - "video" + "docker" + "git" + "networkmanager" + "video" ]) ]; }; diff --git a/hosts/homelab/configuration.nix b/hosts/homelab/configuration.nix index ee48fe9..5d1bf9b 100644 --- a/hosts/homelab/configuration.nix +++ b/hosts/homelab/configuration.nix @@ -19,7 +19,9 @@ "bbr.nix" "endlessh.nix" "fail2ban.nix" + "fonts.nix" "openssh.nix" + "selfhosted" ]) )) ]; diff --git a/hosts/homelab/network.nix b/hosts/homelab/network.nix index 5601ba0..367c6b8 100644 --- a/hosts/homelab/network.nix +++ b/hosts/homelab/network.nix @@ -3,8 +3,8 @@ networking = { hostName = "homelab"; firewall = { - allowedTCPPorts = [ 22 80 443 6969 ]; - allowedUDPPorts = [ 3478 ]; + allowedTCPPorts = [ 22 80 443 2222 6969 ]; + allowedUDPPorts = [ 3478 7359 ]; }; }; diff --git a/secrets.yaml b/secrets.yaml deleted file mode 100644 index bbb261b..0000000 --- a/secrets.yaml +++ /dev/null @@ -1,16 +0,0 @@ -benq-password: ENC[AES256_GCM,data:jWOUkdqTtJDvPp3G0uSJDAlzdswT+efZf8keG8rhw5MgiNjWenegiC4eGdQ0ykeS+wQJJaHsrH5wIgpiMI32s18py70JDEqHnrcB0DPQnbE7DzfO7/4BhwdBWHSeEcQaiHjtFhuKr5c8hw==,iv:aA3qcFE5zyBoVMJzuGYkk5J0UyQPu4Xx3fTAl8G4Slw=,tag:IDjwsJ6c1Sm+lbo8qEfJAw==,type:str] -sops: - age: - - recipient: age1tl5hp6v0t38fl8gxuqds40z503mwz885ev22au4jdy5r24aak3zsza23sx - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbUpHc0l1Snd4eWVUUWRR - ZWdvVDNlNUp4WWxBVGxxOWdDcjQ3WDM2b2hVCjVqNCs2cThsL2gyckE1K2NsSFdJ - d0ZCamFlcVlnRmFDS05NcldmLzZZN2sKLS0tIDF1U2NhQkhjd1hEazRXbVpocnln - cXhlUUY3a0tzSURRVEdYMDZGNGRmaGcKYknpSmwVYxSFVIde43kjvJFD5zS3hmvq - JMXFAHfa7Nf3deu83gSGsvgQADb61ZI9v78x66lWwvdPKzcZFUriUg== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-10T20:15:48Z" - mac: ENC[AES256_GCM,data:MinxRU59BGQm3CH/EtvX0LaPLHkU2CsEqbbURqTtcY9H3tkLPTlf6NVJYaA4WtU2W3DZbxcRP4VzCj6o3LNdZ0qnb/CLG4INhIxRNL71SQZ8r5sIAkYHGLYOvI5kXIfFEdeXbK0Y12tb5CFIldERqbO+kcSARmlOqSoNCpFGlgw=,iv:HjwQ8n4yd8Q8Q7S/5tQFgrve960kcelwLpLIfYf6mcU=,tag:jarkIS3EkTTq2H6ZAdp+xg==,type:str] - unencrypted_suffix: _unencrypted - version: 3.12.2