Changes over time

This commit is contained in:
niten 2022-02-01 10:50:01 -08:00
parent 125d2d3d57
commit 2048377c3f
23 changed files with 464 additions and 314 deletions

View File

@ -1,10 +1,11 @@
{ pkgs, ... }: { pkgs, ... }:
{ {
ip = import ./lib/lib/ip.nix { inherit pkgs; };
dns = import ./lib/lib/dns.nix { inherit pkgs; }; dns = import ./lib/lib/dns.nix { inherit pkgs; };
passwd = import ./lib/lib/passwd.nix { inherit pkgs; }; fs = import ./lib/lib/filesystem.nix { inherit pkgs; };
ip = import ./lib/lib/ip.nix { inherit pkgs; };
lisp = import ./lib/lib/lisp.nix { inherit pkgs; }; lisp = import ./lib/lib/lisp.nix { inherit pkgs; };
network = import ./lib/lib/network.nix { inherit pkgs; }; network = import ./lib/lib/network.nix { inherit pkgs; };
fs = import ./lib/lib/filesystem.nix { inherit pkgs; }; passwd = import ./lib/lib/passwd.nix { inherit pkgs; };
text = import ./lib/lib/text.nix { inherit pkgs; };
} }

View File

@ -118,25 +118,48 @@ in {
# Sigh. Leave it the same as nginx default, so it works whether or not # Sigh. Leave it the same as nginx default, so it works whether or not
# nginx feels like helping or not. # nginx feels like helping or not.
default = "/var/lib/acme/acme-challenge"; default = "/var/lib/acme/acme-challenge";
# default = "/run/acme-challenge";
}; };
}; };
config = { config = {
security.acme.certs = mapAttrs (domain: domainOpts: { security.acme.certs = mapAttrs (domain: domainOpts: {
email = domainOpts.admin-email; # email = domainOpts.admin-email;
webroot = cfg.challenge-path; # webroot = cfg.challenge-path;
extraDomainNames = domainOpts.extra-domains; # group = "nginx";
# extraDomainNames = domainOpts.extra-domains;
}) localDomains; }) localDomains;
# Assume that if we're acquiring SSL certs, we have a real IP for the # Assume that if we're acquiring SSL certs, we have a real IP for the
# host. nginx must have an acme dir for security.acme to work. # host. nginx must have an acme dir for security.acme to work.
services.nginx = mkIf hasLocalDomains { services.nginx = mkIf hasLocalDomains {
enable = true; enable = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true; recommendedTlsSettings = true;
recommendedProxySettings = true; virtualHosts = let
server-path = "/.well-known/acme-challenge";
in (mapAttrs (domain: domainOpts: {
# THIS IS A HACK. Getting redundant paths. So if {domain} is configured
# somewhere else, assume ACME is already set.
# locations.${server-path} = mkIf (! (hasAttr domain config.services.nginx.virtualHosts)) {
# root = cfg.challenge-path;
# extraConfig = "auth_basic off;";
# };
enableACME = true;
forceSSL = true;
serverAliases = domainOpts.extra-domains;
}) localDomains) // {
"default" = {
serverName = "_";
default = true;
locations = {
${server-path} = {
root = cfg.challenge-path;
extraConfig = "auth_basic off;";
};
"/".return = "403 Forbidden";
};
};
};
}; };
networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.firewall.allowedTCPPorts = [ 80 443 ];
@ -170,7 +193,17 @@ in {
if (copyOpts.group != null) then if (copyOpts.group != null) then
"${copyOpts.user}:${copyOpts.group}" "${copyOpts.user}:${copyOpts.group}"
else copyOpts.user; else copyOpts.user;
dirs = unique [
(dirOf copyOpts.certificate)
(dirOf copyOpts.full-certificate)
(dirOf copyOpts.chain)
(dirOf copyOpts.private-key)
];
install-certs = pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" '' install-certs = pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" ''
${concatStringsSep "\n" (map (dir: ''
mkdir -p ${dir}
chown ${owners} ${dir}
'') dirs)}
cp ${source}/cert.pem ${copyOpts.certificate} cp ${source}/cert.pem ${copyOpts.certificate}
chmod 0444 ${copyOpts.certificate} chmod 0444 ${copyOpts.certificate}
chown ${owners} ${copyOpts.certificate} chown ${owners} ${copyOpts.certificate}

View File

@ -32,7 +32,6 @@ in {
domain = mkOption { domain = mkOption {
type = types.str; type = types.str;
description = "Domain under which this host is registered."; description = "Domain under which this host is registered.";
default = "fudo.link";
}; };
server = mkOption { server = mkOption {

View File

@ -1,48 +1,52 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with lib; { with lib; {
imports = [ imports = [
./acme-certs.nix ./backplane-service/dns.nix
./authentication.nix
./backplane ./acme-certs.nix
./chat.nix ./adguard-dns-proxy.nix
./client/dns.nix ./authentication.nix
./deploy.nix ./backplane.nix
./distributed-builds.nix ./chat.nix
./dns.nix ./client/dns.nix
./domains.nix ./deploy.nix
./garbage-collector.nix ./distributed-builds.nix
./git.nix ./dns.nix
./global.nix ./domains.nix
./grafana.nix ./garbage-collector.nix
./hosts.nix ./git.nix
./host-filesystems.nix ./global.nix
./initrd-network.nix ./grafana.nix
./ipfs.nix ./hosts.nix
./jabber.nix ./host-filesystems.nix
./kdc.nix ./initrd-network.nix
./ldap.nix ./ipfs.nix
./local-network.nix ./jabber.nix
./mail.nix ./kdc.nix
./mail-container.nix ./ldap.nix
./minecraft-server.nix ./local-network.nix
./netinfo-email.nix ./mail.nix
./node-exporter.nix ./mail-container.nix
./nsd.nix ./minecraft-server.nix
./password.nix ./netinfo-email.nix
./postgres.nix ./node-exporter.nix
./prometheus.nix ./nsd.nix
./secrets.nix ./password.nix
./secure-dns-proxy.nix ./postgres.nix
./sites.nix ./powerdns.nix
./slynk.nix ./prometheus.nix
./ssh.nix ./secrets.nix
./system.nix ./secure-dns-proxy.nix
./system-networking.nix ./sites.nix
./users.nix ./slynk.nix
./vpn.nix ./ssh.nix
./webmail.nix ./system.nix
./wireless-networks.nix ./system-networking.nix
./zones.nix ./users.nix
]; ./vpn.nix
./webmail.nix
./wireless-networks.nix
./zones.nix
];
} }

View File

@ -48,12 +48,54 @@ let
default = "admin@${domain}"; default = "admin@${domain}";
}; };
gssapi-realm = mkOption { grafana-hosts = mkOption {
type = listOf str;
description = "List of hosts acting as Grafana metric analyzers. Requires prometheus hosts as well.";
default = [];
};
log-aggregator = mkOption {
type = nullOr str; type = nullOr str;
description = "GSSAPI (i.e. Kerberos) realm of this domain."; description = "Host which will accept incoming log pushes.";
default = null; default = null;
}; };
postgresql-server = mkOption {
type = nullOr str;
description = "Hostname acting as the local PostgreSQL server.";
default = null;
};
backplane = mkOption {
type = nullOr (submodule {
options = {
nameserver = mkOption {
type = nullOr str;
description = "Host acting as backplane dynamic DNS server.";
default = null;
};
dns-service = mkOption {
type = nullOr str;
description = "DNS backplane service host.";
default = null;
};
domain = mkOption {
type = str;
description = "Domain name of the dynamic zone served by this server.";
};
};
});
description = "Backplane configuration.";
default = null;
};
gssapi-realm = mkOption {
type = str;
description = "GSSAPI (i.e. Kerberos) realm of this domain.";
};
kerberos-master = mkOption { kerberos-master = mkOption {
type = nullOr str; type = nullOr str;
description = "Hostname of the Kerberos master server for the domain, if applicable."; description = "Hostname of the Kerberos master server for the domain, if applicable.";
@ -72,6 +114,12 @@ let
default = []; default = [];
}; };
prometheus-hosts = mkOption {
type = listOf str;
description = "List of hosts acting aas prometheus metric scrapers for hosts in this network.";
default = [];
};
primary-nameserver = mkOption { primary-nameserver = mkOption {
type = nullOr str; type = nullOr str;
description = "Hostname of the primary nameserver for this domain."; description = "Hostname of the primary nameserver for this domain.";

View File

@ -4,14 +4,14 @@
with lib; with lib;
let let
cfg = config.fudo.grafana; cfg = config.fudo.metrics.grafana;
hostname = config.instance.hostname; hostname = config.instance.hostname;
domain-name = config.fudo.hosts.${hostname}.domain; domain-name = config.fudo.hosts.${hostname}.domain;
in { in {
options.fudo.grafana = with types; { options.fudo.metrics.grafana = with types; {
enable = mkEnableOption "Fudo Metrics Display Service"; enable = mkEnableOption "Fudo Metrics Display Service";
hostname = mkOption { hostname = mkOption {
@ -88,72 +88,75 @@ in {
description = "Directory at which to store Grafana state data."; description = "Directory at which to store Grafana state data.";
default = "/var/lib/grafana"; default = "/var/lib/grafana";
}; };
private-network = mkEnableOption "Network is private, no SSL.";
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
services.nginx = { systemd.tmpfiles.rules = let
enable = true; grafana-user = config.systemd.services.grafana.serviceConfig.User;
in [
"d ${cfg.state-directory} 0700 ${grafana-user} - - -"
];
virtualHosts = { services = {
"${cfg.hostname}" = { nginx = {
enableACME = true; enable = true;
forceSSL = true; recommendedOptimisation = true;
recommendedProxySettings = true;
locations."/" = { virtualHosts = {
proxyPass = "http://127.0.0.1:3000"; "${cfg.hostname}" = {
enableACME = ! cfg.private-network;
extraConfig = '' forceSSL = ! cfg.private-network;
proxy_set_header Host $host; locations."/".proxyPass = "http://127.0.0.1:3000";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-By $server_addr:$server_port;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
'';
}; };
}; };
}; };
};
services.grafana = { grafana = {
enable = true;
addr = "127.0.0.1";
protocol = "http";
port = 3000;
domain = cfg.hostname;
rootUrl = "https://${cfg.hostname}/";
dataDir = cfg.state-directory;
security = {
adminPasswordFile = cfg.admin-password-file;
secretKeyFile = cfg.secret-key-file;
};
smtp = {
enable = true; enable = true;
fromAddress = "metrics@fudo.org";
host = "mail.fudo.org:25";
user = cfg.smtp-username;
passwordFile = cfg.smtp-password-file;
};
database = { addr = "127.0.0.1";
host = cfg.database.hostname; protocol = "http";
name = cfg.database.name; port = 3000;
user = cfg.database.user; domain = cfg.hostname;
passwordFile = cfg.database.password-file; rootUrl = let
type = "postgres"; scheme = if cfg.private-network then "http" else "https";
}; in "${scheme}://${cfg.hostname}/";
dataDir = cfg.state-directory;
provision.datasources = [ security = {
{ adminPasswordFile = cfg.admin-password-file;
secretKeyFile = cfg.secret-key-file;
};
smtp = {
enable = true;
fromAddress = "metrics@fudo.org";
host = "${cfg.smtp.hostname}:25";
user = cfg.smtp.username;
passwordFile = cfg.smtp.password-file;
};
database = {
host = cfg.database.hostname;
name = cfg.database.name;
user = cfg.database.user;
passwordFile = cfg.database.password-file;
type = "postgres";
};
provision.datasources = imap0 (i: host: {
editable = false; editable = false;
isDefault = true; isDefault = (i == 0);
name = cfg.prometheus-host; name = builtins.trace "PROMETHEUS-HOST: ${host}" host;
type = "prometheus"; type = "prometheus";
url = "https://${cfg.prometheus-host}/"; url = let
} scheme = if private-network then "http" else "https";
]; in "${scheme}://${host}/";
}) cfg.prometheus-hosts;
};
}; };
}; };
} }

View File

@ -82,7 +82,7 @@ in {
packages = map (p: "${p.name}") packages = map (p: "${p.name}")
config.environment.systemPackages; config.environment.systemPackages;
sorted-unique = sort lessThan (unique packages); sorted-unique = sort lessThan (unique packages);
in concatStringsSep "\n" sorted-unique; in "${concatStringsSep "\n" sorted-unique}\n";
}; };
systemPackages = with pkgs; systemPackages = with pkgs;

View File

@ -32,8 +32,9 @@ in {
config = { config = {
boot = mkIf (initrd-cfg != null) { boot = mkIf (initrd-cfg != null) {
kernelParams = let kernelParams = let
site = config.fudo.sites.${config.instance.local-site}; site-name = config.instance.local-site;
site-gateway = site.gateway-v4; site = config.fudo.sites.${site-name};
site-gateway = pkgs.lib.network.site-gateway config site-name;
netmask = netmask =
pkgs.lib.ip.maskFromV32Network site.network; pkgs.lib.ip.maskFromV32Network site.network;
in [ in [

View File

@ -337,11 +337,15 @@ in {
users = { users = {
users.${cfg.user} = { users.${cfg.user} = {
isSystemUser = true; isSystemUser = true;
uid = 88;
home = state-directory; home = state-directory;
group = cfg.group; group = cfg.group;
}; };
groups.${cfg.group} = { members = [ cfg.user ]; }; groups.${cfg.group} = {
gid = 88;
members = [ cfg.user ];
};
}; };
krb5 = { krb5 = {

View File

@ -218,6 +218,15 @@ in {
config = mkIf cfg.enable { config = mkIf cfg.enable {
users = {
users.openldap = {
uid = 389;
};
groups.openldap = {
gid = 389;
};
};
environment = { environment = {
etc = { etc = {
"openldap/sasl2/slapd.conf" = mkIf (cfg.kerberos-keytab != null) { "openldap/sasl2/slapd.conf" = mkIf (cfg.kerberos-keytab != null) {

View File

@ -31,7 +31,13 @@ in {
dns-listen-ips = mkOption { dns-listen-ips = mkOption {
type = listOf str; type = listOf str;
description = "A list of IPs on which to server DNS queries."; description = "A list of IPv4 addresses on which to server DNS queries.";
};
dns-listen-ipv6s = mkOption {
type = listOf str;
description = "A list of IPv6 addresses on which to server DNS queries.";
default = [ ];
}; };
gateway = mkOption { gateway = mkOption {
@ -61,10 +67,22 @@ in {
default = false; default = false;
}; };
recursive-resolver = mkOption { # recursive-resolver = mkOption {
type = str; # type = str;
description = "DNS nameserver to use for recursive resolution."; # description = "DNS nameserver to use for recursive resolution.";
default = "1.1.1.1 port 53"; # default = "1.1.1.1 port 53";
# };
recursive-resolver = {
host = mkOption {
type = str;
description = "DNS server host or (preferably) IP.";
};
port = mkOption {
type = port;
description = "Remote host port for DNS queries.";
default = 53;
};
}; };
search-domains = mkOption { search-domains = mkOption {
@ -161,18 +179,22 @@ in {
''; '';
}; };
ipToBlock = ip: filterRedundantIps = official-hosts: hosts: let
host-by-ip = groupBy (hostOpts: hostOpts.ipv4-address) hosts;
in filter (hostOpts:
if (length (getAttr hostOpts.ipv4-address host-by-ip) == 1) then
true
else elem hostOpts.hostname official-hosts) hosts;
ipTo24Block = ip:
concatStringsSep "." (reverseList (take 3 (splitString "." ip))); concatStringsSep "." (reverseList (take 3 (splitString "." ip)));
compactHosts = hostsByBlock = official-hosts:
mapAttrsToList (host: data: data // { host = host; }) zone.hosts; groupBy (host-data: ipTo24Block host-data.ipv4-address)
hostsByBlock = (filterRedundantIps official-hosts (attrValues zone.hosts));
groupBy (host-data: ipToBlock host-data.ipv4-address) compactHosts;
hostPtrRecord = host-data: hostPtrRecord = host-data:
"${ "${last (splitString "." host-data.ipv4-address)} IN PTR ${host-data.hostname}.${cfg.domain}.";
last (splitString "." host-data.ipv4-address)
} IN PTR ${host-data.host}.${cfg.domain}.";
blockZones = mapAttrsToList blockHostsToZone hostsByBlock; blockZones = official-hosts:
mapAttrsToList blockHostsToZone (hostsByBlock official-hosts);
hostARecord = host: data: "${host} IN A ${data.ipv4-address}"; hostARecord = host: data: "${host} IN A ${data.ipv4-address}";
hostSshFpRecords = host: data: hostSshFpRecords = host: data:
@ -189,13 +211,21 @@ in {
known-hosts = config.fudo.hosts; known-hosts = config.fudo.hosts;
domain-name = config.instance.local-domain;
domain-hosts =
attrNames
(filterAttrs (_: hostOpts:
hostOpts.domain == domain-name)
config.fudo.hosts);
in { in {
enable = true; enable = true;
cacheNetworks = [ cfg.network "localhost" "localnets" ]; cacheNetworks = [ cfg.network "localhost" "localnets" ];
forwarders = [ cfg.recursive-resolver ]; forwarders = [ "${cfg.recursive-resolver.host} port ${toString cfg.recursive-resolver.port}" ];
listenOn = cfg.dns-listen-ips; listenOn = cfg.dns-listen-ips;
listenOnIpv6 = cfg.dns-listen-ipv6s;
extraOptions = concatStringsSep "\n" [ extraOptions = concatStringsSep "\n" [
"dnssec-enable yes;"
"dnssec-validation yes;" "dnssec-validation yes;"
"auth-nxdomain no;" "auth-nxdomain no;"
"recursion yes;" "recursion yes;"
@ -204,36 +234,44 @@ in {
zones = [{ zones = [{
master = true; master = true;
name = cfg.domain; name = cfg.domain;
file = pkgs.writeText "${cfg.domain}-zone" '' file = let
@ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. ( zone-data = pkgs.lib.dns.zoneToZonefile
${toString config.instance.build-timestamp} config.instance.build-timestamp
5m cfg.domain
2m zone;
6w in pkgs.writeText "zone-${cfg.domain}" zone-data;
5m) # file = pkgs.writeText "${cfg.domain}-zone" ''
# @ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. (
# ${toString config.instance.build-timestamp}
# 5m
# 2m
# 6w
# 5m)
$TTL 1h # $TTL 1h
@ IN NS ns1.${cfg.domain}. # @ IN NS ns1.${cfg.domain}.
$ORIGIN ${cfg.domain}. # $ORIGIN ${cfg.domain}.
$TTL 30m # $TTL 30m
${optionalString (zone.gssapi-realm != null) # ${optionalString (zone.gssapi-realm != null)
''_kerberos IN TXT "${zone.gssapi-realm}"''} # ''_kerberos IN TXT "${zone.gssapi-realm}"''}
${join-lines # ${join-lines
(imap1 (i: server-ip: "ns${toString i} IN A ${server-ip}") # (imap1 (i: server-ip: "ns${toString i} IN A ${server-ip}")
cfg.dns-servers)} # cfg.dns-servers)}
${join-lines (mapAttrsToList hostARecord zone.hosts)} # ${join-lines (mapAttrsToList hostARecord zone.hosts)}
${join-lines (mapAttrsToList hostSshFpRecords zone.hosts)} # ${join-lines (mapAttrsToList hostSshFpRecords zone.hosts)}
${join-lines (mapAttrsToList cnameRecord zone.aliases)} # ${join-lines (mapAttrsToList cnameRecord zone.aliases)}
${join-lines zone.verbatim-dns-records} # ${join-lines zone.verbatim-dns-records}
${pkgs.lib.dns.srvRecordsToBindZone zone.srv-records} # ${pkgs.lib.dns.srvRecordsToBindZone zone.srv-records}
${join-lines cfg.extra-records} # ${join-lines cfg.extra-records}
''; # '';
}] ++ blockZones; }] ++ (optionals
cfg.enable-reverse-mappings
(blockZones domain-hosts));
}; };
}; };
} }

View File

@ -4,14 +4,14 @@ with lib;
let let
inherit (lib.strings) concatStringsSep; inherit (lib.strings) concatStringsSep;
cfg = config.fudo.node-exporter; cfg = config.fudo.metrics.node-exporter;
allow-network = network: "allow ${network};"; allow-network = network: "allow ${network};";
local-networks = config.instance.local-networks; local-networks = config.instance.local-networks;
in { in {
options.fudo.node-exporter = with types; { options.fudo.metrics.node-exporter = with types; {
enable = mkEnableOption "Enable a Prometheus node exporter with some reasonable settings."; enable = mkEnableOption "Enable a Prometheus node exporter with some reasonable settings.";
hostname = mkOption { hostname = mkOption {
@ -24,6 +24,8 @@ in {
description = "User as which to run the node exporter job."; description = "User as which to run the node exporter job.";
default = "node-exporter"; default = "node-exporter";
}; };
private-network = mkEnableOption "Network is private.";
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
@ -45,10 +47,12 @@ in {
# list of trusted networks, with SSL protection. # list of trusted networks, with SSL protection.
nginx = { nginx = {
enable = true; enable = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
virtualHosts."${cfg.hostname}" = { virtualHosts."${cfg.hostname}" = {
enableACME = true; enableACME = ! cfg.private-network;
forceSSL = true; forceSSL = ! cfg.private-network;
locations."/metrics/node" = { locations."/metrics/node" = {
extraConfig = '' extraConfig = ''
@ -57,7 +61,6 @@ in {
deny all; deny all;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
''; '';
proxyPass = "http://127.0.0.1:9100/metrics"; proxyPass = "http://127.0.0.1:9100/metrics";

View File

@ -105,17 +105,22 @@ let
nameValuePair "DATABASE ${database}" databaseOpts.access) databases; nameValuePair "DATABASE ${database}" databaseOpts.access) databases;
makeEntry = nw: makeEntry = nw:
"host all all ${nw} gss include_realm=0 krb_realm=${gssapi-realm}"; "hostssl all all ${nw} gss include_realm=0 krb_realm=${gssapi-realm}";
makeNetworksEntry = networks: join-lines (map makeEntry networks); makeNetworksEntry = networks: join-lines (map makeEntry networks);
makeLocalUserPasswordEntries = users: makeLocalUserPasswordEntries = users: networks: let
join-lines (mapAttrsToList (user: opts: network-entries = user: db:
join-lines (map (db: '' join-lines
local ${db} ${user} md5 (map (network: "hostssl ${db} ${user} ${network} md5")
host ${db} ${user} 127.0.0.1/16 md5 networks);
host ${db} ${user} ::1/128 md5 in join-lines (mapAttrsToList (user: opts:
'') (attrNames opts.databases))) (filterPasswordedUsers users)); join-lines (map (db: ''
local ${db} ${user} md5
host ${db} ${user} 127.0.0.1/16 md5
host ${db} ${user} ::1/128 md5
${network-entries user db}
'') (attrNames opts.databases))) (filterPasswordedUsers users));
userTableAccessSql = user: entity: access: userTableAccessSql = user: entity: access:
"GRANT ${access} ON ${entity} TO ${user};"; "GRANT ${access} ON ${entity} TO ${user};";
@ -134,18 +139,21 @@ in {
enable = mkEnableOption "Fudo PostgreSQL Server"; enable = mkEnableOption "Fudo PostgreSQL Server";
ssl-private-key = mkOption { ssl-private-key = mkOption {
type = str; type = nullOr str;
description = "Location of the server SSL private key."; description = "Location of the server SSL private key.";
default = null;
}; };
ssl-certificate = mkOption { ssl-certificate = mkOption {
type = str; type = nullOr str;
description = "Location of the server SSL certificate."; description = "Location of the server SSL certificate.";
default = null;
}; };
keytab = mkOption { keytab = mkOption {
type = str; type = nullOr str;
description = "Location of the server Kerberos keytab."; description = "Location of the server Kerberos keytab.";
default = null;
}; };
local-networks = mkOption { local-networks = mkOption {
@ -224,32 +232,9 @@ in {
config = mkIf cfg.enable { config = mkIf cfg.enable {
environment = { networking.firewall.allowedTCPPorts = [ 5432 ];
systemPackages = with pkgs; [ postgresql_11_gssapi ];
# etc = { environment.systemPackages = with pkgs; [ postgresql_11_gssapi ];
# "postgresql/private/privkey.pem" = {
# mode = "0400";
# user = "postgres";
# group = "postgres";
# source = cfg.ssl-private-key;
# };
# "postgresql/cert.pem" = {
# mode = "0444";
# user = "postgres";
# group = "postgres";
# source = cfg.ssl-certificate;
# };
# "postgresql/private/postgres.keytab" = {
# mode = "0400";
# user = "postgres";
# group = "postgres";
# source = cfg.keytab;
# };
# };
};
users.groups = { users.groups = {
${cfg.socket-group} = { members = [ "postgres" ] ++ cfg.local-users; }; ${cfg.socket-group} = { members = [ "postgres" ] ++ cfg.local-users; };
@ -269,12 +254,14 @@ in {
ensurePermissions = { "DATABASE ${database}" = "ALL PRIVILEGES"; }; ensurePermissions = { "DATABASE ${database}" = "ALL PRIVILEGES"; };
}) opts.users)) cfg.databases))); }) opts.users)) cfg.databases)));
settings = { settings = let
krb_server_keyfile = cfg.keytab; ssl-enabled = cfg.ssl-certificate != null;
in {
krb_server_keyfile = mkIf (cfg.keytab != null) cfg.keytab;
ssl = true; ssl = ssl-enabled;
ssl_cert_file = cfg.ssl-certificate; ssl_cert_file = mkIf ssl-enabled cfg.ssl-certificate;
ssl_key_file = cfg.ssl-private-key; ssl_key_file = mkIf ssl-enabled cfg.ssl-private-key;
unix_socket_directories = cfg.socket-directory; unix_socket_directories = cfg.socket-directory;
unix_socket_group = cfg.socket-group; unix_socket_group = cfg.socket-group;
@ -282,12 +269,12 @@ in {
}; };
authentication = lib.mkForce '' authentication = lib.mkForce ''
${makeLocalUserPasswordEntries cfg.users} ${makeLocalUserPasswordEntries cfg.users cfg.local-networks}
local all all ident local all all ident
# host-local # host-local
host all all 127.0.0.1/32 gss include_realm=0 krb_realm=${gssapi-realm} host all all 127.0.0.1/16 gss include_realm=0 krb_realm=${gssapi-realm}
host all all ::1/128 gss include_realm=0 krb_realm=${gssapi-realm} host all all ::1/128 gss include_realm=0 krb_realm=${gssapi-realm}
# local networks # local networks
@ -308,6 +295,22 @@ in {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
}; };
paths = let
user-password-files = mapAttrsToList
(user: userOpts: userOpts.password-file)
cfg.users;
in {
postgresql-password-watcher = mkIf (length user-password-files > 0) {
wantedBy = [ "default.target" ];
description =
"Reset all user passwords if any changes occur.";
pathConfig = {
PathChanged = user-password-files;
Unit = "postgresql-password-setter.service";
};
};
};
services = { services = {
postgresql-password-setter = let postgresql-password-setter = let
passwords-script = passwords-setter-script cfg.users; passwords-script = passwords-setter-script cfg.users;
@ -350,22 +353,42 @@ in {
partOf = [ cfg.systemd-target ]; partOf = [ cfg.systemd-target ];
wants = [ "postgresql-password-setter.service" ]; wants = [ "postgresql-password-setter.service" ];
postStart = let # postStart = let
# allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;";
# extra-settings-sql = pkgs.writeText "settings.sql" ''
# ${concatStringsSep "\n"
# (map allow-user-login (mapAttrsToList (key: val: key) cfg.users))}
# ${usersAccessSql cfg.users}
# '';
# in ''
# ${pkgs.postgresql}/bin/psql --port ${
# toString config.services.postgresql.port
# } -d postgres -f ${extra-settings-sql}
# ${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL*
# '';
postStop = concatStringsSep "\n" cfg.cleanup-tasks;
};
postgresql-finalizer = {
requires = [ "postgresql.target" ];
after = [ "postgresql.target" "postgresql-password-setter.target" ];
partOf = [ cfg.systemd-target ];
script = let
allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;"; allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;";
extra-settings-sql = pkgs.writeText "settings.sql" '' extra-settings-sql = pkgs.writeText "settings.sql" ''
${concatStringsSep "\n" ${concatStringsSep "\n"
(map allow-user-login (mapAttrsToList (key: val: key) cfg.users))} (map allow-user-login (mapAttrsToList (key: val: key) cfg.users))}
${usersAccessSql cfg.users} ${usersAccessSql cfg.users}
''; '';
in '' in ''
${pkgs.postgresql}/bin/psql --port ${ ${pkgs.postgresql}/bin/psql --port ${
toString config.services.postgresql.port toString config.services.postgresql.port
} -d postgres -f ${extra-settings-sql} } -d postgres -f ${extra-settings-sql}
${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL* ${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL*
''; '';
postStop = concatStringsSep "\n" cfg.cleanup-tasks;
}; };
}; };
}; };

View File

@ -2,12 +2,11 @@
with lib; with lib;
let let
inherit (lib.strings) concatStringsSep; cfg = config.fudo.metrics.prometheus;
cfg = config.fudo.prometheus;
in { in {
options.fudo.prometheus = with types; { options.fudo.metrics.prometheus = with types; {
enable = mkEnableOption "Fudo Prometheus Data-Gathering Server"; enable = mkEnableOption "Fudo Prometheus Data-Gathering Server";
service-discovery-dns = mkOption { service-discovery-dns = mkOption {
@ -78,32 +77,33 @@ in {
description = "Directory at which to store Prometheus state."; description = "Directory at which to store Prometheus state.";
default = "/var/lib/prometheus"; default = "/var/lib/prometheus";
}; };
private-network = mkEnableOption "Network is private.";
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d ${cfg.state-directory} 0700 ${config.systemd.services.prometheus.serviceConfig.User} - - -"
];
services.nginx = { services.nginx = {
enable = true; enable = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
virtualHosts = { virtualHosts = {
"${cfg.hostname}" = { "${cfg.hostname}" = {
enableACME = true; enableACME = ! cfg.private-network;
forceSSL = true; forceSSL = ! cfg.private-network;
locations."/" = { locations."/" = {
proxyPass = "http://127.0.0.1:9090"; proxyPass = "http://127.0.0.1:9090";
extraConfig = let extraConfig = let
local-networks = config.instance.local-networks; local-networks = config.instance.local-networks;
in '' in "${optionalString ((length local-networks) > 0)
proxy_set_header Host $host; (concatStringsSep "\n"
proxy_set_header X-Real-IP $remote_addr; (map (network: "allow ${network};") local-networks)) + "\ndeny all;"}";
proxy_set_header X-Forwarded-By $server_addr:$server_port;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
${optionalString ((length local-networks) > 0)
(concatStringsSep "\n" (map (network: "allow ${network};") local-networks)) + "\ndeny all;"}
'';
}; };
}; };
}; };
@ -118,85 +118,18 @@ in {
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
port = 9090; port = 9090;
scrapeConfigs = [ scrapeConfigs = let
{ make-job = type: {
job_name = "docker"; job_name = type;
honor_labels = false; honor_labels = false;
static_configs = [ scheme = if cfg.private-network then "http" else "https";
{ metrics_path = "/metrics/${type}";
targets = cfg.docker-hosts; dns_sd_configs = if (hasAttr type cfg.service-discovery-dns) then
} [ { names = cfg.service-discovery-dns.${type}; } ] else [];
]; static_configs = if (hasAttr type cfg.static-targets) then
} [ { targets = cfg.static-targets.${type}; } ] else [];
};
{ in map make-job ["docker" "node" "dovecot" "postfix" "rspamd"];
job_name = "node";
scheme = "https";
metrics_path = "/metrics/node";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.node;
}
];
static_configs = [
{
targets = cfg.static-targets.node;
}
];
}
{
job_name = "dovecot";
scheme = "https";
metrics_path = "/metrics/dovecot";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.dovecot;
}
];
static_configs = [
{
targets = cfg.static-targets.dovecot;
}
];
}
{
job_name = "postfix";
scheme = "https";
metrics_path = "/metrics/postfix";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.postfix;
}
];
static_configs = [
{
targets = cfg.static-targets.postfix;
}
];
}
{
job_name = "rspamd";
scheme = "https";
metrics_path = "/metrics/rspamd";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.rspamd;
}
];
static_configs = [
{
targets = cfg.static-targets.rspamd;
}
];
}
];
pushgateway = { pushgateway = {
enable = if (cfg.push-url != null) then true else false; enable = if (cfg.push-url != null) then true else false;

View File

@ -66,6 +66,7 @@ let
user = mkOption { user = mkOption {
type = str; type = str;
description = "User (on target host) to which the file will belong."; description = "User (on target host) to which the file will belong.";
default = "root";
}; };
group = mkOption { group = mkOption {

View File

@ -133,6 +133,12 @@ let
type = str; type = str;
description = "Hostname of the mail server to use for this site."; description = "Hostname of the mail server to use for this site.";
}; };
local-gateway = mkOption {
type = nullOr str;
description = "If this is a NAT site, this should point to the host acting as network gateway.";
default = null;
};
}; };
}; };

View File

@ -166,7 +166,7 @@ let
"Level of protection to apply to the system for this service."; "Level of protection to apply to the system for this service.";
}; };
addressFamilies = mkOption { addressFamilies = mkOption {
type = listOf (enum address-families); type = nullOr (listOf (enum address-families));
default = [ ]; default = [ ];
description = "List of address families which the service can use."; description = "List of address families which the service can use.";
}; };
@ -435,7 +435,8 @@ in {
WorkingDirectory = WorkingDirectory =
mkIf (opts.workingDirectory != null) opts.workingDirectory; mkIf (opts.workingDirectory != null) opts.workingDirectory;
RestrictAddressFamilies = RestrictAddressFamilies =
restrict-address-families opts.addressFamilies; optionals (opts.addressFamilies != null)
(restrict-address-families opts.addressFamilies);
RestrictNamespaces = opts.restrictNamespaces; RestrictNamespaces = opts.restrictNamespaces;
User = mkIf (opts.user != null) opts.user; User = mkIf (opts.user != null) opts.user;
Group = mkIf (opts.group != null) opts.group; Group = mkIf (opts.group != null) opts.group;

View File

@ -161,7 +161,7 @@ in {
GEMINI_TEXTFILES_ROOT = cfg.textfiles-archive; GEMINI_TEXTFILES_ROOT = cfg.textfiles-archive;
GEMINI_FEEDS = "${generate-feeds cfg.feeds}"; GEMINI_FEEDS = "${generate-feeds cfg.feeds}";
CL_SOURCE_REGISTRY = "${pkgs.lib.lisp.lisp-source-registry pkgs.cl-gemini}"; CL_SOURCE_REGISTRY = pkgs.lib.lisp.lisp-source-registry pkgs.cl-gemini;
}; };
path = with pkgs; [ path = with pkgs; [

View File

@ -62,9 +62,21 @@ in {
description = "Networks which are considered local to this host, site, or domain."; description = "Networks which are considered local to this host, site, or domain.";
}; };
service-home = mkOption {
type = str;
description = "Path to runtime home directories for services.";
default = "/run/service";
};
build-seed = mkOption { build-seed = mkOption {
type = str; type = str;
description = "Seed used to generate configuration."; description = "Seed used to generate configuration.";
}; };
}; };
config = {
systemd.tmpfiles.rules = [
"d ${config.instance.service-home} 755 root root - -"
];
};
} }

View File

@ -67,7 +67,7 @@ let
toString record.priority toString record.priority
} ${ } ${
toString record.weight toString record.weight
} ${record.host}.") records); } ${toString record.port} ${record.host}.") records);
srvRecordOpts = with types; { srvRecordOpts = with types; {
options = { options = {

View File

@ -44,8 +44,14 @@ let
not-null = o: o != null; not-null = o: o != null;
in filter not-null [ ipv4 ipv6 ]; in filter not-null [ ipv4 ipv6 ];
site-gateway = config: site-name: let
site = config.fudo.sites.${site-name};
in if (site.local-gateway != null)
then host-ipv4 config site.local-gateway
else site.gateway-v4;
in { in {
inherit host-ipv4 host-ipv6 host-ips; inherit host-ipv4 host-ipv6 host-ips site-gateway;
generate-mac-address = hostname: interface: let generate-mac-address = hostname: interface: let
pkg = generate-mac-address hostname interface; pkg = generate-mac-address hostname interface;

View File

@ -12,12 +12,12 @@ let
installPhase = let installPhase = let
passwd = removeSuffix "\n" (readFile passwd-file); passwd = removeSuffix "\n" (readFile passwd-file);
in '' in ''
slappasswd -s ${passwd} > $out slappasswd -s ${passwd} | tr -d '\n' > $out
''; '';
}; };
hash-ldap-passwd = name: passwd-file: hash-ldap-passwd = name: passwd-file:
builtins.readFile "${hash-ldap-passwd-pkg name passwd-file}"; readFile "${hash-ldap-passwd-pkg name passwd-file}";
generate-random-passwd = name: length: pkgs.stdenv.mkDerivation { generate-random-passwd = name: length: pkgs.stdenv.mkDerivation {
name = "${name}-random-passwd"; name = "${name}-random-passwd";
@ -27,10 +27,28 @@ let
buildInputs = with pkgs; [ pwgen ]; buildInputs = with pkgs; [ pwgen ];
installPhase = '' installPhase = ''
pwgen --secure --num-passwords=1 ${toString length} > $out pwgen --secure --num-passwords=1 ${toString length} | tr -d '\n' > $out
''; '';
}; };
bcrypt-passwd-pkg = name: passwd-file: pkgs.stdenv.mkDerivation {
name = "${name}-bcrypt";
phases = [ "installPhase" ];
buildInputs = with pkgs; [ apacheHttpd ];
installPhase = let
passwd = removeSuffix "\n" (readFile passwd-file);
in ''
htpasswd -bnBC 10 "" ${passwd} | tr -d ':\n' | sed 's/$2y/$2a/' > $out
'';
};
bcrypt-passwd = name: passwd-file:
readFile "${bcrypt-passwd-pkg name passwd-file}";
generate-stablerandom-passwd = name: { seed, length ? 20, ... }: generate-stablerandom-passwd = name: { seed, length ? 20, ... }:
pkgs.stdenv.mkDerivation { pkgs.stdenv.mkDerivation {
name = "${name}-stablerandom-passwd"; name = "${name}-stablerandom-passwd";
@ -41,13 +59,15 @@ let
installPhase = '' installPhase = ''
echo "${name}-${seed}" > seedfile echo "${name}-${seed}" > seedfile
pwgen --secure --num-passwords=1 -H seedfile ${toString length} > $out pwgen --secure --num-passwords=1 -H seedfile ${toString length} | tr -d '\n' > $out
''; '';
}; };
in { in {
hash-ldap-passwd = hash-ldap-passwd; hash-ldap-passwd = hash-ldap-passwd;
bcrypt-passwd = bcrypt-passwd;
random-passwd-file = name: length: random-passwd-file = name: length:
builtins.toPath "${generate-random-passwd name length}"; builtins.toPath "${generate-random-passwd name length}";

View File

@ -1,8 +1,13 @@
{ lib, ... }: { lib, ... }:
with lib; with lib;
{ { name, ... }: {
options = with types; { options = with types; {
hostname = mkOption {
type = str;
description = "Hostname.";
default = name;
};
ipv4-address = mkOption { ipv4-address = mkOption {
type = nullOr str; type = nullOr str;
description = "The V4 IP of a given host, if any."; description = "The V4 IP of a given host, if any.";