feat(reverse-proxy): add Pikachu reverse proxy guest
This commit was merged in pull request #47.
This commit is contained in:
86
flake.nix
86
flake.nix
@@ -117,6 +117,92 @@
|
|||||||
${lib.optionalString (!serviceIdentityTestsPass) "exit 1"}
|
${lib.optionalString (!serviceIdentityTestsPass) "exit 1"}
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
reverse-proxy-basic-route =
|
||||||
|
let
|
||||||
|
testConfig =
|
||||||
|
(lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs.my = self.lib;
|
||||||
|
modules = [
|
||||||
|
./modules/reverse-proxy.nix
|
||||||
|
{
|
||||||
|
modules.reverse-proxy = {
|
||||||
|
enable = true;
|
||||||
|
routes."budget.alexion.dev" = {
|
||||||
|
path = "/";
|
||||||
|
backend = "http://10.23.20.42:5006";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}).config;
|
||||||
|
vhost = testConfig.services.nginx.virtualHosts."budget.alexion.dev";
|
||||||
|
in
|
||||||
|
pkgs.runCommand "reverse-proxy-basic-route-check" { } ''
|
||||||
|
${
|
||||||
|
lib.optionalString (
|
||||||
|
!(vhost.forceSSL && vhost.enableACME && vhost.locations."/".proxyPass == "http://10.23.20.42:5006")
|
||||||
|
) "exit 1"
|
||||||
|
}
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
reverse-proxy-actualbudget-guest-route =
|
||||||
|
let
|
||||||
|
testConfig =
|
||||||
|
(self.nixosConfigurations.pikachu.extendModules {
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
guests.actualbudget.reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
host = "budget.alexion.dev";
|
||||||
|
backend = "http://10.23.20.42:5006";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}).config;
|
||||||
|
route = testConfig.modules.reverse-proxy.routes.actualbudget;
|
||||||
|
in
|
||||||
|
pkgs.runCommand "reverse-proxy-actualbudget-guest-route-check" { } ''
|
||||||
|
${
|
||||||
|
lib.optionalString (
|
||||||
|
!(route.host == "budget.alexion.dev" && route.backend == "http://10.23.20.42:5006")
|
||||||
|
) "exit 1"
|
||||||
|
}
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
reverse-proxy-secondary-path-requires-root =
|
||||||
|
let
|
||||||
|
testConfig =
|
||||||
|
(lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs.my = self.lib;
|
||||||
|
modules = [
|
||||||
|
./modules/reverse-proxy.nix
|
||||||
|
{
|
||||||
|
modules.reverse-proxy = {
|
||||||
|
enable = true;
|
||||||
|
routes.raichu-files = {
|
||||||
|
host = "files.alexion.dev";
|
||||||
|
path = "/raichu";
|
||||||
|
backend = "http://10.23.20.10:3923";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}).config;
|
||||||
|
hasFailedRootAssertion = lib.any (
|
||||||
|
assertion:
|
||||||
|
assertion.assertion == false
|
||||||
|
&& assertion.message == "modules.reverse-proxy.routes for files.alexion.dev must include a primary / route."
|
||||||
|
) testConfig.assertions;
|
||||||
|
in
|
||||||
|
pkgs.runCommand "reverse-proxy-secondary-path-requires-root-check" { } ''
|
||||||
|
${lib.optionalString (!hasFailedRootAssertion) "exit 1"}
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
51
guests/reverse-proxy.nix
Normal file
51
guests/reverse-proxy.nix
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
my,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
legacyBackend = "http://10.23.20.32";
|
||||||
|
legacyHosts = [
|
||||||
|
"games.alexion.dev"
|
||||||
|
"git.alexion.dev"
|
||||||
|
"music.alexion.dev"
|
||||||
|
"photos.alexion.dev"
|
||||||
|
"plex.alexion.dev"
|
||||||
|
"servers.alexion.dev"
|
||||||
|
"todo.alexion.dev"
|
||||||
|
"wings.alexion.dev"
|
||||||
|
];
|
||||||
|
legacyRoutes = lib.listToAttrs (
|
||||||
|
map (host: {
|
||||||
|
name = "legacy-${lib.replaceStrings [ "." ] [ "-" ] host}";
|
||||||
|
value = {
|
||||||
|
inherit host;
|
||||||
|
backend = legacyBackend;
|
||||||
|
};
|
||||||
|
}) legacyHosts
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(my.guest {
|
||||||
|
name = "reverse-proxy";
|
||||||
|
interior = {
|
||||||
|
modules.reverse-proxy = {
|
||||||
|
enable = true;
|
||||||
|
routes = config.modules.reverse-proxy.routes // legacyRoutes;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
config = lib.mkIf config.guests.reverse-proxy.enable {
|
||||||
|
guests.reverse-proxy = {
|
||||||
|
limits = {
|
||||||
|
memory = lib.mkDefault "1G";
|
||||||
|
cpu = lib.mkDefault "100%";
|
||||||
|
tasksMax = lib.mkDefault 256;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -79,6 +79,17 @@
|
|||||||
vlan = 20;
|
vlan = 20;
|
||||||
mac = "BC:24:11:C1:CD:28";
|
mac = "BC:24:11:C1:CD:28";
|
||||||
dataPath = "/pikachu/data/actualbudget";
|
dataPath = "/pikachu/data/actualbudget";
|
||||||
|
reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
host = "budget.alexion.dev";
|
||||||
|
backend = "http://10.23.20.42:5006";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
guests.reverse-proxy = {
|
||||||
|
enable = true;
|
||||||
|
vlan = 20;
|
||||||
|
mac = "02:98:64:5a:07:4f";
|
||||||
};
|
};
|
||||||
|
|
||||||
guests.copyparty = {
|
guests.copyparty = {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ let
|
|||||||
;
|
;
|
||||||
|
|
||||||
serviceIdentityRegistry = import ./service-identities.nix { inherit lib; };
|
serviceIdentityRegistry = import ./service-identities.nix { inherit lib; };
|
||||||
|
reverseProxyRoutes = import ./reverse-proxy-routes.nix { inherit lib; };
|
||||||
|
|
||||||
collectNixFiles =
|
collectNixFiles =
|
||||||
dir:
|
dir:
|
||||||
flatten (
|
flatten (
|
||||||
@@ -44,6 +46,7 @@ let
|
|||||||
self
|
self
|
||||||
specialArgs
|
specialArgs
|
||||||
bridgeName
|
bridgeName
|
||||||
|
reverseProxyRoutes
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,6 +84,7 @@ in
|
|||||||
mkHost
|
mkHost
|
||||||
mkHosts
|
mkHosts
|
||||||
bridgeName
|
bridgeName
|
||||||
|
reverseProxyRoutes
|
||||||
;
|
;
|
||||||
|
|
||||||
inherit (guests) deriveMac guest;
|
inherit (guests) deriveMac guest;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
self,
|
self,
|
||||||
specialArgs,
|
specialArgs,
|
||||||
bridgeName,
|
bridgeName,
|
||||||
|
reverseProxyRoutes,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib) escapeShellArg mapAttrsToList;
|
inherit (lib) escapeShellArg mapAttrsToList;
|
||||||
@@ -130,6 +131,10 @@ let
|
|||||||
guest takes its address by DHCP, keeping IP management at the router.
|
guest takes its address by DHCP, keeping IP management at the router.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
reverseProxy = reverseProxyRoutes.mkRouteOptions {
|
||||||
|
subject = "guest route";
|
||||||
|
enableDefault = false;
|
||||||
|
};
|
||||||
mounts = lib.mkOption {
|
mounts = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf (
|
||||||
lib.types.submodule {
|
lib.types.submodule {
|
||||||
@@ -273,6 +278,10 @@ let
|
|||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
sops.secrets = lib.genAttrs cfg.secrets (_: { });
|
sops.secrets = lib.genAttrs cfg.secrets (_: { });
|
||||||
|
|
||||||
|
modules.reverse-proxy.routes.${machineName} = lib.mkIf cfg.reverseProxy.enable (
|
||||||
|
reverseProxyRoutes.routeFields cfg.reverseProxy
|
||||||
|
);
|
||||||
|
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = mountCollisions == [ ];
|
assertion = mountCollisions == [ ];
|
||||||
|
|||||||
77
lib/reverse-proxy-routes.nix
Normal file
77
lib/reverse-proxy-routes.nix
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{ lib }:
|
||||||
|
let
|
||||||
|
mkRouteOptions =
|
||||||
|
{
|
||||||
|
name ? null,
|
||||||
|
subject ? "route",
|
||||||
|
enableDefault ? true,
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = enableDefault;
|
||||||
|
description = "Whether this reverse-proxy ${subject} is active.";
|
||||||
|
};
|
||||||
|
|
||||||
|
host = lib.mkOption (
|
||||||
|
{
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Primary hostname served by this reverse-proxy ${subject}.";
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (name != null) {
|
||||||
|
default = name;
|
||||||
|
defaultText = lib.literalMD "the route attribute name";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
aliases = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Additional hostnames served by this reverse-proxy ${subject}.";
|
||||||
|
};
|
||||||
|
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/";
|
||||||
|
description = "URL path prefix proxied by this reverse-proxy ${subject}.";
|
||||||
|
};
|
||||||
|
|
||||||
|
backend = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Full HTTP backend URL that receives proxied traffic.";
|
||||||
|
};
|
||||||
|
|
||||||
|
tls = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to serve this reverse-proxy ${subject} with ACME-backed TLS.";
|
||||||
|
};
|
||||||
|
|
||||||
|
websockets = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to enable websocket proxy headers for this reverse-proxy ${subject}.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
routeFields = route: {
|
||||||
|
inherit (route)
|
||||||
|
host
|
||||||
|
aliases
|
||||||
|
path
|
||||||
|
backend
|
||||||
|
tls
|
||||||
|
websockets
|
||||||
|
;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit mkRouteOptions routeFields;
|
||||||
|
|
||||||
|
routeType = lib.types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
|
options = mkRouteOptions { inherit name; };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
82
modules/reverse-proxy.nix
Normal file
82
modules/reverse-proxy.nix
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{ config, lib, my, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.modules.reverse-proxy;
|
||||||
|
inherit (my) reverseProxyRoutes;
|
||||||
|
|
||||||
|
enabledRoutes = lib.filterAttrs (_: route: route.enable) cfg.routes;
|
||||||
|
routesByHost = lib.groupBy (route: route.host) (lib.attrValues enabledRoutes);
|
||||||
|
|
||||||
|
rootRoutesByHost = lib.mapAttrs (
|
||||||
|
host: routes:
|
||||||
|
let
|
||||||
|
roots = builtins.filter (route: route.path == "/") routes;
|
||||||
|
in
|
||||||
|
if roots == [ ] then null else builtins.head roots
|
||||||
|
) routesByHost;
|
||||||
|
|
||||||
|
hasDuplicatePaths = routes:
|
||||||
|
let
|
||||||
|
paths = map (route: route.path) routes;
|
||||||
|
in
|
||||||
|
lib.length paths != lib.length (lib.unique paths);
|
||||||
|
|
||||||
|
mkLocation = route: {
|
||||||
|
proxyPass = route.backend;
|
||||||
|
proxyWebsockets = route.websockets;
|
||||||
|
};
|
||||||
|
|
||||||
|
mkVirtualHost = host: routes:
|
||||||
|
let
|
||||||
|
rootRoute = rootRoutesByHost.${host};
|
||||||
|
in
|
||||||
|
lib.mkIf (rootRoute != null) {
|
||||||
|
forceSSL = rootRoute.tls;
|
||||||
|
enableACME = rootRoute.tls;
|
||||||
|
serverAliases = rootRoute.aliases;
|
||||||
|
locations = lib.listToAttrs (
|
||||||
|
map (route: lib.nameValuePair route.path (mkLocation route)) routes
|
||||||
|
);
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.reverse-proxy = {
|
||||||
|
enable = lib.mkEnableOption "the Nginx reverse proxy route realizer";
|
||||||
|
|
||||||
|
routes = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf reverseProxyRoutes.routeType;
|
||||||
|
default = { };
|
||||||
|
description = "Reverse-proxy route declarations keyed by stable route name.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions =
|
||||||
|
lib.mapAttrsToList (host: rootRoute: {
|
||||||
|
assertion = rootRoute != null;
|
||||||
|
message = "modules.reverse-proxy.routes for ${host} must include a primary / route.";
|
||||||
|
}) rootRoutesByHost
|
||||||
|
++ lib.mapAttrsToList (host: routes: {
|
||||||
|
assertion = !hasDuplicatePaths routes;
|
||||||
|
message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once.";
|
||||||
|
}) routesByHost;
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
|
||||||
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults.email = "contact@alexion.dev";
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
recommendedGzipSettings = true;
|
||||||
|
recommendedOptimisation = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
recommendedTlsSettings = true;
|
||||||
|
virtualHosts = lib.mapAttrs mkVirtualHost routesByHost;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ in
|
|||||||
default = "storage";
|
default = "storage";
|
||||||
description = "Group account that owns Actual Budget's writable state.";
|
description = "Group account that owns Actual Budget's writable state.";
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|||||||
Reference in New Issue
Block a user