refactor(nginx): ssl module

This commit is contained in:
arcnmx 2024-03-05 15:42:36 -08:00
parent 69c014b24e
commit a7e35fbc88
28 changed files with 794 additions and 546 deletions

View file

@ -0,0 +1,71 @@
{
config,
lib,
...
}: let
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkDefault mkOptionDefault mkForce mkOverride;
inherit (lib.attrsets) mapAttrsToList filterAttrs removeAttrs;
inherit (lib.lists) concatMap;
mkAlmostOptionDefault = mkOverride 1250;
inherit (config.services) nginx;
extraListenAttrs = [ "enable" ];
listenModule = { config, virtualHost, ... }: {
options = with lib.types; {
enable = mkEnableOption "this port" // {
default = true;
};
ssl = mkOption {
type = bool;
default = false;
};
port = mkOption {
type = nullOr port;
};
};
config = {
enable = mkIf (config.ssl && !virtualHost.ssl.enable) (mkForce false);
_module.freeformType = with lib.types; attrsOf (oneOf [
str (listOf str) (nullOr port) bool
]);
port = mkOptionDefault (
if config.ssl then nginx.defaultSSLListenPort else nginx.defaultHTTPListenPort
);
};
};
hostModule = { config, ... }: let
cfg = config.listenPorts;
enabledPorts = filterAttrs (_: port: port.enable) cfg;
in {
options = with lib.types; {
listenPorts = mkOption {
type = attrsOf (submoduleWith {
modules = [ listenModule ];
specialArgs = {
virtualHost = config;
};
});
default = { };
};
};
config = {
listen = let
addresses = if config.listenAddresses != [ ] then config.listenAddresses else nginx.defaultListenAddresses;
in mkIf (cfg != { }) (mkAlmostOptionDefault (
concatMap (addr: mapAttrsToList (_: listen: {
addr = mkDefault addr;
} // removeAttrs listen extraListenAttrs) enabledPorts) addresses
));
};
};
in {
options = with lib.types; {
services.nginx.virtualHosts = mkOption {
type = attrsOf (submoduleWith {
modules = [ hostModule ];
shorthandOnlyDefinesConfig = true;
});
};
};
}

View file

@ -62,7 +62,7 @@
enable = mkOptionDefault virtualHost.local.enable;
denyGlobal = mkOptionDefault virtualHost.local.denyGlobal;
trusted = mkOptionDefault virtualHost.local.trusted;
emitDenyGlobal = virtualHost.local.emitDenyGlobal;
emitDenyGlobal = config.local.denyGlobal && !virtualHost.local.emitDenyGlobal;
};
};
hostModule = {config, ...}: {

View file

@ -0,0 +1,69 @@
{
config,
lib,
...
}: let
inherit (lib.options) mkOption;
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
inherit (lib.strings) optionalString;
inherit (config.services) tailscale;
inherit (config) networking;
hostModule = {config, ...}: let
cfg = config.name;
in {
options = with lib.types; {
name = {
shortServer = mkOption {
type = nullOr str;
default = null;
};
qualifier = mkOption {
type = nullOr str;
};
includeLocal = mkOption {
type = bool;
default = false;
};
includeTailscale = mkOption {
type = bool;
};
};
allServerNames = mkOption {
type = listOf str;
};
};
config = {
name = {
qualifier = mkOptionDefault (
if config.local.enable then "local"
else null
);
includeTailscale = mkOptionDefault (
config.local.enable && tailscale.enable && cfg.qualifier != "tail"
);
};
serverName = mkIf (cfg.shortServer != null) (mkDefault (
cfg.shortServer
+ optionalString (cfg.qualifier != null) ".${cfg.qualifier}"
+ ".${networking.domain}"
));
serverAliases = mkIf (cfg.shortServer != null) (mkDefault [
(mkIf cfg.includeLocal "${cfg.shortServer}.local.${networking.domain}")
(mkIf cfg.includeTailscale "${cfg.shortServer}.tail.${networking.domain}")
]);
allServerNames = mkOptionDefault (
[ config.serverName ] ++ config.serverAliases
);
};
};
in {
options = with lib.types; {
services.nginx.virtualHosts = mkOption {
type = attrsOf (submoduleWith {
modules = [hostModule];
shorthandOnlyDefinesConfig = true;
});
};
};
}

View file

@ -0,0 +1,204 @@
{
config,
lib,
...
}: let
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkMerge mkBefore mkAfter mkOrder mkDefault mkOptionDefault mkOverride;
inherit (lib.strings) optionalString splitString match;
inherit (lib.attrsets) attrValues;
inherit (lib.lists) length head /*optional*/ any;
inherit (lib.trivial) mapNullable;
#inherit (config) networking;
inherit (config.services) nginx;
mkAlmostAfter = mkOrder 1250;
mkAlmostOptionDefault = mkOverride 1250;
schemeForUrl = url: let
parts = splitString ":" url;
in if length parts == 1 then null else head parts;
pathForUrl = url: let
parts = match ''[^:]+://[^/]+(.*)'' url;
in if parts == null then null else head parts;
hostForUrl = url: let
parts = match ''[^:]+://([^/]+).*'' url;
in if parts == null then null else head parts;
xHeadersDefaults = ''
set $x_scheme $scheme;
set $x_forwarded_for $remote_addr;
set $x_remote_addr $remote_addr;
set $x_forwarded_host $host;
set $x_forwarded_server $host;
set $x_host $host;
set $x_referer $http_referer;
set $x_proxy_host $x_host;
'';
xHeadersProxied = ''
set $x_forwarded_for $proxy_add_x_forwarded_for;
if ($http_x_forwarded_proto) {
set $x_scheme $http_x_forwarded_proto;
}
if ($http_x_real_ip) {
set $x_remote_addr $http_x_real_ip;
}
if ($http_x_forwarded_host) {
set $x_forwarded_host $http_x_forwarded_host;
}
if ($http_x_forwarded_server) {
set $x_forwarded_server $http_x_forwarded_server;
}
if ($x_referer ~ "^https?://([^/]*)/(.*)$") {
set $x_referer_host $1;
set $x_referer_path $2;
}
'';
locationModule = { config, virtualHost, ... }: let
cfg = config.proxied;
in {
options = with lib.types; {
proxied = {
enable = mkOption {
type = enum [ false true "cloudflared" ];
default = false;
};
enabled = mkOption {
type = bool;
readOnly = true;
};
xvars.enable = mkEnableOption "$x_variables";
redirectScheme = mkEnableOption "redirect to X-Forwarded-Proto" // {
default = cfg.enabled;
};
rewriteReferer = mkEnableOption "rewrite Referer header" // {
default = cfg.enabled;
};
};
proxy = {
enabled = mkOption {
type = bool;
readOnly = true;
};
scheme = mkOption {
type = nullOr str;
};
path = mkOption {
type = nullOr str;
};
host = mkOption {
type = nullOr str;
};
headers.enableRecommended = mkOption {
type = enum [ true false "nixpkgs" ];
};
};
force = mkEnableOption "redirect to SSL";
};
config = let
emitVars = cfg.enabled && !virtualHost.proxied.enabled;
emitRedirectScheme = config.proxy.enabled && cfg.redirectScheme;
emitRefererRewrite = config.proxy.enabled && cfg.rewriteReferer;
emitHeaders = config.proxy.enabled && config.proxy.headers.enableRecommended == true;
in {
proxied = {
enabled = mkOptionDefault (virtualHost.proxied.enabled || cfg.enable != false);
xvars.enable = mkIf (cfg.enabled || emitRedirectScheme || emitHeaders) true;
};
proxy = {
enabled = mkOptionDefault (config.proxyPass != null);
headers.enableRecommended = mkOptionDefault (
if !virtualHost.recommendedProxySettings then false
else if cfg.enabled then true
else "nixpkgs"
);
scheme = mkOptionDefault (
mapNullable schemeForUrl config.proxyPass
);
path = mkOptionDefault (
mapNullable pathForUrl config.proxyPass
);
host = mkOptionDefault (
mapNullable hostForUrl config.proxyPass
);
};
recommendedProxySettings = mkMerge [
(mkAlmostOptionDefault (config.proxy.headers.enableRecommended == "nixpkgs"))
];
extraConfig = mkMerge [
(mkIf emitVars (
mkBefore xHeadersProxied
))
(mkIf emitRedirectScheme ''
proxy_redirect ${config.proxy.scheme}://$host/ $x_scheme://$host/;
'')
(mkIf emitRefererRewrite ''
if ($x_referer_host = $host) {
set $x_referer "${config.proxy.scheme}://${config.proxy.host}/$x_referer_path";
}
'')
(mkIf emitHeaders (mkAlmostAfter ''
if ($x_proxy_host = "") {
set $x_proxy_host $proxy_host;
}
if ($x_proxy_host = "") {
set $x_proxy_host ${config.proxy.host};
}
proxy_set_header Host $x_proxy_host;
proxy_set_header Referer $x_referer;
proxy_set_header X-Real-IP $x_remote_addr;
proxy_set_header X-Forwarded-For $x_forwarded_for;
proxy_set_header X-Forwarded-Proto $x_scheme;
proxy_set_header X-Forwarded-Host $x_forwarded_host;
proxy_set_header X-Forwarded-Server $x_forwarded_server;
''))
];
};
};
hostModule = { config, ... }: let
cfg = config.proxied;
in {
options = with lib.types; {
proxied = {
enable = mkOption {
type = enum [ false true "cloudflared" ];
default = false;
};
enabled = mkOption {
type = bool;
default = cfg.enable != false;
};
xvars.enable = mkEnableOption "$x_variables" // {
default = cfg.enabled;
};
};
recommendedProxySettings = mkOption {
type = bool;
default = nginx.recommendedProxySettings;
};
locations = mkOption {
type = attrsOf (submoduleWith {
modules = [ locationModule ];
shorthandOnlyDefinesConfig = true;
});
};
};
config = {
proxied = {
xvars.enable = mkIf (any (loc: loc.proxied.xvars.enable) (attrValues config.locations)) true;
};
local.denyGlobal = mkIf (cfg.enable == "cloudflared") (mkDefault true);
extraConfig = mkIf cfg.xvars.enable (mkBefore ''
${xHeadersDefaults}
${optionalString cfg.enabled xHeadersProxied}
'');
};
};
in {
options = with lib.types; {
services.nginx.virtualHosts = mkOption {
type = attrsOf (submoduleWith {
modules = [ hostModule ];
shorthandOnlyDefinesConfig = true;
});
};
};
}

View file

@ -0,0 +1,92 @@
{
config,
lib,
...
}: let
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkDefault mkOptionDefault mkOverride;
mkAlmostOptionDefault = mkOverride 1250;
forceRedirectConfig = virtualHost: ''
if ($x_scheme = http) {
return ${toString virtualHost.redirectCode} https://$host$request_uri;
}
'';
locationModule = { config, virtualHost, ... }: let
cfg = config.ssl;
emitForce = cfg.force && !virtualHost.ssl.forced;
in {
options.ssl = {
force = mkEnableOption "redirect to SSL";
};
config = {
proxied.xvars.enable = mkIf emitForce true;
extraConfig = mkIf emitForce (forceRedirectConfig virtualHost);
};
};
hostModule = { config, name, ... }: let
cfg = config.ssl;
emitForce = cfg.forced && config.proxied.enabled;
in {
options = with lib.types; {
ssl = {
enable = mkOption {
type = bool;
};
force = mkOption {
# TODO: "force-nonlocal"? exceptions for tailscale?
type = enum [ false true "only" "reject" ];
default = false;
};
forced = mkOption {
type = bool;
readOnly = true;
};
cert = {
name = mkOption {
type = nullOr str;
default = null;
};
keyPath = mkOption {
type = nullOr path;
default = null;
};
path = mkOption {
type = nullOr path;
default = null;
};
};
};
locations = mkOption {
type = attrsOf (submoduleWith {
modules = [ locationModule ];
shorthandOnlyDefinesConfig = true;
});
};
};
config = {
ssl = {
enable = mkOptionDefault (cfg.cert.name != null || cfg.cert.keyPath != null);
forced = mkOptionDefault (cfg.force != false && cfg.force != "reject");
};
addSSL = mkIf (cfg.enable && (cfg.force == false || emitForce)) (mkDefault true);
forceSSL = mkIf (cfg.enable && cfg.force == true && !emitForce) (mkDefault true);
onlySSL = mkIf (cfg.enable && cfg.force == "only" && !emitForce) (mkDefault true);
rejectSSL = mkIf (cfg.force == "reject") (mkDefault true);
useACMEHost = mkAlmostOptionDefault cfg.cert.name;
sslCertificate = mkIf (cfg.cert.path != null) (mkAlmostOptionDefault cfg.cert.path);
sslCertificateKey = mkIf (cfg.cert.keyPath != null) (mkAlmostOptionDefault cfg.cert.keyPath);
proxied.xvars.enable = mkIf emitForce true;
extraConfig = mkIf emitForce (forceRedirectConfig config);
};
};
in {
options = with lib.types; {
services.nginx.virtualHosts = mkOption {
type = attrsOf (submoduleWith {
modules = [ hostModule ];
shorthandOnlyDefinesConfig = true;
});
};
};
}

View file

@ -6,11 +6,123 @@
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkMerge mkBefore mkDefault;
inherit (config) networking;
inherit (config.services) vouch-proxy tailscale;
vouchModule = {config, ...}: {
inherit (config.services) vouch-proxy nginx tailscale;
inherit (nginx) vouch;
locationModule = {config, virtualHost, ...}: {
options.vouch = with lib.types; {
requireAuth = mkEnableOption "require auth to access this location";
};
config = mkIf config.vouch.requireAuth {
proxied.xvars.enable = true;
extraConfig = assert virtualHost.vouch.enable; mkMerge [
''
add_header Access-Control-Allow-Origin ${vouch.url};
add_header Access-Control-Allow-Origin ${vouch.authUrl};
''
(mkIf (vouch.localSso.enable && config.local.enable) ''
add_header Access-Control-Allow-Origin ${vouch.localUrl};
'')
(mkIf (vouch.localSso.enable && config.local.enable && tailscale.enable) ''
add_header Access-Control-Allow-Origin $x_scheme://${vouch.tailDomain};
'')
''
proxy_set_header X-Vouch-User $auth_resp_x_vouch_user;
''
];
};
};
hostModule = {config, ...}: let
cfg = config.vouch;
in {
options = with lib.types; {
locations = mkOption {
type = attrsOf (submodule locationModule);
};
vouch = {
enable = mkEnableOption "vouch auth proxy";
requireAuth = mkEnableOption "require auth to access this host" // {
default = true;
};
errorLocation = mkOption {
type = str;
default = "@error401";
};
authRequestLocation = mkOption {
type = str;
default = "/validate";
};
authRequestDirective = mkOption {
type = lines;
default = ''
auth_request ${cfg.authRequestLocation};
'';
};
};
};
config = {
extraConfig = mkIf (cfg.enable && cfg.requireAuth) ''
${cfg.authRequestDirective}
error_page 401 = ${cfg.errorLocation};
'';
locations = mkIf cfg.enable {
"/" = mkIf cfg.requireAuth {
vouch.requireAuth = true;
};
${cfg.errorLocation} = {
proxied.xvars.enable = true;
extraConfig = let
localVouchUrl = ''
if ($x_forwarded_host ~ "\.local\.${networking.domain}$") {
set $vouch_url ${vouch.localUrl};
}
'';
tailVouchUrl = ''
if ($x_forwarded_host ~ "\.tail\.${networking.domain}$") {
set $vouch_url $x_scheme://${vouch.tailDomain};
}
'';
in
mkMerge [
(mkBefore ''
set $vouch_url ${vouch.url};
'')
(mkIf (vouch.localSso.enable && config.local.enable or false) localVouchUrl)
(mkIf (vouch.localSso.enable && config.local.enable or false && tailscale.enable) tailVouchUrl)
''
return 302 $vouch_url/login?url=$x_scheme://$x_forwarded_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
''
];
};
${cfg.authRequestLocation} = {
proxyPass = "${vouch.proxyOrigin}/validate";
proxy.headers.enableRecommended = true;
extraConfig = let
# nginx-proxied vouch must use X-Forwarded-Host, but vanilla vouch requires Host
vouchProxyHost = if vouch.doubleProxy
then "''"
else "$x_forwarded_host";
in ''
set $x_proxy_host ${vouchProxyHost};
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 {
options = with lib.types; {
services.nginx = {
vouch = {
enable = mkEnableOption "vouch auth proxy";
localSso = {
# NOTE: this won't work without multiple vouch-proxy instances with different auth urls...
enable = mkEnableOption "lan-local auth";
};
proxyOrigin = mkOption {
type = str;
default = "https://login.local.${networking.domain}";
@ -35,117 +147,32 @@
type = str;
default = "login.tail.${networking.domain}";
};
authRequestDirective = mkOption {
type = lines;
default = ''
auth_request /validate;
'';
};
};
virtualHosts = mkOption {
type = attrsOf (submodule hostModule);
};
};
config = mkMerge [
};
config.services.nginx = {
vouch = mkMerge [
{
vouch = mkIf vouch-proxy.enable {
proxyOrigin = let
inherit (vouch-proxy.settings.vouch) listen port;
host =
if listen == "0.0.0.0" || listen == "[::]"
then "localhost"
else listen;
in
mkDefault "http://${host}:${toString port}";
authUrl = mkDefault vouch-proxy.authUrl;
url = mkDefault vouch-proxy.url;
doubleProxy = mkDefault false;
};
}
{
vouch.proxyOrigin = mkIf (tailscale.enable && !vouch-proxy.enable) (
mkDefault
"http://login.tail.${networking.domain}"
proxyOrigin = mkIf (tailscale.enable && !vouch-proxy.enable) (
mkDefault "http://login.tail.${networking.domain}"
);
}
(mkIf config.vouch.enable {
extraConfig = ''
${config.vouch.authRequestDirective}
error_page 401 = @error401;
'';
locations = {
"/" = {
extraConfig = mkMerge [
''
add_header Access-Control-Allow-Origin ${config.vouch.url};
add_header Access-Control-Allow-Origin ${config.vouch.authUrl};
''
(mkIf config.local.enable ''
add_header Access-Control-Allow-Origin ${config.vouch.localUrl};
'')
(mkIf (config.local.enable && tailscale.enable) ''
add_header Access-Control-Allow-Origin $scheme://${config.vouch.tailDomain};
'')
''
proxy_set_header X-Vouch-User $auth_resp_x_vouch_user;
''
];
};
"@error401" = {
extraConfig = let
localVouchUrl = ''
if ($http_host ~ "\.local\.${networking.domain}$") {
set $vouch_url ${config.vouch.localUrl};
}
'';
tailVouchUrl = ''
if ($http_host ~ "\.tail\.${networking.domain}$") {
set $vouch_url $vouch_scheme://${config.vouch.tailDomain};
}
'';
in
mkMerge [
(mkBefore ''
set $vouch_url ${config.vouch.url};
set $vouch_scheme $scheme;
'')
(mkIf config.local.trusted (mkBefore ''
if ($http_x_forwarded_proto) {
set $vouch_scheme $http_x_forwarded_proto;
}
''))
(mkIf (config.local.enable or false) localVouchUrl)
(mkIf (config.local.enable or false && tailscale.enable) tailVouchUrl)
''
return 302 $vouch_url/login?url=$vouch_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 = mkMerge [
(mkIf (!config.vouch.doubleProxy) ''
proxy_set_header Host $host;
'')
(mkIf config.vouch.doubleProxy ''
proxy_set_header X-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 vouch-proxy.enable {
proxyOrigin = let
inherit (vouch-proxy.settings.vouch) listen port;
host =
if listen == "0.0.0.0" || listen == "[::]"
then "localhost"
else listen;
in
mkDefault "http://${host}:${toString port}";
authUrl = mkDefault vouch-proxy.authUrl;
url = mkDefault vouch-proxy.url;
doubleProxy = mkDefault false;
})
];
};
in {
options = with lib.types; {
services.nginx.virtualHosts = mkOption {
type = attrsOf (submodule vouchModule);
};
};
}

View file

@ -7,7 +7,6 @@
};
config = mkIf config.proxy.websocket.enable {
extraConfig = ''
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
'';

View file

@ -1,38 +0,0 @@
{
config,
lib,
}: let
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
cfg = config.services.bazarr;
access = config.services.nginx.access.bazarr;
proxyPass = mkDefault "https://${access.host}:${toString access.port}";
locations = {
"/" = {
inherit proxyPass;
};
};
in {
options.services.nginx.access.bazarr = with lib.types; {
host = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "bazarr.${config.networking.domain}";
};
port = mkOption {
type = port;
};
};
config.services.nginx = {
access.bazarr = mkIf cfg.enable {
host = mkOptionDefault "localhost";
port = mkOptionDefault cfg.listenPort;
};
virtualHosts = {
${access.domain} = {
inherit locations;
};
};
};
}

View file

@ -7,7 +7,8 @@
}: let
inherit (lib.modules) mkDefault;
in {
services.nginx.virtualHosts.${config.networking.domain} = {
services.nginx.virtualHosts.gensokyoZone = {
serverName = config.networking.domain;
locations = {
"/" = {
root = inputs.website.packages.${pkgs.system}.gensokyoZone;

View file

@ -3,20 +3,29 @@
lib,
...
}: let
inherit (lib.modules) mkIf mkDefault;
inherit (config.services) home-assistant tailscale;
proxyPass = "http://localhost:${toString home-assistant.config.http.server_port}/";
inherit (lib.modules) mkIf mkMerge mkDefault;
inherit (config.services) home-assistant nginx;
name.shortServer = mkDefault "home";
listenPorts = {
http = { };
https.ssl = true;
hass = mkIf (!home-assistant.enable) { port = mkDefault home-assistant.config.http.server_port; };
};
in {
services.nginx.virtualHosts."home.local.${config.networking.domain}" = mkIf home-assistant.enable {
local.enable = mkDefault true;
locations."/" = {
inherit proxyPass;
};
};
services.nginx.virtualHosts."home.tail.${config.networking.domain}" = mkIf (home-assistant.enable && tailscale.enable) {
local.enable = mkDefault true;
locations."/" = {
inherit proxyPass;
config.services.nginx.virtualHosts = {
home-assistant = {
inherit name listenPorts;
locations."/".proxyPass = mkIf home-assistant.enable (mkDefault
"http://localhost:${toString home-assistant.config.http.server_port}"
);
};
home-assistant'local = {
inherit name listenPorts;
local.enable = mkDefault true;
locations."/".proxyPass = mkIf home-assistant.enable (mkDefault
nginx.virtualHosts.home-assistant.locations."/".proxyPass
);
};
};
config.networking.firewall.allowedTCPPorts = [ home-assistant.config.http.server_port ];
}

View file

@ -1,38 +0,0 @@
{
config,
lib,
}: let
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
cfg = config.services.jackett;
access = config.services.nginx.access.jackett;
proxyPass = mkDefault "https://${access.host}:${toString access.port}";
locations = {
"/" = {
inherit proxyPass;
};
};
in {
options.services.nginx.access.jackett = with lib.types; {
host = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "jackett.${config.networking.domain}";
};
port = mkOption {
type = port;
default = cfg.port;
};
};
config.services.nginx = {
access.jackett = mkIf cfg.enable {
host = mkOptionDefault "localhost";
};
virtualHosts = {
${access.domain} = {
inherit locations;
};
};
};
}

54
nixos/access/keycloak.nix Normal file
View file

@ -0,0 +1,54 @@
{
config,
lib,
...
}: let
inherit (lib.options) mkOption;
inherit (lib.modules) mkIf mkDefault;
cfg = config.services.keycloak;
inherit (config) networking;
inherit (config.services) nginx;
access = nginx.access.keycloak;
locations = {
"/" = {
proxyPass = mkDefault access.url;
};
};
in {
options.services.nginx.access.keycloak = with lib.types; {
host = mkOption {
type = str;
default = "keycloak.local.${networking.domain}";
};
url = mkOption {
type = str;
default = "https://${access.host}";
};
};
config.services.nginx = {
access.keycloak = mkIf cfg.enable {
host = mkDefault "localhost";
url = mkDefault (if cfg.sslCertificate != null then "https://${access.host}" else "http://${access.host}");
};
virtualHosts = {
keycloak = {
name.shortServer = mkDefault "sso";
ssl.force = mkDefault true;
inherit locations;
};
keycloak'local = {
name.shortServer = mkDefault "sso";
ssl.force = mkDefault true;
local.enable = true;
inherit locations;
extraConfig = mkIf nginx.vouch.localSso.enable ''
set $vouch_local_url ${nginx.vouch.localUrl};
if ($x_forwarded_host ~ "\.tail\.${networking.domain}$") {
set $vouch_local_url $x_scheme://${nginx.vouch.tailDomain};
}
proxy_redirect ${nginx.vouch.url}/ $vouch_local_url/;
'';
};
};
};
}

View file

@ -27,18 +27,6 @@ in {
type = str;
default = "http://${access.host}:${toString access.streamPort}";
};
domain = mkOption {
type = str;
default = "kitchen.${config.networking.domain}";
};
localDomain = mkOption {
type = str;
default = "kitchen.local.${config.networking.domain}";
};
tailDomain = mkOption {
type = str;
default = "kitchen.tail.${config.networking.domain}";
};
useACMEHost = mkOption {
type = nullOr str;
default = null;
@ -46,7 +34,6 @@ in {
};
config.services.nginx = {
virtualHosts = let
addSSL = access.useACMEHost != null || virtualHosts.${access.domain}.addSSL || virtualHosts.${access.domain}.forceSSL;
extraConfig = ''
proxy_redirect off;
proxy_buffering off;
@ -64,47 +51,22 @@ in {
inherit extraConfig;
};
};
streamListen = {config, ...}: {
listen =
concatMap (addr: [
(mkIf config.addSSL {
inherit addr;
port = nginx.defaultSSLListenPort;
ssl = true;
})
{
inherit addr;
port = nginx.defaultHTTPListenPort;
}
{
inherit addr;
port = access.streamPort;
}
])
nginx.defaultListenAddresses;
listenPorts = {
http = { };
https.ssl = true;
stream.port = mkDefault access.streamPort;
};
name.shortServer = mkDefault "kitchen";
kTLS = mkDefault true;
in {
${access.domain} = mkMerge [
{
vouch.enable = true;
kTLS = mkDefault true;
inherit (access) useACMEHost;
addSSL = mkDefault (access.useACMEHost != null);
inherit locations;
}
streamListen
];
${access.localDomain} = mkMerge [
{
serverAliases = mkIf config.services.tailscale.enable [access.tailDomain];
inherit (virtualHosts.${access.domain}) useACMEHost;
addSSL = mkDefault addSSL;
kTLS = mkDefault true;
local.enable = true;
inherit locations;
}
streamListen
];
kitchencam = {
inherit name locations listenPorts kTLS;
vouch.enable = true;
};
kitchencam'local = {
inherit name locations listenPorts kTLS;
local.enable = true;
};
};
};
config.networking.firewall.allowedTCPPorts = [

View file

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

View file

@ -1,38 +0,0 @@
{
config,
lib,
}: let
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
cfg = config.services.ombi;
access = config.services.nginx.access.ombi;
proxyPass = mkDefault "https://${access.host}:${toString access.port}";
locations = {
"/" = {
inherit proxyPass;
};
};
in {
options.services.nginx.access.ombi = with lib.types; {
host = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "ombi.${config.networking.domain}";
};
port = mkOption {
type = port;
};
};
config.services.nginx = {
access.ombi = mkIf cfg.enable {
host = mkOptionDefault "localhost";
port = mkOptionDefault cfg.port;
};
virtualHosts = {
${access.domain} = {
inherit locations;
};
};
};
}

View file

@ -13,14 +13,6 @@ in {
url = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "plex.${config.networking.domain}";
};
localDomain = mkOption {
type = str;
default = "plex.local.${config.networking.domain}";
};
externalPort = mkOption {
type = nullOr port;
default = null;
@ -51,33 +43,25 @@ in {
proxy_redirect off;
proxy_buffering off;
'';
location = {
locations."/" = {
proxy.websocket.enable = true;
proxyPass = access.url;
};
name.shortServer = mkDefault "plex";
kTLS = mkDefault true;
in {
${access.domain} = {
locations."/" = location;
kTLS = mkDefault true;
inherit extraConfig;
plex = {
inherit name locations extraConfig kTLS;
};
${access.localDomain} = {
plex'local = {
inherit name locations extraConfig kTLS;
local.enable = true;
locations."/" = location;
kTLS = mkDefault true;
inherit extraConfig;
};
plex-external = mkIf (access.externalPort != null) {
serverName = mkDefault access.domain;
serverName = mkDefault "plex.${config.networking.domain}";
default = mkDefault true;
listen =
map (addr: {
inherit addr;
port = access.externalPort;
})
nginx.defaultListenAddresses;
locations."/" = location;
inherit extraConfig;
listenPorts.external.port = access.externalPort;
inherit extraConfig locations;
};
};
};

View file

@ -1,38 +0,0 @@
{
config,
lib,
}: let
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
cfg = config.services.radarr;
access = config.services.nginx.access.radarr;
proxyPass = mkDefault "https://${access.host}:${toString access.port}";
locations = {
"/" = {
inherit proxyPass;
};
};
in {
options.services.nginx.access.radarr = with lib.types; {
host = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "radarr.${config.networking.domain}";
};
port = mkOption {
type = port;
default = cfg.port;
};
};
config.services.nginx = {
access.radarr = mkIf cfg.enable {
host = mkOptionDefault "localhost";
};
virtualHosts = {
${access.domain} = {
inherit locations;
};
};
};
}

View file

@ -1,38 +0,0 @@
{
config,
lib,
}: let
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
cfg = config.services.sonarr;
access = config.services.nginx.access.sonarr;
proxyPass = mkDefault "https://${access.host}:${toString access.port}";
locations = {
"/" = {
inherit proxyPass;
};
};
in {
options.services.nginx.access.sonarr = with lib.types; {
host = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "sonarr.${config.networking.domain}";
};
port = mkOption {
type = port;
default = cfg.port;
};
};
config.services.nginx = {
access.sonarr = mkIf cfg.enable {
host = mkOptionDefault "localhost";
};
virtualHosts = {
${access.domain} = {
inherit locations;
};
};
};
}

View file

@ -1,38 +0,0 @@
{
config,
lib,
}: let
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
cfg = config.services.tautulli;
access = config.services.nginx.access.tautulli;
proxyPass = mkDefault "https://${access.host}:${toString access.port}";
locations = {
"/" = {
inherit proxyPass;
};
};
in {
options.services.nginx.access.tautulli = with lib.types; {
host = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "tautulli.${config.networking.domain}";
};
port = mkOption {
type = port;
};
};
config.services.nginx = {
access.tautulli = mkIf cfg.enable {
host = mkOptionDefault "localhost";
port = mkOptionDefault cfg.port;
};
virtualHosts = {
${access.domain} = {
inherit locations;
};
};
};
}

View file

@ -14,22 +14,6 @@ in {
url = mkOption {
type = str;
};
domain = mkOption {
type = str;
default = "login.${networking.domain}";
};
localDomain = mkOption {
type = str;
default = "login.local.${networking.domain}";
};
tailDomain = mkOption {
type = str;
default = "login.tail.${networking.domain}";
};
useACMEHost = mkOption {
type = nullOr str;
default = null;
};
};
config.services.nginx = {
access.vouch = mkIf cfg.enable {
@ -51,42 +35,52 @@ in {
'';
};
"/validate" = {config, ...}: {
proxied.enable = true;
proxyPass = mkDefault (access.url + "/validate");
recommendedProxySettings = mkDefault false;
extraConfig =
if config.local.trusted
then ''
if ($http_x_host = ''') {
set $http_x_host $host;
}
proxy_set_header Host $http_x_host;
''
else ''
proxy_set_header Host $host;
'';
proxy.headers.enableRecommended = true;
local.denyGlobal = true;
extraConfig = ''
set $x_proxy_host $x_forwarded_host;
'';
};
};
localLocations = kanidmDomain: {
"/".extraConfig = ''
proxy_redirect $scheme://sso.${networking.domain}/ $scheme://${kanidmDomain}/;
'';
localLocations = kanidmDomain: mkIf nginx.vouch.localSso.enable {
"/" = {
proxied.xvars.enable = true;
extraConfig = ''
proxy_redirect https://sso.${networking.domain}/ $x_scheme://${kanidmDomain}/;
'';
};
};
name.shortServer = mkDefault "login";
in {
${access.localDomain} = mkIf (access.useACMEHost != null) {
local.enable = true;
locations = mkMerge [
locations
];
useACMEHost = mkDefault access.useACMEHost;
forceSSL = true;
vouch = {
inherit name locations;
ssl.force = true;
};
${access.tailDomain} = mkIf tailscale.enable {
vouch'local = {
name = {
inherit (name) shortServer;
qualifier = mkDefault "local";
includeTailscale = false;
};
local.enable = true;
ssl.force = true;
locations = mkMerge [
locations
(localLocations "sso.local.${networking.domain}")
];
};
vouch'tail = mkIf tailscale.enable {
name = {
inherit (name) shortServer;
qualifier = mkDefault "tail";
};
local.enable = true;
locations = mkMerge [
locations
(localLocations "sso.tail.${networking.domain}")
];
useACMEHost = mkDefault access.useACMEHost;
addSSL = mkIf (access.useACMEHost != null) (mkDefault true);
};
};
};

View file

@ -7,47 +7,35 @@
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
cfg = config.services.zigbee2mqtt;
access = config.services.nginx.access.zigbee2mqtt;
location = {
locations."/" = {
proxy.websocket.enable = true;
proxyPass = mkDefault "http://${access.host}:${toString access.port}";
};
name.shortServer = mkDefault "z2m";
in {
options.services.nginx.access.zigbee2mqtt = with lib.types; {
host = mkOption {
type = str;
};
domain = mkOption {
type = str;
};
localDomain = mkOption {
type = str;
default = "z2m.local.${config.networking.domain}";
};
tailDomain = mkOption {
type = str;
default = "z2m.tail.${config.networking.domain}";
};
port = mkOption {
type = port;
};
};
config.services.nginx = {
access.zigbee2mqtt = mkIf cfg.enable {
domain = mkOptionDefault cfg.domain;
host = mkOptionDefault "localhost";
port = mkIf (cfg.settings ? frontend.port) (
mkOptionDefault cfg.settings.frontend.port
);
};
virtualHosts = {
${access.domain} = {
zigbee2mqtt = {
inherit name locations;
vouch.enable = true;
locations."/" = location;
};
${access.localDomain} = {
serverAliases = mkIf config.services.tailscale.enable [access.tailDomain];
zigbee2mqtt'local = {
inherit name locations;
local.enable = true;
locations."/" = location;
};
};
};

View file

@ -43,6 +43,7 @@ in {
settings = {
hostname = mkDefault hostname;
proxy = mkDefault (if cfg.sslCertificate != null then "reencrypt" else "edge");
proxy-headers = mkDefault "xforwarded";
};
sslCertificate = mkDefault "${cert}/fullchain.pem";

View file

@ -1,4 +1,4 @@
vouch-client-secret: ENC[AES256_GCM,data:RWGjoC+L0FilUipBgvCy5t1bJEzDM8THc/m6RjwSPs9qPqfQCbSQWt/XXtQMR0VJ,iv:PJREZcXjLf3JVU4W5jD/6ruoaCIUV5TRocrmb1PjQeE=,tag:ZgKi+rI4ok7aIB3TOCXFKg==,type:str]
vouch-client-secret: ENC[AES256_GCM,data:gmCOrC3FDSUw/V1FZywiq0MWgKTK7j2ojb/fZahFS4g=,iv:D6UfdfeRaqzEhK9yVsc0TfIfQ/EBzWdtzLt/vUKPR8I=,tag:EZ+XHvi0gnhvJ6s1d6KviA==,type:str]
vouch-jwt: ENC[AES256_GCM,data:7G1/pzEmR7NM7eFb2wED4HR/A00TNdBjBs/OdziDgIuPttqp4AeLRnJ0UhRps5taEx2cTH0U5GyCR/A9ef9hfA==,iv:ugOuH35frzoT6lX9UTJjzoTm2OTCqS3sNJGq8TfViEY=,tag:jvv4DkLwMB9ytefpGpIqPQ==,type:str]
sops:
shamir_threshold: 1
@ -70,8 +70,8 @@ sops:
RkQvakRmdjB3ME1rbnNTWjVDQU5QSmcKi1KhB8zpSLlaCgSelaEYdtOGHFLlc+Z3
hagYdJqojaOYbTGVBkWAYK0Zfh++1/QbDYJH6ySjDC8mFFCqEdSuYQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-01-16T19:11:11Z"
mac: ENC[AES256_GCM,data:1J3dCx5ptr2ah2LbvoP/rcUzPlzm3wZvpWLffIvh7PriNJ6vx2xj5fFK9s2AhIunxAaef0KJLzwzcfNwxEkJO3M6QSf0UUw3wopah1W3ZKLE2H/Z8bNncaNzSuh6QODbYShSG2yK4HmQApd8R9NfKlAHsDno+aRhuh7OYM7CaLI=,iv:KzMfJDJOqYm2epLM6Epd44aRoU7uJcusCl6m6/+cDtQ=,tag:tQ8MQ1NWey4E1dFu2YnOQw==,type:str]
lastmodified: "2024-03-20T00:39:56Z"
mac: ENC[AES256_GCM,data:JY2ttbttavS4RqYEFf95BkiPrK1r4r6hXnoQMCqtoQmBhSbUF9X5gvxZuqBFF7as9KgwiWHXFJ6S0FccmDDcBA/QoGxI4IJoR4nEIp7Y/YHTY6Ni0vZfO27yAtGmnViXadOeVyFChVN6GjRnxLp/FaBdXxtjSH8x4sQqf/2VWCw=,iv:/pnWEOcclzw0xcpL6lwErJLOBE9tBk1pOZZe3ew20TM=,tag:WNWiSG7Buve8YxJb1XxFwg==,type:str]
pgp:
- created_at: "2024-03-19T02:39:12Z"
enc: |-

View file

@ -11,6 +11,7 @@
tei = access.nixosFor "tei";
inherit (mediabox.services) plex;
inherit (keycloak.services) vouch-proxy;
inherit (tei.services) home-assistant;
inherit (config.services) nginx tailscale;
in {
imports = let
@ -31,11 +32,13 @@ in {
nixos.access.nginx
nixos.access.global
nixos.access.gensokyo
nixos.access.keycloak
nixos.access.vouch
nixos.access.freeipa
nixos.access.freepbx
nixos.access.unifi
nixos.access.kitchencam
nixos.access.home-assistant
nixos.access.proxmox
nixos.access.plex
nixos.access.invidious
@ -61,14 +64,31 @@ in {
};
security.acme.certs = let
inherit (nginx) access;
inherit (nginx) access virtualHosts;
in {
${access.vouch.localDomain} = {
keycloak = {
inherit (nginx) group;
domain = virtualHosts.keycloak.serverName;
extraDomainNames = mkMerge [
(mkIf tailscale.enable [
access.vouch.tailDomain
])
virtualHosts.keycloak.serverAliases
virtualHosts.keycloak'local.allServerNames
];
};
home-assistant = {
inherit (nginx) group;
domain = virtualHosts.home-assistant.serverName;
extraDomainNames = mkMerge [
virtualHosts.home-assistant.serverAliases
virtualHosts.home-assistant'local.allServerNames
];
};
vouch = {
inherit (nginx) group;
domain = virtualHosts.vouch.serverName;
extraDomainNames = mkMerge [
virtualHosts.vouch.serverAliases
virtualHosts.vouch'local.allServerNames
(mkIf tailscale.enable virtualHosts.vouch'tail.allServerNames)
];
};
${access.unifi.domain} = {
@ -116,19 +136,20 @@ in {
])
];
};
${access.plex.domain} = {
inherit (nginx) group;
extraDomainNames = [access.plex.localDomain];
};
${access.kitchencam.domain} = {
plex = {
inherit (nginx) group;
domain = virtualHosts.plex.serverName;
extraDomainNames = mkMerge [
[
access.kitchencam.localDomain
]
(mkIf tailscale.enable [
access.kitchencam.tailDomain
])
virtualHosts.plex.serverAliases
virtualHosts.plex'local.allServerNames
];
};
kitchencam = {
inherit (nginx) group;
domain = virtualHosts.kitchencam.serverName;
extraDomainNames = mkMerge [
virtualHosts.kitchencam.serverAliases
virtualHosts.kitchencam'local.allServerNames
];
};
${access.invidious.domain} = {
@ -153,7 +174,6 @@ in {
};
access.vouch = assert vouch-proxy.enable; {
url = "http://${keycloak.lib.access.hostnameForNetwork.local}:${toString vouch-proxy.settings.vouch.port}";
useACMEHost = access.vouch.localDomain;
};
access.unifi = {
host = tei.lib.access.hostnameForNetwork.local;
@ -169,24 +189,40 @@ in {
};
access.kitchencam = {
streamPort = 41081;
useACMEHost = access.kitchencam.domain;
};
access.invidious = {
url = "http://${mediabox.lib.access.hostnameForNetwork.local}:${toString mediabox.services.invidious.port}";
};
virtualHosts = {
gensokyoZone.proxied.enable = "cloudflared";
keycloak = {
# we're not the real sso record-holder, so don't respond globally..
local.denyGlobal = true;
ssl.cert.name = "keycloak";
};
keycloak'local.ssl.cert.name = "keycloak";
vouch.ssl.cert.name = "vouch";
vouch'local.ssl.cert.name = "vouch";
vouch'tail = mkIf tailscale.enable {
ssl.cert.name = "vouch";
};
home-assistant = {
# not the real hass record-holder, so don't respond globally..
local.denyGlobal = true;
ssl.cert.name = "home-assistant";
locations."/".proxyPass = "http://${tei.lib.access.hostnameForNetwork.tail}:${toString home-assistant.config.http.server_port}";
};
home-assistant'local.ssl.cert.name = "home-assistant";
${access.freepbx.domain} = {
local.enable = true;
};
${access.proxmox.domain} = {
useACMEHost = access.proxmox.domain;
};
${access.plex.domain} = {
addSSL = true;
useACMEHost = access.plex.domain;
};
${access.kitchencam.domain} = {
};
plex.ssl.cert.name = "plex";
plex'local.ssl.cert.name = "plex";
kitchencam.ssl.cert.name = "kitchencam";
kitchencam'local.ssl.cert.name = "kitchencam";
${access.invidious.domain} = {
useACMEHost = access.invidious.domain;
forceSSL = true;

View file

@ -17,18 +17,20 @@
tunnels.${tunnelId} = {
default = "http_status:404";
credentialsFile = config.sops.secrets.cloudflared-tunnel-keycloak.path;
ingress = {
${keycloak.settings.hostname} = assert keycloak.enable; let
scheme = if keycloak.sslCertificate != null then "https" else "http";
port = keycloak.settings."${scheme}-port";
in {
service = "${scheme}://localhost:${toString port}";
originRequest.${if scheme == "https" then "noTLSVerify" else null} = true;
ingress = let
keycloakHost = if keycloak.settings.hostname != null then keycloak.settings.hostname else "sso.${config.networking.domain}";
keyCloakScheme = if keycloak.sslCertificate != null then "https" else "http";
keycloakPort = keycloak.settings."${keyCloakScheme}-port";
in {
${keycloakHost} = assert keycloak.enable; {
service = "${keyCloakScheme}://localhost:${toString keycloakPort}";
originRequest.${if keyCloakScheme == "https" then "noTLSVerify" else null} = true;
};
${vouch-proxy.domain}.service = assert vouch-proxy.enable; "http://localhost:${toString vouch-proxy.settings.vouch.port}";
};
};
};
sops.secrets.cloudflared-tunnel-keycloak = {
owner = config.services.cloudflared.user;
};

View file

@ -153,9 +153,9 @@ chmod 0755 /rpool/caches/plex/tautulli/cache
mkshared hass 100286 100286 0700
mkshared kanidm 100994 100993 0700
mkshared mosquitto 100246 100246 0700
mkshared plex 100193 100193 0755
mkshared plex 100193 100193 0750
mkshared postgresql 100071 100071 0750
mkshared unifi 100990 100990 0755
mkshared unifi 100990 100990 0750
mkshared zigbee2mqtt 100317 100317 0700
ln -sf /lib/systemd/system/auth-rpcgss-module.service /etc/systemd/system/

View file

@ -17,7 +17,6 @@ in {
nixos.postgres
nixos.nginx
nixos.access.zigbee2mqtt
nixos.access.home-assistant
nixos.access.unifi
nixos.unifi
nixos.mosquitto
@ -27,13 +26,9 @@ in {
./cloudflared.nix
];
services.nginx = let
inherit (config.services.nginx) access;
in {
services.nginx = {
virtualHosts = {
${access.zigbee2mqtt.domain} = {
local.denyGlobal = true;
};
zigbee2mqtt.proxied.enable = "cloudflared";
};
};

View file

@ -19,12 +19,14 @@ module "hakurei_system_records" {
"prox",
"id",
"login",
"sso",
"ldap",
"freeipa",
"unifi",
"pbx",
"smb",
"kitchen",
"home",
"yt",
]
global_subdomains = [
@ -87,7 +89,6 @@ module "tewi_system_records" {
local_subdomains = [
"mqtt",
"z2m",
"home",
"postgresql",
]
}