refactor: move services out of systems/tewi/

This commit is contained in:
arcnmx 2024-01-09 13:12:55 -08:00
parent 2f68968238
commit 5a661e8809
30 changed files with 992 additions and 638 deletions

View file

@ -0,0 +1,63 @@
{ pkgs, config, utils, lib, ... }: let
inherit (lib.attrsets) mapAttrsToList mapAttrs' nameValuePair filterAttrsRecursive;
inherit (lib.lists) singleton;
inherit (lib.modules) mkIf mkMerge mkForce;
inherit (lib.options) mkOption mkEnableOption;
cfg = config.services.cloudflared;
in {
options.services.cloudflared = with lib.types; {
tunnels = let
tunnelModule = { config, ... }: {
options = {
extraTunnel = {
enable = mkEnableOption "extra tunnels" // {
default = config.extraTunnel.ingress != { };
};
ingress = mkOption {
type = attrs;
default = { };
};
};
};
};
in mkOption {
type = attrsOf (submodule tunnelModule);
};
};
config.systemd.services = let
filterConfig = filterAttrsRecursive (_: v: ! builtins.elem v [ null [ ] { } ]);
mapIngress = hostname: ingress: {
inherit hostname;
} // filterConfig (filterConfig ingress);
in mkIf cfg.enable (mapAttrs' (uuid: tunnel: let
RuntimeDirectory = "cloudflared-tunnel-${uuid}";
configPath = "/run/${RuntimeDirectory}/config.yml";
settings = {
tunnel = uuid;
credentials-file = tunnel.credentialsFile;
ingress = mapAttrsToList mapIngress tunnel.ingress
++ mapAttrsToList mapIngress tunnel.extraTunnel.ingress
++ singleton { service = tunnel.default; };
};
in nameValuePair "cloudflared-tunnel-${uuid}" (mkMerge [
{
after = mkIf config.services.tailscale.enable [ "tailscale-autoconnect.service" ];
serviceConfig = {
RestartSec = 10;
};
}
(mkIf tunnel.extraTunnel.enable {
serviceConfig = {
inherit RuntimeDirectory;
ExecStart = mkForce [
"${cfg.package}/bin/cloudflared tunnel --config=${configPath} --no-autoupdate run"
];
ExecStartPre = [
(pkgs.writeShellScript "cloudflared-tunnel-${uuid}-prepare" ''
${utils.genJqSecretsReplacementSnippet settings configPath}
'')
];
};
})
])) cfg.tunnels);
}

View file

@ -0,0 +1,193 @@
{
pkgs,
config,
lib,
...
}: let
cfg = config.services.home-assistant;
inherit (lib.modules) mkIf mkMerge mkBefore mkDefault;
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.lists) optional elem;
in {
options.services.home-assistant = with lib.types; {
mutableUiConfig = mkEnableOption "UI-editable config files";
domain = mkOption {
type = str;
default = config.networking.domain;
};
homekit.enable = mkEnableOption "homekit" // {
default = cfg.config.homekit or [ ] != [ ];
};
googleAssistant.enable = mkEnableOption "Google Assistant" // {
default = cfg.config.google_assistant or { } != { };
};
androidTv.enable = mkEnableOption "Android TV" // {
default = elem "androidtv" cfg.extraComponents;
};
cast = {
enable = mkEnableOption "Chromecast" // {
default = elem "cast" cfg.extraComponents;
};
openFirewall = mkEnableOption "Chromecast ports" // {
default = cfg.openFirewall;
};
};
};
config = {
networking.firewall = mkIf cfg.enable {
allowedTCPPorts = mkIf (cfg.openFirewall && cfg.homekit.enable) (
map ({ port, ... }: port) cfg.config.homekit or [ ]
);
allowedUDPPortRanges = [
(mkIf (cfg.cast.enable && cfg.cast.openFirewall) {
from = 32768;
to = 60999;
})
];
};
# MDNS
services.avahi = mkIf (cfg.enable && cfg.homekit.enable) {
enable = mkDefault true;
publish.enable = false;
};
systemd.services.home-assistant = mkIf (cfg.enable && cfg.mutableUiConfig) {
# UI-editable config files
preStart = mkBefore ''
touch ${cfg.configDir}/{automations,scenes,scripts,manual,homekit_entity_config,homekit_include_entities}.yaml
'';
};
};
config.services.home-assistant = {
config = mkMerge [
{
homeassistant = {
external_url = "https://${cfg.domain}";
};
logger = {
default = mkDefault "info";
};
http = {
cors_allowed_origins = [
"https://google.com"
"https://www.home-assistant.io"
];
use_x_forwarded_for = "true";
trusted_proxies = [
"127.0.0.0/24"
"200::/7"
"100.64.0.0/10"
"fd7a:115c:a1e0:ab12::/64"
"::1"
];
};
recorder = {
db_url = mkIf config.services.postgresql.enable (mkDefault "postgresql://@/hass");
};
counter = {};
device_tracker = {};
energy = {};
group = {};
history = {};
input_boolean = {};
input_button = {};
input_datetime = {};
input_number = {};
input_select = {};
input_text = {};
logbook = {};
schedule = {};
map = {};
media_source = {};
media_player = [];
mobile_app = {};
my = {};
person = {};
ssdp = {};
switch = {};
stream = {};
sun = {};
system_health = {};
tag = {};
template = {};
timer = {};
webhook = {};
zeroconf = {};
zone = {};
sensor = {};
}
(mkIf cfg.mutableUiConfig {
# https://nixos.wiki/wiki/Home_Assistant#Combine_declarative_and_UI_defined_automations
"automation manual" = [];
"automation ui" = "!include automations.yaml";
# https://nixos.wiki/wiki/Home_Assistant#Combine_declarative_and_UI_defined_scenes
"scene manual" = [];
"scene ui" = "!include scenes.yaml";
"script manual" = [];
"script ui" = "!include scripts.yaml";
})
];
package = let
inherit (cfg.package) python;
hasBrother = elem "brother" cfg.extraComponents;
# https://github.com/pysnmp/pysnmp/issues/51
needsPyasn1pin = if lib.versionOlder python.pkgs.pysnmplib.version "6.0"
then true
else lib.warn "pyasn1 pin likely no longer needed" false;
pyasn1prefix = "${python.pkgs.pysnmp-pyasn1}/${python.sitePackages}";
home-assistant = pkgs.home-assistant.override {
packageOverrides = self: super: {
};
};
in home-assistant.overrideAttrs (old: {
makeWrapperArgs = old.makeWrapperArgs ++ optional (hasBrother && needsPyasn1pin) "--prefix PYTHONPATH : ${pyasn1prefix}";
disabledTests = old.disabledTests or [ ] ++ [
"test_check_config"
];
});
extraPackages = python3Packages: with python3Packages; mkMerge [
[
psycopg2
securetar
getmac # for upnp integration
python-otbr-api
protobuf3
(aiogithubapi.overrideAttrs (_: {doInstallCheck = false;}))
]
(mkIf cfg.homekit.enable [
aiohomekit
])
(mkIf cfg.androidTv.enable [
adb-shell
(callPackage ../../packages/androidtvremote2.nix { })
])
];
extraComponents = mkMerge [
[
"automation"
"scene"
"script"
"default_config"
"environment_canada"
"met"
"generic_thermostat"
"mqtt"
"zeroconf"
]
(mkIf cfg.homekit.enable [
"homekit"
])
(mkIf cfg.googleAssistant.enable [
"google"
"google_assistant"
"google_cloud"
])
(map ({ platform, ... }: platform) cfg.config.media_player or [ ])
(map ({ platform, ... }: platform) cfg.config.tts or [ ])
];
};
}

91
modules/nixos/kanidm.nix Normal file
View file

@ -0,0 +1,91 @@
{
lib,
pkgs,
config,
...
}: let
inherit (lib) mkIf mkMerge mkDefault mkOptionDefault mkEnableOption mkOption;
cfg = config.services.kanidm;
in {
options.services.kanidm = with lib.types; {
server = {
openFirewall = mkEnableOption "firewall ports";
unencrypted = {
enable = mkEnableOption "snake oil certificate";
domain = mkOption {
type = str;
default = cfg.server.frontend.domain;
};
package = mkOption {
type = package;
};
};
frontend = {
domain = mkOption {
type = nullOr str;
default = cfg.serverSettings.domain;
};
address = mkOption {
type = str;
default = "127.0.0.1";
};
port = mkOption {
type = port;
default = 8081;
};
};
ldap = {
enable = mkEnableOption "LDAP interface";
address = mkOption {
type = str;
default = "127.0.0.1";
};
port = mkOption {
type = port;
default = 636;
};
};
};
};
config = {
networking.firewall.allowedTCPPorts = mkIf (cfg.enableServer && cfg.server.openFirewall) [
cfg.server.frontend.port
cfg.server.ldap.port
];
services.kanidm = {
server.unencrypted.package = let
cert = pkgs.runCommand "kanidm-cert" {
inherit (cfg.server.unencrypted) domain;
nativeBuildInputs = [ pkgs.buildPackages.minica ];
} ''
install -d $out
cd $out
minica \
--ca-key ca.key.pem \
--ca-cert ca.cert.pem \
--domains $domain
cat $domain/cert.pem ca.cert.pem > $domain.pem
'';
in mkOptionDefault cert;
clientSettings = mkIf cfg.enableServer {
uri = mkDefault cfg.serverSettings.origin;
};
serverSettings = mkMerge [
{
domain = mkDefault config.networking.domain;
origin = mkDefault "https://${cfg.server.frontend.domain}";
bindaddress = mkDefault "${cfg.server.frontend.address}:${toString cfg.server.frontend.port}";
ldapbindaddress = mkIf cfg.server.ldap.enable (
mkDefault "${cfg.server.ldap.address}:${toString cfg.server.ldap.port}"
);
}
(mkIf cfg.server.unencrypted.enable {
tls_chain = "${cfg.server.unencrypted.package}/${cfg.server.unencrypted.domain}.pem";
tls_key = "${cfg.server.unencrypted.package}/${cfg.server.unencrypted.domain}/key.pem";
})
];
};
};
}

View file

@ -0,0 +1,25 @@
{
config,
lib,
...
}: let
inherit (lib) mkIf mkMerge mkEnableOption mkOption;
cfg = config.services.mosquitto;
in {
options.services.mosquitto = with lib.types; {
listeners = let
listenerModule = { ... }: {
options = {
openFirewall = mkEnableOption "firewall";
};
};
in mkOption {
type = listOf (submodule listenerModule);
};
};
config = {
networking.firewall.allowedTCPPorts = mkIf cfg.enable (mkMerge (
map (listener: mkIf listener.openFirewall [ listener.port ]) cfg.listeners
));
};
}

View file

@ -0,0 +1,75 @@
{
config,
lib,
...
}:
with lib; let
inherit (config.services) vouch-proxy;
in {
options = with types; {
services.nginx.virtualHosts = let
vouchModule = { config, ... }: {
options = {
vouch = {
enable = mkEnableOption "vouch auth proxy";
proxyOrigin = mkOption {
type = str;
};
authUrl = mkOption {
type = str;
};
url = mkOption {
type = str;
};
};
};
config = mkMerge [
{
vouch = mkIf vouch-proxy.enable {
proxyOrigin = let
inherit (vouch-proxy.settings.vouch) listen port;
in mkOptionDefault "http://${listen}:${toString port}";
authUrl = mkOptionDefault vouch-proxy.authUrl;
url = mkOptionDefault vouch-proxy.url;
};
}
(mkIf config.vouch.enable {
extraConfig = ''
auth_request /validate;
error_page 401 = @error401;
'';
locations = {
"/" = {
extraConfig = ''
add_header Access-Control-Allow-Origin ${config.vouch.url};
add_header Access-Control-Allow-Origin ${config.vouch.authUrl};
proxy_set_header X-Vouch-User $auth_resp_x_vouch_user;
'';
};
"@error401" = {
extraConfig = ''
return 302 ${config.vouch.url}/login?url=$scheme://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
'';
};
"/validate" = {
recommendedProxySettings = false;
proxyPass = "${config.vouch.proxyOrigin}/validate";
extraConfig = ''
proxy_set_header Host $host;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;
auth_request_set $auth_resp_jwt $upstream_http_x_vouch_jwt;
auth_request_set $auth_resp_err $upstream_http_x_vouch_err;
auth_request_set $auth_resp_failcount $upstream_http_x_vouch_failcount;
'';
};
};
})
];
};
in mkOption {
type = attrsOf (submodule vouchModule);
};
};
}

163
modules/nixos/vouch.nix Normal file
View file

@ -0,0 +1,163 @@
{
config,
utils,
pkgs,
lib,
...
}: let
inherit (lib) mkIf mkMerge mkDefault mkOptionDefault mkOption mkEnableOption types
getExe;
nixosConfig = config;
cfg = config.services.vouch-proxy;
settingsFormat = pkgs.formats.json { };
in {
options.services.vouch-proxy = with types; {
enable = mkEnableOption "vouch";
user = mkOption {
type = str;
default = "vouch-proxy";
};
group = mkOption {
type = str;
default = "vouch-proxy";
};
authUrl = mkOption {
type = str;
default = config.services.kanidm.serverSettings.origin;
};
domain = mkOption {
type = str;
default = config.networking.domain;
};
url = mkOption {
type = str;
default = "https://${cfg.domain}";
};
enableSettingsSecrets = mkEnableOption "genJqSecretsReplacementSnippet";
settings = let
settingsModule = { ... }: {
freeformType = settingsFormat.type;
options = {
vouch = {
cookie = {
domain = mkOption {
type = nullOr str;
default = nixosConfig.networking.domain;
};
secure = mkOption {
type = bool;
default = true;
};
};
port = mkOption {
type = port;
default = 30746;
};
listen = mkOption {
type = nullOr str;
default = "127.0.0.1";
};
allowAllUsers = mkOption {
type = bool;
default = true;
};
};
oauth = {
auth_url = mkOption {
type = str;
default = "${cfg.authUrl}/ui/oauth2";
};
token_url = mkOption {
type = str;
default = "${cfg.authUrl}/oauth2/token";
};
user_info_url = mkOption {
type = str;
default = "${cfg.authUrl}/oauth2/openid/vouch/userinfo";
};
scopes = mkOption {
type = listOf str;
default = ["openid" "email" "profile"];
};
callback_url = mkOption {
type = str;
default = "${cfg.url}/auth";
};
provider = mkOption {
type = nullOr str;
default = "oidc";
};
code_challenge_method = mkOption {
type = str;
default = "S256";
};
client_id = mkOption {
type = str;
default = "vouch";
};
};
};
};
in mkOption {
type = submodule settingsModule;
default = { };
};
extraSettings = mkOption {
inherit (settingsFormat) type;
default = { };
};
settingsPath = mkOption {
type = path;
};
};
config = let
recursiveMergeAttrs = listOfAttrsets: lib.fold (attrset: acc: lib.recursiveUpdate attrset acc) {} listOfAttrsets;
settings = recursiveMergeAttrs [
cfg.settings
cfg.extraSettings
];
settingsPath = if cfg.enableSettingsSecrets
then "/run/vouch-proxy/vouch-config.json"
else settingsFormat.generate "vouch-config.json" settings;
in mkMerge [
{
services.vouch-proxy = {
settingsPath = mkOptionDefault settingsPath;
};
}
(mkIf cfg.enable {
systemd.services.vouch-proxy = {
description = "Vouch-proxy";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStartPre = let
preprocess = pkgs.writeShellScript "vouch-proxy-prestart" (
utils.genJqSecretsReplacementSnippet settings cfg.settingsPath
);
in mkIf cfg.enableSettingsSecrets [
"${preprocess}"
];
ExecStart = [
"${getExe pkgs.vouch-proxy} -config ${cfg.settingsPath}"
];
Restart = "on-failure";
RestartSec = mkDefault 5;
WorkingDirectory = "/var/lib/vouch-proxy";
StateDirectory = "vouch-proxy";
RuntimeDirectory = "vouch-proxy";
User = cfg.user;
Group = cfg.group;
StartLimitBurst = mkDefault 3;
};
};
users.users.${cfg.user} = {
inherit (cfg) group;
isSystemUser = true;
};
users.groups.${cfg.group} = {};
})
];
}

View file

@ -0,0 +1,22 @@
{
config,
lib,
...
}: let
inherit (lib) mkIf mkEnableOption mkOption;
cfg = config.services.zigbee2mqtt;
in {
options.services.zigbee2mqtt = with lib.types; {
openFirewall = mkEnableOption "firewall port";
domain = mkOption {
type = str;
default = config.networking.domain;
};
};
config = {
networking.firewall.allowedTCPPorts = mkIf (cfg.enable && cfg.openFirewall) [
cfg.settings.frontend.port
];
};
}

12
nixos/access/gensokyo.nix Normal file
View file

@ -0,0 +1,12 @@
{
config,
lib,
pkgs,
...
}: {
services.nginx.virtualHosts.${config.networking.domain} = {
locations."/" = {
root = pkgs.gensokyoZone;
};
};
}

View file

@ -0,0 +1,13 @@
{
config,
lib,
meta,
...
}:
with lib; {
services.nginx.virtualHosts."cloud.${config.networking.domain}" = {
locations = {
"/".proxyPass = meta.tailnet.yukari.ppp 4 80 "nextcloud/";
};
};
}

33
nixos/access/plex.nix Normal file
View file

@ -0,0 +1,33 @@
{
config,
lib,
meta,
...
}:
with lib; {
services.nginx.virtualHosts."plex.${config.networking.domain}" = {
locations = {
"/" = {
proxyPass = meta.tailnet.yukari.pp 4 32400;
extraConfig = ''
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_buffering off;
proxy_set_header X-Plex-Client-Identifier $http_x_plex_client_identifier;
proxy_set_header X-Plex-Device $http_x_plex_device;
proxy_set_header X-Plex-Device-Name $http_x_plex_device_name;
proxy_set_header X-Plex-Platform $http_x_plex_platform;
proxy_set_header X-Plex-Platform-Version $http_x_plex_platform_version;
proxy_set_header X-Plex-Product $http_x_plex_product;
proxy_set_header X-Plex-Token $http_x_plex_token;
proxy_set_header X-Plex-Version $http_x_plex_version;
proxy_set_header X-Plex-Nocache $http_x_plex_nocache;
proxy_set_header X-Plex-Provides $http_x_plex_provides;
proxy_set_header X-Plex-Device-Vendor $http_x_plex_device_vendor;
proxy_set_header X-Plex-Model $http_x_plex_model;
'';
};
};
};
}

View file

@ -0,0 +1,22 @@
{
config,
lib,
...
}:
with lib; let
cfg = config.services.zigbee2mqtt;
in {
services.nginx.virtualHosts.${cfg.domain} = {
vouch.enable = true;
locations = {
"/" = {
proxyPass = "http://127.0.0.1:${toString cfg.settings.frontend.port}";
extraConfig = ''
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
'';
};
};
};
}

5
nixos/cloudflared.nix Normal file
View file

@ -0,0 +1,5 @@
{ config, lib, ... }: let
inherit (lib.modules) mkDefault;
in {
config.services.cloudflared.enable = mkDefault true;
}

32
nixos/deluge.nix Normal file
View file

@ -0,0 +1,32 @@
{ config, lib, ... }: let
inherit (lib) mkDefault;
cfg = config.services.deluge;
in {
sops.secrets.deluge-auth = {
inherit (cfg) group;
owner = cfg.user;
};
services.deluge = {
enable = mkDefault true;
declarative = mkDefault true;
openFirewall = mkDefault true;
web = {
enable = true;
};
config = {
max_upload_speed = 10.0;
#share_ratio_limit = 2.0;
max_connections_global = 1024;
max_connections_per_second = 50;
max_active_limit = 100;
max_active_downloading = 75;
max_upload_slots_global = 25;
max_active_seeding = 1;
allow_remote = true;
daemon_port = 58846;
listen_ports = [ 6881 6889 ];
random_port = false;
};
authFile = config.sops.secrets.deluge-auth.path;
};
}

133
nixos/home-assistant.nix Normal file
View file

@ -0,0 +1,133 @@
{
pkgs,
config,
lib,
...
}: let
cfg = config.services.home-assistant;
inherit (lib.modules) mkDefault;
inherit (lib.lists) optional;
in {
sops.secrets = {
ha-integration = {
owner = "hass";
path = "${cfg.configDir}/integration.yaml";
};
ha-secrets = {
owner = "hass";
path = "${cfg.configDir}/secrets.yaml";
};
};
services.home-assistant = {
enable = mkDefault true;
openFirewall = mkDefault true;
mutableUiConfig = mkDefault true;
domain = mkDefault "home.${config.networking.domain}";
config = {
homeassistant = {
name = "Gensokyo";
unit_system = "metric";
latitude = "!secret home_lat";
longitude = "!secret home_long";
elevation = "!secret home_asl";
currency = "CAD";
country = "CA";
time_zone = "America/Vancouver";
packages = {
manual = "!include manual.yaml";
};
};
frontend = {
themes = "!include_dir_merge_named themes";
};
powercalc = {
};
utility_meter = {
};
withings = {
use_webhook = true;
};
recorder = {
auto_purge = true;
purge_keep_days = 14;
commit_interval = 1;
exclude = {
domains = [
"automation"
"updater"
];
entity_globs = [
"sensor.weather_*"
"sensor.date_*"
];
entities = [
"sun.sun"
"sensor.last_boot"
"sensor.date"
"sensor.time"
];
event_types = [
"call_service"
];
};
};
google_assistant = {
project_id = "gensokyo-5cfaf";
service_account = "!include integration.yaml";
report_state = true;
exposed_domains = [
"scene"
"script"
"climate"
#"sensor"
];
entity_config = {};
};
homekit = [ {
name = "Tewi";
port = 21063;
ip_address = "10.1.1.38";
filter = let
inherit (cfg.config) google_assistant;
in {
include_domains = google_assistant.exposed_domains;
include_entities = "!include homekit_include_entities.yaml";
};
entity_config = "!include homekit_entity_config.yaml";
} ];
tts = [
{
platform = "google_translate";
service_name = "google_say";
}
];
media_player = [
{
platform = "mpd";
name = "Shanghai MPD";
host = "shanghai.local.cutie.moe";
password = "!secret mpd-shanghai-password";
}
];
prometheus = {};
wake_on_lan = {};
};
extraComponents = [
"zha"
"esphome"
"apple_tv"
"spotify"
"brother"
"ipp"
"androidtv"
"cast"
"plex"
"shopping_list"
"tile"
"wake_on_lan"
"withings"
"wled"
];
};
}

33
nixos/kanidm.nix Normal file
View file

@ -0,0 +1,33 @@
{
lib,
config,
...
}: let
inherit (lib) mkDefault;
cfg = config.services.kanidm;
in {
services.kanidm = {
enableServer = true;
enableClient = true;
server = {
unencrypted.enable = mkDefault true;
openFirewall = mkDefault true;
frontend = {
domain = mkDefault "id.${cfg.serverSettings.domain}";
address = mkDefault "0.0.0.0";
};
ldap = {
enable = mkDefault true;
address = mkDefault "0.0.0.0";
};
};
clientSettings = {
verify_ca = mkDefault true;
verify_hostnames = mkDefault true;
};
serverSettings = {
role = mkDefault "WriteReplica";
log_level = mkDefault "info";
};
};
}

View file

@ -2,11 +2,9 @@
config,
lib,
...
}: {
networking.firewall.allowedTCPPorts = [
1883
];
}: let
inherit (lib) mkDefault;
in {
sops.secrets = {
z2m-pass.owner = "mosquitto";
systemd-pass.owner = "mosquitto";
@ -15,10 +13,11 @@
};
services.mosquitto = {
enable = true;
persistence = true;
enable = mkDefault true;
persistence = mkDefault true;
listeners = [
{
openFirewall = mkDefault true;
acl = [
"pattern readwrite #"
];
@ -49,7 +48,7 @@
};
};
settings = {
allow_anonymous = false;
allow_anonymous = mkDefault false;
};
}
];

View file

@ -29,7 +29,5 @@ with lib; {
#proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
'';
clientMaxBodySize = "512m";
virtualHosts = {
};
};
}

View file

@ -0,0 +1,8 @@
{
lib,
...
}: let
inherit (lib) mkDefault;
in {
services.kanidm.serverSettings.db_fs_type = mkDefault "zfs";
}

26
nixos/vouch.nix Normal file
View file

@ -0,0 +1,26 @@
{
lib,
config,
...
}: let
inherit (lib) mkDefault;
cfg = config.services.vouch-proxy;
in {
services.vouch-proxy = {
enable = mkDefault true;
domain = mkDefault "login.${config.networking.domain}";
settings = {
vouch.cookie.secure = mkDefault false;
};
enableSettingsSecrets = mkDefault true;
extraSettings = {
oauth.client_secret._secret = config.sops.secrets.vouch-client-secret.path;
vouch.jwt.secret._secret = config.sops.secrets.vouch-jwt.path;
};
};
sops.secrets = {
vouch-jwt.owner = cfg.user;
vouch-client-secret.owner = cfg.user;
};
}

View file

@ -2,28 +2,27 @@
config,
lib,
...
}: {
networking.firewall.allowedTCPPorts = [
# Zigbee2MQTT Frontend
8072
];
}: let
cfg = config.services.zigbee2mqtt;
inherit (lib) mkDefault;
in {
sops.secrets.z2m-secret = {
owner = "zigbee2mqtt";
path = "${config.services.zigbee2mqtt.dataDir}/secret.yaml";
path = "${cfg.dataDir}/secret.yaml";
};
users.groups.input.members = ["zigbee2mqtt"];
users.groups.input.members = [ "zigbee2mqtt" ];
services.zigbee2mqtt = {
enable = true;
enable = mkDefault true;
openFirewall = mkDefault true;
domain = mkDefault "z2m.${config.networking.domain}";
settings = {
advanced = {
log_level = "info";
network_key = "!secret network_key";
};
mqtt = {
server = "mqtt://127.0.0.1:1883";
user = "z2m";
password = "!secret z2m_pass";
};

View file

@ -1,99 +0,0 @@
{
config,
lib,
meta,
pkgs,
...
}:
with lib; {
services.nginx.virtualHosts = mkMerge [
(mkIf (config.networking.hostName == "tewi") {
"gensokyo.zone" = {
locations."/" = {
root = pkgs.gensokyoZone;
};
};
"z2m.gensokyo.zone" = {
extraConfig = ''
auth_request /validate;
error_page 401 = @error401;
'';
locations = {
"/" = {
proxyPass = "http://127.0.0.1:8072";
extraConfig = ''
add_header Access-Control-Allow-Origin https://login.gensokyo.zone;
add_header Access-Control-Allow-Origin https://id.gensokyo.zone;
proxy_set_header X-Vouch-User $auth_resp_x_vouch_user;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
'';
};
"@error401" = {
extraConfig = ''
return 302 https://login.gensokyo.zone/login?url=$scheme://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
'';
};
"/validate" = {
recommendedProxySettings = false;
proxyPass = "http://127.0.0.1:30746/validate";
extraConfig = ''
proxy_set_header Host $host;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;
auth_request_set $auth_resp_jwt $upstream_http_x_vouch_jwt;
auth_request_set $auth_resp_err $upstream_http_x_vouch_err;
auth_request_set $auth_resp_failcount $upstream_http_x_vouch_failcount;
'';
};
};
};
})
(mkIf (config.networking.hostName != "tewi") {
"home.${config.networking.domain}" = {
locations = {
"/" = {
proxyPass = meta.tailnet.yukari.pp 4 8123;
extraConfig = ''
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
'';
};
};
};
"cloud.kittywit.ch" = {
locations = {
"/".proxyPass = meta.tailnet.yukari.ppp 4 80 "nextcloud/";
};
};
"plex.kittywit.ch" = {
locations = {
"/" = {
proxyPass = meta.tailnet.yukari.pp 4 32400;
extraConfig = ''
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_buffering off;
proxy_set_header X-Plex-Client-Identifier $http_x_plex_client_identifier;
proxy_set_header X-Plex-Device $http_x_plex_device;
proxy_set_header X-Plex-Device-Name $http_x_plex_device_name;
proxy_set_header X-Plex-Platform $http_x_plex_platform;
proxy_set_header X-Plex-Platform-Version $http_x_plex_platform_version;
proxy_set_header X-Plex-Product $http_x_plex_product;
proxy_set_header X-Plex-Token $http_x_plex_token;
proxy_set_header X-Plex-Version $http_x_plex_version;
proxy_set_header X-Plex-Nocache $http_x_plex_nocache;
proxy_set_header X-Plex-Provides $http_x_plex_provides;
proxy_set_header X-Plex-Device-Vendor $http_x_plex_device_vendor;
proxy_set_header X-Plex-Model $http_x_plex_model;
'';
};
};
};
})
];
}

View file

@ -1,75 +1,31 @@
{ pkgs, config, utils, lib, ... }: let
inherit (lib) mapAttrsToList mapAttrs' nameValuePair splitString last singleton
mkIf mkMerge mkForce;
{ config, lib, ... }: let
inherit (config) services;
inherit (services.kanidm.serverSettings) domain;
cfg = services.cloudflared;
apartment = "131222b0-9db0-4168-96f5-7d45ec51c3be";
shadowTunnel = {
${apartment}.ingress.deluge = {
hostname._secret = config.sops.secrets.cloudflared-tunnel-apartment-deluge.path;
service = "http://localhost:${toString services.deluge.web.port}";
};
};
in {
sops.secrets.cloudflared-tunnel-apartment.owner = services.cloudflared.user;
sops.secrets.cloudflared-tunnel-apartment-deluge.owner = services.cloudflared.user;
services.cloudflared = {
enable = true;
tunnels = {
${apartment} = {
credentialsFile = config.sops.secrets.cloudflared-tunnel-apartment.path;
default = "http_status:404";
ingress = mapAttrs' (prefix: nameValuePair "${prefix}${domain}") {
"".service = "http://localhost:80";
"home.".service = "http://localhost:${toString services.home-assistant.config.http.server_port}";
"z2m.".service = "http://localhost:80";
"login.".service = "http://localhost:${toString services.vouch-proxy.settings.vouch.port}";
"id." = let
inherit (services.kanidm.serverSettings) bindaddress;
port = last (splitString ":" bindaddress);
in {
service = "https://127.0.0.1:${port}";
ingress = {
${config.networking.domain}.service = "http://localhost:80";
${services.home-assistant.domain}.service = "http://localhost:${toString services.home-assistant.config.http.server_port}";
${services.zigbee2mqtt.domain}.service = "http://localhost:80";
${services.vouch-proxy.domain}.service = "http://localhost:${toString services.vouch-proxy.settings.vouch.port}";
${services.kanidm.server.frontend.domain} = {
service = "https://127.0.0.1:${toString services.kanidm.server.frontend.port}";
originRequest.noTLSVerify = true;
};
};
extraTunnel.ingress = {
deluge = {
hostname._secret = config.sops.secrets.cloudflared-tunnel-apartment-deluge.path;
service = "http://localhost:${toString services.deluge.web.port}";
};
};
};
};
};
systemd.services = let
filterConfig = lib.filterAttrsRecursive (_: v: ! builtins.elem v [ null [ ] { } ]);
mapIngress = hostname: ingress: {
inherit hostname;
} // filterConfig (filterConfig ingress);
in mkIf cfg.enable (mapAttrs' (uuid: tunnel: let
RuntimeDirectory = "cloudflared-tunnel-${uuid}";
configPath = "/run/${RuntimeDirectory}/config.yml";
settings = {
tunnel = uuid;
credentials-file = tunnel.credentialsFile;
ingress = mapAttrsToList mapIngress tunnel.ingress
++ mapAttrsToList mapIngress shadowTunnel.${uuid}.ingress or { }
++ singleton { service = tunnel.default; };
};
in nameValuePair "cloudflared-tunnel-${uuid}" (mkMerge [
{
after = [ "tailscale-autoconnect.service" ];
serviceConfig = {
RestartSec = 10;
};
}
(mkIf (shadowTunnel.${uuid} or { } != { }) {
serviceConfig = {
inherit RuntimeDirectory;
ExecStart = mkForce [
"${cfg.package}/bin/cloudflared tunnel --config=${configPath} --no-autoupdate run"
];
ExecStartPre = [
(pkgs.writeShellScript "cloudflared-tunnel-${uuid}-prepare" ''
${utils.genJqSecretsReplacementSnippet settings configPath}
'')
];
};
})
])) cfg.tunnels);
}

View file

@ -4,35 +4,12 @@
shadowDir = "/mnt/shadow";
mediaDir = "${shadowDir}/deluge";
in {
sops.secrets.deluge-auth = {
inherit (cfg) group;
owner = cfg.user;
};
services.deluge = {
enable = true;
declarative = true;
openFirewall = true;
web = {
enable = true;
};
config = {
download_location = "${mediaDir}/download";
move_completed_path = "${mediaDir}/complete";
move_completed = true;
max_upload_speed = 10.0;
#share_ratio_limit = 2.0;
max_connections_global = 1024;
max_connections_per_second = 50;
max_active_limit = 100;
max_active_downloading = 75;
max_upload_slots_global = 25;
max_active_seeding = 1;
allow_remote = true;
daemon_port = 58846;
listen_ports = [ 6881 6889 ];
random_port = false;
};
authFile = config.sops.secrets.deluge-auth.path;
};
systemd.services = {
deluged = {

View file

@ -1,260 +0,0 @@
{
pkgs,
config,
lib,
...
}: let
cfg = config.services.home-assistant;
inherit (lib.attrsets) attrNames filterAttrs mapAttrs' nameValuePair;
inherit (lib.strings) hasPrefix;
in {
# MDNS
services.avahi.enable = true;
networking.firewall.allowedTCPPorts = [
# Home Assistant
cfg.config.http.server_port
] ++ map ({ port, ... }: port) cfg.config.homekit;
networking.firewall.allowedUDPPorts = [
# MDNS
5353
];
networking.firewall.allowedUDPPortRanges = [
# Chromecast
{
from = 32768;
to = 60999;
}
];
sops.secrets = {
ha-integration = {
owner = "hass";
path = "${cfg.configDir}/integration.yaml";
};
ha-secrets = {
owner = "hass";
path = "${cfg.configDir}/secrets.yaml";
};
};
systemd.services.home-assistant = {
# UI-editable config files
preStart = lib.mkBefore ''
touch ${cfg.configDir}/{automations,scenes,scripts,manual,homekit_entity_config,homekit_include_entities}.yaml
'';
};
services.home-assistant = {
enable = true;
config = {
homeassistant = {
name = "Gensokyo";
unit_system = "metric";
latitude = "!secret home_lat";
longitude = "!secret home_long";
elevation = "!secret home_asl";
currency = "CAD";
country = "CA";
time_zone = "America/Vancouver";
external_url = "https://home.gensokyo.zone";
packages = {
manual = "!include manual.yaml";
};
};
frontend = {
themes = "!include_dir_merge_named themes";
};
powercalc = {
};
utility_meter = {
};
withings = {
use_webhook = true;
};
logger = {
default = "info";
};
http = {
cors_allowed_origins = [
"https://google.com"
"https://www.home-assistant.io"
];
use_x_forwarded_for = "true";
trusted_proxies = [
"127.0.0.0/24"
"200::/7"
"100.64.0.0/10"
"fd7a:115c:a1e0:ab12::/64"
"::1"
];
};
recorder = {
db_url = "postgresql://@/hass";
auto_purge = true;
purge_keep_days = 14;
commit_interval = 1;
exclude = {
domains = [
"automation"
"updater"
];
entity_globs = [
"sensor.weather_*"
"sensor.date_*"
];
entities = [
"sun.sun"
"sensor.last_boot"
"sensor.date"
"sensor.time"
];
event_types = [
"call_service"
];
};
};
google_assistant = {
project_id = "gensokyo-5cfaf";
service_account = "!include integration.yaml";
report_state = true;
exposed_domains = [
"scene"
"script"
"climate"
#"sensor"
];
entity_config = {};
};
homekit = [ {
name = "Tewi";
port = 21063;
ip_address = "10.1.1.38";
filter = let
inherit (cfg.config) google_assistant;
in {
include_domains = google_assistant.exposed_domains;
include_entities = "!include homekit_include_entities.yaml";
};
entity_config = "!include homekit_entity_config.yaml";
} ];
tts = [
{
platform = "google_translate";
service_name = "google_say";
}
];
# https://nixos.wiki/wiki/Home_Assistant#Combine_declarative_and_UI_defined_automations
"automation manual" = [];
"automation ui" = "!include automations.yaml";
# https://nixos.wiki/wiki/Home_Assistant#Combine_declarative_and_UI_defined_scenes
"scene manual" = [];
"scene ui" = "!include scenes.yaml";
"script manual" = [];
"script ui" = "!include scripts.yaml";
counter = {};
device_tracker = {};
energy = {};
group = {};
history = {};
input_boolean = {};
input_button = {};
input_datetime = {};
input_number = {};
input_select = {};
input_text = {};
logbook = {};
schedule = {};
map = {};
media_source = {};
media_player = [
{
platform = "mpd";
name = "Shanghai MPD";
host = "10.1.1.32";
password = "!secret mpd-shanghai-password";
}
];
mobile_app = {};
my = {};
person = {};
prometheus = {};
ssdp = {};
switch = {};
stream = {};
sun = {};
system_health = {};
tag = {};
template = {};
timer = {};
webhook = {};
wake_on_lan = {};
zeroconf = {};
zone = {};
sensor = {};
};
package = let
inherit (lib) warn versionOlder elem;
inherit (cfg.package) python;
hasBrother = elem "brother" cfg.extraComponents;
# https://github.com/pysnmp/pysnmp/issues/51
needsPyasn1pin = if versionOlder python.pkgs.pysnmplib.version "6.0"
then true
else warn "pyasn1 pin likely no longer needed" false;
pyasn1prefix = "${python.pkgs.pysnmp-pyasn1}/${python.sitePackages}";
home-assistant = pkgs.home-assistant.override {
packageOverrides = self: super: {
};
};
in home-assistant.overrideAttrs (old: {
makeWrapperArgs = old.makeWrapperArgs ++ lib.optional (hasBrother && needsPyasn1pin) "--prefix PYTHONPATH : ${pyasn1prefix}";
disabledTests = old.disabledTests or [ ] ++ [
"test_check_config"
];
});
extraPackages = python3Packages:
with python3Packages; [
psycopg2
aiohomekit
securetar
getmac # for upnp integration
python-otbr-api
protobuf3
adb-shell
(callPackage ./androidtvremote2.nix { })
(aiogithubapi.overrideAttrs (_: {doInstallCheck = false;}))
];
extraComponents = [
"automation"
"scene"
"script"
"zha"
"esphome"
"apple_tv"
"spotify"
"default_config"
"brother"
"ipp"
"androidtv"
"cast"
"plex"
"environment_canada"
"met"
"generic_thermostat"
"google"
"google_assistant"
"google_cloud"
"google_translate"
"homekit"
"mpd"
"mqtt"
"shopping_list"
"tile"
"wake_on_lan"
"withings"
"wled"
"zeroconf"
];
};
}

View file

@ -1,48 +0,0 @@
{
pkgs,
config,
...
}: let
conf = import ./snakeoil-certs.nix;
domain = conf.domain;
unencryptedCert = with pkgs;
runCommand "kanidm-cert" {
domain = "id.gensokyo.zone";
nativeBuildInputs = [minica];
} ''
install -d $out
cd $out
minica \
--ca-key ca.key.pem \
--ca-cert ca.cert.pem \
--domains $domain
cat $domain/cert.pem ca.cert.pem > $domain.pem
'';
in {
networking.firewall.allowedTCPPorts = [
8081
636
];
services.kanidm = {
enableServer = true;
enablePam = false;
enableClient = true;
clientSettings = {
uri = "https://id.gensokyo.zone";
verify_ca = true;
verify_hostnames = true;
};
serverSettings = {
domain = "gensokyo.zone";
origin = "https://id.gensokyo.zone";
role = "WriteReplica";
log_level = "info";
db_fs_type = "zfs";
bindaddress = "0.0.0.0:8081";
ldapbindaddress = "0.0.0.0:636";
tls_chain = "${unencryptedCert}/${unencryptedCert.domain}.pem";
tls_key = "${unencryptedCert}/${unencryptedCert.domain}/key.pem";
};
};
}

View file

@ -43,16 +43,19 @@ in {
(modulesPath + "/installer/scan/not-detected.nix")
nixos.sops
nixos.tailscale
nixos.cloudflared
nixos.nginx
nixos.access.gensokyo
nixos.access.zigbee2mqtt
nixos.postgres
nixos.vouch
nixos.kanidm
nixos.mosquitto
nixos.zigbee2mqtt
nixos.deluge
nixos.syncplay
nixos.home-assistant
inputs.systemd2mqtt.nixosModules.default
./access.nix
./syncplay.nix
./kanidm.nix
./vouch.nix
./home-assistant.nix
./zigbee2mqtt.nix
./mosquitto.nix
./postgres.nix
./nginx.nix
./mediatomb.nix
./deluge.nix
./cloudflared.nix
@ -65,6 +68,7 @@ in {
'';
services.cockroachdb.locality = "provider=local,network=gensokyo,host=${config.networking.hostName}";
services.kanidm.serverSettings.db_fs_type = "zfs";
sops.defaultSopsFile = ./secrets.yaml;

View file

@ -1,121 +0,0 @@
{
config,
utils,
pkgs,
lib,
...
}: {
options = with lib; let
origin = "https://id.gensokyo.zone";
in {
services.vouch-proxy = {
settings = {
vouch = {
cookie = {
domain = mkOption {
type = types.nullOr types.str;
default = "gensokyo.zone";
};
secure = mkOption {
type = types.bool;
default = true;
};
};
port = mkOption {
type = lib.types.port;
default = 30746;
};
listen = mkOption {
type = types.nullOr types.str;
default = "127.0.0.1";
};
allowAllUsers = mkOption {
type = types.bool;
default = true;
};
};
oauth = {
auth_url = mkOption {
type = types.str;
default = "${origin}/ui/oauth2";
};
token_url = mkOption {
type = types.str;
default = "${origin}/oauth2/token";
};
user_info_url = mkOption {
type = types.str;
default = "${origin}/oauth2/openid/vouch/userinfo";
};
scopes = mkOption {
type = types.listOf types.str;
default = ["openid" "email" "profile"];
};
callback_url = mkOption {
type = types.str;
default = "https://login.gensokyo.zone/auth";
};
provider = mkOption {
type = types.nullOr types.str;
default = "oidc";
};
code_challenge_method = mkOption {
type = types.str;
default = "S256";
};
client_id = mkOption {
type = types.str;
default = "vouch";
};
};
};
};
};
config = {
services.vouch-proxy.settings = {
vouch.cookie.secure = false;
};
sops.secrets = {
vouch-jwt.owner = "vouch-proxy";
vouch-client-secret.owner = "vouch-proxy";
};
systemd.services.vouch-proxy = {
description = "Vouch-proxy";
after = ["network.target"];
wantedBy = ["multi-user.target"];
serviceConfig = {
ExecStart = let
recursiveMergeAttrs = listOfAttrsets: lib.fold (attrset: acc: lib.recursiveUpdate attrset acc) {} listOfAttrsets;
settings = recursiveMergeAttrs [
config.services.vouch-proxy.settings
{
oauth.client_secret._secret = config.sops.secrets.vouch-client-secret.path;
vouch.jwt.secret._secret = config.sops.secrets.vouch-jwt.path;
}
];
in
pkgs.writeShellScript "vouch-proxy-start" ''
${utils.genJqSecretsReplacementSnippet settings "/run/vouch-proxy/vouch-config.json"}
${pkgs.vouch-proxy}/bin/vouch-proxy -config /run/vouch-proxy/vouch-config.json
'';
Restart = "on-failure";
RestartSec = 5;
WorkingDirectory = "/var/lib/vouch-proxy";
StateDirectory = "vouch-proxy";
RuntimeDirectory = "vouch-proxy";
User = "vouch-proxy";
Group = "vouch-proxy";
StartLimitBurst = 3;
};
};
users.users.vouch-proxy = {
isSystemUser = true;
group = "vouch-proxy";
};
users.groups.vouch-proxy = {};
};
}