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
];
};
}