feat(hass): vouch auth

disabled for now, nginx config needs more tweaking
This commit is contained in:
arcnmx 2024-05-23 13:37:19 -07:00
parent da991ef980
commit fab441b438
9 changed files with 185 additions and 28 deletions

View file

@ -1,11 +1,12 @@
{ {
pkgs, pkgs,
config, config,
access,
lib, lib,
... ...
}: let }: let
cfg = config.services.home-assistant; cfg = config.services.home-assistant;
inherit (lib.modules) mkIf mkMerge mkBefore mkDefault mkOptionDefault; inherit (lib.modules) mkIf mkMerge mkBefore mkAfter mkDefault mkOptionDefault;
inherit (lib.options) mkOption mkEnableOption; inherit (lib.options) mkOption mkEnableOption;
inherit (lib.lists) optional elem unique; inherit (lib.lists) optional elem unique;
inherit (lib.strings) toLower; inherit (lib.strings) toLower;
@ -16,10 +17,27 @@ in {
type = str; type = str;
default = config.networking.domain; default = config.networking.domain;
}; };
localDomain = mkOption {
type = nullOr str;
default = null;
};
secretsFile = mkOption { secretsFile = mkOption {
type = nullOr path; type = nullOr path;
default = null; default = null;
}; };
reverseProxy = {
enable = mkEnableOption "use_x_forwarded_for";
trustedAddresses = mkOption {
type = listOf str;
};
auth = {
enable = mkEnableOption "auth-header";
debug = mkEnableOption "debug logging";
userHeader = mkOption {
type = str;
};
};
};
homekit = { homekit = {
enable = enable =
mkEnableOption "homekit" mkEnableOption "homekit"
@ -114,27 +132,34 @@ in {
}; };
config.services.home-assistant = { config.services.home-assistant = {
reverseProxy = {
trustedAddresses = access.cidrForNetwork.loopback.all;
};
config = mkMerge [ config = mkMerge [
{ {
homeassistant = { homeassistant = {
external_url = "https://${cfg.domain}"; external_url = "https://${cfg.domain}";
internal_url = mkIf (cfg.localDomain != null) "https://${cfg.localDomain}";
}; };
logger = { logger = {
default = mkDefault "info"; default = mkDefault "info";
logs = {
"custom_components.auth_header" = mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.auth.enable && cfg.reverseProxy.auth.debug) "debug";
};
}; };
http = { http = {
use_x_forwarded_for = cfg.reverseProxy.enable;
trusted_proxies = mkIf cfg.reverseProxy.enable cfg.reverseProxy.trustedAddresses;
cors_allowed_origins = [ cors_allowed_origins = [
"https://google.com" (mkIf cfg.googleAssistant.enable "https://google.com")
(mkIf (cfg.localDomain != null) "https://${cfg.localDomain}")
# TODO: (mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.auth.enable) vouch cors idk)
"https://www.home-assistant.io" "https://www.home-assistant.io"
]; ];
use_x_forwarded_for = "true"; };
trusted_proxies = let auth_header = mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.auth.enable) {
inherit (config.networking.access) cidrForNetwork; username_header = cfg.reverseProxy.auth.userHeader;
in debug = mkIf cfg.reverseProxy.auth.debug true;
cidrForNetwork.allLocal.all
++ [
"200::/7"
];
}; };
recorder = { recorder = {
db_url = mkIf config.services.postgresql.enable (mkDefault "postgresql://@/hass"); db_url = mkIf config.services.postgresql.enable (mkDefault "postgresql://@/hass");
@ -280,5 +305,10 @@ in {
(map ({platform, ...}: platform) cfg.config.media_player or []) (map ({platform, ...}: platform) cfg.config.media_player or [])
(map ({platform, ...}: platform) cfg.config.tts or []) (map ({platform, ...}: platform) cfg.config.tts or [])
]; ];
customComponents = [
(mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.auth.enable)
pkgs.home-assistant-custom-components.auth-header
)
];
}; };
} }

View file

@ -18,7 +18,9 @@
virtualHost, virtualHost,
xvars, xvars,
... ...
}: { }: let
cfg = config.vouch;
in {
options.vouch = with lib.types; { options.vouch = with lib.types; {
requireAuth = mkEnableOption "require auth to access this location"; requireAuth = mkEnableOption "require auth to access this location";
setProxyHeader = mkOption { setProxyHeader = mkOption {
@ -32,7 +34,7 @@
enableVouchTail = enableVouchLocal && tailscale.enable && false; enableVouchTail = enableVouchLocal && tailscale.enable && false;
allowOrigin = url: "add_header Access-Control-Allow-Origin ${url};"; allowOrigin = url: "add_header Access-Control-Allow-Origin ${url};";
in in
mkIf config.vouch.requireAuth { mkIf cfg.requireAuth {
lua = mkIf virtualHost.vouch.auth.lua.enable { lua = mkIf virtualHost.vouch.auth.lua.enable {
access.block = mkMerge [ access.block = mkMerge [
(mkBefore virtualHost.vouch.auth.lua.accessRequest) (mkBefore virtualHost.vouch.auth.lua.accessRequest)
@ -41,7 +43,9 @@
]; ];
}; };
xvars.enable = mkIf (enableVouchTail || virtualHost.vouch.auth.lua.enable) true; xvars.enable = mkIf (enableVouchTail || virtualHost.vouch.auth.lua.enable) true;
proxy.headers.set.X-Vouch-User = mkOptionDefault "$auth_resp_x_vouch_user"; proxy.headers.set = mkIf cfg.setProxyHeader {
X-Vouch-User = mkOptionDefault "$auth_resp_x_vouch_user";
};
extraConfig = assert virtualHost.vouch.enable; extraConfig = assert virtualHost.vouch.enable;
mkMerge [ mkMerge [
(mkIf (!virtualHost.vouch.requireAuth) virtualHost.vouch.auth.requestDirective) (mkIf (!virtualHost.vouch.requireAuth) virtualHost.vouch.auth.requestDirective)

View file

@ -12,6 +12,7 @@
mkDefault mkDefault
mkOptionDefault mkOptionDefault
mkOption mkOption
mkPackageOption
mkEnableOption mkEnableOption
types types
getExe getExe
@ -22,6 +23,7 @@
in { in {
options.services.vouch-proxy = with types; { options.services.vouch-proxy = with types; {
enable = mkEnableOption "vouch"; enable = mkEnableOption "vouch";
package = mkPackageOption pkgs "vouch-proxy" { };
user = mkOption { user = mkOption {
type = str; type = str;
default = "vouch-proxy"; default = "vouch-proxy";
@ -157,7 +159,7 @@ in {
"${preprocess}" "${preprocess}"
]; ];
ExecStart = [ ExecStart = [
"${getExe pkgs.vouch-proxy} -config ${cfg.settingsPath}" "${getExe cfg.package} -config ${cfg.settingsPath}"
]; ];
Restart = "on-failure"; Restart = "on-failure";
RestartSec = mkDefault 5; RestartSec = mkDefault 5;

View file

@ -32,24 +32,60 @@ in {
}; };
}; };
virtualHosts = let virtualHosts = let
vouchHost = { config, ... }: {
vouch = {
requireAuth = mkDefault false;
auth.lua = {
enable = mkDefault true;
accessRequest = ''
ngx.ctx.auth_res = ngx.location.capture("${config.vouch.auth.requestLocation}")
if ngx.ctx.auth_res.status == ngx.HTTP_OK then
local vouch_user = ngx.re.match(ngx.ctx.auth_res.header["X-Vouch-User"], [[^([^@]+)@.*$]])
ngx.var["hass_user"] = vouch_user[1]
end
'';
};
};
extraConfig = ''
set $hass_user "";
'';
};
headers.set.X-Hass-User = mkDefault "$hass_user";
copyFromVhost = mkDefault "home-assistant"; copyFromVhost = mkDefault "home-assistant";
locations = { locations = {
"/" = { "/" = {
proxy.enable = true; proxy = {
inherit headers;
enable = true;
};
};
# TODO: restrict to "/auth/authorize" and "/auth/login_flow" only..?
"/auth/" = { virtualHost, config, ... }: {
proxy = {
inherit headers;
enable = true;
};
vouch = mkIf virtualHost.vouch.enable {
requireAuth = true;
};
}; };
"/api/websocket" = { "/api/websocket" = {
proxy = { proxy = {
inherit headers;
enable = true; enable = true;
websocket.enable = true; websocket.enable = true;
}; };
}; };
}; };
in { in {
home-assistant = { home-assistant = { ... }: {
imports = [ vouchHost ];
inherit name locations; inherit name locations;
proxy.upstream = mkDefault upstreamName; proxy.upstream = mkDefault upstreamName;
}; };
home-assistant'local = { home-assistant'local = { ... }: {
imports = [ vouchHost ];
vouch.enable = mkDefault nginx.virtualHosts.home-assistant.vouch.enable;
inherit name listen' locations; inherit name listen' locations;
ssl.cert = { ssl.cert = {
inherit copyFromVhost; inherit copyFromVhost;
@ -61,6 +97,15 @@ in {
}; };
}; };
}; };
config.services.home-assistant = {
reverseProxy = {
enable = mkDefault true;
auth = {
enable = mkIf (nginx.virtualHosts.home-assistant.enable && nginx.virtualHosts.home-assistant.vouch.enable) true;
userHeader = "X-Hass-User";
};
};
};
config.networking.firewall.allowedTCPPorts = let config.networking.firewall.allowedTCPPorts = let
inherit (nginx.virtualHosts.home-assistant'local) listen'; inherit (nginx.virtualHosts.home-assistant'local) listen';
in in

View file

@ -1,10 +1,12 @@
{ {
config, config,
access,
gensokyo-zone,
lib, lib,
... ...
}: let }: let
cfg = config.services.home-assistant; cfg = config.services.home-assistant;
inherit (lib.modules) mkIf mkDefault; inherit (lib.modules) mkIf mkMerge mkDefault;
sopsFile = mkDefault ./secrets/home-assistant.yaml; sopsFile = mkDefault ./secrets/home-assistant.yaml;
in { in {
sops.secrets = mkIf cfg.enable { sops.secrets = mkIf cfg.enable {
@ -22,7 +24,15 @@ in {
enable = mkDefault true; enable = mkDefault true;
mutableUiConfig = mkDefault true; mutableUiConfig = mkDefault true;
domain = mkDefault "home.${config.networking.domain}"; domain = mkDefault "home.${config.networking.domain}";
localDomain = mkDefault "home.local.${config.networking.domain}";
secretsFile = mkDefault config.sops.secrets.ha-secrets.path; secretsFile = mkDefault config.sops.secrets.ha-secrets.path;
reverseProxy = {
enable = mkDefault true;
trustedAddresses = mkMerge [
access.cidrForNetwork.int.all
# [ "200::/7" ]
];
};
config = { config = {
homeassistant = { homeassistant = {
name = "Gensokyo"; name = "Gensokyo";
@ -33,9 +43,50 @@ in {
currency = "CAD"; currency = "CAD";
country = "CA"; country = "CA";
time_zone = "America/Vancouver"; time_zone = "America/Vancouver";
# media_dirs, allowlist_external_urls, allowlist_external_dirs?
packages = { packages = {
manual = "!include manual.yaml"; manual = "!include manual.yaml";
}; };
auth_providers = let
inherit (lib.attrsets) genAttrs;
shanghai = with gensokyo-zone.systems.shanghai.config.network.networks.local; [
address4
address6
];
nue = with gensokyo-zone.systems.nue.config.network.networks.local; [
address4
address6
];
logistics = with gensokyo-zone.systems.logistics.config.network.networks.local; [
address4
address6
];
koishi = with gensokyo-zone.systems.koishi.config.network.networks.local; [
address4
#address6
];
guest = logistics ++ [
# bedroom tv
"10.1.1.67"
];
kat = koishi;
arc = shanghai ++ nue;
enableTrustedAuth = false;
in mkIf enableTrustedAuth [
{
type = "trusted_networks";
#allow_bypass_login = true;
trusted_networks = guest;
trusted_users =
genAttrs guest (_: "4051fcce77564010a836fd6b108bbb4b")
#genAttrs arc (_: "0c9c9382890746c2b246b76557f22953")
#genAttrs kat (_: "a6e96c523d334aabaea71743839ef584")
;
}
{
type = "homeassistant";
}
];
}; };
frontend = { frontend = {
themes = "!include_dir_merge_named themes"; themes = "!include_dir_merge_named themes";

View file

@ -1,15 +1,24 @@
{ {
lib,
config, config,
pkgs,
lib,
... ...
}: let }: let
inherit (lib.modules) mkIf mkMerge mkDefault; inherit (lib.modules) mkIf mkMerge mkDefault;
cfg = config.services.vouch-proxy; cfg = config.services.vouch-proxy;
sopsFile = mkDefault ./secrets/vouch.yaml; sopsFile = mkDefault ./secrets/vouch.yaml;
enableKeycloak = true; enableKeycloak = true;
hassVouch = false;
in { in {
services.vouch-proxy = { services.vouch-proxy = {
enable = mkDefault true; enable = mkDefault true;
package = mkIf hassVouch (pkgs.vouch-proxy.overrideAttrs (old: {
postPatch = ''
sed -i handlers/login.go \
-e 's/badStrings *=.*$/badStrings = []string{}/'
'' + old.postPatch or "";
doCheck = false;
}));
domain = mkDefault "login.${config.networking.domain}"; domain = mkDefault "login.${config.networking.domain}";
authUrl = mkIf enableKeycloak ( authUrl = mkIf enableKeycloak (
mkDefault "https://sso.${config.networking.domain}/realms/${config.networking.domain}" mkDefault "https://sso.${config.networking.domain}/realms/${config.networking.domain}"

View file

@ -3,13 +3,12 @@
meta, meta,
lib, lib,
access, access,
gensokyo-zone,
... ...
}: let }: let
inherit (gensokyo-zone.lib) mkAddress6;
inherit (lib.modules) mkIf mkMerge; inherit (lib.modules) mkIf mkMerge;
inherit (config.services) nginx; inherit (config.services) nginx;
inherit (nginx) virtualHosts; inherit (nginx) virtualHosts;
hassVouch = false;
in { in {
imports = let imports = let
inherit (meta) nixos; inherit (meta) nixos;
@ -283,6 +282,7 @@ in {
# not the real hass record-holder, so don't respond globally.. # not the real hass record-holder, so don't respond globally..
local.denyGlobal = true; local.denyGlobal = true;
ssl.cert.enable = true; ssl.cert.enable = true;
vouch.enable = mkIf hassVouch true;
}; };
zigbee2mqtt = { zigbee2mqtt = {
# not the real z2m record-holder, so don't respond globally.. # not the real z2m record-holder, so don't respond globally..

View file

@ -19,11 +19,14 @@ in {
(nginx.virtualHosts.zigbee2mqtt.proxied.cloudflared.getIngress {}) (nginx.virtualHosts.zigbee2mqtt.proxied.cloudflared.getIngress {})
(nginx.virtualHosts.grocy.proxied.cloudflared.getIngress {}) (nginx.virtualHosts.grocy.proxied.cloudflared.getIngress {})
(nginx.virtualHosts.barcodebuddy.proxied.cloudflared.getIngress {}) (nginx.virtualHosts.barcodebuddy.proxied.cloudflared.getIngress {})
{ (if home-assistant.reverseProxy.auth.enable
${home-assistant.domain} = assert home-assistant.enable; { then (nginx.virtualHosts.home-assistant.proxied.cloudflared.getIngress {})
service = access.proxyUrlFor {serviceName = "home-assistant";}; else {
}; ${home-assistant.domain} = assert home-assistant.enable && home-assistant.reverseProxy.enable; {
} service = access.proxyUrlFor {serviceName = "home-assistant";};
};
}
)
]; ];
}; };
}; };

View file

@ -1,8 +1,14 @@
{ {
config, config,
meta, meta,
lib,
... ...
}: { }: let
inherit (lib.modules) mkIf;
inherit (lib.lists) optional;
hassVouchAuth = false;
hassVouch = false;
in {
imports = let imports = let
inherit (meta) nixos; inherit (meta) nixos;
in [ in [
@ -21,7 +27,7 @@
nixos.grocy nixos.grocy
nixos.barcodebuddy nixos.barcodebuddy
./cloudflared.nix ./cloudflared.nix
]; ] ++ optional hassVouchAuth nixos.access.home-assistant;
services.nginx = { services.nginx = {
proxied.enable = true; proxied.enable = true;
@ -29,8 +35,15 @@
zigbee2mqtt.proxied.enable = "cloudflared"; zigbee2mqtt.proxied.enable = "cloudflared";
grocy.proxied.enable = "cloudflared"; grocy.proxied.enable = "cloudflared";
barcodebuddy.proxied.enable = "cloudflared"; barcodebuddy.proxied.enable = "cloudflared";
home-assistant = mkIf hassVouchAuth {
proxied.enable = "cloudflared";
vouch.enable = mkIf hassVouch true;
};
}; };
}; };
services.home-assistant = mkIf hassVouchAuth {
reverseProxy.auth.enable = true;
};
sops.defaultSopsFile = ./secrets.yaml; sops.defaultSopsFile = ./secrets.yaml;