mirror of
https://github.com/Swarsel/.dotfiles.git
synced 2026-04-14 21:29:12 +02:00
407 lines
15 KiB
Nix
407 lines
15 KiB
Nix
{ self, lib, pkgs, config, globals, confLib, dns, nodes, ... }:
|
|
let
|
|
inherit (confLib.gen { name = "firezone"; dir = "/var/lib/private/firezone"; }) serviceName serviceDir serviceAddress serviceDomain proxyAddress4 proxyAddress6;
|
|
inherit (confLib.static) isHome isProxied homeProxy webProxy homeWebProxy homeProxyIf webProxyIf idmServer dnsServer homeServiceAddress nginxAccessRules;
|
|
inherit (config.swarselsystems) sopsFile;
|
|
|
|
apiPort = 8081;
|
|
webPort = 8080;
|
|
relayPort = 3478;
|
|
domainPort = 9003;
|
|
|
|
homeServices = lib.attrNames (lib.filterAttrs (_: serviceCfg: serviceCfg.isHome) globals.services);
|
|
homeDomains = map (name: globals.services.${name}.domain) homeServices;
|
|
allow = group: resource: {
|
|
"${group}@${resource}" = {
|
|
inherit group resource;
|
|
description = "Allow ${group} access to ${resource}";
|
|
};
|
|
};
|
|
in
|
|
{
|
|
options = {
|
|
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
|
|
};
|
|
config = lib.mkIf config.swarselmodules.server.${serviceName} {
|
|
|
|
globals = {
|
|
networks = {
|
|
${webProxyIf}.hosts = lib.mkIf isProxied {
|
|
${config.node.name}.firewallRuleForNode.${webProxy} = {
|
|
allowedTCPPorts = [ apiPort webPort domainPort ];
|
|
allowedUDPPorts = [ relayPort ];
|
|
allowedUDPPortRanges = [
|
|
{
|
|
from = config.services.firezone.relay.lowestPort;
|
|
to = config.services.firezone.relay.highestPort;
|
|
}
|
|
];
|
|
};
|
|
};
|
|
${homeProxyIf}.hosts = lib.mkIf isHome {
|
|
${config.node.name}.firewallRuleForNode.${homeWebProxy} = {
|
|
allowedTCPPorts = [ apiPort webPort domainPort ];
|
|
allowedUDPPorts = [ relayPort ];
|
|
allowedUDPPortRanges = [
|
|
{
|
|
from = config.services.firezone.relay.lowestPort;
|
|
to = config.services.firezone.relay.highestPort;
|
|
}
|
|
];
|
|
};
|
|
};
|
|
};
|
|
services.${serviceName} = {
|
|
domain = serviceDomain;
|
|
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
|
|
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
|
|
};
|
|
};
|
|
|
|
topology.self.services.${serviceName} = {
|
|
name = lib.swarselsystems.toCapitalized serviceName;
|
|
info = "https://${serviceDomain}";
|
|
icon = "${self}/files/topology-images/${serviceName}.png";
|
|
};
|
|
|
|
sops = {
|
|
secrets = {
|
|
kanidm-firezone-client = { inherit sopsFile; mode = "0400"; };
|
|
firezone-relay-token = { inherit sopsFile; mode = "0400"; };
|
|
firezone-smtp-password = { inherit sopsFile; mode = "0440"; };
|
|
firezone-adapter-config = { inherit sopsFile; mode = "0440"; };
|
|
};
|
|
};
|
|
|
|
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
|
|
{ directory = serviceDir; mode = "0700"; }
|
|
];
|
|
|
|
services.firezone = {
|
|
server = {
|
|
enable = true;
|
|
enableLocalDB = true;
|
|
|
|
smtp = {
|
|
inherit (config.repo.secrets.local.firezone.mail) from username;
|
|
host = globals.services.mailserver.domain;
|
|
port = 465;
|
|
implicitTls = true;
|
|
passwordFile = config.sops.secrets.firezone-smtp-password.path;
|
|
};
|
|
|
|
provision = {
|
|
enable = true;
|
|
accounts.main = {
|
|
name = "Home";
|
|
relayGroups.relays.name = "Relays";
|
|
gatewayGroups.home.name = "Home";
|
|
actors.admin = {
|
|
type = "account_admin_user";
|
|
name = "Admin";
|
|
email = "admin@${globals.domains.main}";
|
|
};
|
|
groups.anyone = {
|
|
name = "anyone";
|
|
members = [
|
|
"admin"
|
|
];
|
|
};
|
|
|
|
auth.oidc =
|
|
let
|
|
client_id = "firezone";
|
|
in
|
|
{
|
|
name = "Kanidm";
|
|
adapter = "openid_connect";
|
|
adapter_config = {
|
|
scope = "openid email profile";
|
|
response_type = "code";
|
|
inherit client_id;
|
|
discovery_document_uri = "https://${globals.services.kanidm.domain}/oauth2/openid/${client_id}/.well-known/openid-configuration";
|
|
clientSecretFile = config.sops.secrets.kanidm-firezone-client.path;
|
|
};
|
|
};
|
|
|
|
resources =
|
|
lib.genAttrs homeDomains
|
|
(domain: {
|
|
type = "dns";
|
|
name = domain;
|
|
address = domain;
|
|
gatewayGroups = [ "home" ];
|
|
filters = [
|
|
{ protocol = "icmp"; }
|
|
{
|
|
protocol = "tcp";
|
|
ports = [
|
|
443
|
|
80
|
|
];
|
|
}
|
|
{
|
|
protocol = "udp";
|
|
ports = [ 443 ];
|
|
}
|
|
];
|
|
})
|
|
// {
|
|
"home.vlan-services.v4" = {
|
|
type = "cidr";
|
|
name = "home.vlan-services.v4";
|
|
address = globals.networks.home-lan.vlans.services.cidrv4;
|
|
gatewayGroups = [ "home" ];
|
|
};
|
|
"home.vlan-services.v6" = {
|
|
type = "cidr";
|
|
name = "home.vlan-services.v6";
|
|
address = globals.networks.home-lan.vlans.services.cidrv6;
|
|
gatewayGroups = [ "home" ];
|
|
};
|
|
};
|
|
|
|
policies =
|
|
{ }
|
|
// allow "everyone" "home.vlan-services.v4"
|
|
// allow "anyone" "home.vlan-services.v4"
|
|
// allow "everyone" "home.vlan-services.v6"
|
|
// allow "anyone" "home.vlan-services.v6"
|
|
// lib.mergeAttrsList (map (domain: allow "everyone" domain) homeDomains)
|
|
// lib.mergeAttrsList (map (domain: allow "anyone" domain) homeDomains);
|
|
};
|
|
};
|
|
|
|
domain = {
|
|
settings.ERLANG_DISTRIBUTION_PORT = domainPort;
|
|
package = pkgs.firezone-server-domain;
|
|
};
|
|
api = {
|
|
externalUrl = "https://${serviceDomain}/api/";
|
|
address = "0.0.0.0";
|
|
port = apiPort;
|
|
package = pkgs.firezone-server-api;
|
|
};
|
|
web = {
|
|
externalUrl = "https://${serviceDomain}/";
|
|
address = "0.0.0.0";
|
|
port = webPort;
|
|
package = pkgs.firezone-server-web;
|
|
};
|
|
};
|
|
|
|
relay = {
|
|
enable = true;
|
|
port = relayPort;
|
|
inherit (config.node) name;
|
|
apiUrl = "wss://${serviceDomain}/api/";
|
|
tokenFile = config.sops.secrets.firezone-relay-token.path;
|
|
publicIpv4 = proxyAddress4;
|
|
publicIpv6 = proxyAddress6;
|
|
openFirewall = lib.mkIf (!isProxied) true;
|
|
package = pkgs.firezone-relay;
|
|
};
|
|
};
|
|
# systemd.services.firezone-initialize =
|
|
# let
|
|
# generateSecrets =
|
|
# let
|
|
# requiredSecrets = lib.filterAttrs (_: v: v == null) cfg.settingsSecret;
|
|
# in
|
|
# ''
|
|
# mkdir -p secrets
|
|
# chmod 700 secrets
|
|
# ''
|
|
# + lib.concatLines (
|
|
# lib.forEach (builtins.attrNames requiredSecrets) (secret: ''
|
|
# if [[ ! -e secrets/${secret} ]]; then
|
|
# echo "Generating ${secret}"
|
|
# # Some secrets like TOKENS_KEY_BASE require a value >=64 bytes.
|
|
# head -c 64 /dev/urandom | base64 -w 0 > secrets/${secret}
|
|
# chmod 600 secrets/${secret}
|
|
# fi
|
|
# '')
|
|
# );
|
|
# loadSecretEnvironment =
|
|
# component:
|
|
# let
|
|
# relevantSecrets = lib.subtractLists (builtins.attrNames cfg.${component}.settings) (
|
|
# builtins.attrNames cfg.settingsSecret
|
|
# );
|
|
# in
|
|
# lib.concatLines (
|
|
# lib.forEach relevantSecrets (
|
|
# secret:
|
|
# ''export ${secret}=$(< ${
|
|
# if cfg.settingsSecret.${secret} == null then
|
|
# "secrets/${secret}"
|
|
# else
|
|
# "\"$CREDENTIALS_DIRECTORY/${secret}\""
|
|
# })''
|
|
# )
|
|
# );
|
|
# in
|
|
# {
|
|
# script = lib.mkForce ''
|
|
# mkdir -p "$TZDATA_DIR"
|
|
|
|
# # Generate and load secrets
|
|
# ${generateSecrets}
|
|
# ${loadSecretEnvironment "domain"}
|
|
|
|
# echo "Running migrations"
|
|
# ${lib.getExe cfg.domain.package} eval "Domain.Release.migrate(manual: true)"
|
|
# '';
|
|
# };
|
|
|
|
|
|
nodes =
|
|
let
|
|
genNginx = toAddress: extraConfig: {
|
|
upstreams = {
|
|
${serviceName} = {
|
|
servers."${toAddress}:${builtins.toString webPort}" = { };
|
|
};
|
|
"${serviceName}-api" = {
|
|
servers."${toAddress}:${builtins.toString apiPort}" = { };
|
|
};
|
|
};
|
|
virtualHosts = {
|
|
${serviceDomain} = {
|
|
useACMEHost = globals.domains.main;
|
|
forceSSL = true;
|
|
acmeRoot = null;
|
|
inherit extraConfig;
|
|
locations = {
|
|
"/" = {
|
|
# The trailing slash is important to strip the location prefix from the request
|
|
proxyPass = "http://${serviceName}/";
|
|
proxyWebsockets = true;
|
|
};
|
|
"/api/" = {
|
|
# The trailing slash is important to strip the location prefix from the request
|
|
proxyPass = "http://${serviceName}-api/";
|
|
proxyWebsockets = true;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
in
|
|
{
|
|
${homeProxy} =
|
|
let
|
|
nodeCfg = nodes.${homeProxy}.config;
|
|
nodePkgs = nodes.${homeProxy}.pkgs;
|
|
in
|
|
{
|
|
sops.secrets.firezone-gateway-token = { inherit (nodeCfg.swarselsystems) sopsFile; mode = "0400"; };
|
|
networking.nftables = {
|
|
firewall = {
|
|
zones.firezone.interfaces = [ "tun-firezone" ];
|
|
rules = {
|
|
# masquerade firezone traffic
|
|
masquerade-firezone = {
|
|
from = [ "firezone" ];
|
|
to = [ "vlan-services" ];
|
|
# masquerade = true; NOTE: custom rule below for ip4 + ip6
|
|
late = true; # Only accept after any rejects have been processed
|
|
verdict = "accept";
|
|
};
|
|
# forward firezone traffic
|
|
forward-incoming-firezone-traffic = {
|
|
from = [ "firezone" ];
|
|
to = [ "vlan-services" ];
|
|
verdict = "accept";
|
|
};
|
|
|
|
# FIXME: is this needed? conntrack should take care of it and we want to masquerade anyway
|
|
forward-outgoing-firezone-traffic = {
|
|
from = [ "vlan-services" ];
|
|
to = [ "firezone" ];
|
|
verdict = "accept";
|
|
};
|
|
};
|
|
};
|
|
chains.postrouting = {
|
|
masquerade-firezone = {
|
|
after = [ "hook" ];
|
|
late = true;
|
|
rules =
|
|
lib.forEach
|
|
[
|
|
"firezone"
|
|
]
|
|
(
|
|
zone:
|
|
lib.concatStringsSep " " [
|
|
"meta protocol { ip, ip6 }"
|
|
(lib.head nodeCfg.networking.nftables.firewall.zones.${zone}.ingressExpression)
|
|
(lib.head nodeCfg.networking.nftables.firewall.zones.vlan-services.egressExpression)
|
|
"masquerade random"
|
|
]
|
|
);
|
|
};
|
|
};
|
|
};
|
|
|
|
environment.persistence."/persist".directories = lib.mkIf nodeCfg.swarselsystems.isImpermanence [
|
|
{ directory = "${serviceDir}-gateway"; mode = "0700"; }
|
|
];
|
|
|
|
boot.kernel.sysctl = {
|
|
"net.core.wmem_max" = 16777216;
|
|
"net.core.rmem_max" = 134217728;
|
|
};
|
|
services.firezone.gateway = {
|
|
enable = true;
|
|
# logLevel = "trace";
|
|
inherit (nodeCfg.node) name;
|
|
apiUrl = "wss://${globals.services.firezone.domain}/api/";
|
|
tokenFile = nodeCfg.sops.secrets.firezone-gateway-token.path;
|
|
package = nodePkgs.stable25_05.firezone-gateway; # newer versions of firezone-gateway are not compatible with server package
|
|
};
|
|
|
|
topology.self.services."${serviceName}-gateway" = {
|
|
name = lib.swarselsystems.toCapitalized "${serviceName} Gateway";
|
|
icon = "${self}/files/topology-images/${serviceName}.png";
|
|
};
|
|
};
|
|
${idmServer} =
|
|
let
|
|
nodeCfg = nodes.${idmServer}.config;
|
|
accountId = "3e996ad9-c100-40e8-807a-282a5c5e8b6c";
|
|
externalId = "31e7f702-28a7-4bbc-9690-b6db9d4a162a";
|
|
in
|
|
{
|
|
sops.secrets.kanidm-firezone = { inherit (nodeCfg.swarselsystems) sopsFile; owner = "kanidm"; group = "kanidm"; mode = "0440"; };
|
|
services.kanidm.provision = {
|
|
groups."firezone.access" = { };
|
|
systems.oauth2.firezone = {
|
|
displayName = "Firezone VPN";
|
|
# NOTE: state: both uuids are runtime values
|
|
originUrl = [
|
|
"https://${globals.services.firezone.domain}/${accountId}/sign_in/providers/${externalId}/handle_callback"
|
|
"https://${globals.services.firezone.domain}/${accountId}/settings/identity_providers/openid_connect/${externalId}/handle_callback"
|
|
];
|
|
originLanding = "https://${globals.services.firezone.domain}/";
|
|
basicSecretFile = nodeCfg.sops.secrets.kanidm-firezone.path;
|
|
preferShortUsername = true;
|
|
scopeMaps."firezone.access" = [
|
|
"openid"
|
|
"email"
|
|
"profile"
|
|
];
|
|
};
|
|
|
|
};
|
|
};
|
|
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
|
|
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
|
|
};
|
|
${webProxy}.services.nginx = genNginx serviceAddress "";
|
|
${homeWebProxy}.services.nginx = lib.mkIf isHome (genNginx homeServiceAddress nginxAccessRules);
|
|
};
|
|
|
|
};
|
|
}
|