From 73adb395c05f227c498ecf5aad994d92c6c3d8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Numbus?= Date: Sat, 2 May 2026 12:52:08 +0200 Subject: [PATCH] Migrated from multi repos to monorepo architecture. --- .aiexclude | 1 + .gitignore | 2 +- {web => deploy/GUI/web}/index.html | 0 {web => deploy/GUI/web}/logic/bridge.py | 0 .../media/dark/numbus-backup-server-dark.svg | 0 .../web}/media/dark/numbus-computer-dark.svg | 0 .../web}/media/dark/numbus-server-dark.svg | 0 .../web}/media/dark/numbus-template-dark.svg | 0 .../GUI/web}/media/dark/numbus-tv-dark.svg | 0 {web => deploy/GUI/web}/media/favicon.ico | Bin .../light/numbus-backup-server-light.svg | 0 .../media/light/numbus-computer-light.svg | 0 .../web}/media/light/numbus-server-light.svg | 0 .../media/light/numbus-template-light.svg | 0 .../GUI/web}/media/light/numbus-tv-light.svg | 0 {web => deploy/GUI/web}/media/logo.png | Bin .../GUI/web}/pages/configuration.html | 0 .../GUI/web}/pages/installation.html | 0 .../GUI/web}/pages/post-installation.html | 0 .../GUI/web}/pages/preparation.html | 0 deploy/GUI/web/ux/buttons.md | 22 + deploy/GUI/web/ux/color_palette.md | 10 + deploy/GUI/web/ux/css_guidelines.md | 8 + deploy/GUI/web/ux/text.md | 43 ++ deploy.sh => deploy/TUI/deploy.sh | 86 ++-- flake.nix | 74 ++++ modules/backup/default.nix | 13 + modules/backup/flake.nix | 11 + modules/backup/global.nix | 53 +++ modules/backup/hardware/boot.nix | 13 + modules/backup/hardware/cpu.nix | 9 + modules/backup/hardware/default.nix | 10 + modules/backup/hardware/disks.nix | 328 +++++++++++++++ modules/backup/hardware/pcie-coral.nix | 111 +++++ modules/backup/hardware/spindown.nix | 24 ++ modules/backup/mail/clamav.nix | 89 ++++ modules/backup/mail/default.nix | 10 + modules/backup/mail/smart.nix | 61 +++ modules/backup/mail/smtp.nix | 84 ++++ modules/backup/mail/systemd.nix | 55 +++ modules/backup/misc/default.nix | 10 + modules/backup/misc/internationalisation.nix | 24 ++ modules/backup/misc/power.nix | 30 ++ modules/backup/misc/update.nix | 23 ++ modules/backup/misc/users.nix | 16 + modules/backup/networking/default.nix | 8 + modules/backup/networking/firewall.nix | 13 + modules/backup/networking/networking.nix | 60 +++ modules/backup/packages/default.nix | 10 + modules/backup/packages/packages.nix | 25 ++ modules/backup/packages/podman.nix | 16 + modules/backup/packages/ssh.nix | 5 + modules/backup/packages/terminal.nix | 24 ++ .../backup/services/applications/authelia.nix | 171 ++++++++ .../backup/services/applications/crafty.nix | 74 ++++ .../backup/services/applications/dashy.nix | 97 +++++ .../backup/services/applications/default.nix | 29 ++ .../backup/services/applications/frigate.nix | 83 ++++ .../backup/services/applications/gitea.nix | 103 +++++ .../services/applications/home-assistant.nix | 177 ++++++++ .../backup/services/applications/homepage.nix | 63 +++ .../backup/services/applications/immich.nix | 190 +++++++++ .../backup/services/applications/it-tools.nix | 54 +++ .../backup/services/applications/jellyfin.nix | 69 ++++ .../backup/services/applications/lldap.nix | 84 ++++ modules/backup/services/applications/n8n.nix | 72 ++++ .../backup/services/applications/netbird.nix | 203 +++++++++ .../services/applications/netbootxyz.nix | 75 ++++ .../services/applications/nextcloud.nix | 384 ++++++++++++++++++ modules/backup/services/applications/ntfy.nix | 62 +++ modules/backup/services/applications/odoo.nix | 118 ++++++ .../backup/services/applications/passbolt.nix | 112 +++++ .../backup/services/applications/traefik.nix | 178 ++++++++ .../services/applications/uptime-kuma.nix | 54 +++ .../backup/services/applications/vscodium.nix | 81 ++++ modules/backup/services/dns/adguard.nix | 53 +++ modules/backup/services/dns/default.nix | 10 + modules/backup/services/dns/pi-hole.nix | 71 ++++ modules/backup/services/system/clamav.nix | 91 +++++ modules/backup/services/system/default.nix | 11 + .../backup/services/system/virtualization.nix | 17 + modules/common/default.nix | 12 + modules/common/global.nix | 42 ++ modules/common/hardware/cpu.nix | 9 + modules/common/hardware/default.nix | 9 + modules/common/hardware/disks/boot.nix | 192 +++++++++ modules/common/hardware/disks/content.nix | 107 +++++ modules/common/hardware/disks/default.nix | 12 + .../hardware/disks/mergerfs-snapraid.nix | 41 ++ modules/common/hardware/disks/mirror.nix | 75 ++++ modules/common/hardware/disks/parity.nix | 107 +++++ modules/common/hardware/disks/spindown.nix | 46 +++ modules/common/mail/default.nix | 10 + modules/common/mail/disk-space.nix | 130 ++++++ modules/common/mail/smart.nix | 61 +++ modules/common/mail/smtp.nix | 99 +++++ modules/common/misc/audio.nix | 15 + modules/common/misc/default.nix | 13 + modules/common/misc/internationalization.nix | 30 ++ modules/common/misc/power.nix | 12 + modules/common/misc/printer.nix | 8 + modules/common/misc/update.nix | 23 ++ modules/common/misc/users.nix | 20 + modules/common/packages/default.nix | 13 + modules/common/packages/fail2ban.nix | 5 + modules/common/packages/flatpaks.nix | 16 + modules/common/packages/numbus-cli.nix | 155 +++++++ modules/common/packages/ssh.nix | 21 + modules/common/packages/terminal.nix | 24 ++ modules/common/packages/updates.nix | 23 ++ modules/computer/default.nix | 9 + modules/computer/global.nix | 32 ++ modules/computer/hardware/boot.nix | 24 ++ modules/computer/hardware/cpu.nix | 9 + modules/computer/hardware/default.nix | 10 + modules/computer/hardware/disks.nix | 328 +++++++++++++++ modules/computer/hardware/nvidia.nix | 56 +++ modules/computer/misc/default.nix | 11 + modules/computer/misc/networking.nix | 12 + modules/computer/packages/default.nix | 10 + .../computer/packages/desktop-environment.nix | 75 ++++ modules/server/default.nix | 15 + modules/server/hardware/boot.nix | 11 + modules/server/hardware/default.nix | 12 + modules/server/hardware/pcie-coral.nix | 111 +++++ modules/server/mail/clamav.nix | 89 ++++ modules/server/mail/default.nix | 12 + modules/server/mail/systemd.nix | 55 +++ modules/server/networking/default.nix | 9 + modules/server/networking/firewall.nix | 13 + modules/server/networking/networking.nix | 60 +++ modules/server/packages/default.nix | 11 + modules/server/packages/packages.nix | 25 ++ modules/server/packages/podman.nix | 16 + modules/server/service-helper.nix | 181 +++++++++ .../server/services/applications/authelia.nix | 171 ++++++++ .../server/services/applications/crafty.nix | 74 ++++ .../server/services/applications/dashy.nix | 97 +++++ .../server/services/applications/default.nix | 29 ++ .../server/services/applications/frigate.nix | 83 ++++ .../server/services/applications/gitea.nix | 103 +++++ .../services/applications/home-assistant.nix | 177 ++++++++ .../server/services/applications/homepage.nix | 63 +++ .../server/services/applications/immich.nix | 190 +++++++++ .../server/services/applications/it-tools.nix | 54 +++ .../server/services/applications/jellyfin.nix | 69 ++++ .../server/services/applications/lldap.nix | 84 ++++ modules/server/services/applications/n8n.nix | 72 ++++ .../server/services/applications/netbird.nix | 203 +++++++++ .../services/applications/netbootxyz.nix | 75 ++++ .../services/applications/nextcloud.nix | 384 ++++++++++++++++++ modules/server/services/applications/ntfy.nix | 62 +++ modules/server/services/applications/odoo.nix | 118 ++++++ .../server/services/applications/passbolt.nix | 112 +++++ .../server/services/applications/traefik.nix | 178 ++++++++ .../services/applications/uptime-kuma.nix | 54 +++ .../server/services/applications/vscodium.nix | 81 ++++ modules/server/services/default.nix | 9 + modules/server/services/dns/adguard.nix | 53 +++ modules/server/services/dns/default.nix | 10 + modules/server/services/dns/pi-hole.nix | 71 ++++ modules/server/services/global.nix | 19 + .../server/services/system/backup-client.nix | 5 + modules/server/services/system/clamav.nix | 91 +++++ modules/server/services/system/default.nix | 10 + .../server/services/system/virtualization.nix | 17 + modules/tv/configuration.nix | 144 +++++++ modules/tv/flake.nix | 23 ++ modules/tv/flatpak.nix | 18 + .../configuration.nix | 0 .../flake.nix | 0 .../sops-nix/.sops.yaml | 0 .../sops-nix/secrets.yaml | 0 .../configuration.nix | 0 .../{numbus-computer => computer}/flake.nix | 0 .../sops-nix/.sops.yaml | 0 .../sops-nix/secrets.yaml | 0 .../{numbus-server => server}/.sops.yaml | 0 .../custom-configuration.nix | 0 templates/{numbus-server => server}/flake.nix | 0 .../numbus-generated.nix | 0 .../secrets/disks/boot.yaml | 0 .../secrets/disks/content.yaml | 0 .../secrets/disks/parity.yaml | 0 .../secrets/podman/adguard.yaml | 0 .../secrets/podman/authelia.yaml | 0 .../secrets/podman/crafty.yaml | 0 .../secrets/podman/dashy.yaml | 0 .../secrets/podman/frigate.yaml | 0 .../secrets/podman/gitea.yaml | 0 .../secrets/podman/gitlab.yaml | 0 .../secrets/podman/home-assistant.yaml | 0 .../secrets/podman/homepage.yaml | 0 .../secrets/podman/immich.yaml | 0 .../secrets/podman/it-tools.yaml | 0 .../secrets/podman/jellyfin.yaml | 0 .../secrets/podman/lldap.yaml | 0 .../secrets/podman/n8n.yaml | 0 .../secrets/podman/netbird.yaml | 0 .../secrets/podman/netbootxyz.yaml | 0 .../secrets/podman/netbox.yaml | 0 .../secrets/podman/nextcloud.yaml | 0 .../secrets/podman/ntfy.yaml | 0 .../secrets/podman/odoo.yaml | 0 .../secrets/podman/passbolt.yaml | 0 .../secrets/podman/pi-hole.yaml | 0 .../secrets/podman/traefik.yaml | 0 .../secrets/podman/uptime-kuma.yaml | 0 .../secrets/podman/vscodium.yaml | 0 .../secrets/system/backup-client.yaml | 0 .../secrets/system/clamav.yaml | 0 .../secrets/system/mail.yaml | 0 .../secrets/system/ssh.yaml | 0 .../secrets/system/virtualization.yaml | 0 templates/{numbus-tv => tv}/configuration.nix | 0 templates/{numbus-tv => tv}/flake.nix | 0 .../{numbus-tv => tv}/sops-nix/.sops.yaml | 0 .../{numbus-tv => tv}/sops-nix/secrets.yaml | 0 218 files changed, 9639 insertions(+), 57 deletions(-) rename {web => deploy/GUI/web}/index.html (100%) rename {web => deploy/GUI/web}/logic/bridge.py (100%) rename {web => deploy/GUI/web}/media/dark/numbus-backup-server-dark.svg (100%) rename {web => deploy/GUI/web}/media/dark/numbus-computer-dark.svg (100%) rename {web => deploy/GUI/web}/media/dark/numbus-server-dark.svg (100%) rename {web => deploy/GUI/web}/media/dark/numbus-template-dark.svg (100%) rename {web => deploy/GUI/web}/media/dark/numbus-tv-dark.svg (100%) rename {web => deploy/GUI/web}/media/favicon.ico (100%) rename {web => deploy/GUI/web}/media/light/numbus-backup-server-light.svg (100%) rename {web => deploy/GUI/web}/media/light/numbus-computer-light.svg (100%) rename {web => deploy/GUI/web}/media/light/numbus-server-light.svg (100%) rename {web => deploy/GUI/web}/media/light/numbus-template-light.svg (100%) rename {web => deploy/GUI/web}/media/light/numbus-tv-light.svg (100%) rename {web => deploy/GUI/web}/media/logo.png (100%) rename {web => deploy/GUI/web}/pages/configuration.html (100%) rename {web => deploy/GUI/web}/pages/installation.html (100%) rename {web => deploy/GUI/web}/pages/post-installation.html (100%) rename {web => deploy/GUI/web}/pages/preparation.html (100%) create mode 100644 deploy/GUI/web/ux/buttons.md create mode 100644 deploy/GUI/web/ux/color_palette.md create mode 100644 deploy/GUI/web/ux/css_guidelines.md create mode 100644 deploy/GUI/web/ux/text.md rename deploy.sh => deploy/TUI/deploy.sh (97%) create mode 100644 flake.nix create mode 100644 modules/backup/default.nix create mode 100644 modules/backup/flake.nix create mode 100644 modules/backup/global.nix create mode 100644 modules/backup/hardware/boot.nix create mode 100644 modules/backup/hardware/cpu.nix create mode 100644 modules/backup/hardware/default.nix create mode 100644 modules/backup/hardware/disks.nix create mode 100644 modules/backup/hardware/pcie-coral.nix create mode 100644 modules/backup/hardware/spindown.nix create mode 100644 modules/backup/mail/clamav.nix create mode 100644 modules/backup/mail/default.nix create mode 100644 modules/backup/mail/smart.nix create mode 100644 modules/backup/mail/smtp.nix create mode 100644 modules/backup/mail/systemd.nix create mode 100644 modules/backup/misc/default.nix create mode 100644 modules/backup/misc/internationalisation.nix create mode 100644 modules/backup/misc/power.nix create mode 100644 modules/backup/misc/update.nix create mode 100644 modules/backup/misc/users.nix create mode 100644 modules/backup/networking/default.nix create mode 100644 modules/backup/networking/firewall.nix create mode 100644 modules/backup/networking/networking.nix create mode 100644 modules/backup/packages/default.nix create mode 100644 modules/backup/packages/packages.nix create mode 100644 modules/backup/packages/podman.nix create mode 100644 modules/backup/packages/ssh.nix create mode 100644 modules/backup/packages/terminal.nix create mode 100644 modules/backup/services/applications/authelia.nix create mode 100644 modules/backup/services/applications/crafty.nix create mode 100644 modules/backup/services/applications/dashy.nix create mode 100644 modules/backup/services/applications/default.nix create mode 100644 modules/backup/services/applications/frigate.nix create mode 100644 modules/backup/services/applications/gitea.nix create mode 100644 modules/backup/services/applications/home-assistant.nix create mode 100644 modules/backup/services/applications/homepage.nix create mode 100644 modules/backup/services/applications/immich.nix create mode 100644 modules/backup/services/applications/it-tools.nix create mode 100644 modules/backup/services/applications/jellyfin.nix create mode 100644 modules/backup/services/applications/lldap.nix create mode 100644 modules/backup/services/applications/n8n.nix create mode 100644 modules/backup/services/applications/netbird.nix create mode 100644 modules/backup/services/applications/netbootxyz.nix create mode 100644 modules/backup/services/applications/nextcloud.nix create mode 100644 modules/backup/services/applications/ntfy.nix create mode 100644 modules/backup/services/applications/odoo.nix create mode 100644 modules/backup/services/applications/passbolt.nix create mode 100644 modules/backup/services/applications/traefik.nix create mode 100644 modules/backup/services/applications/uptime-kuma.nix create mode 100644 modules/backup/services/applications/vscodium.nix create mode 100644 modules/backup/services/dns/adguard.nix create mode 100644 modules/backup/services/dns/default.nix create mode 100644 modules/backup/services/dns/pi-hole.nix create mode 100644 modules/backup/services/system/clamav.nix create mode 100644 modules/backup/services/system/default.nix create mode 100644 modules/backup/services/system/virtualization.nix create mode 100644 modules/common/default.nix create mode 100644 modules/common/global.nix create mode 100644 modules/common/hardware/cpu.nix create mode 100644 modules/common/hardware/default.nix create mode 100644 modules/common/hardware/disks/boot.nix create mode 100644 modules/common/hardware/disks/content.nix create mode 100644 modules/common/hardware/disks/default.nix create mode 100644 modules/common/hardware/disks/mergerfs-snapraid.nix create mode 100644 modules/common/hardware/disks/mirror.nix create mode 100644 modules/common/hardware/disks/parity.nix create mode 100644 modules/common/hardware/disks/spindown.nix create mode 100644 modules/common/mail/default.nix create mode 100644 modules/common/mail/disk-space.nix create mode 100644 modules/common/mail/smart.nix create mode 100644 modules/common/mail/smtp.nix create mode 100644 modules/common/misc/audio.nix create mode 100644 modules/common/misc/default.nix create mode 100644 modules/common/misc/internationalization.nix create mode 100644 modules/common/misc/power.nix create mode 100644 modules/common/misc/printer.nix create mode 100644 modules/common/misc/update.nix create mode 100644 modules/common/misc/users.nix create mode 100644 modules/common/packages/default.nix create mode 100644 modules/common/packages/fail2ban.nix create mode 100644 modules/common/packages/flatpaks.nix create mode 100644 modules/common/packages/numbus-cli.nix create mode 100644 modules/common/packages/ssh.nix create mode 100644 modules/common/packages/terminal.nix create mode 100644 modules/common/packages/updates.nix create mode 100644 modules/computer/default.nix create mode 100644 modules/computer/global.nix create mode 100644 modules/computer/hardware/boot.nix create mode 100644 modules/computer/hardware/cpu.nix create mode 100644 modules/computer/hardware/default.nix create mode 100644 modules/computer/hardware/disks.nix create mode 100644 modules/computer/hardware/nvidia.nix create mode 100644 modules/computer/misc/default.nix create mode 100644 modules/computer/misc/networking.nix create mode 100644 modules/computer/packages/default.nix create mode 100644 modules/computer/packages/desktop-environment.nix create mode 100644 modules/server/default.nix create mode 100644 modules/server/hardware/boot.nix create mode 100644 modules/server/hardware/default.nix create mode 100644 modules/server/hardware/pcie-coral.nix create mode 100644 modules/server/mail/clamav.nix create mode 100644 modules/server/mail/default.nix create mode 100644 modules/server/mail/systemd.nix create mode 100644 modules/server/networking/default.nix create mode 100644 modules/server/networking/firewall.nix create mode 100644 modules/server/networking/networking.nix create mode 100644 modules/server/packages/default.nix create mode 100644 modules/server/packages/packages.nix create mode 100644 modules/server/packages/podman.nix create mode 100644 modules/server/service-helper.nix create mode 100644 modules/server/services/applications/authelia.nix create mode 100644 modules/server/services/applications/crafty.nix create mode 100644 modules/server/services/applications/dashy.nix create mode 100644 modules/server/services/applications/default.nix create mode 100644 modules/server/services/applications/frigate.nix create mode 100644 modules/server/services/applications/gitea.nix create mode 100644 modules/server/services/applications/home-assistant.nix create mode 100644 modules/server/services/applications/homepage.nix create mode 100644 modules/server/services/applications/immich.nix create mode 100644 modules/server/services/applications/it-tools.nix create mode 100644 modules/server/services/applications/jellyfin.nix create mode 100644 modules/server/services/applications/lldap.nix create mode 100644 modules/server/services/applications/n8n.nix create mode 100644 modules/server/services/applications/netbird.nix create mode 100644 modules/server/services/applications/netbootxyz.nix create mode 100644 modules/server/services/applications/nextcloud.nix create mode 100644 modules/server/services/applications/ntfy.nix create mode 100644 modules/server/services/applications/odoo.nix create mode 100644 modules/server/services/applications/passbolt.nix create mode 100644 modules/server/services/applications/traefik.nix create mode 100644 modules/server/services/applications/uptime-kuma.nix create mode 100644 modules/server/services/applications/vscodium.nix create mode 100644 modules/server/services/default.nix create mode 100644 modules/server/services/dns/adguard.nix create mode 100644 modules/server/services/dns/default.nix create mode 100644 modules/server/services/dns/pi-hole.nix create mode 100644 modules/server/services/global.nix create mode 100644 modules/server/services/system/backup-client.nix create mode 100644 modules/server/services/system/clamav.nix create mode 100644 modules/server/services/system/default.nix create mode 100644 modules/server/services/system/virtualization.nix create mode 100644 modules/tv/configuration.nix create mode 100644 modules/tv/flake.nix create mode 100644 modules/tv/flatpak.nix rename templates/{numbus-backup-server => backup}/configuration.nix (100%) rename templates/{numbus-backup-server => backup}/flake.nix (100%) rename templates/{numbus-backup-server => backup}/sops-nix/.sops.yaml (100%) rename templates/{numbus-backup-server => backup}/sops-nix/secrets.yaml (100%) rename templates/{numbus-computer => computer}/configuration.nix (100%) rename templates/{numbus-computer => computer}/flake.nix (100%) rename templates/{numbus-computer => computer}/sops-nix/.sops.yaml (100%) rename templates/{numbus-computer => computer}/sops-nix/secrets.yaml (100%) rename templates/{numbus-server => server}/.sops.yaml (100%) rename templates/{numbus-server => server}/custom-configuration.nix (100%) rename templates/{numbus-server => server}/flake.nix (100%) rename templates/{numbus-server => server}/numbus-generated.nix (100%) rename templates/{numbus-server => server}/secrets/disks/boot.yaml (100%) rename templates/{numbus-server => server}/secrets/disks/content.yaml (100%) rename templates/{numbus-server => server}/secrets/disks/parity.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/adguard.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/authelia.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/crafty.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/dashy.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/frigate.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/gitea.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/gitlab.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/home-assistant.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/homepage.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/immich.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/it-tools.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/jellyfin.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/lldap.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/n8n.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/netbird.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/netbootxyz.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/netbox.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/nextcloud.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/ntfy.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/odoo.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/passbolt.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/pi-hole.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/traefik.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/uptime-kuma.yaml (100%) rename templates/{numbus-server => server}/secrets/podman/vscodium.yaml (100%) rename templates/{numbus-server => server}/secrets/system/backup-client.yaml (100%) rename templates/{numbus-server => server}/secrets/system/clamav.yaml (100%) rename templates/{numbus-server => server}/secrets/system/mail.yaml (100%) rename templates/{numbus-server => server}/secrets/system/ssh.yaml (100%) rename templates/{numbus-server => server}/secrets/system/virtualization.yaml (100%) rename templates/{numbus-tv => tv}/configuration.nix (100%) rename templates/{numbus-tv => tv}/flake.nix (100%) rename templates/{numbus-tv => tv}/sops-nix/.sops.yaml (100%) rename templates/{numbus-tv => tv}/sops-nix/secrets.yaml (100%) diff --git a/.aiexclude b/.aiexclude index 1bd21c2..213602f 100644 --- a/.aiexclude +++ b/.aiexclude @@ -1,4 +1,5 @@ config/ +web/ux/ test* .DS_Store .env \ No newline at end of file diff --git a/.gitignore b/.gitignore index af6c9bb..213602f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -web/ux/ config/ +web/ux/ test* .DS_Store .env \ No newline at end of file diff --git a/web/index.html b/deploy/GUI/web/index.html similarity index 100% rename from web/index.html rename to deploy/GUI/web/index.html diff --git a/web/logic/bridge.py b/deploy/GUI/web/logic/bridge.py similarity index 100% rename from web/logic/bridge.py rename to deploy/GUI/web/logic/bridge.py diff --git a/web/media/dark/numbus-backup-server-dark.svg b/deploy/GUI/web/media/dark/numbus-backup-server-dark.svg similarity index 100% rename from web/media/dark/numbus-backup-server-dark.svg rename to deploy/GUI/web/media/dark/numbus-backup-server-dark.svg diff --git a/web/media/dark/numbus-computer-dark.svg b/deploy/GUI/web/media/dark/numbus-computer-dark.svg similarity index 100% rename from web/media/dark/numbus-computer-dark.svg rename to deploy/GUI/web/media/dark/numbus-computer-dark.svg diff --git a/web/media/dark/numbus-server-dark.svg b/deploy/GUI/web/media/dark/numbus-server-dark.svg similarity index 100% rename from web/media/dark/numbus-server-dark.svg rename to deploy/GUI/web/media/dark/numbus-server-dark.svg diff --git a/web/media/dark/numbus-template-dark.svg b/deploy/GUI/web/media/dark/numbus-template-dark.svg similarity index 100% rename from web/media/dark/numbus-template-dark.svg rename to deploy/GUI/web/media/dark/numbus-template-dark.svg diff --git a/web/media/dark/numbus-tv-dark.svg b/deploy/GUI/web/media/dark/numbus-tv-dark.svg similarity index 100% rename from web/media/dark/numbus-tv-dark.svg rename to deploy/GUI/web/media/dark/numbus-tv-dark.svg diff --git a/web/media/favicon.ico b/deploy/GUI/web/media/favicon.ico similarity index 100% rename from web/media/favicon.ico rename to deploy/GUI/web/media/favicon.ico diff --git a/web/media/light/numbus-backup-server-light.svg b/deploy/GUI/web/media/light/numbus-backup-server-light.svg similarity index 100% rename from web/media/light/numbus-backup-server-light.svg rename to deploy/GUI/web/media/light/numbus-backup-server-light.svg diff --git a/web/media/light/numbus-computer-light.svg b/deploy/GUI/web/media/light/numbus-computer-light.svg similarity index 100% rename from web/media/light/numbus-computer-light.svg rename to deploy/GUI/web/media/light/numbus-computer-light.svg diff --git a/web/media/light/numbus-server-light.svg b/deploy/GUI/web/media/light/numbus-server-light.svg similarity index 100% rename from web/media/light/numbus-server-light.svg rename to deploy/GUI/web/media/light/numbus-server-light.svg diff --git a/web/media/light/numbus-template-light.svg b/deploy/GUI/web/media/light/numbus-template-light.svg similarity index 100% rename from web/media/light/numbus-template-light.svg rename to deploy/GUI/web/media/light/numbus-template-light.svg diff --git a/web/media/light/numbus-tv-light.svg b/deploy/GUI/web/media/light/numbus-tv-light.svg similarity index 100% rename from web/media/light/numbus-tv-light.svg rename to deploy/GUI/web/media/light/numbus-tv-light.svg diff --git a/web/media/logo.png b/deploy/GUI/web/media/logo.png similarity index 100% rename from web/media/logo.png rename to deploy/GUI/web/media/logo.png diff --git a/web/pages/configuration.html b/deploy/GUI/web/pages/configuration.html similarity index 100% rename from web/pages/configuration.html rename to deploy/GUI/web/pages/configuration.html diff --git a/web/pages/installation.html b/deploy/GUI/web/pages/installation.html similarity index 100% rename from web/pages/installation.html rename to deploy/GUI/web/pages/installation.html diff --git a/web/pages/post-installation.html b/deploy/GUI/web/pages/post-installation.html similarity index 100% rename from web/pages/post-installation.html rename to deploy/GUI/web/pages/post-installation.html diff --git a/web/pages/preparation.html b/deploy/GUI/web/pages/preparation.html similarity index 100% rename from web/pages/preparation.html rename to deploy/GUI/web/pages/preparation.html diff --git a/deploy/GUI/web/ux/buttons.md b/deploy/GUI/web/ux/buttons.md new file mode 100644 index 0000000..f26c0bd --- /dev/null +++ b/deploy/GUI/web/ux/buttons.md @@ -0,0 +1,22 @@ +# Buttons + +1. Button with : +- an offset ring on focus +- a small shadow +- a nice transition +- a disabled feature + +```html +px-10 py-3 bg-fuchsia-600 rounded-xl font-bold shadow-lg shadow-fuchsia-600/30 transition duration-200 hover:bg-fuchsia-500 focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2 focus:ring-offset-slate-700 disabled:opacity-50 disabled:cursor-not-allowed +``` + +2. Button with : +- a grow transformation on hover +- a gradient +- a small shadow +- a nice transition +- a disabled feature + +```html +px-10 py-3 text-white font-bold rounded-xl scale-100 transition duration-200 ease-in bg-gradient-to-r from-fuchsia-500 via-fuchsia-600 to-fuchsia-700 hover:scale-105 hover:bg-gradient-to-br shadow-lg shadow-fuchsia-500/30 dark:shadow-lg dark:shadow-fuchsia-800/80 disabled:opacity-50 disabled:cursor-not-allowed +``` \ No newline at end of file diff --git a/deploy/GUI/web/ux/color_palette.md b/deploy/GUI/web/ux/color_palette.md new file mode 100644 index 0000000..bf8b63d --- /dev/null +++ b/deploy/GUI/web/ux/color_palette.md @@ -0,0 +1,10 @@ +# Color palette + +|Usage|Name|Tailwind code|Matching border| +|-|-|-|-| +|Body background|Slate|slate-900|N/A| +|Cards Background|Slate|slate-800|slate-700| +|Accent|Fuchsia|fuchsia-600|fuchsia-600 +|Warning|Amber|amber-500|amber-400| +|Success|Emerald|emerald-500|emerald-400| +|Failure|Rose|rose-600|rose-500| \ No newline at end of file diff --git a/deploy/GUI/web/ux/css_guidelines.md b/deploy/GUI/web/ux/css_guidelines.md new file mode 100644 index 0000000..da251a5 --- /dev/null +++ b/deploy/GUI/web/ux/css_guidelines.md @@ -0,0 +1,8 @@ +# Guidelines + +|Type|Importance|Classes| +|-|-|-| +|Title|1|text-4xl font-bold text-sky-400| +|Title|2|text-4xl font-bold text-sky-400| +|Title|3|text-4xl font-bold text-sky-400| +|Title|4|text-4xl font-bold text-sky-400| \ No newline at end of file diff --git a/deploy/GUI/web/ux/text.md b/deploy/GUI/web/ux/text.md new file mode 100644 index 0000000..3175b65 --- /dev/null +++ b/deploy/GUI/web/ux/text.md @@ -0,0 +1,43 @@ +# Taglines, subtitles and descriptions + +## Global + +**Tagline :** Reclaim Your Digital Independence. + +## Devices + +### Numbus Server +The Infrastructure + +**Tagline :** Reclaim Your Cloud. + +**Subtitle :** Professional-grade hosting, strictly kept under your roof. + +**Detailed Description :** The Numbus Server is the heartbeat of your digital sovereignty. Built on an immutable NixOS foundation, it provides a rock-solid environment to host the services you rely on—from Nextcloud and Passbolt to Home Assistant and Frigate. Whether you are a small business looking to internalize your data or a power user securing your smart home, the Numbus Server delivers containerized efficiency and VM flexibility without the "Big Tech" tax. It’s not just a server; it’s your private corner of the internet, accessible from anywhere but controlled only by you. + +### Numbus Backup Server +The Guardian + +**Tagline :** Your Digital Safety Net. + +**Subtitle :** Automated, high-efficiency protection for your entire ecosystem. + +**Detailed Description :** Data loss and downtime aren't just frustrating; they're disastrous. The Numbus Backup Server is your ecosystem's ultimate guardian. It works silently in the background, automatically pulling high-efficiency backups from your Numbus devices the moment they start charging. But it does more than just store files—it actively acts as a watchdog. By constantly monitoring your Numbus Server and other critical network devices, it immediately alerts you the second something goes offline. It is automated peace of mind for your entire digital infrastructure. + +### Numbus Computer +The Workstation + +**Tagline :** The No-Bullshit Workstation. + +**Subtitle :** A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat. + +**Description :** Experience computing the way it was meant to be: entirely under your control. The Numbus Computer is a powerful workstation that strips away unwanted telemetry, forced updates, and bundled adware. Under the hood, it harnesses the unparalleled stability of NixOS, but we've hidden the complexity behind a sleek, intuitive interface. Our default apps make managing drivers, software, and system options a breeze. Whether you are coding, browsing, or gaming via Proton, the Numbus Computer delivers a snappy, secure, and truly "libre" desktop experience that respects your privacy while delivering the raw power of the hardware you paid for. + +### Numbus TV +The Experience + +**Tagline :** Entertainment, Liberated. + +**Subtitle :** A premium cinematic experience free from trackers and forced subscriptions. + +**Detailed Description :** The Numbus TV reclaims your living room. Unlike "Smart" TVs that have essentially become surveillance machines and clutter your screen with unwanted ads, the Numbus TV offers a clean, classy interface powered by KDE Bigscreen. It’s a full-power PC in a TV’s body, allowing you to stream, browse, and play on your own terms. It doesn't force you into a specific ecosystem; it simply provides a beautiful, privacy-hardened portal to your favorite media. No spying, no "walled gardens"—just the big screen experience, perfected. \ No newline at end of file diff --git a/deploy.sh b/deploy/TUI/deploy.sh similarity index 97% rename from deploy.sh rename to deploy/TUI/deploy.sh index 243fce8..3c5b5b4 100755 --- a/deploy.sh +++ b/deploy/TUI/deploy.sh @@ -15,78 +15,50 @@ echod() { ssh_to_host() { local COMMAND="${1}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}" -} +}"Invalid IP address format." get_valid_input() { local VAR_NAME="${1}" local HEADER="${2}" local PLACEHOLDER="${3}" local REGEX="${4}" - local MANDATORY="${5:-false}" + local MANDATORY="${5:-true}" local SENSITIVE="${6:-false}" + if [[ "${MANDATORY}" == "true" ]]; then + local PROMPT="(Required) > " + elif [[ "${MANDATORY}" == "false" ]]; then + local PROMPT="(Optional) > " + fi + while true; do - local input - # --value pre-fills the input for easy editing - input=$(gum input --header "${HEADER}" --placeholder "${PLACEHOLDER}" --width 50) - + local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}") + # Handle empty input - if [[ -z "$input" ]]; then - if [[ "$is_mandatory" == true ]]; then + if [[ -z "${INPUT}" ]]; then + if [[ "${MANDATORY}" == true ]]; then gum style --foreground "#ff0000" -- "✖ This field is mandatory." continue else - return_var="" + INPUT="" break fi fi - + # Handle Regex Validation - if [[ -n "$regex" ]]; then - if [[ "$input" =~ $regex ]]; then - return_var="$input" + if [[ -n "${REGEX}" ]]; then + if [[ "${INPUT}" =~ ${REGEX} ]]; then + export "${VAR_NAME}"="${INPUT}" break else gum style --foreground "#ff0000" -- "✖ Invalid format. Please try again." fi else - return_var="$input" + export "${VAR_NAME}"="${INPUT}" break fi done } - -user_input() { - local VAR_NAME="${1}" - local HEADER="${2}" - local PLACEHOLDER="${3}" - local REGEX="${4}" - local ERROR_MSG="${5}" - local SENSITIVE="${6:-false}" - - echo "" - gum style --foreground 212 --bold "${HEADER}" - - while true; do - [[ "$SENSITIVE" == "false" ]] && INPUT_VALUE=$(gum input --placeholder "${PLACEHOLDER}") - [[ "$SENSITIVE" == "true" ]] && INPUT_VALUE=$(gum input --password --placeholder "${PLACEHOLDER}") - - if [[ -z "${INPUT_VALUE}" ]]; then - echo "❌ Error: Input cannot be empty. Please provide the necessary information." - continue - fi - - if [[ -n "${REGEX}" ]]; then - if [[ ! "${INPUT_VALUE}" =~ ${REGEX} ]]; then - echo "❌ Error: ${ERROR_MSG}" - continue - fi - fi - - export "${VAR_NAME}"="${INPUT_VALUE}" - break - done -} # --- UTILITY FUNCTIONS ---< @@ -364,32 +336,31 @@ preparation() { gum confirm "Is the device ready ?" || { echo "❌ You need to prepare the device. The script cannot continue."; exit 1; } # LIVE TARGET SETTINGS - user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" "Invalid IP address format." + user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" user_input "LIVE_TARGET_PASSWORD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true" # INTERNATIONALIZATION SETTINGS - user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc" "" "" - user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc" "" "" - user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc" "" "" + user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc" + user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc" + user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc" } configuration() { if [[ "${DEVICE_TYPE}" == "server" ]]; then - # Users & Groups - user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve" "" "" - user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}" "Invalid email address format." + user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve" + user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}" user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide the SSH public key of an authorized device (or a comma-separated list) :" "For example : ssh-ed25519 AAAAC3Nzam0uYewNAbxL8Fci8 user@your-pc or ssh-* * *, ssh-* * *, etc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." echo -e "\n\n ➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains" # TRAEFIK SETTINGS - user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}" "Invalid domain name format." + user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}" user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true" echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)" # SMTP SETTINGS - user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format." + user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true" user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format." user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number." @@ -1343,7 +1314,10 @@ PORT_REGEX='^[0-9]{1,5}$' SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*' PHONE_REGEX='^\+[1-9][0-9]{7,14}$' - +GUM_INPUT_PADDING="1 1" +GUM_INPUT_HEADER_FOREGROUND="212" +GUM_INPUT_CURSOR_FOREGROUND="212" +GUM_INPUT_TIMEOUT="3600" # --- DEFAULTS VARIABLES ---< diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ad30c1b --- /dev/null +++ b/flake.nix @@ -0,0 +1,74 @@ +{ + description = "Numbus - Simplified NixOS deployments"; + + inputs = { + # Nix unstable packages + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + # Nix stable packages + nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-25.11"; + # Numbus + numbus.url = "https://gittea.dev/numbus/numbus"; + # Disko + disko.url = "github:nix-community/disko"; + disko.inputs.nixpkgs.follows = "nixpkgs"; + # Sops-nix + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "nixpkgs"; + # Flatpaks + nix-flatpak.url = "github:gmodena/nix-flatpak"; + }; + + outputs = { self, nixpkgs, ... }@inputs: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + # Helper for defining systems + mkNumbus = { deviceModule, extraModules ? [], nixpkgsRef, deviceType }: nixpkgsRef.lib.nixosSystem { + inherit system; + specialArgs = { inherit inputs deviceType; }; + modules = [ + inputs.disko.nixosModules.disko + inputs.sops-nix.nixosModules.sops + self.nixosModules.common + deviceModule + ] ++ extraModules; + }; + in { + nixosModules = { + common = ./modules/common; + server = ./modules/server; + backup = ./modules/backup; + computer = ./modules/computer; + tv = ./modules/tv; + }; + + nixosConfigurations = { + numbus-server = mkNumbus { + deviceModule = self.nixosModules.server; + nixpkgsRef = inputs.nixpkgs-stable; + deviceType = "server"; + }; + + numbus-backup = mkNumbus { + deviceModule = self.nixosModules.backup; + nixpkgsRef = inputs.nixpkgs-stable; + deviceType = "backup"; + }; + + numbus-computer = mkNumbus { + deviceModule = self.nixosModules.computer; + extraModules = [ inputs.nix-flatpak.nixosModules.nix-flatpak ]; + nixpkgsRef = inputs.nixpkgs; + deviceType = "computer"; + }; + + numbus-tv = mkNumbus { + deviceModule = self.nixosModules.tv; + nixpkgsRef = inputs.nixpkgs; + extraModules = [ inputs.nix-flatpak.nixosModules.nix-flatpak ]; + deviceType = "tv"; + }; + }; + }; +} diff --git a/modules/backup/default.nix b/modules/backup/default.nix new file mode 100644 index 0000000..b64d5e5 --- /dev/null +++ b/modules/backup/default.nix @@ -0,0 +1,13 @@ +{ ... }: + +{ + imports = [ + ./hardware/default.nix + ./mail/default.nix + ./misc/default.nix + ./networking/default.nix + ./packages/default.nix + ./services/default.nix + ./global.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/flake.nix b/modules/backup/flake.nix new file mode 100644 index 0000000..04c8219 --- /dev/null +++ b/modules/backup/flake.nix @@ -0,0 +1,11 @@ +{ + description = "Numbus Server Module"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + }; + + outputs = { self, nixpkgs }: { + nixosModules.numbus = import ./modules/default.nix; + }; +} \ No newline at end of file diff --git a/modules/backup/global.nix b/modules/backup/global.nix new file mode 100644 index 0000000..8cf2c1a --- /dev/null +++ b/modules/backup/global.nix @@ -0,0 +1,53 @@ +{ lib, ... }: + +with lib; + +{ + options.numbus = { + owner = mkOption { + type = types.str; + example = "Alex"; + default = "Numbus"; + description = "The name of the person who owns this server"; + }; + language = mkOption { + type = types.str; + example = "FR"; + default = "FR"; + description = "The language for this server"; + }; + locale = mkOption { + type = types.str; + example = "fr_FR"; + default = "fr_FR"; + description = "The default locale for this server"; + }; + + services = { + domain = mkOption { + type = types.str; + example = "numbus.eu"; + description = "The root domain name (i.e. example.com) that your services will use"; + }; + dns = mkOption { + type = types.enum [ "pi-hole" "adguard" ]; + default = "pi-hole"; + example = "pi-hole"; + description = "The preferred DNS resolver service (pi-hole or adguard) that other services should depend on"; + }; + }; + + email = { + administratorEmail = mkOption { + type = types.str; + example = "admin@your-domain.com"; + description = "The email that will be used to send critical notifications such as hardware failures, services errors, ACME updates, etc"; + }; + userEmail = mkOption { + type = types.str; + example = "user@your-domain.com"; + description = "The email that will be used by services to send notifications"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/hardware/boot.nix b/modules/backup/hardware/boot.nix new file mode 100644 index 0000000..86de227 --- /dev/null +++ b/modules/backup/hardware/boot.nix @@ -0,0 +1,13 @@ +{ config, ... }: + +{ + config = { + boot.initrd.systemd.enable = true; + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + boot.kernel.sysctl = { + "vm.overcommit_memory" = 1; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/hardware/cpu.nix b/modules/backup/hardware/cpu.nix new file mode 100644 index 0000000..d3fe470 --- /dev/null +++ b/modules/backup/hardware/cpu.nix @@ -0,0 +1,9 @@ +{ config, ... }: + +{ + config = { + hardware.enableRedistributableFirmware = true; + hardware.cpu.intel.updateMicrocode = true; + hardware.cpu.amd.updateMicrocode = true; + }; +} \ No newline at end of file diff --git a/modules/backup/hardware/default.nix b/modules/backup/hardware/default.nix new file mode 100644 index 0000000..8a652d9 --- /dev/null +++ b/modules/backup/hardware/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + ./boot.nix + ./cpu.nix + ./disks.nix + ./pcie-coral.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/hardware/disks.nix b/modules/backup/hardware/disks.nix new file mode 100644 index 0000000..6e1acee --- /dev/null +++ b/modules/backup/hardware/disks.nix @@ -0,0 +1,328 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus.hardware; + + mkDataDisk = idx: device: { + name = "content-${toString idx}"; + value = { + type = "disk"; + device = device; + content = { + type = "gpt"; + partitions = { + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-content-${toString idx}"; + initrdUnlock = false; + settings = { + keyFile = "/etc/secrets/disks/content-${toString idx}"; + allowDiscards = true; + crypttabExtraOpts = [ "nofail" ]; + }; + content = { + type = "filesystem"; + format = cfg.dataDisksFilesystem; + mountpoint = "/mnt/content-${toString idx}"; + mountOptions = [ "nofail" "noauto" ]; + }; + }; + }; + }; + }; + }; + }; + + mkParityDisk = idx: device: { + name = "parity-${toString idx}"; + value = { + type = "disk"; + device = device; + content = { + type = "gpt"; + partitions = { + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-parity-${toString idx}"; + initrdUnlock = false; + settings = { + keyFile = "/etc/secrets/disks/parity-${toString idx}"; + allowDiscards = true; + crypttabExtraOpts = [ "nofail" ]; + }; + content = { + type = "filesystem"; + format = cfg.parityDisksFilesystem; + mountpoint = "/mnt/parity-${toString idx}"; + mountOptions = [ "nofail" "noauto" ]; + }; + }; + }; + }; + }; + }; + }; + + isMirror = length cfg.bootDisksList > 1; + + bootDisksConfig = if isMirror then { + mdadm = { + boot = { + type = "mdadm"; + level = 1; + metadata = "1.2"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + }; + disk = listToAttrs (imap1 (i: device: { + name = "boot-${toString i}"; + value = { + type = "disk"; + device = device; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "1G"; + type = "EF00"; + content = { + type = "mdraid"; + name = "boot"; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-boot-${toString i}"; + settings = { + allowDiscards = true; + keyFile = "/etc/secrets/disks/boot-${toString i}"; + }; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + }) cfg.bootDisksList); + } else { + disk = { + "boot-1" = { + type = "disk"; + device = head cfg.bootDisksList; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "1G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-boot-1"; + settings = { + keyFile = "/etc/secrets/disks/boot-1"; + allowDiscards = true; + }; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + }; + }; + + lvmConfig = { + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + swap = { + size = cfg.swapSize; + content = { + type = "swap"; + }; + } // optionalAttrs isMirror { lvm_type = "mirror"; }; + snapraid = { + size = "1G"; + content = { + type = "filesystem"; + format = "btrfs"; + mountpoint = "/mnt/content-0"; + }; + } // optionalAttrs isMirror { lvm_type = "mirror"; }; + root = { + size = "100%"; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; + subvolumes = { + "/rootfs" = { + mountpoint = "/"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "/home" = { + mountpoint = "/home"; + mountOptions = [ "compress=zstd" ]; + }; + "/nix" = { + mountpoint = "/nix"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + }; + }; + } // optionalAttrs isMirror { lvm_type = "mirror"; }; + }; + }; + }; + }; + +in +{ + options.numbus.hardware = { + dataDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; + description = "List by-id path of devices for data disks"; + }; + parityDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; + description = "List of by-id path of devices for parity disks"; + }; + bootDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/nvme_SAMSUNG_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ]; + description = "List of by-id path of devices for boot disks"; + }; + spindownDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; + description = "List of by-id path of devices to spindown when inactive to save power (HDD only)"; + }; + swapSize = mkOption { + type = types.str; + default = "16G"; + example = "16G"; + description = "Size of the swap partition"; + }; + dataDisksFilesystem = mkOption { + type = types.enum [ "xfs" "ext4" "btrfs" ]; + default = "xfs"; + example = "xfs"; + description = "Filesystem for data disks. Available filesystem options : xfs, ext4, btrfs"; + }; + parityDisksFilesystem = mkOption { + type = types.enum [ "xfs" "ext4" "btrfs" ]; + default = "xfs"; + example = "xfs"; + description = "Filesystem for parity disks. Available filesystem options : xfs, ext4, btrfs"; + }; + }; + + config = mkIf (cfg.bootDisksList != []) { + disko.devices = mkMerge [ + bootDisksConfig + lvmConfig + { + disk = listToAttrs (imap1 mkDataDisk cfg.dataDisksList); + } + { + disk = listToAttrs (imap1 mkParityDisk cfg.parityDisksList); + } + ]; + + services.snapraid = { + enable = true; + contentFiles = [ "/mnt/content-0/snapraid.content" ] ++ + (map (i: "/mnt/content-${toString i}/snapraid.content") (range 1 (length cfg.dataDisksList))); + parityFiles = map (i: "/mnt/parity-${toString i}/snapraid.parity") (range 1 (length cfg.parityDisksList)); + dataDisks = listToAttrs (imap1 (i: _: nameValuePair "d${toString i}" "/mnt/content-${toString i}") cfg.dataDisksList); + }; + + fileSystems."/mnt/data" = { + device = "/mnt/content-*"; + fsType = "fuse.mergerfs"; + options = [ + "category.create=ff" + "cache.files=partial" + "dropcacheonclose=true" + "defaults" + "noauto" + "nofail" + "allow_other" + "moveonenospc=1" + "minfreespace=50G" + "func.getattr=newest" + "fsname=mergerfs_data" + "x-mount.mkdir" + "x-systemd.automount" + ]; + }; + + systemd.services.mount-disks = { + description = "Mount data and parity disks"; + before = [ "mnt-data.mount" ]; + requiredBy = [ "mnt-data.mount" ]; + path = [ pkgs.cryptsetup pkgs.util-linux ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = let + mountDataDisk = i: '' + if [ ! -e /dev/mapper/crypted-content-${toString i} ]; then + cryptsetup luksOpen --key-file /etc/secrets/disks/content-${toString i} /dev/disk/by-partlabel/disk-content-${toString i}-luks crypted-content-${toString i} + fi + mkdir -p /mnt/content-${toString i} + if ! mountpoint -q /mnt/content-${toString i}; then + mount -t ${cfg.dataDisksFilesystem} /dev/mapper/crypted-content-${toString i} /mnt/content-${toString i} + fi + ''; + mountParityDisk = i: '' + if [ ! -e /dev/mapper/crypted-parity-${toString i} ]; then + cryptsetup luksOpen --key-file /etc/secrets/disks/parity-${toString i} /dev/disk/by-partlabel/disk-parity-${toString i}-luks crypted-parity-${toString i} + fi + mkdir -p /mnt/parity-${toString i} + if ! mountpoint -q /mnt/parity-${toString i}; then + mount -t ${cfg.parityDisksFilesystem} /dev/mapper/crypted-parity-${toString i} /mnt/parity-${toString i} + fi + ''; + in '' + ${concatMapStrings mountDataDisk (range 1 (length cfg.dataDisksList))} + ${concatMapStrings mountParityDisk (range 1 (length cfg.parityDisksList))} + ''; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/hardware/pcie-coral.nix b/modules/backup/hardware/pcie-coral.nix new file mode 100644 index 0000000..06fe328 --- /dev/null +++ b/modules/backup/hardware/pcie-coral.nix @@ -0,0 +1,111 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.numbus.hardware.pcie-coral; + + gasket-driver = { stdenv, lib, fetchFromGitHub, kernel }: stdenv.mkDerivation rec { + pname = "gasket"; + version = "1.0-18"; + + src = fetchFromGitHub { + owner = "google"; + repo = "gasket-driver"; + rev = "97aeba584efd18983850c36dcf7384b0185284b3"; + sha256 = "pJwrrI7jVKFts4+bl2xmPIAD01VKFta2SRuElerQnTo="; + }; + + makeFlags = [ + "-C" + "${kernel.dev}/lib/modules/${kernel.modDirVersion}/build" + "M=$(PWD)" + ]; + buildFlags = [ "modules" ]; + + installFlags = [ "INSTALL_MOD_PATH=${placeholder "out"}" ]; + installTargets = [ "modules_install" ]; + + sourceRoot = "source/src"; + hardeningDisable = [ "pic" "format" ]; + nativeBuildInputs = kernel.moduleBuildDependencies; + + meta = with lib; { + description = "The Coral Gasket Driver allows usage of the Coral EdgeTPU on Linux systems."; + homepage = "https://github.com/google/gasket-driver"; + license = licenses.gpl2; + maintainers = [ maintainers.kylehendricks ]; + platforms = platforms.linux; + }; + }; + + libedgetpu-pkg = { stdenv, lib, fetchFromGitHub, libusb1, abseil-cpp, flatbuffers, xxd }: + let + flatbuffers_1_12 = flatbuffers.overrideAttrs (oldAttrs: rec { + version = "1.12.0"; + NIX_CFLAGS_COMPILE = "-Wno-error=class-memaccess -Wno-error=maybe-uninitialized"; + cmakeFlags = (oldAttrs.cmakeFlags or []) ++ ["-DFLATBUFFERS_BUILD_SHAREDLIB=ON"]; + NIX_CXXSTDLIB_COMPILE = "-std=c++17"; + configureFlags = (oldAttrs.configureFlags or []) ++ ["--enable-shared"]; + src = fetchFromGitHub { + owner = "google"; + repo = "flatbuffers"; + rev = "v${version}"; + sha256 = "sha256-L1B5Y/c897Jg9fGwT2J3+vaXsZ+lfXnskp8Gto1p/Tg="; + }; + }); + + in stdenv.mkDerivation rec { + pname = "libedgetpu"; + version = "grouper"; + + src = fetchFromGitHub { + owner = "google-coral"; + repo = pname; + rev = "release-${version}"; + sha256 = "sha256-73hwItimf88Iqnb40lk4ul/PzmCNIfdt6Afi+xjNiBE="; + }; + + makeFlags = ["-f" "makefile_build/Makefile" "libedgetpu" ]; + + buildInputs = [ + libusb1 + abseil-cpp + flatbuffers_1_12 + ]; + + nativeBuildInputs = [ + xxd + ]; + + NIX_CXXSTDLIB_COMPILE = "-std=c++17"; + + TFROOT = "${fetchFromGitHub { + owner = "tensorflow"; + repo = "tensorflow"; + rev = "v2.7.4"; + sha256 = "sha256-liDbUAdaVllB0b74aBeqNxkYNu/zPy7k3CevzRF5dk0="; + }}"; + + enableParallelBuilding = false; + + installPhase = '' + mkdir -p $out/lib + cp out/direct/k8/libedgetpu.so.1.0 $out/lib + ln -s $out/lib/libedgetpu.so.1.0 $out/lib/libedgetpu.so.1 + mkdir -p $out/lib/udev/rules.d + cp debian/edgetpu-accelerator.rules $out/lib/udev/rules.d/99-edgetpu-accelerator.rules + ''; + }; + + gasket = config.boot.kernelPackages.callPackage gasket-driver {}; + libedgetpu = pkgs.callPackage libedgetpu-pkg {}; +in + +{ + options.numbus.hardware.pcie-coral = lib.mkEnableOption "PCIe Coral TPU support"; + + config = lib.mkIf cfg { + services.udev.packages = [ libedgetpu ]; + users.groups.plugdev = {}; + boot.extraModulePackages = [ gasket ]; + }; +} \ No newline at end of file diff --git a/modules/backup/hardware/spindown.nix b/modules/backup/hardware/spindown.nix new file mode 100644 index 0000000..219d113 --- /dev/null +++ b/modules/backup/hardware/spindown.nix @@ -0,0 +1,24 @@ +{ config, lib, ... }: + +with lib; + +{ + options.numbus-backup = { + hardware = { + HddSpindown = { + enable = mkOption { + description = "Spin down Hard drives when inactive in order to save power."; + type = types.bool; + example = true; + default = true; + }; + optimize = mkOption { + description = "Optimize services to reduce HDD wakeups when HddSpindown is enabled. Can be set to \"compatible\" to optimize all compatible services, or a list of service names to optimize."; + type = types.nullOr (types.either (types.enum [ "compatible" ]) (types.listOf types.str)); + default = "compatible"; + example = "[ \"crafty\" \"gitea\" ]"; + }; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/mail/clamav.nix b/modules/backup/mail/clamav.nix new file mode 100644 index 0000000..a4c99bc --- /dev/null +++ b/modules/backup/mail/clamav.nix @@ -0,0 +1,89 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus.services.clamav; + clamav_notifier = pkgs.writeScript "clamav-notify.sh" '' + #!${pkgs.bash}/bin/bash + + # Check if triggered by Real-time event (file exists) + if [ -f /var/lib/clamav/virus_event.env ]; then + source /var/lib/clamav/virus_event.env + rm /var/lib/clamav/virus_event.env + fi + + ADMIN_EMAIL="${config.numbus.mail.adminAddress}" + USER_EMAIL="${config.numbus.mail.userAddress}" + OWNER_NAME="${config.numbus.owner}" + + if [ -n "$CLAM_VIRUSEVENT_VIRUSNAME" ]; then + # --- Real-time / VirusEvent Mode --- + SUBJECT="Numbus Server Alert: Virus Detected (Real-time)" + + # Retrieve logs from clamav-daemon + LOGS=$(journalctl -u clamav-daemon.service -n 50 --no-pager | grep "FOUND") + + TECH_BODY=" + ClamAV Real-time Alert: + Server owner: $OWNER_NAME + + Virus detected: $CLAM_VIRUSEVENT_VIRUSNAME + File: $CLAM_VIRUSEVENT_FILENAME + + Logs: + $LOGS + + Action taken: Access blocked (OnAccessPrevention). + Please investigate manually. + " + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + L'antivirus de votre serveur a détecté et bloqué une menace en temps réel. + Fichier : $CLAM_VIRUSEVENT_FILENAME + + Votre administrateur a été notifié. + " + else + # --- Scheduled Scan Summary Mode --- + SUBJECT="Numbus Server Alert: Virus Detected during Scheduled Scan" + + # Retrieve logs (clamdscan prints FOUND when a virus is detected) + LOGS=$(journalctl -u clamav-periodic-scan.service -n 100 --no-pager | grep "FOUND") + + TECH_BODY=" + ClamAV Scan Alert: + Server owner: $OWNER_NAME + + Viruses detected: + $LOGS + + Action taken: Detection only. + Please investigate manually. + " + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + L'antivirus de votre serveur a détecté une menace potentielle lors de l'analyse périodique. + Votre administrateur a été notifié avec les détails techniques. + Nous vous conseillons d'être prudent avec vos fichiers récents. + " + fi + + printf "Subject: [ADMIN] %s\n\n%s" "$SUBJECT" "$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" + printf "Subject: [Alerte] Menace détectée sur votre serveur Numbus\n\n%s\n\nMerci de votre confiance,\nL'équipe de support,\nNumbus-Server." "$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" + ''; +in + +{ + config = mkIf cfg.enable { + systemd.services.clamav-virus-notify = { + description = "Email notification for ClamAV virus detection"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${clamav_notifier}"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/mail/default.nix b/modules/backup/mail/default.nix new file mode 100644 index 0000000..a7af913 --- /dev/null +++ b/modules/backup/mail/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + ./clamav.nix + ./smart.nix + ./systemd.nix + ./smtp.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/mail/smart.nix b/modules/backup/mail/smart.nix new file mode 100644 index 0000000..c84e0ab --- /dev/null +++ b/modules/backup/mail/smart.nix @@ -0,0 +1,61 @@ +{ config, pkgs, ... }: + +let + smartd_notifier = pkgs.writeScript "smartd-notify.sh" '' + #!${pkgs.bash}/bin/bash + + # 1. Send Technical Email to Admin + ADMIN_EMAIL="${config.numbus.mail.adminAddress}" + SUBJECT="Numbus Server Alert: $SMARTD_FAILTYPE on $SMARTD_DEVICE" + + TECH_BODY=" + SMARTD Alert Details: + Server owner: $OWNER_NAME + Device: $SMARTD_DEVICE + Type: $SMARTD_DEVICETYPE + Failure Type: $SMARTD_FAILTYPE + Message: $SMARTD_MESSAGE + + Full Message: + $SMARTD_FULLMESSAGE + " + printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" + + # 2. Send Friendly Email to Owner + USER_EMAIL="${config.numbus.mail.userAddress}" + OWNER_NAME="${config.numbus.owner}" + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + Votre serveur a automatiquement détecté une panne matérielle de disque dur. + Ce genre de panne est tout à fait normal selon l'âge de votre matériel et n'entraîne + dans la grande majorité des cas aucune perte de données grâce au système de + stockage redondant préventif. + + Votre administrateur a été notifié de cette panne. Il vous recontactera dans de très + brefs délais afin de procéder au remplacement, si nécessaire, du disque dur défaillant. + + Merci de votre confiance, + L'équipe de support, + Numbus-Server." + + printf "Subject: [Alerte] Défaillance matérielle sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" + ''; +in + +{ + services.smartd = { + enable = true; + defaults.autodetected = "-a -o on -S on -s (S/../.././00|L/../../6/01) -n standby,q -M exec ${smartd_notifier}"; + notifications = { + wall = { + enable = true; + }; + mail = { + enable = true; + sender = config.numbus.mail.fromAddress; + recipient = "${config.numbus.mail.userAddress},${config.numbus.mail.adminAddress}"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/mail/smtp.nix b/modules/backup/mail/smtp.nix new file mode 100644 index 0000000..55b84f5 --- /dev/null +++ b/modules/backup/mail/smtp.nix @@ -0,0 +1,84 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.numbus.mail; +in + +{ + options.numbus.mail = { + enable = mkEnableOption "Email sending functionality"; + + userAddress = mkOption { + description = "The address of the user this server will send emails to"; + type = types.str; + example = "user@your-domain.com"; + }; + + adminAddress = mkOption { + description = "The address of the admin this server will send emails to"; + type = types.str; + example = "admin@your-domain.com"; + }; + + smtpUsername = mkOption { + description = "The username/email that will be use to authenticate to the SMTP server"; + type = types.str; + example = "your-smtp-enabled-address@your-domain.com"; + }; + + smtpPasswordPath = mkOption { + description = "The path to a file containing the password that will be use to authenticate to the SMTP server"; + type = types.path; + example = /run/secrets/smtp-password; + }; + + fromAddress = mkOption { + description = "This server will send emails from this address"; + type = types.str; + default = "numbus-server-noreply@${config.numbus.services.domain}"; + example = "numbus-server-noreply@your-domain.com"; + }; + + smtpServer = mkOption { + description = "The SMTP server address your server will use to send emails"; + type = types.str; + default = "smtp.gmail.com"; + example = "smtp.your-provider.com"; + }; + + smtpPort = mkOption { + description = "The SMTP port your server will connect to to send emails"; + type = types.port; + default = 587; + example = 587; + }; + }; + + config = mkIf cfg.enable { + environment.etc."aliases".text = '' + root: ${cfg.userAddress}, ${cfg.adminAddress} + default: ${cfg.userAddress}, ${cfg.adminAddress} + ''; + + programs.msmtp = { + enable = true; + defaults = { + aliases = "/etc/aliases"; + timeout = 60; + syslog = "on"; + }; + accounts.default = { + auth = true; + host = cfg.smtpServer; + port = cfg.smtpPort; + from = cfg.fromAddress; + user = cfg.smtpUsername; + tls = true; + tls_starttls = true; + passwordeval = "${pkgs.coreutils}/bin/cat ${cfg.smtpPasswordPath}"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/mail/systemd.nix b/modules/backup/mail/systemd.nix new file mode 100644 index 0000000..0b7a566 --- /dev/null +++ b/modules/backup/mail/systemd.nix @@ -0,0 +1,55 @@ +{ config, pkgs, ... }: + +let + systemd_notifier = pkgs.writeScript "systemd-email-notify.sh" '' + #!${pkgs.bash}/bin/bash + + # The failing service name is passed as the first argument + UNIT=$1 + + # 1. Send Technical Email to Admin + ADMIN_EMAIL="${config.numbus.mail.adminAddress}" + SUBJECT="Numbus Server Alert: Service $UNIT Failed" + + # Retrieve recent logs for context + LOGS=$(journalctl -u "$UNIT" -n 20 --no-pager) + + TECH_BODY=" + Systemd Service Failure Alert: + Server owner: ${config.numbus.owner} + Service: $UNIT + + Recent Logs: + $LOGS + " + printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" + + # 2. Send Friendly Email to Owner + USER_EMAIL="${config.numbus.mail.userAddress}" + OWNER_NAME="${config.numbus.owner}" + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + Votre serveur a détecté une défaillance du service $UNIT. + Le système a tenté de gérer l'erreur, mais une intervention peut être nécessaire. + + Votre administrateur a été notifié de cet incident avec les détails techniques nécessaires. + Il interviendra si une action manuelle est requise. + + Merci de votre confiance, + L'équipe de support, + Numbus-Server." + + printf "Subject: [Alerte] Erreur sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" + ''; +in +{ + systemd.services."service-failure-notify@" = { + description = "Email notification for failed service %i"; + onFailure = [ ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${systemd_notifier} %i"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/misc/default.nix b/modules/backup/misc/default.nix new file mode 100644 index 0000000..bc990b8 --- /dev/null +++ b/modules/backup/misc/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + ./internationalisation.nix + ./power.nix + ./update.nix + ./users.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/misc/internationalisation.nix b/modules/backup/misc/internationalisation.nix new file mode 100644 index 0000000..4eff542 --- /dev/null +++ b/modules/backup/misc/internationalisation.nix @@ -0,0 +1,24 @@ +{ config, lib, ... }: + +{ + config = { + i18n.defaultLocale = "${config.numbus.locale}.UTF-8"; + i18n.extraLocaleSettings = { + LC_ADDRESS = "${config.numbus.locale}.UTF-8"; + LC_IDENTIFICATION = "${config.numbus.locale}.UTF-8"; + LC_MEASUREMENT = "${config.numbus.locale}.UTF-8"; + LC_MONETARY = "${config.numbus.locale}.UTF-8"; + LC_NAME = "${config.numbus.locale}.UTF-8"; + LC_NUMERIC = "${config.numbus.locale}.UTF-8"; + LC_PAPER = "${config.numbus.locale}.UTF-8"; + LC_TELEPHONE = "${config.numbus.locale}.UTF-8"; + LC_TIME = "${config.numbus.locale}.UTF-8"; + }; + + console.keyMap = lib.toLower config.numbus.language; + services.xserver.xkb = { + layout = lib.toLower config.numbus.language; + variant = ""; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/misc/power.nix b/modules/backup/misc/power.nix new file mode 100644 index 0000000..f1c73f9 --- /dev/null +++ b/modules/backup/misc/power.nix @@ -0,0 +1,30 @@ +{ config, lib, pkgs, ... }: + +let + hardDrives = config.numbus.hardware.spindownDisksList; +in + +{ + config = { + systemd.services.hd-idle = { + description = "External HD spin down daemon"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = + let + idleTime = toString 1800; + hardDriveParameter = lib.strings.concatMapStringsSep " " (x: "-a ${x} -i ${idleTime}") hardDrives; + in + "${pkgs.hd-idle}/bin/hd-idle -i 0 ${hardDriveParameter}"; + }; + }; + + services.autoaspm.enable = true; + powerManagement.powertop.enable = true; + boot.kernelParams = [ + "pcie_aspm=force" + "consoleblank=60" + ]; + }; +} \ No newline at end of file diff --git a/modules/backup/misc/update.nix b/modules/backup/misc/update.nix new file mode 100644 index 0000000..26b22dd --- /dev/null +++ b/modules/backup/misc/update.nix @@ -0,0 +1,23 @@ +{ config, inputs, ... }: + +{ + config = { + system.autoUpgrade = { + enable = true; + allowReboot = false; + flake = inputs.self.outPath; + flags = [ "--print-build-logs" ]; + dates = "02:00"; + randomizedDelaySec = "45min"; + }; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 7d"; + }; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.auto-optimise-store = true; + }; +} \ No newline at end of file diff --git a/modules/backup/misc/users.nix b/modules/backup/misc/users.nix new file mode 100644 index 0000000..9ab0662 --- /dev/null +++ b/modules/backup/misc/users.nix @@ -0,0 +1,16 @@ +{ config, pkgs, ... }: + +{ + users.users.numbus-admin = { + shell = pkgs.fish; + isNormalUser = true; + description = "Numbus Admin"; + extraGroups = [ "wheel" ]; + uid = 1000; + initialPassword = "changeMe!"; + # required for auto start before user login + linger = true; + # required for rootless container with multiple users + autoSubUidGidRange = true; + }; +} \ No newline at end of file diff --git a/modules/backup/networking/default.nix b/modules/backup/networking/default.nix new file mode 100644 index 0000000..d1beb31 --- /dev/null +++ b/modules/backup/networking/default.nix @@ -0,0 +1,8 @@ +{ ... }: + +{ + imports = [ + ./firewall.nix + ./networking.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/networking/firewall.nix b/modules/backup/networking/firewall.nix new file mode 100644 index 0000000..e28edef --- /dev/null +++ b/modules/backup/networking/firewall.nix @@ -0,0 +1,13 @@ +{ config, pkgs, lib, ... }: + +{ + config = { + networking.nftables.enable = true; + networking.firewall = { + enable = true; + allowPing = true; + allowedTCPPorts = [ 53 80 443 ]; + allowedUDPPorts = [ 53 443 ]; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/networking/networking.nix b/modules/backup/networking/networking.nix new file mode 100644 index 0000000..3111a7b --- /dev/null +++ b/modules/backup/networking/networking.nix @@ -0,0 +1,60 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.numbus.networking; +in + +{ + options.numbus.networking = { + ipAddress = mkOption { + description = "The IP address that this server will use"; + type = types.str; + example = "192.168.1.100"; + }; + interface = mkOption { + description = "The interface that this server will use to connect to the network"; + type = types.str; + example = "enp1s0"; + }; + routerIpAddress = mkOption { + description = "The IP address of the router of your network"; + type = types.str; + example = "192.168.1.1"; + }; + networkSubnet = mkOption { + description = "The subnet of your network"; + type = types.str; + default = ""; + example = "192.168.1.0/24"; + }; + dnsServers = mkOption { + description = "The list of DNS servers that this server will use"; + type = types.listOf types.str; + default = [ "${cfg.ipAddress}" "9.9.9.9" ]; + example = [ "${cfg.ipAddress}" "9.9.9.9" ]; + }; + }; + + config = { + networking.hostName = "numbus-server"; + networking.networkmanager.enable = false; + + # Allow rootless containers to bind to port 53 and up + boot.kernel.sysctl."net.ipv4.ip_unprivileged_port_start" = 53; + + networking.bridges.br0.interfaces = [ "${cfg.interface}" ]; + networking.interfaces."${cfg.interface}".useDHCP = false; + networking.interfaces.br0.useDHCP = false; + networking.nameservers = cfg.dnsServers; + networking.interfaces.br0.ipv4.addresses = [{ + address = "${cfg.ipAddress}"; + prefixLength = 24; + }]; + networking.defaultGateway = { + address = "${cfg.routerIpAddress}"; + interface = "br0"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/packages/default.nix b/modules/backup/packages/default.nix new file mode 100644 index 0000000..66c45e1 --- /dev/null +++ b/modules/backup/packages/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + ./packages.nix + ./podman.nix + ./ssh.nix + ./terminal.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/packages/packages.nix b/modules/backup/packages/packages.nix new file mode 100644 index 0000000..13dc56e --- /dev/null +++ b/modules/backup/packages/packages.nix @@ -0,0 +1,25 @@ +{ config, pkgs, ... }: + +{ + nixpkgs.config.allowUnfree = true; + + environment.systemPackages = with pkgs; [ + git + ncdu + fastfetch + tpm2-tss + sops + age + powertop + pciutils + hdparm + hd-idle + hddtemp + smartmontools + cpufrequtils + intel-gpu-tools + snapraid + mergerfs + mergerfs-tools + ]; +} \ No newline at end of file diff --git a/modules/backup/packages/podman.nix b/modules/backup/packages/podman.nix new file mode 100644 index 0000000..ddf80e1 --- /dev/null +++ b/modules/backup/packages/podman.nix @@ -0,0 +1,16 @@ +{ pkgs, ... }: + +{ + virtualisation.podman.enable = true; + virtualisation.podman.defaultNetwork.settings.dns_enabled = true; + + virtualisation.containers.containersConf.settings = { + network.default_rootless_network_cmd = "slirp4netns"; + }; + + environment.systemPackages = with pkgs; [ + podman-compose + podman-tui + slirp4netns + ]; +} \ No newline at end of file diff --git a/modules/backup/packages/ssh.nix b/modules/backup/packages/ssh.nix new file mode 100644 index 0000000..6cc7ccc --- /dev/null +++ b/modules/backup/packages/ssh.nix @@ -0,0 +1,5 @@ +{ config, ... }: + +{ + services.openssh.enable = true; +} \ No newline at end of file diff --git a/modules/backup/packages/terminal.nix b/modules/backup/packages/terminal.nix new file mode 100644 index 0000000..88312a3 --- /dev/null +++ b/modules/backup/packages/terminal.nix @@ -0,0 +1,24 @@ +{ config, pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ + fish + fishPlugins.fzf-fish + fishPlugins.grc + grc + fzf + ]; + + programs.fish = { + enable = true; + interactiveShellInit = '' + set fish_greeting # Disable greeting + fastfetch + echo -e "\n\nWelcome to your Numbus-Server !\n\n- This system is managed by NixOS\n- All changes are futile\n- Please consider buying support if you can't get your server running\n- Have a nice day and enjoy !" + ''; + shellAliases = { + nixup = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch --upgrade && cd -"; + nixwitch = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch && cd -"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/authelia.nix b/modules/backup/services/applications/authelia.nix new file mode 100644 index 0000000..cf0fa96 --- /dev/null +++ b/modules/backup/services/applications/authelia.nix @@ -0,0 +1,171 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "authelia"; + # Version tagging + autheliaVersion = "v4.39.16"; + databaseVersion = "18.3"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.authelia; + # Derive Base DN from domain (e.g., example.com -> dc=example,dc=com) + domainParts = splitString "." config.numbus-server.services.domain; + baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts); + # Generate dynamic access control rules based on groups and allowedApps + mkGroupRule = groupName: appName: + let + app = config.numbus-server.service.${appName} or {}; + in + if app ? subdomain && app ? domain then '' + - domain: "${app.subdomain}.${app.domain}" + policy: two_factor + subject: "group:${groupName}"'' + else ""; + allGroupRules = concatStringsSep "\n" (filter (s: s != "") (flatten (mapAttrsToList (groupName: groupCfg: + map (appName: mkGroupRule groupName appName) (groupCfg.allowedApps or []) + ) (config.numbus-server.groups or {})))); + + defaultRedirectionUrl = + if config.numbus-server.services.homepage.enable then + "https://${config.numbus-server.services.homepage.subdomain}.${config.numbus-server.services.domain}" + else if config.numbus-server.services.dashy.enable then + "https://${config.numbus-server.services.dashy.subdomain}.${config.numbus-server.services.domain}" + else null; +in + +helper.mkPodmanService { + inherit name; + pod = name; + description = "Authelia, your own unified login provider"; + defaultPort = "9091"; + dependencies = [ + "sops-install-secrets.service" + "traefik.service" + "${config.numbus-server.services.dns}.service" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "authelia/db_name" + "authelia/db_username" + "authelia/db_password" + "authelia/jwt_secret" + "authelia/session_secret" + "authelia/storage_secret" + ]; + + composeText = '' + services: + authelia-server: + image: ghcr.io/authelia/authelia:${autheliaVersion} + container_name: authelia-server + hostname: authelia-server + user: '1000:1000' + networks: + authelia: + ipv4_address: 10.89.251.253 + ports: + - "${cfg.port}:9091/tcp" + volumes: + - ${cfg.configDir}/server:/config + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + authelia-database: + container_name: authelia-database + hostname: authelia-database + image: docker.io/library/postgres:${databaseVersion} + user: '1000:1000' + networks: + authelia: + ipv4_address: 10.89.251.252 + environment: + POSTGRES_DB: ${config.sops.placeholder."authelia/db_name"} + POSTGRES_USER: ${config.sops.placeholder."authelia/db_username"} + POSTGRES_PASSWORD: ${config.sops.placeholder."authelia/db_password"} + POSTGRES_INITDB_ARGS: '--data-checksums' + volumes: + - ${cfg.configDir}/database:/var/lib/postgresql/data + shm_size: 128mb + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + authelia: + driver: bridge + name: authelia + ipam: + config: + - subnet: "10.89.251.0/24" + gateway: "10.89.251.254" + ''; + + extraConfig = { + sops.templates."authelia-config" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + authelia: + identity_validation: + reset_password: + jwt_secret: "${config.sops.placeholder."authelia/jwt_secret"}" + jwt_lifespan: "5 minutes" + jwt_algorithm: "HS256" + storage: + encryption_key: "${config.sops.placeholder."authelia/storage_secret"}" + postgres: + address: "tcp://authelia-database:5432" + database: "${config.sops.placeholder."authelia/db_name"}" + username: "${config.sops.placeholder."authelia/db_username"}" + password: "${config.sops.placeholder."authelia/db_password"}" + session: + secret: "${config.sops.placeholder."authelia/session_secret"}" + cookies: + - domain: "${config.numbus-server.services.domain}" + authelia_url: "https://${cfg.subdomain}.${config.numbus-server.services.domain}" + ${optionalString (defaultRedirectionUrl != null) "default_redirection_url: \"${defaultRedirectionUrl}\""} + authentication_backend: + ldap: + implementation: "lldap" + address: "ldap://host.containers.internal:3890" + base_dn: "${baseDN}" + user: "UID=authelia,OU=people,${baseDN}" + password: "${config.sops.placeholder."lldap/"}" + notifier: + smtp: + address: submission://${config.numbus-server.mail.smtpHost}:${config.numbus-server.mail.smtpPort} + username: ${config.numbus-server.mail.smtpUsername} + password: ${config.sops.placeholder.smtpPassword} + sender: ${config.numbus-server.mail.fromAddress} + tls: + server_name: ${config.numbus-server.mail.smtpHost} + minimum_version: TLS1.2 + skip_verify: false + access_control: + default_policy: 'deny' + rules: + - domain: "*.${config.numbus-server.service.domain}" + policy: two_factor + subject: "group:admin" + ${allGroupRules} + ''; + path = "/etc/authelia/authelia.yaml"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/crafty.nix b/modules/backup/services/applications/crafty.nix new file mode 100644 index 0000000..9dc53d1 --- /dev/null +++ b/modules/backup/services/applications/crafty.nix @@ -0,0 +1,74 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "crafty"; + # Version tagging + craftyVersion = "v4.10.1"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.crafty; +in + +helper.mkPodmanService { + inherit name; + description = "Crafty controller, one place to manage your minecraft servers"; + defaultPort = "8443"; + scheme = "https"; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/log" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${optimizedDir}/import" + "100999:100 ${optimizedDir}/backups" + "100999:100 ${optimizedDir}/servers" + ]; + + composeText = '' + services: + crafty: + image: registry.gitlab.com/crafty-controller/crafty-4:${craftyVersion} + container_name: crafty + user: '1000:1000' + networks: + crafty: + ipv4_address: 10.89.250.253 + ports: + - "${cfg.port}:8443/tcp" + - "19132:19132/udp" + - "25500-25600:25500-25600" + volumes: + - ${optimizedDir}/backups:/crafty/backups + - ${optimizedDir}/servers:/crafty/servers + - ${optimizedDir}/import:/crafty/import + - ${cfg.configDir}/logs:/crafty/logs + - ${cfg.configDir}/config:/crafty/app/config + environment: + - TZ=${time.timeZone} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + crafty: + driver: bridge + name: crafty + ipam: + config: + - subnet: "10.89.250.0/24" + gateway: "10.89.250.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/dashy.nix b/modules/backup/services/applications/dashy.nix new file mode 100644 index 0000000..4fd4491 --- /dev/null +++ b/modules/backup/services/applications/dashy.nix @@ -0,0 +1,97 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "dashy"; + # Version tagging + dashyVersion = "v3.2.3"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.dashy; +in + +helper.mkPodmanService { + inherit name; + description = "Dashy, the ultimate dashboard for your homelab"; + defaultPort = "8999"; + configDirEnabled = false; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + + composeText = '' + services: + dashy: + image: lissy93/dashy:${dashyVersion} + container_name: dashy + hostname: dashy + user: '1000:1000' + networks: + dashy: + ipv4_address: 10.89.235.253 + ports: + - ${cfg.port}:8080 + volumes: + - ${config.sops."dashy/config".path}:/app/user-data/conf.yml + environment: + - UID=1000 + - GID=1000 + - NODE_ENV=production + healthcheck: + test: ['CMD', 'node', '/app/services/healthcheck'] + interval: 1m30s + timeout: 10s + retries: 3 + start_period: 40s + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + dashy: + driver: bridge + name: dashy + ipam: + config: + - subnet: "10.89.235.0/24" + gateway: "10.89.235.254" + ''; + + extraConfig = { + sops.templates."dashy/config" = { + gid = "100"; + uid = "100999"; + mode = "0440" ; + content = '' + pageInfo: + title: My Homelab + sections: + - name: Example Section + icon: far fa-rocket + items: + - title: GitHub + description: Dashy source code and docs + icon: fab fa-github + url: https://github.com/Lissy93/dashy + - title: Issues + description: View open issues, or raise a new one + icon: fas fa-bug + url: https://github.com/Lissy93/dashy/issues + - name: Local Services + items: + - title: Firewall + icon: favicon + url: http://192.168.1.1/ + - title: Game Server + icon: https://i.ibb.co/710B3Yc/space-invader-x256.png + url: http://192.168.130.1/ + ''; + path = "/etc/dashy/dashy.yaml"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/default.nix b/modules/backup/services/applications/default.nix new file mode 100644 index 0000000..13a4f6b --- /dev/null +++ b/modules/backup/services/applications/default.nix @@ -0,0 +1,29 @@ +{ ... }: + +{ + imports = [ + # Good + ./gitea.nix + ./immich.nix + ./nextcloud.nix + ./passbolt.nix + ./traefik.nix + # Testing needed + ./authelia.nix + ./crafty.nix + ./dashy.nix + ./frigate.nix + ./home-assistant.nix + ./homepage.nix + ./it-tools.nix + ./jellyfin.nix + ./lldap.nix + ./n8n.nix + ./netbird.nix + ./netbootxyz.nix + ./ntfy.nix + ./odoo.nix + ./uptime-kuma.nix + ./vscodium.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/services/applications/frigate.nix b/modules/backup/services/applications/frigate.nix new file mode 100644 index 0000000..558efbf --- /dev/null +++ b/modules/backup/services/applications/frigate.nix @@ -0,0 +1,83 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "frigate"; + # Version tagging + frigateVersion = "0.16.4"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.frigate; +in + +helper.mkPodmanService { + inherit name; + pod = "home-assistant"; + description = "Frigate, your fully-local NVR (Network Video Recorder)"; + defaultPort = "8971"; + scheme = "https"; + dependencies = [ + "sops-install-secrets.service" + "traefik.service" + "authelia.service" + "home-assistant.service" + "${config.numbus-server.services.dns}.service" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "1000:100 ${cfg.configDir}" + "1000:100 ${cfg.dataDir}" + ]; + + composeText = '' + services: + frigate: + image: ghcr.io/blakeblackshear/frigate:${frigateVersion} + container_name: frigate + hostname: frigate + shm_size: "256mb" + networks: + home-assistant: + ipv4_address: 10.89.230.253 + ports: + - "${cfg.port}:8971/tcp" + volumes: + - ${cfg.configDir}:/config + - ${cfg.dataDir}:/media/frigate + - /etc/localtime:/etc/localtime:ro + - type: tmpfs + target: /tmp/cache + tmpfs: + size: 1000000000 + environment: + - FRIGATE_MQTT_USER=${config.sops.placeholder."home-assistant/mqtt_username"} + - FRIGATE_MQTT_PASSWORD=${config.sops.placeholder."home-assistant/mqtt_password"} + ${lib.optionalString (cfg.devices != []) '' + devices: + ${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)} + ''} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + stop_grace_period: 30s + restart: unless-stopped + + networks: + home-assistant: + external: true + ''; + + extraOptions = { + devices = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/dri:/dev/dri" "/dev/bus/usb:/dev/bus/usb" "/dev/apex_0:/dev/apex_0" ]; + description = "List of devices to map into the container. /dev/dri is used for graphics acceleration, /dev/bus/usb for USB Coral TPUs, and /dev/apex_0 for PCI coral TPUs"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/gitea.nix b/modules/backup/services/applications/gitea.nix new file mode 100644 index 0000000..8beaf8b --- /dev/null +++ b/modules/backup/services/applications/gitea.nix @@ -0,0 +1,103 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "gitea"; + # Version tagging + giteaVersion = "1.25.4-rootless"; + databaseVersion = "18-alpine"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.gitea; +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "Gitea, your own self-hosted git platform"; + defaultPort = "3000"; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${optimizedDir}/data" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/database" + ]; + secrets = [ + "gitea/db_name" + "gitea/db_username" + "gitea/db_password" + ]; + + composeText = '' + services: + gitea-database: + image: docker.io/library/postgres:${databaseVersion} + container_name: gitea-database + hostname: gitea-database + user: '1000:1000' + networks: + gitea: + ipv4_address: 10.89.240.253 + volumes: + - ${cfg.configDir}/database:/var/lib/postgresql + environment: + - POSTGRES_DB=${config.sops.placeholder."gitea/db_name"} + - POSTGRES_USER=${config.sops.placeholder."gitea/db_username"} + - POSTGRES_PASSWORD=${config.sops.placeholder."gitea/db_password"} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + gitea-server: + image: docker.gitea.com/gitea:${giteaVersion} + container_name: gitea-server + hostname: gitea-server + user: '1000:1000' + networks: + gitea: + ipv4_address: 10.89.240.252 + ports: + - "${cfg.port}:3000/tcp" + volumes: + - ${optimizedDir}/data:/var/lib/gitea + - ${cfg.configDir}/config:/etc/gitea + - /etc/localtime:/etc/localtime:ro + environment: + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=gitea-database:5432 + - GITEA__database__NAME=${config.sops.placeholder."gitea/db_name"} + - GITEA__database__USER=${config.sops.placeholder."gitea/db_username"} + - GITEA__database__PASSWD=${config.sops.placeholder."gitea/db_password"} + - GITEA__server__SSH_PORT=2424 + - GITEA__server__ROOT_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain} + depends_on: + - gitea-database + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + gitea: + driver: bridge + name: gitea + ipam: + config: + - subnet: "10.89.240.0/24" + gateway: "10.89.240.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/home-assistant.nix b/modules/backup/services/applications/home-assistant.nix new file mode 100644 index 0000000..0a9dca9 --- /dev/null +++ b/modules/backup/services/applications/home-assistant.nix @@ -0,0 +1,177 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "home-assistant"; + # Version tagging + homeAssistantVersion = "2026.2.3"; + mqttVersion = "2.1-alpine"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.home-assistant; +in + +helper.mkPodmanService { + inherit name; + description = "Home Assistant, libre house control and much more"; + defaultPort = "8123"; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "1000:100 ${cfg.configDir}" + "1000:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/mqtt" + ]; + secrets = [ + "home-assistant/mqtt_user" + "home-assistant/mqtt_password" + ]; + +# Compose file good + composeText = '' + services: + home-assistant: + image: ghcr.io/home-assistant/home-assistant:${homeAssistantVersion} + container_name: home-assistant + hostname: home-assistant + networks: + home-assistant: + ipv4_address: 10.89.230.252 + ports: + - "${cfg.port}:8123/tcp" + volumes: + - ${cfg.configDir}/config:/config + - /etc/localtime:/etc/localtime:ro + - /run/dbus:/run/dbus:ro + ${lib.optionalString (cfg.devices != []) '' + devices: + ${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)} + ''} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + home-assistant-mqtt: + image: docker.io/library/eclipse-mosquitto:${mqttVersion} + container_name: home-assistant-mqtt + hostname: home-assistant-mqtt + user: '1000:1000' + networks: + home-assistant: + ipv4_address: 10.89.230.252 + volumes: + - ${cfg.configDir}/mqtt:/mosquitto + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + home-assistant: + driver: bridge + name: home-assistant + ipam: + config: + - subnet: "10.89.230.0/24" + gateway: "10.89.230.254" + ''; + + extraOptions = { + devices = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/serial/by-id/Sonoff_Zigbee_3.0-id-port0:/dev/ttyUSB0" ]; + description = "List of devices to map into the container. /dev/ttyUSB0 is used for Zigbee dongles"; + }; + }; + + extraConfig = { + systemd.services."${name}-quirk" = { + description = "Podman container quirk : ${name}"; + after = [ "${name}.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.coreutils pkgs.systemd ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + mkdir -p /var/lib/numbus-server/${name} + if [[ -e ${cfg.configDir}/config/configuration.yaml ]]; then + if grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then + exit 0 + elif grep -qF "use_x_forwarded_for" ${cfg.configDir}/config/configuration.yaml && ! grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then + tmp=$(mktemp) + head -n -6 ${cfg.configDir}/config/configuration.yaml > "$tmp" + mv "$tmp" ${cfg.configDir}/config/configuration.yaml + fi + fi + + until [[ -e ${cfg.configDir}/config/configuration.yaml ]]; do + sleep 15 + done + cat << 'EOF' >> ${cfg.configDir}/config/configuration.yaml + + http: + use_x_forwarded_for: true + trusted_proxies: 10.89.230.1 + + zha: + EOF + + systemctl restart ${name}.service + ''; + }; + }; + + systemd.services."mqtt-quirk" = { + description = "Podman container quirk : Home-assistant MQTT"; + after = [ "sops-install-secrets.service" ]; + before = [ "${name}.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.coreutils pkgs.mosquitto ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if [[ -e ${cfg.configDir}/mqtt/mosquitto.conf && ${cfg.configDir}/mqtt/password.txt ]]; then + if grep -qF "listener 1883" ${cfg.configDir}/mqtt/mosquitto.conf; then + exit 0 + else + rm ${cfg.configDir}/mqtt/mosquitto.conf + rm ${cfg.configDir}/mqtt/password.txt + touch ${cfg.configDir}/mqtt/mosquitto.conf + touch ${cfg.configDir}/mqtt/password.txt + fi + fi + + cat << EOF >> ${cfg.configDir}/mqtt/mosquitto.conf + persistence true + persistence_location /mosquitto/data/ + log_dest file /mosquitto/log/mosquitto.log + listener 1883 + ## Authentication ## + allow_anonymous false + password_file /mosquitto/password.txt + EOF + + HOME_ASSISTANT_MQTT_USER=$(cat /run/secrets/home-assistant/mqtt_user) + HOME_ASSISTANT_MQTT_PASSWORD=$(cat /run/secrets/home-assistant/mqtt_password) + + mosquitto_passwd -b ${cfg.configDir}/mqtt/password.txt "$HOME_ASSISTANT_MQTT_USER" "$HOME_ASSISTANT_MQTT_PASSWORD" + chmod 0400 ${cfg.configDir}/mqtt/password.txt + ''; + }; +} diff --git a/modules/backup/services/applications/homepage.nix b/modules/backup/services/applications/homepage.nix new file mode 100644 index 0000000..689ba16 --- /dev/null +++ b/modules/backup/services/applications/homepage.nix @@ -0,0 +1,63 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "homepage"; + # Version tagging + homepageVersion = "v1.10.1"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.homepage; +in + +helper.mkPodmanService { + inherit name; + description = "Homepage, a modern and highly customizable application dashboard"; + defaultPort = "3003"; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/images" + "100999:100 ${cfg.configDir}/icons" + ]; + + composeText = '' + services: + homepage: + image: ghcr.io/gethomepage/homepage:${homepageVersion} + container_name: homepage + hostname: homepage + user: '1000:1000' + networks: + homepage: + ports: + - "${cfg.port}:3000/tcp" + volumes: + - ${cfg.configDir}/config:/app/config + - ${cfg.configDir}/images:/app/public/images + - ${cfg.configDir}/icons:/app/public/icons + environment: + PUID: 1000 + PGID: 1000 + HOMEPAGE_ALLOWED_HOSTS: ${cfg.subdomain}.${config.numbus-server.services.domain} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + homepage: + driver: bridge + name: homepage + ipam: + config: + - subnet: "10.89.220.0/24" + gateway: "10.89.220.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/immich.nix b/modules/backup/services/applications/immich.nix new file mode 100644 index 0000000..6c875b4 --- /dev/null +++ b/modules/backup/services/applications/immich.nix @@ -0,0 +1,190 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container configuration + name = "immich"; + # Version tagging + immichVersion = "v2.5.6"; + redisVersion = "9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63"; + databaseVersion = "14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.immich; +in + +helper.mkPodmanService { + inherit name; + description = "Immich, Google Photos but better"; + defaultPort = "2283"; + middlewares = [ + "immichSecureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/redis" + "100999:100 ${cfg.configDir}/model-cache" + "100999:100 ${cfg.configDir}/machine-learning-cache" + "100999:100 ${cfg.configDir}/machine-learning-config" + "100999:100 ${cfg.configDir}/database" + "100999:100 ${cfg.dataDir}" + ]; + secrets = [ + "immich/redis_hostname" + "immich/db_hostname" + "immich/db_name" + "immich/db_username" + "immich/db_password" + ]; + +# Compose file good + composeText = '' + services: + immich-server: + container_name: immich-server + hostname: immich-server + image: ghcr.io/immich-app/immich-server:${immichVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.253 + ports: + - "${cfg.port}:2283/tcp" + volumes: + - $UPLOAD_LOCATION:/data + - /etc/localtime:/etc/localtime:ro + environment: + TZ: $TZ + REDIS_HOSTNAME: ${config.sops.placeholder."immich/redis_hostname"} + DB_HOSTNAME: ${config.sops.placeholder."immich/db_hostname"} + DB_DATABASE_NAME: ${config.sops.placeholder."immich/db_name"} + DB_USERNAME: ${config.sops.placeholder."immich/db_username"} + DB_PASSWORD: ${config.sops.placeholder."immich/db_password"} + IMMICH_TRUSTED_PROXIES: 10.89.210.1 + depends_on: + - immich-redis + - immich-database + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + immich-machine-learning: + container_name: immich-machine-learning + hostname: immich-machine-learning + image: ghcr.io/immich-app/immich-machine-learning:${immichVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.252 + volumes: + - ${cfg.configDir}/model-cache:/cache + - ${cfg.configDir}/machine-learning-config:/usr/src/.config + - ${cfg.configDir}/machine-learning-cache:/usr/src/.cache/ + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + immich-redis: + container_name: immich-redis + hostname: immich-redis + image: docker.io/valkey/valkey:${redisVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.251 + volumes: + - ${cfg.configDir}/redis:/data + healthcheck: + test: redis-cli ping || exit 1 + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + immich-database: + container_name: immich-database + hostname: immich-database + image: ghcr.io/immich-app/postgres:${databaseVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.250 + environment: + POSTGRES_DB: ${config.sops.placeholder."immich/db_name"} + POSTGRES_USER: ${config.sops.placeholder."immich/db_username"} + POSTGRES_PASSWORD: ${config.sops.placeholder."immich/db_password"} + POSTGRES_INITDB_ARGS: '--data-checksums' + volumes: + - $DB_DATA_LOCATION:/var/lib/postgresql/data + shm_size: 128mb + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + immich: + driver: bridge + name: immich + ipam: + config: + - subnet: "10.89.210.0/24" + gateway: "10.89.210.254" + ''; + + extraConfig = { + sops.templates."immich/env" = { + gid = "100"; + uid = "1000"; + mode = "0400"; + content = '' + DB_DATA_LOCATION=${cfg.configDir}/database + UPLOAD_LOCATION=${cfg.dataDir} + ''; + path = "/etc/podman/immich/.env"; + }; + + sops.templates."traefik/rules/immich-secureHeaders" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + middlewares: + immichSecureHeaders: + headers: + FrameDeny: true + AccessControlAllowMethods: 'GET,POST,PUT,DELETE,OPTIONS' + AccessControlAllowOriginList: + - https://${cfg.subdomain}.${config.numbus-server.services.domain} + - origin-list-or-null + AccessControlMaxAge: 100 + AddVaryHeader: true + BrowserXssFilter: true + ContentTypeNosniff: true + ForceSTSHeader: true + STSIncludeSubdomains: true + STSPreload: true + ContentSecurityPolicy: "default-src 'self'; base-uri 'self'; img-src 'self' https://static.immich.cloud https://tiles.immich.cloud data: blob:; connect-src 'self' https://${cfg.subdomain}.${config.numbus-server.services.domain} wss://${cfg.subdomain}.${config.numbus-server.services.domain} https://static.immich.cloud https://tiles.immich.cloud; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob: https://${cfg.subdomain}.${config.numbus-server.services.domain}; frame-ancestors 'self';" + CustomFrameOptionsValue: SAMEORIGIN + ReferrerPolicy: same-origin + PermissionsPolicy: vibrate 'self' + STSSeconds: 315360000 + ''; + path = "/etc/traefik/rules/immich-secureHeaders.yaml"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/it-tools.nix b/modules/backup/services/applications/it-tools.nix new file mode 100644 index 0000000..2d35d9a --- /dev/null +++ b/modules/backup/services/applications/it-tools.nix @@ -0,0 +1,54 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "it-tools"; + # Version tagging + it-toolsVersion = "2024.10.22-7ca5933"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.it-tools; +in + +helper.mkPodmanService { + inherit name; + description = "IT-tools, useful tools when doing IT"; + pod = "false"; + defaultPort = "8880"; + configDirEnabled = false; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + +# Compose file good + composeText = '' + services: + it-tools: + image: docker.io/corentinth/it-tools:${it-toolsVersion} + container_name: it-tools + hostname: it-tools + user: '1000:1000' + networks: + it-tools: + ipv4_address: 10.89.200.253 + ports: + - "${cfg.port}:80/tcp" + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + it-tools: + driver: bridge + name: it-tools + ipam: + config: + - subnet: "10.89.200.0/24" + gateway: "10.89.200.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/jellyfin.nix b/modules/backup/services/applications/jellyfin.nix new file mode 100644 index 0000000..991bce2 --- /dev/null +++ b/modules/backup/services/applications/jellyfin.nix @@ -0,0 +1,69 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "jellyfin"; + # Version tagging + jellyfinVersion = "10.11.6"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.jellyfin; +in + +helper.mkPodmanService { + inherit name; + description = "Jellyfin : A self-hosted media server to stream your movies and music"; + defaultPort = "8096"; + scheme = "https"; #TODO CHECK + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.dataDir}" + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.dataDir}/media" + "100999:100 ${cfg.dataDir}/fonts" + "100999:100 ${cfg.configDir}/cache" + "100999:100 ${cfg.configDir}/config" + ]; + + composeText = '' + services: + jellyfin: + image: docker.io/jellyfin/jellyfin:${jellyfinVersion} + container_name: jellyfin + hostname: jellyfin + user: '1000:1000' + networks: + jellyfin: + ipv4_address: 10.89.190.253 + ports: + - "${cfg.port}:8096/tcp" + volumes: + - ${cfg.configDir}/config:/config + - ${cfg.configDir}/cache:/cache + - type: bind + source: ${cfg.dataDir}/media + target: /media + - type: bind + source: ${cfg.dataDir}/fonts + target: /usr/local/share/fonts/custom + read_only: true + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + jellyfin: + driver: bridge + name: jellyfin + ipam: + config: + - subnet: "10.89.190.0/24" + gateway: "10.89.190.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/lldap.nix b/modules/backup/services/applications/lldap.nix new file mode 100644 index 0000000..7e423ed --- /dev/null +++ b/modules/backup/services/applications/lldap.nix @@ -0,0 +1,84 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "lldap"; + # Version tagging + lldapVersion = "v0.6.2"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.lldap; + # Derive Base DN from domain (e.g., example.com -> dc=example,dc=com) + domainParts = splitString "." config.numbus-server.services.domain; + baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts); +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "LLDAP, unified user management"; + defaultPort = "17170"; + dependencies = [ + "sops-install-secrets.service" + "network-online.target" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "lldap/jwt_secret" + "lldap/key_seed" + "lldap/admin_password" + ]; + + composeText = '' + services: + lldap: + image: lldap/lldap:${lldapVersion} + container_name: lldap + hostname: lldap + user: '1000:1000' + networks: + lldap: + ipv4_address: 10.89.185.253 + ports: + - "3890:3890" + - "${cfg.port}:17170" + volumes: + - ${cfg.configDir}:/data + environment: + - UID=1000 + - GID=1000 + - TZ=${config.time.timeZone} + - LLDAP_LDAP_BASE_DN=${baseDN} + - LLDAP_JWT_SECRET="${config.sops.placeholder."lldap/jwt_secret"}" + - LLDAP_KEY_SEED="${config.sops.placeholder."lldap/key_seed"}" + - LLDAP_LDAP_USER_PASS="${config.sops.placeholder."lldap/admin_password"}" + - LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true + - LLDAP_SMTP_OPTIONS__SERVER=${config.numbus-server.mail.smtpServer} + - LLDAP_SMTP_OPTIONS__PORT=${config.numbus-server.mail.smtpPort} + - LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=${config.numbus-server.mail.smtpEncryption} + - LLDAP_SMTP_OPTIONS__USER=${config.numbus-server.mail.smtpUsername} + - LLDAP_SMTP_OPTIONS__PASSWORD=${config.sops.placeholder."mail/smtpPassword"} + - LLDAP_SMTP_OPTIONS__FROM=no-reply <${config.numbus-server.mail.fromAddress}> + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + lldap: + driver: bridge + name: lldap + ipam: + config: + - subnet: "10.89.185.0/24" + gateway: "10.89.185.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/n8n.nix b/modules/backup/services/applications/n8n.nix new file mode 100644 index 0000000..bbfffc6 --- /dev/null +++ b/modules/backup/services/applications/n8n.nix @@ -0,0 +1,72 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "n8n"; + # Version tagging + n8nVersion = "2.11.4"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.n8n; +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "n8n, the ultimate automation platform"; + defaultPort = "5678"; + scheme = "https"; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${optimizedDir}" + ]; + + composeText = '' + services: + n8n: + image: docker.n8n.io/n8nio/n8n:${n8nVersion} + container_name: n8n + hostname: n8n + user: '1000:1000' + networks: + n8n: + ipv4_address: 10.89.180.253 + ports: + - "${cfg.port}:5678" + volumes: + - ${optimizedDir}:/home/node/.n8n + environment: + - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true + - N8N_HOST=${cfg.subdomain}.${config.numbus-server.services.domain} + - N8N_PORT=5678 + - N8N_PROTOCOL=https + - N8N_RUNNERS_ENABLED=true + - NODE_ENV=production + - WEBHOOK_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}/ + - GENERIC_TIMEZONE=${time.timeZone} + - TZ=${time.timeZone} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + n8n: + driver: bridge + name: n8n + ipam: + config: + - subnet: "10.89.180.0/24" + gateway: "10.89.180.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/netbird.nix b/modules/backup/services/applications/netbird.nix new file mode 100644 index 0000000..2018076 --- /dev/null +++ b/modules/backup/services/applications/netbird.nix @@ -0,0 +1,203 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "netbird"; + # Version tagging + netbirdDashboardVersion = ""; + netbirdServerVersion = ""; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.netbird; +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "NetBird, an all-in-one ZTNA remote access platform"; + defaultPort = "8888"; + reverseProxied = false; + dependencies = [ + "sops-install-secrets.service" + "traefik.service" + "${config.numbus-server.services.dns}.service" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "netbird/auth_key" + "netbird/encryption_key" + ]; + + composeText = '' + services: + netbird-dashboard: + image: netbirdio/dashboard:${netbirdDashboardVersion} + container_name: netbird-dashboard + hostname: netbird-dashboard + user: '1000:1000' + networks: + netbird: + ipv4_address: 10.89.175.253 + ports: + - "${defaultPort}:8080/tcp" + environment: + # Endpoints + - NETBIRD_MGMT_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain} + - NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain} + # OIDC - using embedded IdP + - AUTH_AUDIENCE=netbird-dashboard + - AUTH_CLIENT_ID=netbird-dashboard + - AUTH_CLIENT_SECRET= + - AUTH_AUTHORITY=https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2 + - USE_AUTH0=false + - AUTH_SUPPORTED_SCOPES=openid profile email groups + - AUTH_REDIRECT_URI=/nb-auth + - AUTH_SILENT_REDIRECT_URI=/nb-silent-auth + # SSL + - NGINX_SSL_PORT=443 + - LETSENCRYPT_DOMAIN=none + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "2" + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + netbird-server: + image: netbirdio/netbird-server:${netbirdServerVersion} + container_name: netbird-server + hostname: netbird-server + user: '1000:1000' + networks: + netbird: + ipv4_address: 10.89.175.252 + ports: + - "8889:8081/tcp" + - "3478:3478/udp" + volumes: + - ${config.sops.templates."netbird-config".path}:/etc/netbird/config.yaml + - ${cfg.configDir}:/var/lib/netbird + command: ["--config", "/etc/netbird/config.yaml"] + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "2" + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + netbird: + driver: bridge + name: netbird + ipam: + config: + - subnet: "10.89.175.0/24" + gateway: "10.89.175.254" + ''; + + extraConfig = { + sops.templates."netbird-config" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + server: + listenAddress: ":80" + exposedAddress: "https://${cfg.subdomain}.${config.numbus-server.services.domain}:443" + stunPorts: + - 3478 + metricsPort: 9090 + healthcheckAddress: ":9000" + logLevel: "info" + logFile: "console" + authSecret: "${config.sops.placeholder."netbird/auth_key"}" + dataDir: "/var/lib/netbird" + + auth: + issuer: "https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2" + signKeyRefreshEnabled: true + dashboardRedirectURIs: + - "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-auth" + - "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-silent-auth" + cliRedirectURIs: + - "http://localhost:53000/" + + reverseProxy: + trustedHTTPProxies: + - "10.89.175.1/32" + + store: + engine: "sqlite" + encryptionKey: "${config.sops.placeholder."netbird/encryption_key"}" + ''; + path = "/etc/netbird/netbird.yaml"; + }; + + sops.templates."traefik/rules/${name}" = { + gid = "100"; + uid = "1000"; + mode = "0400"; + content = '' + http: + routers: + ${name}-dashboard: + rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`)" + entrypoints: + - "websecure" + middlewares: + - secureHeaders + tls: + certresolver: "cloudflare" + options: "secureTLS" + priority: 1 + ${name}-grpc: + rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`))" + entrypoints: + - "websecure" + service: ${name}-server-h2c + middlewares: + - secureHeaders + tls: + certresolver: "cloudflare" + options: "secureTLS" + ${name}-backend: + rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))" + entrypoints: + - "websecure" + service: ${name}-server + middlewares: + - secureHeaders + tls: + certresolver: "cloudflare" + options: "secureTLS" + + services:${cfg.port} + ${name}-dashboard: + loadBalancer: + servers: + - url: "http://host.containers.internal:${cfg.port}" + ${name}-server: + loadBalancer: + servers: + - url: "http://host.containers.internal:8889" + ${name}-server-h2c: + loadBalancer: + servers: + - url: "h2c://host.containers.internal:3478" + ''; + path = "/etc/traefik/rules/${name}"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/netbootxyz.nix b/modules/backup/services/applications/netbootxyz.nix new file mode 100644 index 0000000..bda78ae --- /dev/null +++ b/modules/backup/services/applications/netbootxyz.nix @@ -0,0 +1,75 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "netbootxyz"; + # Version tagging + netbootxyzVersion = "3.0.0"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.netbootxyz; +in + +helper.mkPodmanService { + inherit name; + description = "Netboot.xyz, forget about flashing isos on USB sticks with PXE boot"; + pod = "false"; + defaultPort = "3004"; + configDirEnabled = optimizedDir == cfg.configDir; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${optimizedDir}" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${optimizedDir}/assets" + ]; + + composeText = '' + services: + netbootxyz: + image: ghcr.io/netbootxyz/netbootxyz:${netbootxyzVersion} + container_name: netbootxyz + hostname: netbootxyz + user: '1000:1000' + networks: + netbootxyz: + ipv4_address: 10.89.170.253 + ports: + - "${cfg.port}:3000/tcp" + - "69:69/udp" + - "8008:80/tcp" + volumes: + - ${cfg.configDir}/config:/config + - ${optimizedDir}/assets:/assets + environment: + - PUID=1000 + - PGID=1000 + - TZ=${time.timeZone} + - PORT_RANGE=30000:30010 + - SUBFOLDER=/ + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + netbootxyz: + driver: bridge + name: netbootxyz + ipam: + config: + - subnet: "10.89.170.0/24" + gateway: "10.89.170.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/nextcloud.nix b/modules/backup/services/applications/nextcloud.nix new file mode 100644 index 0000000..599b57e --- /dev/null +++ b/modules/backup/services/applications/nextcloud.nix @@ -0,0 +1,384 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Version tagging + nextcloudVersion = "33.0.0"; + redisVersion = "8.6-alpine"; + databaseVersion = "11.8"; + onlyofficeVersion = "9.2"; + whiteboardVersion = "v1.5.6"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.nextcloud; + # Container config + name = "nextcloud"; +in + +helper.mkPodmanService { + inherit name; + description = "Nextcloud, your own online office suite"; + defaultPort = "1100"; + middlewares = [ + "nextcloudSecureHeaders" + ]; + secrets = [ + "nextcloud/db_name" + "nextcloud/db_username" + "nextcloud/db_password" + "nextcloud/redis_password" + "nextcloud/onlyoffice_secret" + "nextcloud/whiteboard_secret" + ]; + dirPermissions = [ + "100032:100 ${cfg.dataDir}" + "100032:100 ${cfg.configDir}" + "100032:100 ${cfg.configDir}/web" + "100999:100 ${cfg.configDir}/redis" + "100999:100 ${cfg.configDir}/database" + "1000:100 ${cfg.configDir}/onlyoffice" + "1000:100 ${cfg.configDir}/onlyoffice/log" + "1000:100 ${cfg.configDir}/onlyoffice/cache" + "1000:100 ${cfg.configDir}/onlyoffice/data" + "1000:100 ${cfg.configDir}/onlyoffice/database" + ]; + +# Compose file good + composeText = '' + services: + nextcloud-server: + image: docker.io/library/nextcloud:${nextcloudVersion} + container_name: nextcloud-server + hostname: nextcloud-server + networks: + nextcloud: + ipv4_address: 10.89.160.253 + ports: + - "${cfg.port}:80/tcp" + volumes: + - ${cfg.configDir}/web:/var/www/html + - ${cfg.dataDir}:/mnt/ncdata + environment: + MYSQL_HOST: nextcloud-database:3306 + MYSQL_DATABASE: ${config.sops.placeholder."nextcloud/db_name"} + MYSQL_USER: ${config.sops.placeholder."nextcloud/db_username"} + MYSQL_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"} + REDIS_HOST_PASSWORD: ${config.sops.placeholder."nextcloud/redis_password"} + REDIS_HOST: nextcloud-redis + NEXTCLOUD_TRUSTED_DOMAINS: ${cfg.subdomain}.${config.numbus-server.services.domain} + NEXTCLOUD_DATA_DIR: /mnt/ncdata + SMTP_SECURE: tls + SMTP_HOST: ${config.numbus-server.mail.smtpServer} + SMTP_PORT: ${toString config.numbus-server.mail.smtpPort} + SMTP_NAME: ${config.numbus-server.mail.smtpUsername} + SMTP_PASSWORD: ${config.sops.placeholder.smtpPassword} + MAIL_FROM_ADDRESS: no-reply + MAIL_DOMAIN: ${config.numbus-server.services.domain} + APACHE_DISABLE_REWRITE_IP: 1 + OVERWRITEPROTOCOL: https + TRUSTED_PROXIES: 10.89.160.1 + NC_default_phone_region: "${config.numbus-server.language}" + NC_default_language: "${config.numbus-server.language}" + NC_default_locale: "${config.numbus-server.locale}" + NC_default_timezone: "${config.time.timeZone}" + NC_maintenance_window_start: "1" + depends_on: + - nextcloud-database + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + nextcloud-redis: + image: docker.io/library/redis:${redisVersion} + container_name: nextcloud-redis + hostname: nextcloud-redis + user: '1000:1000' + networks: + nextcloud: + ipv4_address: 10.89.160.252 + volumes: + - ${cfg.configDir}/redis:/data + command: redis-server --requirepass ${config.sops.placeholder."nextcloud/redis_password"} --save 60 1 --loglevel warning + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + nextcloud-database: + image: docker.io/library/mariadb:${databaseVersion} + container_name: nextcloud-database + hostname: nextcloud-database + user: '1000:1000' + networks: + nextcloud: + ipv4_address: 10.89.160.251 + volumes: + - ${cfg.configDir}/database:/var/lib/mysql + environment: + MARIADB_DATABASE: ${config.sops.placeholder."nextcloud/db_name"} + MARIADB_USER: ${config.sops.placeholder."nextcloud/db_username"} + MARIADB_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"} + MARIADB_RANDOM_ROOT_PASSWORD: true + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + command: + - "--transaction-isolation=READ-COMMITTED" + - "--binlog-format=ROW" + restart: unless-stopped + + nextcloud-onlyoffice: + image: docker.io/onlyoffice/documentserver:${onlyofficeVersion} + container_name: nextcloud-onlyoffice + hostname: nextcloud-onlyoffice + networks: + nextcloud: + ipv4_address: 10.89.160.250 + ports: + - "9980:80/tcp" + volumes: + - ${cfg.configDir}/onlyoffice/log:/var/log/onlyoffice + - ${cfg.configDir}/onlyoffice/cache:/var/lib/onlyoffice + - ${cfg.configDir}/onlyoffice/data:/var/www/onlyoffice/Data + - ${cfg.configDir}/onlyoffice/database:/var/lib/postgresql + environment: + - JWT_SECRET=${config.sops.placeholder."nextcloud/onlyoffice_secret"} + - REDIS_SERVER_PASS=${config.sops.placeholder."nextcloud/redis_password"} + - REDIS_SERVER_HOST=nextcloud-redis + - REDIS_SERVER_PORT=6379 + - ADMINPANEL_ENABLED=false + - EXAMPLE_ENABLED=false + - METRICS_ENABLED=false + cap_drop: + - NET_RAW + restart: unless-stopped + + nextcloud-whiteboard: + image: ghcr.io/nextcloud-releases/whiteboard:${whiteboardVersion} + container_name: nextcloud-whiteboard + hostname: nextcloud-whiteboard + user: '1000:1000' + ports: + - "3002:3002/tcp" + environment: + NEXTCLOUD_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain} + JWT_SECRET_KEY: ${config.sops.placeholder."nextcloud/whiteboard_secret"} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + nextcloud: + driver: bridge + name: nextcloud + ipam: + config: + - subnet: "10.89.160.0/24" + gateway: "10.89.160.254" + ''; + + extraOptions = { + onlyoffice = { + subdomain = mkOption { + type = types.str; + default = "onlyoffice"; + example = "onlyoffice"; + description = "The subdomain that onlyoffice for nextcloud will use"; + }; + }; + whiteboard = { + subdomain = mkOption { + type = types.str; + default = "whiteboard"; + example = "whiteboard"; + description = "The subdomain that whiteboard for nextcloud will use"; + }; + }; + }; + + extraConfig = { + sops.templates."traefik/rules/nextcloud-onlyoffice" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + routers: + nextcloud-onlyoffice: + rule: "Host(`${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}`)" + entrypoints: + - "websecure" + service: nextcloud-onlyoffice + tls: + certresolver: "cloudflare" + options: "secureTLS" + services: + nextcloud-onlyoffice: + loadBalancer: + servers: + - url: "http://host.containers.internal:9980" + ''; + path = "/etc/traefik/rules/nextcloud-onlyoffice.yaml"; + }; + + sops.templates."traefik/rules/nextcloud-whiteboard" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + routers: + nextcloud-whiteboard: + rule: "Host(`${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}`)" + entrypoints: + - "websecure" + service: nextcloud-whiteboard + middlewares: + - "secureHeaders" + tls: + certresolver: "cloudflare" + options: "secureTLS" + services: + nextcloud-whiteboard: + loadBalancer: + servers: + - url: "http://host.containers.internal:3002" + ''; + path = "/etc/traefik/rules/nextcloud-whiteboard.yaml"; + }; + + sops.templates."traefik/rules/nextcloud-secureHeaders" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + middlewares: + nextcloudSecureHeaders: + headers: + FrameDeny: false + CustomFrameOptionsValue: "SAMEORIGIN" + AddVaryHeader: true + BrowserXssFilter: true + ContentTypeNosniff: true + ForceSTSHeader: true + STSSeconds: 315360000 + STSIncludeSubdomains: true + STSPreload: true + AccessControlAllowMethods: "GET,OPTIONS,PUT" + AccessControlAllowOriginList: + - origin-list-or-null + AccessControlMaxAge: 100 + ReferrerPolicy: same-origin + PermissionsPolicy: "vibrate=()" + ContentSecurityPolicy: >- + default-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self'; + script-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + connect-src 'self'; + img-src 'self' data:; + font-src 'self' data:; + frame-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self'; + frame-ancestors https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self'; + object-src 'none'; + base-uri 'self'; + ''; + path = "/etc/traefik/rules/nextcloud-secureHeaders"; + }; + + systemd.services."${name}-quirk" = { + description = "Podman container quirk : ${name}"; + wantedBy = [ "multi-user.target" ]; + after = [ "${name}.service" "${name}-secrets.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.coreutils pkgs.sudo pkgs.podman pkgs.systemd pkgs.gnugrep ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + OCC="sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ" + + [[ ! -e /var/lib/numbus-server/${name}/.env ]] && systemctl start ${name}-secrets.service + until [[ -e /var/lib/numbus-server/${name}/.env ]]; do + echo "Waiting for secrets generation..." + sleep 5 + done + source /var/lib/numbus-server/${name}/.env + + until $OCC status | grep -iq "installed: true" >/dev/null 2>&1; do + echo "Waiting for Nextcloud to be up and running..." + sleep 60 + done + + $OCC db:add-missing-indices + $OCC maintenance:repair --include-expensive + + INSTALL_APPS_LIST=( "calendar" "contacts" "mail" "notes" "onlyoffice" "cookbook" "whiteboard" ) + DISABLE_APPS_LIST=( "activity" "federation" "webhook_listeners" "photos" "recommendations" "sharebymail" "teams" "support" "richdocumentscode" ) + + for app in ''${INSTALL_APPS_LIST[@]}; do + if ! $OCC --no-warnings app:list | grep -iq "$app:"; then + $OCC --no-warnings app:install "$app" + fi + if $OCC --no-warnings app:list --disabled | grep -iq "$app:"; then + $OCC --no-warnings app:enable "$app" + fi + done + for app in ''${DISABLE_APPS_LIST[@]}; do + if $OCC --no-warnings app:list --enabled | grep -iq "$app:"; then + $OCC --no-warnings app:disable "$app" + fi + done + $OCC --no-warnings config:system:set onlyoffice DocumentServerInternalUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/" + $OCC --no-warnings config:system:set onlyoffice DocumentServerUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/" + $OCC --no-warnings config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_PASSWORD" + $OCC --no-warnings config:app:set whiteboard collabBackendUrl --value="https://${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}" + $OCC --no-warnings config:app:set whiteboard jwt_secret_key --value="$WHITEBOARD_PASSWORD" + + if [[ ! -f /var/lib/numbus-server/${name}/croned.true ]]; then + $OCC background:cron + sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php + touch /var/lib/numbus-server/${name}/croned.true + fi + + if [[ ! -f /var/lib/numbus-server/${name}/scanned.true ]]; then + $OCC files:scan --all + $OCC files:repair-tree + touch /var/lib/numbus-server/${name}/scanned.true + fi + ''; + }; + + systemd.services."${name}-cron" = { + description = "Podman container crontab : ${name}"; + after = [ "${name}.service" "${name}-quirk.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + path = [ pkgs.sudo pkgs.podman ]; + serviceConfig = { + Type = "oneshot"; + ExecCondition = ''${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ status''; + ExecStart = "${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php"; + }; + }; + + systemd.timers."${name}-cron" = { + description = "Timer for Nextcloud cron"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + Unit = "${name}-cron.service"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/ntfy.nix b/modules/backup/services/applications/ntfy.nix new file mode 100644 index 0000000..6650008 --- /dev/null +++ b/modules/backup/services/applications/ntfy.nix @@ -0,0 +1,62 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "ntfy"; + # Version tagging + ntfyVersion = "v2.18.0"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.ntfy; +in + +helper.mkPodmanService { + inherit name; + description = "Ntfy, get notified easily"; + defaultPort = "8099"; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/cache" + "100999:100 ${cfg.configDir}/config" + ]; + + composeText = '' + services: + ntfy: + image: docker.io/binwiederhier/ntfy + container_name: ntfy + hostname: ntfy + user: "1000:1000" + networks: + ntfy: + ipv4_address: 10.89.150.253 + ports: + - "${cfg.port}:80/tcp" + command: + - serve + volumes: + - ${cfg.config}/cache:/var/cache/ntfy + - ${cfg.config}/config:/etc/ntfy + environment: + - TZ=${time.timeZone} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + ntfy: + driver: bridge + name: ntfy + ipam: + config: + - subnet: "10.89.150.0/24" + gateway: "10.89.150.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/odoo.nix b/modules/backup/services/applications/odoo.nix new file mode 100644 index 0000000..fba6c67 --- /dev/null +++ b/modules/backup/services/applications/odoo.nix @@ -0,0 +1,118 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "odoo"; + # Version tagging + odooVersion = "10.11.6"; + databaseVersion = "15.17"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.odoo; +in + +helper.mkPodmanService { + inherit name; + description = "Odoo : An open ERP (Enterprise resource planning) solution"; + defaultPort = "8069"; + configDirEnabled = optimizedDir == cfg.configDir; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${optimizedDir}" + "100999:100 ${optimizedDir}/odoo" + "100999:100 ${cfg.configDir}/addons" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/database" + ]; + + composeText = '' + services: + odoo-database: + image: docker.io/library/postgres:${databaseVersion} + container_name: odoo-database + hostname: odoo-database + user: '1000:1000' + shm_size: 128mb + networks: + odoo: + ipv4_address: 10.89.190.253 + volumes: + - ${cfg.configDir}/database:/var/lib/postgresql/data + environment: + - POSTGRES_DB=${config.sops.placeholder."odoo/db_name"} + - POSTGRES_PASSWORD=${config.sops.placeholder."odoo/db_password"} + - POSTGRES_USER=${config.sops.placeholder."odoo/db_username"} + - PGDATA=/var/lib/postgresql/data + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + odoo-server: + image: docker.io/library/odoo:${odooVersion} + container_name: odoo-server + hostname: odoo-server + user: '1000:1000' + networks: + odoo: + ipv4_address: 10.89.190.252 + ports: + - "${cfg.port}:8069/tcp" + volumes: + - ${optimizedDir}/odoo:/var/lib/odoo + - ${cfg.configDir}/config:/etc/odoo + - ${cfg.configDir}/addons:/mnt/extra-addons + environment: + - HOST=odoo-database + - USER=${config.sops.placeholder."odoo/db_username"} + - PASSWORD=${config.sops.placeholder."odoo/db_password"} + depends_on: + - odoo-database + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + odoo: + driver: bridge + name: odoo + ipam: + config: + - subnet: "10.89.190.0/24" + gateway: "10.89.190.254" + ''; + + extraConfig = { + sops.secrets."odoo/db_name" = { + sopsFile = /etc/nixos/secrets/podman/odoo.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + sops.secrets."odoo/db_username" = { + sopsFile = /etc/nixos/secrets/podman/odoo.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + sops.secrets."odoo/db_password" = { + sopsFile = /etc/nixos/secrets/podman/odoo.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + }; +} diff --git a/modules/backup/services/applications/passbolt.nix b/modules/backup/services/applications/passbolt.nix new file mode 100644 index 0000000..d16c53a --- /dev/null +++ b/modules/backup/services/applications/passbolt.nix @@ -0,0 +1,112 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "passbolt"; + # Version tagging + passboltVersion = "5.9.0-1-ce-non-root"; + databaseVersion = "12.2"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.passbolt; +in + +helper.mkPodmanService { + inherit name; + description = "Passbolt, your password manager"; + defaultPort = "4433"; + scheme = "https"; + dataDirEnabled = false; + middlewares = [ "secureHeaders" ]; + dirPermissions = [ + "100032:100 ${cfg.configDir}" + "100032:100 ${cfg.configDir}/gpg" + "100032:100 ${cfg.configDir}/jwt" + "100999:100 ${cfg.configDir}/database" + ]; + secrets = [ + "passbolt/db_name" + "passbolt/db_username" + "passbolt/db_password" + ]; + +# Compose file good + composeText = '' + services: + passbolt-server: + image: docker.io/passbolt/passbolt:${passboltVersion} + container_name: passbolt-server + hostname: passbolt-server + user: '33:33' + networks: + passbolt: + ports: + - "${cfg.port}:4433/tcp" + volumes: + - ${cfg.configDir}/gpg:/etc/passbolt/gpg + - ${cfg.configDir}/jwt:/etc/passbolt/jwt + environment: + APP_DEFAULT_TIMEZONE: ${config.time.timeZone} + APP_FULL_BASE_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain} + DATASOURCES_DEFAULT_HOST: "passbolt-database" + DATASOURCES_DEFAULT_USERNAME: ${config.sops.placeholder."passbolt/db_username"} + DATASOURCES_DEFAULT_PASSWORD: ${config.sops.placeholder."passbolt/db_password"} + DATASOURCES_DEFAULT_DATABASE: ${config.sops.placeholder."passbolt/db_name"} + EMAIL_DEFAULT_FROM_NAME: "Passbolt" + EMAIL_TRANSPORT_DEFAULT_HOST: ${config.numbus-server.mail.smtpServer} + EMAIL_TRANSPORT_DEFAULT_PORT: ${toString config.numbus-server.mail.smtpPort} + EMAIL_TRANSPORT_DEFAULT_USERNAME: ${config.numbus-server.mail.smtpUsername} + EMAIL_TRANSPORT_DEFAULT_PASSWORD: ${config.sops.placeholder."mail/smtpPassword"} + EMAIL_TRANSPORT_DEFAULT_TLS: true + EMAIL_DEFAULT_FROM: passbolt-noreply@${config.numbus-server.services.domain} + PASSBOLT_SSL_FORCE: true + command: + [ + "/usr/bin/wait-for.sh", + "-t", + "0", + "passbolt-database:3306", + "--", + "/docker-entrypoint.sh" + ] + depends_on: + - passbolt-database + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + passbolt-database: + image: docker.io/library/mariadb:${databaseVersion} + container_name: passbolt-database + hostname: passbolt-database + user: '1000:1000' + networks: + passbolt: + volumes: + - ${cfg.configDir}/database:/var/lib/mysql + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "true" + MYSQL_DATABASE: ${config.sops."passbolt/db_name"} + MYSQL_USER: ${config.sops."passbolt/db_username"} + MYSQL_PASSWORD: ${config.sops."passbolt/db_password"} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + passbolt: + name: passbolt + driver: bridge + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/traefik.nix b/modules/backup/services/applications/traefik.nix new file mode 100644 index 0000000..d776c07 --- /dev/null +++ b/modules/backup/services/applications/traefik.nix @@ -0,0 +1,178 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "traefik"; + # Version tagging + traefikVersion = "v3.6.8"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.traefik; +in + +helper.mkPodmanService { + inherit name; + description = "Traefik reverse proxy, one to rule them all"; + defaultPort = "7780"; + pod = "false"; + startDelay = 10; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + dependencies = [ + "sops-install-secrets.service" + "network-online.target" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/certs" + ]; + +# Compose file good + composeText = '' + services: + traefik: + image: docker.io/library/traefik:${traefikVersion} + container_name: traefik + hostname: traefik + user: '1000:1000' + network_mode: pasta + ports: + - "${cfg.port}:8080/tcp" + - "443:443/tcp" + volumes: + - ${config.sops.templates."traefik/config".path}:/etc/traefik/traefik.yaml:ro + - ${cfg.configDir}/certs:/var/traefik/certs + - /etc/traefik/rules:/etc/traefik/rules:ro + environment: + - CF_DNS_API_TOKEN=${config.sops.placeholder."traefik/cloudflare_api_token"} + cap_add: + - NET_BIND_SERVICE + security_opt: + - no-new-privileges:true + restart: unless-stopped + ''; + + extraConfig = { + sops.secrets."traefik/cloudflare_api_token" = { + sopsFile = /etc/nixos/secrets/podman/traefik.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + + sops.templates."traefik/config"= { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + global: + checkNewVersion: false + sendAnonymousUsage: false + log: + level: ${cfg.logLevel} + accesslog: {} + api: + dashboard: true + insecure: false + entryPoints: + web: + address: :80 + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: :443 + forwardedHeaders: + trustedIPs: + - "127.0.0.1/32" + - "10.0.0.0/8" + - "192.168.0.0/16" + - "172.16.0.0/12" + certificatesResolvers: + cloudflare: + acme: + email: ${config.numbus-server.mail.adminAddress} + storage: /var/traefik/certs/cloudflare-acme.json + caServer: "https://acme-v02.api.letsencrypt.org/directory" + dnsChallenge: + provider: cloudflare + resolvers: + - "1.1.1.1:53" + - "9.9.9.9:53" + serversTransport: + insecureSkipVerify: true + providers: + file: + directory: "/etc/traefik/rules" + watch: true + ''; + path = "/etc/traefik/traefik.yaml"; + }; + + sops.templates."traefik/rules/secureHeaders" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + middlewares: + secureHeaders: + headers: + FrameDeny: true + AccessControlAllowMethods: 'GET,OPTIONS,PUT' + AccessControlAllowOriginList: + - origin-list-or-null + AccessControlMaxAge: 100 + AddVaryHeader: true + BrowserXssFilter: true + ContentTypeNosniff: true + ForceSTSHeader: true + STSIncludeSubdomains: true + STSPreload: true + ContentSecurityPolicy: default-src 'self' 'unsafe-inline' + CustomFrameOptionsValue: SAMEORIGIN + ReferrerPolicy: same-origin + PermissionsPolicy: vibrate 'self' + STSSeconds: 315360000 + ''; + path = "/etc/traefik/rules/secureHeaders.yaml"; + }; + + sops.templates."traefik/rules/secureTLS" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + tls: + options: + secureTLS: + minVersion: VersionTLS12 + sniStrict: true + curvePreferences: + - CurveP521 + - CurveP384 + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + ''; + path = "/etc/traefik/rules/secureTLS.yaml"; + }; + }; + + extraOptions = { + enable.default = true; + logLevel = mkOption { + type = types.enum [ "TRACE" "DEBUG" "INFO" "WARN" "ERROR" "FATAL" ]; + default = "ERROR"; + example = "ERROR"; + description = "The level of detail Traefik should print in the logs."; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/applications/uptime-kuma.nix b/modules/backup/services/applications/uptime-kuma.nix new file mode 100644 index 0000000..38aa259 --- /dev/null +++ b/modules/backup/services/applications/uptime-kuma.nix @@ -0,0 +1,54 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "uptimeKuma"; + # Version tagging + uptimeKumaVersion = "2.2.0-rootless"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.uptimeKuma; +in + +helper.mkPodmanService { + inherit name; + description = "Uptime-Kuma, \"don't let your server down !\" monitoring tools"; + defaultPort = "3001"; + scheme = "http"; + middlewares = [ "secureHeaders" ]; + dirPermissions = [ "100999:100 ${optimizedDir}" ]; + + composeText = '' + services: + uptimekuma: + image: docker.io/louislam/uptime-kuma:${uptimeKumaVersion} + container_name: uptime-kuma + hostname: uptime-kuma + user: '1000:1000' + networks: + uptime-kuma: + ipv4_address: 10.89.100.253 + ports: + - "${cfg.port}:3001/tcp" + volumes: + - ${optimizedDir}:/app/data + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + uptime-kuma: + driver: bridge + ipam: + config: + - subnet: "10.89.100.0/24" + gateway: "10.89.100.254" + ''; +} \ No newline at end of file diff --git a/modules/backup/services/applications/vscodium.nix b/modules/backup/services/applications/vscodium.nix new file mode 100644 index 0000000..0421ac2 --- /dev/null +++ b/modules/backup/services/applications/vscodium.nix @@ -0,0 +1,81 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "vscodium"; + # Version tagging + vscodiumVersion = "1.110.11607-ls15"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.vscodium; +in + +helper.mkPodmanService { + inherit name; + description = "VScodium, an open-source version of VScode in your web browser"; + defaultPort = "8000"; + configDirEnabled = optimizedDir == cfg.configDir; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ "secureHeaders" ]; + dirPermissions = [ + "100999:100 ${optimizedDir}" + "100999:100 ${cfg.configDir}" + "100999:100 ${optimizedDir}/workspace" + "100999:100 ${cfg.configDir}/config" + ]; + + composeText = '' + services: + vscodium: + image: lscr.io/linuxserver/vscodium-web:${vscodiumVersion} + container_name: vscodium + hostname: vscodium + user: '1000:1000' + networks: + vscodium: + ipv4_address: 10.89.50.253 + ports: + - "${defaultPort}:8000" + volumes: + - ${cfg.configDir}/config:/config + - ${optimizedDir}/workspace:/workspace + environment: + - PUID=1000 + - PGID=1000 + - TZ=${time.timeZone} + - CONNECTION_TOKEN=${config.sops.placeholder."vscodium/connection_token"} + shm_size: "1gb" + cap_add: + - IPC_LOCK + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + vscodium: + name: vscodium + driver: bridge + ipam: + config: + - subnet: "10.89.50.0/24" + gateway: "10.89.50.254" + ''; + + extraConfig = { + sops.secrets."vscodium/connection_token" = { + sopsFile = /etc/nixos/secrets/podman/vscodium.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/dns/adguard.nix b/modules/backup/services/dns/adguard.nix new file mode 100644 index 0000000..e08369f --- /dev/null +++ b/modules/backup/services/dns/adguard.nix @@ -0,0 +1,53 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Version tagging + adguardVersion = "latest"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.adguard; + # Container config + name = "adguard"; +in + +helper.mkPodmanService { + inherit name; + description = "AdGuard, feature-rich DNS service"; + defaultPort = "3000"; + scheme = "http"; + dataDirEnabled = false; + startDelay = 10; + dependencies = [ + "network.target" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + + composeText = '' + services: + adguardhome: + image: adguard/adguardhome:${adguardVersion} + container_name: adguard + hostname: adguard + network_mode: pasta + user: '1000:1000' + ports: + - "${cfg.port}:3000/tcp" + - "53:53/tcp" + - "53:53/udp" + volumes: + - ${cfg.configDir}/work:/opt/adguardhome/work + - ${cfg.configDir}/config:/opt/adguardhome/conf + cap_add: + - SYS_NICE + security_opt: + - no-new-privileges:true + restart: unless-stopped + ''; +} \ No newline at end of file diff --git a/modules/backup/services/dns/default.nix b/modules/backup/services/dns/default.nix new file mode 100644 index 0000000..bf6213a --- /dev/null +++ b/modules/backup/services/dns/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + # To test + ./adguard.nix + # Tested + ./pi-hole.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/services/dns/pi-hole.nix b/modules/backup/services/dns/pi-hole.nix new file mode 100644 index 0000000..b5de2f9 --- /dev/null +++ b/modules/backup/services/dns/pi-hole.nix @@ -0,0 +1,71 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Version tagging + piholeVersion = "2026.02.0"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.pi-hole; + # Container config + name = "pi-hole"; + # DNS config + dnsConfig = '' + + ''; +in + +helper.mkPodmanService { + inherit name; + description = "Pi-Hole, the ads black hole"; + defaultPort = "4443"; + scheme = "https"; + dataDirEnabled = false; + startDelay = 10; + dependencies = [ + "network.target" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "pi-hole/web_password" + ]; + +# Compose file good + composeText = '' + services: + pi-hole: + image: docker.io/pihole/pihole:${piholeVersion} + container_name: pi-hole + hostname: pi-hole + network_mode: pasta + ports: + - "${cfg.port}:443/tcp" + - "53:53/tcp" + - "53:53/udp" + volumes: + - ${cfg.configDir}:/etc/pihole + environment: + PIHOLE_UID: '1000' + PIHOLE_GID: '1000' + TZ: ${config.time.timeZone} + FTLCONF_webserver_domain: ${cfg.subdomain}.${config.numbus-server.services.domain} + FTLCONF_dns_domain_name: "${config.numbus-server.services.domain}" + FTLCONF_webserver_api_password: ${config.sops.placeholder."pi-hole/web_password"} + FTLCONF_dns_upstreams: 9.9.9.9;149.112.112.112 + FTLCONF_dns_listeningMode: "BIND" + FTLCONF_dns_domain_local: "true" + FTLCONF_dhcp_active: "false" + FTLCONF_ntp_ipv4_active: "false" + FTLCONF_ntp_ipv6_active: "false" + FTLCONF_ntp_sync_active: "false" + cap_add: + - SYS_NICE + restart: unless-stopped + ''; +} \ No newline at end of file diff --git a/modules/backup/services/system/clamav.nix b/modules/backup/services/system/clamav.nix new file mode 100644 index 0000000..05a0924 --- /dev/null +++ b/modules/backup/services/system/clamav.nix @@ -0,0 +1,91 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus-server.services.clamav; + onAccessPaths = lib.mapAttrsToList (n: v: v.dataDir) (lib.filterAttrs (n: v: + v ? enable && v.enable && v ? dataDir && v.dataDir != null && v.dataDir != false + ) config.numbus-server.services); + clamonacc_virus_notifier = pkgs.writeScript "clamonacc_virus_notifier.sh" '' + #!${pkgs.bash}/bin/bash + + echo "CLAM_VIRUSEVENT_VIRUSNAME=\"$CLAM_VIRUSEVENT_VIRUSNAME\"" > /var/lib/clamav/virus_event.env + echo "CLAM_VIRUSEVENT_FILENAME=\"$CLAM_VIRUSEVENT_FILENAME\"" >> /var/lib/clamav/virus_event.env + + /run/wrappers/bin/sudo /run/current-system/sw/bin/systemctl start clamav-virus-notify.service + ''; +in + +{ + options.numbus-server.services.clamav = { + enable = mkEnableOption "ClamAV open-source anti-virus software"; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.clamav pkgs.curl ]; + + system.activationScripts.clamav-quarantine = '' + mkdir -p /quarantine + chown clamav:clamav /quarantine + chmod 440 /quarantine + ''; + + security.sudo.extraRules = [{ + users = [ "clamav" ]; + commands = [{ + command = "/run/current-system/sw/bin/systemctl start clamav-virus-notify.service"; + options = [ "NOPASSWD" ]; + }]; + }]; + + services.clamav = { + updater.enable = true; + clamonacc.enable = true; + + scanner = { + enable = true; + interval = "*-*-* 04:00:00"; # Everyday at 4am + scanDirectories = [ + "/etc" + "/home" + "/var/lib" + "/var/tmp" + "/tmp" + ]; + }; + + daemon = { + enable = true; + settings = { + OnAccessPrevention = true; + OnAccessIncludePath = onAccessPaths; + VirusEvent = "${clamonacc_virus_notifier}"; + }; + }; + }; + + systemd.services.clamav-periodic-scan = mkIf (onAccessPaths != []) { + description = "Periodic ClamAV virus scan"; + after = [ "clamav-daemon.service" "clamav-freshclam.service" ]; + requires = [ "clamav-daemon.service" ]; + wants = [ "clamav-freshclam.service" ]; + onFailure = [ "clamav-virus-notify.service" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.clamav}/bin/clamdscan --multiscan --fdpass --infected --allmatch --move=/quarantine ${lib.escapeShellArgs onAccessPaths}"; + Slice = "system-clamav.slice"; + }; + }; + + systemd.timers.clamav-periodic-scan = mkIf (onAccessPaths != []) { + description = "Timer for ClamAV periodic scan"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-1/3-01 04:00:00"; + Persistent = true; + Unit = "clamav-periodic-scan.service"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/backup/services/system/default.nix b/modules/backup/services/system/default.nix new file mode 100644 index 0000000..a2c0fb0 --- /dev/null +++ b/modules/backup/services/system/default.nix @@ -0,0 +1,11 @@ +{ ... }: + +{ + imports = [ + # To add +# ./backup-client.nix + # To test + ./clamav.nix + ./virtualization.nix + ]; +} \ No newline at end of file diff --git a/modules/backup/services/system/virtualization.nix b/modules/backup/services/system/virtualization.nix new file mode 100644 index 0000000..873062c --- /dev/null +++ b/modules/backup/services/system/virtualization.nix @@ -0,0 +1,17 @@ +{ config, lib, ... }: + +with lib; + +let + cfg = config.numbus-server.services.virtualization; +in + +{ + options.numbus-server.services.virtualization = { + enable = mkEnableOption "QEMU/KVM virtualization software"; + }; + + config = mkIf cfg.enable { + virtualisation.libvirtd.enable = true; + }; +} \ No newline at end of file diff --git a/modules/common/default.nix b/modules/common/default.nix new file mode 100644 index 0000000..6ed7aa6 --- /dev/null +++ b/modules/common/default.nix @@ -0,0 +1,12 @@ +{ ... }: + +{ + imports = [ + # To test + ./hardware/default.nix + ./mail/default.nix + ./misc/default.nix + ./packages/default.nix + ./global.nix + ]; +} \ No newline at end of file diff --git a/modules/common/global.nix b/modules/common/global.nix new file mode 100644 index 0000000..eaa585c --- /dev/null +++ b/modules/common/global.nix @@ -0,0 +1,42 @@ +{ lib, deviceType, ... }: + +with lib; + +let + cfg = config.numbus; + + country = ""; + language = ""; +in + +{ + options.numbus = { + owner = mkOption { + type = types.str; + example = "Alex"; + default = "Numbus"; + description = "The name of the person who owns this ${deviceType}."; + }; + + internationalization = { + country = mkOption { + type = types.str; + example = "FR"; + default = country; + description = "The country where this ${deviceType} is located."; + }; + language = mkOption { + type = types.str; + example = "fr"; + default = language; + description = "The language for this ${deviceType}."; + }; + locale = mkOption { + type = types.str; + example = "fr_FR"; + default = "fr_FR"; + description = "The locale for this ${deviceType}."; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/common/hardware/cpu.nix b/modules/common/hardware/cpu.nix new file mode 100644 index 0000000..d3fe470 --- /dev/null +++ b/modules/common/hardware/cpu.nix @@ -0,0 +1,9 @@ +{ config, ... }: + +{ + config = { + hardware.enableRedistributableFirmware = true; + hardware.cpu.intel.updateMicrocode = true; + hardware.cpu.amd.updateMicrocode = true; + }; +} \ No newline at end of file diff --git a/modules/common/hardware/default.nix b/modules/common/hardware/default.nix new file mode 100644 index 0000000..d580f5a --- /dev/null +++ b/modules/common/hardware/default.nix @@ -0,0 +1,9 @@ +{ ... }: + +{ + imports = [ + # To test + ./disks/default.nix + ./cpu.nix + ]; +} \ No newline at end of file diff --git a/modules/common/hardware/disks/boot.nix b/modules/common/hardware/disks/boot.nix new file mode 100644 index 0000000..b51087c --- /dev/null +++ b/modules/common/hardware/disks/boot.nix @@ -0,0 +1,192 @@ +{ config, lib, ... }: + +with lib; + +let + cfg = config.numbus.hardware.disks.boot; + + bootCount = builtins.length cfg.list; + + singleDiskConfig = { + disko.devices.disk.main = { + type = "disk"; + device = head cfg.list; + content = { + type = cfg.partitionTableScheme; + partitions = { + ESP = { + size = cfg.partition.boot.size; + type = cfg.partition.boot.esp; + content = { + type = "filesystem"; + format = cfg.partition.boot.filesystem; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + swap = { + size = cfg.partition.swap.size; + content = { + type = "swap"; + randomEncryption = cfg.partition.swap.encrypt; + }; + }; + luks = { + size = cfg.partition.root.size; + content = { + type = "luks"; + name = "boot"; + settings.keyFile = "/run/secrets/disks/boot"; + content = { + type = "filesystem"; + format = cfg.partition.root.filesystem; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; + + raid1DiskConfig = { + disko.devices.disk = lib.listToAttrs (lib.imap0 (i: device: { + name = "boot-${toString i}"; + value = { + type = "disk"; + inherit device; + content = { + type = cfg.partitionTableScheme; + partitions = { + ESP = { size = cfg.partition.boot.size; type = cfg.partition.boot.esp; content = { type = "mdraid"; name = "boot"; }; }; + swap = { size = cfg.partition.swap.size; content = { type = "mdraid"; name = "swap"; }; }; + mdadm = { size = cfg.partition.root.size; content = { type = "mdraid"; name = "raid1"; }; }; + }; + }; + }; + }) cfg.list); + + disko.devices.mdadm = { + boot = { + type = "mdadm"; + level = 1; + metadata = "1.0"; + content = { + type = "filesystem"; + format = cfg.partition.boot.filesystem; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + swap = { + type = "mdadm"; + level = 1; + content = { + type = "swap"; + randomEncryption = cfg.partition.swap.encrypt; + }; + }; + raid1 = { + type = "mdadm"; + level = 1; + content = { + type = "luks"; + name = "boot"; + settings.keyFile = "/run/secrets/disks/boot"; + content = { + type = "filesystem"; + format = cfg.partition.root.filesystem; + mountpoint = "/"; + }; + }; + }; + }; + }; +in + +{ + options.numbus.hardware.disks = { + boot = { + list = mkOption { + type = types.listOf types.str; + example = [ "/dev/disk/by-id/nvme_SAMSUNG_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ]; + description = "A set of by-id path of disk(s) that will be used as boot disk(s). At least one disk must be set."; + }; + partitionTableScheme = mkOption { + type = types.enum [ "gpt" "mbr" ]; + default = "gpt"; + example = "gpt"; + description = "The scheme of the partition table. Use \"gpt\" for modern devices and \"mbr\" for legacy ones."; + }; + partition = { + root = { + filesystem = mkOption { + type = types.enum [ "ext4" "btrfs" "xfs" ]; + default = "ext4"; + example = "ext4"; + description = "The filesystem to use for the root partition of the boot disk(s)."; + }; + size = mkOption { + type = types.str; + default = "100%"; + example = "100%"; + description = "The size of the root partition. Use G for GBs and M for MBs."; + }; + }; + boot = { + filesystem = mkOption { + type = types.enum [ "vfat" ]; + default = "vfat"; + example = "vfat"; + description = "The filesystem to use for the boot partition of the boot disk(s)."; + }; + esp = mkOption { + type = types.enum [ "EF00" "EF02" ]; + default = "EF00"; + example = "EF00"; + description = "The ESP type to use for the boot partition. Use EF02 for UEFI and EF00 for BIOS."; + }; + size = mkOption { + type = types.str; + default = "1G"; + example = "1G"; + description = "The size of the boot partition."; + }; + }; + swap = { + enable = mkOption { + type = types.bool; + default = true; + example = true; + description = "Wether to create a swap partition. Useful for servers that don't have a lot of RAM."; + }; + encrypt = mkOption { + type = types.bool; + default = true; + example = true; + description = "Wether to encrypt randomly the swap partition. Disable if you need hibernation"; + }; + size = mkOption { + type = types.str; + default = "16G"; + example = "16G"; + description = "Size of the swap partition. Use G for GBs and M for MBs."; + }; + }; + }; + }; + }; + + config = mkMerge [ + { + sops.secrets."disks/boot" = { + sopsFile = "/etc/nixos/secrets/disks/boot.yaml"; + gid = "0"; + uid = "0"; + mode = "0400"; + }; + } + (mkIf (bootCount == 1) singleDiskConfig) + (mkIf (bootCount == 2) raid1DiskConfig) + ]; +} \ No newline at end of file diff --git a/modules/common/hardware/disks/content.nix b/modules/common/hardware/disks/content.nix new file mode 100644 index 0000000..a960cdb --- /dev/null +++ b/modules/common/hardware/disks/content.nix @@ -0,0 +1,107 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus.hardware.disks.content; + + contentCount = builtins.length cfg.list; + parityCount = builtins.length config.numbus.hardware.disks.parity.list; + + contentDisks = lib.imap0 (i: device: { + name = "content-${toString i}"; + value = { + type = "disk"; + inherit device; + content = { + type = cfg.partitionTableScheme; + partitions.luks = { + size = cfg.partition.size; + content = { + type = "luks"; + name = "content-${toString i}"; + settings.keyFile = "/run/secrets/disks/content-${toString i}"; + initrdUnlock = false; + content = { + type = "filesystem"; + format = cfg.partition.filesystem; + mountpoint = "/mnt/content-${toString i}"; + mountOptions = [ "noauto" "nofail" ]; + }; + }; + }; + }; + }; + }) cfg.list; +in + +{ + options.numbus.hardware.disks = { + content = { + list = mkOption { + type = types.listOf types.str; + example = [ "/dev/disk/by-id/ata_Hitachi_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-WD_159Ejz224G" ]; + default = []; + description = "A set of by-id path of disk(s) that will be used as content disk(s)."; + }; + partitionTableScheme = mkOption { + type = types.enum [ "gpt" "mbr" ]; + default = "gpt"; + example = "gpt"; + description = "The scheme of the partition table. Use \"gpt\" for modern devices and \"mbr\" for legacy ones."; + }; + partition = { + filesystem = mkOption { + type = types.enum [ "ext4" "btrfs" "xfs" ]; + default = "xfs"; + example = "xfs"; + description = "The filesystem to use for the main partition of the content disk(s)."; + }; + size = mkOption { + type = types.str; + default = "100%"; + example = "100%"; + description = "The size of the main partition. Use G for GBs and M for MBs."; + }; + }; + }; + }; + + config = mkIf (contentCount > 0 && (parityCount != 1 && contentCount != 1)) { + disko.devices.disk = builtins.listToAttrs contentDisks; + + sops.secrets = listToAttrs (map (i: + nameValuePair "disks/content-${toString i}" { + sopsFile = "/etc/nixos/secrets/disks/content.yaml"; + gid = "0"; + uid = "0"; + mode = "0400"; + } + ) (range 0 (contentCount - 1))); + + systemd.services.mount-content-disks = { + description = "Mount content disks."; + before = [ "mnt-data.mount" ]; + requiredBy = [ "mnt-data.mount" ]; + requires = [ "sops-install-secrets.service" ]; + path = [ pkgs.cryptsetup pkgs.util-linux ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = let + mountContentDisk = i: '' + if [ ! -e /dev/mapper/content-${toString i} ]; then + cryptsetup luksOpen --key-file /run/secrets/disks/content-${toString i} /dev/disk/by-partlabel/disk-content-${toString i}-luks content-${toString i} + fi + mkdir -p /mnt/content-${toString i} + if ! mountpoint -q /mnt/content-${toString i}; then + mount -t ${cfg.partition.filesystem} /dev/mapper/content-${toString i} /mnt/content-${toString i} + fi + ''; + in '' + ${concatMapStrings mountContentDisk (range 0 (contentCount - 1))} + ''; + }; + }; +} \ No newline at end of file diff --git a/modules/common/hardware/disks/default.nix b/modules/common/hardware/disks/default.nix new file mode 100644 index 0000000..1485d81 --- /dev/null +++ b/modules/common/hardware/disks/default.nix @@ -0,0 +1,12 @@ +{ ... }: + +{ + imports = [ + ./boot.nix + ./content.nix + ./mergerfs-snapraid.nix + ./mirror.nix + ./parity.nix + ./spindown.nix + ]; +} \ No newline at end of file diff --git a/modules/common/hardware/disks/mergerfs-snapraid.nix b/modules/common/hardware/disks/mergerfs-snapraid.nix new file mode 100644 index 0000000..76d88d7 --- /dev/null +++ b/modules/common/hardware/disks/mergerfs-snapraid.nix @@ -0,0 +1,41 @@ +{ config, lib, ... }: + +with lib; + +let + cfg = config.numbus.hardware.disks; + + contentCount = builtins.length cfg.content.list; + parityCount = builtins.length cfg.parity.list; +in + +{ + config = mkIf (contentCount >= 2 && parityCount >= 1) { + services.snapraid = { + enable = true; + contentFiles = map (i: "/mnt/content-${toString i}/snapraid.content") (range 0 (contentCount - 1)); + parityFiles = map (i: "/mnt/parity-${toString i}/snapraid.parity") (range 0 (parityCount - 1)); + dataDisks = listToAttrs (imap0 (i: _: nameValuePair "d${toString i}" "/mnt/content-${toString i}") cfg.content.list); + }; + + fileSystems."/mnt/data" = { + device = concatStringsSep ":" (map (i: "/mnt/content-${toString i}") (range 0 (contentCount - 1))); + fsType = "fuse.mergerfs"; + options = [ + "category.create=ff" + "cache.files=partial" + "dropcacheonclose=true" + "defaults" + "noauto" + "nofail" + "allow_other" + "moveonenospc=1" + "minfreespace=50G" + "func.getattr=newest" + "fsname=mergerfs_data" + "x-mount.mkdir" + "x-systemd.automount" + ]; + }; + }; +} \ No newline at end of file diff --git a/modules/common/hardware/disks/mirror.nix b/modules/common/hardware/disks/mirror.nix new file mode 100644 index 0000000..8939d94 --- /dev/null +++ b/modules/common/hardware/disks/mirror.nix @@ -0,0 +1,75 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus.hardware.disks; + + contentCount = builtins.length cfg.content.list; + parityCount = builtins.length cfg.parity.list; + + dataMirror = { + disko.devices.disk = listToAttrs (imap0 (i: device: { + name = "mirror-${toString i}"; + value = { + type = "disk"; + inherit device; + content = { + type = cfg.partitionTableScheme; + partitions.raid = { + size = cfg.content.partition.size; + content = { + type = "mdraid"; + name = "mirror"; + }; + }; + }; + }; + }) (cfg.content.list ++ cfg.parity.list)); + + disko.devices.mdadm.mirror = { + type = "mdadm"; + level = 1; + content = { + type = "luks"; + name = "mirror"; + settings.keyFile = "/run/secrets/disks/mirror"; + initrdUnlock = false; + content = { + type = "filesystem"; + format = cfg.content.partition.filesystem; + mountpoint = "/mnt/data"; + mountOptions = [ "noauto" "nofail" ]; + }; + }; + }; + }; +in + +{ + config = mkIf (contentCount == 1 && parityCount == 1) (mkMerge [ + dataMirror + { + systemd.services.mount-mirror = { + description = "Mount the disks mirror."; + before = [ "mnt-data.mount" ]; + requiredBy = [ "mnt-data.mount" ]; + requires = [ "sops-install-secrets.service" ]; + path = [ pkgs.cryptsetup pkgs.util-linux ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if [ ! -e /dev/mapper/mirror ]; then + cryptsetup open /dev/md/mirror mirror --key-file /run/secrets/disks/mirror + fi + mkdir -p /mnt/data + if ! mountpoint -q /mnt/data; then + mount -t ${cfg.content.partition.filesystem} /dev/mapper/mirror /mnt/data + fi + ''; + }; + } + ]); +} \ No newline at end of file diff --git a/modules/common/hardware/disks/parity.nix b/modules/common/hardware/disks/parity.nix new file mode 100644 index 0000000..85ea5c7 --- /dev/null +++ b/modules/common/hardware/disks/parity.nix @@ -0,0 +1,107 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus.hardware.disks.parity; + + parityCount = builtins.length cfg.list; + + parityDisks = lib.imap0 (i: device: { + name = "parity-${toString i}"; + value = { + type = "disk"; + inherit device; + content = { + type = cfg.partitionTableScheme; + partitions.luks = { + size = cfg.partition.size; + content = { + type = "luks"; + name = "parity-${toString i}"; + settings.keyFile = "/run/secrets/disks/parity-${toString i}"; + initrdUnlock = false; + content = { + type = "filesystem"; + format = cfg.partition.filesystem; + mountpoint = "/mnt/parity-${toString i}"; + mountOptions = [ "noauto" "nofail" ]; + }; + }; + }; + }; + }; + }) cfg.list; +in + +{ + options.numbus.hardware.disks = { + parity = { + list = mkOption { + type = types.listOf types.str; + example = [ "/dev/disk/by-id/ata_WDC_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ]; + default = []; + description = "A set of by-id path of disk(s) that will be used as parity disk(s)."; + }; + partitionTableScheme = mkOption { + type = types.enum [ "gpt" "mbr" ]; + default = "gpt"; + example = "gpt"; + description = "The scheme of the partition table. Use \"gpt\" for modern devices and \"mbr\" for legacy ones."; + }; + partition = { + filesystem = mkOption { + type = types.enum [ "ext4" "btrfs" "xfs" ]; + default = "xfs"; + example = "xfs"; + description = "The filesystem to use for the main partition of the parity disk(s)."; + }; + size = mkOption { + type = types.str; + default = "100%"; + example = "100%"; + description = "The size of the main partition. Use G for GBs and M for MBs."; + }; + }; + }; + }; + + config = mkIf (parityCount > 1) { + disko.devices.disk = builtins.listToAttrs parityDisks; + + sops.secrets = listToAttrs (map (i: + nameValuePair "disks/parity-${toString i}" { + sopsFile = "/etc/nixos/secrets/disks/parity.yaml"; + gid = "0"; + uid = "0"; + mode = "0400"; + } + ) (range 0 (parityCount - 1))); + + systemd.services.mount-parity-disks = { + description = "Mount parity disks."; + before = [ "mnt-data.mount" ]; + requiredBy = [ "mnt-data.mount" ]; + requires = [ "sops-install-secrets.service" ]; + path = [ pkgs.cryptsetup pkgs.util-linux ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = let + mountparityDisk = i: '' + if [ ! -e /dev/mapper/parity-${toString i} ]; then + cryptsetup luksOpen --key-file /run/secrets/disks/parity-${toString i} /dev/disk/by-partlabel/disk-parity-${toString i}-luks parity-${toString i} + fi + mkdir -p /mnt/parity-${toString i} + if ! mountpoint -q /mnt/parity-${toString i}; then + mount -t ${cfg.partition.filesystem} /dev/mapper/parity-${toString i} /mnt/parity-${toString i} + fi + ''; + in + '' + ${concatMapStrings mountparityDisk (range 0 (parityCount - 1))} + ''; + }; + }; +} \ No newline at end of file diff --git a/modules/common/hardware/disks/spindown.nix b/modules/common/hardware/disks/spindown.nix new file mode 100644 index 0000000..f373250 --- /dev/null +++ b/modules/common/hardware/disks/spindown.nix @@ -0,0 +1,46 @@ +{ config, lib, ... }: + +with lib; + +let + hardDrives = config.numbus.hardware.spindown.list; + cfg = config.numbus.hardware; +in + +{ + config = mkIf (cfg.HddSpindown.enable == true) { + systemd.services.hd-idle = { + description = "External HD spin down daemon"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = + let + idleTime = toString 1800; + hardDriveParameter = lib.strings.concatMapStringsSep " " (x: "-a ${x} -i ${idleTime}") hardDrives; + in + "${pkgs.hd-idle}/bin/hd-idle -i 0 ${hardDriveParameter}"; + }; + }; + }; + + options.numbus = { + hardware = { + spindown = { + enable = mkEnableOption "hard drives spin down when inactive in order to save power."; + list = mkOption { + description = "The list of compatible hard drives that will spin down."; + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/ata_Hitachi_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-WD_159Ejz224G" ]; + }; + optimize = mkOption { + description = "Optimize services to reduce HDD wakeups when spindown is enabled. Can be set to \"compatible\" to optimize all compatible services, or a list of service names to optimize."; + type = types.nullOr (types.either (types.enum [ "compatible" ]) (types.listOf types.str)); + default = "compatible"; + example = "[ \"crafty\" \"gitea\" ]"; + }; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/common/mail/default.nix b/modules/common/mail/default.nix new file mode 100644 index 0000000..1de7393 --- /dev/null +++ b/modules/common/mail/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + # To test + ./disk-space.nix + ./smart.nix + ./smtp.nix + ]; +} \ No newline at end of file diff --git a/modules/common/mail/disk-space.nix b/modules/common/mail/disk-space.nix new file mode 100644 index 0000000..e7d0ba5 --- /dev/null +++ b/modules/common/mail/disk-space.nix @@ -0,0 +1,130 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus-server.services.disk-space-checker; + + disk_space_notifier = pkgs.writeScript "disk-space-notifier.sh" '' + #!${pkgs.bash}/bin/bash + + ALERT_FILE="/var/lib/numbus-server/disk_alert.env" + if [ ! -f "$ALERT_FILE" ]; then + exit 0 + fi + + source "$ALERT_FILE" + rm "$ALERT_FILE" + + # Update the timestamp for this specific path to prevent spamming + SAFE_PATH=$(echo "$DISK_ALERT_PATH" | tr '/' '_') + date +%s > "/var/lib/numbus-server/last_alert_$SAFE_PATH.ts" + + ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}" + USER_EMAIL="${config.numbus-server.mail.userAddress}" + OWNER_NAME="${config.numbus-server.owner}" + + SUBJECT="Numbus Server Alert: Low Disk Space Detected" + + TECH_BODY=" + Disk Space Alert: + Server owner: $OWNER_NAME + + The following mount point has exceeded the safety threshold: + Mount: $DISK_ALERT_PATH + Usage: $DISK_ALERT_USAGE% + + Full partition details: + $(df -h "$DISK_ALERT_PATH") + + Action required: Please investigate and clear space or expand the storage capacity. + " + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + L'espace de stockage de votre serveur Numbus est presque saturé. + Disque concerné : $DISK_ALERT_PATH ($DISK_ALERT_USAGE% utilisé) + + Votre administrateur a été notifié avec les détails techniques. + Nous vous conseillons d'éviter d'ajouter des fichiers volumineux pour garantir le bon fonctionnement de vos services. + Contactez votre administrateur afin d'évoquer les possibilités d'expansion du stockage. + " + + printf "Subject: [ADMIN] %s\n\n%s" "$SUBJECT" "$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" + printf "Subject: [Alerte] Espace disque presque saturé sur votre serveur Numbus\n\n%s\n\nMerci de votre confiance,\nL'équipe de support,\nNumbus-Server." "$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" + ''; + + disk_space_checker = pkgs.writeScript "disk-space-checker.sh" '' + #!${pkgs.bash}/bin/bash + + # Safety threshold in percentage + THRESHOLD=90 + # Paths to monitor (Root and MergerFS data pool) + PATHS=("/" "/mnt/data") + ALERT_FILE="/var/lib/numbus-server/disk_alert.env" + + for path in "''${PATHS[@]}"; do + # Skip if path does not exist (e.g. if mergerfs is not mounted yet) + if [ ! -d "$path" ]; then + continue + fi + + # Anti-spam logic: Check if we alerted on this path recently (7 days = 604800 seconds) + SAFE_PATH=$(echo "$path" | tr '/' '_') + TS_FILE="/var/lib/numbus-server/last_alert_$SAFE_PATH.ts" + NOW=$(date +%s) + + if [ -f "$TS_FILE" ]; then + LAST_SENT=$(cat "$TS_FILE") + DIFF=$((NOW - LAST_SENT)) + if [ "$DIFF" -lt 604800 ]; then + echo "Alert for $path was sent recently. Skipping notification to avoid spam." + continue + fi + fi + + # Extract usage percentage using df + USAGE=$(df -h "$path" | awk 'NR==2 {print $5}' | sed 's/%//') + + if [ "$USAGE" -ge "$THRESHOLD" ]; then + echo "DISK_ALERT_PATH=$path" > "$ALERT_FILE" + echo "DISK_ALERT_USAGE=$USAGE" >> "$ALERT_FILE" + + echo "Threshold exceeded for $path ($USAGE%). Triggering notification." + + # Trigger the notification service + /run/current-system/sw/bin/systemctl start disk-space-notifier.service + + # We exit after the first alert to avoid multiple overlapping emails in one run + exit 0 + fi + done + ''; +in + +{ + config = mkIf cfg.enable { + systemd.services.disk-space-notifier = { + description = "Email notification for low disk space"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${disk_space_notifier}"; + }; + }; + systemd.services.disk-space-checker = { + description = "Check for low disk space"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${disk_space_checker}"; + }; + }; + systemd.timers.disk-space-checker = { + description = "Run disk space check every day"; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + }; + wantedBy = [ "timers.target" ]; + }; + }; +} \ No newline at end of file diff --git a/modules/common/mail/smart.nix b/modules/common/mail/smart.nix new file mode 100644 index 0000000..45371dc --- /dev/null +++ b/modules/common/mail/smart.nix @@ -0,0 +1,61 @@ +{ config, pkgs, ... }: + +let + smartd_notifier = pkgs.writeScript "smartd-notify.sh" '' + #!${pkgs.bash}/bin/bash + + # 1. Send Technical Email to Admin + ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}" + SUBJECT="Numbus Server Alert: $SMARTD_FAILTYPE on $SMARTD_DEVICE" + + TECH_BODY=" + SMARTD Alert Details: + Server owner: $OWNER_NAME + Device: $SMARTD_DEVICE + Type: $SMARTD_DEVICETYPE + Failure Type: $SMARTD_FAILTYPE + Message: $SMARTD_MESSAGE + + Full Message: + $SMARTD_FULLMESSAGE + " + printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" + + # 2. Send Friendly Email to Owner + USER_EMAIL="${config.numbus-server.mail.userAddress}" + OWNER_NAME="${config.numbus-server.owner}" + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + Votre serveur a automatiquement détecté une panne matérielle de disque dur. + Ce genre de panne est tout à fait normal selon l'âge de votre matériel et n'entraîne + dans la grande majorité des cas aucune perte de données grâce au système de + stockage redondant préventif. + + Votre administrateur a été notifié de cette panne. Il vous recontactera dans de très + brefs délais afin de procéder au remplacement, si nécessaire, du disque dur défaillant. + + Merci de votre confiance, + L'équipe de support, + Numbus-Server." + + printf "Subject: [Alerte] Défaillance matérielle sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" + ''; +in + +{ + services.smartd = { + enable = true; + defaults.autodetected = "-a -o on -S on -s (S/../.././00|L/../../6/01) -n standby,q -M exec ${smartd_notifier}"; + notifications = { + wall = { + enable = true; + }; + mail = { + enable = true; + sender = config.numbus-server.mail.fromAddress; + recipient = "${config.numbus-server.mail.userAddress},${config.numbus-server.mail.adminAddress}"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/common/mail/smtp.nix b/modules/common/mail/smtp.nix new file mode 100644 index 0000000..a414098 --- /dev/null +++ b/modules/common/mail/smtp.nix @@ -0,0 +1,99 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.numbus.mail; +in + +{ + options.numbus.mail = { + enable = mkEnableOption "Email sending functionality"; + + userAddress = mkOption { + description = "The address of the user this server will send emails to"; + type = types.str; + example = "user@your-domain.com"; + }; + + adminAddress = mkOption { + description = "The address of the admin this server will send emails to"; + type = types.str; + example = "admin@your-domain.com"; + }; + + smtpUsername = mkOption { + description = "The username/email that will be use to authenticate to the SMTP server"; + type = types.str; + example = "your-smtp-enabled-address@your-domain.com"; + }; + + smtpPasswordPath = mkOption { + description = "The path to a file containing the password that will be use to authenticate to the SMTP server"; + type = types.path; + example = /run/secrets/system/mail/smtpPassword; + }; + + fromAddress = mkOption { + description = "This server will send emails from this address"; + type = types.str; + default = "numbus-server-noreply@${config.numbus.services.domain}"; + example = "numbus-server-noreply@your-domain.com"; + }; + + smtpServer = mkOption { + description = "The SMTP server address your server will use to send emails"; + type = types.str; + default = "smtp.gmail.com"; + example = "smtp.your-provider.com"; + }; + + smtpPort = mkOption { + description = "The SMTP port your server will connect to to send emails"; + type = types.port; + default = 587; + example = 587; + }; + + smtpEncryption = mkOption { + description = "The encryption method for SMTP : NONE (NOT RECOMMENDED), TLS (port 465, also called SSL), or STARTTLS (port 587). STARTTLS is recommended."; + type = types.enum [ "NONE" "TLS" "STARTTLS" ]; + default = "STARTTLS"; + example = "STARTTLS"; + }; + }; + + config = mkIf cfg.enable { + sops.secrets."smtpPassword" = { + sopsFile = /etc/nixos/secrets/system/mail.yaml; + owner = "numbus-admin"; + mode = "0600"; + }; + + environment.etc."aliases" ={ + mode = "0440"; + text = '' + root: ${cfg.userAddress}, ${cfg.adminAddress} + ''; + }; + + programs.msmtp = { + enable = true; + defaults = { + aliases = "/etc/aliases"; + timeout = 60; + syslog = "on"; + }; + accounts.default = { + auth = true; + host = cfg.smtpServer; + port = cfg.smtpPort; + from = cfg.fromAddress; + user = cfg.smtpUsername; + tls = true; + tls_starttls = true; + passwordeval = "${pkgs.coreutils}/bin/cat ${cfg.smtpPasswordPath}"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/common/misc/audio.nix b/modules/common/misc/audio.nix new file mode 100644 index 0000000..7016402 --- /dev/null +++ b/modules/common/misc/audio.nix @@ -0,0 +1,15 @@ +{ config, deviceType, ... }: + +{ + config = mkIf (deviceType == "computer" || deviceType == "tv" ) { + # Enable sound with pipewire. + services.pulseaudio.enable = false; + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + }; + }; +} \ No newline at end of file diff --git a/modules/common/misc/default.nix b/modules/common/misc/default.nix new file mode 100644 index 0000000..e441233 --- /dev/null +++ b/modules/common/misc/default.nix @@ -0,0 +1,13 @@ +{ ... }: + +{ + imports = [ + # To test + ./audio.nix + ./internationalisation.nix + ./power.nix + ./printer.nix + ./update.nix + ./users.nix + ]; +} \ No newline at end of file diff --git a/modules/common/misc/internationalization.nix b/modules/common/misc/internationalization.nix new file mode 100644 index 0000000..d08d2fe --- /dev/null +++ b/modules/common/misc/internationalization.nix @@ -0,0 +1,30 @@ +{ config, lib, ... }: + +with lib; + +let + cfg = config.numbus.internationalization; +in + +{ + config = { + i18n.defaultLocale = "${cfg.locale}.UTF-8"; + i18n.extraLocaleSettings = { + LC_ADDRESS = "${cfg.locale}.UTF-8"; + LC_IDENTIFICATION = "${cfg.locale}.UTF-8"; + LC_MEASUREMENT = "${cfg.locale}.UTF-8"; + LC_MONETARY = "${cfg.locale}.UTF-8"; + LC_NAME = "${cfg.locale}.UTF-8"; + LC_NUMERIC = "${cfg.locale}.UTF-8"; + LC_PAPER = "${cfg.locale}.UTF-8"; + LC_TELEPHONE = "${cfg.locale}.UTF-8"; + LC_TIME = "${cfg.locale}.UTF-8"; + }; + + console.keyMap = toLower cfg.language; + services.xserver.xkb = { + layout = toLower cfg.language; + variant = ""; + }; + }; +} \ No newline at end of file diff --git a/modules/common/misc/power.nix b/modules/common/misc/power.nix new file mode 100644 index 0000000..eb4848a --- /dev/null +++ b/modules/common/misc/power.nix @@ -0,0 +1,12 @@ +{ config, lib, pkgs, ... }: + +{ + config = { + services.autoaspm.enable = true; + powerManagement.powertop.enable = true; + boot.kernelParams = [ + "pcie_aspm=force" + "consoleblank=60" + ]; + }; +} \ No newline at end of file diff --git a/modules/common/misc/printer.nix b/modules/common/misc/printer.nix new file mode 100644 index 0000000..bd760d0 --- /dev/null +++ b/modules/common/misc/printer.nix @@ -0,0 +1,8 @@ +{ config, ... }: + +{ + config = mkIf (deviceType == "computer" || deviceType == "tv" ) { + # Enable CUPS to print documents. + services.printing.enable = true; + }; +} \ No newline at end of file diff --git a/modules/common/misc/update.nix b/modules/common/misc/update.nix new file mode 100644 index 0000000..26b22dd --- /dev/null +++ b/modules/common/misc/update.nix @@ -0,0 +1,23 @@ +{ config, inputs, ... }: + +{ + config = { + system.autoUpgrade = { + enable = true; + allowReboot = false; + flake = inputs.self.outPath; + flags = [ "--print-build-logs" ]; + dates = "02:00"; + randomizedDelaySec = "45min"; + }; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 7d"; + }; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.auto-optimise-store = true; + }; +} \ No newline at end of file diff --git a/modules/common/misc/users.nix b/modules/common/misc/users.nix new file mode 100644 index 0000000..6216e91 --- /dev/null +++ b/modules/common/misc/users.nix @@ -0,0 +1,20 @@ +{ config, pkgs, ... }: + +let + cfg = config.numbus; +in + +{ + users.users.numbus-admin = { + shell = pkgs.fish; + isNormalUser = true; + description = cfg.owner; + extraGroups = [ "wheel" ]; + uid = 1000; + initialPassword = "changeMe!"; + # required for auto start before user login + linger = true; + # required for rootless container with multiple users + autoSubUidGidRange = true; + }; +} \ No newline at end of file diff --git a/modules/common/packages/default.nix b/modules/common/packages/default.nix new file mode 100644 index 0000000..c882f63 --- /dev/null +++ b/modules/common/packages/default.nix @@ -0,0 +1,13 @@ +{ ... }: + +{ + imports = [ + # To test + ./fail2ban.nix + ./flatpaks.nix + ./numbus-cli.nix + ./ssh.nix + ./terminal.nix + ./updates.nix + ]; +} \ No newline at end of file diff --git a/modules/common/packages/fail2ban.nix b/modules/common/packages/fail2ban.nix new file mode 100644 index 0000000..b70574e --- /dev/null +++ b/modules/common/packages/fail2ban.nix @@ -0,0 +1,5 @@ +{ config, ... }: + +{ + services.fail2ban.enable = true; +} \ No newline at end of file diff --git a/modules/common/packages/flatpaks.nix b/modules/common/packages/flatpaks.nix new file mode 100644 index 0000000..3d257e5 --- /dev/null +++ b/modules/common/packages/flatpaks.nix @@ -0,0 +1,16 @@ +{ config, lib, ... }: + +with lib; + +{ + config = mkIf (services.flatpak.packages != []) { + services.flatpak.enable = true; + services.flatpak.update.auto.enable = true; + services.flatpak.uninstallUnmanaged = true; + + services.flatpak.remotes = mkOptionDefault [{ + name = "flathub"; + location = "https://dl.flathub.org/repo/flathub.flatpakrepo"; + }]; + }; +} \ No newline at end of file diff --git a/modules/common/packages/numbus-cli.nix b/modules/common/packages/numbus-cli.nix new file mode 100644 index 0000000..53a8ca9 --- /dev/null +++ b/modules/common/packages/numbus-cli.nix @@ -0,0 +1,155 @@ +{ pkgs, lib, ... }: + +let + # Base script header and common setup for all device types + baseScriptHeader = '' + #!/usr/bin/env bash + set -euo pipefail + + # The device type is baked into the script at build time + readonly NUMBUS_DEVICE_TYPE="${deviceType}" + + # Common utility function for consistent output + numbus_echo() { + echo "[Numbus CLI - $NUMBUS_DEVICE_TYPE] $*" + } + ''; + + # --- Device-specific script definitions --- + + serverScript = baseScriptHeader + '' + case "$1" in + test) + numbus_echo "Hello World! This is a Numbus Server." + ;; + status) + numbus_echo "Checking system status for Server..." + numbus_echo "--- Podman Containers ---" + podman ps || numbus_echo "No Podman containers found or Podman not running." + systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found." + ;; + upgrade) + numbus_echo "Pulling latest configuration and upgrading for Server..." + # Add server-specific upgrade logic here (e.g., nixos-rebuild switch) + ;; + *) + numbus_echo "Numbus CLI (Server edition)" + echo "" + echo "Usage: numbus " + echo "" + echo "Commands:" + echo " test - Print a test message" + numbus_echo " status - Show status of Numbus services (Podman, systemd)" + numbus_echo " upgrade - Upgrade the server configuration" + ;; + esac + ''; + + backupScript = baseScriptHeader + '' + case "$1" in + test) + numbus_echo "Hello World! This is a Numbus Backup Server." + ;; + status) + numbus_echo "Checking system status for Backup Server..." + systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found." + # Add backup-specific status checks here (e.g., SnapRAID status, rsync jobs) + ;; + restore) + numbus_echo "Starting interactive restore wizard for Backup Server..." + # Add backup-specific restore logic here + ;; + upgrade) + numbus_echo "Pulling latest configuration and upgrading for Backup Server..." + # Add backup-specific upgrade logic here + ;; + *) + numbus_echo "Numbus CLI (Backup Server edition)" + echo "" + echo "Usage: numbus " + echo "" + echo "Commands:" + numbus_echo " test - Print a test message" + numbus_echo " status - Show status of Numbus services" + numbus_echo " restore - Start interactive restore wizard" + numbus_echo " upgrade - Upgrade the backup server configuration" + ;; + esac + ''; + + computerScript = baseScriptHeader + '' + case "$1" in + test) + numbus_echo "Hello World! This is a Numbus Computer." + ;; + status) + numbus_echo "Checking system status for Computer..." + systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found." + # Add computer-specific status checks (e.g., GPU status, Flatpak updates) + ;; + upgrade) + numbus_echo "Pulling latest configuration and upgrading for Computer..." + # Add computer-specific upgrade logic here + ;; + *) + numbus_echo "Numbus CLI (Computer edition)" + echo "" + echo "Usage: numbus " + echo "" + echo "Commands:" + numbus_echo " test - Print a test message" + numbus_echo " status - Show status of Numbus services" + numbus_echo " upgrade - Upgrade the computer configuration" + ;; + esac + ''; + + tvScript = baseScriptHeader + '' + case "$1" in + test) + numbus_echo "Hello World! This is a Numbus TV." + ;; + status) + numbus_echo "Checking system status for TV..." + systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found." + # Add TV-specific status checks (e.g., media server status, remote connectivity) + ;; + remote) + numbus_echo "Pairing a new Bluetooth remote for TV..." + # Add TV-specific remote pairing logic here + ;; + upgrade) + numbus_echo "Pulling latest configuration and upgrading for TV..." + # Add TV-specific upgrade logic here + ;; + *) + numbus_echo "Numbus CLI (TV edition)" + echo "" + echo "Usage: numbus " + echo "" + numbus_echo "Commands:" + numbus_echo " test - Print a test message" + numbus_echo " status - Show status of Numbus services" + numbus_echo " remote - Pair a new Bluetooth remote" + numbus_echo " upgrade - Upgrade the TV configuration" + ;; + esac + ''; + + # Use lib.switch to select the correct script based on deviceType + selectedScript = lib.switch deviceType { + server = serverScript; + backup = backupScript; + computer = computerScript; + tv = tvScript; + } (throw "Unknown Numbus device type: ${deviceType}"); # Fail if an unknown deviceType is encountered + + # Define the numbus-cli package using the selected script + numbus = pkgs.writeShellScriptBin "numbus" selectedScript; + +in { + environment.systemPackages = [ numbus ]; + + # Add a useful alias so people can check the type via env + environment.variables.NUMBUS_DEVICE_TYPE = deviceType; +} \ No newline at end of file diff --git a/modules/common/packages/ssh.nix b/modules/common/packages/ssh.nix new file mode 100644 index 0000000..a5e7e60 --- /dev/null +++ b/modules/common/packages/ssh.nix @@ -0,0 +1,21 @@ +{ config, ... }: + +{ + config.services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + PermitRootLogin = "no"; + }; + AllowUsers = [ "numbus-admin" ]; + ports = [ 245 ] + }; + + config.sops.secrets."authorizedSshPublicKeys" = { + sopsFile = /etc/nixos/secrets/system/ssh.yaml; + mode = "0440"; + owner = "numbus-admin"; + path = "/home/numbus-admin/.ssh/authorized_keys"; + }; +} \ No newline at end of file diff --git a/modules/common/packages/terminal.nix b/modules/common/packages/terminal.nix new file mode 100644 index 0000000..7fd47b6 --- /dev/null +++ b/modules/common/packages/terminal.nix @@ -0,0 +1,24 @@ +{ config, pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ + fish + fishPlugins.fzf-fish + fishPlugins.grc + grc + fzf + ]; + + programs.fish = { + enable = true; + interactiveShellInit = '' + set fish_greeting # Disable greeting + fastfetch + echo -e "\n\nWelcome to Numbus !\n\n- This system is managed by NixOS\n- All changes are futile\n- Please consider buying support to get assistance\n- Have a nice day and enjoy !" + ''; + shellAliases = { + nixup = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch --upgrade && cd -"; + nixwitch = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch && cd -"; + }; + }; +} \ No newline at end of file diff --git a/modules/common/packages/updates.nix b/modules/common/packages/updates.nix new file mode 100644 index 0000000..b6f8312 --- /dev/null +++ b/modules/common/packages/updates.nix @@ -0,0 +1,23 @@ +{ config, inputs, ... }: + +{ + config = { + system.autoUpgrade = { + enable = true; + allowReboot = false; + flake = inputs.self.outPath; + flags = [ "--print-build-logs" ]; + dates = "21:00"; + randomizedDelaySec = "45min"; + }; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 7d"; + }; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.auto-optimise-store = true; + }; +} \ No newline at end of file diff --git a/modules/computer/default.nix b/modules/computer/default.nix new file mode 100644 index 0000000..15239bd --- /dev/null +++ b/modules/computer/default.nix @@ -0,0 +1,9 @@ +{ ... }: + +{ + imports=[ + ./hardware/default.nix + ./misc/default.nix + ./packages/default.nix + ]; +} \ No newline at end of file diff --git a/modules/computer/global.nix b/modules/computer/global.nix new file mode 100644 index 0000000..35a7bb5 --- /dev/null +++ b/modules/computer/global.nix @@ -0,0 +1,32 @@ +{ lib, ... }: + +with lib; + +{ + options.numbus-computer = { + owner = mkOption { + type = types.str; + example = "Alex"; + default = "Numbus"; + description = "The name of the person who owns this computer"; + }; + language = mkOption { + type = types.str; + example = "FR"; + default = "FR"; + description = "The language for this computer"; + }; + keyboardLayout = mkOption { + type = types.str; + example = "FR"; + default = "FR"; + description = "The keyboard layout for this computer"; + }; + locale = mkOption { + type = types.str; + example = "fr_FR"; + default = "fr_FR"; + description = "The default locale for this computer"; + }; + }; +} \ No newline at end of file diff --git a/modules/computer/hardware/boot.nix b/modules/computer/hardware/boot.nix new file mode 100644 index 0000000..3812345 --- /dev/null +++ b/modules/computer/hardware/boot.nix @@ -0,0 +1,24 @@ +{ config, ... }: + +{ + config = { + # Bootloader options + boot.initrd.systemd.enable = true; + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + # Boot splash instead of log messages + boot = { + plymouth.enable = true; + # Enable "Silent boot" + consoleLogLevel = 3; + initrd.verbose = false; + kernelParams = [ + "quiet" + "udev.log_level=3" + "systemd.show_status=auto" + ]; + loader.timeout = 1; + }; + }; +} \ No newline at end of file diff --git a/modules/computer/hardware/cpu.nix b/modules/computer/hardware/cpu.nix new file mode 100644 index 0000000..d3fe470 --- /dev/null +++ b/modules/computer/hardware/cpu.nix @@ -0,0 +1,9 @@ +{ config, ... }: + +{ + config = { + hardware.enableRedistributableFirmware = true; + hardware.cpu.intel.updateMicrocode = true; + hardware.cpu.amd.updateMicrocode = true; + }; +} \ No newline at end of file diff --git a/modules/computer/hardware/default.nix b/modules/computer/hardware/default.nix new file mode 100644 index 0000000..0d931d1 --- /dev/null +++ b/modules/computer/hardware/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports=[ + ./boot.nix + ./cpu.nix + ./disks.nix + ./nvidia.nix + ]; +} \ No newline at end of file diff --git a/modules/computer/hardware/disks.nix b/modules/computer/hardware/disks.nix new file mode 100644 index 0000000..6e1acee --- /dev/null +++ b/modules/computer/hardware/disks.nix @@ -0,0 +1,328 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus.hardware; + + mkDataDisk = idx: device: { + name = "content-${toString idx}"; + value = { + type = "disk"; + device = device; + content = { + type = "gpt"; + partitions = { + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-content-${toString idx}"; + initrdUnlock = false; + settings = { + keyFile = "/etc/secrets/disks/content-${toString idx}"; + allowDiscards = true; + crypttabExtraOpts = [ "nofail" ]; + }; + content = { + type = "filesystem"; + format = cfg.dataDisksFilesystem; + mountpoint = "/mnt/content-${toString idx}"; + mountOptions = [ "nofail" "noauto" ]; + }; + }; + }; + }; + }; + }; + }; + + mkParityDisk = idx: device: { + name = "parity-${toString idx}"; + value = { + type = "disk"; + device = device; + content = { + type = "gpt"; + partitions = { + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-parity-${toString idx}"; + initrdUnlock = false; + settings = { + keyFile = "/etc/secrets/disks/parity-${toString idx}"; + allowDiscards = true; + crypttabExtraOpts = [ "nofail" ]; + }; + content = { + type = "filesystem"; + format = cfg.parityDisksFilesystem; + mountpoint = "/mnt/parity-${toString idx}"; + mountOptions = [ "nofail" "noauto" ]; + }; + }; + }; + }; + }; + }; + }; + + isMirror = length cfg.bootDisksList > 1; + + bootDisksConfig = if isMirror then { + mdadm = { + boot = { + type = "mdadm"; + level = 1; + metadata = "1.2"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + }; + disk = listToAttrs (imap1 (i: device: { + name = "boot-${toString i}"; + value = { + type = "disk"; + device = device; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "1G"; + type = "EF00"; + content = { + type = "mdraid"; + name = "boot"; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-boot-${toString i}"; + settings = { + allowDiscards = true; + keyFile = "/etc/secrets/disks/boot-${toString i}"; + }; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + }) cfg.bootDisksList); + } else { + disk = { + "boot-1" = { + type = "disk"; + device = head cfg.bootDisksList; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "1G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted-boot-1"; + settings = { + keyFile = "/etc/secrets/disks/boot-1"; + allowDiscards = true; + }; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + }; + }; + + lvmConfig = { + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + swap = { + size = cfg.swapSize; + content = { + type = "swap"; + }; + } // optionalAttrs isMirror { lvm_type = "mirror"; }; + snapraid = { + size = "1G"; + content = { + type = "filesystem"; + format = "btrfs"; + mountpoint = "/mnt/content-0"; + }; + } // optionalAttrs isMirror { lvm_type = "mirror"; }; + root = { + size = "100%"; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; + subvolumes = { + "/rootfs" = { + mountpoint = "/"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "/home" = { + mountpoint = "/home"; + mountOptions = [ "compress=zstd" ]; + }; + "/nix" = { + mountpoint = "/nix"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + }; + }; + } // optionalAttrs isMirror { lvm_type = "mirror"; }; + }; + }; + }; + }; + +in +{ + options.numbus.hardware = { + dataDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; + description = "List by-id path of devices for data disks"; + }; + parityDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; + description = "List of by-id path of devices for parity disks"; + }; + bootDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/nvme_SAMSUNG_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ]; + description = "List of by-id path of devices for boot disks"; + }; + spindownDisksList = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; + description = "List of by-id path of devices to spindown when inactive to save power (HDD only)"; + }; + swapSize = mkOption { + type = types.str; + default = "16G"; + example = "16G"; + description = "Size of the swap partition"; + }; + dataDisksFilesystem = mkOption { + type = types.enum [ "xfs" "ext4" "btrfs" ]; + default = "xfs"; + example = "xfs"; + description = "Filesystem for data disks. Available filesystem options : xfs, ext4, btrfs"; + }; + parityDisksFilesystem = mkOption { + type = types.enum [ "xfs" "ext4" "btrfs" ]; + default = "xfs"; + example = "xfs"; + description = "Filesystem for parity disks. Available filesystem options : xfs, ext4, btrfs"; + }; + }; + + config = mkIf (cfg.bootDisksList != []) { + disko.devices = mkMerge [ + bootDisksConfig + lvmConfig + { + disk = listToAttrs (imap1 mkDataDisk cfg.dataDisksList); + } + { + disk = listToAttrs (imap1 mkParityDisk cfg.parityDisksList); + } + ]; + + services.snapraid = { + enable = true; + contentFiles = [ "/mnt/content-0/snapraid.content" ] ++ + (map (i: "/mnt/content-${toString i}/snapraid.content") (range 1 (length cfg.dataDisksList))); + parityFiles = map (i: "/mnt/parity-${toString i}/snapraid.parity") (range 1 (length cfg.parityDisksList)); + dataDisks = listToAttrs (imap1 (i: _: nameValuePair "d${toString i}" "/mnt/content-${toString i}") cfg.dataDisksList); + }; + + fileSystems."/mnt/data" = { + device = "/mnt/content-*"; + fsType = "fuse.mergerfs"; + options = [ + "category.create=ff" + "cache.files=partial" + "dropcacheonclose=true" + "defaults" + "noauto" + "nofail" + "allow_other" + "moveonenospc=1" + "minfreespace=50G" + "func.getattr=newest" + "fsname=mergerfs_data" + "x-mount.mkdir" + "x-systemd.automount" + ]; + }; + + systemd.services.mount-disks = { + description = "Mount data and parity disks"; + before = [ "mnt-data.mount" ]; + requiredBy = [ "mnt-data.mount" ]; + path = [ pkgs.cryptsetup pkgs.util-linux ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = let + mountDataDisk = i: '' + if [ ! -e /dev/mapper/crypted-content-${toString i} ]; then + cryptsetup luksOpen --key-file /etc/secrets/disks/content-${toString i} /dev/disk/by-partlabel/disk-content-${toString i}-luks crypted-content-${toString i} + fi + mkdir -p /mnt/content-${toString i} + if ! mountpoint -q /mnt/content-${toString i}; then + mount -t ${cfg.dataDisksFilesystem} /dev/mapper/crypted-content-${toString i} /mnt/content-${toString i} + fi + ''; + mountParityDisk = i: '' + if [ ! -e /dev/mapper/crypted-parity-${toString i} ]; then + cryptsetup luksOpen --key-file /etc/secrets/disks/parity-${toString i} /dev/disk/by-partlabel/disk-parity-${toString i}-luks crypted-parity-${toString i} + fi + mkdir -p /mnt/parity-${toString i} + if ! mountpoint -q /mnt/parity-${toString i}; then + mount -t ${cfg.parityDisksFilesystem} /dev/mapper/crypted-parity-${toString i} /mnt/parity-${toString i} + fi + ''; + in '' + ${concatMapStrings mountDataDisk (range 1 (length cfg.dataDisksList))} + ${concatMapStrings mountParityDisk (range 1 (length cfg.parityDisksList))} + ''; + }; + }; +} \ No newline at end of file diff --git a/modules/computer/hardware/nvidia.nix b/modules/computer/hardware/nvidia.nix new file mode 100644 index 0000000..648c770 --- /dev/null +++ b/modules/computer/hardware/nvidia.nix @@ -0,0 +1,56 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.numbus-computer.hardware; +in + +{ + options.numbus-computer.hardware = { + nvidia = { + enable = mkEnableOption "Wether to install the NVIDIA driver. Required for better performance with NVIDIA graphics cards." + } + } + + config = mkMerge [ + ({ + # Enable OpenGL + hardware.graphics = { + enable = true; + }; + }) + + ( mkIf (cfg.nvidia.enable == true) { + # Load nvidia driver for Xorg and Wayland + services.xserver.videoDrivers = [ "nvidia" ]; + + hardware.nvidia = { + # Modesetting is required. + modesetting.enable = true; + # Nvidia power management. Experimental, and can cause sleep/suspend to fail. + # Enable this if you have graphical corruption issues or application crashes after waking + # up from sleep. This fixes it by saving the entire VRAM memory to /tmp/ instead + # of just the bare essentials. + powerManagement.enable = false; + + # Fine-grained power management. Turns off GPU when not in use. + # Experimental and only works on modern Nvidia GPUs (Turing or newer). + powerManagement.finegrained = false; + + # Use the NVidia open source kernel module (not to be confused with the + # independent third-party "nouveau" open source driver). + # Support is limited to the Turing and later architectures. Full list of + # supported GPUs is at: + # https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus + # Only available from driver 515.43.04+ + open = false; + + # Enable the Nvidia settings menu, + # accessible via `nvidia-settings`. + nvidiaSettings = true; + + # Optionally, you may need to select the appropriate driver version for your specific GPU. + package = config.boot.kernelPackages.nvidiaPackages.stable; + }; + }) + ]; +} \ No newline at end of file diff --git a/modules/computer/misc/default.nix b/modules/computer/misc/default.nix new file mode 100644 index 0000000..c40367e --- /dev/null +++ b/modules/computer/misc/default.nix @@ -0,0 +1,11 @@ +{ ... }: + +{ + imports=[ + ./audio.nix + ./internationalization.nix + ./networking.nix + ./printer.nix + ./users.nix + ]; +} \ No newline at end of file diff --git a/modules/computer/misc/networking.nix b/modules/computer/misc/networking.nix new file mode 100644 index 0000000..accc6c1 --- /dev/null +++ b/modules/computer/misc/networking.nix @@ -0,0 +1,12 @@ +{ config, ... }: + +{ + # Enable networking + networking.networkmanager.enable = true; + networking.hostName = "numbus-computer"; + + # Open ports in the firewall. + networking.firewall.allowedTCPPorts = [ ]; + networking.firewall.allowedUDPPorts = [ ]; + networking.firewall.enable = true; +} \ No newline at end of file diff --git a/modules/computer/packages/default.nix b/modules/computer/packages/default.nix new file mode 100644 index 0000000..c8c54c8 --- /dev/null +++ b/modules/computer/packages/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports=[ + ./desktop-environment.nix + ./flatpaks.nix + ./terminal.nix + ./updates.nix + ]; +} \ No newline at end of file diff --git a/modules/computer/packages/desktop-environment.nix b/modules/computer/packages/desktop-environment.nix new file mode 100644 index 0000000..94f7b18 --- /dev/null +++ b/modules/computer/packages/desktop-environment.nix @@ -0,0 +1,75 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.numbus-computer.packages.desktop; +in + +{ + options.numbus-computer.packages.desktop = { + gnome = { + enable = mkEnableOption "Wether to enable the GNOME desktop environment."; + + extensions = mkOption { + type = types.listOf types.str ; + default = []; + example = [ "dash-to-dock" "caffeine" "clipboard-history" "appindicator-support" ]; + description = "Extensions to add to the Gnome desktop environment to improve your experience."; + }; + }; + kde_plasma = { + enable = mkEnableOption "Wether to enable the KDE Plasma desktop environment."; + }; + hyprland = { + enable = mkEnableOption "Wether to enable the hyprland desktop environment."; + }; + }; + + config = mkMerge [ + # GNOME + (mkIf (cfg.gnome.enable == true) { + services.xserver.enable = false; + services.displayManager.gdm.enable = true; + services.desktopManager.gnome.enable = true; + services.gnome.core-apps.enable = false; + services.gnome.core-developer-tools.enable = false; + services.gnome.games.enable = false; + environment.gnome.excludePackages = with pkgs; [ gnome-tour gnome-user-docs ]; + }) + + # GNOME extensions + (mkIf (cfg.gnome.enable == true && cfg.desktop.gnome.extensions != [ ]) { + environment.systemPackages = map (ext: pkgs.gnomeExtensions.${ext}) cfg.desktop.gnome.extensions; + }) + + # KDE Plasma + (mkIf (cfg.kde_plasma.enable == true) { + services = { + desktopManager.plasma6.enable = true; + displayManager.sddm.enable = true; + displayManager.sddm.wayland.enable = true; + }; + environment.systemPackages = with pkgs; [ + kdePackages.discover + kdePackages.kcalc + kdePackages.kcharselect + kdePackages.kclock + kdePackages.kcolorchooser + kdePackages.kolourpaint + kdePackages.sddm-kcm + kdiff3 + wayland-utils + wl-clipboard + ]; + }) + + # Hyprland + (mkIf (cfg.hyprland.enable == true) { + programs.hyprland.enable = true; + environment.systemPackages = [ + pkgs.kitty + ]; + }) + ]; +} \ No newline at end of file diff --git a/modules/server/default.nix b/modules/server/default.nix new file mode 100644 index 0000000..5564710 --- /dev/null +++ b/modules/server/default.nix @@ -0,0 +1,15 @@ +{ ... }: + +{ + imports = [ + ./dns-services/default.nix + ./hardware/default.nix + ./mail/default.nix + ./misc/default.nix + ./networking/default.nix + ./packages/default.nix + ./system-services/default.nix + ./web-applications/default.nix + ./global.nix + ]; +} \ No newline at end of file diff --git a/modules/server/hardware/boot.nix b/modules/server/hardware/boot.nix new file mode 100644 index 0000000..89c82ad --- /dev/null +++ b/modules/server/hardware/boot.nix @@ -0,0 +1,11 @@ +{ config, ... }: + +{ + boot.initrd.systemd.enable = true; + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + boot.kernel.sysctl = { + "vm.overcommit_memory" = 1; + }; +} \ No newline at end of file diff --git a/modules/server/hardware/default.nix b/modules/server/hardware/default.nix new file mode 100644 index 0000000..f1a7eb8 --- /dev/null +++ b/modules/server/hardware/default.nix @@ -0,0 +1,12 @@ +{ ... }: + +{ + imports = [ + # Tested + ./boot.nix + ./cpu.nix + # To test + ./pcie-coral.nix + ./disks/default.nix + ]; +} \ No newline at end of file diff --git a/modules/server/hardware/pcie-coral.nix b/modules/server/hardware/pcie-coral.nix new file mode 100644 index 0000000..db7709b --- /dev/null +++ b/modules/server/hardware/pcie-coral.nix @@ -0,0 +1,111 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.numbus-server.hardware.pcie-coral; + + gasket-driver = { stdenv, lib, fetchFromGitHub, kernel }: stdenv.mkDerivation rec { + pname = "gasket"; + version = "1.0-18"; + + src = fetchFromGitHub { + owner = "google"; + repo = "gasket-driver"; + rev = "97aeba584efd18983850c36dcf7384b0185284b3"; + sha256 = "pJwrrI7jVKFts4+bl2xmPIAD01VKFta2SRuElerQnTo="; + }; + + makeFlags = [ + "-C" + "${kernel.dev}/lib/modules/${kernel.modDirVersion}/build" + "M=$(PWD)" + ]; + buildFlags = [ "modules" ]; + + installFlags = [ "INSTALL_MOD_PATH=${placeholder "out"}" ]; + installTargets = [ "modules_install" ]; + + sourceRoot = "source/src"; + hardeningDisable = [ "pic" "format" ]; + nativeBuildInputs = kernel.moduleBuildDependencies; + + meta = with lib; { + description = "The Coral Gasket Driver allows usage of the Coral EdgeTPU on Linux systems."; + homepage = "https://github.com/google/gasket-driver"; + license = licenses.gpl2; + maintainers = [ maintainers.kylehendricks ]; + platforms = platforms.linux; + }; + }; + + libedgetpu-pkg = { stdenv, lib, fetchFromGitHub, libusb1, abseil-cpp, flatbuffers, xxd }: + let + flatbuffers_1_12 = flatbuffers.overrideAttrs (oldAttrs: rec { + version = "1.12.0"; + NIX_CFLAGS_COMPILE = "-Wno-error=class-memaccess -Wno-error=maybe-uninitialized"; + cmakeFlags = (oldAttrs.cmakeFlags or []) ++ ["-DFLATBUFFERS_BUILD_SHAREDLIB=ON"]; + NIX_CXXSTDLIB_COMPILE = "-std=c++17"; + configureFlags = (oldAttrs.configureFlags or []) ++ ["--enable-shared"]; + src = fetchFromGitHub { + owner = "google"; + repo = "flatbuffers"; + rev = "v${version}"; + sha256 = "sha256-L1B5Y/c897Jg9fGwT2J3+vaXsZ+lfXnskp8Gto1p/Tg="; + }; + }); + + in stdenv.mkDerivation rec { + pname = "libedgetpu"; + version = "grouper"; + + src = fetchFromGitHub { + owner = "google-coral"; + repo = pname; + rev = "release-${version}"; + sha256 = "sha256-73hwItimf88Iqnb40lk4ul/PzmCNIfdt6Afi+xjNiBE="; + }; + + makeFlags = ["-f" "makefile_build/Makefile" "libedgetpu" ]; + + buildInputs = [ + libusb1 + abseil-cpp + flatbuffers_1_12 + ]; + + nativeBuildInputs = [ + xxd + ]; + + NIX_CXXSTDLIB_COMPILE = "-std=c++17"; + + TFROOT = "${fetchFromGitHub { + owner = "tensorflow"; + repo = "tensorflow"; + rev = "v2.7.4"; + sha256 = "sha256-liDbUAdaVllB0b74aBeqNxkYNu/zPy7k3CevzRF5dk0="; + }}"; + + enableParallelBuilding = false; + + installPhase = '' + mkdir -p $out/lib + cp out/direct/k8/libedgetpu.so.1.0 $out/lib + ln -s $out/lib/libedgetpu.so.1.0 $out/lib/libedgetpu.so.1 + mkdir -p $out/lib/udev/rules.d + cp debian/edgetpu-accelerator.rules $out/lib/udev/rules.d/99-edgetpu-accelerator.rules + ''; + }; + + gasket = config.boot.kernelPackages.callPackage gasket-driver {}; + libedgetpu = pkgs.callPackage libedgetpu-pkg {}; +in + +{ + options.numbus-server.hardware.pcie-coral = lib.mkEnableOption "PCIe Coral TPU support"; + + config = lib.mkIf cfg.enable { + services.udev.packages = [ libedgetpu ]; + users.groups.plugdev = {}; + boot.extraModulePackages = [ gasket ]; + }; +} \ No newline at end of file diff --git a/modules/server/mail/clamav.nix b/modules/server/mail/clamav.nix new file mode 100644 index 0000000..7713d89 --- /dev/null +++ b/modules/server/mail/clamav.nix @@ -0,0 +1,89 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus-server.services.clamav; + clamav_notifier = pkgs.writeScript "clamav-notify.sh" '' + #!${pkgs.bash}/bin/bash + + # Check if triggered by Real-time event (file exists) + if [ -f /var/lib/clamav/virus_event.env ]; then + source /var/lib/clamav/virus_event.env + rm /var/lib/clamav/virus_event.env + fi + + ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}" + USER_EMAIL="${config.numbus-server.mail.userAddress}" + OWNER_NAME="${config.numbus-server.owner}" + + if [ -n "$CLAM_VIRUSEVENT_VIRUSNAME" ]; then + # --- Real-time / VirusEvent Mode --- + SUBJECT="Numbus Server Alert: Virus Detected (Real-time)" + + # Retrieve logs from clamav-daemon + LOGS=$(journalctl -u clamav-daemon.service -n 50 --no-pager | grep "FOUND") + + TECH_BODY=" + ClamAV Real-time Alert: + Server owner: $OWNER_NAME + + Virus detected: $CLAM_VIRUSEVENT_VIRUSNAME + File: $CLAM_VIRUSEVENT_FILENAME + + Logs: + $LOGS + + Action taken: Access blocked (OnAccessPrevention). + Please investigate manually. + " + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + L'antivirus de votre serveur a détecté et bloqué une menace en temps réel. + Fichier : $CLAM_VIRUSEVENT_FILENAME + + Votre administrateur a été notifié. + " + else + # --- Scheduled Scan Summary Mode --- + SUBJECT="Numbus Server Alert: Virus Detected during Scheduled Scan" + + # Retrieve logs (clamdscan prints FOUND when a virus is detected) + LOGS=$(journalctl -u clamav-periodic-scan.service -n 100 --no-pager | grep "FOUND") + + TECH_BODY=" + ClamAV Scan Alert: + Server owner: $OWNER_NAME + + Viruses detected: + $LOGS + + Action taken: Detection only. + Please investigate manually. + " + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + L'antivirus de votre serveur a détecté une menace potentielle lors de l'analyse périodique. + Votre administrateur a été notifié avec les détails techniques. + Nous vous conseillons d'être prudent avec vos fichiers récents. + " + fi + + printf "Subject: [ADMIN] %s\n\n%s" "$SUBJECT" "$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" + printf "Subject: [Alerte] Menace détectée sur votre serveur Numbus\n\n%s\n\nMerci de votre confiance,\nL'équipe de support,\nNumbus-Server." "$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" + ''; +in + +{ + config = mkIf cfg.enable { + systemd.services.clamav-virus-notify = { + description = "Email notification for ClamAV virus detection"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${clamav_notifier}"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/server/mail/default.nix b/modules/server/mail/default.nix new file mode 100644 index 0000000..b5f75e8 --- /dev/null +++ b/modules/server/mail/default.nix @@ -0,0 +1,12 @@ +{ ... }: + +{ + imports = [ + # To test + ./clamav.nix + ./smart.nix + ./systemd.nix + ./smtp.nix + ./disk-space.nix + ]; +} \ No newline at end of file diff --git a/modules/server/mail/systemd.nix b/modules/server/mail/systemd.nix new file mode 100644 index 0000000..763672e --- /dev/null +++ b/modules/server/mail/systemd.nix @@ -0,0 +1,55 @@ +{ config, pkgs, ... }: + +let + systemd_notifier = pkgs.writeScript "systemd-email-notify.sh" '' + #!${pkgs.bash}/bin/bash + + # The failing service name is passed as the first argument + UNIT=$1 + + # 1. Send Technical Email to Admin + ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}" + SUBJECT="Numbus Server Alert: Service $UNIT Failed" + + # Retrieve recent logs for context + LOGS=$(journalctl -u "$UNIT" -n 20 --no-pager) + + TECH_BODY=" + Systemd Service Failure Alert: + Server owner: ${config.numbus-server.owner} + Service: $UNIT + + Recent Logs: + $LOGS + " + printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" + + # 2. Send Friendly Email to Owner + USER_EMAIL="${config.numbus-server.mail.userAddress}" + OWNER_NAME="${config.numbus-server.owner}" + + FRIENDLY_BODY="Cher/Chère $OWNER_NAME, + + Votre serveur a détecté une défaillance du service $UNIT. + Le système a tenté de gérer l'erreur, mais une intervention peut être nécessaire. + + Votre administrateur a été notifié de cet incident avec les détails techniques nécessaires. + Il interviendra si une action manuelle est requise. + + Merci de votre confiance, + L'équipe de support, + Numbus-Server." + + printf "Subject: [Alerte] Erreur sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" + ''; +in +{ + systemd.services."service-failure-notify@" = { + description = "Email notification for failed service %i"; + onFailure = [ ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${systemd_notifier} %i"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/networking/default.nix b/modules/server/networking/default.nix new file mode 100644 index 0000000..fe4a2ce --- /dev/null +++ b/modules/server/networking/default.nix @@ -0,0 +1,9 @@ +{ ... }: + +{ + imports = [ + # To test + ./firewall.nix + ./networking.nix + ]; +} \ No newline at end of file diff --git a/modules/server/networking/firewall.nix b/modules/server/networking/firewall.nix new file mode 100644 index 0000000..e28edef --- /dev/null +++ b/modules/server/networking/firewall.nix @@ -0,0 +1,13 @@ +{ config, pkgs, lib, ... }: + +{ + config = { + networking.nftables.enable = true; + networking.firewall = { + enable = true; + allowPing = true; + allowedTCPPorts = [ 53 80 443 ]; + allowedUDPPorts = [ 53 443 ]; + }; + }; +} \ No newline at end of file diff --git a/modules/server/networking/networking.nix b/modules/server/networking/networking.nix new file mode 100644 index 0000000..f5350fb --- /dev/null +++ b/modules/server/networking/networking.nix @@ -0,0 +1,60 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.numbus-server.networking; +in + +{ + options.numbus-server.networking = { + ipAddress = mkOption { + description = "The IP address that this server will use"; + type = types.str; + example = "192.168.1.100"; + }; + interface = mkOption { + description = "The interface that this server will use to connect to the network"; + type = types.str; + example = "enp1s0"; + }; + routerIpAddress = mkOption { + description = "The IP address of the router of your network"; + type = types.str; + example = "192.168.1.1"; + }; + networkSubnet = mkOption { + description = "The subnet of your network"; + type = types.str; + default = ""; + example = "192.168.1.0/24"; + }; + dnsServers = mkOption { + description = "The list of DNS servers that this server will use"; + type = types.listOf types.str; + default = [ "${cfg.ipAddress}" "9.9.9.9" ]; + example = [ "${cfg.ipAddress}" "9.9.9.9" ]; + }; + }; + + config = { + networking.hostName = "numbus-server"; + networking.networkmanager.enable = false; + + # Allow rootless containers to bind to port 53 and up + boot.kernel.sysctl."net.ipv4.ip_unprivileged_port_start" = 53; + + networking.bridges.br0.interfaces = [ "${cfg.interface}" ]; + networking.interfaces."${cfg.interface}".useDHCP = false; + networking.interfaces.br0.useDHCP = false; + networking.nameservers = cfg.dnsServers; + networking.interfaces.br0.ipv4.addresses = [{ + address = "${cfg.ipAddress}"; + prefixLength = 24; + }]; + networking.defaultGateway = { + address = "${cfg.routerIpAddress}"; + interface = "br0"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/packages/default.nix b/modules/server/packages/default.nix new file mode 100644 index 0000000..175367d --- /dev/null +++ b/modules/server/packages/default.nix @@ -0,0 +1,11 @@ +{ ... }: + +{ + imports = [ + # To test + ./packages.nix + ./podman.nix + ./ssh.nix + ./terminal.nix + ]; +} \ No newline at end of file diff --git a/modules/server/packages/packages.nix b/modules/server/packages/packages.nix new file mode 100644 index 0000000..13dc56e --- /dev/null +++ b/modules/server/packages/packages.nix @@ -0,0 +1,25 @@ +{ config, pkgs, ... }: + +{ + nixpkgs.config.allowUnfree = true; + + environment.systemPackages = with pkgs; [ + git + ncdu + fastfetch + tpm2-tss + sops + age + powertop + pciutils + hdparm + hd-idle + hddtemp + smartmontools + cpufrequtils + intel-gpu-tools + snapraid + mergerfs + mergerfs-tools + ]; +} \ No newline at end of file diff --git a/modules/server/packages/podman.nix b/modules/server/packages/podman.nix new file mode 100644 index 0000000..ddf80e1 --- /dev/null +++ b/modules/server/packages/podman.nix @@ -0,0 +1,16 @@ +{ pkgs, ... }: + +{ + virtualisation.podman.enable = true; + virtualisation.podman.defaultNetwork.settings.dns_enabled = true; + + virtualisation.containers.containersConf.settings = { + network.default_rootless_network_cmd = "slirp4netns"; + }; + + environment.systemPackages = with pkgs; [ + podman-compose + podman-tui + slirp4netns + ]; +} \ No newline at end of file diff --git a/modules/server/service-helper.nix b/modules/server/service-helper.nix new file mode 100644 index 0000000..2fbb8e4 --- /dev/null +++ b/modules/server/service-helper.nix @@ -0,0 +1,181 @@ +{ lib, config, pkgs }: + +with lib; + +{ + mkPodmanService = { + name, + description, + defaultPort ? "0", + defaultSubdomain ? name, + pod ? "false", + reverseProxied ? true, + composeText, + scheme ? "http", + middlewares ? null, + dependencies ? [ "sops-install-secrets.service" "traefik.service" "authelia.service" "${config.numbus-server.services.dns}.service" ], + extraOptions ? {}, + extraConfig ? {}, + configDirEnabled ? true, + dataDirEnabled ? true, + startDelay ? 180, + dirPermissions ? [], + secrets ? [], + envFile ? null, + ... + }: + + let + cfg = config.numbus-server.services.${name}; + in + + { + options.numbus-server.services.${name} = recursiveUpdate ({ + enable = mkEnableOption description; + + subdomain = mkOption { + type = types.str; + default = defaultSubdomain; + example = defaultSubdomain; + description = "The subdomain that ${name} will use"; + }; + + port = mkOption { + type = types.str; + default = defaultPort; + example = defaultPort; + description = "The port that ${name} will use."; + }; + + reverseProxied = mkOption { + type = types.bool; + default = reverseProxied; + example = reverseProxied; + description = "Whether to create a basic Traefik reverse proxy configuration for this service. You might need to set it to false for custom configurations or services that don't need to be reverse proxied."; + }; + } // (optionalAttrs configDirEnabled { + configDir = mkOption { + type = types.str; + default = "/mnt/config/${name}"; + example = "/mnt/config/${name}"; + description = "The directory where ${name}'s configuration files will be stored"; + }; + }) // (optionalAttrs dataDirEnabled { + dataDir = mkOption { + type = types.str; + default = "/mnt/data/${name}"; + example = "/mnt/data/${name}"; + description = "The directory where ${name}'s data will be stored"; + }; + })) extraOptions; + + config = mkIf cfg.enable (mkMerge [ + { + # Compose file + sops.templates."podman/${name}" = { + gid = "100"; + uid = "1000"; + mode = "0400"; + content = composeText; + path = "/etc/podman/${name}/compose.yaml"; + }; + + # Traefik config + sops.templates."traefik/rules/${name}" = mkIf cfg.reverseProxied { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + routers: + ${name}: + rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`)" + entrypoints: + - "websecure" + service: ${name} + middlewares: + ${concatStringsSep "\n" (map (m: " - ${m}") middlewares)} + tls: + certresolver: "cloudflare" + options: "secureTLS" + services: + ${name}: + loadBalancer: + servers: + - url: "${scheme}://host.containers.internal:${cfg.port}" + ''; + path = "/etc/traefik/rules/${name}.yaml"; + }; + + # Secrets config + sops.secrets = genAttrs secrets (secretPath: { + sopsFile = "/etc/nixos/secrets/podman/${name}.yaml"; + gid = "100"; + uid = "1000"; + mode = "0400"; + }); + + # SystemD config + systemd.services."${name}" = { + description = "Podman container : ${name}"; + after = dependencies; + wantedBy = [ "multi-user.target" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.podman pkgs.podman-compose pkgs.slirp4netns pkgs.su pkgs.sudo pkgs.coreutils ]; + serviceConfig = { + Type = "exec"; + TimeoutStartSec = "1000"; + ExecStartPre = [ + "${pkgs.bash}/bin/bash -c 'sleep $((RANDOM % ${toString startDelay}))'" + "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose -f /etc/podman/${name}/compose.yaml pull'" + ]; + ExecStart = "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml up --remove-orphans'"; + ExecStop = "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml down'"; + Restart = "on-failure"; + RestartSec = "3m"; + }; + }; + + # Permissions config + systemd.services."${name}-permissions" = mkIf (dirPermissions != []) { + description = "Podman container : ${name} : check and fix permissions"; + before = [ "${name}.service" ]; + wantedBy = [ "multi-user.target" "${name}.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.coreutils ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + Restart = "on-failure"; + RestartSec = "5m"; + }; + script = '' + ${concatStringsSep "\n" (map (perm: '' + set -- ${perm} + WANTED_PERMISSIONS=$1 + FOLDER_PATH=$2 + + if [[ ! -e "$FOLDER_PATH" ]]; then + mkdir -p "$FOLDER_PATH" + elif [[ ! -d "$FOLDER_PATH" ]]; then + rm "$FOLDER_PATH" + mkdir -p "$FOLDER_PATH" + fi + + ACTUAL_PERMISSIONS=$(stat -c '%u:%g' "$FOLDER_PATH") + if [[ "$ACTUAL_PERMISSIONS" != "$WANTED_PERMISSIONS" ]]; then + chown -R "$WANTED_PERMISSIONS" "$FOLDER_PATH" + fi + '') dirPermissions)} + exit 0 + ''; + }; + } + extraConfig + ]); + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/authelia.nix b/modules/server/services/applications/authelia.nix new file mode 100644 index 0000000..cf0fa96 --- /dev/null +++ b/modules/server/services/applications/authelia.nix @@ -0,0 +1,171 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "authelia"; + # Version tagging + autheliaVersion = "v4.39.16"; + databaseVersion = "18.3"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.authelia; + # Derive Base DN from domain (e.g., example.com -> dc=example,dc=com) + domainParts = splitString "." config.numbus-server.services.domain; + baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts); + # Generate dynamic access control rules based on groups and allowedApps + mkGroupRule = groupName: appName: + let + app = config.numbus-server.service.${appName} or {}; + in + if app ? subdomain && app ? domain then '' + - domain: "${app.subdomain}.${app.domain}" + policy: two_factor + subject: "group:${groupName}"'' + else ""; + allGroupRules = concatStringsSep "\n" (filter (s: s != "") (flatten (mapAttrsToList (groupName: groupCfg: + map (appName: mkGroupRule groupName appName) (groupCfg.allowedApps or []) + ) (config.numbus-server.groups or {})))); + + defaultRedirectionUrl = + if config.numbus-server.services.homepage.enable then + "https://${config.numbus-server.services.homepage.subdomain}.${config.numbus-server.services.domain}" + else if config.numbus-server.services.dashy.enable then + "https://${config.numbus-server.services.dashy.subdomain}.${config.numbus-server.services.domain}" + else null; +in + +helper.mkPodmanService { + inherit name; + pod = name; + description = "Authelia, your own unified login provider"; + defaultPort = "9091"; + dependencies = [ + "sops-install-secrets.service" + "traefik.service" + "${config.numbus-server.services.dns}.service" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "authelia/db_name" + "authelia/db_username" + "authelia/db_password" + "authelia/jwt_secret" + "authelia/session_secret" + "authelia/storage_secret" + ]; + + composeText = '' + services: + authelia-server: + image: ghcr.io/authelia/authelia:${autheliaVersion} + container_name: authelia-server + hostname: authelia-server + user: '1000:1000' + networks: + authelia: + ipv4_address: 10.89.251.253 + ports: + - "${cfg.port}:9091/tcp" + volumes: + - ${cfg.configDir}/server:/config + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + authelia-database: + container_name: authelia-database + hostname: authelia-database + image: docker.io/library/postgres:${databaseVersion} + user: '1000:1000' + networks: + authelia: + ipv4_address: 10.89.251.252 + environment: + POSTGRES_DB: ${config.sops.placeholder."authelia/db_name"} + POSTGRES_USER: ${config.sops.placeholder."authelia/db_username"} + POSTGRES_PASSWORD: ${config.sops.placeholder."authelia/db_password"} + POSTGRES_INITDB_ARGS: '--data-checksums' + volumes: + - ${cfg.configDir}/database:/var/lib/postgresql/data + shm_size: 128mb + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + authelia: + driver: bridge + name: authelia + ipam: + config: + - subnet: "10.89.251.0/24" + gateway: "10.89.251.254" + ''; + + extraConfig = { + sops.templates."authelia-config" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + authelia: + identity_validation: + reset_password: + jwt_secret: "${config.sops.placeholder."authelia/jwt_secret"}" + jwt_lifespan: "5 minutes" + jwt_algorithm: "HS256" + storage: + encryption_key: "${config.sops.placeholder."authelia/storage_secret"}" + postgres: + address: "tcp://authelia-database:5432" + database: "${config.sops.placeholder."authelia/db_name"}" + username: "${config.sops.placeholder."authelia/db_username"}" + password: "${config.sops.placeholder."authelia/db_password"}" + session: + secret: "${config.sops.placeholder."authelia/session_secret"}" + cookies: + - domain: "${config.numbus-server.services.domain}" + authelia_url: "https://${cfg.subdomain}.${config.numbus-server.services.domain}" + ${optionalString (defaultRedirectionUrl != null) "default_redirection_url: \"${defaultRedirectionUrl}\""} + authentication_backend: + ldap: + implementation: "lldap" + address: "ldap://host.containers.internal:3890" + base_dn: "${baseDN}" + user: "UID=authelia,OU=people,${baseDN}" + password: "${config.sops.placeholder."lldap/"}" + notifier: + smtp: + address: submission://${config.numbus-server.mail.smtpHost}:${config.numbus-server.mail.smtpPort} + username: ${config.numbus-server.mail.smtpUsername} + password: ${config.sops.placeholder.smtpPassword} + sender: ${config.numbus-server.mail.fromAddress} + tls: + server_name: ${config.numbus-server.mail.smtpHost} + minimum_version: TLS1.2 + skip_verify: false + access_control: + default_policy: 'deny' + rules: + - domain: "*.${config.numbus-server.service.domain}" + policy: two_factor + subject: "group:admin" + ${allGroupRules} + ''; + path = "/etc/authelia/authelia.yaml"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/crafty.nix b/modules/server/services/applications/crafty.nix new file mode 100644 index 0000000..9dc53d1 --- /dev/null +++ b/modules/server/services/applications/crafty.nix @@ -0,0 +1,74 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "crafty"; + # Version tagging + craftyVersion = "v4.10.1"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.crafty; +in + +helper.mkPodmanService { + inherit name; + description = "Crafty controller, one place to manage your minecraft servers"; + defaultPort = "8443"; + scheme = "https"; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/log" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${optimizedDir}/import" + "100999:100 ${optimizedDir}/backups" + "100999:100 ${optimizedDir}/servers" + ]; + + composeText = '' + services: + crafty: + image: registry.gitlab.com/crafty-controller/crafty-4:${craftyVersion} + container_name: crafty + user: '1000:1000' + networks: + crafty: + ipv4_address: 10.89.250.253 + ports: + - "${cfg.port}:8443/tcp" + - "19132:19132/udp" + - "25500-25600:25500-25600" + volumes: + - ${optimizedDir}/backups:/crafty/backups + - ${optimizedDir}/servers:/crafty/servers + - ${optimizedDir}/import:/crafty/import + - ${cfg.configDir}/logs:/crafty/logs + - ${cfg.configDir}/config:/crafty/app/config + environment: + - TZ=${time.timeZone} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + crafty: + driver: bridge + name: crafty + ipam: + config: + - subnet: "10.89.250.0/24" + gateway: "10.89.250.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/dashy.nix b/modules/server/services/applications/dashy.nix new file mode 100644 index 0000000..4fd4491 --- /dev/null +++ b/modules/server/services/applications/dashy.nix @@ -0,0 +1,97 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "dashy"; + # Version tagging + dashyVersion = "v3.2.3"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.dashy; +in + +helper.mkPodmanService { + inherit name; + description = "Dashy, the ultimate dashboard for your homelab"; + defaultPort = "8999"; + configDirEnabled = false; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + + composeText = '' + services: + dashy: + image: lissy93/dashy:${dashyVersion} + container_name: dashy + hostname: dashy + user: '1000:1000' + networks: + dashy: + ipv4_address: 10.89.235.253 + ports: + - ${cfg.port}:8080 + volumes: + - ${config.sops."dashy/config".path}:/app/user-data/conf.yml + environment: + - UID=1000 + - GID=1000 + - NODE_ENV=production + healthcheck: + test: ['CMD', 'node', '/app/services/healthcheck'] + interval: 1m30s + timeout: 10s + retries: 3 + start_period: 40s + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + dashy: + driver: bridge + name: dashy + ipam: + config: + - subnet: "10.89.235.0/24" + gateway: "10.89.235.254" + ''; + + extraConfig = { + sops.templates."dashy/config" = { + gid = "100"; + uid = "100999"; + mode = "0440" ; + content = '' + pageInfo: + title: My Homelab + sections: + - name: Example Section + icon: far fa-rocket + items: + - title: GitHub + description: Dashy source code and docs + icon: fab fa-github + url: https://github.com/Lissy93/dashy + - title: Issues + description: View open issues, or raise a new one + icon: fas fa-bug + url: https://github.com/Lissy93/dashy/issues + - name: Local Services + items: + - title: Firewall + icon: favicon + url: http://192.168.1.1/ + - title: Game Server + icon: https://i.ibb.co/710B3Yc/space-invader-x256.png + url: http://192.168.130.1/ + ''; + path = "/etc/dashy/dashy.yaml"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/default.nix b/modules/server/services/applications/default.nix new file mode 100644 index 0000000..13a4f6b --- /dev/null +++ b/modules/server/services/applications/default.nix @@ -0,0 +1,29 @@ +{ ... }: + +{ + imports = [ + # Good + ./gitea.nix + ./immich.nix + ./nextcloud.nix + ./passbolt.nix + ./traefik.nix + # Testing needed + ./authelia.nix + ./crafty.nix + ./dashy.nix + ./frigate.nix + ./home-assistant.nix + ./homepage.nix + ./it-tools.nix + ./jellyfin.nix + ./lldap.nix + ./n8n.nix + ./netbird.nix + ./netbootxyz.nix + ./ntfy.nix + ./odoo.nix + ./uptime-kuma.nix + ./vscodium.nix + ]; +} \ No newline at end of file diff --git a/modules/server/services/applications/frigate.nix b/modules/server/services/applications/frigate.nix new file mode 100644 index 0000000..558efbf --- /dev/null +++ b/modules/server/services/applications/frigate.nix @@ -0,0 +1,83 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "frigate"; + # Version tagging + frigateVersion = "0.16.4"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.frigate; +in + +helper.mkPodmanService { + inherit name; + pod = "home-assistant"; + description = "Frigate, your fully-local NVR (Network Video Recorder)"; + defaultPort = "8971"; + scheme = "https"; + dependencies = [ + "sops-install-secrets.service" + "traefik.service" + "authelia.service" + "home-assistant.service" + "${config.numbus-server.services.dns}.service" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "1000:100 ${cfg.configDir}" + "1000:100 ${cfg.dataDir}" + ]; + + composeText = '' + services: + frigate: + image: ghcr.io/blakeblackshear/frigate:${frigateVersion} + container_name: frigate + hostname: frigate + shm_size: "256mb" + networks: + home-assistant: + ipv4_address: 10.89.230.253 + ports: + - "${cfg.port}:8971/tcp" + volumes: + - ${cfg.configDir}:/config + - ${cfg.dataDir}:/media/frigate + - /etc/localtime:/etc/localtime:ro + - type: tmpfs + target: /tmp/cache + tmpfs: + size: 1000000000 + environment: + - FRIGATE_MQTT_USER=${config.sops.placeholder."home-assistant/mqtt_username"} + - FRIGATE_MQTT_PASSWORD=${config.sops.placeholder."home-assistant/mqtt_password"} + ${lib.optionalString (cfg.devices != []) '' + devices: + ${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)} + ''} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + stop_grace_period: 30s + restart: unless-stopped + + networks: + home-assistant: + external: true + ''; + + extraOptions = { + devices = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/dri:/dev/dri" "/dev/bus/usb:/dev/bus/usb" "/dev/apex_0:/dev/apex_0" ]; + description = "List of devices to map into the container. /dev/dri is used for graphics acceleration, /dev/bus/usb for USB Coral TPUs, and /dev/apex_0 for PCI coral TPUs"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/gitea.nix b/modules/server/services/applications/gitea.nix new file mode 100644 index 0000000..8beaf8b --- /dev/null +++ b/modules/server/services/applications/gitea.nix @@ -0,0 +1,103 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "gitea"; + # Version tagging + giteaVersion = "1.25.4-rootless"; + databaseVersion = "18-alpine"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.gitea; +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "Gitea, your own self-hosted git platform"; + defaultPort = "3000"; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${optimizedDir}/data" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/database" + ]; + secrets = [ + "gitea/db_name" + "gitea/db_username" + "gitea/db_password" + ]; + + composeText = '' + services: + gitea-database: + image: docker.io/library/postgres:${databaseVersion} + container_name: gitea-database + hostname: gitea-database + user: '1000:1000' + networks: + gitea: + ipv4_address: 10.89.240.253 + volumes: + - ${cfg.configDir}/database:/var/lib/postgresql + environment: + - POSTGRES_DB=${config.sops.placeholder."gitea/db_name"} + - POSTGRES_USER=${config.sops.placeholder."gitea/db_username"} + - POSTGRES_PASSWORD=${config.sops.placeholder."gitea/db_password"} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + gitea-server: + image: docker.gitea.com/gitea:${giteaVersion} + container_name: gitea-server + hostname: gitea-server + user: '1000:1000' + networks: + gitea: + ipv4_address: 10.89.240.252 + ports: + - "${cfg.port}:3000/tcp" + volumes: + - ${optimizedDir}/data:/var/lib/gitea + - ${cfg.configDir}/config:/etc/gitea + - /etc/localtime:/etc/localtime:ro + environment: + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=gitea-database:5432 + - GITEA__database__NAME=${config.sops.placeholder."gitea/db_name"} + - GITEA__database__USER=${config.sops.placeholder."gitea/db_username"} + - GITEA__database__PASSWD=${config.sops.placeholder."gitea/db_password"} + - GITEA__server__SSH_PORT=2424 + - GITEA__server__ROOT_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain} + depends_on: + - gitea-database + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + gitea: + driver: bridge + name: gitea + ipam: + config: + - subnet: "10.89.240.0/24" + gateway: "10.89.240.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/home-assistant.nix b/modules/server/services/applications/home-assistant.nix new file mode 100644 index 0000000..0a9dca9 --- /dev/null +++ b/modules/server/services/applications/home-assistant.nix @@ -0,0 +1,177 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "home-assistant"; + # Version tagging + homeAssistantVersion = "2026.2.3"; + mqttVersion = "2.1-alpine"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.home-assistant; +in + +helper.mkPodmanService { + inherit name; + description = "Home Assistant, libre house control and much more"; + defaultPort = "8123"; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "1000:100 ${cfg.configDir}" + "1000:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/mqtt" + ]; + secrets = [ + "home-assistant/mqtt_user" + "home-assistant/mqtt_password" + ]; + +# Compose file good + composeText = '' + services: + home-assistant: + image: ghcr.io/home-assistant/home-assistant:${homeAssistantVersion} + container_name: home-assistant + hostname: home-assistant + networks: + home-assistant: + ipv4_address: 10.89.230.252 + ports: + - "${cfg.port}:8123/tcp" + volumes: + - ${cfg.configDir}/config:/config + - /etc/localtime:/etc/localtime:ro + - /run/dbus:/run/dbus:ro + ${lib.optionalString (cfg.devices != []) '' + devices: + ${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)} + ''} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + home-assistant-mqtt: + image: docker.io/library/eclipse-mosquitto:${mqttVersion} + container_name: home-assistant-mqtt + hostname: home-assistant-mqtt + user: '1000:1000' + networks: + home-assistant: + ipv4_address: 10.89.230.252 + volumes: + - ${cfg.configDir}/mqtt:/mosquitto + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + home-assistant: + driver: bridge + name: home-assistant + ipam: + config: + - subnet: "10.89.230.0/24" + gateway: "10.89.230.254" + ''; + + extraOptions = { + devices = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/serial/by-id/Sonoff_Zigbee_3.0-id-port0:/dev/ttyUSB0" ]; + description = "List of devices to map into the container. /dev/ttyUSB0 is used for Zigbee dongles"; + }; + }; + + extraConfig = { + systemd.services."${name}-quirk" = { + description = "Podman container quirk : ${name}"; + after = [ "${name}.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.coreutils pkgs.systemd ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + mkdir -p /var/lib/numbus-server/${name} + if [[ -e ${cfg.configDir}/config/configuration.yaml ]]; then + if grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then + exit 0 + elif grep -qF "use_x_forwarded_for" ${cfg.configDir}/config/configuration.yaml && ! grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then + tmp=$(mktemp) + head -n -6 ${cfg.configDir}/config/configuration.yaml > "$tmp" + mv "$tmp" ${cfg.configDir}/config/configuration.yaml + fi + fi + + until [[ -e ${cfg.configDir}/config/configuration.yaml ]]; do + sleep 15 + done + cat << 'EOF' >> ${cfg.configDir}/config/configuration.yaml + + http: + use_x_forwarded_for: true + trusted_proxies: 10.89.230.1 + + zha: + EOF + + systemctl restart ${name}.service + ''; + }; + }; + + systemd.services."mqtt-quirk" = { + description = "Podman container quirk : Home-assistant MQTT"; + after = [ "sops-install-secrets.service" ]; + before = [ "${name}.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.coreutils pkgs.mosquitto ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if [[ -e ${cfg.configDir}/mqtt/mosquitto.conf && ${cfg.configDir}/mqtt/password.txt ]]; then + if grep -qF "listener 1883" ${cfg.configDir}/mqtt/mosquitto.conf; then + exit 0 + else + rm ${cfg.configDir}/mqtt/mosquitto.conf + rm ${cfg.configDir}/mqtt/password.txt + touch ${cfg.configDir}/mqtt/mosquitto.conf + touch ${cfg.configDir}/mqtt/password.txt + fi + fi + + cat << EOF >> ${cfg.configDir}/mqtt/mosquitto.conf + persistence true + persistence_location /mosquitto/data/ + log_dest file /mosquitto/log/mosquitto.log + listener 1883 + ## Authentication ## + allow_anonymous false + password_file /mosquitto/password.txt + EOF + + HOME_ASSISTANT_MQTT_USER=$(cat /run/secrets/home-assistant/mqtt_user) + HOME_ASSISTANT_MQTT_PASSWORD=$(cat /run/secrets/home-assistant/mqtt_password) + + mosquitto_passwd -b ${cfg.configDir}/mqtt/password.txt "$HOME_ASSISTANT_MQTT_USER" "$HOME_ASSISTANT_MQTT_PASSWORD" + chmod 0400 ${cfg.configDir}/mqtt/password.txt + ''; + }; +} diff --git a/modules/server/services/applications/homepage.nix b/modules/server/services/applications/homepage.nix new file mode 100644 index 0000000..689ba16 --- /dev/null +++ b/modules/server/services/applications/homepage.nix @@ -0,0 +1,63 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "homepage"; + # Version tagging + homepageVersion = "v1.10.1"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.homepage; +in + +helper.mkPodmanService { + inherit name; + description = "Homepage, a modern and highly customizable application dashboard"; + defaultPort = "3003"; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/images" + "100999:100 ${cfg.configDir}/icons" + ]; + + composeText = '' + services: + homepage: + image: ghcr.io/gethomepage/homepage:${homepageVersion} + container_name: homepage + hostname: homepage + user: '1000:1000' + networks: + homepage: + ports: + - "${cfg.port}:3000/tcp" + volumes: + - ${cfg.configDir}/config:/app/config + - ${cfg.configDir}/images:/app/public/images + - ${cfg.configDir}/icons:/app/public/icons + environment: + PUID: 1000 + PGID: 1000 + HOMEPAGE_ALLOWED_HOSTS: ${cfg.subdomain}.${config.numbus-server.services.domain} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + homepage: + driver: bridge + name: homepage + ipam: + config: + - subnet: "10.89.220.0/24" + gateway: "10.89.220.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/immich.nix b/modules/server/services/applications/immich.nix new file mode 100644 index 0000000..6c875b4 --- /dev/null +++ b/modules/server/services/applications/immich.nix @@ -0,0 +1,190 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container configuration + name = "immich"; + # Version tagging + immichVersion = "v2.5.6"; + redisVersion = "9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63"; + databaseVersion = "14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.immich; +in + +helper.mkPodmanService { + inherit name; + description = "Immich, Google Photos but better"; + defaultPort = "2283"; + middlewares = [ + "immichSecureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/redis" + "100999:100 ${cfg.configDir}/model-cache" + "100999:100 ${cfg.configDir}/machine-learning-cache" + "100999:100 ${cfg.configDir}/machine-learning-config" + "100999:100 ${cfg.configDir}/database" + "100999:100 ${cfg.dataDir}" + ]; + secrets = [ + "immich/redis_hostname" + "immich/db_hostname" + "immich/db_name" + "immich/db_username" + "immich/db_password" + ]; + +# Compose file good + composeText = '' + services: + immich-server: + container_name: immich-server + hostname: immich-server + image: ghcr.io/immich-app/immich-server:${immichVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.253 + ports: + - "${cfg.port}:2283/tcp" + volumes: + - $UPLOAD_LOCATION:/data + - /etc/localtime:/etc/localtime:ro + environment: + TZ: $TZ + REDIS_HOSTNAME: ${config.sops.placeholder."immich/redis_hostname"} + DB_HOSTNAME: ${config.sops.placeholder."immich/db_hostname"} + DB_DATABASE_NAME: ${config.sops.placeholder."immich/db_name"} + DB_USERNAME: ${config.sops.placeholder."immich/db_username"} + DB_PASSWORD: ${config.sops.placeholder."immich/db_password"} + IMMICH_TRUSTED_PROXIES: 10.89.210.1 + depends_on: + - immich-redis + - immich-database + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + immich-machine-learning: + container_name: immich-machine-learning + hostname: immich-machine-learning + image: ghcr.io/immich-app/immich-machine-learning:${immichVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.252 + volumes: + - ${cfg.configDir}/model-cache:/cache + - ${cfg.configDir}/machine-learning-config:/usr/src/.config + - ${cfg.configDir}/machine-learning-cache:/usr/src/.cache/ + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + immich-redis: + container_name: immich-redis + hostname: immich-redis + image: docker.io/valkey/valkey:${redisVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.251 + volumes: + - ${cfg.configDir}/redis:/data + healthcheck: + test: redis-cli ping || exit 1 + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + immich-database: + container_name: immich-database + hostname: immich-database + image: ghcr.io/immich-app/postgres:${databaseVersion} + user: '1000:1000' + networks: + immich: + ipv4_address: 10.89.210.250 + environment: + POSTGRES_DB: ${config.sops.placeholder."immich/db_name"} + POSTGRES_USER: ${config.sops.placeholder."immich/db_username"} + POSTGRES_PASSWORD: ${config.sops.placeholder."immich/db_password"} + POSTGRES_INITDB_ARGS: '--data-checksums' + volumes: + - $DB_DATA_LOCATION:/var/lib/postgresql/data + shm_size: 128mb + healthcheck: + disable: false + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + immich: + driver: bridge + name: immich + ipam: + config: + - subnet: "10.89.210.0/24" + gateway: "10.89.210.254" + ''; + + extraConfig = { + sops.templates."immich/env" = { + gid = "100"; + uid = "1000"; + mode = "0400"; + content = '' + DB_DATA_LOCATION=${cfg.configDir}/database + UPLOAD_LOCATION=${cfg.dataDir} + ''; + path = "/etc/podman/immich/.env"; + }; + + sops.templates."traefik/rules/immich-secureHeaders" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + middlewares: + immichSecureHeaders: + headers: + FrameDeny: true + AccessControlAllowMethods: 'GET,POST,PUT,DELETE,OPTIONS' + AccessControlAllowOriginList: + - https://${cfg.subdomain}.${config.numbus-server.services.domain} + - origin-list-or-null + AccessControlMaxAge: 100 + AddVaryHeader: true + BrowserXssFilter: true + ContentTypeNosniff: true + ForceSTSHeader: true + STSIncludeSubdomains: true + STSPreload: true + ContentSecurityPolicy: "default-src 'self'; base-uri 'self'; img-src 'self' https://static.immich.cloud https://tiles.immich.cloud data: blob:; connect-src 'self' https://${cfg.subdomain}.${config.numbus-server.services.domain} wss://${cfg.subdomain}.${config.numbus-server.services.domain} https://static.immich.cloud https://tiles.immich.cloud; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob: https://${cfg.subdomain}.${config.numbus-server.services.domain}; frame-ancestors 'self';" + CustomFrameOptionsValue: SAMEORIGIN + ReferrerPolicy: same-origin + PermissionsPolicy: vibrate 'self' + STSSeconds: 315360000 + ''; + path = "/etc/traefik/rules/immich-secureHeaders.yaml"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/it-tools.nix b/modules/server/services/applications/it-tools.nix new file mode 100644 index 0000000..2d35d9a --- /dev/null +++ b/modules/server/services/applications/it-tools.nix @@ -0,0 +1,54 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "it-tools"; + # Version tagging + it-toolsVersion = "2024.10.22-7ca5933"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.it-tools; +in + +helper.mkPodmanService { + inherit name; + description = "IT-tools, useful tools when doing IT"; + pod = "false"; + defaultPort = "8880"; + configDirEnabled = false; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + +# Compose file good + composeText = '' + services: + it-tools: + image: docker.io/corentinth/it-tools:${it-toolsVersion} + container_name: it-tools + hostname: it-tools + user: '1000:1000' + networks: + it-tools: + ipv4_address: 10.89.200.253 + ports: + - "${cfg.port}:80/tcp" + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + it-tools: + driver: bridge + name: it-tools + ipam: + config: + - subnet: "10.89.200.0/24" + gateway: "10.89.200.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/jellyfin.nix b/modules/server/services/applications/jellyfin.nix new file mode 100644 index 0000000..991bce2 --- /dev/null +++ b/modules/server/services/applications/jellyfin.nix @@ -0,0 +1,69 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "jellyfin"; + # Version tagging + jellyfinVersion = "10.11.6"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.jellyfin; +in + +helper.mkPodmanService { + inherit name; + description = "Jellyfin : A self-hosted media server to stream your movies and music"; + defaultPort = "8096"; + scheme = "https"; #TODO CHECK + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.dataDir}" + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.dataDir}/media" + "100999:100 ${cfg.dataDir}/fonts" + "100999:100 ${cfg.configDir}/cache" + "100999:100 ${cfg.configDir}/config" + ]; + + composeText = '' + services: + jellyfin: + image: docker.io/jellyfin/jellyfin:${jellyfinVersion} + container_name: jellyfin + hostname: jellyfin + user: '1000:1000' + networks: + jellyfin: + ipv4_address: 10.89.190.253 + ports: + - "${cfg.port}:8096/tcp" + volumes: + - ${cfg.configDir}/config:/config + - ${cfg.configDir}/cache:/cache + - type: bind + source: ${cfg.dataDir}/media + target: /media + - type: bind + source: ${cfg.dataDir}/fonts + target: /usr/local/share/fonts/custom + read_only: true + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + jellyfin: + driver: bridge + name: jellyfin + ipam: + config: + - subnet: "10.89.190.0/24" + gateway: "10.89.190.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/lldap.nix b/modules/server/services/applications/lldap.nix new file mode 100644 index 0000000..7e423ed --- /dev/null +++ b/modules/server/services/applications/lldap.nix @@ -0,0 +1,84 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "lldap"; + # Version tagging + lldapVersion = "v0.6.2"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.lldap; + # Derive Base DN from domain (e.g., example.com -> dc=example,dc=com) + domainParts = splitString "." config.numbus-server.services.domain; + baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts); +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "LLDAP, unified user management"; + defaultPort = "17170"; + dependencies = [ + "sops-install-secrets.service" + "network-online.target" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "lldap/jwt_secret" + "lldap/key_seed" + "lldap/admin_password" + ]; + + composeText = '' + services: + lldap: + image: lldap/lldap:${lldapVersion} + container_name: lldap + hostname: lldap + user: '1000:1000' + networks: + lldap: + ipv4_address: 10.89.185.253 + ports: + - "3890:3890" + - "${cfg.port}:17170" + volumes: + - ${cfg.configDir}:/data + environment: + - UID=1000 + - GID=1000 + - TZ=${config.time.timeZone} + - LLDAP_LDAP_BASE_DN=${baseDN} + - LLDAP_JWT_SECRET="${config.sops.placeholder."lldap/jwt_secret"}" + - LLDAP_KEY_SEED="${config.sops.placeholder."lldap/key_seed"}" + - LLDAP_LDAP_USER_PASS="${config.sops.placeholder."lldap/admin_password"}" + - LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true + - LLDAP_SMTP_OPTIONS__SERVER=${config.numbus-server.mail.smtpServer} + - LLDAP_SMTP_OPTIONS__PORT=${config.numbus-server.mail.smtpPort} + - LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=${config.numbus-server.mail.smtpEncryption} + - LLDAP_SMTP_OPTIONS__USER=${config.numbus-server.mail.smtpUsername} + - LLDAP_SMTP_OPTIONS__PASSWORD=${config.sops.placeholder."mail/smtpPassword"} + - LLDAP_SMTP_OPTIONS__FROM=no-reply <${config.numbus-server.mail.fromAddress}> + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + lldap: + driver: bridge + name: lldap + ipam: + config: + - subnet: "10.89.185.0/24" + gateway: "10.89.185.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/n8n.nix b/modules/server/services/applications/n8n.nix new file mode 100644 index 0000000..bbfffc6 --- /dev/null +++ b/modules/server/services/applications/n8n.nix @@ -0,0 +1,72 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "n8n"; + # Version tagging + n8nVersion = "2.11.4"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.n8n; +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "n8n, the ultimate automation platform"; + defaultPort = "5678"; + scheme = "https"; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${optimizedDir}" + ]; + + composeText = '' + services: + n8n: + image: docker.n8n.io/n8nio/n8n:${n8nVersion} + container_name: n8n + hostname: n8n + user: '1000:1000' + networks: + n8n: + ipv4_address: 10.89.180.253 + ports: + - "${cfg.port}:5678" + volumes: + - ${optimizedDir}:/home/node/.n8n + environment: + - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true + - N8N_HOST=${cfg.subdomain}.${config.numbus-server.services.domain} + - N8N_PORT=5678 + - N8N_PROTOCOL=https + - N8N_RUNNERS_ENABLED=true + - NODE_ENV=production + - WEBHOOK_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}/ + - GENERIC_TIMEZONE=${time.timeZone} + - TZ=${time.timeZone} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + n8n: + driver: bridge + name: n8n + ipam: + config: + - subnet: "10.89.180.0/24" + gateway: "10.89.180.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/netbird.nix b/modules/server/services/applications/netbird.nix new file mode 100644 index 0000000..2018076 --- /dev/null +++ b/modules/server/services/applications/netbird.nix @@ -0,0 +1,203 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "netbird"; + # Version tagging + netbirdDashboardVersion = ""; + netbirdServerVersion = ""; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.netbird; +in + +helper.mkPodmanService { + inherit name; + pod = "false"; + description = "NetBird, an all-in-one ZTNA remote access platform"; + defaultPort = "8888"; + reverseProxied = false; + dependencies = [ + "sops-install-secrets.service" + "traefik.service" + "${config.numbus-server.services.dns}.service" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "netbird/auth_key" + "netbird/encryption_key" + ]; + + composeText = '' + services: + netbird-dashboard: + image: netbirdio/dashboard:${netbirdDashboardVersion} + container_name: netbird-dashboard + hostname: netbird-dashboard + user: '1000:1000' + networks: + netbird: + ipv4_address: 10.89.175.253 + ports: + - "${defaultPort}:8080/tcp" + environment: + # Endpoints + - NETBIRD_MGMT_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain} + - NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain} + # OIDC - using embedded IdP + - AUTH_AUDIENCE=netbird-dashboard + - AUTH_CLIENT_ID=netbird-dashboard + - AUTH_CLIENT_SECRET= + - AUTH_AUTHORITY=https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2 + - USE_AUTH0=false + - AUTH_SUPPORTED_SCOPES=openid profile email groups + - AUTH_REDIRECT_URI=/nb-auth + - AUTH_SILENT_REDIRECT_URI=/nb-silent-auth + # SSL + - NGINX_SSL_PORT=443 + - LETSENCRYPT_DOMAIN=none + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "2" + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + netbird-server: + image: netbirdio/netbird-server:${netbirdServerVersion} + container_name: netbird-server + hostname: netbird-server + user: '1000:1000' + networks: + netbird: + ipv4_address: 10.89.175.252 + ports: + - "8889:8081/tcp" + - "3478:3478/udp" + volumes: + - ${config.sops.templates."netbird-config".path}:/etc/netbird/config.yaml + - ${cfg.configDir}:/var/lib/netbird + command: ["--config", "/etc/netbird/config.yaml"] + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "2" + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + netbird: + driver: bridge + name: netbird + ipam: + config: + - subnet: "10.89.175.0/24" + gateway: "10.89.175.254" + ''; + + extraConfig = { + sops.templates."netbird-config" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + server: + listenAddress: ":80" + exposedAddress: "https://${cfg.subdomain}.${config.numbus-server.services.domain}:443" + stunPorts: + - 3478 + metricsPort: 9090 + healthcheckAddress: ":9000" + logLevel: "info" + logFile: "console" + authSecret: "${config.sops.placeholder."netbird/auth_key"}" + dataDir: "/var/lib/netbird" + + auth: + issuer: "https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2" + signKeyRefreshEnabled: true + dashboardRedirectURIs: + - "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-auth" + - "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-silent-auth" + cliRedirectURIs: + - "http://localhost:53000/" + + reverseProxy: + trustedHTTPProxies: + - "10.89.175.1/32" + + store: + engine: "sqlite" + encryptionKey: "${config.sops.placeholder."netbird/encryption_key"}" + ''; + path = "/etc/netbird/netbird.yaml"; + }; + + sops.templates."traefik/rules/${name}" = { + gid = "100"; + uid = "1000"; + mode = "0400"; + content = '' + http: + routers: + ${name}-dashboard: + rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`)" + entrypoints: + - "websecure" + middlewares: + - secureHeaders + tls: + certresolver: "cloudflare" + options: "secureTLS" + priority: 1 + ${name}-grpc: + rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`))" + entrypoints: + - "websecure" + service: ${name}-server-h2c + middlewares: + - secureHeaders + tls: + certresolver: "cloudflare" + options: "secureTLS" + ${name}-backend: + rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))" + entrypoints: + - "websecure" + service: ${name}-server + middlewares: + - secureHeaders + tls: + certresolver: "cloudflare" + options: "secureTLS" + + services:${cfg.port} + ${name}-dashboard: + loadBalancer: + servers: + - url: "http://host.containers.internal:${cfg.port}" + ${name}-server: + loadBalancer: + servers: + - url: "http://host.containers.internal:8889" + ${name}-server-h2c: + loadBalancer: + servers: + - url: "h2c://host.containers.internal:3478" + ''; + path = "/etc/traefik/rules/${name}"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/netbootxyz.nix b/modules/server/services/applications/netbootxyz.nix new file mode 100644 index 0000000..bda78ae --- /dev/null +++ b/modules/server/services/applications/netbootxyz.nix @@ -0,0 +1,75 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "netbootxyz"; + # Version tagging + netbootxyzVersion = "3.0.0"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.netbootxyz; +in + +helper.mkPodmanService { + inherit name; + description = "Netboot.xyz, forget about flashing isos on USB sticks with PXE boot"; + pod = "false"; + defaultPort = "3004"; + configDirEnabled = optimizedDir == cfg.configDir; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${optimizedDir}" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${optimizedDir}/assets" + ]; + + composeText = '' + services: + netbootxyz: + image: ghcr.io/netbootxyz/netbootxyz:${netbootxyzVersion} + container_name: netbootxyz + hostname: netbootxyz + user: '1000:1000' + networks: + netbootxyz: + ipv4_address: 10.89.170.253 + ports: + - "${cfg.port}:3000/tcp" + - "69:69/udp" + - "8008:80/tcp" + volumes: + - ${cfg.configDir}/config:/config + - ${optimizedDir}/assets:/assets + environment: + - PUID=1000 + - PGID=1000 + - TZ=${time.timeZone} + - PORT_RANGE=30000:30010 + - SUBFOLDER=/ + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + netbootxyz: + driver: bridge + name: netbootxyz + ipam: + config: + - subnet: "10.89.170.0/24" + gateway: "10.89.170.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/nextcloud.nix b/modules/server/services/applications/nextcloud.nix new file mode 100644 index 0000000..599b57e --- /dev/null +++ b/modules/server/services/applications/nextcloud.nix @@ -0,0 +1,384 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Version tagging + nextcloudVersion = "33.0.0"; + redisVersion = "8.6-alpine"; + databaseVersion = "11.8"; + onlyofficeVersion = "9.2"; + whiteboardVersion = "v1.5.6"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.nextcloud; + # Container config + name = "nextcloud"; +in + +helper.mkPodmanService { + inherit name; + description = "Nextcloud, your own online office suite"; + defaultPort = "1100"; + middlewares = [ + "nextcloudSecureHeaders" + ]; + secrets = [ + "nextcloud/db_name" + "nextcloud/db_username" + "nextcloud/db_password" + "nextcloud/redis_password" + "nextcloud/onlyoffice_secret" + "nextcloud/whiteboard_secret" + ]; + dirPermissions = [ + "100032:100 ${cfg.dataDir}" + "100032:100 ${cfg.configDir}" + "100032:100 ${cfg.configDir}/web" + "100999:100 ${cfg.configDir}/redis" + "100999:100 ${cfg.configDir}/database" + "1000:100 ${cfg.configDir}/onlyoffice" + "1000:100 ${cfg.configDir}/onlyoffice/log" + "1000:100 ${cfg.configDir}/onlyoffice/cache" + "1000:100 ${cfg.configDir}/onlyoffice/data" + "1000:100 ${cfg.configDir}/onlyoffice/database" + ]; + +# Compose file good + composeText = '' + services: + nextcloud-server: + image: docker.io/library/nextcloud:${nextcloudVersion} + container_name: nextcloud-server + hostname: nextcloud-server + networks: + nextcloud: + ipv4_address: 10.89.160.253 + ports: + - "${cfg.port}:80/tcp" + volumes: + - ${cfg.configDir}/web:/var/www/html + - ${cfg.dataDir}:/mnt/ncdata + environment: + MYSQL_HOST: nextcloud-database:3306 + MYSQL_DATABASE: ${config.sops.placeholder."nextcloud/db_name"} + MYSQL_USER: ${config.sops.placeholder."nextcloud/db_username"} + MYSQL_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"} + REDIS_HOST_PASSWORD: ${config.sops.placeholder."nextcloud/redis_password"} + REDIS_HOST: nextcloud-redis + NEXTCLOUD_TRUSTED_DOMAINS: ${cfg.subdomain}.${config.numbus-server.services.domain} + NEXTCLOUD_DATA_DIR: /mnt/ncdata + SMTP_SECURE: tls + SMTP_HOST: ${config.numbus-server.mail.smtpServer} + SMTP_PORT: ${toString config.numbus-server.mail.smtpPort} + SMTP_NAME: ${config.numbus-server.mail.smtpUsername} + SMTP_PASSWORD: ${config.sops.placeholder.smtpPassword} + MAIL_FROM_ADDRESS: no-reply + MAIL_DOMAIN: ${config.numbus-server.services.domain} + APACHE_DISABLE_REWRITE_IP: 1 + OVERWRITEPROTOCOL: https + TRUSTED_PROXIES: 10.89.160.1 + NC_default_phone_region: "${config.numbus-server.language}" + NC_default_language: "${config.numbus-server.language}" + NC_default_locale: "${config.numbus-server.locale}" + NC_default_timezone: "${config.time.timeZone}" + NC_maintenance_window_start: "1" + depends_on: + - nextcloud-database + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + nextcloud-redis: + image: docker.io/library/redis:${redisVersion} + container_name: nextcloud-redis + hostname: nextcloud-redis + user: '1000:1000' + networks: + nextcloud: + ipv4_address: 10.89.160.252 + volumes: + - ${cfg.configDir}/redis:/data + command: redis-server --requirepass ${config.sops.placeholder."nextcloud/redis_password"} --save 60 1 --loglevel warning + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + nextcloud-database: + image: docker.io/library/mariadb:${databaseVersion} + container_name: nextcloud-database + hostname: nextcloud-database + user: '1000:1000' + networks: + nextcloud: + ipv4_address: 10.89.160.251 + volumes: + - ${cfg.configDir}/database:/var/lib/mysql + environment: + MARIADB_DATABASE: ${config.sops.placeholder."nextcloud/db_name"} + MARIADB_USER: ${config.sops.placeholder."nextcloud/db_username"} + MARIADB_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"} + MARIADB_RANDOM_ROOT_PASSWORD: true + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + command: + - "--transaction-isolation=READ-COMMITTED" + - "--binlog-format=ROW" + restart: unless-stopped + + nextcloud-onlyoffice: + image: docker.io/onlyoffice/documentserver:${onlyofficeVersion} + container_name: nextcloud-onlyoffice + hostname: nextcloud-onlyoffice + networks: + nextcloud: + ipv4_address: 10.89.160.250 + ports: + - "9980:80/tcp" + volumes: + - ${cfg.configDir}/onlyoffice/log:/var/log/onlyoffice + - ${cfg.configDir}/onlyoffice/cache:/var/lib/onlyoffice + - ${cfg.configDir}/onlyoffice/data:/var/www/onlyoffice/Data + - ${cfg.configDir}/onlyoffice/database:/var/lib/postgresql + environment: + - JWT_SECRET=${config.sops.placeholder."nextcloud/onlyoffice_secret"} + - REDIS_SERVER_PASS=${config.sops.placeholder."nextcloud/redis_password"} + - REDIS_SERVER_HOST=nextcloud-redis + - REDIS_SERVER_PORT=6379 + - ADMINPANEL_ENABLED=false + - EXAMPLE_ENABLED=false + - METRICS_ENABLED=false + cap_drop: + - NET_RAW + restart: unless-stopped + + nextcloud-whiteboard: + image: ghcr.io/nextcloud-releases/whiteboard:${whiteboardVersion} + container_name: nextcloud-whiteboard + hostname: nextcloud-whiteboard + user: '1000:1000' + ports: + - "3002:3002/tcp" + environment: + NEXTCLOUD_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain} + JWT_SECRET_KEY: ${config.sops.placeholder."nextcloud/whiteboard_secret"} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + nextcloud: + driver: bridge + name: nextcloud + ipam: + config: + - subnet: "10.89.160.0/24" + gateway: "10.89.160.254" + ''; + + extraOptions = { + onlyoffice = { + subdomain = mkOption { + type = types.str; + default = "onlyoffice"; + example = "onlyoffice"; + description = "The subdomain that onlyoffice for nextcloud will use"; + }; + }; + whiteboard = { + subdomain = mkOption { + type = types.str; + default = "whiteboard"; + example = "whiteboard"; + description = "The subdomain that whiteboard for nextcloud will use"; + }; + }; + }; + + extraConfig = { + sops.templates."traefik/rules/nextcloud-onlyoffice" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + routers: + nextcloud-onlyoffice: + rule: "Host(`${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}`)" + entrypoints: + - "websecure" + service: nextcloud-onlyoffice + tls: + certresolver: "cloudflare" + options: "secureTLS" + services: + nextcloud-onlyoffice: + loadBalancer: + servers: + - url: "http://host.containers.internal:9980" + ''; + path = "/etc/traefik/rules/nextcloud-onlyoffice.yaml"; + }; + + sops.templates."traefik/rules/nextcloud-whiteboard" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + routers: + nextcloud-whiteboard: + rule: "Host(`${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}`)" + entrypoints: + - "websecure" + service: nextcloud-whiteboard + middlewares: + - "secureHeaders" + tls: + certresolver: "cloudflare" + options: "secureTLS" + services: + nextcloud-whiteboard: + loadBalancer: + servers: + - url: "http://host.containers.internal:3002" + ''; + path = "/etc/traefik/rules/nextcloud-whiteboard.yaml"; + }; + + sops.templates."traefik/rules/nextcloud-secureHeaders" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + middlewares: + nextcloudSecureHeaders: + headers: + FrameDeny: false + CustomFrameOptionsValue: "SAMEORIGIN" + AddVaryHeader: true + BrowserXssFilter: true + ContentTypeNosniff: true + ForceSTSHeader: true + STSSeconds: 315360000 + STSIncludeSubdomains: true + STSPreload: true + AccessControlAllowMethods: "GET,OPTIONS,PUT" + AccessControlAllowOriginList: + - origin-list-or-null + AccessControlMaxAge: 100 + ReferrerPolicy: same-origin + PermissionsPolicy: "vibrate=()" + ContentSecurityPolicy: >- + default-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self'; + script-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + connect-src 'self'; + img-src 'self' data:; + font-src 'self' data:; + frame-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self'; + frame-ancestors https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self'; + object-src 'none'; + base-uri 'self'; + ''; + path = "/etc/traefik/rules/nextcloud-secureHeaders"; + }; + + systemd.services."${name}-quirk" = { + description = "Podman container quirk : ${name}"; + wantedBy = [ "multi-user.target" ]; + after = [ "${name}.service" "${name}-secrets.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + startLimitBurst = 5; + startLimitIntervalSec = 600; + path = [ pkgs.coreutils pkgs.sudo pkgs.podman pkgs.systemd pkgs.gnugrep ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + OCC="sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ" + + [[ ! -e /var/lib/numbus-server/${name}/.env ]] && systemctl start ${name}-secrets.service + until [[ -e /var/lib/numbus-server/${name}/.env ]]; do + echo "Waiting for secrets generation..." + sleep 5 + done + source /var/lib/numbus-server/${name}/.env + + until $OCC status | grep -iq "installed: true" >/dev/null 2>&1; do + echo "Waiting for Nextcloud to be up and running..." + sleep 60 + done + + $OCC db:add-missing-indices + $OCC maintenance:repair --include-expensive + + INSTALL_APPS_LIST=( "calendar" "contacts" "mail" "notes" "onlyoffice" "cookbook" "whiteboard" ) + DISABLE_APPS_LIST=( "activity" "federation" "webhook_listeners" "photos" "recommendations" "sharebymail" "teams" "support" "richdocumentscode" ) + + for app in ''${INSTALL_APPS_LIST[@]}; do + if ! $OCC --no-warnings app:list | grep -iq "$app:"; then + $OCC --no-warnings app:install "$app" + fi + if $OCC --no-warnings app:list --disabled | grep -iq "$app:"; then + $OCC --no-warnings app:enable "$app" + fi + done + for app in ''${DISABLE_APPS_LIST[@]}; do + if $OCC --no-warnings app:list --enabled | grep -iq "$app:"; then + $OCC --no-warnings app:disable "$app" + fi + done + $OCC --no-warnings config:system:set onlyoffice DocumentServerInternalUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/" + $OCC --no-warnings config:system:set onlyoffice DocumentServerUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/" + $OCC --no-warnings config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_PASSWORD" + $OCC --no-warnings config:app:set whiteboard collabBackendUrl --value="https://${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}" + $OCC --no-warnings config:app:set whiteboard jwt_secret_key --value="$WHITEBOARD_PASSWORD" + + if [[ ! -f /var/lib/numbus-server/${name}/croned.true ]]; then + $OCC background:cron + sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php + touch /var/lib/numbus-server/${name}/croned.true + fi + + if [[ ! -f /var/lib/numbus-server/${name}/scanned.true ]]; then + $OCC files:scan --all + $OCC files:repair-tree + touch /var/lib/numbus-server/${name}/scanned.true + fi + ''; + }; + + systemd.services."${name}-cron" = { + description = "Podman container crontab : ${name}"; + after = [ "${name}.service" "${name}-quirk.service" ]; + onFailure = [ "service-failure-notify@%n.service" ]; + path = [ pkgs.sudo pkgs.podman ]; + serviceConfig = { + Type = "oneshot"; + ExecCondition = ''${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ status''; + ExecStart = "${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php"; + }; + }; + + systemd.timers."${name}-cron" = { + description = "Timer for Nextcloud cron"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + Unit = "${name}-cron.service"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/ntfy.nix b/modules/server/services/applications/ntfy.nix new file mode 100644 index 0000000..6650008 --- /dev/null +++ b/modules/server/services/applications/ntfy.nix @@ -0,0 +1,62 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "ntfy"; + # Version tagging + ntfyVersion = "v2.18.0"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.ntfy; +in + +helper.mkPodmanService { + inherit name; + description = "Ntfy, get notified easily"; + defaultPort = "8099"; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/cache" + "100999:100 ${cfg.configDir}/config" + ]; + + composeText = '' + services: + ntfy: + image: docker.io/binwiederhier/ntfy + container_name: ntfy + hostname: ntfy + user: "1000:1000" + networks: + ntfy: + ipv4_address: 10.89.150.253 + ports: + - "${cfg.port}:80/tcp" + command: + - serve + volumes: + - ${cfg.config}/cache:/var/cache/ntfy + - ${cfg.config}/config:/etc/ntfy + environment: + - TZ=${time.timeZone} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + ntfy: + driver: bridge + name: ntfy + ipam: + config: + - subnet: "10.89.150.0/24" + gateway: "10.89.150.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/odoo.nix b/modules/server/services/applications/odoo.nix new file mode 100644 index 0000000..fba6c67 --- /dev/null +++ b/modules/server/services/applications/odoo.nix @@ -0,0 +1,118 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "odoo"; + # Version tagging + odooVersion = "10.11.6"; + databaseVersion = "15.17"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.odoo; +in + +helper.mkPodmanService { + inherit name; + description = "Odoo : An open ERP (Enterprise resource planning) solution"; + defaultPort = "8069"; + configDirEnabled = optimizedDir == cfg.configDir; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${optimizedDir}" + "100999:100 ${optimizedDir}/odoo" + "100999:100 ${cfg.configDir}/addons" + "100999:100 ${cfg.configDir}/config" + "100999:100 ${cfg.configDir}/database" + ]; + + composeText = '' + services: + odoo-database: + image: docker.io/library/postgres:${databaseVersion} + container_name: odoo-database + hostname: odoo-database + user: '1000:1000' + shm_size: 128mb + networks: + odoo: + ipv4_address: 10.89.190.253 + volumes: + - ${cfg.configDir}/database:/var/lib/postgresql/data + environment: + - POSTGRES_DB=${config.sops.placeholder."odoo/db_name"} + - POSTGRES_PASSWORD=${config.sops.placeholder."odoo/db_password"} + - POSTGRES_USER=${config.sops.placeholder."odoo/db_username"} + - PGDATA=/var/lib/postgresql/data + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + odoo-server: + image: docker.io/library/odoo:${odooVersion} + container_name: odoo-server + hostname: odoo-server + user: '1000:1000' + networks: + odoo: + ipv4_address: 10.89.190.252 + ports: + - "${cfg.port}:8069/tcp" + volumes: + - ${optimizedDir}/odoo:/var/lib/odoo + - ${cfg.configDir}/config:/etc/odoo + - ${cfg.configDir}/addons:/mnt/extra-addons + environment: + - HOST=odoo-database + - USER=${config.sops.placeholder."odoo/db_username"} + - PASSWORD=${config.sops.placeholder."odoo/db_password"} + depends_on: + - odoo-database + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + odoo: + driver: bridge + name: odoo + ipam: + config: + - subnet: "10.89.190.0/24" + gateway: "10.89.190.254" + ''; + + extraConfig = { + sops.secrets."odoo/db_name" = { + sopsFile = /etc/nixos/secrets/podman/odoo.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + sops.secrets."odoo/db_username" = { + sopsFile = /etc/nixos/secrets/podman/odoo.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + sops.secrets."odoo/db_password" = { + sopsFile = /etc/nixos/secrets/podman/odoo.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + }; +} diff --git a/modules/server/services/applications/passbolt.nix b/modules/server/services/applications/passbolt.nix new file mode 100644 index 0000000..d16c53a --- /dev/null +++ b/modules/server/services/applications/passbolt.nix @@ -0,0 +1,112 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "passbolt"; + # Version tagging + passboltVersion = "5.9.0-1-ce-non-root"; + databaseVersion = "12.2"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.passbolt; +in + +helper.mkPodmanService { + inherit name; + description = "Passbolt, your password manager"; + defaultPort = "4433"; + scheme = "https"; + dataDirEnabled = false; + middlewares = [ "secureHeaders" ]; + dirPermissions = [ + "100032:100 ${cfg.configDir}" + "100032:100 ${cfg.configDir}/gpg" + "100032:100 ${cfg.configDir}/jwt" + "100999:100 ${cfg.configDir}/database" + ]; + secrets = [ + "passbolt/db_name" + "passbolt/db_username" + "passbolt/db_password" + ]; + +# Compose file good + composeText = '' + services: + passbolt-server: + image: docker.io/passbolt/passbolt:${passboltVersion} + container_name: passbolt-server + hostname: passbolt-server + user: '33:33' + networks: + passbolt: + ports: + - "${cfg.port}:4433/tcp" + volumes: + - ${cfg.configDir}/gpg:/etc/passbolt/gpg + - ${cfg.configDir}/jwt:/etc/passbolt/jwt + environment: + APP_DEFAULT_TIMEZONE: ${config.time.timeZone} + APP_FULL_BASE_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain} + DATASOURCES_DEFAULT_HOST: "passbolt-database" + DATASOURCES_DEFAULT_USERNAME: ${config.sops.placeholder."passbolt/db_username"} + DATASOURCES_DEFAULT_PASSWORD: ${config.sops.placeholder."passbolt/db_password"} + DATASOURCES_DEFAULT_DATABASE: ${config.sops.placeholder."passbolt/db_name"} + EMAIL_DEFAULT_FROM_NAME: "Passbolt" + EMAIL_TRANSPORT_DEFAULT_HOST: ${config.numbus-server.mail.smtpServer} + EMAIL_TRANSPORT_DEFAULT_PORT: ${toString config.numbus-server.mail.smtpPort} + EMAIL_TRANSPORT_DEFAULT_USERNAME: ${config.numbus-server.mail.smtpUsername} + EMAIL_TRANSPORT_DEFAULT_PASSWORD: ${config.sops.placeholder."mail/smtpPassword"} + EMAIL_TRANSPORT_DEFAULT_TLS: true + EMAIL_DEFAULT_FROM: passbolt-noreply@${config.numbus-server.services.domain} + PASSBOLT_SSL_FORCE: true + command: + [ + "/usr/bin/wait-for.sh", + "-t", + "0", + "passbolt-database:3306", + "--", + "/docker-entrypoint.sh" + ] + depends_on: + - passbolt-database + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + passbolt-database: + image: docker.io/library/mariadb:${databaseVersion} + container_name: passbolt-database + hostname: passbolt-database + user: '1000:1000' + networks: + passbolt: + volumes: + - ${cfg.configDir}/database:/var/lib/mysql + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "true" + MYSQL_DATABASE: ${config.sops."passbolt/db_name"} + MYSQL_USER: ${config.sops."passbolt/db_username"} + MYSQL_PASSWORD: ${config.sops."passbolt/db_password"} + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + restart: unless-stopped + + networks: + passbolt: + name: passbolt + driver: bridge + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/traefik.nix b/modules/server/services/applications/traefik.nix new file mode 100644 index 0000000..d776c07 --- /dev/null +++ b/modules/server/services/applications/traefik.nix @@ -0,0 +1,178 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "traefik"; + # Version tagging + traefikVersion = "v3.6.8"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.traefik; +in + +helper.mkPodmanService { + inherit name; + description = "Traefik reverse proxy, one to rule them all"; + defaultPort = "7780"; + pod = "false"; + startDelay = 10; + dataDirEnabled = false; + middlewares = [ + "secureHeaders" + ]; + dependencies = [ + "sops-install-secrets.service" + "network-online.target" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + "100999:100 ${cfg.configDir}/certs" + ]; + +# Compose file good + composeText = '' + services: + traefik: + image: docker.io/library/traefik:${traefikVersion} + container_name: traefik + hostname: traefik + user: '1000:1000' + network_mode: pasta + ports: + - "${cfg.port}:8080/tcp" + - "443:443/tcp" + volumes: + - ${config.sops.templates."traefik/config".path}:/etc/traefik/traefik.yaml:ro + - ${cfg.configDir}/certs:/var/traefik/certs + - /etc/traefik/rules:/etc/traefik/rules:ro + environment: + - CF_DNS_API_TOKEN=${config.sops.placeholder."traefik/cloudflare_api_token"} + cap_add: + - NET_BIND_SERVICE + security_opt: + - no-new-privileges:true + restart: unless-stopped + ''; + + extraConfig = { + sops.secrets."traefik/cloudflare_api_token" = { + sopsFile = /etc/nixos/secrets/podman/traefik.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + + sops.templates."traefik/config"= { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + global: + checkNewVersion: false + sendAnonymousUsage: false + log: + level: ${cfg.logLevel} + accesslog: {} + api: + dashboard: true + insecure: false + entryPoints: + web: + address: :80 + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: :443 + forwardedHeaders: + trustedIPs: + - "127.0.0.1/32" + - "10.0.0.0/8" + - "192.168.0.0/16" + - "172.16.0.0/12" + certificatesResolvers: + cloudflare: + acme: + email: ${config.numbus-server.mail.adminAddress} + storage: /var/traefik/certs/cloudflare-acme.json + caServer: "https://acme-v02.api.letsencrypt.org/directory" + dnsChallenge: + provider: cloudflare + resolvers: + - "1.1.1.1:53" + - "9.9.9.9:53" + serversTransport: + insecureSkipVerify: true + providers: + file: + directory: "/etc/traefik/rules" + watch: true + ''; + path = "/etc/traefik/traefik.yaml"; + }; + + sops.templates."traefik/rules/secureHeaders" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + http: + middlewares: + secureHeaders: + headers: + FrameDeny: true + AccessControlAllowMethods: 'GET,OPTIONS,PUT' + AccessControlAllowOriginList: + - origin-list-or-null + AccessControlMaxAge: 100 + AddVaryHeader: true + BrowserXssFilter: true + ContentTypeNosniff: true + ForceSTSHeader: true + STSIncludeSubdomains: true + STSPreload: true + ContentSecurityPolicy: default-src 'self' 'unsafe-inline' + CustomFrameOptionsValue: SAMEORIGIN + ReferrerPolicy: same-origin + PermissionsPolicy: vibrate 'self' + STSSeconds: 315360000 + ''; + path = "/etc/traefik/rules/secureHeaders.yaml"; + }; + + sops.templates."traefik/rules/secureTLS" = { + gid = "100"; + uid = "100999"; + mode = "0400"; + content = '' + tls: + options: + secureTLS: + minVersion: VersionTLS12 + sniStrict: true + curvePreferences: + - CurveP521 + - CurveP384 + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + ''; + path = "/etc/traefik/rules/secureTLS.yaml"; + }; + }; + + extraOptions = { + enable.default = true; + logLevel = mkOption { + type = types.enum [ "TRACE" "DEBUG" "INFO" "WARN" "ERROR" "FATAL" ]; + default = "ERROR"; + example = "ERROR"; + description = "The level of detail Traefik should print in the logs."; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/applications/uptime-kuma.nix b/modules/server/services/applications/uptime-kuma.nix new file mode 100644 index 0000000..38aa259 --- /dev/null +++ b/modules/server/services/applications/uptime-kuma.nix @@ -0,0 +1,54 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "uptimeKuma"; + # Version tagging + uptimeKumaVersion = "2.2.0-rootless"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.uptimeKuma; +in + +helper.mkPodmanService { + inherit name; + description = "Uptime-Kuma, \"don't let your server down !\" monitoring tools"; + defaultPort = "3001"; + scheme = "http"; + middlewares = [ "secureHeaders" ]; + dirPermissions = [ "100999:100 ${optimizedDir}" ]; + + composeText = '' + services: + uptimekuma: + image: docker.io/louislam/uptime-kuma:${uptimeKumaVersion} + container_name: uptime-kuma + hostname: uptime-kuma + user: '1000:1000' + networks: + uptime-kuma: + ipv4_address: 10.89.100.253 + ports: + - "${cfg.port}:3001/tcp" + volumes: + - ${optimizedDir}:/app/data + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + uptime-kuma: + driver: bridge + ipam: + config: + - subnet: "10.89.100.0/24" + gateway: "10.89.100.254" + ''; +} \ No newline at end of file diff --git a/modules/server/services/applications/vscodium.nix b/modules/server/services/applications/vscodium.nix new file mode 100644 index 0000000..0421ac2 --- /dev/null +++ b/modules/server/services/applications/vscodium.nix @@ -0,0 +1,81 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Container config + name = "vscodium"; + # Version tagging + vscodiumVersion = "1.110.11607-ls15"; + # Storage optimization + spindown = config.numbus-server.hardware.HddSpindown; + optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize)) + then cfg.configDir + else cfg.dataDir; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.vscodium; +in + +helper.mkPodmanService { + inherit name; + description = "VScodium, an open-source version of VScode in your web browser"; + defaultPort = "8000"; + configDirEnabled = optimizedDir == cfg.configDir; + dataDirEnabled = optimizedDir == cfg.dataDir; + middlewares = [ "secureHeaders" ]; + dirPermissions = [ + "100999:100 ${optimizedDir}" + "100999:100 ${cfg.configDir}" + "100999:100 ${optimizedDir}/workspace" + "100999:100 ${cfg.configDir}/config" + ]; + + composeText = '' + services: + vscodium: + image: lscr.io/linuxserver/vscodium-web:${vscodiumVersion} + container_name: vscodium + hostname: vscodium + user: '1000:1000' + networks: + vscodium: + ipv4_address: 10.89.50.253 + ports: + - "${defaultPort}:8000" + volumes: + - ${cfg.configDir}/config:/config + - ${optimizedDir}/workspace:/workspace + environment: + - PUID=1000 + - PGID=1000 + - TZ=${time.timeZone} + - CONNECTION_TOKEN=${config.sops.placeholder."vscodium/connection_token"} + shm_size: "1gb" + cap_add: + - IPC_LOCK + cap_drop: + - NET_RAW + security_opt: + - no-new-privileges:true + restart: unless-stopped + + networks: + vscodium: + name: vscodium + driver: bridge + ipam: + config: + - subnet: "10.89.50.0/24" + gateway: "10.89.50.254" + ''; + + extraConfig = { + sops.secrets."vscodium/connection_token" = { + sopsFile = /etc/nixos/secrets/podman/vscodium.yaml; + gid = "100"; + uid = "1000"; + mode = "0400"; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/default.nix b/modules/server/services/default.nix new file mode 100644 index 0000000..ce841be --- /dev/null +++ b/modules/server/services/default.nix @@ -0,0 +1,9 @@ +{ ... }: + +{ + imports = [ + ./applications/default.nix + ./dns/default.nix + ./system/default.nix + ]; +} \ No newline at end of file diff --git a/modules/server/services/dns/adguard.nix b/modules/server/services/dns/adguard.nix new file mode 100644 index 0000000..e08369f --- /dev/null +++ b/modules/server/services/dns/adguard.nix @@ -0,0 +1,53 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Version tagging + adguardVersion = "latest"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.adguard; + # Container config + name = "adguard"; +in + +helper.mkPodmanService { + inherit name; + description = "AdGuard, feature-rich DNS service"; + defaultPort = "3000"; + scheme = "http"; + dataDirEnabled = false; + startDelay = 10; + dependencies = [ + "network.target" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + + composeText = '' + services: + adguardhome: + image: adguard/adguardhome:${adguardVersion} + container_name: adguard + hostname: adguard + network_mode: pasta + user: '1000:1000' + ports: + - "${cfg.port}:3000/tcp" + - "53:53/tcp" + - "53:53/udp" + volumes: + - ${cfg.configDir}/work:/opt/adguardhome/work + - ${cfg.configDir}/config:/opt/adguardhome/conf + cap_add: + - SYS_NICE + security_opt: + - no-new-privileges:true + restart: unless-stopped + ''; +} \ No newline at end of file diff --git a/modules/server/services/dns/default.nix b/modules/server/services/dns/default.nix new file mode 100644 index 0000000..bf6213a --- /dev/null +++ b/modules/server/services/dns/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + # To test + ./adguard.nix + # Tested + ./pi-hole.nix + ]; +} \ No newline at end of file diff --git a/modules/server/services/dns/pi-hole.nix b/modules/server/services/dns/pi-hole.nix new file mode 100644 index 0000000..b5de2f9 --- /dev/null +++ b/modules/server/services/dns/pi-hole.nix @@ -0,0 +1,71 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + # Version tagging + piholeVersion = "2026.02.0"; + # Helper + helper = import ../service-helper.nix { inherit config pkgs lib; }; + cfg = config.numbus-server.services.pi-hole; + # Container config + name = "pi-hole"; + # DNS config + dnsConfig = '' + + ''; +in + +helper.mkPodmanService { + inherit name; + description = "Pi-Hole, the ads black hole"; + defaultPort = "4443"; + scheme = "https"; + dataDirEnabled = false; + startDelay = 10; + dependencies = [ + "network.target" + ]; + middlewares = [ + "secureHeaders" + ]; + dirPermissions = [ + "100999:100 ${cfg.configDir}" + ]; + secrets = [ + "pi-hole/web_password" + ]; + +# Compose file good + composeText = '' + services: + pi-hole: + image: docker.io/pihole/pihole:${piholeVersion} + container_name: pi-hole + hostname: pi-hole + network_mode: pasta + ports: + - "${cfg.port}:443/tcp" + - "53:53/tcp" + - "53:53/udp" + volumes: + - ${cfg.configDir}:/etc/pihole + environment: + PIHOLE_UID: '1000' + PIHOLE_GID: '1000' + TZ: ${config.time.timeZone} + FTLCONF_webserver_domain: ${cfg.subdomain}.${config.numbus-server.services.domain} + FTLCONF_dns_domain_name: "${config.numbus-server.services.domain}" + FTLCONF_webserver_api_password: ${config.sops.placeholder."pi-hole/web_password"} + FTLCONF_dns_upstreams: 9.9.9.9;149.112.112.112 + FTLCONF_dns_listeningMode: "BIND" + FTLCONF_dns_domain_local: "true" + FTLCONF_dhcp_active: "false" + FTLCONF_ntp_ipv4_active: "false" + FTLCONF_ntp_ipv6_active: "false" + FTLCONF_ntp_sync_active: "false" + cap_add: + - SYS_NICE + restart: unless-stopped + ''; +} \ No newline at end of file diff --git a/modules/server/services/global.nix b/modules/server/services/global.nix new file mode 100644 index 0000000..a338b28 --- /dev/null +++ b/modules/server/services/global.nix @@ -0,0 +1,19 @@ +{ config, ... }: + +{ + options.numbus = { + services = { + domain = mkOption { + type = types.str; + example = "numbus.eu"; + description = "The root domain name (i.e. example.com) that your services will use"; + }; + dns = mkOption { + type = types.enum [ "pi-hole" "adguard" ]; + default = "pi-hole"; + example = "pi-hole"; + description = "The preferred DNS resolver service (pi-hole or adguard) that other services should depend on"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/system/backup-client.nix b/modules/server/services/system/backup-client.nix new file mode 100644 index 0000000..1ea63d5 --- /dev/null +++ b/modules/server/services/system/backup-client.nix @@ -0,0 +1,5 @@ +{ ... }: + +{ + +} \ No newline at end of file diff --git a/modules/server/services/system/clamav.nix b/modules/server/services/system/clamav.nix new file mode 100644 index 0000000..05a0924 --- /dev/null +++ b/modules/server/services/system/clamav.nix @@ -0,0 +1,91 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.numbus-server.services.clamav; + onAccessPaths = lib.mapAttrsToList (n: v: v.dataDir) (lib.filterAttrs (n: v: + v ? enable && v.enable && v ? dataDir && v.dataDir != null && v.dataDir != false + ) config.numbus-server.services); + clamonacc_virus_notifier = pkgs.writeScript "clamonacc_virus_notifier.sh" '' + #!${pkgs.bash}/bin/bash + + echo "CLAM_VIRUSEVENT_VIRUSNAME=\"$CLAM_VIRUSEVENT_VIRUSNAME\"" > /var/lib/clamav/virus_event.env + echo "CLAM_VIRUSEVENT_FILENAME=\"$CLAM_VIRUSEVENT_FILENAME\"" >> /var/lib/clamav/virus_event.env + + /run/wrappers/bin/sudo /run/current-system/sw/bin/systemctl start clamav-virus-notify.service + ''; +in + +{ + options.numbus-server.services.clamav = { + enable = mkEnableOption "ClamAV open-source anti-virus software"; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.clamav pkgs.curl ]; + + system.activationScripts.clamav-quarantine = '' + mkdir -p /quarantine + chown clamav:clamav /quarantine + chmod 440 /quarantine + ''; + + security.sudo.extraRules = [{ + users = [ "clamav" ]; + commands = [{ + command = "/run/current-system/sw/bin/systemctl start clamav-virus-notify.service"; + options = [ "NOPASSWD" ]; + }]; + }]; + + services.clamav = { + updater.enable = true; + clamonacc.enable = true; + + scanner = { + enable = true; + interval = "*-*-* 04:00:00"; # Everyday at 4am + scanDirectories = [ + "/etc" + "/home" + "/var/lib" + "/var/tmp" + "/tmp" + ]; + }; + + daemon = { + enable = true; + settings = { + OnAccessPrevention = true; + OnAccessIncludePath = onAccessPaths; + VirusEvent = "${clamonacc_virus_notifier}"; + }; + }; + }; + + systemd.services.clamav-periodic-scan = mkIf (onAccessPaths != []) { + description = "Periodic ClamAV virus scan"; + after = [ "clamav-daemon.service" "clamav-freshclam.service" ]; + requires = [ "clamav-daemon.service" ]; + wants = [ "clamav-freshclam.service" ]; + onFailure = [ "clamav-virus-notify.service" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.clamav}/bin/clamdscan --multiscan --fdpass --infected --allmatch --move=/quarantine ${lib.escapeShellArgs onAccessPaths}"; + Slice = "system-clamav.slice"; + }; + }; + + systemd.timers.clamav-periodic-scan = mkIf (onAccessPaths != []) { + description = "Timer for ClamAV periodic scan"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-1/3-01 04:00:00"; + Persistent = true; + Unit = "clamav-periodic-scan.service"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/server/services/system/default.nix b/modules/server/services/system/default.nix new file mode 100644 index 0000000..488df34 --- /dev/null +++ b/modules/server/services/system/default.nix @@ -0,0 +1,10 @@ +{ ... }: + +{ + imports = [ + # To test + ./backup-client.nix + ./clamav.nix + ./virtualization.nix + ]; +} \ No newline at end of file diff --git a/modules/server/services/system/virtualization.nix b/modules/server/services/system/virtualization.nix new file mode 100644 index 0000000..873062c --- /dev/null +++ b/modules/server/services/system/virtualization.nix @@ -0,0 +1,17 @@ +{ config, lib, ... }: + +with lib; + +let + cfg = config.numbus-server.services.virtualization; +in + +{ + options.numbus-server.services.virtualization = { + enable = mkEnableOption "QEMU/KVM virtualization software"; + }; + + config = mkIf cfg.enable { + virtualisation.libvirtd.enable = true; + }; +} \ No newline at end of file diff --git a/modules/tv/configuration.nix b/modules/tv/configuration.nix new file mode 100644 index 0000000..c4e3772 --- /dev/null +++ b/modules/tv/configuration.nix @@ -0,0 +1,144 @@ +{ config, pkgs, inputs, ... }: + +{ + imports = [ ]; + + # Bootloader options + boot.initrd.systemd.enable = true; + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + # Boot splash instead of log messages + boot = { + plymouth.enable = true; + # Enable "Silent boot" + consoleLogLevel = 3; + initrd.verbose = false; + kernelParams = [ + "quiet" + "udev.log_level=3" + "systemd.show_status=auto" + "pcie_aspm=off" + ]; + loader.timeout = 1; + }; + + # Hardware settings + hardware.enableRedistributableFirmware = true; + hardware.cpu.intel.updateMicrocode = true; + hardware.cpu.amd.updateMicrocode = true; + + # Enable networking + networking.networkmanager.enable = true; + networking.hostName = "nix-tv"; # Define your hostname. + + # Set your time zone. + time.timeZone = "Europe/Paris"; + + # Select internationalisation properties. + i18n.defaultLocale = "fr_FR.UTF-8"; + + i18n.extraLocaleSettings = { + LC_ADDRESS = "fr_FR.UTF-8"; + LC_IDENTIFICATION = "fr_FR.UTF-8"; + LC_MEASUREMENT = "fr_FR.UTF-8"; + LC_MONETARY = "fr_FR.UTF-8"; + LC_NAME = "fr_FR.UTF-8"; + LC_NUMERIC = "fr_FR.UTF-8"; + LC_PAPER = "fr_FR.UTF-8"; + LC_TELEPHONE = "fr_FR.UTF-8"; + LC_TIME = "fr_FR.UTF-8"; + }; + + # Disable the X11 windowing system. + services.xserver.enable = false; + services.xserver.xkb.layout = "fr"; + + # Enable the KDE Plasma Desktop Environment. + services.displayManager.sddm.enable = true; + services.displayManager.sddm.wayland.enable = true; + services.desktopManager.plasma6.enable = true; + services.displayManager.autoLogin.enable = true; + services.displayManager.autoLogin.user = "nix-tv"; + + # Configure console keymap + console.keyMap = "fr"; + + # Enable CUPS to print documents. + services.printing.enable = true; + + # Enable sound with pipewire. + services.pulseaudio.enable = false; + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + }; + + # Define a user account. Don't forget to set a password with ‘passwd’. + users.users.nix-tv = { + isNormalUser = true; + description = "Nix TV"; + extraGroups = [ "networkmanager" "wheel" ]; + uid = 1000; + initialPassword = "changeMe!"; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjX8YqPMXpuqIrEClMHD2Ol+gqj3+28rYXGWybVNpim raphael@MacBook-Pro-de-Raphael.local" + ]; + }; + + # Login message + environment.loginShellInit = '' + if [ "$(id -u)" -eq 1000 ]; then + if [ -n "$SSH_TTY" ]; then + fastfetch + echo -e "\n\nWelcome to your Nix TV !\n\n- This system is managed by NixOS\n- All changes are futile\n- Please consider buying support if you can't get your TV running\n- Have a nice day and enjoy !" + fi + fi + ''; + + # Allow unfree packages + nixpkgs.config.allowUnfree = true; + + environment.systemPackages = with pkgs; [ + fastfetch + ]; + + # Install flatpak + services.flatpak.enable = true; + + + # Enable the OpenSSH daemon. + services.openssh.enable = true; + + # Open ports in the firewall. + networking.firewall.allowedTCPPorts = [ ]; + networking.firewall.allowedUDPPorts = [ ]; + networking.firewall.enable = true; + + # Enable auto updates + system.autoUpgrade = { + enable = true; + allowReboot = false; + flake = inputs.self.outPath; + flags = [ "--print-build-logs" ]; + dates = "20:00"; + randomizedDelaySec = "45min"; + }; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 7d"; + }; + + # Enable NixOS flakes + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + # Enable auto nix-store optimization + nix.settings.auto-optimise-store = true; + + system.stateVersion = "25.11"; + +} \ No newline at end of file diff --git a/modules/tv/flake.nix b/modules/tv/flake.nix new file mode 100644 index 0000000..37da3da --- /dev/null +++ b/modules/tv/flake.nix @@ -0,0 +1,23 @@ +{ + description = "Flake file for Nix TV project"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + nix-flatpak.url = "github:gmodena/nix-flatpak/?ref=latest"; + }; + + outputs = { self, nixpkgs, nix-flatpak, ... }@inputs: { + nixosConfigurations."nix-tv" = nixpkgs.lib.nixosSystem { + specialArgs = { inherit inputs; }; + system = "x86_64-linux"; + modules = [ + # Nix flatpak + nix-flatpak.nixosModules.nix-flatpak + ./flatpak.nix + # Core configuration + ./configuration.nix + ./hardware-configuration.nix + ]; + }; + }; +} \ No newline at end of file diff --git a/modules/tv/flatpak.nix b/modules/tv/flatpak.nix new file mode 100644 index 0000000..8370322 --- /dev/null +++ b/modules/tv/flatpak.nix @@ -0,0 +1,18 @@ +{ lib, ... }: + +{ + services.flatpak.remotes = lib.mkOptionDefault [{ + name = "flathub"; + location = "https://dl.flathub.org/repo/flathub.flatpakrepo"; + }]; + + services.flatpak.update.auto.enable = true; + services.flatpak.uninstallUnmanaged = true; + + services.flatpak.packages = [ + "io.gitlab.librewolf-community" + "app.grayjay.Grayjay" + "com.github.unrud.VideoDownloader" + "com.github.tchx84.Flatseal" + ]; +} \ No newline at end of file diff --git a/templates/numbus-backup-server/configuration.nix b/templates/backup/configuration.nix similarity index 100% rename from templates/numbus-backup-server/configuration.nix rename to templates/backup/configuration.nix diff --git a/templates/numbus-backup-server/flake.nix b/templates/backup/flake.nix similarity index 100% rename from templates/numbus-backup-server/flake.nix rename to templates/backup/flake.nix diff --git a/templates/numbus-backup-server/sops-nix/.sops.yaml b/templates/backup/sops-nix/.sops.yaml similarity index 100% rename from templates/numbus-backup-server/sops-nix/.sops.yaml rename to templates/backup/sops-nix/.sops.yaml diff --git a/templates/numbus-backup-server/sops-nix/secrets.yaml b/templates/backup/sops-nix/secrets.yaml similarity index 100% rename from templates/numbus-backup-server/sops-nix/secrets.yaml rename to templates/backup/sops-nix/secrets.yaml diff --git a/templates/numbus-computer/configuration.nix b/templates/computer/configuration.nix similarity index 100% rename from templates/numbus-computer/configuration.nix rename to templates/computer/configuration.nix diff --git a/templates/numbus-computer/flake.nix b/templates/computer/flake.nix similarity index 100% rename from templates/numbus-computer/flake.nix rename to templates/computer/flake.nix diff --git a/templates/numbus-computer/sops-nix/.sops.yaml b/templates/computer/sops-nix/.sops.yaml similarity index 100% rename from templates/numbus-computer/sops-nix/.sops.yaml rename to templates/computer/sops-nix/.sops.yaml diff --git a/templates/numbus-computer/sops-nix/secrets.yaml b/templates/computer/sops-nix/secrets.yaml similarity index 100% rename from templates/numbus-computer/sops-nix/secrets.yaml rename to templates/computer/sops-nix/secrets.yaml diff --git a/templates/numbus-server/.sops.yaml b/templates/server/.sops.yaml similarity index 100% rename from templates/numbus-server/.sops.yaml rename to templates/server/.sops.yaml diff --git a/templates/numbus-server/custom-configuration.nix b/templates/server/custom-configuration.nix similarity index 100% rename from templates/numbus-server/custom-configuration.nix rename to templates/server/custom-configuration.nix diff --git a/templates/numbus-server/flake.nix b/templates/server/flake.nix similarity index 100% rename from templates/numbus-server/flake.nix rename to templates/server/flake.nix diff --git a/templates/numbus-server/numbus-generated.nix b/templates/server/numbus-generated.nix similarity index 100% rename from templates/numbus-server/numbus-generated.nix rename to templates/server/numbus-generated.nix diff --git a/templates/numbus-server/secrets/disks/boot.yaml b/templates/server/secrets/disks/boot.yaml similarity index 100% rename from templates/numbus-server/secrets/disks/boot.yaml rename to templates/server/secrets/disks/boot.yaml diff --git a/templates/numbus-server/secrets/disks/content.yaml b/templates/server/secrets/disks/content.yaml similarity index 100% rename from templates/numbus-server/secrets/disks/content.yaml rename to templates/server/secrets/disks/content.yaml diff --git a/templates/numbus-server/secrets/disks/parity.yaml b/templates/server/secrets/disks/parity.yaml similarity index 100% rename from templates/numbus-server/secrets/disks/parity.yaml rename to templates/server/secrets/disks/parity.yaml diff --git a/templates/numbus-server/secrets/podman/adguard.yaml b/templates/server/secrets/podman/adguard.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/adguard.yaml rename to templates/server/secrets/podman/adguard.yaml diff --git a/templates/numbus-server/secrets/podman/authelia.yaml b/templates/server/secrets/podman/authelia.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/authelia.yaml rename to templates/server/secrets/podman/authelia.yaml diff --git a/templates/numbus-server/secrets/podman/crafty.yaml b/templates/server/secrets/podman/crafty.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/crafty.yaml rename to templates/server/secrets/podman/crafty.yaml diff --git a/templates/numbus-server/secrets/podman/dashy.yaml b/templates/server/secrets/podman/dashy.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/dashy.yaml rename to templates/server/secrets/podman/dashy.yaml diff --git a/templates/numbus-server/secrets/podman/frigate.yaml b/templates/server/secrets/podman/frigate.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/frigate.yaml rename to templates/server/secrets/podman/frigate.yaml diff --git a/templates/numbus-server/secrets/podman/gitea.yaml b/templates/server/secrets/podman/gitea.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/gitea.yaml rename to templates/server/secrets/podman/gitea.yaml diff --git a/templates/numbus-server/secrets/podman/gitlab.yaml b/templates/server/secrets/podman/gitlab.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/gitlab.yaml rename to templates/server/secrets/podman/gitlab.yaml diff --git a/templates/numbus-server/secrets/podman/home-assistant.yaml b/templates/server/secrets/podman/home-assistant.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/home-assistant.yaml rename to templates/server/secrets/podman/home-assistant.yaml diff --git a/templates/numbus-server/secrets/podman/homepage.yaml b/templates/server/secrets/podman/homepage.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/homepage.yaml rename to templates/server/secrets/podman/homepage.yaml diff --git a/templates/numbus-server/secrets/podman/immich.yaml b/templates/server/secrets/podman/immich.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/immich.yaml rename to templates/server/secrets/podman/immich.yaml diff --git a/templates/numbus-server/secrets/podman/it-tools.yaml b/templates/server/secrets/podman/it-tools.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/it-tools.yaml rename to templates/server/secrets/podman/it-tools.yaml diff --git a/templates/numbus-server/secrets/podman/jellyfin.yaml b/templates/server/secrets/podman/jellyfin.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/jellyfin.yaml rename to templates/server/secrets/podman/jellyfin.yaml diff --git a/templates/numbus-server/secrets/podman/lldap.yaml b/templates/server/secrets/podman/lldap.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/lldap.yaml rename to templates/server/secrets/podman/lldap.yaml diff --git a/templates/numbus-server/secrets/podman/n8n.yaml b/templates/server/secrets/podman/n8n.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/n8n.yaml rename to templates/server/secrets/podman/n8n.yaml diff --git a/templates/numbus-server/secrets/podman/netbird.yaml b/templates/server/secrets/podman/netbird.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/netbird.yaml rename to templates/server/secrets/podman/netbird.yaml diff --git a/templates/numbus-server/secrets/podman/netbootxyz.yaml b/templates/server/secrets/podman/netbootxyz.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/netbootxyz.yaml rename to templates/server/secrets/podman/netbootxyz.yaml diff --git a/templates/numbus-server/secrets/podman/netbox.yaml b/templates/server/secrets/podman/netbox.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/netbox.yaml rename to templates/server/secrets/podman/netbox.yaml diff --git a/templates/numbus-server/secrets/podman/nextcloud.yaml b/templates/server/secrets/podman/nextcloud.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/nextcloud.yaml rename to templates/server/secrets/podman/nextcloud.yaml diff --git a/templates/numbus-server/secrets/podman/ntfy.yaml b/templates/server/secrets/podman/ntfy.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/ntfy.yaml rename to templates/server/secrets/podman/ntfy.yaml diff --git a/templates/numbus-server/secrets/podman/odoo.yaml b/templates/server/secrets/podman/odoo.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/odoo.yaml rename to templates/server/secrets/podman/odoo.yaml diff --git a/templates/numbus-server/secrets/podman/passbolt.yaml b/templates/server/secrets/podman/passbolt.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/passbolt.yaml rename to templates/server/secrets/podman/passbolt.yaml diff --git a/templates/numbus-server/secrets/podman/pi-hole.yaml b/templates/server/secrets/podman/pi-hole.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/pi-hole.yaml rename to templates/server/secrets/podman/pi-hole.yaml diff --git a/templates/numbus-server/secrets/podman/traefik.yaml b/templates/server/secrets/podman/traefik.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/traefik.yaml rename to templates/server/secrets/podman/traefik.yaml diff --git a/templates/numbus-server/secrets/podman/uptime-kuma.yaml b/templates/server/secrets/podman/uptime-kuma.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/uptime-kuma.yaml rename to templates/server/secrets/podman/uptime-kuma.yaml diff --git a/templates/numbus-server/secrets/podman/vscodium.yaml b/templates/server/secrets/podman/vscodium.yaml similarity index 100% rename from templates/numbus-server/secrets/podman/vscodium.yaml rename to templates/server/secrets/podman/vscodium.yaml diff --git a/templates/numbus-server/secrets/system/backup-client.yaml b/templates/server/secrets/system/backup-client.yaml similarity index 100% rename from templates/numbus-server/secrets/system/backup-client.yaml rename to templates/server/secrets/system/backup-client.yaml diff --git a/templates/numbus-server/secrets/system/clamav.yaml b/templates/server/secrets/system/clamav.yaml similarity index 100% rename from templates/numbus-server/secrets/system/clamav.yaml rename to templates/server/secrets/system/clamav.yaml diff --git a/templates/numbus-server/secrets/system/mail.yaml b/templates/server/secrets/system/mail.yaml similarity index 100% rename from templates/numbus-server/secrets/system/mail.yaml rename to templates/server/secrets/system/mail.yaml diff --git a/templates/numbus-server/secrets/system/ssh.yaml b/templates/server/secrets/system/ssh.yaml similarity index 100% rename from templates/numbus-server/secrets/system/ssh.yaml rename to templates/server/secrets/system/ssh.yaml diff --git a/templates/numbus-server/secrets/system/virtualization.yaml b/templates/server/secrets/system/virtualization.yaml similarity index 100% rename from templates/numbus-server/secrets/system/virtualization.yaml rename to templates/server/secrets/system/virtualization.yaml diff --git a/templates/numbus-tv/configuration.nix b/templates/tv/configuration.nix similarity index 100% rename from templates/numbus-tv/configuration.nix rename to templates/tv/configuration.nix diff --git a/templates/numbus-tv/flake.nix b/templates/tv/flake.nix similarity index 100% rename from templates/numbus-tv/flake.nix rename to templates/tv/flake.nix diff --git a/templates/numbus-tv/sops-nix/.sops.yaml b/templates/tv/sops-nix/.sops.yaml similarity index 100% rename from templates/numbus-tv/sops-nix/.sops.yaml rename to templates/tv/sops-nix/.sops.yaml diff --git a/templates/numbus-tv/sops-nix/secrets.yaml b/templates/tv/sops-nix/secrets.yaml similarity index 100% rename from templates/numbus-tv/sops-nix/secrets.yaml rename to templates/tv/sops-nix/secrets.yaml