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";
'';