forked from jan-leila/nix-config
refactor: split server modules into smaller more manageable files
This commit is contained in:
parent
b2e5ae1f98
commit
cdeb4e108b
49 changed files with 1519 additions and 1270 deletions
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
dataDirectory = "/var/lib/actual/";
|
|
||||||
in {
|
|
||||||
options.services.actual = {
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "actual";
|
|
||||||
description = "subdomain of base domain that actual will be hosted at";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.actual.enable (lib.mkMerge [
|
|
||||||
{
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d ${dataDirectory} 2770 actual actual"
|
|
||||||
];
|
|
||||||
|
|
||||||
services.actual = {
|
|
||||||
settings = {
|
|
||||||
ACTUAL_DATA_DIR = dataDirectory;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host = {
|
|
||||||
reverse_proxy.subdomains.${config.services.actual.subdomain} = {
|
|
||||||
target = "http://localhost:${toString config.services.actual.settings.port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.services.fail2ban.enable {
|
|
||||||
# TODO: configuration for fail2ban for actual
|
|
||||||
})
|
|
||||||
(lib.mkIf config.host.impermanence.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.services.actual.settings.ACTUAL_DATA_DIR == dataDirectory;
|
|
||||||
message = "actual data location does not match persistence";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
environment.persistence."/persist/system/root" = {
|
|
||||||
directories = [
|
|
||||||
{
|
|
||||||
directory = dataDirectory;
|
|
||||||
user = "actual";
|
|
||||||
group = "actual";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
3
modules/nixos-modules/server/actual/const.nix
Normal file
3
modules/nixos-modules/server/actual/const.nix
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
dataDirectory = "/var/lib/actual/";
|
||||||
|
}
|
||||||
34
modules/nixos-modules/server/actual/default.nix
Normal file
34
modules/nixos-modules/server/actual/default.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
const = import ./const.nix;
|
||||||
|
dataDirectory = const.dataDirectory;
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
./fail2ban.nix
|
||||||
|
./impermanence.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.actual = {
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "actual";
|
||||||
|
description = "subdomain of base domain that actual will be hosted at";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.actual.enable {
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${dataDirectory} 2770 actual actual"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.actual = {
|
||||||
|
settings = {
|
||||||
|
ACTUAL_DATA_DIR = dataDirectory;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
9
modules/nixos-modules/server/actual/fail2ban.nix
Normal file
9
modules/nixos-modules/server/actual/fail2ban.nix
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.actual.enable && config.services.fail2ban.enable) {
|
||||||
|
# TODO: configuration for fail2ban for actual
|
||||||
|
};
|
||||||
|
}
|
||||||
26
modules/nixos-modules/server/actual/impermanence.nix
Normal file
26
modules/nixos-modules/server/actual/impermanence.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
const = import ./const.nix;
|
||||||
|
dataDirectory = const.dataDirectory;
|
||||||
|
in {
|
||||||
|
config = lib.mkIf (config.services.actual.enable && config.host.impermanence.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.actual.settings.ACTUAL_DATA_DIR == dataDirectory;
|
||||||
|
message = "actual data location does not match persistence";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
environment.persistence."/persist/system/root" = {
|
||||||
|
directories = [
|
||||||
|
{
|
||||||
|
directory = dataDirectory;
|
||||||
|
user = "actual";
|
||||||
|
group = "actual";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
13
modules/nixos-modules/server/actual/proxy.nix
Normal file
13
modules/nixos-modules/server/actual/proxy.nix
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.actual.enable && config.host.reverse_proxy.enable) {
|
||||||
|
host = {
|
||||||
|
reverse_proxy.subdomains.${config.services.actual.subdomain} = {
|
||||||
|
target = "http://localhost:${toString config.services.actual.settings.port}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
{...}: {
|
{...}: {
|
||||||
imports = [
|
imports = [
|
||||||
./fail2ban.nix
|
|
||||||
./network_storage
|
|
||||||
./reverse_proxy.nix
|
./reverse_proxy.nix
|
||||||
|
./fail2ban.nix
|
||||||
./postgres.nix
|
./postgres.nix
|
||||||
|
./network_storage
|
||||||
./podman.nix
|
./podman.nix
|
||||||
./jellyfin.nix
|
|
||||||
./forgejo.nix
|
./actual
|
||||||
./searx.nix
|
./immich
|
||||||
./home-assistant.nix
|
./panoramax
|
||||||
./wyoming.nix
|
./forgejo
|
||||||
./immich.nix
|
./home-assistant
|
||||||
|
./jellyfin
|
||||||
|
./paperless
|
||||||
|
./searx
|
||||||
./qbittorent.nix
|
./qbittorent.nix
|
||||||
./paperless.nix
|
./wyoming.nix
|
||||||
./actual.nix
|
|
||||||
./panoramax.nix
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
forgejoPort = 8081;
|
|
||||||
stateDir = "/var/lib/forgejo";
|
|
||||||
db_user = "forgejo";
|
|
||||||
sshPort = 22222;
|
|
||||||
in {
|
|
||||||
options.services.forgejo = {
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "subdomain of base domain that forgejo will be hosted at";
|
|
||||||
default = "forgejo";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.forgejo.enable (lib.mkMerge [
|
|
||||||
{
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.services.forgejo.settings.server.BUILTIN_SSH_SERVER_USER == config.users.users.git.name;
|
|
||||||
message = "Forgejo BUILTIN_SSH_SERVER_USER hardcoded value does not match expected git user name";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
host = {
|
|
||||||
postgres = {
|
|
||||||
enable = true;
|
|
||||||
extraUsers = {
|
|
||||||
${db_user} = {
|
|
||||||
isClient = true;
|
|
||||||
createUser = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
extraDatabases = {
|
|
||||||
${db_user} = {
|
|
||||||
name = db_user;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.forgejo = {
|
|
||||||
database = {
|
|
||||||
type = "postgres";
|
|
||||||
socket = "/run/postgresql";
|
|
||||||
};
|
|
||||||
lfs.enable = true;
|
|
||||||
settings = {
|
|
||||||
server = {
|
|
||||||
DOMAIN = "${config.services.forgejo.subdomain}.${config.host.reverse_proxy.hostname}";
|
|
||||||
HTTP_PORT = forgejoPort;
|
|
||||||
START_SSH_SERVER = true;
|
|
||||||
SSH_LISTEN_PORT = sshPort;
|
|
||||||
SSH_PORT = 22;
|
|
||||||
BUILTIN_SSH_SERVER_USER = "git";
|
|
||||||
ROOT_URL = "https://git.jan-leila.com";
|
|
||||||
};
|
|
||||||
service = {
|
|
||||||
DISABLE_REGISTRATION = true;
|
|
||||||
};
|
|
||||||
database = {
|
|
||||||
DB_TYPE = "postgres";
|
|
||||||
NAME = db_user;
|
|
||||||
USER = db_user;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [
|
|
||||||
config.services.forgejo.settings.server.SSH_LISTEN_PORT
|
|
||||||
];
|
|
||||||
}
|
|
||||||
(lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host = {
|
|
||||||
reverse_proxy.subdomains.${config.services.forgejo.subdomain} = {
|
|
||||||
target = "http://localhost:${toString forgejoPort}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.services.fail2ban.enable {
|
|
||||||
environment.etc = {
|
|
||||||
"fail2ban/filter.d/forgejo.local".text = lib.mkIf config.services.forgejo.enable (
|
|
||||||
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
|
||||||
[Definition]
|
|
||||||
failregex = ".*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>"
|
|
||||||
'')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
services.fail2ban = {
|
|
||||||
jails = {
|
|
||||||
forgejo-iptables.settings = lib.mkIf config.services.forgejo.enable {
|
|
||||||
enabled = true;
|
|
||||||
filter = "forgejo";
|
|
||||||
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
|
||||||
logpath = "${config.services.forgejo.settings.log.ROOT_PATH}/*.log";
|
|
||||||
backend = "auto";
|
|
||||||
findtime = 600;
|
|
||||||
bantime = 600;
|
|
||||||
maxretry = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.host.impermanence.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.services.forgejo.stateDir == stateDir;
|
|
||||||
message = "forgejo state directory does not match persistence";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
environment.persistence."/persist/system/root" = {
|
|
||||||
enable = true;
|
|
||||||
hideMounts = true;
|
|
||||||
directories = [
|
|
||||||
{
|
|
||||||
directory = stateDir;
|
|
||||||
user = "forgejo";
|
|
||||||
group = "forgejo";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
4
modules/nixos-modules/server/forgejo/const.nix
Normal file
4
modules/nixos-modules/server/forgejo/const.nix
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
httpPort = 8081;
|
||||||
|
sshPort = 22222;
|
||||||
|
}
|
||||||
41
modules/nixos-modules/server/forgejo/database.nix
Normal file
41
modules/nixos-modules/server/forgejo/database.nix
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf config.services.forgejo.enable (
|
||||||
|
lib.mkMerge [
|
||||||
|
{
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.forgejo.settings.database.DB_TYPE == "postgres";
|
||||||
|
message = "Forgejo database type must be postgres";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
(lib.mkIf config.host.postgres.enable {
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
extraUsers = {
|
||||||
|
forgejo = {
|
||||||
|
isClient = true;
|
||||||
|
createUser = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraDatabases = {
|
||||||
|
forgejo = {
|
||||||
|
name = "forgejo";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
61
modules/nixos-modules/server/forgejo/default.nix
Normal file
61
modules/nixos-modules/server/forgejo/default.nix
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
const = import ./const.nix;
|
||||||
|
httpPort = const.httpPort;
|
||||||
|
sshPort = const.sshPort;
|
||||||
|
db_user = "forgejo";
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
./database.nix
|
||||||
|
./fail2ban.nix
|
||||||
|
./impermanence.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.forgejo = {
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "subdomain of base domain that forgejo will be hosted at";
|
||||||
|
default = "forgejo";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.forgejo.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.forgejo.settings.server.BUILTIN_SSH_SERVER_USER == config.users.users.git.name;
|
||||||
|
message = "Forgejo BUILTIN_SSH_SERVER_USER hardcoded value does not match expected git user name";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.forgejo = {
|
||||||
|
database = {
|
||||||
|
type = "postgres";
|
||||||
|
socket = "/run/postgresql";
|
||||||
|
};
|
||||||
|
lfs.enable = true;
|
||||||
|
settings = {
|
||||||
|
server = {
|
||||||
|
DOMAIN = "${config.services.forgejo.subdomain}.${config.host.reverse_proxy.hostname}";
|
||||||
|
HTTP_PORT = httpPort;
|
||||||
|
START_SSH_SERVER = true;
|
||||||
|
SSH_LISTEN_PORT = sshPort;
|
||||||
|
SSH_PORT = 22;
|
||||||
|
BUILTIN_SSH_SERVER_USER = "git";
|
||||||
|
ROOT_URL = "https://git.jan-leila.com";
|
||||||
|
};
|
||||||
|
service = {
|
||||||
|
DISABLE_REGISTRATION = true;
|
||||||
|
};
|
||||||
|
database = {
|
||||||
|
DB_TYPE = "postgres";
|
||||||
|
NAME = db_user;
|
||||||
|
USER = db_user;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
32
modules/nixos-modules/server/forgejo/fail2ban.nix
Normal file
32
modules/nixos-modules/server/forgejo/fail2ban.nix
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.forgejo.enable && config.services.fail2ban.enable) {
|
||||||
|
environment.etc = {
|
||||||
|
"fail2ban/filter.d/forgejo.local".text = lib.mkIf config.services.forgejo.enable (
|
||||||
|
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
||||||
|
[Definition]
|
||||||
|
failregex = ".*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>"
|
||||||
|
'')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
services.fail2ban = {
|
||||||
|
jails = {
|
||||||
|
forgejo-iptables.settings = lib.mkIf config.services.forgejo.enable {
|
||||||
|
enabled = true;
|
||||||
|
filter = "forgejo";
|
||||||
|
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
||||||
|
logpath = "${config.services.forgejo.settings.log.ROOT_PATH}/*.log";
|
||||||
|
backend = "auto";
|
||||||
|
findtime = 600;
|
||||||
|
bantime = 600;
|
||||||
|
maxretry = 5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
28
modules/nixos-modules/server/forgejo/impermanence.nix
Normal file
28
modules/nixos-modules/server/forgejo/impermanence.nix
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
stateDir = "/var/lib/forgejo";
|
||||||
|
in {
|
||||||
|
config = lib.mkIf (config.services.forgejo.enable && config.host.impermanence.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.forgejo.stateDir == stateDir;
|
||||||
|
message = "forgejo state directory does not match persistence";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.persistence."/persist/system/root" = {
|
||||||
|
enable = true;
|
||||||
|
hideMounts = true;
|
||||||
|
directories = [
|
||||||
|
{
|
||||||
|
directory = stateDir;
|
||||||
|
user = "forgejo";
|
||||||
|
group = "forgejo";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
18
modules/nixos-modules/server/forgejo/proxy.nix
Normal file
18
modules/nixos-modules/server/forgejo/proxy.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
const = import ./const.nix;
|
||||||
|
httpPort = const.httpPort;
|
||||||
|
in {
|
||||||
|
config = lib.mkIf (config.services.forgejo.enable && config.host.reverse_proxy.enable) {
|
||||||
|
host.reverse_proxy.subdomains.${config.services.forgejo.subdomain} = {
|
||||||
|
target = "http://localhost:${toString httpPort}";
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
config.services.forgejo.settings.server.SSH_LISTEN_PORT
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
configDir = "/var/lib/hass";
|
|
||||||
dbUser = "hass";
|
|
||||||
in {
|
|
||||||
options.services.home-assistant = {
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "subdomain of base domain that home-assistant will be hosted at";
|
|
||||||
default = "home-assistant";
|
|
||||||
};
|
|
||||||
|
|
||||||
database = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"builtin"
|
|
||||||
"postgres"
|
|
||||||
];
|
|
||||||
description = "what database do we want to use";
|
|
||||||
default = "builtin";
|
|
||||||
};
|
|
||||||
|
|
||||||
extensions = {
|
|
||||||
sonos = {
|
|
||||||
enable = lib.mkEnableOption "enable the sonos plugin";
|
|
||||||
port = lib.mkOption {
|
|
||||||
type = lib.types.int;
|
|
||||||
default = 1400;
|
|
||||||
description = "what port to use for sonos discovery";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
jellyfin = {
|
|
||||||
enable = lib.mkEnableOption "enable the jellyfin plugin";
|
|
||||||
};
|
|
||||||
wyoming = {
|
|
||||||
enable = lib.mkEnableOption "enable wyoming";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.home-assistant.enable (lib.mkMerge [
|
|
||||||
{
|
|
||||||
services.home-assistant = {
|
|
||||||
configDir = configDir;
|
|
||||||
extraComponents = [
|
|
||||||
"default_config"
|
|
||||||
"esphome"
|
|
||||||
"met"
|
|
||||||
"radio_browser"
|
|
||||||
"isal"
|
|
||||||
"zha"
|
|
||||||
"webostv"
|
|
||||||
"tailscale"
|
|
||||||
"syncthing"
|
|
||||||
"analytics_insights"
|
|
||||||
"unifi"
|
|
||||||
"openweathermap"
|
|
||||||
"ollama"
|
|
||||||
"mobile_app"
|
|
||||||
"logbook"
|
|
||||||
"ssdp"
|
|
||||||
"usb"
|
|
||||||
"webhook"
|
|
||||||
"bluetooth"
|
|
||||||
"dhcp"
|
|
||||||
"energy"
|
|
||||||
"history"
|
|
||||||
"backup"
|
|
||||||
"assist_pipeline"
|
|
||||||
"conversation"
|
|
||||||
"sun"
|
|
||||||
"zeroconf"
|
|
||||||
"cpuspeed"
|
|
||||||
];
|
|
||||||
config = {
|
|
||||||
http = {
|
|
||||||
server_port = 8123;
|
|
||||||
use_x_forwarded_for = true;
|
|
||||||
trusted_proxies = ["127.0.0.1" "::1"];
|
|
||||||
ip_ban_enabled = true;
|
|
||||||
login_attempts_threshold = 10;
|
|
||||||
};
|
|
||||||
homeassistant = {
|
|
||||||
external_url = "https://${config.services.home-assistant.subdomain}.${config.host.reverse_proxy.hostname}";
|
|
||||||
# internal_url = "http://192.168.1.2:8123";
|
|
||||||
};
|
|
||||||
recorder.db_url = "postgresql://@/${dbUser}";
|
|
||||||
"automation manual" = [];
|
|
||||||
"automation ui" = "!include automations.yaml";
|
|
||||||
mobile_app = {};
|
|
||||||
};
|
|
||||||
extraPackages = python3Packages:
|
|
||||||
with python3Packages; [
|
|
||||||
hassil
|
|
||||||
numpy
|
|
||||||
gtts
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# TODO: configure /var/lib/hass/secrets.yaml via sops
|
|
||||||
|
|
||||||
networking.firewall.allowedUDPPorts = [
|
|
||||||
1900
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
(lib.mkIf (config.services.home-assistant.extensions.sonos.enable) {
|
|
||||||
services.home-assistant.extraComponents = ["sonos"];
|
|
||||||
networking.firewall.allowedTCPPorts = [
|
|
||||||
config.services.home-assistant.extensions.sonos.port
|
|
||||||
];
|
|
||||||
})
|
|
||||||
(lib.mkIf (config.services.home-assistant.extensions.jellyfin.enable) {
|
|
||||||
services.home-assistant.extraComponents = ["jellyfin"];
|
|
||||||
# TODO: configure port, address, and login information here
|
|
||||||
})
|
|
||||||
(lib.mkIf (config.services.home-assistant.extensions.wyoming.enable) {
|
|
||||||
services.home-assistant.extraComponents = ["wyoming"];
|
|
||||||
services.wyoming.enable = true;
|
|
||||||
})
|
|
||||||
(lib.mkIf (config.services.home-assistant.database == "postgres") {
|
|
||||||
host = {
|
|
||||||
postgres = {
|
|
||||||
enable = true;
|
|
||||||
extraUsers = {
|
|
||||||
${dbUser} = {
|
|
||||||
isClient = true;
|
|
||||||
createUser = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
extraDatabases = {
|
|
||||||
${dbUser} = {
|
|
||||||
name = dbUser;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.home-assistant = {
|
|
||||||
extraPackages = python3Packages:
|
|
||||||
with python3Packages; [
|
|
||||||
psycopg2
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.home-assistant = {
|
|
||||||
requires = [
|
|
||||||
config.systemd.services.postgresql.name
|
|
||||||
];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host = {
|
|
||||||
reverse_proxy.subdomains.${config.services.home-assistant.subdomain} = {
|
|
||||||
target = "http://localhost:${toString config.services.home-assistant.config.http.server_port}";
|
|
||||||
|
|
||||||
websockets.enable = true;
|
|
||||||
forwardHeaders.enable = true;
|
|
||||||
|
|
||||||
extraConfig = ''
|
|
||||||
add_header Upgrade $http_upgrade;
|
|
||||||
add_header Connection \"upgrade\";
|
|
||||||
|
|
||||||
proxy_buffering off;
|
|
||||||
|
|
||||||
proxy_read_timeout 90;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.services.fail2ban.enable {
|
|
||||||
environment.etc = {
|
|
||||||
"fail2ban/filter.d/hass.local".text = lib.mkIf config.services.home-assistant.enable (
|
|
||||||
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
|
||||||
[INCLUDES]
|
|
||||||
before = common.conf
|
|
||||||
|
|
||||||
[Definition]
|
|
||||||
failregex = ^%(__prefix_line)s.*Login attempt or request with invalid authentication from <HOST>.*$
|
|
||||||
|
|
||||||
ignoreregex =
|
|
||||||
|
|
||||||
[Init]
|
|
||||||
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S
|
|
||||||
'')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
services.fail2ban = {
|
|
||||||
jails = {
|
|
||||||
home-assistant-iptables.settings = lib.mkIf config.services.home-assistant.enable {
|
|
||||||
enabled = true;
|
|
||||||
filter = "hass";
|
|
||||||
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
|
||||||
logpath = "${config.services.home-assistant.configDir}/*.log";
|
|
||||||
backend = "auto";
|
|
||||||
findtime = 600;
|
|
||||||
bantime = 600;
|
|
||||||
maxretry = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.host.impermanence.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.services.home-assistant.configDir == configDir;
|
|
||||||
message = "home assistant config directory does not match persistence";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
environment.persistence."/persist/system/root" = {
|
|
||||||
enable = true;
|
|
||||||
hideMounts = true;
|
|
||||||
directories = [
|
|
||||||
{
|
|
||||||
directory = configDir;
|
|
||||||
user = "hass";
|
|
||||||
group = "hass";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
56
modules/nixos-modules/server/home-assistant/database.nix
Normal file
56
modules/nixos-modules/server/home-assistant/database.nix
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
dbUser = "hass";
|
||||||
|
in {
|
||||||
|
config = lib.mkIf config.services.home-assistant.enable (
|
||||||
|
lib.mkMerge [
|
||||||
|
{
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.home-assistant.database == "postgres";
|
||||||
|
message = "Home Assistant database type must be postgres";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
(lib.mkIf config.host.postgres.enable {
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
extraUsers = {
|
||||||
|
${dbUser} = {
|
||||||
|
isClient = true;
|
||||||
|
createUser = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraDatabases = {
|
||||||
|
${dbUser} = {
|
||||||
|
name = dbUser;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.home-assistant = {
|
||||||
|
extraPackages = python3Packages:
|
||||||
|
with python3Packages; [
|
||||||
|
psycopg2
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.home-assistant = {
|
||||||
|
requires = [
|
||||||
|
config.systemd.services.postgresql.name
|
||||||
|
];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
118
modules/nixos-modules/server/home-assistant/default.nix
Normal file
118
modules/nixos-modules/server/home-assistant/default.nix
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
./database.nix
|
||||||
|
./fail2ban.nix
|
||||||
|
./impermanence.nix
|
||||||
|
./extensions
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.home-assistant = {
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "subdomain of base domain that home-assistant will be hosted at";
|
||||||
|
default = "home-assistant";
|
||||||
|
};
|
||||||
|
|
||||||
|
database = lib.mkOption {
|
||||||
|
type = lib.types.enum [
|
||||||
|
"builtin"
|
||||||
|
"postgres"
|
||||||
|
];
|
||||||
|
description = "what database do we want to use";
|
||||||
|
default = "builtin";
|
||||||
|
};
|
||||||
|
|
||||||
|
extensions = {
|
||||||
|
sonos = {
|
||||||
|
enable = lib.mkEnableOption "enable the sonos plugin";
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 1400;
|
||||||
|
description = "what port to use for sonos discovery";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
jellyfin = {
|
||||||
|
enable = lib.mkEnableOption "enable the jellyfin plugin";
|
||||||
|
};
|
||||||
|
wyoming = {
|
||||||
|
enable = lib.mkEnableOption "enable wyoming";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.home-assistant.enable (lib.mkMerge [
|
||||||
|
{
|
||||||
|
services.home-assistant = {
|
||||||
|
configDir = "/var/lib/hass";
|
||||||
|
extraComponents = [
|
||||||
|
"default_config"
|
||||||
|
"esphome"
|
||||||
|
"met"
|
||||||
|
"radio_browser"
|
||||||
|
"isal"
|
||||||
|
"zha"
|
||||||
|
"webostv"
|
||||||
|
"tailscale"
|
||||||
|
"syncthing"
|
||||||
|
"analytics_insights"
|
||||||
|
"unifi"
|
||||||
|
"openweathermap"
|
||||||
|
"ollama"
|
||||||
|
"mobile_app"
|
||||||
|
"logbook"
|
||||||
|
"ssdp"
|
||||||
|
"usb"
|
||||||
|
"webhook"
|
||||||
|
"bluetooth"
|
||||||
|
"dhcp"
|
||||||
|
"energy"
|
||||||
|
"history"
|
||||||
|
"backup"
|
||||||
|
"assist_pipeline"
|
||||||
|
"conversation"
|
||||||
|
"sun"
|
||||||
|
"zeroconf"
|
||||||
|
"cpuspeed"
|
||||||
|
];
|
||||||
|
config = {
|
||||||
|
http = {
|
||||||
|
server_port = 8123;
|
||||||
|
use_x_forwarded_for = true;
|
||||||
|
trusted_proxies = ["127.0.0.1" "::1"];
|
||||||
|
ip_ban_enabled = true;
|
||||||
|
login_attempts_threshold = 10;
|
||||||
|
};
|
||||||
|
homeassistant = {
|
||||||
|
external_url = "https://${config.services.home-assistant.subdomain}.${config.host.reverse_proxy.hostname}";
|
||||||
|
# internal_url = "http://192.168.1.2:8123";
|
||||||
|
};
|
||||||
|
recorder.db_url = "postgresql://@/${config.services.home-assistant.configDir}";
|
||||||
|
"automation manual" = [];
|
||||||
|
"automation ui" = "!include automations.yaml";
|
||||||
|
mobile_app = {};
|
||||||
|
};
|
||||||
|
extraPackages = python3Packages:
|
||||||
|
with python3Packages; [
|
||||||
|
hassil
|
||||||
|
numpy
|
||||||
|
gtts
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# TODO: configure /var/lib/hass/secrets.yaml via sops
|
||||||
|
|
||||||
|
networking.firewall.allowedUDPPorts = [
|
||||||
|
1900
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
imports = [
|
||||||
|
./sonos.nix
|
||||||
|
./jellyfin.nix
|
||||||
|
./wyoming.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
lib.mkIf (config.services.home-assistant.extensions.jellyfin.enable) {
|
||||||
|
services.home-assistant.extraComponents = ["jellyfin"];
|
||||||
|
# TODO: configure port, address, and login information here
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
lib.mkIf (config.services.home-assistant.extensions.sonos.enable) {
|
||||||
|
services.home-assistant.extraComponents = ["sonos"];
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
config.services.home-assistant.extensions.sonos.port
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
lib.mkIf (config.services.home-assistant.extensions.wyoming.enable) {
|
||||||
|
services.home-assistant.extraComponents = ["wyoming"];
|
||||||
|
services.wyoming.enable = true;
|
||||||
|
}
|
||||||
39
modules/nixos-modules/server/home-assistant/fail2ban.nix
Normal file
39
modules/nixos-modules/server/home-assistant/fail2ban.nix
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
lib.mkIf (config.services.fail2ban.enable && config.services.home-assistant.enable) {
|
||||||
|
environment.etc = {
|
||||||
|
"fail2ban/filter.d/hass.local".text = (
|
||||||
|
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
||||||
|
[INCLUDES]
|
||||||
|
before = common.conf
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
failregex = ^%(__prefix_line)s.*Login attempt or request with invalid authentication from <HOST>.*$
|
||||||
|
|
||||||
|
ignoreregex =
|
||||||
|
|
||||||
|
[Init]
|
||||||
|
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S
|
||||||
|
'')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
services.fail2ban = {
|
||||||
|
jails = {
|
||||||
|
home-assistant-iptables.settings = {
|
||||||
|
enabled = true;
|
||||||
|
filter = "hass";
|
||||||
|
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
||||||
|
logpath = "${config.services.home-assistant.configDir}/*.log";
|
||||||
|
backend = "auto";
|
||||||
|
findtime = 600;
|
||||||
|
bantime = 600;
|
||||||
|
maxretry = 5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
26
modules/nixos-modules/server/home-assistant/impermanence.nix
Normal file
26
modules/nixos-modules/server/home-assistant/impermanence.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
configDir = "/var/lib/hass";
|
||||||
|
in
|
||||||
|
lib.mkIf (config.host.impermanence.enable && config.services.home-assistant.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.home-assistant.configDir == configDir;
|
||||||
|
message = "home assistant config directory does not match persistence";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
environment.persistence."/persist/system/root" = {
|
||||||
|
enable = true;
|
||||||
|
hideMounts = true;
|
||||||
|
directories = [
|
||||||
|
{
|
||||||
|
directory = configDir;
|
||||||
|
user = "hass";
|
||||||
|
group = "hass";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
24
modules/nixos-modules/server/home-assistant/proxy.nix
Normal file
24
modules/nixos-modules/server/home-assistant/proxy.nix
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
lib.mkIf (config.host.reverse_proxy.enable && config.services.home-assistant.enable) {
|
||||||
|
host = {
|
||||||
|
reverse_proxy.subdomains.${config.services.home-assistant.subdomain} = {
|
||||||
|
target = "http://localhost:${toString config.services.home-assistant.config.http.server_port}";
|
||||||
|
|
||||||
|
websockets.enable = true;
|
||||||
|
forwardHeaders.enable = true;
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
add_header Upgrade $http_upgrade;
|
||||||
|
add_header Connection \"upgrade\";
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
proxy_read_timeout 90;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
mediaLocation = "/var/lib/immich";
|
|
||||||
in {
|
|
||||||
options.services.immich = {
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "subdomain of base domain that immich will be hosted at";
|
|
||||||
default = "immich";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.immich.enable (lib.mkMerge [
|
|
||||||
{
|
|
||||||
host = {
|
|
||||||
postgres = {
|
|
||||||
enable = true;
|
|
||||||
extraUsers = {
|
|
||||||
${config.services.immich.database.user} = {
|
|
||||||
isClient = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.firewall.interfaces.${config.services.tailscale.interfaceName} = {
|
|
||||||
allowedUDPPorts = [
|
|
||||||
config.services.immich.port
|
|
||||||
];
|
|
||||||
allowedTCPPorts = [
|
|
||||||
config.services.immich.port
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host = {
|
|
||||||
reverse_proxy.subdomains.${config.services.immich.subdomain} = {
|
|
||||||
target = "http://localhost:${toString config.services.immich.port}";
|
|
||||||
|
|
||||||
websockets.enable = true;
|
|
||||||
forwardHeaders.enable = true;
|
|
||||||
|
|
||||||
extraConfig = ''
|
|
||||||
# allow large file uploads
|
|
||||||
client_max_body_size 50000M;
|
|
||||||
|
|
||||||
# set timeout
|
|
||||||
proxy_read_timeout 600s;
|
|
||||||
proxy_send_timeout 600s;
|
|
||||||
send_timeout 600s;
|
|
||||||
proxy_redirect off;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.services.fail2ban.enable {
|
|
||||||
environment.etc = {
|
|
||||||
"fail2ban/filter.d/immich.local".text = lib.mkIf config.services.immich.enable (
|
|
||||||
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
|
||||||
[Definition]
|
|
||||||
failregex = immich-server.*Failed login attempt for user.+from ip address\s?<ADDR>
|
|
||||||
journalmatch = CONTAINER_TAG=immich-server
|
|
||||||
'')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
services.fail2ban = {
|
|
||||||
jails = {
|
|
||||||
immich-iptables.settings = lib.mkIf config.services.immich.enable {
|
|
||||||
enabled = true;
|
|
||||||
filter = "immich";
|
|
||||||
backend = "systemd";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.host.impermanence.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.services.immich.mediaLocation == mediaLocation;
|
|
||||||
message = "immich media location does not match persistence";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
environment.persistence."/persist/system/root" = {
|
|
||||||
directories = [
|
|
||||||
{
|
|
||||||
directory = mediaLocation;
|
|
||||||
user = "immich";
|
|
||||||
group = "immich";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
26
modules/nixos-modules/server/immich/database.nix
Normal file
26
modules/nixos-modules/server/immich/database.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf config.services.immich.enable (lib.mkMerge [
|
||||||
|
{
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(lib.mkIf config.host.postgres.enable {
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
extraUsers = {
|
||||||
|
${config.services.immich.database.user} = {
|
||||||
|
isClient = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
28
modules/nixos-modules/server/immich/default.nix
Normal file
28
modules/nixos-modules/server/immich/default.nix
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{lib, ...}: {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
./database.nix
|
||||||
|
./fail2ban.nix
|
||||||
|
./impermanence.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.immich = {
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "subdomain of base domain that immich will be hosted at";
|
||||||
|
default = "immich";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# NOTE: This shouldn't be needed now that we are out of testing
|
||||||
|
# config = lib.mkIf config.services.immich.enable {
|
||||||
|
# networking.firewall.interfaces.${config.services.tailscale.interfaceName} = {
|
||||||
|
# allowedUDPPorts = [
|
||||||
|
# config.services.immich.port
|
||||||
|
# ];
|
||||||
|
# allowedTCPPorts = [
|
||||||
|
# config.services.immich.port
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
}
|
||||||
26
modules/nixos-modules/server/immich/fail2ban.nix
Normal file
26
modules/nixos-modules/server/immich/fail2ban.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.fail2ban.enable && config.services.immich.enable) {
|
||||||
|
environment.etc = {
|
||||||
|
"fail2ban/filter.d/immich.local".text = pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
||||||
|
[Definition]
|
||||||
|
failregex = immich-server.*Failed login attempt for user.+from ip address\s?<ADDR>
|
||||||
|
journalmatch = CONTAINER_TAG=immich-server
|
||||||
|
'');
|
||||||
|
};
|
||||||
|
|
||||||
|
services.fail2ban = {
|
||||||
|
jails = {
|
||||||
|
immich-iptables.settings = {
|
||||||
|
enabled = true;
|
||||||
|
filter = "immich";
|
||||||
|
backend = "systemd";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
25
modules/nixos-modules/server/immich/impermanence.nix
Normal file
25
modules/nixos-modules/server/immich/impermanence.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
mediaLocation = "/var/lib/immich";
|
||||||
|
in {
|
||||||
|
config = lib.mkIf (config.services.immich.enable && config.host.impermanence.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.immich.mediaLocation == mediaLocation;
|
||||||
|
message = "immich media location does not match persistence";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
environment.persistence."/persist/system/root" = {
|
||||||
|
directories = [
|
||||||
|
{
|
||||||
|
directory = mediaLocation;
|
||||||
|
user = "immich";
|
||||||
|
group = "immich";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
27
modules/nixos-modules/server/immich/proxy.nix
Normal file
27
modules/nixos-modules/server/immich/proxy.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.immich.enable && config.host.reverse_proxy.enable) {
|
||||||
|
host = {
|
||||||
|
reverse_proxy.subdomains.${config.services.immich.subdomain} = {
|
||||||
|
target = "http://localhost:${toString config.services.immich.port}";
|
||||||
|
|
||||||
|
websockets.enable = true;
|
||||||
|
forwardHeaders.enable = true;
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
# allow large file uploads
|
||||||
|
client_max_body_size 50000M;
|
||||||
|
|
||||||
|
# set timeout
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
send_timeout 600s;
|
||||||
|
proxy_redirect off;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
jellyfinPort = 8096;
|
|
||||||
dlanPort = 1900;
|
|
||||||
jellyfin_data_directory = "/var/lib/jellyfin";
|
|
||||||
jellyfin_cache_directory = "/var/cache/jellyfin";
|
|
||||||
in {
|
|
||||||
options.services.jellyfin = {
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "subdomain of base domain that jellyfin will be hosted at";
|
|
||||||
default = "jellyfin";
|
|
||||||
};
|
|
||||||
extraSubdomains = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
description = "ex subdomain of base domain that jellyfin will be hosted at";
|
|
||||||
default = [];
|
|
||||||
};
|
|
||||||
media_directory = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "directory jellyfin media will be hosted at";
|
|
||||||
default = "/srv/jellyfin/media";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.jellyfin.enable (
|
|
||||||
lib.mkMerge [
|
|
||||||
{
|
|
||||||
environment.systemPackages = [
|
|
||||||
pkgs.jellyfin
|
|
||||||
pkgs.jellyfin-web
|
|
||||||
pkgs.jellyfin-ffmpeg
|
|
||||||
];
|
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [jellyfinPort dlanPort];
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d ${config.services.jellyfin.media_directory} 2770 jellyfin jellyfin_media"
|
|
||||||
"A ${config.services.jellyfin.media_directory} - - - - u:jellyfin:rwX,g:jellyfin_media:rwX,o::-"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
(lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host.reverse_proxy.subdomains.jellyfin = {
|
|
||||||
target = "http://localhost:${toString jellyfinPort}";
|
|
||||||
|
|
||||||
subdomain = config.services.jellyfin.subdomain;
|
|
||||||
extraSubdomains = config.services.jellyfin.extraSubdomains;
|
|
||||||
|
|
||||||
forwardHeaders.enable = true;
|
|
||||||
|
|
||||||
extraConfig = ''
|
|
||||||
client_max_body_size 20M;
|
|
||||||
add_header X-Content-Type-Options "nosniff";
|
|
||||||
|
|
||||||
proxy_buffering off;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.services.fail2ban.enable {
|
|
||||||
environment.etc = {
|
|
||||||
"fail2ban/filter.d/jellyfin.local".text = (
|
|
||||||
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
|
||||||
[Definition]
|
|
||||||
failregex = "^.*Authentication request for .* has been denied \\\(IP: \"<ADDR>\"\\\)\\\."
|
|
||||||
'')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
services.fail2ban = {
|
|
||||||
jails = {
|
|
||||||
jellyfin-iptables.settings = {
|
|
||||||
enabled = true;
|
|
||||||
filter = "jellyfin";
|
|
||||||
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
|
||||||
logpath = "${config.services.jellyfin.dataDir}/log/*.log";
|
|
||||||
backend = "auto";
|
|
||||||
findtime = 600;
|
|
||||||
bantime = 600;
|
|
||||||
maxretry = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.host.impermanence.enable {
|
|
||||||
fileSystems."/persist/system/jellyfin".neededForBoot = true;
|
|
||||||
|
|
||||||
host.storage.pool.extraDatasets = {
|
|
||||||
# sops age key needs to be available to pre persist for user generation
|
|
||||||
"persist/system/jellyfin" = {
|
|
||||||
type = "zfs_fs";
|
|
||||||
mountpoint = "/persist/system/jellyfin";
|
|
||||||
options = {
|
|
||||||
atime = "off";
|
|
||||||
relatime = "off";
|
|
||||||
canmount = "on";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.services.jellyfin.dataDir == jellyfin_data_directory;
|
|
||||||
message = "jellyfin data directory does not match persistence";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = config.services.jellyfin.cacheDir == jellyfin_cache_directory;
|
|
||||||
message = "jellyfin cache directory does not match persistence";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.persistence = {
|
|
||||||
"/persist/system/root" = {
|
|
||||||
directories = [
|
|
||||||
{
|
|
||||||
directory = jellyfin_data_directory;
|
|
||||||
user = "jellyfin";
|
|
||||||
group = "jellyfin";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
directory = jellyfin_cache_directory;
|
|
||||||
user = "jellyfin";
|
|
||||||
group = "jellyfin";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
"/persist/system/jellyfin" = {
|
|
||||||
enable = true;
|
|
||||||
hideMounts = true;
|
|
||||||
directories = [
|
|
||||||
{
|
|
||||||
directory = config.services.jellyfin.media_directory;
|
|
||||||
user = "jellyfin";
|
|
||||||
group = "jellyfin_media";
|
|
||||||
mode = "1770";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
48
modules/nixos-modules/server/jellyfin/default.nix
Normal file
48
modules/nixos-modules/server/jellyfin/default.nix
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
jellyfinPort = 8096;
|
||||||
|
dlanPort = 1900;
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
./fail2ban.nix
|
||||||
|
./impermanence.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.jellyfin = {
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "subdomain of base domain that jellyfin will be hosted at";
|
||||||
|
default = "jellyfin";
|
||||||
|
};
|
||||||
|
extraSubdomains = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
description = "ex subdomain of base domain that jellyfin will be hosted at";
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
media_directory = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "directory jellyfin media will be hosted at";
|
||||||
|
default = "/srv/jellyfin/media";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.jellyfin.enable {
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.jellyfin
|
||||||
|
pkgs.jellyfin-web
|
||||||
|
pkgs.jellyfin-ffmpeg
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [jellyfinPort dlanPort];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${config.services.jellyfin.media_directory} 2770 jellyfin jellyfin_media"
|
||||||
|
"A ${config.services.jellyfin.media_directory} - - - - u:jellyfin:rwX,g:jellyfin_media:rwX,o::-"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
32
modules/nixos-modules/server/jellyfin/fail2ban.nix
Normal file
32
modules/nixos-modules/server/jellyfin/fail2ban.nix
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.jellyfin.enable && config.services.fail2ban.enable) {
|
||||||
|
environment.etc = {
|
||||||
|
"fail2ban/filter.d/jellyfin.local".text = (
|
||||||
|
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
||||||
|
[Definition]
|
||||||
|
failregex = "^.*Authentication request for .* has been denied \\\\\\(IP: \\\"<ADDR>\\\"\\\\\\)\\\\\\."
|
||||||
|
'')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
services.fail2ban = {
|
||||||
|
jails = {
|
||||||
|
jellyfin-iptables.settings = {
|
||||||
|
enabled = true;
|
||||||
|
filter = "jellyfin";
|
||||||
|
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
||||||
|
logpath = "${config.services.jellyfin.dataDir}/log/*.log";
|
||||||
|
backend = "auto";
|
||||||
|
findtime = 600;
|
||||||
|
bantime = 600;
|
||||||
|
maxretry = 5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
66
modules/nixos-modules/server/jellyfin/impermanence.nix
Normal file
66
modules/nixos-modules/server/jellyfin/impermanence.nix
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
jellyfin_data_directory = "/var/lib/jellyfin";
|
||||||
|
jellyfin_cache_directory = "/var/cache/jellyfin";
|
||||||
|
in {
|
||||||
|
config = lib.mkIf (config.services.jellyfin.enable && config.host.impermanence.enable) {
|
||||||
|
fileSystems."/persist/system/jellyfin".neededForBoot = true;
|
||||||
|
|
||||||
|
host.storage.pool.extraDatasets = {
|
||||||
|
# sops age key needs to be available to pre persist for user generation
|
||||||
|
"persist/system/jellyfin" = {
|
||||||
|
type = "zfs_fs";
|
||||||
|
mountpoint = "/persist/system/jellyfin";
|
||||||
|
options = {
|
||||||
|
atime = "off";
|
||||||
|
relatime = "off";
|
||||||
|
canmount = "on";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.jellyfin.dataDir == jellyfin_data_directory;
|
||||||
|
message = "jellyfin data directory does not match persistence";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = config.services.jellyfin.cacheDir == jellyfin_cache_directory;
|
||||||
|
message = "jellyfin cache directory does not match persistence";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.persistence = {
|
||||||
|
"/persist/system/root" = {
|
||||||
|
directories = [
|
||||||
|
{
|
||||||
|
directory = jellyfin_data_directory;
|
||||||
|
user = "jellyfin";
|
||||||
|
group = "jellyfin";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
directory = jellyfin_cache_directory;
|
||||||
|
user = "jellyfin";
|
||||||
|
group = "jellyfin";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
"/persist/system/jellyfin" = {
|
||||||
|
enable = true;
|
||||||
|
hideMounts = true;
|
||||||
|
directories = [
|
||||||
|
{
|
||||||
|
directory = config.services.jellyfin.media_directory;
|
||||||
|
user = "jellyfin";
|
||||||
|
group = "jellyfin_media";
|
||||||
|
mode = "1770";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
25
modules/nixos-modules/server/jellyfin/proxy.nix
Normal file
25
modules/nixos-modules/server/jellyfin/proxy.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
jellyfinPort = 8096;
|
||||||
|
in {
|
||||||
|
config = lib.mkIf (config.services.jellyfin.enable && config.host.reverse_proxy.enable) {
|
||||||
|
host.reverse_proxy.subdomains.jellyfin = {
|
||||||
|
target = "http://localhost:${toString jellyfinPort}";
|
||||||
|
|
||||||
|
subdomain = config.services.jellyfin.subdomain;
|
||||||
|
extraSubdomains = config.services.jellyfin.extraSubdomains;
|
||||||
|
|
||||||
|
forwardHeaders.enable = true;
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 20M;
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,408 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
osConfig,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
with lib; let
|
|
||||||
# Database configuration assertions
|
|
||||||
dbUrlConfigured = config.services.panoramax.database.url != null;
|
|
||||||
individualDbConfigured = all (x: x != null) [
|
|
||||||
config.services.panoramax.database.host
|
|
||||||
config.services.panoramax.database.port
|
|
||||||
config.services.panoramax.database.username
|
|
||||||
config.services.panoramax.database.password
|
|
||||||
config.services.panoramax.database.name
|
|
||||||
];
|
|
||||||
|
|
||||||
envContent = ''
|
|
||||||
# Panoramax Configuration
|
|
||||||
FLASK_APP=geovisio
|
|
||||||
${
|
|
||||||
if dbUrlConfigured
|
|
||||||
then "DB_URL=${config.services.panoramax.database.url}"
|
|
||||||
else ''
|
|
||||||
DB_HOST=${config.services.panoramax.database.host}
|
|
||||||
DB_PORT=${toString config.services.panoramax.database.port}
|
|
||||||
DB_USERNAME=${config.services.panoramax.database.username}
|
|
||||||
DB_PASSWORD=${config.services.panoramax.database.password}
|
|
||||||
DB_NAME=${config.services.panoramax.database.name}
|
|
||||||
''
|
|
||||||
}
|
|
||||||
${optionalString (config.services.panoramax.storage.fsUrl != null) "FS_URL=${config.services.panoramax.storage.fsUrl}"}
|
|
||||||
${optionalString (config.services.panoramax.infrastructure.nbProxies != null) "INFRA_NB_PROXIES=${toString config.services.panoramax.infrastructure.nbProxies}"}
|
|
||||||
${optionalString (config.services.panoramax.flask.secretKey != null) "FLASK_SECRET_KEY=${config.services.panoramax.flask.secretKey}"}
|
|
||||||
${optionalString (config.services.panoramax.flask.sessionCookieDomain != null) "FLASK_SESSION_COOKIE_DOMAIN=${config.services.panoramax.flask.sessionCookieDomain}"}
|
|
||||||
${optionalString (config.services.panoramax.api.pictures.licenseSpdxId != null) "API_PICTURES_LICENSE_SPDX_ID=${config.services.panoramax.api.pictures.licenseSpdxId}"}
|
|
||||||
${optionalString (config.services.panoramax.api.pictures.licenseUrl != null) "API_PICTURES_LICENSE_URL=${config.services.panoramax.api.pictures.licenseUrl}"}
|
|
||||||
${optionalString (config.services.panoramax.port != null) "PORT=${toString config.services.panoramax.port}"}
|
|
||||||
${optionalString (config.services.panoramax.sgblur.enable) "SGBLUR_API_URL=${config.services.panoramax.sgblur.url}"}
|
|
||||||
${concatStringsSep "\n" (mapAttrsToList (name: value: "${name}=${value}") config.services.panoramax.extraEnvironment)}
|
|
||||||
'';
|
|
||||||
|
|
||||||
envFile = pkgs.writeText "panoramax.env" envContent;
|
|
||||||
in {
|
|
||||||
options.services.panoramax = {
|
|
||||||
enable = lib.mkEnableOption "panoramax";
|
|
||||||
|
|
||||||
package = lib.mkOption {
|
|
||||||
type = lib.types.package;
|
|
||||||
default = pkgs.panoramax;
|
|
||||||
description = "The panoramax package to use";
|
|
||||||
};
|
|
||||||
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "subdomain of base domain that panoramax will be hosted at";
|
|
||||||
default = "panoramax";
|
|
||||||
};
|
|
||||||
|
|
||||||
database = {
|
|
||||||
createDB = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Whether to automatically create the database and user";
|
|
||||||
};
|
|
||||||
|
|
||||||
url = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
Complete database URL connection string (e.g., "postgresql://user:password@host:port/dbname").
|
|
||||||
If provided, individual database options (host, port, username, password, name) are ignored.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
port = mkOption {
|
|
||||||
type = types.nullOr types.port;
|
|
||||||
default = 5432;
|
|
||||||
description = "Database port (ignored if database.url is set)";
|
|
||||||
};
|
|
||||||
|
|
||||||
host = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = "localhost";
|
|
||||||
description = "Database host (ignored if database.url is set)";
|
|
||||||
};
|
|
||||||
|
|
||||||
username = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = "panoramax";
|
|
||||||
description = "Database username (ignored if database.url is set)";
|
|
||||||
};
|
|
||||||
|
|
||||||
password = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Database password (ignored if database.url is set)";
|
|
||||||
};
|
|
||||||
|
|
||||||
name = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "panoramax";
|
|
||||||
description = "Database name (ignored if database.url is set)";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sgblur = {
|
|
||||||
enable = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Whether to enable sgblur integration for face and license plate blurring";
|
|
||||||
};
|
|
||||||
|
|
||||||
package = mkOption {
|
|
||||||
type = types.package;
|
|
||||||
default = pkgs.sgblur;
|
|
||||||
description = "The sgblur package to use";
|
|
||||||
};
|
|
||||||
|
|
||||||
port = mkOption {
|
|
||||||
type = types.port;
|
|
||||||
default = 8080;
|
|
||||||
description = "Port for the sgblur service";
|
|
||||||
};
|
|
||||||
|
|
||||||
host = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "127.0.0.1";
|
|
||||||
description = "Host to bind the sgblur service to";
|
|
||||||
};
|
|
||||||
|
|
||||||
url = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "http://127.0.0.1:8080";
|
|
||||||
description = "URL where sgblur service is accessible";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
port = mkOption {
|
|
||||||
type = types.nullOr types.port;
|
|
||||||
default = 5000;
|
|
||||||
description = "Port for the Panoramax service";
|
|
||||||
};
|
|
||||||
|
|
||||||
host = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "127.0.0.1";
|
|
||||||
description = "Host to bind the Panoramax service to";
|
|
||||||
};
|
|
||||||
|
|
||||||
urlScheme = mkOption {
|
|
||||||
type = types.enum ["http" "https"];
|
|
||||||
default = "https";
|
|
||||||
description = "URL scheme for the application";
|
|
||||||
};
|
|
||||||
|
|
||||||
storage = {
|
|
||||||
fsUrl = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = "/var/lib/panoramax/storage";
|
|
||||||
description = "File system URL for storage";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
infrastructure = {
|
|
||||||
nbProxies = mkOption {
|
|
||||||
type = types.nullOr types.int;
|
|
||||||
default = 1;
|
|
||||||
description = "Number of proxies in front of the application";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
flask = {
|
|
||||||
secretKey = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Flask secret key for session security";
|
|
||||||
};
|
|
||||||
|
|
||||||
sessionCookieDomain = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Flask session cookie domain";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
api = {
|
|
||||||
pictures = {
|
|
||||||
licenseSpdxId = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "SPDX license identifier for API pictures";
|
|
||||||
};
|
|
||||||
|
|
||||||
licenseUrl = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "License URL for API pictures";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
extraEnvironment = mkOption {
|
|
||||||
type = types.attrsOf types.str;
|
|
||||||
default = {};
|
|
||||||
description = "Additional environment variables";
|
|
||||||
example = {
|
|
||||||
CUSTOM_SETTING = "value";
|
|
||||||
DEBUG = "true";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.panoramax.enable (
|
|
||||||
lib.mkMerge [
|
|
||||||
{
|
|
||||||
environment.systemPackages = with pkgs;
|
|
||||||
[
|
|
||||||
config.services.panoramax.package
|
|
||||||
python3Packages.waitress
|
|
||||||
]
|
|
||||||
++ optionals config.services.panoramax.sgblur.enable [
|
|
||||||
config.services.panoramax.sgblur.package
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.panoramax = {
|
|
||||||
description = "Panoramax Service";
|
|
||||||
after = ["network.target"];
|
|
||||||
wantedBy = ["multi-user.target"];
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStart = "${pkgs.python3Packages.waitress}/bin/waitress-serve --env-file=${envFile} --host=${config.services.panoramax.host} --port=${toString config.services.panoramax.port} --url-scheme=${config.services.panoramax.urlScheme} --call geovisio:create_app";
|
|
||||||
Restart = "always";
|
|
||||||
User = "panoramax";
|
|
||||||
Group = "panoramax";
|
|
||||||
WorkingDirectory = "/var/lib/panoramax";
|
|
||||||
Environment = "PYTHONPATH=${config.services.panoramax.package}/lib/python3.11/site-packages";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users.panoramax = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "panoramax";
|
|
||||||
home = "/var/lib/panoramax";
|
|
||||||
createHome = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
users.groups.panoramax = {};
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d /var/lib/panoramax 0755 panoramax panoramax -"
|
|
||||||
"d ${config.services.panoramax.storage.fsUrl} 0755 panoramax panoramax -"
|
|
||||||
];
|
|
||||||
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = dbUrlConfigured || individualDbConfigured;
|
|
||||||
message = ''
|
|
||||||
Panoramax database configuration requires either:
|
|
||||||
- A complete database URL (services.panoramax.database.url), OR
|
|
||||||
- All individual database options (host, port, username, password, name)
|
|
||||||
|
|
||||||
Currently configured:
|
|
||||||
- database.url: ${
|
|
||||||
if dbUrlConfigured
|
|
||||||
then "✓ configured"
|
|
||||||
else "✗ not configured"
|
|
||||||
}
|
|
||||||
- individual options: ${
|
|
||||||
if individualDbConfigured
|
|
||||||
then "✓ all configured"
|
|
||||||
else "✗ some missing"
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = !config.services.panoramax.database.createDB || config.services.panoramax.database.url == null || (lib.hasPrefix "/run/" config.services.panoramax.database.url || lib.hasPrefix "unix:" config.services.panoramax.database.url || lib.hasPrefix "/" config.services.panoramax.database.host);
|
|
||||||
message = ''
|
|
||||||
Panoramax createDB option can only be used with socket connections when a database URL is provided.
|
|
||||||
Socket connections are identified by:
|
|
||||||
- URLs starting with "unix:"
|
|
||||||
- URLs starting with "/run/"
|
|
||||||
- Host paths starting with "/"
|
|
||||||
|
|
||||||
Current configuration:
|
|
||||||
- createDB: ${lib.boolToString config.services.panoramax.database.createDB}
|
|
||||||
- database.url: ${
|
|
||||||
if config.services.panoramax.database.url != null
|
|
||||||
then config.services.panoramax.database.url
|
|
||||||
else "not set"
|
|
||||||
}
|
|
||||||
- database.host: ${config.services.panoramax.database.host}
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
(
|
|
||||||
lib.mkIf config.services.panoramax.sgblur.enable {
|
|
||||||
systemd.services.sgblur = {
|
|
||||||
description = "SGBlur AI-powered face and license plate blurring service";
|
|
||||||
after = ["network.target"];
|
|
||||||
wantedBy = ["multi-user.target"];
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStart = "${config.services.panoramax.sgblur.package}/bin/uvicorn sgblur.main:app --host ${config.services.panoramax.sgblur.host} --port ${toString config.services.panoramax.sgblur.port}";
|
|
||||||
Restart = "always";
|
|
||||||
User = "sgblur";
|
|
||||||
Group = "sgblur";
|
|
||||||
WorkingDirectory = "/var/lib/sgblur";
|
|
||||||
Environment = "PYTHONPATH=${config.services.panoramax.sgblur.package}/lib/python3.11/site-packages";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users.sgblur = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "sgblur";
|
|
||||||
home = "/var/lib/sgblur";
|
|
||||||
createHome = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
users.groups.sgblur = {};
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d /var/lib/sgblur 0755 sgblur sgblur -"
|
|
||||||
];
|
|
||||||
|
|
||||||
# Update panoramax service dependencies when sgblur is enabled
|
|
||||||
systemd.services.panoramax = {
|
|
||||||
after = ["sgblur.service"];
|
|
||||||
wants = ["sgblur.service"];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(
|
|
||||||
lib.mkIf config.services.panoramax.database.createDB {
|
|
||||||
services.postgresql = {
|
|
||||||
enable = true;
|
|
||||||
ensureDatabases = [config.services.panoramax.database.name];
|
|
||||||
ensureUsers = [
|
|
||||||
{
|
|
||||||
name = config.services.panoramax.database.username;
|
|
||||||
ensureDBOwnership = true;
|
|
||||||
ensureClauses.login = true;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
extensions = ps: with ps; [postgis];
|
|
||||||
settings = {
|
|
||||||
shared_preload_libraries = ["postgis"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.postgresql.serviceConfig.ExecStartPost = let
|
|
||||||
sqlFile = pkgs.writeText "panoramax-postgis-setup.sql" ''
|
|
||||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
|
||||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
|
||||||
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
|
|
||||||
CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder;
|
|
||||||
|
|
||||||
ALTER SCHEMA public OWNER TO ${config.services.panoramax.database.username};
|
|
||||||
GRANT ALL ON SCHEMA public TO ${config.services.panoramax.database.username};
|
|
||||||
'';
|
|
||||||
in [
|
|
||||||
''
|
|
||||||
${lib.getExe' config.services.postgresql.package "psql"} -d "${config.services.panoramax.database.name}" -f "${sqlFile}"
|
|
||||||
''
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.panoramax = {
|
|
||||||
after = ["postgresql.service"];
|
|
||||||
requires = ["postgresql.service"];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(
|
|
||||||
lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host = {
|
|
||||||
reverse_proxy.subdomains.${config.services.panoramax.subdomain} = {
|
|
||||||
target = "http://localhost:${toString config.services.panoramax.port}";
|
|
||||||
|
|
||||||
websockets.enable = true;
|
|
||||||
forwardHeaders.enable = true;
|
|
||||||
|
|
||||||
extraConfig = ''
|
|
||||||
# allow large file uploads for panoramic images
|
|
||||||
client_max_body_size 100M;
|
|
||||||
|
|
||||||
# set timeout for image processing
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
send_timeout 300s;
|
|
||||||
proxy_redirect off;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(
|
|
||||||
lib.mkIf config.services.fail2ban {
|
|
||||||
# TODO: configure options for fail2ban
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(
|
|
||||||
lib.mkIf osConfig.host.impermanence.enable {
|
|
||||||
# TODO: configure impermanence for panoramax data
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
340
modules/nixos-modules/server/panoramax/default.nix
Normal file
340
modules/nixos-modules/server/panoramax/default.nix
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
with lib; let
|
||||||
|
# Database configuration assertions
|
||||||
|
dbUrlConfigured = config.services.panoramax.database.url != null;
|
||||||
|
individualDbConfigured = all (x: x != null) [
|
||||||
|
config.services.panoramax.database.host
|
||||||
|
config.services.panoramax.database.port
|
||||||
|
config.services.panoramax.database.username
|
||||||
|
config.services.panoramax.database.password
|
||||||
|
config.services.panoramax.database.name
|
||||||
|
];
|
||||||
|
|
||||||
|
envContent = ''
|
||||||
|
# Panoramax Configuration
|
||||||
|
FLASK_APP=geovisio
|
||||||
|
${
|
||||||
|
if dbUrlConfigured
|
||||||
|
then "DB_URL=${config.services.panoramax.database.url}"
|
||||||
|
else ''
|
||||||
|
DB_HOST=${config.services.panoramax.database.host}
|
||||||
|
DB_PORT=${toString config.services.panoramax.database.port}
|
||||||
|
DB_USERNAME=${config.services.panoramax.database.username}
|
||||||
|
DB_PASSWORD=${config.services.panoramax.database.password}
|
||||||
|
DB_NAME=${config.services.panoramax.database.name}
|
||||||
|
''
|
||||||
|
}
|
||||||
|
${optionalString (config.services.panoramax.storage.fsUrl != null) "FS_URL=${config.services.panoramax.storage.fsUrl}"}
|
||||||
|
${optionalString (config.services.panoramax.infrastructure.nbProxies != null) "INFRA_NB_PROXIES=${toString config.services.panoramax.infrastructure.nbProxies}"}
|
||||||
|
${optionalString (config.services.panoramax.flask.secretKey != null) "FLASK_SECRET_KEY=${config.services.panoramax.flask.secretKey}"}
|
||||||
|
${optionalString (config.services.panoramax.flask.sessionCookieDomain != null) "FLASK_SESSION_COOKIE_DOMAIN=${config.services.panoramax.flask.sessionCookieDomain}"}
|
||||||
|
${optionalString (config.services.panoramax.api.pictures.licenseSpdxId != null) "API_PICTURES_LICENSE_SPDX_ID=${config.services.panoramax.api.pictures.licenseSpdxId}"}
|
||||||
|
${optionalString (config.services.panoramax.api.pictures.licenseUrl != null) "API_PICTURES_LICENSE_URL=${config.services.panoramax.api.pictures.licenseUrl}"}
|
||||||
|
${optionalString (config.services.panoramax.port != null) "PORT=${toString config.services.panoramax.port}"}
|
||||||
|
${optionalString (config.services.panoramax.sgblur.enable) "SGBLUR_API_URL=${config.services.panoramax.sgblur.url}"}
|
||||||
|
${concatStringsSep "\n" (mapAttrsToList (name: value: "${name}=${value}") config.services.panoramax.extraEnvironment)}
|
||||||
|
'';
|
||||||
|
|
||||||
|
envFile = pkgs.writeText "panoramax.env" envContent;
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
./fail2ban.nix
|
||||||
|
./impermanence.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.panoramax = {
|
||||||
|
enable = lib.mkEnableOption "panoramax";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = pkgs.panoramax;
|
||||||
|
description = "The panoramax package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "subdomain of base domain that panoramax will be hosted at";
|
||||||
|
default = "panoramax";
|
||||||
|
};
|
||||||
|
|
||||||
|
database = {
|
||||||
|
createDB = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to automatically create the database and user";
|
||||||
|
};
|
||||||
|
|
||||||
|
url = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Complete database URL connection string (e.g., "postgresql://user:password@host:port/dbname").
|
||||||
|
If provided, individual database options (host, port, username, password, name) are ignored.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.nullOr types.port;
|
||||||
|
default = 5432;
|
||||||
|
description = "Database port (ignored if database.url is set)";
|
||||||
|
};
|
||||||
|
|
||||||
|
host = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "localhost";
|
||||||
|
description = "Database host (ignored if database.url is set)";
|
||||||
|
};
|
||||||
|
|
||||||
|
username = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "panoramax";
|
||||||
|
description = "Database username (ignored if database.url is set)";
|
||||||
|
};
|
||||||
|
|
||||||
|
password = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Database password (ignored if database.url is set)";
|
||||||
|
};
|
||||||
|
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "panoramax";
|
||||||
|
description = "Database name (ignored if database.url is set)";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sgblur = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to enable sgblur integration for face and license plate blurring";
|
||||||
|
};
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.sgblur;
|
||||||
|
description = "The sgblur package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8080;
|
||||||
|
description = "Port for the sgblur service";
|
||||||
|
};
|
||||||
|
|
||||||
|
host = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Host to bind the sgblur service to";
|
||||||
|
};
|
||||||
|
|
||||||
|
url = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "http://127.0.0.1:8080";
|
||||||
|
description = "URL where sgblur service is accessible";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.nullOr types.port;
|
||||||
|
default = 5000;
|
||||||
|
description = "Port for the Panoramax service";
|
||||||
|
};
|
||||||
|
|
||||||
|
host = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Host to bind the Panoramax service to";
|
||||||
|
};
|
||||||
|
|
||||||
|
urlScheme = mkOption {
|
||||||
|
type = types.enum ["http" "https"];
|
||||||
|
default = "https";
|
||||||
|
description = "URL scheme for the application";
|
||||||
|
};
|
||||||
|
|
||||||
|
storage = {
|
||||||
|
fsUrl = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "/var/lib/panoramax/storage";
|
||||||
|
description = "File system URL for storage";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
infrastructure = {
|
||||||
|
nbProxies = mkOption {
|
||||||
|
type = types.nullOr types.int;
|
||||||
|
default = 1;
|
||||||
|
description = "Number of proxies in front of the application";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
flask = {
|
||||||
|
secretKey = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Flask secret key for session security";
|
||||||
|
};
|
||||||
|
|
||||||
|
sessionCookieDomain = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Flask session cookie domain";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
api = {
|
||||||
|
pictures = {
|
||||||
|
licenseSpdxId = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "SPDX license identifier for API pictures";
|
||||||
|
};
|
||||||
|
|
||||||
|
licenseUrl = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "License URL for API pictures";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraEnvironment = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = {};
|
||||||
|
description = "Additional environment variables";
|
||||||
|
example = {
|
||||||
|
CUSTOM_SETTING = "value";
|
||||||
|
DEBUG = "true";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.panoramax.enable (lib.mkMerge [
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs;
|
||||||
|
[
|
||||||
|
config.services.panoramax.package
|
||||||
|
python3Packages.waitress
|
||||||
|
]
|
||||||
|
++ optionals config.services.panoramax.sgblur.enable [
|
||||||
|
config.services.panoramax.sgblur.package
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.panoramax = {
|
||||||
|
description = "Panoramax Service";
|
||||||
|
after = ["network.target"];
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${pkgs.python3Packages.waitress}/bin/waitress-serve --env-file=${envFile} --host=${config.services.panoramax.host} --port=${toString config.services.panoramax.port} --url-scheme=${config.services.panoramax.urlScheme} --call geovisio:create_app";
|
||||||
|
Restart = "always";
|
||||||
|
User = "panoramax";
|
||||||
|
Group = "panoramax";
|
||||||
|
WorkingDirectory = "/var/lib/panoramax";
|
||||||
|
Environment = "PYTHONPATH=${config.services.panoramax.package}/lib/python3.11/site-packages";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.panoramax = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "panoramax";
|
||||||
|
home = "/var/lib/panoramax";
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.panoramax = {};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/panoramax 0755 panoramax panoramax -"
|
||||||
|
"d ${config.services.panoramax.storage.fsUrl} 0755 panoramax panoramax -"
|
||||||
|
];
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = dbUrlConfigured || individualDbConfigured;
|
||||||
|
message = ''
|
||||||
|
Panoramax database configuration requires either:
|
||||||
|
- A complete database URL (services.panoramax.database.url), OR
|
||||||
|
- All individual database options (host, port, username, password, name)
|
||||||
|
|
||||||
|
Currently configured:
|
||||||
|
- database.url: ${
|
||||||
|
if dbUrlConfigured
|
||||||
|
then "✓ configured"
|
||||||
|
else "✗ not configured"
|
||||||
|
}
|
||||||
|
- individual options: ${
|
||||||
|
if individualDbConfigured
|
||||||
|
then "✓ all configured"
|
||||||
|
else "✗ some missing"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !config.services.panoramax.database.createDB || config.services.panoramax.database.url == null || (lib.hasPrefix "/run/" config.services.panoramax.database.url || lib.hasPrefix "unix:" config.services.panoramax.database.url || lib.hasPrefix "/" config.services.panoramax.database.host);
|
||||||
|
message = ''
|
||||||
|
Panoramax createDB option can only be used with socket connections when a database URL is provided.
|
||||||
|
Socket connections are identified by:
|
||||||
|
- URLs starting with "unix:"
|
||||||
|
- URLs starting with "/run/"
|
||||||
|
- Host paths starting with "/"
|
||||||
|
|
||||||
|
Current configuration:
|
||||||
|
- createDB: ${lib.boolToString config.services.panoramax.database.createDB}
|
||||||
|
- database.url: ${
|
||||||
|
if config.services.panoramax.database.url != null
|
||||||
|
then config.services.panoramax.database.url
|
||||||
|
else "not set"
|
||||||
|
}
|
||||||
|
- database.host: ${config.services.panoramax.database.host}
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
(lib.mkIf config.services.panoramax.database.createDB {
|
||||||
|
systemd.services.panoramax = {
|
||||||
|
after = ["postgresql.service"];
|
||||||
|
requires = ["postgresql.service"];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [config.services.panoramax.database.name];
|
||||||
|
ensureUsers = [
|
||||||
|
{
|
||||||
|
name = config.services.panoramax.database.username;
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
ensureClauses.login = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
extensions = ps: with ps; [postgis];
|
||||||
|
settings = {
|
||||||
|
shared_preload_libraries = ["postgis"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.postgresql.serviceConfig.ExecStartPost = let
|
||||||
|
sqlFile = pkgs.writeText "panoramax-postgis-setup.sql" ''
|
||||||
|
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder;
|
||||||
|
|
||||||
|
ALTER SCHEMA public OWNER TO ${config.services.panoramax.database.username};
|
||||||
|
GRANT ALL ON SCHEMA public TO ${config.services.panoramax.database.username};
|
||||||
|
'';
|
||||||
|
in [
|
||||||
|
''
|
||||||
|
${lib.getExe' config.services.postgresql.package "psql"} -d "${config.services.panoramax.database.name}" -f "${sqlFile}"
|
||||||
|
''
|
||||||
|
];
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
11
modules/nixos-modules/server/panoramax/fail2ban.nix
Normal file
11
modules/nixos-modules/server/panoramax/fail2ban.nix
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.panoramax.enable && config.services.fail2ban.enable) {
|
||||||
|
# TODO: configure options for fail2ban
|
||||||
|
# This is a placeholder - panoramax fail2ban configuration would need to be defined
|
||||||
|
# based on the specific log patterns and security requirements
|
||||||
|
};
|
||||||
|
}
|
||||||
14
modules/nixos-modules/server/panoramax/impermanence.nix
Normal file
14
modules/nixos-modules/server/panoramax/impermanence.nix
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
osConfig,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.panoramax.enable && osConfig.host.impermanence.enable) {
|
||||||
|
# TODO: configure impermanence for panoramax data
|
||||||
|
# This would typically include directories like:
|
||||||
|
# - /var/lib/panoramax
|
||||||
|
# - panoramax storage directories
|
||||||
|
# - any cache or temporary directories that need to persist
|
||||||
|
};
|
||||||
|
}
|
||||||
27
modules/nixos-modules/server/panoramax/proxy.nix
Normal file
27
modules/nixos-modules/server/panoramax/proxy.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.panoramax.enable && config.host.reverse_proxy.enable) {
|
||||||
|
host = {
|
||||||
|
reverse_proxy.subdomains.${config.services.panoramax.subdomain} = {
|
||||||
|
target = "http://localhost:${toString config.services.panoramax.port}";
|
||||||
|
|
||||||
|
websockets.enable = true;
|
||||||
|
forwardHeaders.enable = true;
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
# allow large file uploads for panoramic images
|
||||||
|
client_max_body_size 100M;
|
||||||
|
|
||||||
|
# set timeout for image processing
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
send_timeout 300s;
|
||||||
|
proxy_redirect off;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
dataDir = "/var/lib/paperless";
|
|
||||||
in {
|
|
||||||
options.services.paperless = {
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "subdomain of base domain that paperless will be hosted at";
|
|
||||||
default = "paperless";
|
|
||||||
};
|
|
||||||
database = {
|
|
||||||
user = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "what is the user and database that we are going to use for paperless";
|
|
||||||
default = "paperless";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.paperless.enable (lib.mkMerge [
|
|
||||||
{
|
|
||||||
host = {
|
|
||||||
postgres = {
|
|
||||||
enable = true;
|
|
||||||
extraUsers = {
|
|
||||||
${config.services.paperless.database.user} = {
|
|
||||||
isClient = true;
|
|
||||||
createUser = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
extraDatabases = {
|
|
||||||
${config.services.paperless.database.user} = {
|
|
||||||
name = config.services.paperless.database.user;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
services.paperless = {
|
|
||||||
domain = "${config.services.paperless.subdomain}.${config.host.reverse_proxy.hostname}";
|
|
||||||
configureTika = true;
|
|
||||||
settings = {
|
|
||||||
PAPERLESS_DBENGINE = "postgresql";
|
|
||||||
PAPERLESS_DBHOST = "/run/postgresql";
|
|
||||||
PAPERLESS_DBNAME = config.services.paperless.database.user;
|
|
||||||
PAPERLESS_DBUSER = config.services.paperless.database.user;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host = {
|
|
||||||
reverse_proxy.subdomains.${config.services.paperless.subdomain} = {
|
|
||||||
target = "http://${config.services.paperless.address}:${toString config.services.paperless.port}";
|
|
||||||
|
|
||||||
websockets.enable = true;
|
|
||||||
forwardHeaders.enable = true;
|
|
||||||
|
|
||||||
extraConfig = ''
|
|
||||||
# allow large file uploads
|
|
||||||
client_max_body_size 50000M;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.services.fail2ban.enable {
|
|
||||||
environment.etc = {
|
|
||||||
"fail2ban/filter.d/paperless.local".text = (
|
|
||||||
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
|
||||||
[Definition]
|
|
||||||
failregex = Login failed for user `.*` from (?:IP|private IP) `<HOST>`\.$
|
|
||||||
ignoreregex =
|
|
||||||
|
|
||||||
'')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
services.fail2ban = {
|
|
||||||
jails = {
|
|
||||||
paperless.settings = {
|
|
||||||
enabled = true;
|
|
||||||
filter = "paperless";
|
|
||||||
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
|
||||||
logpath = "${config.services.paperless.dataDir}/log/*.log";
|
|
||||||
backend = "auto";
|
|
||||||
findtime = 600;
|
|
||||||
bantime = 600;
|
|
||||||
maxretry = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.host.impermanence.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.services.paperless.dataDir == dataDir;
|
|
||||||
message = "paperless data location does not match persistence";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
environment.persistence."/persist/system/root" = {
|
|
||||||
directories = [
|
|
||||||
{
|
|
||||||
directory = dataDir;
|
|
||||||
user = "paperless";
|
|
||||||
group = "paperless";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
34
modules/nixos-modules/server/paperless/database.nix
Normal file
34
modules/nixos-modules/server/paperless/database.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf config.services.paperless.enable (lib.mkMerge [
|
||||||
|
{
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(
|
||||||
|
lib.mkIf config.host.postgres.enable {
|
||||||
|
host = {
|
||||||
|
postgres = {
|
||||||
|
extraUsers = {
|
||||||
|
${config.services.paperless.database.user} = {
|
||||||
|
isClient = true;
|
||||||
|
createUser = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraDatabases = {
|
||||||
|
${config.services.paperless.database.user} = {
|
||||||
|
name = config.services.paperless.database.user;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
40
modules/nixos-modules/server/paperless/default.nix
Normal file
40
modules/nixos-modules/server/paperless/default.nix
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
./database.nix
|
||||||
|
./fail2ban.nix
|
||||||
|
./impermanence.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.paperless = {
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "subdomain of base domain that paperless will be hosted at";
|
||||||
|
default = "paperless";
|
||||||
|
};
|
||||||
|
database = {
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "what is the user and database that we are going to use for paperless";
|
||||||
|
default = "paperless";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.paperless.enable {
|
||||||
|
services.paperless = {
|
||||||
|
domain = "${config.services.paperless.subdomain}.${config.host.reverse_proxy.hostname}";
|
||||||
|
configureTika = true;
|
||||||
|
settings = {
|
||||||
|
PAPERLESS_DBENGINE = "postgresql";
|
||||||
|
PAPERLESS_DBHOST = "/run/postgresql";
|
||||||
|
PAPERLESS_DBNAME = config.services.paperless.database.user;
|
||||||
|
PAPERLESS_DBUSER = config.services.paperless.database.user;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
34
modules/nixos-modules/server/paperless/fail2ban.nix
Normal file
34
modules/nixos-modules/server/paperless/fail2ban.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.paperless.enable && config.services.fail2ban.enable) {
|
||||||
|
environment.etc = {
|
||||||
|
"fail2ban/filter.d/paperless.local".text = (
|
||||||
|
pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
|
||||||
|
[Definition]
|
||||||
|
failregex = Login failed for user `.*` from (?:IP|private IP) `<HOST>`\.$
|
||||||
|
ignoreregex =
|
||||||
|
|
||||||
|
'')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
services.fail2ban = {
|
||||||
|
jails = {
|
||||||
|
paperless.settings = {
|
||||||
|
enabled = true;
|
||||||
|
filter = "paperless";
|
||||||
|
action = ''iptables-multiport[name=HTTP, port="http,https"]'';
|
||||||
|
logpath = "${config.services.paperless.dataDir}/log/*.log";
|
||||||
|
backend = "auto";
|
||||||
|
findtime = 600;
|
||||||
|
bantime = 600;
|
||||||
|
maxretry = 5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
25
modules/nixos-modules/server/paperless/impermanence.nix
Normal file
25
modules/nixos-modules/server/paperless/impermanence.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
dataDir = "/var/lib/paperless";
|
||||||
|
in {
|
||||||
|
config = lib.mkIf (config.services.paperless.enable && config.host.impermanence.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.services.paperless.dataDir == dataDir;
|
||||||
|
message = "paperless data location does not match persistence";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
environment.persistence."/persist/system/root" = {
|
||||||
|
directories = [
|
||||||
|
{
|
||||||
|
directory = dataDir;
|
||||||
|
user = "paperless";
|
||||||
|
group = "paperless";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
21
modules/nixos-modules/server/paperless/proxy.nix
Normal file
21
modules/nixos-modules/server/paperless/proxy.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.paperless.enable && config.host.reverse_proxy.enable) {
|
||||||
|
host = {
|
||||||
|
reverse_proxy.subdomains.${config.services.paperless.subdomain} = {
|
||||||
|
target = "http://${config.services.paperless.address}:${toString config.services.paperless.port}";
|
||||||
|
|
||||||
|
websockets.enable = true;
|
||||||
|
forwardHeaders.enable = true;
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
# allow large file uploads
|
||||||
|
client_max_body_size 50000M;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
inputs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.services.searx = {
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "subdomain of base domain that searx will be hosted at";
|
|
||||||
default = "searx";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.services.searx.enable (
|
|
||||||
lib.mkMerge [
|
|
||||||
{
|
|
||||||
sops.secrets = {
|
|
||||||
"services/searx" = {
|
|
||||||
sopsFile = "${inputs.secrets}/defiant-services.yaml";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
services.searx = {
|
|
||||||
environmentFile = config.sops.secrets."services/searx".path;
|
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
limiterSettings = {
|
|
||||||
real_ip = {
|
|
||||||
x_for = 1;
|
|
||||||
ipv4_prefix = 32;
|
|
||||||
ipv6_prefix = 56;
|
|
||||||
};
|
|
||||||
|
|
||||||
botdetection = {
|
|
||||||
ip_limit = {
|
|
||||||
filter_link_local = true;
|
|
||||||
link_token = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
server = {
|
|
||||||
port = 8083;
|
|
||||||
secret_key = "@SEARXNG_SECRET@";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Search engine settings
|
|
||||||
search = {
|
|
||||||
safe_search = 2;
|
|
||||||
autocomplete_min = 2;
|
|
||||||
autocomplete = "duckduckgo";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Enabled plugins
|
|
||||||
enabled_plugins = [
|
|
||||||
"Basic Calculator"
|
|
||||||
"Hash plugin"
|
|
||||||
"Tor check plugin"
|
|
||||||
"Open Access DOI rewrite"
|
|
||||||
"Hostnames plugin"
|
|
||||||
"Unit converter plugin"
|
|
||||||
"Tracker URL remover"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(lib.mkIf config.host.reverse_proxy.enable {
|
|
||||||
host = {
|
|
||||||
reverse_proxy.subdomains.searx = {
|
|
||||||
subdomain = config.services.searx.subdomain;
|
|
||||||
target = "http://localhost:${toString config.services.searx.settings.server.port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
71
modules/nixos-modules/server/searx/default.nix
Normal file
71
modules/nixos-modules/server/searx/default.nix
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
inputs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
imports = [
|
||||||
|
./proxy.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.searx = {
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "subdomain of base domain that searx will be hosted at";
|
||||||
|
default = "searx";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.searx.enable {
|
||||||
|
sops.secrets = {
|
||||||
|
"services/searx" = {
|
||||||
|
sopsFile = "${inputs.secrets}/defiant-services.yaml";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.searx = {
|
||||||
|
environmentFile = config.sops.secrets."services/searx".path;
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
limiterSettings = {
|
||||||
|
real_ip = {
|
||||||
|
x_for = 1;
|
||||||
|
ipv4_prefix = 32;
|
||||||
|
ipv6_prefix = 56;
|
||||||
|
};
|
||||||
|
|
||||||
|
botdetection = {
|
||||||
|
ip_limit = {
|
||||||
|
filter_link_local = true;
|
||||||
|
link_token = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
server = {
|
||||||
|
port = 8083;
|
||||||
|
secret_key = "@SEARXNG_SECRET@";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Search engine settings
|
||||||
|
search = {
|
||||||
|
safe_search = 2;
|
||||||
|
autocomplete_min = 2;
|
||||||
|
autocomplete = "duckduckgo";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enabled plugins
|
||||||
|
enabled_plugins = [
|
||||||
|
"Basic Calculator"
|
||||||
|
"Hash plugin"
|
||||||
|
"Tor check plugin"
|
||||||
|
"Open Access DOI rewrite"
|
||||||
|
"Hostnames plugin"
|
||||||
|
"Unit converter plugin"
|
||||||
|
"Tracker URL remover"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
14
modules/nixos-modules/server/searx/proxy.nix
Normal file
14
modules/nixos-modules/server/searx/proxy.nix
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
config = lib.mkIf (config.services.searx.enable && config.host.reverse_proxy.enable) {
|
||||||
|
host = {
|
||||||
|
reverse_proxy.subdomains.searx = {
|
||||||
|
subdomain = config.services.searx.subdomain;
|
||||||
|
target = "http://localhost:${toString config.services.searx.settings.server.port}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue