nixos/pangolin: init (#416148)
This commit is contained in:
@@ -72,6 +72,8 @@
|
||||
|
||||
- [Draupnir](https://github.com/the-draupnir-project/draupnir), a Matrix moderation bot. Available as [services.draupnir](#opt-services.draupnir.enable).
|
||||
|
||||
- [Pangolin](https://github.com/fosrl/pangolin), a tunneled reverse proxy server with access control. Available as [services.pangolin](#opt-services.pangolin.enable).
|
||||
|
||||
- [postfix-tlspol](https://github.com/Zuplu/postfix-tlspol), MTA-STS and DANE resolver and TLS policy server for Postfix. Available as [services.postfix-tlspol](#opt-services.postfix-tlspol.enable).
|
||||
|
||||
- [crowdsec](https://www.crowdsec.net/), a free, open-source and collaborative IPS. Available as [services.crowdsec](#opt-services.crowdsec.enable).
|
||||
|
||||
@@ -1293,6 +1293,7 @@
|
||||
./services/networking/openvpn.nix
|
||||
./services/networking/ostinato.nix
|
||||
./services/networking/owamp.nix
|
||||
./services/networking/pangolin.nix
|
||||
./services/networking/pdns-recursor.nix
|
||||
./services/networking/pdnsd.nix
|
||||
./services/networking/peroxide.nix
|
||||
|
||||
555
nixos/modules/services/networking/pangolin.nix
Normal file
555
nixos/modules/services/networking/pangolin.nix
Normal file
@@ -0,0 +1,555 @@
|
||||
{
|
||||
utils,
|
||||
config,
|
||||
options,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.pangolin;
|
||||
format = pkgs.formats.yaml { };
|
||||
finalSettings = lib.attrsets.recursiveUpdate pangolinConf cfg.settings;
|
||||
cfgFile = format.generate "config.yml" finalSettings;
|
||||
# override the type to allow for optionality
|
||||
nullOrOpt = t: lib.types.nullOr t // { _optional = true; };
|
||||
|
||||
gerbil-wg0-fix-script = pkgs.writeShellApplication {
|
||||
name = "gerbil-wg0-fix-script";
|
||||
runtimeInputs = with pkgs; [
|
||||
coreutils
|
||||
iproute2
|
||||
];
|
||||
# will not work if the interface is renamed
|
||||
# https://github.com/fosrl/newt/issues/37#issuecomment-3193385911
|
||||
text = ''
|
||||
if [ ! -f /var/lib/pangolin/config/wg0 ]; then
|
||||
until ip l d wg0
|
||||
do
|
||||
sleep 2
|
||||
done
|
||||
touch /var/lib/pangolin/config/wg0
|
||||
systemctl restart gerbil --no-block
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
pangolinConf = {
|
||||
app.dashboard_url = "https://${cfg.dashboardDomain}";
|
||||
domains.domain1 = {
|
||||
base_domain = cfg.baseDomain;
|
||||
prefer_wildcard_cert = false;
|
||||
};
|
||||
server = {
|
||||
external_port = 3000;
|
||||
internal_port = 3001;
|
||||
next_port = 3002;
|
||||
integration_port = 3004;
|
||||
# needs to be set, otherwise this fails silently
|
||||
# see https://github.com/fosrl/newt/issues/37
|
||||
internal_hostname = "localhost";
|
||||
};
|
||||
gerbil.base_endpoint = cfg.dashboardDomain;
|
||||
flags.enable_integration_api = false;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.services = {
|
||||
pangolin = {
|
||||
enable = lib.mkEnableOption "Pangolin reverse proxy server";
|
||||
package = lib.mkPackageOption pkgs "fosrl-pangolin" { };
|
||||
|
||||
settings = lib.mkOption {
|
||||
inherit (format) type;
|
||||
default = { };
|
||||
description = ''
|
||||
Additional attributes to be merged with the configuration options and written to Pangolin's `config.yml` file.
|
||||
'';
|
||||
example = {
|
||||
app = {
|
||||
save_logs = true;
|
||||
};
|
||||
server = {
|
||||
external_port = 3007;
|
||||
internal_port = 3008;
|
||||
};
|
||||
domains.domain1 = {
|
||||
prefer_wildcard_cert = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
openFirewall = lib.mkEnableOption "opening TCP ports 80 and 443, and UDP port 51820 in the firewall for the Pangolin service(s)";
|
||||
|
||||
baseDomain = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = null;
|
||||
description = ''
|
||||
Your base fully qualified domain name (without any subdomains).
|
||||
'';
|
||||
example = "example.com";
|
||||
};
|
||||
|
||||
dashboardDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if (isNull cfg.baseDomain) then "" else "pangolin.${cfg.baseDomain}";
|
||||
defaultText = "pangolin.\${config.services.pangolin.baseDomain}";
|
||||
description = ''
|
||||
The domain where the application will be hosted. This is used for many things, including generating links. You can run Pangolin on a subdomain or root domain. Do not prefix with `http` or `https`.
|
||||
'';
|
||||
example = "auth.example.com";
|
||||
};
|
||||
|
||||
letsEncryptEmail = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = config.security.acme.defaults.email;
|
||||
defaultText = lib.literalExpression "config.security.acme.defaults.email";
|
||||
description = ''
|
||||
An email address for SSL certificate registration with Let's Encrypt. This should be an email you have access to.
|
||||
'';
|
||||
};
|
||||
|
||||
# this assumes that all domains are hosted by the same provider
|
||||
dnsProvider = lib.mkOption {
|
||||
type = nullOrOpt lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The DNS provider Traefik will request wildcard certificates from. See the [Traefik Documentation](https://doc.traefik.io/traefik/https/acme/#providers) for more information.
|
||||
'';
|
||||
};
|
||||
|
||||
# provide path to file to keep secrets out of the nix store
|
||||
environmentFile = lib.mkOption {
|
||||
type = with lib.types; nullOr path;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to a file containing sensitive environment variables for Pangolin. See the [Pangolin Documentation](https://docs.fossorial.io/Pangolin/Configuration/config) for more information.
|
||||
These will overwrite anything defined in the config.
|
||||
The file should contain environment-variable assignments like:
|
||||
```
|
||||
SERVER_SECRET=1234567890abc
|
||||
```
|
||||
'';
|
||||
example = "/etc/nixos/secrets/pangolin.env";
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/var/lib/pangolin";
|
||||
example = "/srv/pangolin";
|
||||
description = "Path to variable state data directory for Pangolin.";
|
||||
};
|
||||
};
|
||||
gerbil = {
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 3003;
|
||||
description = ''
|
||||
Specifies the port to listen on for Gerbil.
|
||||
'';
|
||||
};
|
||||
|
||||
environmentFile = lib.mkOption {
|
||||
type = nullOrOpt lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to a file containing sensitive environment variables for Gerbil. See the [Gerbil Documentation](https://docs.fossorial.io/Pangolin/Configuration/config) for more information.
|
||||
These will overwrite anything defined in the config.
|
||||
'';
|
||||
example = "/etc/nixos/secrets/gerbil.env";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
assertions =
|
||||
(lib.mapAttrsToList (name: value: {
|
||||
# check if the value is optional by looking at the type
|
||||
assertion = (value == null) -> options.services.pangolin."${name}".type._optional or false;
|
||||
message = "services.pangolin.${name} must be provided when Pangolin is enabled.";
|
||||
}) cfg)
|
||||
++ [
|
||||
{
|
||||
# wildcards implies (dnsProvider and traefikEnvironmentFile)
|
||||
assertion =
|
||||
(finalSettings.traefik.prefer_wildcard_cert or finalSettings.domains.domain1.prefer_wildcard_cert)
|
||||
-> (cfg.dnsProvider != "" && config.services.traefik.environmentFiles != [ ]);
|
||||
message = "services.pangolin.dnsProvider and services.traefik.environmentFile must be provided when prefer_wildcard_cert is true.";
|
||||
}
|
||||
];
|
||||
|
||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
];
|
||||
allowedUDPPorts = [ 51820 ];
|
||||
};
|
||||
|
||||
users = {
|
||||
users = {
|
||||
pangolin = {
|
||||
description = "Pangolin service user";
|
||||
group = "fossorial";
|
||||
isSystemUser = true;
|
||||
packages = [ cfg.package ];
|
||||
};
|
||||
gerbil = {
|
||||
description = "Gerbil service user";
|
||||
group = "fossorial";
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
groups.fossorial = {
|
||||
members = [
|
||||
"pangolin"
|
||||
"gerbil"
|
||||
"traefik"
|
||||
];
|
||||
};
|
||||
};
|
||||
# order is as follows
|
||||
# "pangolin.service"
|
||||
# "gerbil.service"
|
||||
# "traefik.service"
|
||||
### TODO:
|
||||
# make tunnels declarative by calling API
|
||||
###
|
||||
systemd = {
|
||||
tmpfiles.settings."10-fossorial-paths" = {
|
||||
"${cfg.dataDir}".d = {
|
||||
user = "pangolin";
|
||||
group = "fossorial";
|
||||
mode = "0770";
|
||||
};
|
||||
"${cfg.dataDir}/config".d = {
|
||||
user = "pangolin";
|
||||
group = "fossorial";
|
||||
mode = "0770";
|
||||
};
|
||||
"${cfg.dataDir}/config/letsencrypt".d = {
|
||||
user = "traefik";
|
||||
group = "fossorial";
|
||||
mode = "0700";
|
||||
};
|
||||
};
|
||||
services = {
|
||||
pangolin = {
|
||||
description = "Pangolin reverse proxy tunneling service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = [ "network.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
preStart = ''
|
||||
mkdir -p ${cfg.dataDir}/config
|
||||
cp -f ${cfgFile} ${cfg.dataDir}/config/config.yml
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = "pangolin";
|
||||
Group = "fossorial";
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
Restart = "always";
|
||||
EnvironmentFile = cfg.environmentFile;
|
||||
# hardening
|
||||
ProtectSystem = "full";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = "disconnected";
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "noaccess";
|
||||
ProtectHostname = true;
|
||||
NoNewPrivileges = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
SocketBindDeny = [
|
||||
"ipv4:tcp"
|
||||
"ipv4:udp"
|
||||
"ipv6:udp"
|
||||
];
|
||||
CapabilityBoundingSet = [
|
||||
"~CAP_BLOCK_SUSPEND"
|
||||
"~CAP_BPF"
|
||||
"~CAP_CHOWN"
|
||||
"~CAP_MKNOD"
|
||||
"~CAP_NET_RAW"
|
||||
"~CAP_PERFMON"
|
||||
"~CAP_SYS_BOOT"
|
||||
"~CAP_SYS_CHROOT"
|
||||
"~CAP_SYS_MODULE"
|
||||
"~CAP_SYS_NICE"
|
||||
"~CAP_SYS_PACCT"
|
||||
"~CAP_SYS_PTRACE"
|
||||
"~CAP_SYS_TIME"
|
||||
"~CAP_SYSLOG"
|
||||
"~CAP_WAKE_ALARM"
|
||||
];
|
||||
SystemCallFilter = [
|
||||
"~@chown:EPERM"
|
||||
"~@clock:EPERM"
|
||||
"~@cpu-emulation:EPERM"
|
||||
"~@debug:EPERM"
|
||||
"~@keyring:EPERM"
|
||||
"~@memlock:EPERM"
|
||||
"~@module:EPERM"
|
||||
"~@mount:EPERM"
|
||||
"~@obsolete:EPERM"
|
||||
"~@pkey:EPERM"
|
||||
"~@privileged:EPERM"
|
||||
"~@raw-io:EPERM"
|
||||
"~@reboot:EPERM"
|
||||
"~@resources:EPERM"
|
||||
"~@sandbox:EPERM"
|
||||
"~@setuid:EPERM"
|
||||
"~@swap:EPERM"
|
||||
"~@timer:EPERM"
|
||||
];
|
||||
ExecStart = lib.getExe cfg.package;
|
||||
};
|
||||
};
|
||||
gerbil = {
|
||||
description = "Gerbil Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "pangolin.service" ];
|
||||
requires = [ "pangolin.service" ];
|
||||
before = [ "traefik.service" ];
|
||||
requiredBy = [ "traefik.service" ];
|
||||
# restarting gerbil restarts traefik
|
||||
upholds = [ "traefik.service" ];
|
||||
|
||||
# provide default to use correct port without envfile
|
||||
environment = {
|
||||
LISTEN = "localhost:" + toString config.services.gerbil.port;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
User = "gerbil";
|
||||
Group = "fossorial";
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
Restart = "always";
|
||||
EnvironmentFile = cfg.environmentFile;
|
||||
ReadWritePaths = "${cfg.dataDir}/config";
|
||||
# hardening
|
||||
AmbientCapabilities = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_SYS_MODULE"
|
||||
];
|
||||
CapabilityBoundingSet = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_SYS_MODULE"
|
||||
"~CAP_BLOCK_SUSPEND"
|
||||
"~CAP_BPF"
|
||||
"~CAP_CHOWN"
|
||||
"~CAP_MKNOD"
|
||||
"~CAP_PERFMON"
|
||||
"~CAP_SYS_BOOT"
|
||||
"~CAP_SYS_CHROOT"
|
||||
"~CAP_SYS_NICE"
|
||||
"~CAP_SYS_PACCT"
|
||||
"~CAP_SYS_PTRACE"
|
||||
"~CAP_SYS_TIME"
|
||||
"~CAP_SYS_TTY_CONFIG"
|
||||
"~CAP_SYSLOG"
|
||||
"~CAP_WAKE_ALARM"
|
||||
];
|
||||
ProtectSystem = "full";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = "disconnected";
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "noaccess";
|
||||
ProtectHostname = true;
|
||||
NoNewPrivileges = true;
|
||||
RestrictSUIDSGID = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
SystemCallFilter = [
|
||||
"~@aio:EPERM"
|
||||
"~@chown:EPERM"
|
||||
"~@clock:EPERM"
|
||||
"~@cpu-emulation:EPERM"
|
||||
"~@debug:EPERM"
|
||||
"~@keyring:EPERM"
|
||||
"~@memlock:EPERM"
|
||||
"~@mount:EPERM"
|
||||
"~@obsolete:EPERM"
|
||||
"~@pkey:EPERM"
|
||||
"~@privileged:EPERM"
|
||||
"~@raw-io:EPERM"
|
||||
"~@reboot:EPERM"
|
||||
"~@resources:EPERM"
|
||||
"~@sandbox:EPERM"
|
||||
"~@setuid:EPERM"
|
||||
"~@swap:EPERM"
|
||||
"~@sync:EPERM"
|
||||
"~@timer:EPERM"
|
||||
];
|
||||
ExecStart = utils.escapeSystemdExecArgs [
|
||||
(lib.getExe pkgs.fosrl-gerbil)
|
||||
"--reachableAt=http://localhost:${toString config.services.gerbil.port}"
|
||||
"--generateAndSaveKeyTo=${toString cfg.dataDir}/config/key"
|
||||
"--remoteConfig=http://localhost:${toString finalSettings.server.internal_port}/api/v1/gerbil/get-config"
|
||||
];
|
||||
# will not work if the interface is renamed
|
||||
# https://github.com/fosrl/newt/issues/37#issuecomment-3193385911
|
||||
ExecStartPost = lib.getExe gerbil-wg0-fix-script;
|
||||
};
|
||||
};
|
||||
traefik = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "gerbil.service" ];
|
||||
requires = [ "gerbil.service" ];
|
||||
partOf = [ "gerbil.service" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.traefik = {
|
||||
enable = true;
|
||||
group = "fossorial";
|
||||
dataDir = "${cfg.dataDir}/config/traefik";
|
||||
staticConfigOptions = {
|
||||
providers.http = {
|
||||
endpoint = "http://localhost:${toString finalSettings.server.internal_port}/api/v1/traefik-config";
|
||||
pollInterval = "5s";
|
||||
};
|
||||
# TODO to change this once #437073 is merged.
|
||||
experimental.plugins.badger = {
|
||||
moduleName = "github.com/fosrl/badger";
|
||||
version = "v1.2.0";
|
||||
};
|
||||
certificatesResolvers.letsencrypt.acme =
|
||||
(
|
||||
if finalSettings.domains.domain1.prefer_wildcard_cert then
|
||||
{
|
||||
# see https://doc.traefik.io/traefik/https/acme/#providers
|
||||
dnsChallenge.provider = cfg.dnsProvider;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpChallenge.entryPoint = "web";
|
||||
}
|
||||
)
|
||||
//
|
||||
# common
|
||||
{
|
||||
email = cfg.letsEncryptEmail;
|
||||
storage = "${cfg.dataDir}/config/letsencrypt/acme.json";
|
||||
caServer = "https://acme-v02.api.letsencrypt.org/directory";
|
||||
};
|
||||
entryPoints = {
|
||||
web.address = ":80";
|
||||
websecure = {
|
||||
address = ":443";
|
||||
transport.respondingTimeouts.readTimeout = "30m";
|
||||
http.tls.certResolver = "letsencrypt";
|
||||
};
|
||||
};
|
||||
};
|
||||
dynamicConfigOptions = {
|
||||
http = {
|
||||
middlewares.redirect-to-https.redirectScheme.scheme = "https";
|
||||
routers = {
|
||||
# HTTP to HTTPS redirect router
|
||||
main-app-router-redirect = {
|
||||
rule = "Host(`${cfg.dashboardDomain}`)";
|
||||
service = "next-service";
|
||||
entryPoints = [ "web" ];
|
||||
middlewares = [ "redirect-to-https" ];
|
||||
};
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router = {
|
||||
rule = "Host(`${cfg.dashboardDomain}`) && !PathPrefix(`/api/v1`)";
|
||||
service = "next-service";
|
||||
entryPoints = [ "websecure" ];
|
||||
tls =
|
||||
lib.optionalAttrs (finalSettings.domains.domain1.prefer_wildcard_cert) {
|
||||
domains = [
|
||||
{ main = cfg.baseDomain; }
|
||||
{ sans = "*.${cfg.baseDomain}"; }
|
||||
];
|
||||
}
|
||||
//
|
||||
# common
|
||||
{
|
||||
certResolver = "letsencrypt";
|
||||
};
|
||||
};
|
||||
# API router (handles /api/v1 paths)
|
||||
api-router = {
|
||||
rule = "Host(`${cfg.dashboardDomain}`) && PathPrefix(`/api/v1`)";
|
||||
service = "api-service";
|
||||
entryPoints = [ "websecure" ];
|
||||
tls.certResolver = "letsencrypt";
|
||||
};
|
||||
# WebSocket router
|
||||
ws-router = {
|
||||
rule = "Host(`${cfg.dashboardDomain}`)";
|
||||
service = "api-service";
|
||||
entryPoints = [ "websecure" ];
|
||||
tls.certResolver = "letsencrypt";
|
||||
};
|
||||
# Integration API router
|
||||
int-api-router-redirect = lib.mkIf (finalSettings.flags.enable_integration_api) {
|
||||
rule = "Host(`api.${cfg.baseDomain}`)";
|
||||
service = "int-api-service";
|
||||
entryPoints = [ "web" ];
|
||||
middlewares = [ "redirect-to-https" ];
|
||||
};
|
||||
int-api-router = lib.mkIf (finalSettings.flags.enable_integration_api) {
|
||||
rule = "Host(`api.${cfg.baseDomain}`)";
|
||||
service = "int-api-service";
|
||||
entryPoints = [ "websecure" ];
|
||||
tls.certResolver = "letsencrypt";
|
||||
};
|
||||
};
|
||||
# could be map
|
||||
services = {
|
||||
# Next.js server
|
||||
next-service.loadBalancer.servers = [
|
||||
{ url = "http://localhost:${toString finalSettings.server.next_port}"; }
|
||||
];
|
||||
# API/WebSocket server
|
||||
api-service.loadBalancer.servers = [
|
||||
{ url = "http://localhost:${toString finalSettings.server.external_port}"; }
|
||||
];
|
||||
# Integration API server
|
||||
int-api-service.loadBalancer.servers = lib.mkIf (finalSettings.flags.enable_integration_api) [
|
||||
{ url = "http://localhost:${toString finalSettings.server.integration_port}"; }
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
jackr
|
||||
sigmasquadron
|
||||
];
|
||||
}
|
||||
@@ -1142,6 +1142,7 @@ in
|
||||
pam-u2f = runTest ./pam/pam-u2f.nix;
|
||||
pam-ussh = runTest ./pam/pam-ussh.nix;
|
||||
pam-zfs-key = runTest ./pam/zfs-key.nix;
|
||||
pangolin = runTest ./pangolin.nix;
|
||||
pantalaimon = runTest ./matrix/pantalaimon.nix;
|
||||
pantheon = runTest ./pantheon.nix;
|
||||
paperless = runTest ./paperless.nix;
|
||||
|
||||
138
nixos/tests/pangolin.nix
Normal file
138
nixos/tests/pangolin.nix
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# cant use .test, since that gets caught by traefik
|
||||
domain = "nixos.eu";
|
||||
secret = "1234567890";
|
||||
|
||||
dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress;
|
||||
|
||||
in
|
||||
{
|
||||
name = "pangolin";
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
jackr
|
||||
sigmasquadron
|
||||
];
|
||||
|
||||
# The full test is not yet implemented, but once upstream supports a way to
|
||||
# configure Pangolin non-interactively, the full test will look like the following:
|
||||
# - 'acme': ACME server to replace the real servers at Let's Encrypt.
|
||||
# - 'dnsserver': The pebble challenge test server so we can use a private DNS
|
||||
# for everything here.
|
||||
# - 'VPS': The Pangolin instance, running Gerbil, Traefik, and Badger as well.
|
||||
# - 'privateHost': The private server running an HTTP server on its local
|
||||
# network that will be tunnelled via Newt to the VPS.
|
||||
# - 'client': An outside node that will test if the service hosted in
|
||||
# 'privateHost' is publicly accessible.
|
||||
# TODO: In the future, we should also have a machine to test the
|
||||
# functionality of Olm, as well as a split Pangolin/Gerbil
|
||||
# configuration once that is implemented into the module.
|
||||
nodes = {
|
||||
acme =
|
||||
{ nodes, ... }:
|
||||
{
|
||||
imports = [ ./common/acme/server ];
|
||||
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
|
||||
};
|
||||
|
||||
dnsserver =
|
||||
{ nodes, ... }:
|
||||
{
|
||||
networking = {
|
||||
firewall.allowedTCPPorts = [
|
||||
8055
|
||||
53
|
||||
];
|
||||
firewall.allowedUDPPorts = [ 53 ];
|
||||
|
||||
# nixos/lib/testing/network.nix will provide name resolution via /etc/hosts
|
||||
# for all nodes based on their host names and domain
|
||||
hostName = "dnsserver";
|
||||
domain = "eu";
|
||||
};
|
||||
systemd.services.pebble-challtestsrv = {
|
||||
description = "Pebble ACME challenge test server";
|
||||
wantedBy = [ "network.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${lib.getExe' pkgs.pebble "pebble-challtestsrv"} -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.VPS.networking.primaryIPAddress}'";
|
||||
# Required to bind on privileged ports.
|
||||
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
VPS =
|
||||
{ nodes, ... }:
|
||||
{
|
||||
imports = [ ./common/acme/client ];
|
||||
networking = {
|
||||
inherit domain;
|
||||
hosts.${nodes.VPS.networking.primaryIPAddress} = [
|
||||
domain
|
||||
"pangolin.${domain}"
|
||||
];
|
||||
nameservers = lib.mkForce [ (dnsServerIP nodes) ];
|
||||
};
|
||||
|
||||
environment = {
|
||||
etc = {
|
||||
"nixos/secrets/pangolin.env".text = ''
|
||||
SERVER_SECRET=${secret}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
services = {
|
||||
pangolin = {
|
||||
enable = true;
|
||||
baseDomain = domain;
|
||||
letsEncryptEmail = "pangolin@${domain}";
|
||||
openFirewall = true;
|
||||
environmentFile = "/etc/nixos/secrets/pangolin.env";
|
||||
settings = {
|
||||
flags.enable_integration_api = true;
|
||||
};
|
||||
};
|
||||
# set up local ca server, so we can get our certs signed without going on the internet
|
||||
traefik.staticConfigOptions.certificatesResolvers.letsencrypt.acme.caServer =
|
||||
lib.mkForce "https://${nodes.acme.test-support.acme.caDomain}/dir";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
testScript = ''
|
||||
${(import ./acme/utils.nix).pythonUtils}
|
||||
|
||||
with subtest("start ACME and DNS server"):
|
||||
acme.start()
|
||||
wait_for_running(acme)
|
||||
acme.wait_for_open_port(443)
|
||||
dnsserver.start()
|
||||
dnsserver.wait_for_open_port(53)
|
||||
|
||||
VPS.start()
|
||||
|
||||
with subtest("start Pangolin"):
|
||||
VPS.wait_for_unit("pangolin.service")
|
||||
VPS.wait_for_open_port(3000)
|
||||
VPS.wait_for_open_port(3001)
|
||||
VPS.wait_for_open_port(3002)
|
||||
VPS.wait_for_open_port(3003)
|
||||
|
||||
with subtest("start Gerbil"):
|
||||
VPS.wait_for_unit("gerbil.service")
|
||||
|
||||
with subtest("start Traefik"):
|
||||
VPS.wait_for_unit("traefik.service")
|
||||
VPS.wait_for_open_port(80)
|
||||
VPS.wait_for_open_port(443)
|
||||
|
||||
with subtest("check traefik certs}"):
|
||||
download_ca_certs(VPS, "acme.test")
|
||||
|
||||
'';
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
inter,
|
||||
databaseType ? "sqlite",
|
||||
environmentVariables ? { },
|
||||
nixosTests,
|
||||
}:
|
||||
|
||||
assert lib.assertOneOf "databaseType" databaseType [
|
||||
@@ -151,7 +152,10 @@ buildNpmPackage (finalAttrs: {
|
||||
}
|
||||
];
|
||||
|
||||
passthru = { inherit databaseType; };
|
||||
passthru = {
|
||||
inherit databaseType;
|
||||
tests = { inherit (nixosTests) pangolin; };
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Tunneled reverse proxy server with identity and access control";
|
||||
|
||||
Reference in New Issue
Block a user