Compare commits
134 Commits
Author | SHA1 | Date |
---|---|---|
|
f9c38a8479 | |
|
b0b280c231 | |
|
d604090b94 | |
|
6d08c1a790 | |
|
60d5d50336 | |
|
68c8052e0c | |
|
7bdae87de3 | |
|
e6109489d4 | |
|
751ee6be5d | |
|
b1197db920 | |
|
cc0a8b5cba | |
|
fe27dc3f3d | |
|
62e4276a86 | |
|
38640140c8 | |
|
c1c67ecc80 | |
|
b0c7b7bd88 | |
|
2d2f1099ea | |
|
9e36dfc6ca | |
|
c5d1d11137 | |
|
16a105a24e | |
|
168dc68251 | |
|
031600e175 | |
|
083defd692 | |
|
ace843b59d | |
|
e8a191908e | |
|
2a27c0f7b1 | |
|
45b15e65fd | |
|
92be492607 | |
|
f7a5a43d30 | |
|
2c206d394b | |
|
7e533a6d6f | |
|
dd2df768f1 | |
|
c405597d81 | |
|
4196468cf8 | |
|
1186de6c36 | |
|
42f486a6fc | |
|
8ba0e155d3 | |
|
12d62caf25 | |
|
8e08f115c5 | |
|
9f96e0fa62 | |
|
a33c958625 | |
|
216e84c94e | |
|
893538d442 | |
|
f142ce296c | |
|
1aea58945d | |
|
deb3e455bd | |
|
09e6262e83 | |
|
078a150080 | |
|
e8ad5ae2ca | |
|
3b89330f81 | |
|
26b130ceab | |
|
9b20a19e27 | |
|
72a33bd452 | |
|
a169209046 | |
|
c884d59f47 | |
|
e770842fa6 | |
|
cf4b24da99 | |
|
6015d8eaf0 | |
|
04263beb7f | |
|
d4044d886a | |
|
88bcd94803 | |
|
fdc658a263 | |
|
10e78ba89f | |
|
2d96202a58 | |
|
e37fe381d5 | |
|
7ae766bd8a | |
|
70a1a6d6af | |
|
9d031308aa | |
|
a241c56c59 | |
|
cd1c104bce | |
|
a46b71e72b | |
|
56f4e49df0 | |
|
356c452d66 | |
|
c629223e85 | |
|
19f117a14b | |
|
1c0d4b207a | |
|
5079561573 | |
|
1e5cdcba79 | |
|
169cf84263 | |
|
130086b6a8 | |
|
28e16e19e4 | |
|
7fcbc0bddb | |
|
db5c0b7cd1 | |
|
0890bb7b1e | |
|
37bd62f950 | |
|
49009f67e7 | |
|
a72e23a198 | |
|
ad5570b7c3 | |
|
7ed8b64466 | |
|
f3312f8fce | |
|
1e1bd7e3b9 | |
|
dbf6cd6337 | |
|
cea913f1fe | |
|
169dee3939 | |
|
bfcd26ae7e | |
|
59a0f02001 | |
|
92962b83ed | |
|
b2d3ef23f2 | |
|
930f12ab17 | |
|
83b85a7495 | |
|
f5e5e263ef | |
|
e546c70eaa | |
|
421ecd1f65 | |
|
e2f29e8f44 | |
|
42332121be | |
|
5fa5877579 | |
|
3d585efebe | |
|
d347309d5e | |
|
7028e169d3 | |
|
ebf7d59e30 | |
|
7b318d0aaf | |
|
306087355c | |
|
8b21b53e4b | |
|
f82dc851f1 | |
|
f7966d3fee | |
|
cb039ceabd | |
|
8b3f771c23 | |
|
860bb22034 | |
|
4549d51c7b | |
|
5ca65c3ee6 | |
|
9865877695 | |
|
977ae77a76 | |
|
c4ec7d8e04 | |
|
00d3019934 | |
|
97babc3947 | |
|
882bfe1d53 | |
|
0971f01d66 | |
|
65f7f1c542 | |
|
ed86976d84 | |
|
e2ce36d2f8 | |
|
2720ce9be9 | |
|
93bc54ec67 | |
|
bae4f4ddef | |
|
179dda3ec6 |
11
flake.nix
11
flake.nix
|
@ -2,9 +2,16 @@
|
||||||
description = "Fudo Nix Helper Functions";
|
description = "Fudo Nix Helper Functions";
|
||||||
|
|
||||||
outputs = { self, ... }: {
|
outputs = { self, ... }: {
|
||||||
overlay = import ./overlay.nix;
|
overlays = rec {
|
||||||
|
default = lib;
|
||||||
|
lib = import ./overlay.nix;
|
||||||
|
};
|
||||||
|
|
||||||
nixosModule = import ./module.nix;
|
nixosModules = rec {
|
||||||
|
default = fudo;
|
||||||
|
fudo = import ./module.nix;
|
||||||
|
lib = { ... }: { config.nixpkgs.overlays = [ self.overlays.default ]; };
|
||||||
|
};
|
||||||
|
|
||||||
lib = import ./lib.nix;
|
lib = import ./lib.nix;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,109 +1,108 @@
|
||||||
{ config, lib, pkgs, ... } @ toplevel:
|
{ config, lib, pkgs, ... }@toplevel:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
hostname = config.instance.hostname;
|
hostname = config.instance.hostname;
|
||||||
|
|
||||||
domainOpts = { name, ... }: let
|
domainOpts = { name, ... }:
|
||||||
domain = name;
|
let domain = name;
|
||||||
in {
|
in {
|
||||||
options = with types; {
|
options = with types; {
|
||||||
admin-email = mkOption {
|
admin-email = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Domain administrator email.";
|
description = "Domain administrator email.";
|
||||||
default = "admin@${domain}";
|
default = "admin@${domain}";
|
||||||
};
|
};
|
||||||
|
|
||||||
extra-domains = mkOption {
|
extra-domains = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of domains to add to this certificate.";
|
description = "List of domains to add to this certificate.";
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
local-copies = let
|
local-copies = let
|
||||||
localCopyOpts = { name, ... }: let
|
localCopyOpts = { name, ... }:
|
||||||
copy = name;
|
let copy = name;
|
||||||
in {
|
in {
|
||||||
options = with types; let
|
options = with types;
|
||||||
target-path = "/run/ssl-certificates/${domain}/${copy}";
|
let target-path = "/run/ssl-certificates/${domain}/${copy}";
|
||||||
in {
|
in {
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "User to which this copy belongs.";
|
description = "User to which this copy belongs.";
|
||||||
};
|
};
|
||||||
|
|
||||||
group = mkOption {
|
group = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = "Group to which this copy belongs.";
|
description = "Group to which this copy belongs.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
service = mkOption {
|
service = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "systemd job to copy certs.";
|
description = "systemd job to copy certs.";
|
||||||
default = "fudo-acme-${domain}-${copy}-certs.service";
|
default = "fudo-acme-${domain}-${copy}-certs.service";
|
||||||
};
|
};
|
||||||
|
|
||||||
certificate = mkOption {
|
certificate = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Full path to the local copy certificate.";
|
description = "Full path to the local copy certificate.";
|
||||||
default = "${target-path}/cert.pem";
|
default = "${target-path}/cert.pem";
|
||||||
};
|
};
|
||||||
|
|
||||||
full-certificate = mkOption {
|
full-certificate = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Full path to the local copy certificate.";
|
description = "Full path to the local copy certificate.";
|
||||||
default = "${target-path}/fullchain.pem";
|
default = "${target-path}/fullchain.pem";
|
||||||
};
|
};
|
||||||
|
|
||||||
chain = mkOption {
|
chain = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Full path to the local copy certificate.";
|
description = "Full path to the local copy certificate.";
|
||||||
default = "${target-path}/chain.pem";
|
default = "${target-path}/chain.pem";
|
||||||
};
|
};
|
||||||
|
|
||||||
private-key = mkOption {
|
private-key = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Full path to the local copy certificate.";
|
description = "Full path to the local copy certificate.";
|
||||||
default = "${target-path}/key.pem";
|
default = "${target-path}/key.pem";
|
||||||
};
|
};
|
||||||
|
|
||||||
dependent-services = mkOption {
|
dependent-services = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of systemd services depending on this copy.";
|
description =
|
||||||
default = [ ];
|
"List of systemd services depending on this copy.";
|
||||||
};
|
default = [ ];
|
||||||
|
};
|
||||||
part-of = mkOption {
|
|
||||||
type = listOf str;
|
part-of = mkOption {
|
||||||
description = "List of systemd targets to which this copy belongs.";
|
type = listOf str;
|
||||||
default = [ ];
|
description =
|
||||||
};
|
"List of systemd targets to which this copy belongs.";
|
||||||
};
|
default = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in mkOption {
|
||||||
|
type = attrsOf (submodule localCopyOpts);
|
||||||
|
description = "Map of copies to make for use by services.";
|
||||||
|
default = { };
|
||||||
};
|
};
|
||||||
in mkOption {
|
|
||||||
type = attrsOf (submodule localCopyOpts);
|
|
||||||
description = "Map of copies to make for use by services.";
|
|
||||||
default = {};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
head-or-null = lst: if (lst == []) then null else head lst;
|
head-or-null = lst: if (lst == [ ]) then null else head lst;
|
||||||
rm-service-ext = filename:
|
rm-service-ext = filename:
|
||||||
head-or-null (builtins.match "^(.+)\.service$" filename);
|
head-or-null (builtins.match "^(.+).service$" filename);
|
||||||
|
|
||||||
concatMapAttrs = f: attrs:
|
concatMapAttrs = f: attrs: foldr (a: b: a // b) { } (mapAttrsToList f attrs);
|
||||||
foldr (a: b: a // b) {} (mapAttrsToList f attrs);
|
|
||||||
|
|
||||||
cfg = config.fudo.acme;
|
cfg = config.fudo.acme;
|
||||||
hasLocalDomains = hasAttr hostname cfg.host-domains;
|
hasLocalDomains = hasAttr hostname cfg.host-domains;
|
||||||
localDomains = if hasLocalDomains then
|
localDomains = if hasLocalDomains then cfg.host-domains.${hostname} else { };
|
||||||
cfg.host-domains.${hostname} else {};
|
|
||||||
|
optionalStringOr = str: default: if (str != null) then str else default;
|
||||||
|
|
||||||
optionalStringOr = str: default:
|
|
||||||
if (str != null) then str else default;
|
|
||||||
|
|
||||||
in {
|
in {
|
||||||
options.fudo.acme = with types; {
|
options.fudo.acme = with types; {
|
||||||
host-domains = mkOption {
|
host-domains = mkOption {
|
||||||
|
@ -121,22 +120,22 @@ in {
|
||||||
# default = "/run/acme-challenge";
|
# default = "/run/acme-challenge";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
security.acme.certs = mapAttrs (domain: domainOpts: {
|
# security.acme.certs = mapAttrs (domain: domainOpts:
|
||||||
# email = domainOpts.admin-email;
|
# {
|
||||||
# webroot = cfg.challenge-path;
|
# # email = domainOpts.admin-email;
|
||||||
# group = "nginx";
|
# # webroot = cfg.challenge-path;
|
||||||
# extraDomainNames = domainOpts.extra-domains;
|
# # group = "nginx";
|
||||||
}) localDomains;
|
# # extraDomainNames = domainOpts.extra-domains;
|
||||||
|
# }) 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;
|
||||||
recommendedTlsSettings = true;
|
recommendedTlsSettings = true;
|
||||||
virtualHosts = let
|
virtualHosts = let serverPath = "/.well-known/acme-challenge";
|
||||||
server-path = "/.well-known/acme-challenge";
|
|
||||||
in (mapAttrs (domain: domainOpts: {
|
in (mapAttrs (domain: domainOpts: {
|
||||||
# THIS IS A HACK. Getting redundant paths. So if {domain} is configured
|
# THIS IS A HACK. Getting redundant paths. So if {domain} is configured
|
||||||
# somewhere else, assume ACME is already set.
|
# somewhere else, assume ACME is already set.
|
||||||
|
@ -152,7 +151,7 @@ in {
|
||||||
serverName = "_";
|
serverName = "_";
|
||||||
default = true;
|
default = true;
|
||||||
locations = {
|
locations = {
|
||||||
${server-path} = {
|
"${serverPath}" = {
|
||||||
root = cfg.challenge-path;
|
root = cfg.challenge-path;
|
||||||
extraConfig = "auth_basic off;";
|
extraConfig = "auth_basic off;";
|
||||||
};
|
};
|
||||||
|
@ -163,80 +162,86 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
tmpfiles = mkIf hasLocalDomains {
|
tmpfiles = mkIf hasLocalDomains {
|
||||||
rules = let
|
rules = let
|
||||||
copies = concatMapAttrs (domain: domainOpts:
|
copies = concatMapAttrs (domain: domainOpts: domainOpts.local-copies)
|
||||||
domainOpts.local-copies) localDomains;
|
localDomains;
|
||||||
perms = copyOpts: if (copyOpts.group != null) then "0550" else "0500";
|
perms = copyOpts: if (copyOpts.group != null) then "0550" else "0500";
|
||||||
copy-paths = mapAttrsToList (copy: copyOpts:
|
copy-paths = mapAttrsToList (copy: copyOpts:
|
||||||
let
|
let
|
||||||
dir-entry = copyOpts: file: "d \"${dirOf file}\" ${perms copyOpts} ${copyOpts.user} ${optionalStringOr copyOpts.group "-"} - -";
|
dir-entry = copyOpts: file:
|
||||||
|
''
|
||||||
|
d "${dirOf file}" ${perms copyOpts} ${copyOpts.user} ${
|
||||||
|
optionalStringOr copyOpts.group "-"
|
||||||
|
} - -'';
|
||||||
in map (dir-entry copyOpts) [
|
in map (dir-entry copyOpts) [
|
||||||
copyOpts.certificate
|
copyOpts.certificate
|
||||||
copyOpts.full-certificate
|
copyOpts.full-certificate
|
||||||
copyOpts.chain
|
copyOpts.chain
|
||||||
copyOpts.private-key
|
copyOpts.private-key
|
||||||
]) copies;
|
]) copies;
|
||||||
in (unique (concatMap (i: unique i) copy-paths)) ++ [
|
in (unique (concatMap (i: unique i) copy-paths))
|
||||||
"d \"${cfg.challenge-path}\" 755 acme nginx - -"
|
++ [ ''d "${cfg.challenge-path}" 755 acme nginx - -'' ];
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services = concatMapAttrs (domain: domainOpts:
|
services = concatMapAttrs (domain: domainOpts:
|
||||||
concatMapAttrs (copy: copyOpts: let
|
concatMapAttrs (copy: copyOpts:
|
||||||
key-perms = copyOpts: if (copyOpts.group != null) then "0440" else "0400";
|
let
|
||||||
source = config.security.acme.certs.${domain}.directory;
|
key-perms = copyOpts:
|
||||||
target = copyOpts.path;
|
if (copyOpts.group != null) then "0440" else "0400";
|
||||||
owners =
|
source = config.security.acme.certs.${domain}.directory;
|
||||||
if (copyOpts.group != null) then
|
target = copyOpts.path;
|
||||||
|
owners = if (copyOpts.group != null) then
|
||||||
"${copyOpts.user}:${copyOpts.group}"
|
"${copyOpts.user}:${copyOpts.group}"
|
||||||
else copyOpts.user;
|
else
|
||||||
dirs = unique [
|
copyOpts.user;
|
||||||
(dirOf copyOpts.certificate)
|
dirs = unique [
|
||||||
(dirOf copyOpts.full-certificate)
|
(dirOf copyOpts.certificate)
|
||||||
(dirOf copyOpts.chain)
|
(dirOf copyOpts.full-certificate)
|
||||||
(dirOf copyOpts.private-key)
|
(dirOf copyOpts.chain)
|
||||||
];
|
(dirOf copyOpts.private-key)
|
||||||
install-certs = pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" ''
|
];
|
||||||
${concatStringsSep "\n" (map (dir: ''
|
install-certs =
|
||||||
mkdir -p ${dir}
|
pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" ''
|
||||||
chown ${owners} ${dir}
|
${concatStringsSep "\n" (map (dir: ''
|
||||||
'') dirs)}
|
mkdir -p ${dir}
|
||||||
cp ${source}/cert.pem ${copyOpts.certificate}
|
chown ${owners} ${dir}
|
||||||
chmod 0444 ${copyOpts.certificate}
|
'') dirs)}
|
||||||
chown ${owners} ${copyOpts.certificate}
|
cp ${source}/cert.pem ${copyOpts.certificate}
|
||||||
|
chmod 0444 ${copyOpts.certificate}
|
||||||
|
chown ${owners} ${copyOpts.certificate}
|
||||||
|
|
||||||
cp ${source}/full.pem ${copyOpts.full-certificate}
|
cp ${source}/full.pem ${copyOpts.full-certificate}
|
||||||
chmod 0444 ${copyOpts.full-certificate}
|
chmod 0444 ${copyOpts.full-certificate}
|
||||||
chown ${owners} ${copyOpts.full-certificate}
|
chown ${owners} ${copyOpts.full-certificate}
|
||||||
|
|
||||||
cp ${source}/chain.pem ${copyOpts.chain}
|
cp ${source}/chain.pem ${copyOpts.chain}
|
||||||
chmod 0444 ${copyOpts.chain}
|
chmod 0444 ${copyOpts.chain}
|
||||||
chown ${owners} ${copyOpts.chain}
|
chown ${owners} ${copyOpts.chain}
|
||||||
|
|
||||||
cp ${source}/key.pem ${copyOpts.private-key}
|
cp ${source}/key.pem ${copyOpts.private-key}
|
||||||
chmod ${key-perms copyOpts} ${copyOpts.private-key}
|
chmod ${key-perms copyOpts} ${copyOpts.private-key}
|
||||||
chown ${owners} ${copyOpts.private-key}
|
chown ${owners} ${copyOpts.private-key}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
service-name = rm-service-ext copyOpts.service;
|
service-name = rm-service-ext copyOpts.service;
|
||||||
in {
|
in {
|
||||||
${service-name} = {
|
${service-name} = {
|
||||||
description = "Copy ${domain} ACME certs for ${copy}.";
|
description = "Copy ${domain} ACME certs for ${copy}.";
|
||||||
after = [ "acme-${domain}.service" ];
|
after = [ "acme-${domain}.service" ];
|
||||||
before = copyOpts.dependent-services;
|
before = copyOpts.dependent-services;
|
||||||
wantedBy = [ "multi-user.target" ] ++ copyOpts.dependent-services;
|
wantedBy = [ "multi-user.target" ] ++ copyOpts.dependent-services;
|
||||||
partOf = copyOpts.part-of;
|
partOf = copyOpts.part-of;
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
ExecStart = install-certs;
|
ExecStart = install-certs;
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
StandardOutput = "journal";
|
StandardOutput = "journal";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}) domainOpts.local-copies) localDomains;
|
||||||
}) domainOpts.local-copies) localDomains;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,18 +9,17 @@ let
|
||||||
get-basename = filename:
|
get-basename = filename:
|
||||||
head (builtins.match "^[a-zA-Z0-9]+-(.+)$" (baseNameOf filename));
|
head (builtins.match "^[a-zA-Z0-9]+-(.+)$" (baseNameOf filename));
|
||||||
|
|
||||||
format-json-file = filename: pkgs.stdenv.mkDerivation {
|
format-json-file = filename:
|
||||||
name = "formatted-${get-basename filename}";
|
pkgs.stdenv.mkDerivation {
|
||||||
phases = [ "installPhase" ];
|
name = "formatted-${get-basename filename}";
|
||||||
buildInputs = with pkgs; [ python ];
|
phases = [ "installPhase" ];
|
||||||
installPhase = "python -mjson.tool ${filename} > $out";
|
buildInputs = with pkgs; [ python3 ];
|
||||||
};
|
installPhase = "python -mjson.tool ${filename} > $out";
|
||||||
|
};
|
||||||
|
|
||||||
admin-passwd-file =
|
admin-passwd-file =
|
||||||
pkgs.lib.passwd.stablerandom-passwd-file
|
pkgs.lib.passwd.stablerandom-passwd-file "adguard-dns-proxy-admin"
|
||||||
"adguard-dns-proxy-admin"
|
config.instance.build-seed;
|
||||||
config.instance.build-seed;
|
|
||||||
|
|
||||||
filterOpts = {
|
filterOpts = {
|
||||||
options = with types; {
|
options = with types; {
|
||||||
|
@ -41,50 +40,45 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
generate-config = { dns,
|
generate-config = { dns, http, filters, verbose, upstream-dns, bootstrap-dns
|
||||||
http,
|
, blocked-hosts, enable-dnssec, local-domain-name, ... }: {
|
||||||
filters,
|
bind_host = http.listen-ip;
|
||||||
verbose,
|
bind_port = http.listen-port;
|
||||||
upstream-dns,
|
users = [{
|
||||||
bootstrap-dns,
|
|
||||||
blocked-hosts,
|
|
||||||
enable-dnssec,
|
|
||||||
local-domain-name,
|
|
||||||
... }: {
|
|
||||||
bind_host = http.listen-ip;
|
|
||||||
bind_port = http.listen-port;
|
|
||||||
users = [
|
|
||||||
{
|
|
||||||
name = "admin";
|
name = "admin";
|
||||||
password = pkgs.lib.passwd.bcrypt-passwd
|
password = pkgs.lib.passwd.bcrypt-passwd "adguard-dns-proxy-admin"
|
||||||
"adguard-dns-proxy-admin"
|
|
||||||
admin-passwd-file;
|
admin-passwd-file;
|
||||||
}
|
}];
|
||||||
];
|
auth_attempts = 5;
|
||||||
auth_attempts = 5;
|
block_auth_min = 30;
|
||||||
block_auth_min = 30;
|
web_session_ttl = 720;
|
||||||
web_session_ttl = 720;
|
dns = {
|
||||||
dns = {
|
bind_hosts = dns.listen-ips;
|
||||||
bind_hosts = dns.listen-ips;
|
port = dns.listen-port;
|
||||||
port = dns.listen-port;
|
upstream_dns = upstream-dns;
|
||||||
upstream_dns = upstream-dns;
|
bootstrap_dns = bootstrap-dns;
|
||||||
bootstrap_dns = bootstrap-dns;
|
enable_dnssec = enable-dnssec;
|
||||||
blocking_mode = "default";
|
local_domain_name = local-domain-name;
|
||||||
blocked_hosts = blocked-hosts;
|
protection_enabled = true;
|
||||||
enable_dnssec = enable-dnssec;
|
blocking_mode = "default";
|
||||||
local_domain_name = local-domain-name;
|
blocked_hosts = blocked-hosts;
|
||||||
|
filtering_enabled = true;
|
||||||
|
parental_enabled = false;
|
||||||
|
safesearch_enabled = false;
|
||||||
|
use_private_ptr_resolvers = cfg.dns.reverse-dns != [ ];
|
||||||
|
local_ptr_upstreams = cfg.dns.reverse-dns;
|
||||||
|
};
|
||||||
|
tls.enabled = false;
|
||||||
|
filters = imap1 (i: filter: {
|
||||||
|
enabled = true;
|
||||||
|
name = filter.name;
|
||||||
|
url = filter.url;
|
||||||
|
}) filters;
|
||||||
|
dhcp.enabled = false;
|
||||||
|
clients = [ ];
|
||||||
|
verbose = verbose;
|
||||||
|
schema_version = 10;
|
||||||
};
|
};
|
||||||
tls.enabled = false;
|
|
||||||
filters = imap1 (i: filter: {
|
|
||||||
enabled = true;
|
|
||||||
name = filter.name;
|
|
||||||
url = filter.url;
|
|
||||||
}) filters;
|
|
||||||
dhcp.enabled = false;
|
|
||||||
clients = [];
|
|
||||||
verbose = verbose;
|
|
||||||
schema_version = 10;
|
|
||||||
};
|
|
||||||
|
|
||||||
generate-config-file = opts:
|
generate-config-file = opts:
|
||||||
format-json-file (pkgs.writeText "adguard-dns-proxy-config.yaml"
|
format-json-file (pkgs.writeText "adguard-dns-proxy-config.yaml"
|
||||||
|
@ -106,6 +100,13 @@ in {
|
||||||
description = "Port on which to listen for DNS queries.";
|
description = "Port on which to listen for DNS queries.";
|
||||||
default = 53;
|
default = 53;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reverse-dns = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"DNS servers on which to perform reverse lookups for private addresses (if any).";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
http = {
|
http = {
|
||||||
|
@ -127,7 +128,8 @@ in {
|
||||||
default = [
|
default = [
|
||||||
{
|
{
|
||||||
name = "AdGuard DNS filter";
|
name = "AdGuard DNS filter";
|
||||||
url = "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt";
|
url =
|
||||||
|
"https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "AdAway Default Blocklist";
|
name = "AdAway Default Blocklist";
|
||||||
|
@ -141,17 +143,25 @@ in {
|
||||||
name = "OISD.NL Blocklist";
|
name = "OISD.NL Blocklist";
|
||||||
url = "https://abp.oisd.nl/";
|
url = "https://abp.oisd.nl/";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "FireBog Easy Privacy";
|
||||||
|
url = "https://v.firebog.net/hosts/Easyprivacy.txt";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "FireBog Easy Ads";
|
||||||
|
url = "https://v.firebog.net/hosts/Easylist.txt";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "FireBog Easy Admiral";
|
||||||
|
url = "https://v.firebog.net/hosts/Admiral.txt";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
blocked-hosts = mkOption {
|
blocked-hosts = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of hosts to explicitly block.";
|
description = "List of hosts to explicitly block.";
|
||||||
default = [
|
default = [ "version.bind" "id.server" "hostname.bind" ];
|
||||||
"version.bind"
|
|
||||||
"id.server"
|
|
||||||
"hostname.bind"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enable-dnssec = mkOption {
|
enable-dnssec = mkOption {
|
||||||
|
@ -193,7 +203,8 @@ in {
|
||||||
|
|
||||||
allowed-networks = mkOption {
|
allowed-networks = mkOption {
|
||||||
type = nullOr (listOf str);
|
type = nullOr (listOf str);
|
||||||
description = "Optional list of networks with which this job may communicate.";
|
description =
|
||||||
|
"Optional list of networks with which this job may communicate.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -220,9 +231,7 @@ in {
|
||||||
group = cfg.user;
|
group = cfg.user;
|
||||||
};
|
};
|
||||||
|
|
||||||
groups.${cfg.user} = {
|
groups.${cfg.user} = { members = [ cfg.user ]; };
|
||||||
members = [ cfg.user ];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fudo = {
|
fudo = {
|
||||||
|
@ -234,39 +243,40 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
system.services.adguard-dns-proxy = let
|
system.services.adguard-dns-proxy =
|
||||||
cfg-path = "/run/adguard-dns-proxy/config.yaml";
|
let cfg-path = "/run/adguard-dns-proxy/config.yaml";
|
||||||
in {
|
in {
|
||||||
description = "DNS Proxy for ad filtering and DNS-over-HTTPS lookups.";
|
description =
|
||||||
wantedBy = [ "default.target" ];
|
"DNS Proxy for ad filtering and DNS-over-HTTPS lookups.";
|
||||||
after = [ "syslog.target" ];
|
wantedBy = [ "default.target" ];
|
||||||
requires = [ "network.target" ];
|
after = [ "syslog.target" ];
|
||||||
privateNetwork = false;
|
requires = [ "network.target" ];
|
||||||
requiredCapabilities = optional upgrade-perms "CAP_NET_BIND_SERVICE";
|
privateNetwork = false;
|
||||||
restartWhen = "always";
|
requiredCapabilities = optional upgrade-perms "CAP_NET_BIND_SERVICE";
|
||||||
addressFamilies = null;
|
restartWhen = "always";
|
||||||
networkWhitelist = cfg.allowed-networks;
|
addressFamilies = null;
|
||||||
user = mkIf upgrade-perms cfg.user;
|
networkWhitelist = cfg.allowed-networks;
|
||||||
runtimeDirectory = "adguard-dns-proxy";
|
user = mkIf upgrade-perms cfg.user;
|
||||||
stateDirectory = "adguard-dns-proxy";
|
runtimeDirectory = "adguard-dns-proxy";
|
||||||
preStart = ''
|
stateDirectory = "adguard-dns-proxy";
|
||||||
cp ${generate-config-file cfg} ${cfg-path};
|
preStart = ''
|
||||||
chown $USER ${cfg-path};
|
cp ${generate-config-file cfg} ${cfg-path};
|
||||||
chmod u+w ${cfg-path};
|
chown $USER ${cfg-path};
|
||||||
'';
|
chmod u+w ${cfg-path};
|
||||||
|
'';
|
||||||
|
|
||||||
execStart = let
|
execStart = let
|
||||||
args = [
|
args = [
|
||||||
"--no-check-update"
|
"--no-check-update"
|
||||||
"--work-dir /var/lib/adguard-dns-proxy"
|
"--work-dir /var/lib/adguard-dns-proxy"
|
||||||
"--pidfile /run/adguard-dns-proxy/adguard-dns-proxy.pid"
|
"--pidfile /run/adguard-dns-proxy/adguard-dns-proxy.pid"
|
||||||
"--host ${cfg.http.listen-ip}"
|
"--host ${cfg.http.listen-ip}"
|
||||||
"--port ${toString cfg.http.listen-port}"
|
"--port ${toString cfg.http.listen-port}"
|
||||||
"--config ${cfg-path}"
|
"--config ${cfg-path}"
|
||||||
];
|
];
|
||||||
arg-string = concatStringsSep " " args;
|
arg-string = concatStringsSep " " args;
|
||||||
in "${pkgs.adguardhome}/bin/adguardhome ${arg-string}";
|
in "${pkgs.adguardhome}/bin/adguardhome ${arg-string}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,28 +68,27 @@ let
|
||||||
environment.systemPackages = [ kadminLocal ];
|
environment.systemPackages = [ kadminLocal ];
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
|
tmpfiles.rules =
|
||||||
|
[ "f ${cfg.kdc.database} 0700 ${cfg.user} ${cfg.group} - -" ];
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
heimdal-kdc = {
|
heimdal-kdc = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network-online.target" ];
|
|
||||||
description =
|
description =
|
||||||
"Heimdal Kerberos Key Distribution Center (primary ticket server).";
|
"Heimdal Kerberos Key Distribution Center (primary ticket server).";
|
||||||
path = with pkgs; [ heimdal ];
|
path = with pkgs; [ heimdal ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
PrivateDevices = true;
|
PrivateDevices = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
# PrivateMounts = true;
|
|
||||||
ProtectControlGroups = true;
|
ProtectControlGroups = true;
|
||||||
ProtectKernelTunables = true;
|
ProtectKernelTunables = true;
|
||||||
# ProtectSystem = true;
|
|
||||||
ProtectHostname = true;
|
ProtectHostname = true;
|
||||||
# ProtectHome = true;
|
|
||||||
ProtectClock = true;
|
ProtectClock = true;
|
||||||
ProtectKernelLogs = true;
|
ProtectKernelLogs = true;
|
||||||
MemoryDenyWriteExecute = true;
|
MemoryDenyWriteExecute = true;
|
||||||
RestrictRealtime = true;
|
RestrictRealtime = true;
|
||||||
# LockPersonality = true;
|
PermissionsStartOnly = false;
|
||||||
# PermissionsStartOnly = true;
|
|
||||||
LimitNOFILE = 4096;
|
LimitNOFILE = 4096;
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
|
@ -97,39 +96,37 @@ let
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||||
SecureBits = "keep-caps";
|
SecureBits = "keep-caps";
|
||||||
|
ExecStartPre = let
|
||||||
|
chownScript = pkgs.writeShellScript "kerberos-chown.sh" ''
|
||||||
|
${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.kdc.database}
|
||||||
|
${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.kdc.state-directory}/kerberos.log
|
||||||
|
'';
|
||||||
|
in "+${chownScript}";
|
||||||
ExecStart = let
|
ExecStart = let
|
||||||
ips = if (cfg.kdc.bind-addresses != [ ]) then
|
ips = if (cfg.kdc.bind-addresses != [ ]) then
|
||||||
cfg.kdc.bind-addresses
|
cfg.kdc.bind-addresses
|
||||||
else
|
else
|
||||||
[ "0.0.0.0" ];
|
[ "0.0.0.0" ];
|
||||||
bindClause = "--addresses=${concatStringsSep "," ips}";
|
bindClause = "--addresses=${concatStringsSep "," ips}";
|
||||||
in "${pkgs.heimdal}/libexec/heimdal/kdc --config-file=${kdcConf} --ports=88 ${bindClause}";
|
in "${pkgs.heimdal}/libexec/kdc --config-file=${kdcConf} --ports=88 ${bindClause}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
heimdal-kadmind = {
|
heimdal-kadmind = {
|
||||||
wantedBy = [ "heimdal-kdc.service" ];
|
wantedBy = [ "heimdal-kdc.service" ];
|
||||||
after = [ "heimdal-kdc.service" ];
|
after = [ "heimdal-kdc.service" ];
|
||||||
bindsTo = [ "heimdal-kdc.service" ];
|
|
||||||
description = "Heimdal Kerberos Administration Server.";
|
description = "Heimdal Kerberos Administration Server.";
|
||||||
path = with pkgs; [ heimdal ];
|
path = with pkgs; [ heimdal ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
# StandardInput = "socket";
|
|
||||||
# StandardOutput = "socket";
|
|
||||||
PrivateDevices = true;
|
PrivateDevices = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
# PrivateMounts = true;
|
|
||||||
ProtectControlGroups = true;
|
ProtectControlGroups = true;
|
||||||
ProtectKernelTunables = true;
|
ProtectKernelTunables = true;
|
||||||
# ProtectSystem = true;
|
|
||||||
ProtectHostname = true;
|
ProtectHostname = true;
|
||||||
# ProtectHome = true;
|
|
||||||
ProtectClock = true;
|
ProtectClock = true;
|
||||||
ProtectKernelLogs = true;
|
ProtectKernelLogs = true;
|
||||||
MemoryDenyWriteExecute = true;
|
MemoryDenyWriteExecute = true;
|
||||||
RestrictRealtime = true;
|
RestrictRealtime = true;
|
||||||
# LockPersonality = true;
|
|
||||||
# PermissionsStartOnly = true;
|
|
||||||
LimitNOFILE = 4096;
|
LimitNOFILE = 4096;
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
|
@ -138,7 +135,7 @@ let
|
||||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||||
SecureBits = "keep-caps";
|
SecureBits = "keep-caps";
|
||||||
ExecStart = concatStringsSep " " [
|
ExecStart = concatStringsSep " " [
|
||||||
"${pkgs.heimdal}/libexec/heimdal/kadmind"
|
"${pkgs.heimdal}/libexec/kadmind"
|
||||||
"--config-file=${kdcConf}"
|
"--config-file=${kdcConf}"
|
||||||
"--keytab=${cfg.kdc.primary.keytabs.kadmind}"
|
"--keytab=${cfg.kdc.primary.keytabs.kadmind}"
|
||||||
"--realm=${cfg.realm}"
|
"--realm=${cfg.realm}"
|
||||||
|
@ -149,16 +146,11 @@ let
|
||||||
heimdal-kpasswdd = {
|
heimdal-kpasswdd = {
|
||||||
wantedBy = [ "heimdal-kdc.service" ];
|
wantedBy = [ "heimdal-kdc.service" ];
|
||||||
after = [ "heimdal-kdc.service" ];
|
after = [ "heimdal-kdc.service" ];
|
||||||
bindsTo = [ "heimdal-kdc.service" ];
|
|
||||||
description = "Heimdal Kerberos Password Server.";
|
description = "Heimdal Kerberos Password Server.";
|
||||||
path = with pkgs; [ heimdal ];
|
path = with pkgs; [ heimdal ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
# This wasn't working:
|
|
||||||
# StandardInput = "socket";
|
|
||||||
# StandardOutput = "socket";
|
|
||||||
PrivateDevices = true;
|
PrivateDevices = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
# PrivateMounts = true;
|
|
||||||
ProtectControlGroups = true;
|
ProtectControlGroups = true;
|
||||||
ProtectKernelTunables = true;
|
ProtectKernelTunables = true;
|
||||||
ProtectSystem = true;
|
ProtectSystem = true;
|
||||||
|
@ -178,7 +170,7 @@ let
|
||||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||||
SecureBits = "keep-caps";
|
SecureBits = "keep-caps";
|
||||||
ExecStart = concatStringsSep " " [
|
ExecStart = concatStringsSep " " [
|
||||||
"${pkgs.heimdal}/libexec/heimdal/kpasswdd"
|
"${pkgs.heimdal}/libexec/kpasswdd"
|
||||||
"--config-file=${kdcConf}"
|
"--config-file=${kdcConf}"
|
||||||
"--keytab=${cfg.kdc.primary.keytabs.kpasswdd}"
|
"--keytab=${cfg.kdc.primary.keytabs.kpasswdd}"
|
||||||
"--realm=${cfg.realm}"
|
"--realm=${cfg.realm}"
|
||||||
|
@ -187,9 +179,7 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
heimdal-hprop = mkIf hasSecondary {
|
heimdal-hprop = mkIf hasSecondary {
|
||||||
wantedBy = [ "heimdal-kdc.service" ];
|
wantedBy = [ "multi-user.service" ];
|
||||||
bindsTo = [ "heimdal-kdc.service" ];
|
|
||||||
after = [ "heimdal-kdc.service" ];
|
|
||||||
description =
|
description =
|
||||||
"Service to propagate the KDC database to secondary servers.";
|
"Service to propagate the KDC database to secondary servers.";
|
||||||
path = with pkgs; [ heimdal ];
|
path = with pkgs; [ heimdal ];
|
||||||
|
@ -198,21 +188,8 @@ let
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
|
Restart = "on-failure";
|
||||||
RuntimeDirectory = "heimdal-hprop";
|
RuntimeDirectory = "heimdal-hprop";
|
||||||
# ExecStartPre = let
|
|
||||||
# convertCmd = concatStringsSep " " [
|
|
||||||
# "${pkgs.kdcConvertDatabase}/bin/kdc-convert-database"
|
|
||||||
# "--config-file=${kdcConf}"
|
|
||||||
# "--key=${cfg.kdc.master-key-file}"
|
|
||||||
# "--format=db3"
|
|
||||||
# "--realm=${cfg.realm}"
|
|
||||||
# "--output=${staging-db}"
|
|
||||||
# "--verbose"
|
|
||||||
# ];
|
|
||||||
# in pkgs.writeShellScript "convert-kdc-database.sh" ''
|
|
||||||
# ${convertCmd}
|
|
||||||
# ls $RUNTIME_DIRECTORY
|
|
||||||
# '';
|
|
||||||
ExecStartPre = pkgs.writeShellScript "kdc-prepare-hprop-dump.sh"
|
ExecStartPre = pkgs.writeShellScript "kdc-prepare-hprop-dump.sh"
|
||||||
(concatStringsSep " " [
|
(concatStringsSep " " [
|
||||||
"${pkgs.heimdal}/bin/kadmin"
|
"${pkgs.heimdal}/bin/kadmin"
|
||||||
|
@ -220,12 +197,13 @@ let
|
||||||
"--config-file=${kdcConf}"
|
"--config-file=${kdcConf}"
|
||||||
"--"
|
"--"
|
||||||
"dump"
|
"dump"
|
||||||
"--format=MIT"
|
"--format=Heimdal"
|
||||||
"$(echo ${staging-db})"
|
"${staging-db}"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ExecStart = pkgs.writeShellScript "kdc-hprop.sh"
|
ExecStart = pkgs.writeShellScript "kdc-hprop.sh"
|
||||||
(concatStringsSep " " ([
|
(concatStringsSep " " ([
|
||||||
"${pkgs.heimdal}/libexec/heimdal/hprop"
|
"${pkgs.heimdal}/libexec/hprop"
|
||||||
''--master-key="${cfg.kdc.master-key-file}"''
|
''--master-key="${cfg.kdc.master-key-file}"''
|
||||||
#''--database="(echo "${staging-db}")"''
|
#''--database="(echo "${staging-db}")"''
|
||||||
"--database=sqlite:${cfg.kdc.database}"
|
"--database=sqlite:${cfg.kdc.database}"
|
||||||
|
@ -239,36 +217,12 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
paths.heimdal-hprop = mkIf hasSecondary {
|
paths.heimdal-hprop = mkIf hasSecondary {
|
||||||
wantedBy = [ "heimdal-kdc.service" ];
|
wantedBy = [ "heimdal-hprop.service" ];
|
||||||
bindsTo = [ "heimdal-kdc.service" ];
|
bindsTo = [ "heimdal-hprop.service" ];
|
||||||
after = [ "heimdal-kdc.service" ];
|
pathConfig.PathModified = cfg.kdc.database;
|
||||||
pathConfig = { PathModified = cfg.kdc.database; };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# services.xinetd = {
|
|
||||||
# enable = true;
|
|
||||||
|
|
||||||
# services = [
|
|
||||||
# {
|
|
||||||
# name = "kadmin";
|
|
||||||
# user = cfg.user;
|
|
||||||
# server = "${pkgs.heimdal}/libexec/heimdal/kadmind";
|
|
||||||
# protocol = "tcp";
|
|
||||||
# serverArgs =
|
|
||||||
# "--config-file=${kdcConf} --keytab=${cfg.kdc.primary.keytabs.kadmind}";
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# name = "kpasswd";
|
|
||||||
# user = cfg.user;
|
|
||||||
# server = "${pkgs.heimdal}/libexec/heimdal/kpasswdd";
|
|
||||||
# protocol = "udp";
|
|
||||||
# serverArgs =
|
|
||||||
# "--config-file=${kdcConf} --keytab=${cfg.kdc.primary.keytabs.kpasswdd}";
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
allowedTCPPorts = [ 88 749 ];
|
allowedTCPPorts = [ 88 749 ];
|
||||||
allowedUDPPorts = [ 88 464 ];
|
allowedUDPPorts = [ 88 464 ];
|
||||||
|
@ -310,32 +264,31 @@ let
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = cfg.group;
|
group = cfg.group;
|
||||||
};
|
};
|
||||||
groups."${cfg.group}" = { members = [ cfg.user ]; };
|
groups."${cfg.group}".members = [ cfg.user ];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
|
|
||||||
|
tmpfiles.rules =
|
||||||
|
[ "f ${cfg.kdc.database} 0700 ${cfg.user} ${cfg.group} - -" ];
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
heimdal-kdc-secondary = {
|
heimdal-kdc-secondary = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network-online.target" ];
|
|
||||||
description =
|
description =
|
||||||
"Heimdal Kerberos Key Distribution Center (secondary ticket server).";
|
"Heimdal Kerberos Key Distribution Center (secondary ticket server).";
|
||||||
path = with pkgs; [ heimdal ];
|
path = with pkgs; [ heimdal ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
PrivateDevices = true;
|
PrivateDevices = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
# PrivateMounts = true;
|
|
||||||
ProtectControlGroups = true;
|
ProtectControlGroups = true;
|
||||||
ProtectKernelTunables = true;
|
ProtectKernelTunables = true;
|
||||||
# ProtectSystem = true;
|
|
||||||
ProtectHostname = true;
|
ProtectHostname = true;
|
||||||
# ProtectHome = true;
|
|
||||||
ProtectClock = true;
|
ProtectClock = true;
|
||||||
ProtectKernelLogs = true;
|
ProtectKernelLogs = true;
|
||||||
MemoryDenyWriteExecute = true;
|
MemoryDenyWriteExecute = true;
|
||||||
RestrictRealtime = true;
|
RestrictRealtime = true;
|
||||||
# LockPersonality = true;
|
PermissionsStartOnly = false;
|
||||||
# PermissionsStartOnly = true;
|
|
||||||
LimitNOFILE = 4096;
|
LimitNOFILE = 4096;
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
|
@ -343,50 +296,72 @@ let
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||||
SecureBits = "keep-caps";
|
SecureBits = "keep-caps";
|
||||||
|
ExecStartPre = let
|
||||||
|
chownScript = pkgs.writeShellScript "kerberos-chown.sh" ''
|
||||||
|
${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.kdc.database}
|
||||||
|
${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.kdc.state-directory}/kerberos.log
|
||||||
|
'';
|
||||||
|
in "+${chownScript}";
|
||||||
ExecStart = let
|
ExecStart = let
|
||||||
ips = if (cfg.kdc.bind-addresses != [ ]) then
|
ips = if (cfg.kdc.bind-addresses != [ ]) then
|
||||||
cfg.kdc.bind-addresses
|
cfg.kdc.bind-addresses
|
||||||
else
|
else
|
||||||
[ "0.0.0.0" ];
|
[ "0.0.0.0" ];
|
||||||
bindClause = "--addresses=${concatStringsSep "," ips}";
|
bindClause = "--addresses=${concatStringsSep "," ips}";
|
||||||
in "${pkgs.heimdal}/libexec/heimdal/kdc --config-file=${kdcConf} --ports=88 ${bindClause}";
|
in "${pkgs.heimdal}/libexec/kdc --config-file=${kdcConf} --ports=88 ${bindClause}";
|
||||||
};
|
};
|
||||||
|
unitConfig.ConditionPathExists = [ cfg.kdc.database ];
|
||||||
};
|
};
|
||||||
|
|
||||||
heimdal-hpropd = {
|
"heimdal-hpropd@" = {
|
||||||
wantedBy = [ "heimdal-kdc-secondary.service" ];
|
|
||||||
after = [ "heimdal-kdc-secondary.service" ];
|
|
||||||
bindsTo = [ "heimdal-kdc-secondary.service" ];
|
|
||||||
description = "Heimdal propagation listener server.";
|
description = "Heimdal propagation listener server.";
|
||||||
path = with pkgs; [ heimdal ];
|
path = with pkgs; [ coreutils heimdal ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
StandardInput = "socket";
|
||||||
|
StandardOutput = "socket";
|
||||||
PrivateDevices = true;
|
PrivateDevices = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
# PrivateMounts = true;
|
|
||||||
ProtectControlGroups = true;
|
ProtectControlGroups = true;
|
||||||
ProtectKernelTunables = true;
|
ProtectKernelTunables = true;
|
||||||
# ProtectSystem = true;
|
|
||||||
ProtectHostname = true;
|
ProtectHostname = true;
|
||||||
# ProtectHome = true;
|
|
||||||
ProtectClock = true;
|
ProtectClock = true;
|
||||||
ProtectKernelLogs = true;
|
ProtectKernelLogs = true;
|
||||||
MemoryDenyWriteExecute = true;
|
MemoryDenyWriteExecute = true;
|
||||||
RestrictRealtime = true;
|
RestrictRealtime = true;
|
||||||
# LockPersonality = true;
|
LimitNOFILE = "4096";
|
||||||
# PermissionsStartOnly = true;
|
|
||||||
LimitNOFILE = 4096;
|
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
Restart = "always";
|
# Server will retry -- this results in stacking
|
||||||
RestartSec = "5s";
|
Restart = "no";
|
||||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||||
SecureBits = "keep-caps";
|
SecureBits = "keep-caps";
|
||||||
ExecStart = concatStringsSep " " [
|
ReadWritePaths = [ "${dirOf cfg.kdc.database}" ];
|
||||||
"${pkgs.heimdal}/libexec/heimdal/hpropd"
|
StateDirectory = "hemidal-hpropd";
|
||||||
"--database=sqlite:${cfg.kdc.database}"
|
ExecStartPre = pkgs.writeShellScript "heimdal-prepare-db.sh"
|
||||||
"--keytab=${cfg.kdc.secondary.keytabs.hpropd}"
|
"cp ${cfg.kdc.database} $STATE_DIRECTORY/realm.db";
|
||||||
];
|
ExecStart = let
|
||||||
|
startScript = pkgs.writeShellScript "launch-heimdal-hpropd.sh"
|
||||||
|
(concatStringsSep " " [
|
||||||
|
"${pkgs.heimdal}/libexec/hpropd"
|
||||||
|
"--database=sqlite:$STATE_DIRECTORY/realm.db"
|
||||||
|
"--keytab=${cfg.kdc.secondary.keytabs.hpropd}"
|
||||||
|
]);
|
||||||
|
in "${startScript}";
|
||||||
|
ExecStartPost = pkgs.writeShellScript "heimdal-restore-db.sh" ''
|
||||||
|
chown ${cfg.user}:${cfg.group} $STATE_DIRECTORY/realm.db
|
||||||
|
mv $STATE_DIRECTORY/realm.db ${cfg.kdc.database}
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
unitConfig.ConditionPathExists =
|
||||||
|
[ cfg.kdc.database cfg.kdc.secondary.keytabs.hpropd ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sockets.heimdal-hpropd = {
|
||||||
|
wantedBy = [ "sockets.target" ];
|
||||||
|
socketConfig = {
|
||||||
|
ListenStream = "0.0.0.0:754";
|
||||||
|
Accept = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{ config, lib, pkgs, ... } @ toplevel:
|
{ config, lib, pkgs, ... }@toplevel:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
|
@ -12,7 +12,8 @@ in {
|
||||||
|
|
||||||
required-services = mkOption {
|
required-services = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of systemd units on which the DNS backplane job depends.";
|
description =
|
||||||
|
"List of systemd units on which the DNS backplane job depends.";
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,11 +55,12 @@ in {
|
||||||
|
|
||||||
password-file = mkOption {
|
password-file = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "File containing password for DNS backplane database user.";
|
description =
|
||||||
|
"File containing password for DNS backplane database user.";
|
||||||
};
|
};
|
||||||
|
|
||||||
ssl-mode = mkOption {
|
ssl-mode = mkOption {
|
||||||
type = enum ["no" "yes" "full" "try" "require"];
|
type = enum [ "no" "yes" "full" "try" "require" ];
|
||||||
description = "SSL connection mode.";
|
description = "SSL connection mode.";
|
||||||
default = "require";
|
default = "require";
|
||||||
};
|
};
|
||||||
|
@ -86,9 +88,7 @@ in {
|
||||||
home = backplane-dns-home;
|
home = backplane-dns-home;
|
||||||
createHome = true;
|
createHome = true;
|
||||||
};
|
};
|
||||||
groups.${cfg.group} = {
|
groups.${cfg.group} = { members = [ cfg.user ]; };
|
||||||
members = [ cfg.user ];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fudo.system.services = {
|
fudo.system.services = {
|
||||||
|
@ -110,12 +110,12 @@ in {
|
||||||
environment = {
|
environment = {
|
||||||
FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = cfg.backplane-server;
|
FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = cfg.backplane-server;
|
||||||
FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane-role.role;
|
FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane-role.role;
|
||||||
FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE = cfg.backplane-role.password-file;
|
FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE =
|
||||||
|
cfg.backplane-role.password-file;
|
||||||
|
|
||||||
FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.database.host;
|
FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.database.host;
|
||||||
FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.database.database;
|
FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.database.database;
|
||||||
FUDO_DNS_BACKPLANE_DATABASE_USERNAME =
|
FUDO_DNS_BACKPLANE_DATABASE_USERNAME = cfg.database.username;
|
||||||
cfg.database.username;
|
|
||||||
FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE =
|
FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE =
|
||||||
cfg.database.password-file;
|
cfg.database.password-file;
|
||||||
FUDO_DNS_BACKPLANE_DATABASE_USE_SSL = cfg.database.ssl-mode;
|
FUDO_DNS_BACKPLANE_DATABASE_USE_SSL = cfg.database.ssl-mode;
|
||||||
|
|
|
@ -7,20 +7,19 @@ let
|
||||||
|
|
||||||
host-secrets = config.fudo.secrets.host-secrets.${hostname};
|
host-secrets = config.fudo.secrets.host-secrets.${hostname};
|
||||||
|
|
||||||
generate-auth-file = name: files: let
|
generate-auth-file = name: files:
|
||||||
make-entry = name: passwd-file:
|
let
|
||||||
''("${name}" . "${readFile passwd-file}")'';
|
make-entry = name: passwd-file:
|
||||||
entries = mapAttrsToList make-entry files;
|
''("${name}" . "${readFile passwd-file}")'';
|
||||||
content = concatStringsSep "\n" entries;
|
entries = mapAttrsToList make-entry files;
|
||||||
in pkgs.writeText "${name}-backplane-auth.scm" "'(${content})";
|
content = concatStringsSep "\n" entries;
|
||||||
|
in pkgs.writeText "${name}-backplane-auth.scm" "'(${content})";
|
||||||
|
|
||||||
host-auth-file = generate-auth-file "host"
|
host-auth-file = generate-auth-file "host"
|
||||||
(mapAttrs (hostname: hostOpts: hostOpts.password-file)
|
(mapAttrs (hostname: hostOpts: hostOpts.password-file) cfg.client-hosts);
|
||||||
cfg.client-hosts);
|
|
||||||
|
|
||||||
service-auth-file = generate-auth-file "service"
|
service-auth-file = generate-auth-file "service"
|
||||||
(mapAttrs (service: serviceOpts: serviceOpts.password-file)
|
(mapAttrs (service: serviceOpts: serviceOpts.password-file) cfg.services);
|
||||||
cfg.services);
|
|
||||||
|
|
||||||
clientHostOpts = { name, ... }: {
|
clientHostOpts = { name, ... }: {
|
||||||
options = with types; {
|
options = with types; {
|
||||||
|
@ -49,13 +48,13 @@ in {
|
||||||
client-hosts = mkOption {
|
client-hosts = mkOption {
|
||||||
type = attrsOf (submodule clientHostOpts);
|
type = attrsOf (submodule clientHostOpts);
|
||||||
description = "List of backplane client options.";
|
description = "List of backplane client options.";
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
services = mkOption {
|
services = mkOption {
|
||||||
type = attrsOf (submodule serviceOpts);
|
type = attrsOf (submodule serviceOpts);
|
||||||
description = "List of backplane service options.";
|
description = "List of backplane service options.";
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
backplane-hostname = mkOption {
|
backplane-hostname = mkOption {
|
||||||
|
@ -85,8 +84,7 @@ in {
|
||||||
|
|
||||||
jabber = {
|
jabber = {
|
||||||
environment = {
|
environment = {
|
||||||
FUDO_HOST_PASSWD_FILE =
|
FUDO_HOST_PASSWD_FILE = host-secrets.backplane-host-auth.target-file;
|
||||||
host-secrets.backplane-host-auth.target-file;
|
|
||||||
FUDO_SERVICE_PASSWD_FILE =
|
FUDO_SERVICE_PASSWD_FILE =
|
||||||
host-secrets.backplane-service-auth.target-file;
|
host-secrets.backplane-service-auth.target-file;
|
||||||
# GUILE_AUTO_COMPILE = "0";
|
# GUILE_AUTO_COMPILE = "0";
|
||||||
|
@ -98,22 +96,21 @@ in {
|
||||||
site-config = {
|
site-config = {
|
||||||
auth_method = "external";
|
auth_method = "external";
|
||||||
extauth_program =
|
extauth_program =
|
||||||
# "${pkgs.guile}/bin/guile -s /run/backplane-auth.scm";
|
|
||||||
"${pkgs.guile}/bin/guile -s ${pkgs.backplane-auth}/backplane-auth.scm";
|
"${pkgs.guile}/bin/guile -s ${pkgs.backplane-auth}/backplane-auth.scm";
|
||||||
extauth_pool_size = 3;
|
extauth_pool_size = 3;
|
||||||
auth_use_cache = false;
|
auth_use_cache = false;
|
||||||
|
|
||||||
modules = {
|
modules = {
|
||||||
mod_adhoc = {};
|
mod_adhoc = { };
|
||||||
# mod_caps = {};
|
# mod_caps = {};
|
||||||
# mod_carboncopy = {};
|
# mod_carboncopy = {};
|
||||||
# mod_client_state = {};
|
# mod_client_state = {};
|
||||||
mod_configure = {};
|
mod_configure = { };
|
||||||
# mod_disco = {};
|
# mod_disco = {};
|
||||||
mod_fail2ban = {};
|
mod_fail2ban = { };
|
||||||
# mod_last = {};
|
# mod_last = {};
|
||||||
mod_offline.access_max_user_messages = 5000;
|
mod_offline.access_max_user_messages = 5000;
|
||||||
mod_ping = {};
|
mod_ping = { };
|
||||||
# mod_pubsub = {
|
# mod_pubsub = {
|
||||||
# access_createnode = "pubsub_createnode";
|
# access_createnode = "pubsub_createnode";
|
||||||
# ignore_pep_from_offline = true;
|
# ignore_pep_from_offline = true;
|
||||||
|
@ -124,9 +121,9 @@ in {
|
||||||
# ];
|
# ];
|
||||||
# };
|
# };
|
||||||
# mod_roster = {};
|
# mod_roster = {};
|
||||||
mod_stream_mgmt = {};
|
mod_stream_mgmt = { };
|
||||||
mod_time = {};
|
mod_time = { };
|
||||||
mod_version = {};
|
mod_version = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -200,51 +200,7 @@ in {
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://127.0.0.1:8065";
|
proxyPass = "http://127.0.0.1:8065";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
|
|
||||||
# extraConfig = ''
|
|
||||||
# client_max_body_size 50M;
|
|
||||||
# proxy_set_header Connection "";
|
|
||||||
# proxy_set_header Host $host;
|
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# proxy_set_header X-Forwarded-By $server_addr:$server_port;
|
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# proxy_set_header X-Frame-Options SAMEORIGIN;
|
|
||||||
# proxy_buffers 256 16k;
|
|
||||||
# proxy_buffer_size 16k;
|
|
||||||
# proxy_read_timeout 600s;
|
|
||||||
# proxy_cache mattermost_cache;
|
|
||||||
# proxy_cache_revalidate on;
|
|
||||||
# proxy_cache_min_uses 2;
|
|
||||||
# proxy_cache_use_stale timeout;
|
|
||||||
# proxy_cache_lock on;
|
|
||||||
# proxy_http_version 1.1;
|
|
||||||
# '';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# locations."~ /api/v[0-9]+/(users/)?websocket$" = {
|
|
||||||
# proxyPass = "http://127.0.0.1:8065";
|
|
||||||
|
|
||||||
# extraConfig = ''
|
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
|
||||||
# proxy_set_header Connection "upgrade";
|
|
||||||
# client_max_body_size 50M;
|
|
||||||
# proxy_set_header Host $host;
|
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# proxy_set_header X-Forwarded-By $server_addr:$server_port;
|
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# proxy_set_header X-Frame-Options SAMEORIGIN;
|
|
||||||
# proxy_buffers 256 16k;
|
|
||||||
# proxy_buffer_size 16k;
|
|
||||||
# client_body_timeout 60;
|
|
||||||
# send_timeout 300;
|
|
||||||
# lingering_timeout 5;
|
|
||||||
# proxy_connect_timeout 90;
|
|
||||||
# proxy_send_timeout 300;
|
|
||||||
# proxy_read_timeout 90s;
|
|
||||||
# '';
|
|
||||||
# };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,13 +4,12 @@ with lib;
|
||||||
let
|
let
|
||||||
cfg = config.fudo.client.dns;
|
cfg = config.fudo.client.dns;
|
||||||
|
|
||||||
ssh-key-files =
|
hostname = config.instance.hostname;
|
||||||
map (host-key: host-key.path) config.services.openssh.hostKeys;
|
|
||||||
|
|
||||||
ssh-key-args = concatStringsSep " " (map (file: "-f ${file}") ssh-key-files);
|
|
||||||
|
|
||||||
in {
|
in {
|
||||||
options.fudo.client.dns = {
|
options.fudo.client.dns = {
|
||||||
|
enable = mkEnableOption "Enable Backplane DNS client.";
|
||||||
|
|
||||||
ipv4 = mkOption {
|
ipv4 = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
|
@ -68,7 +67,7 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
users = {
|
users = {
|
||||||
users = {
|
users = {
|
||||||
|
@ -98,37 +97,96 @@ in {
|
||||||
timerConfig = { OnCalendar = cfg.frequency; };
|
timerConfig = { OnCalendar = cfg.frequency; };
|
||||||
};
|
};
|
||||||
|
|
||||||
services.backplane-dns-client-pw-file = {
|
services = let sshfp-file = "/tmp/${hostname}-sshfp/fingerprints";
|
||||||
enable = true;
|
in {
|
||||||
requiredBy = [ "backplane-dns-client.service" ];
|
backplane-dns-client-pw-file = {
|
||||||
reloadIfChanged = true;
|
requiredBy = [ "backplane-dns-client.service" ];
|
||||||
serviceConfig = { Type = "oneshot"; };
|
reloadIfChanged = true;
|
||||||
script = ''
|
serviceConfig = { Type = "oneshot"; };
|
||||||
chmod 400 ${cfg.password-file}
|
script = ''
|
||||||
chown ${cfg.user} ${cfg.password-file}
|
chmod 400 ${cfg.password-file}
|
||||||
'';
|
chown ${cfg.user} ${cfg.password-file}
|
||||||
};
|
|
||||||
|
|
||||||
services.backplane-dns-client = {
|
|
||||||
enable = true;
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
StandardOutput = "journal";
|
|
||||||
User = cfg.user;
|
|
||||||
ExecStart = pkgs.writeShellScript "start-backplane-dns-client.sh" ''
|
|
||||||
${pkgs.backplane-dns-client}/bin/backplane-dns-client ${
|
|
||||||
optionalString cfg.ipv4 "-4"
|
|
||||||
} ${optionalString cfg.ipv6 "-6"} ${
|
|
||||||
optionalString cfg.sshfp ssh-key-args
|
|
||||||
} ${
|
|
||||||
optionalString (cfg.external-interface != null)
|
|
||||||
"--interface=${cfg.external-interface}"
|
|
||||||
} --domain=${cfg.domain} --server=${cfg.server} --password-file=${cfg.password-file}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
# Needed to generate SSH fingerprinst
|
|
||||||
path = [ pkgs.openssh ];
|
backplane-dns-generate-sshfps = mkIf cfg.sshfp {
|
||||||
reloadIfChanged = true;
|
requiredBy = [ "backplane-dns-client.service" ];
|
||||||
|
before = [ "backplane-dns-client.service" ];
|
||||||
|
path = with pkgs; [ coreutils openssh ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
#ProtectSystem = true;
|
||||||
|
#LockPersonality = true;
|
||||||
|
#PermissionsStartOnly = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
LimitNOFILE = 1024;
|
||||||
|
ReadWritePaths = [ (dirOf sshfp-file) ];
|
||||||
|
};
|
||||||
|
script = let
|
||||||
|
keyPaths = map (key: key.path) config.services.openssh.hostKeys;
|
||||||
|
keyGenCmds = map (path:
|
||||||
|
''
|
||||||
|
ssh-keygen -r hostname -f "${path}" | sed 's/hostname IN SSHFP '// >> ${sshfp-file}'')
|
||||||
|
keyPaths;
|
||||||
|
in ''
|
||||||
|
[ -f ${sshfp-file} ] && rm -f ${sshfp-file}
|
||||||
|
SSHFP_DIR=$(dirname ${sshfp-file})
|
||||||
|
[ -d $SSHFP_DIR ] || mkdir $SSHFP_DIR
|
||||||
|
chown ${cfg.user} $SSHFP_DIR
|
||||||
|
chmod go-rwx $SSHFP_DIR
|
||||||
|
${concatStringsSep "\n" keyGenCmds}
|
||||||
|
chown ${cfg.user} ${sshfp-file}
|
||||||
|
chmod 600 ${sshfp-file}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
backplane-dns-client = {
|
||||||
|
enable = true;
|
||||||
|
path = with pkgs; [ coreutils ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
StandardOutput = "journal";
|
||||||
|
User = cfg.user;
|
||||||
|
ExecStart = pkgs.writeShellScript "start-backplane-dns-client.sh" ''
|
||||||
|
SSHFP_ARGS=""
|
||||||
|
${optionalString cfg.sshfp ''
|
||||||
|
while read LINE; do SSHFP_ARGS="$SSHFP_ARGS --ssh-fp=\"$LINE\""; done < ${sshfp-file}
|
||||||
|
''}
|
||||||
|
CMD="${pkgs.backplaneDnsClient}/bin/backplane-dns-client ${
|
||||||
|
optionalString cfg.ipv4 "-4"
|
||||||
|
} ${optionalString cfg.ipv6 "-6"} ${
|
||||||
|
optionalString cfg.sshfp "$SSHFP_ARGS"
|
||||||
|
} ${
|
||||||
|
optionalString (cfg.external-interface != null)
|
||||||
|
"--interface=${cfg.external-interface}"
|
||||||
|
} --domain=${cfg.domain} --server=${cfg.server} --password-file=${cfg.password-file}"
|
||||||
|
echo $CMD
|
||||||
|
$CMD
|
||||||
|
'';
|
||||||
|
ExecStartPost = mkIf cfg.sshfp "rm ${sshfp-file}";
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
ProtectSystem = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
PermissionsStartOnly = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
ReadOnlyPaths = [ sshfp-file ];
|
||||||
|
LimitNOFILE = 1024;
|
||||||
|
};
|
||||||
|
reloadIfChanged = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,6 +33,7 @@ with lib; {
|
||||||
./minecraft-clj.nix
|
./minecraft-clj.nix
|
||||||
./minecraft-server.nix
|
./minecraft-server.nix
|
||||||
./netinfo-email.nix
|
./netinfo-email.nix
|
||||||
|
./nexus.nix
|
||||||
./node-exporter.nix
|
./node-exporter.nix
|
||||||
./nsd.nix
|
./nsd.nix
|
||||||
./password.nix
|
./password.nix
|
||||||
|
|
|
@ -20,6 +20,13 @@ let
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ksk = {
|
||||||
|
key-file = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Key-signing key for this zone.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
zone-definition = mkOption {
|
zone-definition = mkOption {
|
||||||
type = submodule (import ../types/zone-definition.nix);
|
type = submodule (import ../types/zone-definition.nix);
|
||||||
description =
|
description =
|
||||||
|
@ -64,25 +71,30 @@ in {
|
||||||
allowedUDPPorts = [ 53 ];
|
allowedUDPPorts = [ 53 ];
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/var/lib/nsd" = {
|
# fileSystems."/var/lib/nsd" = {
|
||||||
device = cfg.state-directory;
|
# device = cfg.state-directory;
|
||||||
options = [ "bind" ];
|
# options = [ "bind" ];
|
||||||
};
|
# };
|
||||||
|
|
||||||
services.nsd = {
|
fudo = {
|
||||||
enable = true;
|
nsd = {
|
||||||
identity = cfg.identity;
|
enable = true;
|
||||||
interfaces = cfg.listen-ips;
|
identity = cfg.identity;
|
||||||
# stateDir = cfg.state-directory;
|
interfaces = cfg.listen-ips;
|
||||||
zones = mapAttrs' (dom: dom-cfg:
|
stateDirectory = cfg.state-directory;
|
||||||
let net-cfg = dom-cfg.zone-definition;
|
zones = mapAttrs' (dom: dom-cfg:
|
||||||
in nameValuePair "${dom}." {
|
let net-cfg = dom-cfg.zone-definition;
|
||||||
dnssec = dom-cfg.dnssec;
|
in nameValuePair "${dom}." {
|
||||||
|
dnssec = dom-cfg.dnssec;
|
||||||
|
|
||||||
data = pkgs.lib.dns.zoneToZonefile config.instance.build-timestamp dom
|
ksk.keyFile = dom-cfg.ksk.key-file;
|
||||||
dom-cfg.zone-definition;
|
|
||||||
|
|
||||||
}) cfg.domains;
|
data =
|
||||||
|
pkgs.lib.dns.zoneToZonefile config.instance.build-timestamp dom
|
||||||
|
dom-cfg.zone-definition;
|
||||||
|
|
||||||
|
}) cfg.domains;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,21 @@ let
|
||||||
default = "admin@${domain}";
|
default = "admin@${domain}";
|
||||||
};
|
};
|
||||||
|
|
||||||
grafana-hosts = mkOption {
|
metrics = mkOption {
|
||||||
type = listOf str;
|
type = nullOr (submodule {
|
||||||
description =
|
options = {
|
||||||
"List of hosts acting as Grafana metric analyzers. Requires prometheus hosts as well.";
|
grafana-host = mkOption {
|
||||||
default = [ ];
|
type = str;
|
||||||
|
description = "Hostname of the Grafana Metrics Analysis tool.";
|
||||||
|
};
|
||||||
|
prometheus-host = mkOption {
|
||||||
|
type = str;
|
||||||
|
description =
|
||||||
|
"Hostname of the Prometheus Metrics Aggregator tool.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
log-aggregator = mkOption {
|
log-aggregator = mkOption {
|
||||||
|
@ -120,8 +130,9 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
gssapi-realm = mkOption {
|
gssapi-realm = mkOption {
|
||||||
type = str;
|
type = nullOr str;
|
||||||
description = "GSSAPI (i.e. Kerberos) realm of this domain.";
|
description = "GSSAPI (i.e. Kerberos) realm of this domain.";
|
||||||
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
kerberos-master = mkOption {
|
kerberos-master = mkOption {
|
||||||
|
@ -145,13 +156,6 @@ 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.";
|
||||||
|
@ -182,6 +186,28 @@ let
|
||||||
description = "Name of the DNS zone associated with domain.";
|
description = "Name of the DNS zone associated with domain.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
nexus = {
|
||||||
|
public-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "Nexus domains to which hosts in this domain belong.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
private-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"Nexus private domains to which hosts in this domain belong.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
tailscale-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"Nexus tailscale domains to which hosts in this domain belong.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -123,19 +123,13 @@ in {
|
||||||
passwordFile = cfg.database.password-file;
|
passwordFile = cfg.database.password-file;
|
||||||
type = "postgres";
|
type = "postgres";
|
||||||
};
|
};
|
||||||
domain = cfg.hostname;
|
|
||||||
httpAddress = "127.0.0.1";
|
|
||||||
httpPort = cfg.local-port;
|
|
||||||
repositoryRoot = cfg.repository-dir;
|
repositoryRoot = cfg.repository-dir;
|
||||||
stateDir = cfg.state-dir;
|
stateDir = cfg.state-dir;
|
||||||
rootUrl = "https://${cfg.hostname}/";
|
|
||||||
user = mkIf (cfg.user != null) cfg.user;
|
user = mkIf (cfg.user != null) cfg.user;
|
||||||
ssh = {
|
|
||||||
#enable = true;
|
|
||||||
clonePort = cfg.ssh.listen-port;
|
|
||||||
};
|
|
||||||
settings = mkIf (cfg.ssh != null) {
|
settings = mkIf (cfg.ssh != null) {
|
||||||
server = {
|
server = {
|
||||||
|
START_SSH_SERVER = true;
|
||||||
|
|
||||||
# Displayed in the clone URL
|
# Displayed in the clone URL
|
||||||
SSH_DOMAIN = cfg.hostname;
|
SSH_DOMAIN = cfg.hostname;
|
||||||
SSH_PORT = mkForce cfg.ssh.listen-port;
|
SSH_PORT = mkForce cfg.ssh.listen-port;
|
||||||
|
@ -143,6 +137,11 @@ in {
|
||||||
# Actual ip/port on which to listen
|
# Actual ip/port on which to listen
|
||||||
SSH_LISTEN_PORT = cfg.ssh.listen-port;
|
SSH_LISTEN_PORT = cfg.ssh.listen-port;
|
||||||
SSH_LISTEN_HOST = cfg.ssh.listen-ip;
|
SSH_LISTEN_HOST = cfg.ssh.listen-ip;
|
||||||
|
|
||||||
|
ROOT_URL = "https://${cfg.hostname}/";
|
||||||
|
HTTP_ADDR = "127.0.0.1";
|
||||||
|
HTTP_PORT = cfg.local-port;
|
||||||
|
DOMAIN = cfg.hostname;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -105,34 +105,40 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
database = {
|
oauth = let
|
||||||
name = mkOption {
|
oauthOpts.options = {
|
||||||
type = str;
|
hostname = mkOption {
|
||||||
description = "Database name.";
|
type = str;
|
||||||
default = "grafana";
|
description = "Host of the OAuth server.";
|
||||||
};
|
};
|
||||||
hostname = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "Hostname of the database server.";
|
|
||||||
default = "localhost";
|
|
||||||
};
|
|
||||||
user = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "Database username.";
|
|
||||||
default = "grafana";
|
|
||||||
};
|
|
||||||
password-file = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "File containing the database user's password.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
ldap = mkOption {
|
client-id = mkOption {
|
||||||
type = nullOr (submodule ldapOpts);
|
type = str;
|
||||||
description = "";
|
description = "Path to file containing the Grafana OAuth client ID.";
|
||||||
|
};
|
||||||
|
|
||||||
|
client-secret = mkOption {
|
||||||
|
type = str;
|
||||||
|
description =
|
||||||
|
"Path to file containing the Grafana OAuth client secret.";
|
||||||
|
};
|
||||||
|
|
||||||
|
slug = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "The application slug on the OAuth server.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in mkOption {
|
||||||
|
type = nullOr (submodule oauthOpts);
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# ldap = mkOption {
|
||||||
|
# type = nullOr (submodule ldapOpts);
|
||||||
|
# description = "";
|
||||||
|
# default = null;
|
||||||
|
# };
|
||||||
|
|
||||||
admin-password-file = mkOption {
|
admin-password-file = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Path to a file containing the admin user's password.";
|
description = "Path to a file containing the admin user's password.";
|
||||||
|
@ -164,20 +170,16 @@ in {
|
||||||
tmpfiles.rules =
|
tmpfiles.rules =
|
||||||
let grafana-user = config.systemd.services.grafana.serviceConfig.User;
|
let grafana-user = config.systemd.services.grafana.serviceConfig.User;
|
||||||
in [ "d ${cfg.state-directory} 0700 ${grafana-user} - - -" ];
|
in [ "d ${cfg.state-directory} 0700 ${grafana-user} - - -" ];
|
||||||
|
|
||||||
services.grafana.serviceConfig = {
|
|
||||||
EnvironmentFile = host-secrets.grafana-environment-file.target-file;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fudo.secrets.host-secrets.${hostname}.grafana-environment-file = {
|
# fudo.secrets.host-secrets.${hostname}.grafana-environment-file = {
|
||||||
source-file = pkgs.writeText "grafana.env" ''
|
# source-file = pkgs.writeText "grafana.env" ''
|
||||||
${optionalString (cfg.ldap != null)
|
# ${optionalString (cfg.ldap != null)
|
||||||
''GRAFANA_LDAP_BIND_PASSWD="${cfg.ldap.bind-passwd}"''}
|
# ''GRAFANA_LDAP_BIND_PASSWD="${cfg.ldap.bind-passwd}"''}
|
||||||
'';
|
# '';
|
||||||
target-file = "/run/metrics/grafana/auth-bind.passwd";
|
# target-file = "/run/metrics/grafana/auth-bind.passwd";
|
||||||
user = config.systemd.services.grafana.serviceConfig.User;
|
# user = config.systemd.services.grafana.serviceConfig.User;
|
||||||
};
|
# };
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
nginx = {
|
nginx = {
|
||||||
|
@ -226,49 +228,31 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
database = {
|
database = {
|
||||||
host = cfg.database.hostname;
|
type = "sqlite3";
|
||||||
name = cfg.database.name;
|
path = "${cfg.state-directory}/database.sqlite";
|
||||||
user = cfg.database.user;
|
|
||||||
password = "$__file{${cfg.database.password-file}}";
|
|
||||||
type = "postgres";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
"ldap.auth" = mkIf (cfg.ldap != null) (let
|
auth = mkIf (!isNull cfg.oauth) {
|
||||||
base = cfg.ldap.base-dn;
|
signout_redirect_url =
|
||||||
|
"https://${cfg.oauth.hostname}/application/o/${cfg.oauth.slug}/end-session/";
|
||||||
|
oauth_auto_login = true;
|
||||||
|
};
|
||||||
|
|
||||||
config-file = pkgs.writeText "grafana-ldap.toml" ''
|
"auth.generic_oauth" = mkIf (!isNull cfg.oauth) {
|
||||||
[[servers]]
|
name = "Authentik";
|
||||||
host = "${concatStringsSep " " cfg.ldap.hosts}"
|
|
||||||
port = 389
|
|
||||||
start_tls = true
|
|
||||||
|
|
||||||
bind_dn = "uid=%s,ou=members,${base}"
|
|
||||||
|
|
||||||
search_filter = "(uid=%s)"
|
|
||||||
search_base_dns = [ "ou=members,${base}" ]
|
|
||||||
|
|
||||||
group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
|
|
||||||
group_search_base_dns = ["ou=groups,${base}"]
|
|
||||||
group_search_filter_user_attribute = "uid"
|
|
||||||
|
|
||||||
[[servers.group_mappings]]
|
|
||||||
group_dn = "cn=admin,ou=groups,${base}"
|
|
||||||
org_role = "Admin"
|
|
||||||
grafana_admin = true
|
|
||||||
|
|
||||||
[[servers.group_mappings]]
|
|
||||||
group_dn = "cn=*,ou=groups,${base}"
|
|
||||||
org_role = "Viewer"
|
|
||||||
'';
|
|
||||||
in {
|
|
||||||
enabled = true;
|
enabled = true;
|
||||||
allow_sign_up = true;
|
client_id = "$__file{${cfg.oauth.client-id}}";
|
||||||
config_file = "${config-file}";
|
client_secret = "$__file{${cfg.oauth.client-secret}}";
|
||||||
|
scopes = "openid email profile";
|
||||||
# AUTH_LDAP_ENABLED = "true";
|
auth_url = "https://${cfg.oauth.hostname}/application/o/authorize/";
|
||||||
# AUTH_LDAP_ALLOW_SIGN_UP = "true";
|
token_url = "https://${cfg.oauth.hostname}/application/o/token/";
|
||||||
# AUTH_LDAP_CONFIG_FILE = config-file;
|
api_url = "https://${cfg.oauth.hostname}/application/o/userinfo/";
|
||||||
});
|
role_attribute_path = concatStringsSep " || " [
|
||||||
|
"contains(groups[*], 'Metrics Admin') && 'Admin'"
|
||||||
|
"contains(groups[*], 'Metrics Editor') && 'Editor'"
|
||||||
|
"'Viewer'"
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
provision = {
|
provision = {
|
||||||
|
|
|
@ -4,120 +4,114 @@ with lib;
|
||||||
let
|
let
|
||||||
hostname = config.instance.hostname;
|
hostname = config.instance.hostname;
|
||||||
host-filesystems = config.fudo.hosts.${hostname}.encrypted-filesystems;
|
host-filesystems = config.fudo.hosts.${hostname}.encrypted-filesystems;
|
||||||
|
|
||||||
optionalOrDefault = str: default: if (str != null) then str else default;
|
optionalOrDefault = str: default: if (str != null) then str else default;
|
||||||
|
|
||||||
filesystemsToMountpointLists = mapAttrsToList
|
filesystemsToMountpointLists =
|
||||||
(fs: fsOpts: fsOpts.mountpoints);
|
mapAttrsToList (fs: fsOpts: fsOpts.mountpoints);
|
||||||
|
|
||||||
concatMapAttrs = f: as: concatMap (i: i) (mapAttrsToList f as);
|
concatMapAttrs = f: as: concatMap (i: i) (mapAttrsToList f as);
|
||||||
|
|
||||||
concatMapAttrsToList = f: attrs:
|
concatMapAttrsToList = f: attrs: concatMap (i: i) (mapAttrsToList f attrs);
|
||||||
concatMap (i: i) (mapAttrsToList f attrs);
|
|
||||||
|
|
||||||
in {
|
in {
|
||||||
config = {
|
config = {
|
||||||
users.groups = let
|
users.groups = let
|
||||||
site-name = config.instance.local-site;
|
site-name = config.instance.local-site;
|
||||||
site-hosts = filterAttrs
|
site-hosts = filterAttrs (hostname: hostOpts: hostOpts.site == site-name)
|
||||||
(hostname: hostOpts: hostOpts.site == site-name)
|
|
||||||
config.fudo.hosts;
|
config.fudo.hosts;
|
||||||
site-mountpoints = concatMapAttrsToList
|
site-mountpoints = concatMapAttrsToList (host: hostOpts:
|
||||||
(host: hostOpts: concatMapAttrsToList
|
concatMapAttrsToList (fs: fsOpts: attrValues fsOpts.mountpoints)
|
||||||
(fs: fsOpts: attrValues fsOpts.mountpoints)
|
hostOpts.encrypted-filesystems) site-hosts;
|
||||||
hostOpts.encrypted-filesystems)
|
|
||||||
site-hosts;
|
|
||||||
in listToAttrs
|
in listToAttrs
|
||||||
(map (mp: nameValuePair mp.group { members = mp.users; })
|
(map (mp: nameValuePair mp.group { members = mp.users; }) site-mountpoints);
|
||||||
site-mountpoints);
|
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
# Ensure the mountpoints exist
|
# Ensure the mountpoints exist
|
||||||
tmpfiles.rules = let
|
tmpfiles.rules = let
|
||||||
mpPerms = mpOpts: if mpOpts.world-readable then "755" else "750";
|
mpPerms = mpOpts: if mpOpts.world-readable then "755" else "750";
|
||||||
mountpointToPath = mp: mpOpts:
|
mountpointToPath = mp: mpOpts:
|
||||||
"d '${mp}' ${mpPerms mpOpts} root ${optionalOrDefault mpOpts.group "-"} - -";
|
"d '${mp}' ${mpPerms mpOpts} root ${
|
||||||
filesystemsToMountpointLists = mapAttrsToList
|
optionalOrDefault mpOpts.group "-"
|
||||||
(fs: fsOpts: fsOpts.mountpoints);
|
} - -";
|
||||||
mountpointListsToPaths = concatMap
|
filesystemsToMountpointLists =
|
||||||
(mps: mapAttrsToList mountpointToPath mps);
|
mapAttrsToList (fs: fsOpts: fsOpts.mountpoints);
|
||||||
|
mountpointListsToPaths =
|
||||||
|
concatMap (mps: mapAttrsToList mountpointToPath mps);
|
||||||
in mountpointListsToPaths (filesystemsToMountpointLists host-filesystems);
|
in mountpointListsToPaths (filesystemsToMountpointLists host-filesystems);
|
||||||
|
|
||||||
# Actual mounts of decrypted filesystems
|
# Actual mounts of decrypted filesystems
|
||||||
mounts = let
|
mounts = let
|
||||||
filesystems = mapAttrsToList
|
filesystems = mapAttrsToList (fs: opts: {
|
||||||
(fs: opts: { filesystem = fs; opts = opts; })
|
filesystem = fs;
|
||||||
host-filesystems;
|
opts = opts;
|
||||||
|
}) host-filesystems;
|
||||||
|
|
||||||
mounts = concatMap
|
mounts = concatMap (fs:
|
||||||
(fs: mapAttrsToList
|
mapAttrsToList (mp: mp-opts: {
|
||||||
(mp: mp-opts:
|
what = "/dev/mapper/${fs.filesystem}";
|
||||||
{
|
type = fs.opts.filesystem-type;
|
||||||
what = "/dev/mapper/${fs.filesystem}";
|
where = mp;
|
||||||
type = fs.opts.filesystem-type;
|
options = concatStringsSep "," (fs.opts.options ++ mp-opts.options);
|
||||||
where = mp;
|
description =
|
||||||
options = concatStringsSep "," (fs.opts.options ++ mp-opts.options);
|
"${fs.opts.filesystem-type} filesystem on ${fs.filesystem} mounted to ${mp}";
|
||||||
description = "${fs.opts.filesystem-type} filesystem on ${fs.filesystem} mounted to ${mp}";
|
requires = [ "${fs.filesystem}-decrypt.service" ];
|
||||||
requires = [ "${fs.filesystem}-decrypt.service" ];
|
partOf = [ "${fs.filesystem}.target" ];
|
||||||
partOf = [ "${fs.filesystem}.target" ];
|
wantedBy = [ "${fs.filesystem}.target" ];
|
||||||
wantedBy = [ "${fs.filesystem}.target" ];
|
}) fs.opts.mountpoints) filesystems;
|
||||||
})
|
|
||||||
fs.opts.mountpoints)
|
|
||||||
filesystems;
|
|
||||||
in mounts;
|
in mounts;
|
||||||
|
|
||||||
# Jobs to decrypt the encrypted devices
|
# Jobs to decrypt the encrypted devices
|
||||||
services = mapAttrs' (filesystem-name: opts:
|
services = mapAttrs' (filesystem-name: opts:
|
||||||
nameValuePair "${filesystem-name}-decrypt"
|
nameValuePair "${filesystem-name}-decrypt" {
|
||||||
{
|
description =
|
||||||
description = "Decrypt the ${filesystem-name} filesystem when the key is available at ${opts.key-path}";
|
"Decrypt the ${filesystem-name} filesystem when the key is available at ${opts.key-path}";
|
||||||
path = with pkgs; [ cryptsetup ];
|
path = with pkgs; [ cryptsetup ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
ExecStart = pkgs.writeShellScript "decrypt-${filesystem-name}.sh" ''
|
ExecStart = pkgs.writeShellScript "decrypt-${filesystem-name}.sh" ''
|
||||||
[ -e /dev/mapper/${filesystem-name} ] || cryptsetup open --type luks --key-file ${opts.key-path} ${opts.encrypted-device} ${filesystem-name}
|
[ -e /dev/mapper/${filesystem-name} ] || cryptsetup open --type ${opts.type} --key-file ${opts.key-path} ${opts.encrypted-device} ${filesystem-name}
|
||||||
'';
|
'';
|
||||||
ExecStartPost = pkgs.writeShellScript "remove-${filesystem-name}-key.sh" ''
|
ExecStartPost = mkIf opts.remove-key
|
||||||
|
(pkgs.writeShellScript "remove-${filesystem-name}-key.sh" ''
|
||||||
rm ${opts.key-path}
|
rm ${opts.key-path}
|
||||||
'';
|
'');
|
||||||
ExecStop = pkgs.writeShellScript "close-${filesystem-name}.sh" ''
|
ExecStop = pkgs.writeShellScript "close-${filesystem-name}.sh" ''
|
||||||
cryptsetup close /dev/mapper/${filesystem-name}
|
cryptsetup close /dev/mapper/${filesystem-name}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
restartIfChanged = true;
|
restartIfChanged = true;
|
||||||
})
|
}) host-filesystems;
|
||||||
host-filesystems;
|
|
||||||
|
|
||||||
# Watch the path of the key, trigger decrypt when it's available
|
# Watch the path of the key, trigger decrypt when it's available
|
||||||
paths = let
|
paths = let
|
||||||
decryption-jobs = mapAttrs' (filesystem-name: opts:
|
decryption-jobs = mapAttrs' (filesystem-name: opts:
|
||||||
nameValuePair "${filesystem-name}-decrypt"
|
nameValuePair "${filesystem-name}-decrypt" {
|
||||||
{
|
wantedBy = [ "default.target" ];
|
||||||
wantedBy = [ "default.target" ];
|
description =
|
||||||
description = "Watch for decryption key, then decrypt the target filesystem.";
|
"Watch for decryption key, then decrypt the target filesystem.";
|
||||||
pathConfig = {
|
pathConfig = {
|
||||||
PathExists = opts.key-path;
|
PathExists = opts.key-path;
|
||||||
Unit = "${filesystem-name}-decrypt.service";
|
Unit = "${filesystem-name}-decrypt.service";
|
||||||
};
|
};
|
||||||
}) host-filesystems;
|
}) host-filesystems;
|
||||||
|
|
||||||
post-decryption-jobs = mapAttrs' (filesystem-name: opts:
|
post-decryption-jobs = mapAttrs' (filesystem-name: opts:
|
||||||
nameValuePair "${filesystem-name}-mount"
|
nameValuePair "${filesystem-name}-mount" {
|
||||||
{
|
wantedBy = [ "default.target" ];
|
||||||
wantedBy = [ "default.target" ];
|
description =
|
||||||
description = "Mount ${filesystem-name} filesystems once the decrypted device is available.";
|
"Mount ${filesystem-name} filesystems once the decrypted device is available.";
|
||||||
pathConfig = {
|
pathConfig = {
|
||||||
PathExists = "/dev/mapper/${filesystem-name}";
|
PathExists = "/dev/mapper/${filesystem-name}";
|
||||||
Unit = "${filesystem-name}.target";
|
Unit = "${filesystem-name}.target";
|
||||||
};
|
};
|
||||||
}) host-filesystems;
|
}) host-filesystems;
|
||||||
in decryption-jobs // post-decryption-jobs;
|
in decryption-jobs // post-decryption-jobs;
|
||||||
|
|
||||||
targets = mapAttrs (filesystem-name: opts:
|
targets = mapAttrs (filesystem-name: opts: {
|
||||||
{
|
description = "${filesystem-name} enabled and available.";
|
||||||
description = "${filesystem-name} enabled and available.";
|
}) host-filesystems;
|
||||||
}) host-filesystems;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,13 +43,13 @@ in {
|
||||||
firewall = mkIf ((length host-cfg.external-interfaces) > 0) {
|
firewall = mkIf ((length host-cfg.external-interfaces) > 0) {
|
||||||
enable = true;
|
enable = true;
|
||||||
allowedTCPPorts = [ 22 2112 ]; # Make sure _at least_ SSH is allowed
|
allowedTCPPorts = [ 22 2112 ]; # Make sure _at least_ SSH is allowed
|
||||||
trustedInterfaces = let
|
trustedInterfaces =
|
||||||
all-interfaces = attrNames config.networking.interfaces;
|
let all-interfaces = attrNames config.networking.interfaces;
|
||||||
in subtractLists host-cfg.external-interfaces all-interfaces;
|
in subtractLists host-cfg.external-interfaces all-interfaces;
|
||||||
};
|
};
|
||||||
|
|
||||||
hostId = mkIf (host-cfg.machine-id != null)
|
hostId =
|
||||||
(substring 0 8 host-cfg.machine-id);
|
mkIf (host-cfg.machine-id != null) (substring 0 8 host-cfg.machine-id);
|
||||||
};
|
};
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
|
@ -78,13 +78,15 @@ in {
|
||||||
mode = "0444";
|
mode = "0444";
|
||||||
};
|
};
|
||||||
|
|
||||||
current-system-packages.text = with builtins; let
|
current-system-packages.text = with builtins;
|
||||||
packages = map (p: "${p.name}")
|
let
|
||||||
config.environment.systemPackages;
|
packages = map (p: "${p.name}") config.environment.systemPackages;
|
||||||
sorted-unique = sort lessThan (unique packages);
|
sorted-unique = sort lessThan (unique packages);
|
||||||
in "${concatStringsSep "\n" sorted-unique}\n";
|
in ''
|
||||||
|
${concatStringsSep "\n" sorted-unique}
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemPackages = with pkgs;
|
systemPackages = with pkgs;
|
||||||
mkIf (host-cfg.docker-server) [ docker nix-prefetch-docker ];
|
mkIf (host-cfg.docker-server) [ docker nix-prefetch-docker ];
|
||||||
};
|
};
|
||||||
|
@ -105,10 +107,9 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
programs.adb.enable = host-cfg.android-dev;
|
programs.adb.enable = host-cfg.android-dev;
|
||||||
users.groups.adbusers = mkIf host-cfg.android-dev {
|
users.groups.adbusers =
|
||||||
members = config.instance.local-admins;
|
mkIf host-cfg.android-dev { members = config.instance.local-admins; };
|
||||||
};
|
|
||||||
|
|
||||||
boot.tmpOnTmpfs = host-cfg.tmp-on-tmpfs;
|
boot.tmp.useTmpfs = host-cfg.tmp-on-tmpfs;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,47 +5,46 @@ let
|
||||||
hostname = config.instance.hostname;
|
hostname = config.instance.hostname;
|
||||||
|
|
||||||
host-secrets = config.fudo.secrets.host-secrets.${hostname};
|
host-secrets = config.fudo.secrets.host-secrets.${hostname};
|
||||||
|
|
||||||
siteOpts = { ... }: with types; {
|
|
||||||
options = {
|
|
||||||
enableACME = mkOption {
|
|
||||||
type = bool;
|
|
||||||
description = "Use ACME to get SSL certificates for this site.";
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
hostname = mkOption {
|
siteOpts = { ... }:
|
||||||
type = str;
|
with types; {
|
||||||
description = "Hostname of this server.";
|
options = {
|
||||||
};
|
enableACME = mkOption {
|
||||||
|
type = bool;
|
||||||
site-config = mkOption {
|
description = "Use ACME to get SSL certificates for this site.";
|
||||||
type = attrs;
|
default = true;
|
||||||
description = "Site-specific configuration.";
|
};
|
||||||
|
|
||||||
|
hostname = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Hostname of this server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
site-config = mkOption {
|
||||||
|
type = attrs;
|
||||||
|
description = "Site-specific configuration.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
config-dir = dirOf cfg.config-file;
|
config-dir = dirOf cfg.config-file;
|
||||||
|
|
||||||
concatMapAttrs = f: attrs:
|
concatMapAttrs = f: attrs: foldr (a: b: a // b) { } (mapAttrs f attrs);
|
||||||
foldr (a: b: a // b) {} (mapAttrs f attrs);
|
|
||||||
|
|
||||||
concatMapAttrsToList = f: attr:
|
concatMapAttrsToList = f: attr:
|
||||||
concatMap (i: i) (attrValues (mapAttrs f attr));
|
concatMap (i: i) (attrValues (mapAttrs f attr));
|
||||||
|
|
||||||
host-domains = config.fudo.acme.host-domains.${hostname};
|
host-domains = config.fudo.acme.host-domains.${hostname};
|
||||||
|
|
||||||
hostCerts = host: let
|
hostCerts = host:
|
||||||
cert-copy = host-domains.${host}.local-copies.ejabberd;
|
let cert-copy = host-domains.${host}.local-copies.ejabberd;
|
||||||
in [
|
in [
|
||||||
cert-copy.certificate
|
cert-copy.certificate
|
||||||
cert-copy.private-key
|
cert-copy.private-key
|
||||||
# cert-copy.full-certificate
|
# cert-copy.full-certificate
|
||||||
];
|
];
|
||||||
|
|
||||||
hostCertService = host:
|
hostCertService = host: host-domains.${host}.local-copies.ejabberd.service;
|
||||||
host-domains.${host}.local-copies.ejabberd.service;
|
|
||||||
|
|
||||||
config-file-template = let
|
config-file-template = let
|
||||||
jabber-config = {
|
jabber-config = {
|
||||||
|
@ -60,8 +59,7 @@ let
|
||||||
|
|
||||||
acl.admin = {
|
acl.admin = {
|
||||||
user = concatMap
|
user = concatMap
|
||||||
(admin: map (site: "${admin}@${site}")
|
(admin: map (site: "${admin}@${site}") (attrNames cfg.sites))
|
||||||
(attrNames cfg.sites))
|
|
||||||
cfg.admins;
|
cfg.admins;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,48 +75,41 @@ let
|
||||||
starttls = true;
|
starttls = true;
|
||||||
starttls_required = true;
|
starttls_required = true;
|
||||||
};
|
};
|
||||||
in
|
in if (cfg.listen-ips != null) then
|
||||||
if (cfg.listen-ips != null) then
|
map (ip: { ip = ip; } // common) cfg.listen-ips
|
||||||
map (ip: { ip = ip; } // common)
|
else
|
||||||
cfg.listen-ips
|
[ common ];
|
||||||
else [ common ];
|
|
||||||
|
|
||||||
certfiles = concatMapAttrsToList
|
certfiles = concatMapAttrsToList (site: siteOpts:
|
||||||
(site: siteOpts:
|
if (siteOpts.enableACME) then (hostCerts siteOpts.hostname) else [ ])
|
||||||
if (siteOpts.enableACME) then
|
|
||||||
(hostCerts siteOpts.hostname)
|
|
||||||
else [])
|
|
||||||
cfg.sites;
|
cfg.sites;
|
||||||
|
|
||||||
host_config =
|
host_config = mapAttrs (site: siteOpts: siteOpts.site-config) cfg.sites;
|
||||||
mapAttrs (site: siteOpts: siteOpts.site-config)
|
|
||||||
cfg.sites;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config-file = builtins.toJSON jabber-config;
|
config-file = builtins.toJSON jabber-config;
|
||||||
in pkgs.lib.text.format-json-file
|
in pkgs.lib.text.format-json-file
|
||||||
(pkgs.writeText "ejabberd.config.yml.template" config-file);
|
(pkgs.writeText "ejabberd.config.yml.template" config-file);
|
||||||
|
|
||||||
enter-secrets = template: secrets: target: let
|
enter-secrets = template: secrets: target:
|
||||||
secret-swappers = map
|
let
|
||||||
(secret: "sed s/${secret}/\$${secret}/g")
|
secret-swappers = map (secret: "sed s/${secret}/\$${secret}/g") secrets;
|
||||||
secrets;
|
swapper = concatStringsSep " | " secret-swappers;
|
||||||
swapper = concatStringsSep " | " secret-swappers;
|
in pkgs.writeShellScript "ejabberd-generate-config.sh" ''
|
||||||
in pkgs.writeShellScript "ejabberd-generate-config.sh" ''
|
[ -f \$''${target} ] && rm -f ${target}
|
||||||
[ -f \$${target} ] && rm -f ${target}
|
echo "Copying from ${template} to ${target}"
|
||||||
echo "Copying from ${template} to ${target}"
|
touch ${target}
|
||||||
touch ${target}
|
chmod go-rwx ${target}
|
||||||
chmod go-rwx ${target}
|
chmod u+rw ${target}
|
||||||
chmod u+rw ${target}
|
cat ${template} | ${swapper} > ${target}
|
||||||
cat ${template} | ${swapper} > ${target}
|
echo "Copying from ${template} to ${target} completed"
|
||||||
echo "Copying from ${template} to ${target} completed"
|
'';
|
||||||
'';
|
|
||||||
|
|
||||||
cfg = config.fudo.jabber;
|
cfg = config.fudo.jabber;
|
||||||
|
|
||||||
log-dir = "${cfg.state-directory}/logs";
|
log-dir = "${cfg.state-directory}/logs";
|
||||||
spool-dir = "${cfg.state-directory}/spool";
|
spool-dir = "${cfg.state-directory}/spool";
|
||||||
|
|
||||||
in {
|
in {
|
||||||
options.fudo.jabber = with types; {
|
options.fudo.jabber = with types; {
|
||||||
enable = mkEnableOption "Enable ejabberd server.";
|
enable = mkEnableOption "Enable ejabberd server.";
|
||||||
|
@ -128,7 +119,7 @@ in {
|
||||||
description = "IPs on which to listen for Jabber connections.";
|
description = "IPs on which to listen for Jabber connections.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = port;
|
type = port;
|
||||||
description = "Port on which to listen for Jabber connections.";
|
description = "Port on which to listen for Jabber connections.";
|
||||||
|
@ -150,7 +141,7 @@ in {
|
||||||
admins = mkOption {
|
admins = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of admin users for the server.";
|
description = "List of admin users for the server.";
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
sites = mkOption {
|
sites = mkOption {
|
||||||
|
@ -160,8 +151,9 @@ in {
|
||||||
|
|
||||||
secret-files = mkOption {
|
secret-files = mkOption {
|
||||||
type = attrsOf str;
|
type = attrsOf str;
|
||||||
description = "Map of secret-name to file. File contents will be subbed for the name in the config.";
|
description =
|
||||||
default = {};
|
"Map of secret-name to file. File contents will be subbed for the name in the config.";
|
||||||
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
config-file = mkOption {
|
config-file = mkOption {
|
||||||
|
@ -189,27 +181,20 @@ in {
|
||||||
environment = mkOption {
|
environment = mkOption {
|
||||||
type = attrsOf str;
|
type = attrsOf str;
|
||||||
description = "Environment variables to set for the ejabberd daemon.";
|
description = "Environment variables to set for the ejabberd daemon.";
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
users = {
|
users = {
|
||||||
users.${cfg.user} = {
|
users.${cfg.user} = { isSystemUser = true; };
|
||||||
isSystemUser = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
groups.${cfg.group} = {
|
groups.${cfg.group} = { members = [ cfg.user ]; };
|
||||||
members = [ cfg.user ];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = { allowedTCPPorts = [ 5222 5223 5269 8010 ]; };
|
||||||
allowedTCPPorts = [ 5222 5223 5269 8010 ];
|
|
||||||
};
|
|
||||||
|
|
||||||
fudo = let
|
fudo = let host-fqdn = config.instance.host-fqdn;
|
||||||
host-fqdn = config.instance.host-fqdn;
|
|
||||||
in {
|
in {
|
||||||
acme.host-domains.${hostname} = mapAttrs' (site: siteOpts:
|
acme.host-domains.${hostname} = mapAttrs' (site: siteOpts:
|
||||||
nameValuePair siteOpts.hostname {
|
nameValuePair siteOpts.hostname {
|
||||||
|
@ -235,35 +220,39 @@ in {
|
||||||
"d ${config-dir} 0700 ${cfg.user} ${cfg.group} - -"
|
"d ${config-dir} 0700 ${cfg.user} ${cfg.group} - -"
|
||||||
"d ${cfg.state-directory} 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.state-directory} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
ejabberd = {
|
ejabberd = {
|
||||||
wants =
|
wants = map hostCertService
|
||||||
map hostCertService
|
(mapAttrsToList (_: siteOpts: siteOpts.hostname) cfg.sites);
|
||||||
(mapAttrsToList (_: siteOpts: siteOpts.hostname) cfg.sites);
|
|
||||||
requires = [ "ejabberd-config-generator.service" ];
|
requires = [ "ejabberd-config-generator.service" ];
|
||||||
environment = cfg.environment;
|
environment = cfg.environment;
|
||||||
};
|
};
|
||||||
|
|
||||||
ejabberd-config-generator = let
|
ejabberd-config-generator = let
|
||||||
config-generator =
|
config-generator =
|
||||||
enter-secrets config-file-template (attrNames cfg.secret-files) cfg.config-file;
|
enter-secrets config-file-template (attrNames cfg.secret-files)
|
||||||
|
cfg.config-file;
|
||||||
in {
|
in {
|
||||||
description = "Generate ejabberd config file containing passwords.";
|
description = "Generate ejabberd config file containing passwords.";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
ExecStart = "${config-generator}";
|
ExecStart = "${config-generator}";
|
||||||
ExecStartPost = pkgs.writeShellScript "protect-ejabberd-config.sh" ''
|
ExecStartPost =
|
||||||
chown ${cfg.user}:${cfg.group} ${cfg.config-file}
|
pkgs.writeShellScript "protect-ejabberd-config.sh" ''
|
||||||
chmod 0400 ${cfg.config-file}
|
chown ${cfg.user}:${cfg.group} ${cfg.config-file}
|
||||||
'';
|
chmod 0400 ${cfg.config-file}
|
||||||
|
'';
|
||||||
EnvironmentFile = host-secrets.ejabberd-password-env.target-file;
|
EnvironmentFile = host-secrets.ejabberd-password-env.target-file;
|
||||||
};
|
};
|
||||||
|
unitConfig.ConditionPathExists =
|
||||||
|
[ host-secrets.ejabberd-password-env.target-file ];
|
||||||
requires = [ host-secrets.ejabberd-password-env.service ];
|
requires = [ host-secrets.ejabberd-password-env.service ];
|
||||||
|
after = [ host-secrets.ejabberd-password-env.service ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
services.ejabberd = {
|
services.ejabberd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
|
|
|
@ -31,22 +31,27 @@ let
|
||||||
else
|
else
|
||||||
"/home/${user-opts.primary-group}/${username}";
|
"/home/${user-opts.primary-group}/${username}";
|
||||||
|
|
||||||
userLdif = base: name: group-map: opts: ''
|
userLdif = base: name: group-map: opts:
|
||||||
dn: uid=${name},ou=members,${base}
|
''
|
||||||
uid: ${name}
|
dn: uid=${name},ou=members,${base}
|
||||||
objectClass: account
|
uid: ${name}
|
||||||
objectClass: shadowAccount
|
objectClass: shadowAccount
|
||||||
objectClass: posixAccount
|
objectClass: posixAccount
|
||||||
cn: ${opts.common-name}
|
objectClass: inetOrgPerson
|
||||||
uidNumber: ${toString (opts.uid)}
|
cn: ${opts.common-name}
|
||||||
gidNumber: ${toString (getUserGidNumber opts group-map)}
|
uidNumber: ${toString (opts.uid)}
|
||||||
homeDirectory: ${mkHomeDir name opts}
|
gidNumber: ${toString (getUserGidNumber opts group-map)}
|
||||||
description: ${opts.description}
|
homeDirectory: ${mkHomeDir name opts}
|
||||||
shadowLastChange: 12230
|
description: ${opts.description}
|
||||||
shadowMax: 99999
|
shadowLastChange: 12230
|
||||||
shadowWarning: 7
|
shadowMax: 99999
|
||||||
userPassword: ${opts.ldap-hashed-passwd}
|
shadowWarning: 7
|
||||||
'';
|
userPassword: ${opts.ldap-hashed-passwd}
|
||||||
|
mail: ${if (opts.email != null) then opts.email else ""}
|
||||||
|
sn: ${if (opts.surname != null) then opts.surname else name}
|
||||||
|
'' + (optionalString (opts.given-name != null) ''
|
||||||
|
givenName: ${opts.given-name}
|
||||||
|
'');
|
||||||
|
|
||||||
systemUserLdif = base: name: opts: ''
|
systemUserLdif = base: name: opts: ''
|
||||||
dn: cn=${name},${base}
|
dn: cn=${name},${base}
|
||||||
|
@ -243,10 +248,10 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = {
|
# networking.firewall = {
|
||||||
allowedTCPPorts = [ 389 636 ];
|
# allowedTCPPorts = [ 389 636 ];
|
||||||
allowedUDPPorts = [ 389 ];
|
# allowedUDPPorts = [ 389 ];
|
||||||
};
|
# };
|
||||||
|
|
||||||
systemd = let
|
systemd = let
|
||||||
user = config.services.openldap.user;
|
user = config.services.openldap.user;
|
||||||
|
@ -408,7 +413,11 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
declarativeContents = {
|
declarativeContents = let
|
||||||
|
usersWithPasswords =
|
||||||
|
filterAttrs (_: userOpts: userOpts.ldap-hashed-passwd != null)
|
||||||
|
cfg.users;
|
||||||
|
in {
|
||||||
"${cfg.base}" = ''
|
"${cfg.base}" = ''
|
||||||
dn: ${cfg.base}
|
dn: ${cfg.base}
|
||||||
objectClass: top
|
objectClass: top
|
||||||
|
@ -431,7 +440,7 @@ in {
|
||||||
|
|
||||||
${systemUsersLdif cfg.base cfg.system-users}
|
${systemUsersLdif cfg.base cfg.system-users}
|
||||||
${groupsLdif cfg.base cfg.groups}
|
${groupsLdif cfg.base cfg.groups}
|
||||||
${usersLdif cfg.base cfg.groups cfg.users}
|
${usersLdif cfg.base cfg.groups usersWithPasswords}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,8 @@ let
|
||||||
|
|
||||||
join-lines = concatStringsSep "\n";
|
join-lines = concatStringsSep "\n";
|
||||||
|
|
||||||
traceout = out: builtins.trace out out;
|
inherit (pkgs.lib.ip)
|
||||||
|
getNetworkBase maskFromV32Network networkMinIp networkMaxIp;
|
||||||
|
|
||||||
in {
|
in {
|
||||||
|
|
||||||
|
@ -14,6 +15,11 @@ in {
|
||||||
|
|
||||||
enable = mkEnableOption "Enable local network configuration (DHCP & DNS).";
|
enable = mkEnableOption "Enable local network configuration (DHCP & DNS).";
|
||||||
|
|
||||||
|
state-directory = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Path at which to store server state.";
|
||||||
|
};
|
||||||
|
|
||||||
domain = mkOption {
|
domain = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "The domain to use for the local network.";
|
description = "The domain to use for the local network.";
|
||||||
|
@ -92,13 +98,14 @@ in {
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
zone-definition = let
|
zone-definition =
|
||||||
zoneOpts = import ../types/zone-definition.nix { inherit lib; };
|
let zoneOpts = import ../types/zone-definition.nix { inherit lib; };
|
||||||
in mkOption {
|
in mkOption {
|
||||||
type = submodule zoneOpts;
|
type = submodule zoneOpts;
|
||||||
description = "Definition of network zone to be served by local server.";
|
description =
|
||||||
default = { };
|
"Definition of network zone to be served by local server.";
|
||||||
};
|
default = { };
|
||||||
|
};
|
||||||
|
|
||||||
extra-records = mkOption {
|
extra-records = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
|
@ -109,47 +116,70 @@ in {
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
fudo.system.hostfile-entries = let
|
fudo.system.hostfile-entries = let
|
||||||
other-hosts = filterAttrs
|
other-hosts =
|
||||||
(hostname: hostOpts: hostname != config.instance.hostname)
|
filterAttrs (hostname: hostOpts: hostname != config.instance.hostname)
|
||||||
cfg.zone-definition.hosts;
|
cfg.zone-definition.hosts;
|
||||||
in mapAttrs' (hostname: hostOpts:
|
in mapAttrs' (hostname: hostOpts:
|
||||||
nameValuePair hostOpts.ipv4-address ["${hostname}.${cfg.domain}" hostname])
|
nameValuePair hostOpts.ipv4-address [
|
||||||
other-hosts;
|
"${hostname}.${cfg.domain}"
|
||||||
|
hostname
|
||||||
services.dhcpd4 = let
|
]) other-hosts;
|
||||||
zone = cfg.zone-definition;
|
|
||||||
in {
|
services.kea.dhcp4 = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
settings = {
|
||||||
machines = mapAttrsToList (hostname: hostOpts: {
|
interfaces-config.interfaces = cfg.dhcp-interfaces;
|
||||||
ethernetAddress = hostOpts.mac-address;
|
valid-lifetime = 4000;
|
||||||
hostName = hostname;
|
rebind-timer = 2000;
|
||||||
ipAddress = hostOpts.ipv4-address;
|
renew-timer = 1000;
|
||||||
}) (filterAttrs (host: hostOpts:
|
option-data = let joinList = concatStringsSep ", ";
|
||||||
hostOpts.mac-address != null && hostOpts.ipv4-address != null)
|
in [
|
||||||
zone.hosts);
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
interfaces = cfg.dhcp-interfaces;
|
data = joinList cfg.dns-servers;
|
||||||
|
}
|
||||||
extraConfig = ''
|
{
|
||||||
subnet ${pkgs.lib.ip.getNetworkBase cfg.network} netmask ${
|
name = "subnet-mask";
|
||||||
pkgs.lib.ip.maskFromV32Network cfg.network
|
data = maskFromV32Network cfg.network;
|
||||||
} {
|
}
|
||||||
authoritative;
|
{
|
||||||
option subnet-mask ${pkgs.lib.ip.maskFromV32Network cfg.network};
|
name = "broadcast-address";
|
||||||
option broadcast-address ${pkgs.lib.ip.networkMaxIp cfg.network};
|
data = networkMaxIp cfg.network;
|
||||||
option routers ${cfg.gateway};
|
}
|
||||||
option domain-name-servers ${concatStringsSep " " cfg.dns-servers};
|
{
|
||||||
option domain-name "${cfg.domain}";
|
name = "routers";
|
||||||
option domain-search "${
|
data = cfg.gateway;
|
||||||
concatStringsSep " " ([ cfg.domain ] ++ cfg.search-domains)
|
}
|
||||||
}";
|
{
|
||||||
range ${pkgs.lib.ip.networkMinIp cfg.dhcp-dynamic-network} ${
|
name = "domain-name";
|
||||||
pkgs.lib.ip.networkMaxButOneIp cfg.dhcp-dynamic-network
|
data = cfg.domain;
|
||||||
};
|
}
|
||||||
}
|
{
|
||||||
'';
|
name = "domain-search";
|
||||||
|
data = joinList ([ cfg.domain ] ++ cfg.search-domains);
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subnet4 = [{
|
||||||
|
pools = [{
|
||||||
|
pool = let
|
||||||
|
minIp = networkMinIp cfg.dhcp-dynamic-network;
|
||||||
|
maxIp = networkMaxIp cfg.dhcp-dynamic-network;
|
||||||
|
in "${minIp} - ${maxIp}";
|
||||||
|
}];
|
||||||
|
subnet = cfg.network;
|
||||||
|
reservations = let
|
||||||
|
hostsWithMac = filterAttrs (_: hostOpts:
|
||||||
|
!isNull hostOpts.mac-address && !isNull hostOpts.ipv4-address)
|
||||||
|
cfg.zone-definition.hosts;
|
||||||
|
in mapAttrsToList (hostname:
|
||||||
|
{ mac-address, ipv4-address, ... }: {
|
||||||
|
hw-address = mac-address;
|
||||||
|
# hostName = hostname;
|
||||||
|
ip-address = ipv4-address;
|
||||||
|
}) hostsWithMac;
|
||||||
|
}];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
services.bind = let
|
services.bind = let
|
||||||
|
@ -179,19 +209,22 @@ in {
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
filterRedundantIps = official-hosts: hosts: let
|
filterRedundantIps = official-hosts: hosts:
|
||||||
host-by-ip = groupBy (hostOpts: hostOpts.ipv4-address) hosts;
|
let host-by-ip = groupBy (hostOpts: hostOpts.ipv4-address) hosts;
|
||||||
in filter (hostOpts:
|
in filter (hostOpts:
|
||||||
if (length (getAttr hostOpts.ipv4-address host-by-ip) == 1) then
|
if (length (getAttr hostOpts.ipv4-address host-by-ip) == 1) then
|
||||||
true
|
true
|
||||||
else elem hostOpts.hostname official-hosts) hosts;
|
else
|
||||||
|
elem hostOpts.hostname official-hosts) hosts;
|
||||||
ipTo24Block = ip:
|
ipTo24Block = ip:
|
||||||
concatStringsSep "." (reverseList (take 3 (splitString "." ip)));
|
concatStringsSep "." (reverseList (take 3 (splitString "." ip)));
|
||||||
hostsByBlock = official-hosts:
|
hostsByBlock = official-hosts:
|
||||||
groupBy (host-data: ipTo24Block host-data.ipv4-address)
|
groupBy (host-data: ipTo24Block host-data.ipv4-address)
|
||||||
(filterRedundantIps official-hosts (attrValues zone.hosts));
|
(filterRedundantIps official-hosts (attrValues zone.hosts));
|
||||||
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.hostname}.${cfg.domain}.";
|
||||||
|
|
||||||
blockZones = official-hosts:
|
blockZones = official-hosts:
|
||||||
mapAttrsToList blockHostsToZone (hostsByBlock official-hosts);
|
mapAttrsToList blockHostsToZone (hostsByBlock official-hosts);
|
||||||
|
@ -213,16 +246,18 @@ in {
|
||||||
|
|
||||||
domain-name = config.instance.local-domain;
|
domain-name = config.instance.local-domain;
|
||||||
|
|
||||||
domain-hosts =
|
domain-hosts = attrNames
|
||||||
attrNames
|
(filterAttrs (_: hostOpts: hostOpts.domain == domain-name)
|
||||||
(filterAttrs (_: hostOpts:
|
config.fudo.hosts);
|
||||||
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.host} port ${toString cfg.recursive-resolver.port}" ];
|
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;
|
listenOnIpv6 = cfg.dns-listen-ipv6s;
|
||||||
extraOptions = concatStringsSep "\n" [
|
extraOptions = concatStringsSep "\n" [
|
||||||
|
@ -235,43 +270,11 @@ in {
|
||||||
master = true;
|
master = true;
|
||||||
name = cfg.domain;
|
name = cfg.domain;
|
||||||
file = let
|
file = let
|
||||||
zone-data = pkgs.lib.dns.zoneToZonefile
|
zone-data =
|
||||||
config.instance.build-timestamp
|
pkgs.lib.dns.zoneToZonefile config.instance.build-timestamp
|
||||||
cfg.domain
|
cfg.domain zone;
|
||||||
zone;
|
|
||||||
in pkgs.writeText "zone-${cfg.domain}" zone-data;
|
in pkgs.writeText "zone-${cfg.domain}" zone-data;
|
||||||
# file = pkgs.writeText "${cfg.domain}-zone" ''
|
}] ++ (optionals cfg.enable-reverse-mappings (blockZones domain-hosts));
|
||||||
# @ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. (
|
|
||||||
# ${toString config.instance.build-timestamp}
|
|
||||||
# 5m
|
|
||||||
# 2m
|
|
||||||
# 6w
|
|
||||||
# 5m)
|
|
||||||
|
|
||||||
# $TTL 1h
|
|
||||||
|
|
||||||
# @ IN NS ns1.${cfg.domain}.
|
|
||||||
|
|
||||||
# $ORIGIN ${cfg.domain}.
|
|
||||||
|
|
||||||
# $TTL 30m
|
|
||||||
|
|
||||||
# ${optionalString (zone.gssapi-realm != null)
|
|
||||||
# ''_kerberos IN TXT "${zone.gssapi-realm}"''}
|
|
||||||
|
|
||||||
# ${join-lines
|
|
||||||
# (imap1 (i: server-ip: "ns${toString i} IN A ${server-ip}")
|
|
||||||
# cfg.dns-servers)}
|
|
||||||
# ${join-lines (mapAttrsToList hostARecord zone.hosts)}
|
|
||||||
# ${join-lines (mapAttrsToList hostSshFpRecords zone.hosts)}
|
|
||||||
# ${join-lines (mapAttrsToList cnameRecord zone.aliases)}
|
|
||||||
# ${join-lines zone.verbatim-dns-records}
|
|
||||||
# ${pkgs.lib.dns.srvRecordsToBindZone zone.srv-records}
|
|
||||||
# ${join-lines cfg.extra-records}
|
|
||||||
# '';
|
|
||||||
}] ++ (optionals
|
|
||||||
cfg.enable-reverse-mappings
|
|
||||||
(blockZones domain-hosts));
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,52 @@ with lib;
|
||||||
let cfg = config.fudo.mail-server;
|
let cfg = config.fudo.mail-server;
|
||||||
|
|
||||||
in {
|
in {
|
||||||
options.fudo.mail-server.clamav = {
|
options.fudo.mail-server.clamav = with types; {
|
||||||
enable = mkOption {
|
enable = mkOption {
|
||||||
description = "Enable virus scanning with ClamAV.";
|
description = "Enable virus scanning with ClamAV.";
|
||||||
type = types.bool;
|
type = bool;
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state-directory = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Path at which to store the ClamAV database.";
|
||||||
|
default = "/var/lib/clamav";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf (cfg.enable && cfg.clamav.enable) {
|
config = mkIf (cfg.enable && cfg.clamav.enable) {
|
||||||
|
|
||||||
|
users = {
|
||||||
|
users.clamav = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = config.ids.uids.clamav;
|
||||||
|
home = mkForce cfg.clamav.state-directory;
|
||||||
|
description = "ClamAV daemon user";
|
||||||
|
group = "clamav";
|
||||||
|
};
|
||||||
|
groups.clamav = {
|
||||||
|
members = [ "clamav" ];
|
||||||
|
gid = config.ids.gids.clamav;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules =
|
||||||
|
[ "d ${cfg.clamav.state-directory} 0750 clamav clamav - -" ];
|
||||||
|
|
||||||
services.clamav = {
|
services.clamav = {
|
||||||
daemon = {
|
daemon = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = { PhishingScanURLs = "no"; };
|
settings = {
|
||||||
|
PhishingScanURLs = "no";
|
||||||
|
DatabaseDirectory = mkForce cfg.clamav.state-directory;
|
||||||
|
User = "clamav";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
updater = {
|
||||||
|
enable = true;
|
||||||
|
settings = { DatabaseDirectory = mkForce cfg.clamav.state-directory; };
|
||||||
};
|
};
|
||||||
updater.enable = true;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,32 +9,32 @@ let
|
||||||
let
|
let
|
||||||
dkim_key = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key";
|
dkim_key = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key";
|
||||||
dkim_txt = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.txt";
|
dkim_txt = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.txt";
|
||||||
in
|
in ''
|
||||||
''
|
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]; then
|
||||||
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
|
${cfg.dkim.package}/bin/opendkim-genkey -s "${cfg.dkim.selector}" \
|
||||||
then
|
-d "${dom}" \
|
||||||
${cfg.dkim.package}/bin/opendkim-genkey -s "${cfg.dkim.selector}" \
|
--bits="${toString cfg.dkim.key-bits}" \
|
||||||
-d "${dom}" \
|
--directory="${cfg.dkim.key-directory}"
|
||||||
--bits="${toString cfg.dkim.key-bits}" \
|
mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.private" "${dkim_key}"
|
||||||
--directory="${cfg.dkim.key-directory}"
|
mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.txt" "${dkim_txt}"
|
||||||
mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.private" "${dkim_key}"
|
echo "Generated key for domain ${dom} selector ${cfg.dkim.selector}"
|
||||||
mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.txt" "${dkim_txt}"
|
fi
|
||||||
echo "Generated key for domain ${dom} selector ${cfg.dkim.selector}"
|
'';
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.local-domains);
|
createAllCerts =
|
||||||
|
lib.concatStringsSep "\n" (map createDomainDkimCert cfg.local-domains);
|
||||||
|
|
||||||
keyTable = pkgs.writeText "opendkim-KeyTable"
|
keyTable = pkgs.writeText "opendkim-KeyTable" (lib.concatStringsSep "\n"
|
||||||
(lib.concatStringsSep "\n" (lib.flip map cfg.local-domains
|
(lib.flip map cfg.local-domains (dom:
|
||||||
(dom: "${dom} ${dom}:${cfg.dkim.selector}:${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key")));
|
"${dom} ${dom}:${cfg.dkim.selector}:${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key")));
|
||||||
signingTable = pkgs.writeText "opendkim-SigningTable"
|
signingTable = pkgs.writeText "opendkim-SigningTable"
|
||||||
(lib.concatStringsSep "\n" (lib.flip map cfg.local-domains (dom: "${dom} ${dom}")));
|
(lib.concatStringsSep "\n"
|
||||||
|
(lib.flip map cfg.local-domains (dom: "${dom} ${dom}")));
|
||||||
|
|
||||||
dkim = config.services.opendkim;
|
dkim = config.services.opendkim;
|
||||||
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
|
args = [ "-f" "-l" ]
|
||||||
in
|
++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
|
||||||
{
|
in {
|
||||||
|
|
||||||
options.fudo.mail-server.dkim = {
|
options.fudo.mail-server.dkim = {
|
||||||
signing = mkOption {
|
signing = mkOption {
|
||||||
|
@ -59,12 +59,12 @@ in
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 2048;
|
default = 2048;
|
||||||
description = ''
|
description = ''
|
||||||
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
|
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
|
||||||
|
|
||||||
If you have already deployed a key with a different number of bits than specified
|
If you have already deployed a key with a different number of bits than specified
|
||||||
here, then you should use a different selector (dkimSelector). In order to get
|
here, then you should use a different selector (dkimSelector). In order to get
|
||||||
this package to generate a key with the new number of bits, you will either have to
|
this package to generate a key with the new number of bits, you will either have to
|
||||||
change the selector or delete the old key file.
|
change the selector or delete the old key file.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,16 +81,16 @@ in
|
||||||
selector = cfg.dkim.selector;
|
selector = cfg.dkim.selector;
|
||||||
domains = "csl:${builtins.concatStringsSep "," cfg.local-domains}";
|
domains = "csl:${builtins.concatStringsSep "," cfg.local-domains}";
|
||||||
configFile = pkgs.writeText "opendkim.conf" (''
|
configFile = pkgs.writeText "opendkim.conf" (''
|
||||||
Canonicalization relaxed/simple
|
Canonicalization relaxed/simple
|
||||||
UMask 0002
|
UMask 0002
|
||||||
Socket ${dkim.socket}
|
Socket ${dkim.socket}
|
||||||
KeyTable file:${keyTable}
|
KeyTable file:${keyTable}
|
||||||
SigningTable file:${signingTable}
|
SigningTable file:${signingTable}
|
||||||
'' + (lib.optionalString cfg.debug ''
|
'' + (lib.optionalString cfg.debug ''
|
||||||
Syslog yes
|
Syslog yes
|
||||||
SyslogSuccess yes
|
SyslogSuccess yes
|
||||||
LogWhy yes
|
LogWhy yes
|
||||||
''));
|
''));
|
||||||
};
|
};
|
||||||
|
|
||||||
users.users = {
|
users.users = {
|
||||||
|
@ -99,16 +99,19 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.opendkim = {
|
systemd = {
|
||||||
preStart = lib.mkForce createAllCerts;
|
tmpfiles.rules = [
|
||||||
serviceConfig = {
|
"d '${cfg.dkim.key-directory}' - ${config.services.opendkim.user} ${config.services.opendkim.group} - -"
|
||||||
ExecStart = lib.mkForce "${cfg.dkim.package}/bin/opendkim ${escapeShellArgs args}";
|
];
|
||||||
PermissionsStartOnly = lib.mkForce false;
|
services.opendkim = {
|
||||||
|
preStart = lib.mkForce createAllCerts;
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = lib.mkForce
|
||||||
|
"${cfg.dkim.package}/bin/opendkim ${escapeShellArgs args}";
|
||||||
|
PermissionsStartOnly = lib.mkForce false;
|
||||||
|
ReadWritePaths = [ cfg.dkim.key-directory ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d '${cfg.dkim.key-directory}' - ${config.services.opendkim.user} ${config.services.opendkim.group} - -"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,30 +33,30 @@ let
|
||||||
tls = yes
|
tls = yes
|
||||||
tls_require_cert = try
|
tls_require_cert = try
|
||||||
'';
|
'';
|
||||||
in
|
in pkgs.writeText "dovecot2-ldap-config.conf.template" ''
|
||||||
pkgs.writeText "dovecot2-ldap-config.conf.template" ''
|
uris = ${concatStringsSep " " ldap-cfg.server-urls}
|
||||||
uris = ${concatStringsSep " " ldap-cfg.server-urls}
|
ldap_version = 3
|
||||||
ldap_version = 3
|
dn = ${ldap-cfg.reader-dn}
|
||||||
dn = ${ldap-cfg.reader-dn}
|
dnpass = __LDAP_READER_PASSWORD__
|
||||||
dnpass = __LDAP_READER_PASSWORD__
|
auth_bind = yes
|
||||||
auth_bind = yes
|
auth_bind_userdn = uid=%u,ou=members,dc=fudo,dc=org
|
||||||
auth_bind_userdn = uid=%u,ou=members,dc=fudo,dc=org
|
base = dc=fudo,dc=org
|
||||||
base = dc=fudo,dc=org
|
${ssl-config}
|
||||||
${ssl-config}
|
'';
|
||||||
'';
|
|
||||||
|
|
||||||
ldap-conf-generator = ldap-cfg: let
|
ldap-conf-generator = ldap-cfg:
|
||||||
template = ldap-conf-template ldap-cfg;
|
let
|
||||||
target-dir = dirOf ldap-cfg.generated-ldap-config;
|
template = ldap-conf-template ldap-cfg;
|
||||||
target = ldap-cfg.generated-ldap-config;
|
target-dir = dirOf ldap-cfg.generated-ldap-config;
|
||||||
in pkgs.writeScript "dovecot2-ldap-password-swapper.sh" ''
|
target = ldap-cfg.generated-ldap-config;
|
||||||
mkdir -p ${target-dir}
|
in pkgs.writeScript "dovecot2-ldap-password-swapper.sh" ''
|
||||||
touch ${target}
|
mkdir -p ${target-dir}
|
||||||
chmod 600 ${target}
|
touch ${target}
|
||||||
chown ${config.services.dovecot2.user} ${target}
|
chmod 600 ${target}
|
||||||
LDAP_READER_PASSWORD=$( cat "${ldap-cfg.reader-password-file}" )
|
chown ${config.services.dovecot2.user} ${target}
|
||||||
sed 's/__LDAP_READER_PASSWORD__/$LDAP_READER_PASSWORD/' '${template}' > ${target}
|
LDAP_READER_PASSWORD=$( cat "${ldap-cfg.reader-password-file}" )
|
||||||
'';
|
sed 's/__LDAP_READER_PASSWORD__/$LDAP_READER_PASSWORD/' '${template}' > ${target}
|
||||||
|
'';
|
||||||
|
|
||||||
ldap-passwd-entry = ldap-config: ''
|
ldap-passwd-entry = ldap-config: ''
|
||||||
passdb {
|
passdb {
|
||||||
|
@ -69,7 +69,8 @@ let
|
||||||
options = with types; {
|
options = with types; {
|
||||||
ca = mkOption {
|
ca = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = "The path to the CA cert used to sign the LDAP server certificate.";
|
description =
|
||||||
|
"The path to the CA cert used to sign the LDAP server certificate.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -99,7 +100,8 @@ let
|
||||||
|
|
||||||
generated-ldap-config = mkOption {
|
generated-ldap-config = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Path at which to store the generated LDAP config file, including password.";
|
description =
|
||||||
|
"Path at which to store the generated LDAP config file, including password.";
|
||||||
default = "/run/dovecot2/config/ldap.conf";
|
default = "/run/dovecot2/config/ldap.conf";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -132,7 +134,7 @@ in {
|
||||||
|
|
||||||
services.prometheus.exporters.dovecot = mkIf cfg.monitoring.enable {
|
services.prometheus.exporters.dovecot = mkIf cfg.monitoring.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
scopes = ["user" "global"];
|
scopes = [ "user" "global" ];
|
||||||
listenAddress = "127.0.0.1";
|
listenAddress = "127.0.0.1";
|
||||||
port = cfg.monitoring.dovecot-listen-port;
|
port = cfg.monitoring.dovecot-listen-port;
|
||||||
socketPath = "/var/run/dovecot2/old-stats";
|
socketPath = "/var/run/dovecot2/old-stats";
|
||||||
|
@ -295,9 +297,8 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
tmpfiles.rules = [
|
tmpfiles.rules =
|
||||||
"d ${sieve-path} 750 ${dovecot-user} ${cfg.mail-group} - -"
|
[ "d ${sieve-path} 750 ${dovecot-user} ${cfg.mail-group} - -" ];
|
||||||
];
|
|
||||||
|
|
||||||
services.dovecot2.preStart = ''
|
services.dovecot2.preStart = ''
|
||||||
rm -f ${sieve-path}/*
|
rm -f ${sieve-path}/*
|
||||||
|
@ -307,7 +308,7 @@ in {
|
||||||
done
|
done
|
||||||
|
|
||||||
${optionalString (cfg.dovecot.ldap != null)
|
${optionalString (cfg.dovecot.ldap != null)
|
||||||
(ldap-conf-generator cfg.dovecot.ldap)}
|
(ldap-conf-generator cfg.dovecot.ldap)}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -o errexit
|
set -o errexit
|
||||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
|
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
|
||||||
|
|
|
@ -8,48 +8,40 @@ let
|
||||||
|
|
||||||
# The final newline is important
|
# The final newline is important
|
||||||
write-entries = filename: entries:
|
write-entries = filename: entries:
|
||||||
let
|
let entries-string = (concatStringsSep "\n" entries);
|
||||||
entries-string = (concatStringsSep "\n" entries);
|
|
||||||
in builtins.toFile filename ''
|
in builtins.toFile filename ''
|
||||||
${entries-string}
|
${entries-string}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
make-user-aliases = entries:
|
make-user-aliases = entries:
|
||||||
concatStringsSep "\n"
|
concatStringsSep "\n" (mapAttrsToList (user: aliases:
|
||||||
(mapAttrsToList (user: aliases:
|
concatStringsSep "\n" (map (alias: "${alias} ${user}") aliases))
|
||||||
concatStringsSep "\n"
|
entries);
|
||||||
(map (alias: "${alias} ${user}") aliases))
|
|
||||||
entries);
|
|
||||||
|
|
||||||
make-alias-users = domains: entries:
|
make-alias-users = domains: entries:
|
||||||
concatStringsSep "\n"
|
concatStringsSep "\n" (flatten (mapAttrsToList (alias: users:
|
||||||
(flatten
|
(map (domain: "${alias}@${domain} ${concatStringsSep "," users}")
|
||||||
(mapAttrsToList (alias: users:
|
domains)) entries));
|
||||||
(map (domain:
|
|
||||||
"${alias}@${domain} ${concatStringsSep "," users}")
|
|
||||||
domains))
|
|
||||||
entries));
|
|
||||||
|
|
||||||
policyd-spf = pkgs.writeText "policyd-spf.conf" (
|
policyd-spf = pkgs.writeText "policyd-spf.conf"
|
||||||
cfg.postfix.policy-spf-extra-config
|
(cfg.postfix.policy-spf-extra-config + (lib.optionalString cfg.debug ''
|
||||||
+ (lib.optionalString cfg.debug ''
|
debugLevel = 4
|
||||||
debugLevel = 4
|
''));
|
||||||
''));
|
|
||||||
|
|
||||||
submission-header-cleanup-rules = pkgs.writeText "submission_header_cleanup_rules" (''
|
submission-header-cleanup-rules =
|
||||||
# Removes sensitive headers from mails handed in via the submission port.
|
pkgs.writeText "submission_header_cleanup_rules" (''
|
||||||
# See https://thomas-leister.de/mailserver-debian-stretch/
|
# Removes sensitive headers from mails handed in via the submission port.
|
||||||
# Uses "pcre" style regex.
|
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||||
|
# Uses "pcre" style regex.
|
||||||
|
|
||||||
/^Received:/ IGNORE
|
/^Received:/ IGNORE
|
||||||
/^X-Originating-IP:/ IGNORE
|
/^X-Originating-IP:/ IGNORE
|
||||||
/^X-Mailer:/ IGNORE
|
/^X-Mailer:/ IGNORE
|
||||||
/^User-Agent:/ IGNORE
|
/^User-Agent:/ IGNORE
|
||||||
/^X-Enigmail:/ IGNORE
|
/^X-Enigmail:/ IGNORE
|
||||||
'');
|
'');
|
||||||
blacklist-postfix-entry = sender: "${sender} REJECT";
|
blacklist-postfix-entry = sender: "${sender} REJECT";
|
||||||
blacklist-postfix-file = filename: entries:
|
blacklist-postfix-file = filename: entries: write-entries filename entries;
|
||||||
write-entries filename entries;
|
|
||||||
sender-blacklist-file = blacklist-postfix-file "reject_senders"
|
sender-blacklist-file = blacklist-postfix-file "reject_senders"
|
||||||
(map blacklist-postfix-entry cfg.sender-blacklist);
|
(map blacklist-postfix-entry cfg.sender-blacklist);
|
||||||
recipient-blacklist-file = blacklist-postfix-file "reject_recipients"
|
recipient-blacklist-file = blacklist-postfix-file "reject_recipients"
|
||||||
|
@ -57,12 +49,13 @@ let
|
||||||
|
|
||||||
# A list of domains for which we accept mail
|
# A list of domains for which we accept mail
|
||||||
virtual-mailbox-map-file = write-entries "virtual_mailbox_map"
|
virtual-mailbox-map-file = write-entries "virtual_mailbox_map"
|
||||||
(map (domain: "@${domain} OK") (cfg.local-domains ++ [cfg.domain]));
|
(map (domain: "@${domain} OK") (cfg.local-domains ++ [ cfg.domain ]));
|
||||||
|
|
||||||
sender-login-map-file = let
|
sender-login-map-file =
|
||||||
escapeDot = (str: replaceStrings ["."] ["\\."] str);
|
let escapeDot = (str: replaceStrings [ "." ] [ "\\." ] str);
|
||||||
in write-entries "sender_login_maps"
|
in write-entries "sender_login_maps"
|
||||||
(map (domain: "/^(.*)@${escapeDot domain}$/ \${1}") (cfg.local-domains ++ [cfg.domain]));
|
(map (domain: "/^(.*)@${escapeDot domain}$/ \${1}")
|
||||||
|
(cfg.local-domains ++ [ cfg.domain ]));
|
||||||
|
|
||||||
mapped-file = name: "hash:/var/lib/postfix/conf/${name}";
|
mapped-file = name: "hash:/var/lib/postfix/conf/${name}";
|
||||||
|
|
||||||
|
@ -112,7 +105,7 @@ in {
|
||||||
domain = cfg.domain;
|
domain = cfg.domain;
|
||||||
origin = cfg.domain;
|
origin = cfg.domain;
|
||||||
hostname = cfg.mail-hostname;
|
hostname = cfg.mail-hostname;
|
||||||
destination = ["localhost" "localhost.localdomain"];
|
destination = [ "localhost" "localhost.localdomain" ];
|
||||||
# destination = ["localhost" "localhost.localdomain" cfg.hostname] ++
|
# destination = ["localhost" "localhost.localdomain" cfg.hostname] ++
|
||||||
# cfg.local-domains;;
|
# cfg.local-domains;;
|
||||||
|
|
||||||
|
@ -132,25 +125,27 @@ in {
|
||||||
virtual = ''
|
virtual = ''
|
||||||
${make-user-aliases cfg.user-aliases}
|
${make-user-aliases cfg.user-aliases}
|
||||||
|
|
||||||
${make-alias-users ([cfg.domain] ++ cfg.local-domains) cfg.alias-users}
|
${make-alias-users ([ cfg.domain ] ++ cfg.local-domains)
|
||||||
|
cfg.alias-users}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
sslCert = cfg.postfix.ssl-certificate;
|
sslCert = cfg.postfix.ssl-certificate;
|
||||||
sslKey = cfg.postfix.ssl-private-key;
|
sslKey = cfg.postfix.ssl-private-key;
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
virtual_mailbox_domains = cfg.local-domains ++ [cfg.domain];
|
virtual_mailbox_domains = cfg.local-domains ++ [ cfg.domain ];
|
||||||
# virtual_mailbox_base = "${cfg.mail-directory}/";
|
# virtual_mailbox_base = "${cfg.mail-directory}/";
|
||||||
virtual_mailbox_maps = mapped-file "virtual_mailbox_map";
|
virtual_mailbox_maps = mapped-file "virtual_mailbox_map";
|
||||||
|
|
||||||
virtual_uid_maps = "static:${toString cfg.mail-user-id}";
|
virtual_uid_maps = "static:${toString cfg.mail-user-id}";
|
||||||
virtual_gid_maps = "static:${toString config.users.groups."${cfg.mail-group}".gid}";
|
virtual_gid_maps =
|
||||||
|
"static:${toString config.users.groups."${cfg.mail-group}".gid}";
|
||||||
|
|
||||||
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||||
|
|
||||||
# NOTE: it's important that this ends with /, to indicate Maildir format!
|
# NOTE: it's important that this ends with /, to indicate Maildir format!
|
||||||
# mail_spool_directory = "${cfg.mail-directory}/";
|
# mail_spool_directory = "${cfg.mail-directory}/";
|
||||||
message_size_limit = toString(cfg.message-size-limit * 1024 * 1024);
|
message_size_limit = toString (cfg.message-size-limit * 1024 * 1024);
|
||||||
|
|
||||||
smtpd_banner = "${cfg.mail-hostname} ESMTP NO UCE";
|
smtpd_banner = "${cfg.mail-hostname} ESMTP NO UCE";
|
||||||
|
|
||||||
|
@ -176,7 +171,8 @@ in {
|
||||||
recipient_delimiter = "+";
|
recipient_delimiter = "+";
|
||||||
|
|
||||||
milter_protocol = "6";
|
milter_protocol = "6";
|
||||||
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
milter_mail_macros =
|
||||||
|
"i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
||||||
|
|
||||||
smtpd_milters = [
|
smtpd_milters = [
|
||||||
"unix:/run/rspamd/rspamd-milter.sock"
|
"unix:/run/rspamd/rspamd-milter.sock"
|
||||||
|
@ -218,11 +214,8 @@ in {
|
||||||
"reject_non_fqdn_recipient"
|
"reject_non_fqdn_recipient"
|
||||||
];
|
];
|
||||||
|
|
||||||
smtpd_helo_restrictions = [
|
smtpd_helo_restrictions =
|
||||||
"permit_mynetworks"
|
[ "permit_mynetworks" "reject_invalid_hostname" "permit" ];
|
||||||
"reject_invalid_hostname"
|
|
||||||
"permit"
|
|
||||||
];
|
|
||||||
|
|
||||||
# Handled by submission
|
# Handled by submission
|
||||||
smtpd_tls_security_level = "may";
|
smtpd_tls_security_level = "may";
|
||||||
|
@ -230,44 +223,27 @@ in {
|
||||||
smtpd_tls_eecdh_grade = "ultra";
|
smtpd_tls_eecdh_grade = "ultra";
|
||||||
|
|
||||||
# Disable obselete protocols
|
# Disable obselete protocols
|
||||||
smtpd_tls_protocols = [
|
smtpd_tls_protocols =
|
||||||
"TLSv1.2"
|
[ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
"TLSv1.1"
|
smtp_tls_protocols = [ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
"!TLSv1"
|
smtpd_tls_mandatory_protocols =
|
||||||
"!SSLv2"
|
[ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
"!SSLv3"
|
smtp_tls_mandatory_protocols =
|
||||||
];
|
[ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
smtp_tls_protocols = [
|
|
||||||
"TLSv1.2"
|
|
||||||
"TLSv1.1"
|
|
||||||
"!TLSv1"
|
|
||||||
"!SSLv2"
|
|
||||||
"!SSLv3"
|
|
||||||
];
|
|
||||||
smtpd_tls_mandatory_protocols = [
|
|
||||||
"TLSv1.2"
|
|
||||||
"TLSv1.1"
|
|
||||||
"!TLSv1"
|
|
||||||
"!SSLv2"
|
|
||||||
"!SSLv3"
|
|
||||||
];
|
|
||||||
smtp_tls_mandatory_protocols = [
|
|
||||||
"TLSv1.2"
|
|
||||||
"TLSv1.1"
|
|
||||||
"!TLSv1"
|
|
||||||
"!SSLv2"
|
|
||||||
"!SSLv3"
|
|
||||||
];
|
|
||||||
|
|
||||||
smtp_tls_ciphers = "high";
|
smtp_tls_ciphers = "high";
|
||||||
smtpd_tls_ciphers = "high";
|
smtpd_tls_ciphers = "high";
|
||||||
smtp_tls_mandatory_ciphers = "high";
|
smtp_tls_mandatory_ciphers = "high";
|
||||||
smtpd_tls_mandatory_ciphers = "high";
|
smtpd_tls_mandatory_ciphers = "high";
|
||||||
|
|
||||||
smtpd_tls_mandatory_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
|
smtpd_tls_mandatory_exclude_ciphers =
|
||||||
smtpd_tls_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
smtp_tls_mandatory_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
|
smtpd_tls_exclude_ciphers =
|
||||||
smtp_tls_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
|
smtp_tls_mandatory_exclude_ciphers =
|
||||||
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
|
smtp_tls_exclude_ciphers =
|
||||||
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
|
|
||||||
tls_preempt_cipherlist = "yes";
|
tls_preempt_cipherlist = "yes";
|
||||||
|
|
||||||
|
@ -286,8 +262,10 @@ in {
|
||||||
smtpd_sasl_security_options = "noanonymous";
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
smtpd_sasl_local_domain = cfg.domain;
|
smtpd_sasl_local_domain = cfg.domain;
|
||||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||||
smtpd_sender_restrictions = "reject_sender_login_mismatch,reject_unknown_sender_domain";
|
smtpd_sender_restrictions =
|
||||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
"reject_sender_login_mismatch,reject_unknown_sender_domain";
|
||||||
|
smtpd_recipient_restrictions =
|
||||||
|
"reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||||
cleanup_service_name = "submission-header-cleanup";
|
cleanup_service_name = "submission-header-cleanup";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -297,7 +275,11 @@ in {
|
||||||
privileged = true;
|
privileged = true;
|
||||||
chroot = false;
|
chroot = false;
|
||||||
command = "spawn";
|
command = "spawn";
|
||||||
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
|
args = [
|
||||||
|
"user=nobody"
|
||||||
|
"argv=${pkgs.pypolicyd-spf}/bin/policyd-spf"
|
||||||
|
"${policyd-spf}"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"submission-header-cleanup" = {
|
"submission-header-cleanup" = {
|
||||||
type = "unix";
|
type = "unix";
|
||||||
|
@ -305,7 +287,8 @@ in {
|
||||||
chroot = false;
|
chroot = false;
|
||||||
maxproc = 0;
|
maxproc = 0;
|
||||||
command = "cleanup";
|
command = "cleanup";
|
||||||
args = ["-o" "header_checks=pcre:${submission-header-cleanup-rules}"];
|
args =
|
||||||
|
[ "-o" "header_checks=pcre:${submission-header-cleanup-rules}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -313,9 +296,9 @@ in {
|
||||||
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
||||||
systemd.services.postfix = {
|
systemd.services.postfix = {
|
||||||
after = [ "dovecot2.service" ]
|
after = [ "dovecot2.service" ]
|
||||||
++ (lib.optional cfg.dkim.signing "opendkim.service");
|
++ (lib.optional cfg.dkim.signing "opendkim.service");
|
||||||
requires = [ "dovecot2.service" ]
|
requires = [ "dovecot2.service" ]
|
||||||
++ (lib.optional cfg.dkim.signing "opendkim.service");
|
++ (lib.optional cfg.dkim.signing "opendkim.service");
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let cfg = config.fudo.mail-server;
|
||||||
cfg = config.fudo.mail-server;
|
|
||||||
|
|
||||||
in {
|
in {
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
@ -73,7 +72,7 @@ in {
|
||||||
mode = "0666";
|
mode = "0666";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
includes = [];
|
includes = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,6 +86,7 @@ in {
|
||||||
requires = [ "rspamd.service" ];
|
requires = [ "rspamd.service" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
users.extraUsers.${config.services.postfix.user}.extraGroups = [ config.services.rspamd.group ];
|
users.extraUsers.${config.services.postfix.user}.extraGroups =
|
||||||
|
[ config.services.rspamd.group ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,18 @@ let
|
||||||
default = 25565;
|
default = 25565;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
query-port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port for queries.";
|
||||||
|
default = 25566;
|
||||||
|
};
|
||||||
|
|
||||||
|
rcon-port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port for remote commands.";
|
||||||
|
default = 25567;
|
||||||
|
};
|
||||||
|
|
||||||
difficulty = mkOption {
|
difficulty = mkOption {
|
||||||
type = enum [ "peaceful" "easy" "medium" "hard" ];
|
type = enum [ "peaceful" "easy" "medium" "hard" ];
|
||||||
description = "Difficulty setting of this server.";
|
description = "Difficulty setting of this server.";
|
||||||
|
@ -120,6 +132,12 @@ let
|
||||||
description = "Allow player-vs-player combat.";
|
description = "Allow player-vs-player combat.";
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = package;
|
||||||
|
description = "PaperMC package to launch.";
|
||||||
|
default = pkgs.papermc;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,6 +162,8 @@ let
|
||||||
server-port = worldOpts.port;
|
server-port = worldOpts.port;
|
||||||
hardcore = worldOpts.hardcore;
|
hardcore = worldOpts.hardcore;
|
||||||
pvp = worldOpts.pvp;
|
pvp = worldOpts.pvp;
|
||||||
|
"query.port" = worldOpts.query-port;
|
||||||
|
"rcon.port" = worldOpts.rcon-port;
|
||||||
};
|
};
|
||||||
|
|
||||||
toProps = attrs:
|
toProps = attrs:
|
||||||
|
@ -202,7 +222,9 @@ in {
|
||||||
groups."${cfg.group}" = { members = [ cfg.user ]; };
|
groups."${cfg.group}" = { members = [ cfg.user ]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 25555 ];
|
networking.firewall.allowedTCPPorts = [ 25555 ] ++ (lib.concatMap
|
||||||
|
(worldOpts: with worldOpts; [ port query-port rcon-port ])
|
||||||
|
(attrNames cfg.worlds));
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
tmpfiles.rules = map (worldOpts:
|
tmpfiles.rules = map (worldOpts:
|
||||||
|
@ -226,7 +248,8 @@ in {
|
||||||
cp -f ${props-file} ${stateDir}/server.properties
|
cp -f ${props-file} ${stateDir}/server.properties
|
||||||
cp -f ${eula-file} ${stateDir}/eula.txt
|
cp -f ${eula-file} ${stateDir}/eula.txt
|
||||||
mkdir -p ${stateDir}/plugins
|
mkdir -p ${stateDir}/plugins
|
||||||
cp -f ${witchcraft-plugin} ${stateDir}/plugins/witchcraft-plugin.jar
|
# Version not working...
|
||||||
|
# cp -f ${witchcraft-plugin} ${stateDir}/plugins/witchcraft-plugin.jar
|
||||||
chmod u+w ${stateDir}/server.properties
|
chmod u+w ${stateDir}/server.properties
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
@ -237,7 +260,7 @@ in {
|
||||||
++ (optionals (worldOpts.allocated-memory >= 12) highMemFlags);
|
++ (optionals (worldOpts.allocated-memory >= 12) highMemFlags);
|
||||||
flagStr = concatStringsSep " " flags;
|
flagStr = concatStringsSep " " flags;
|
||||||
in pkgs.writeShellScript "mc-start-${sanitizedName}.sh"
|
in pkgs.writeShellScript "mc-start-${sanitizedName}.sh"
|
||||||
"${pkgs.papermc}/bin/minecraft-server ${flagStr}";
|
"${worldOpts.package}/bin/minecraft-server ${flagStr}";
|
||||||
|
|
||||||
in nameValuePair serverName {
|
in nameValuePair serverName {
|
||||||
enable = worldOpts.enable;
|
enable = worldOpts.enable;
|
||||||
|
|
|
@ -4,53 +4,82 @@ with lib;
|
||||||
let cfg = config.fudo.minecraft-server;
|
let cfg = config.fudo.minecraft-server;
|
||||||
|
|
||||||
in {
|
in {
|
||||||
options.fudo.minecraft-server = {
|
options.fudo.minecraft-server = with types; {
|
||||||
enable = mkEnableOption "Start a minecraft server.";
|
enable = mkEnableOption "Start a minecraft server.";
|
||||||
|
|
||||||
package = mkOption {
|
package = mkOption {
|
||||||
type = types.package;
|
type = package;
|
||||||
description = "Minecraft package to use.";
|
description = "Minecraft package to use.";
|
||||||
default = pkgs.minecraft-current;
|
default = pkgs.minecraft-current;
|
||||||
};
|
};
|
||||||
|
|
||||||
data-dir = mkOption {
|
data-dir = mkOption {
|
||||||
type = types.path;
|
type = path;
|
||||||
description = "Path at which to store minecraft data.";
|
description = "Path at which to store minecraft data.";
|
||||||
};
|
};
|
||||||
|
|
||||||
world-name = mkOption {
|
world-name = mkOption {
|
||||||
type = types.str;
|
type = str;
|
||||||
description = "Name of the server world (used in saves etc).";
|
description = "Name of the server world (used in saves etc).";
|
||||||
};
|
};
|
||||||
|
|
||||||
motd = mkOption {
|
motd = mkOption {
|
||||||
type = types.str;
|
type = str;
|
||||||
description = "Welcome message for newcomers.";
|
description = "Welcome message for newcomers.";
|
||||||
default = "Welcome to Minecraft! Have fun building...";
|
default = "Welcome to Minecraft! Have fun building...";
|
||||||
};
|
};
|
||||||
|
|
||||||
game-mode = mkOption {
|
game-mode = mkOption {
|
||||||
type = types.enum [ "survival" "creative" "adventure" "spectator" ];
|
type = enum [ "survival" "creative" "adventure" "spectator" ];
|
||||||
description = "Game mode of the server.";
|
description = "Game mode of the server.";
|
||||||
default = "survival";
|
default = "survival";
|
||||||
};
|
};
|
||||||
|
|
||||||
difficulty = mkOption {
|
difficulty = mkOption {
|
||||||
type = types.int;
|
type = int;
|
||||||
description = "Difficulty level, where 0 is peaceful and 3 is hard.";
|
description = "Difficulty level, where 0 is peaceful and 3 is hard.";
|
||||||
default = 2;
|
default = 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
allow-cheats = mkOption {
|
allow-cheats = mkOption {
|
||||||
type = types.bool;
|
type = bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
allow-pvp = mkOption {
|
||||||
|
type = bool;
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
allocated-memory = mkOption {
|
allocated-memory = mkOption {
|
||||||
type = types.int;
|
type = int;
|
||||||
description = "Memory (in GB) to allocate to the Minecraft server.";
|
description = "Memory (in GB) to allocate to the Minecraft server.";
|
||||||
default = 2;
|
default = 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port on which to run the Minecraft server.";
|
||||||
|
default = 25565;
|
||||||
|
};
|
||||||
|
|
||||||
|
query-port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port for queries.";
|
||||||
|
default = 25566;
|
||||||
|
};
|
||||||
|
|
||||||
|
rcon-port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port for remote commands.";
|
||||||
|
default = 25567;
|
||||||
|
};
|
||||||
|
|
||||||
|
world-seed = mkOption {
|
||||||
|
type = nullOr int;
|
||||||
|
description = "Seed to use while generating the world.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
@ -64,10 +93,15 @@ in {
|
||||||
declarative = true;
|
declarative = true;
|
||||||
serverProperties = {
|
serverProperties = {
|
||||||
level-name = cfg.world-name;
|
level-name = cfg.world-name;
|
||||||
|
level-seed = toString cfg.world-seed;
|
||||||
motd = cfg.motd;
|
motd = cfg.motd;
|
||||||
difficulty = cfg.difficulty;
|
difficulty = cfg.difficulty;
|
||||||
gamemode = cfg.game-mode;
|
gamemode = cfg.game-mode;
|
||||||
allow-cheats = true;
|
allow-cheats = cfg.allow-cheats;
|
||||||
|
server-port = cfg.port;
|
||||||
|
"rcon.port" = cfg.rcon-port;
|
||||||
|
"query.port" = cfg.query-port;
|
||||||
|
pvp = cfg.allow-pvp;
|
||||||
};
|
};
|
||||||
jvmOpts = let
|
jvmOpts = let
|
||||||
opts = [
|
opts = [
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with pkgs.lib;
|
||||||
|
let
|
||||||
|
domainOpts = { name, ... }: {
|
||||||
|
options = with types; {
|
||||||
|
domain = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
|
||||||
|
servers = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "List of servers for this Nexus domain.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dns-servers = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "List of DNS servers for this Nexus domain.";
|
||||||
|
};
|
||||||
|
|
||||||
|
gssapi-realm = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
trusted-networks = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
records = let
|
||||||
|
recordOpts = { name, ... }: {
|
||||||
|
options = {
|
||||||
|
name = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Name of this record.";
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
|
||||||
|
type = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Record type of this record.";
|
||||||
|
};
|
||||||
|
|
||||||
|
content = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Data associated with this record.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in mkOption {
|
||||||
|
type = listOf (submodule recordOpts);
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.fudo.nexus = with types; {
|
||||||
|
domains = mkOption {
|
||||||
|
type = attrsOf (submodule domainOpts);
|
||||||
|
description = "Nexus domain configurations.";
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
312
lib/fudo/nsd.nix
312
lib/fudo/nsd.nix
|
@ -1,8 +1,6 @@
|
||||||
# ## NOTE:
|
# ## NOTE:
|
||||||
## This is a copy of the upstream version, which allows for overriding the state directory
|
## This is a copy of the upstream version, which allows for overriding the state directory
|
||||||
|
|
||||||
## OBSOLETE
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
@ -10,9 +8,9 @@ with lib;
|
||||||
let
|
let
|
||||||
cfg = config.fudo.nsd;
|
cfg = config.fudo.nsd;
|
||||||
|
|
||||||
username = "nsd";
|
username = cfg.user;
|
||||||
stateDir = cfg.stateDir;
|
stateDir = cfg.stateDirectory;
|
||||||
pidFile = stateDir + "/var/nsd.pid";
|
pidFile = "${stateDir}/run/nsd.pid";
|
||||||
|
|
||||||
# build nsd with the options needed for the given config
|
# build nsd with the options needed for the given config
|
||||||
nsdPkg = pkgs.nsd.override {
|
nsdPkg = pkgs.nsd.override {
|
||||||
|
@ -44,6 +42,7 @@ let
|
||||||
postBuild = ''
|
postBuild = ''
|
||||||
echo "checking zone files"
|
echo "checking zone files"
|
||||||
cd $out/zones
|
cd $out/zones
|
||||||
|
|
||||||
for zoneFile in *; do
|
for zoneFile in *; do
|
||||||
echo "|- checking zone '$out/zones/$zoneFile'"
|
echo "|- checking zone '$out/zones/$zoneFile'"
|
||||||
${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || {
|
${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || {
|
||||||
|
@ -52,9 +51,11 @@ let
|
||||||
echo Escaping them is not needed any more. Please make sure \
|
echo Escaping them is not needed any more. Please make sure \
|
||||||
to unescape them where they prefix a variable name.
|
to unescape them where they prefix a variable name.
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "checking configuration file"
|
echo "checking configuration file"
|
||||||
# Save original config file including key references...
|
# Save original config file including key references...
|
||||||
cp $out/nsd.conf{,.orig}
|
cp $out/nsd.conf{,.orig}
|
||||||
|
@ -79,16 +80,20 @@ let
|
||||||
server:
|
server:
|
||||||
chroot: "${stateDir}"
|
chroot: "${stateDir}"
|
||||||
username: ${username}
|
username: ${username}
|
||||||
|
|
||||||
# The directory for zonefile: files. The daemon chdirs here.
|
# The directory for zonefile: files. The daemon chdirs here.
|
||||||
zonesdir: "${stateDir}"
|
zonesdir: "${stateDir}"
|
||||||
|
|
||||||
# the list of dynamically added zones.
|
# the list of dynamically added zones.
|
||||||
database: "${stateDir}/var/nsd.db"
|
database: "${stateDir}/var/nsd.db"
|
||||||
pidfile: "${pidFile}"
|
pidfile: "${pidFile}"
|
||||||
xfrdfile: "${stateDir}/var/xfrd.state"
|
xfrdfile: "${stateDir}/var/xfrd.state"
|
||||||
xfrdir: "${stateDir}/tmp"
|
xfrdir: "${stateDir}/tmp"
|
||||||
zonelistfile: "${stateDir}/var/zone.list"
|
zonelistfile: "${stateDir}/var/zone.list"
|
||||||
|
|
||||||
# interfaces
|
# interfaces
|
||||||
${forEach " ip-address: " cfg.interfaces}
|
${forEach " ip-address: " cfg.interfaces}
|
||||||
|
|
||||||
ip-freebind: ${yesOrNo cfg.ipFreebind}
|
ip-freebind: ${yesOrNo cfg.ipFreebind}
|
||||||
hide-version: ${yesOrNo cfg.hideVersion}
|
hide-version: ${yesOrNo cfg.hideVersion}
|
||||||
identity: "${cfg.identity}"
|
identity: "${cfg.identity}"
|
||||||
|
@ -111,13 +116,16 @@ let
|
||||||
${maybeString "version: " cfg.version}
|
${maybeString "version: " cfg.version}
|
||||||
xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
|
xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
|
||||||
zonefiles-check: ${yesOrNo cfg.zonefilesCheck}
|
zonefiles-check: ${yesOrNo cfg.zonefilesCheck}
|
||||||
|
|
||||||
${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
|
${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
|
||||||
${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
|
${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
|
||||||
rrl-ratelimit: ${toString cfg.ratelimit.ratelimit}
|
rrl-ratelimit: ${toString cfg.ratelimit.ratelimit}
|
||||||
${maybeString "rrl-slip: " cfg.ratelimit.slip}
|
${maybeString "rrl-slip: " cfg.ratelimit.slip}
|
||||||
rrl-size: ${toString cfg.ratelimit.size}
|
rrl-size: ${toString cfg.ratelimit.size}
|
||||||
rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
|
rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
|
||||||
|
|
||||||
${keyConfigFile}
|
${keyConfigFile}
|
||||||
|
|
||||||
remote-control:
|
remote-control:
|
||||||
control-enable: ${yesOrNo cfg.remoteControl.enable}
|
control-enable: ${yesOrNo cfg.remoteControl.enable}
|
||||||
control-key-file: "${cfg.remoteControl.controlKeyFile}"
|
control-key-file: "${cfg.remoteControl.controlKeyFile}"
|
||||||
|
@ -126,7 +134,9 @@ let
|
||||||
control-port: ${toString cfg.remoteControl.port}
|
control-port: ${toString cfg.remoteControl.port}
|
||||||
server-key-file: "${cfg.remoteControl.serverKeyFile}"
|
server-key-file: "${cfg.remoteControl.serverKeyFile}"
|
||||||
server-cert-file: "${cfg.remoteControl.serverCertFile}"
|
server-cert-file: "${cfg.remoteControl.serverCertFile}"
|
||||||
|
|
||||||
${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
|
${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
|
||||||
|
|
||||||
${cfg.extraConfig}
|
${cfg.extraConfig}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
@ -159,13 +169,16 @@ let
|
||||||
${maybeString "outgoing-interface: " zone.outgoingInterface}
|
${maybeString "outgoing-interface: " zone.outgoingInterface}
|
||||||
${forEach " rrl-whitelist: " zone.rrlWhitelist}
|
${forEach " rrl-whitelist: " zone.rrlWhitelist}
|
||||||
${maybeString "zonestats: " zone.zoneStats}
|
${maybeString "zonestats: " zone.zoneStats}
|
||||||
|
|
||||||
${maybeToString "max-refresh-time: " zone.maxRefreshSecs}
|
${maybeToString "max-refresh-time: " zone.maxRefreshSecs}
|
||||||
${maybeToString "min-refresh-time: " zone.minRefreshSecs}
|
${maybeToString "min-refresh-time: " zone.minRefreshSecs}
|
||||||
${maybeToString "max-retry-time: " zone.maxRetrySecs}
|
${maybeToString "max-retry-time: " zone.maxRetrySecs}
|
||||||
${maybeToString "min-retry-time: " zone.minRetrySecs}
|
${maybeToString "min-retry-time: " zone.minRetrySecs}
|
||||||
|
|
||||||
allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback}
|
allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback}
|
||||||
${forEach " allow-notify: " zone.allowNotify}
|
${forEach " allow-notify: " zone.allowNotify}
|
||||||
${forEach " request-xfr: " zone.requestXFR}
|
${forEach " request-xfr: " zone.requestXFR}
|
||||||
|
|
||||||
${forEach " notify: " zone.notify}
|
${forEach " notify: " zone.notify}
|
||||||
notify-retry: ${toString zone.notifyRetry}
|
notify-retry: ${toString zone.notifyRetry}
|
||||||
${forEach " provide-xfr: " zone.provideXFR}
|
${forEach " provide-xfr: " zone.provideXFR}
|
||||||
|
@ -194,7 +207,7 @@ let
|
||||||
allowAXFRFallback = mkOption {
|
allowAXFRFallback = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
If NSD as secondary server should be allowed to AXFR if the primary
|
If NSD as secondary server should be allowed to AXFR if the primary
|
||||||
server does not allow IXFR.
|
server does not allow IXFR.
|
||||||
'';
|
'';
|
||||||
|
@ -208,21 +221,24 @@ let
|
||||||
"10.0.0.1-10.0.0.5 my_tsig_key_name"
|
"10.0.0.1-10.0.0.5 my_tsig_key_name"
|
||||||
"10.0.3.4&255.255.0.0 BLOCKED"
|
"10.0.3.4&255.255.0.0 BLOCKED"
|
||||||
];
|
];
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Listed primary servers are allowed to notify this secondary server.
|
Listed primary servers are allowed to notify this secondary server.
|
||||||
<screen><![CDATA[
|
|
||||||
Format: <ip> <key-name | NOKEY | BLOCKED>
|
Format: `<ip> <key-name | NOKEY | BLOCKED>`
|
||||||
<ip> either a plain IPv4/IPv6 address or range. Valid patters for ranges:
|
|
||||||
* 10.0.0.0/24 # via subnet size
|
`<ip>` either a plain IPv4/IPv6 address or range.
|
||||||
* 10.0.0.0&255.255.255.0 # via subnet mask
|
Valid patters for ranges:
|
||||||
* 10.0.0.1-10.0.0.254 # via range
|
* `10.0.0.0/24`: via subnet size
|
||||||
|
* `10.0.0.0&255.255.255.0`: via subnet mask
|
||||||
|
* `10.0.0.1-10.0.0.254`: via range
|
||||||
|
|
||||||
A optional port number could be added with a '@':
|
A optional port number could be added with a '@':
|
||||||
* 2001:1234::1@1234
|
* `2001:1234::1@1234`
|
||||||
<key-name | NOKEY | BLOCKED>
|
|
||||||
* <key-name> will use the specified TSIG key
|
`<key-name | NOKEY | BLOCKED>`
|
||||||
* NOKEY no TSIG signature is required
|
* `<key-name>` will use the specified TSIG key
|
||||||
* BLOCKED notifies from non-listed or blocked IPs will be ignored
|
* `NOKEY` no TSIG signature is required
|
||||||
* ]]></screen>
|
* `BLOCKED`notifies from non-listed or blocked IPs will be ignored
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,7 +251,7 @@ let
|
||||||
# to default values, breaking the parent inheriting function.
|
# to default values, breaking the parent inheriting function.
|
||||||
type = types.attrsOf types.anything;
|
type = types.attrsOf types.anything;
|
||||||
default = { };
|
default = { };
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Children zones inherit all options of their parents. Attributes
|
Children zones inherit all options of their parents. Attributes
|
||||||
defined in a child will overwrite the ones of its parent. Only
|
defined in a child will overwrite the ones of its parent. Only
|
||||||
leaf zones will be actually served. This way it's possible to
|
leaf zones will be actually served. This way it's possible to
|
||||||
|
@ -248,29 +264,29 @@ let
|
||||||
data = mkOption {
|
data = mkOption {
|
||||||
type = types.lines;
|
type = types.lines;
|
||||||
default = "";
|
default = "";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
The actual zone data. This is the content of your zone file.
|
The actual zone data. This is the content of your zone file.
|
||||||
Use imports or pkgs.lib.readFile if you don't want this data in your config file.
|
Use imports or pkgs.lib.readFile if you don't want this data in your config file.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dnssec = mkEnableOption "DNSSEC";
|
dnssec = mkEnableOption (lib.mdDoc "DNSSEC");
|
||||||
|
|
||||||
dnssecPolicy = {
|
dnssecPolicy = {
|
||||||
algorithm = mkOption {
|
algorithm = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "RSASHA256";
|
default = "RSASHA256";
|
||||||
description = "Which algorithm to use for DNSSEC";
|
description = lib.mdDoc "Which algorithm to use for DNSSEC";
|
||||||
};
|
};
|
||||||
keyttl = mkOption {
|
keyttl = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "1h";
|
default = "1h";
|
||||||
description = "TTL for dnssec records";
|
description = lib.mdDoc "TTL for dnssec records";
|
||||||
};
|
};
|
||||||
coverage = mkOption {
|
coverage = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "1y";
|
default = "1y";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
The length of time to ensure that keys will be correct; no action will be taken to create new keys to be activated after this time.
|
The length of time to ensure that keys will be correct; no action will be taken to create new keys to be activated after this time.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -282,7 +298,7 @@ let
|
||||||
postPublish = "1w";
|
postPublish = "1w";
|
||||||
rollPeriod = "1mo";
|
rollPeriod = "1mo";
|
||||||
};
|
};
|
||||||
description = "Key policy for zone signing keys";
|
description = lib.mdDoc "Key policy for zone signing keys";
|
||||||
};
|
};
|
||||||
ksk = mkOption {
|
ksk = mkOption {
|
||||||
type = keyPolicy;
|
type = keyPolicy;
|
||||||
|
@ -292,14 +308,22 @@ let
|
||||||
postPublish = "1mo";
|
postPublish = "1mo";
|
||||||
rollPeriod = "0";
|
rollPeriod = "0";
|
||||||
};
|
};
|
||||||
description = "Key policy for key signing keys";
|
description = lib.mdDoc "Key policy for key signing keys";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
ksk = {
|
||||||
|
keyFile = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description =
|
||||||
|
"Location of the zone key-signing key file on the local host.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
maxRefreshSecs = mkOption {
|
maxRefreshSecs = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Limit refresh time for secondary zones. This is the timer which
|
Limit refresh time for secondary zones. This is the timer which
|
||||||
checks to see if the zone has to be refetched when it expires.
|
checks to see if the zone has to be refetched when it expires.
|
||||||
Normally the value from the SOA record is used, but this option
|
Normally the value from the SOA record is used, but this option
|
||||||
|
@ -310,7 +334,7 @@ let
|
||||||
minRefreshSecs = mkOption {
|
minRefreshSecs = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Limit refresh time for secondary zones.
|
Limit refresh time for secondary zones.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -318,7 +342,7 @@ let
|
||||||
maxRetrySecs = mkOption {
|
maxRetrySecs = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Limit retry time for secondary zones. This is the timeout after
|
Limit retry time for secondary zones. This is the timeout after
|
||||||
a failed fetch attempt for the zone. Normally the value from
|
a failed fetch attempt for the zone. Normally the value from
|
||||||
the SOA record is used, but this option restricts that value.
|
the SOA record is used, but this option restricts that value.
|
||||||
|
@ -328,7 +352,7 @@ let
|
||||||
minRetrySecs = mkOption {
|
minRetrySecs = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Limit retry time for secondary zones.
|
Limit retry time for secondary zones.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -337,23 +361,24 @@ let
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
|
example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
This primary server will notify all given secondary servers about
|
This primary server will notify all given secondary servers about
|
||||||
zone changes.
|
zone changes.
|
||||||
<screen><![CDATA[
|
|
||||||
Format: <ip> <key-name | NOKEY>
|
Format: `<ip> <key-name | NOKEY>`
|
||||||
<ip> a plain IPv4/IPv6 address with on optional port number (ip@port)
|
|
||||||
<key-name | NOKEY>
|
`<ip>` a plain IPv4/IPv6 address with on optional port number (ip@port)
|
||||||
* <key-name> sign notifies with the specified key
|
|
||||||
* NOKEY don't sign notifies
|
`<key-name | NOKEY>`
|
||||||
]]></screen>
|
- `<key-name>` sign notifies with the specified key
|
||||||
|
- `NOKEY` don't sign notifies
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
notifyRetry = mkOption {
|
notifyRetry = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 5;
|
default = 5;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Specifies the number of retries for failed notifies. Set this along with notify.
|
Specifies the number of retries for failed notifies. Set this along with notify.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -362,8 +387,8 @@ let
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "2000::1@1234";
|
example = "2000::1@1234";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
This address will be used for zone-transfere requests if configured
|
This address will be used for zone-transfer requests if configured
|
||||||
as a secondary server or notifications in case of a primary server.
|
as a secondary server or notifications in case of a primary server.
|
||||||
Supply either a plain IPv4 or IPv6 address with an optional port
|
Supply either a plain IPv4 or IPv6 address with an optional port
|
||||||
number (ip@port).
|
number (ip@port).
|
||||||
|
@ -374,17 +399,17 @@ let
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
|
example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
|
Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
|
||||||
address range 192.0.2.0/24, 1.2.3.4&255.255.0.0, 3.0.2.20-3.0.2.40
|
address range 192.0.2.0/24, 1.2.3.4&255.255.0.0, 3.0.2.20-3.0.2.40
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
requestXFR = mkOption {
|
requestXFR = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Format: <code>[AXFR|UDP] <ip-address> <key-name | NOKEY></code>
|
Format: `[AXFR|UDP] <ip-address> <key-name | NOKEY>`
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -403,7 +428,7 @@ let
|
||||||
"all"
|
"all"
|
||||||
]);
|
]);
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whitelists the given rrl-types.
|
Whitelists the given rrl-types.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -412,7 +437,7 @@ let
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "%s";
|
example = "%s";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
When set to something distinct to null NSD is able to collect
|
When set to something distinct to null NSD is able to collect
|
||||||
statistics per zone. All statistics of this zone(s) will be added
|
statistics per zone. All statistics of this zone(s) will be added
|
||||||
to the group specified by this given name. Use "%s" to use the zones
|
to the group specified by this given name. Use "%s" to use the zones
|
||||||
|
@ -427,19 +452,20 @@ let
|
||||||
options = {
|
options = {
|
||||||
keySize = mkOption {
|
keySize = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
description = "Key size in bits";
|
description = lib.mdDoc "Key size in bits";
|
||||||
};
|
};
|
||||||
prePublish = mkOption {
|
prePublish = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "How long in advance to publish new keys";
|
description = lib.mdDoc "How long in advance to publish new keys";
|
||||||
};
|
};
|
||||||
postPublish = mkOption {
|
postPublish = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "How long after deactivation to keep a key in the zone";
|
description =
|
||||||
|
lib.mdDoc "How long after deactivation to keep a key in the zone";
|
||||||
};
|
};
|
||||||
rollPeriod = mkOption {
|
rollPeriod = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "How frequently to change keys";
|
description = lib.mdDoc "How frequently to change keys";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -450,20 +476,42 @@ let
|
||||||
dnssec = dnssecZones != { };
|
dnssec = dnssecZones != { };
|
||||||
|
|
||||||
dnssecTools = pkgs.bind.override { enablePython = true; };
|
dnssecTools = pkgs.bind.override { enablePython = true; };
|
||||||
|
ldnsTools = pkgs.ldns.examples;
|
||||||
|
|
||||||
signZones = optionalString dnssec ''
|
signZones = let
|
||||||
|
dnssecZoneNames = attrNames dnssecZones;
|
||||||
|
mkdirCmds = map (zone: ''
|
||||||
|
mkdir -p ${stateDir}/dnssec/${zone}
|
||||||
|
chown ${username}:${username} ${stateDir}/dnssec/${zone}
|
||||||
|
chmod 0600 ${stateDir}/dnssec/${zone}
|
||||||
|
'') (attrNames dnssecZones);
|
||||||
|
in optionalString dnssec ''
|
||||||
mkdir -p ${stateDir}/dnssec
|
mkdir -p ${stateDir}/dnssec
|
||||||
chown ${username}:${username} ${stateDir}/dnssec
|
chown ${username}:${username} ${stateDir}/dnssec
|
||||||
chmod 0600 ${stateDir}/dnssec
|
chmod 0600 ${stateDir}/dnssec
|
||||||
${concatStrings (mapAttrsToList signZone dnssecZones)}
|
${concatStringsSep "\n" mkdirCmds}
|
||||||
|
|
||||||
|
${concatStringsSep "\n" (mapAttrsToList signZone dnssecZones)}
|
||||||
'';
|
'';
|
||||||
signZone = name: zone: ''
|
signZone = name: zone: ''
|
||||||
${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${
|
${pkgs.nsdSignZone}/bin/nsd-sign-zone \
|
||||||
policyFile name zone.dnssecPolicy
|
--verbose \
|
||||||
} ${name}
|
--domain=${name} \
|
||||||
${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name}
|
--ksk-file=${zone.ksk.keyFile} \
|
||||||
${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name}
|
--zsk-metadata=${stateDir}/dnssec/${name}/metadata.json \
|
||||||
|
${stateDir}/zones/${name}
|
||||||
|
${nsdPkg}/sbin/nsd-checkzone \
|
||||||
|
${name} \
|
||||||
|
${stateDir}/zones/${name}.signed &&
|
||||||
|
mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name}
|
||||||
'';
|
'';
|
||||||
|
# signZone = name: zone: ''
|
||||||
|
# ${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${
|
||||||
|
# policyFile name zone.dnssecPolicy
|
||||||
|
# } ${name}
|
||||||
|
# ${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name}
|
||||||
|
# ${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name}
|
||||||
|
# '';
|
||||||
policyFile = name: policy:
|
policyFile = name: policy:
|
||||||
pkgs.writeText "${name}.policy" ''
|
pkgs.writeText "${name}.policy" ''
|
||||||
zone ${name} {
|
zone ${name} {
|
||||||
|
@ -484,20 +532,26 @@ in {
|
||||||
# options are ordered alphanumerically
|
# options are ordered alphanumerically
|
||||||
options.fudo.nsd = {
|
options.fudo.nsd = {
|
||||||
|
|
||||||
enable = mkEnableOption "NSD authoritative DNS server";
|
enable = mkEnableOption (lib.mdDoc "NSD authoritative DNS server");
|
||||||
|
|
||||||
stateDir = mkOption {
|
user = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "Directory at which to store NSD state data.";
|
description = "User as which to run the NSD server.";
|
||||||
|
default = "nsd";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDirectory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Location at which to store NSD state.";
|
||||||
default = "/var/lib/nsd";
|
default = "/var/lib/nsd";
|
||||||
};
|
};
|
||||||
|
|
||||||
bind8Stats = mkEnableOption "BIND8 like statistics";
|
bind8Stats = mkEnableOption (lib.mdDoc "BIND8 like statistics");
|
||||||
|
|
||||||
dnssecInterval = mkOption {
|
dnssecInterval = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "1h";
|
default = "1h";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
How often to check whether dnssec key rollover is required
|
How often to check whether dnssec key rollover is required
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -505,7 +559,7 @@ in {
|
||||||
extraConfig = mkOption {
|
extraConfig = mkOption {
|
||||||
type = types.lines;
|
type = types.lines;
|
||||||
default = "";
|
default = "";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Extra nsd config.
|
Extra nsd config.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -513,7 +567,7 @@ in {
|
||||||
hideVersion = mkOption {
|
hideVersion = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
|
Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -521,7 +575,7 @@ in {
|
||||||
identity = mkOption {
|
identity = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "unidentified server";
|
default = "unidentified server";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Identify the server (CH TXT ID.SERVER entry).
|
Identify the server (CH TXT ID.SERVER entry).
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -529,7 +583,7 @@ in {
|
||||||
interfaces = mkOption {
|
interfaces = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ "127.0.0.0" "::1" ];
|
default = [ "127.0.0.0" "::1" ];
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
What addresses the server should listen to.
|
What addresses the server should listen to.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -537,7 +591,7 @@ in {
|
||||||
ipFreebind = mkOption {
|
ipFreebind = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whether to bind to nonlocal addresses and interfaces that are down.
|
Whether to bind to nonlocal addresses and interfaces that are down.
|
||||||
Similar to ip-transparent.
|
Similar to ip-transparent.
|
||||||
'';
|
'';
|
||||||
|
@ -546,7 +600,7 @@ in {
|
||||||
ipTransparent = mkOption {
|
ipTransparent = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Allow binding to non local addresses.
|
Allow binding to non local addresses.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -554,7 +608,7 @@ in {
|
||||||
ipv4 = mkOption {
|
ipv4 = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whether to listen on IPv4 connections.
|
Whether to listen on IPv4 connections.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -562,7 +616,7 @@ in {
|
||||||
ipv4EDNSSize = mkOption {
|
ipv4EDNSSize = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 4096;
|
default = 4096;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Preferred EDNS buffer size for IPv4.
|
Preferred EDNS buffer size for IPv4.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -570,7 +624,7 @@ in {
|
||||||
ipv6 = mkOption {
|
ipv6 = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whether to listen on IPv6 connections.
|
Whether to listen on IPv6 connections.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -578,7 +632,7 @@ in {
|
||||||
ipv6EDNSSize = mkOption {
|
ipv6EDNSSize = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 4096;
|
default = 4096;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Preferred EDNS buffer size for IPv6.
|
Preferred EDNS buffer size for IPv6.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -586,7 +640,7 @@ in {
|
||||||
logTimeAscii = mkOption {
|
logTimeAscii = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Log time in ascii, if false then in unix epoch seconds.
|
Log time in ascii, if false then in unix epoch seconds.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -594,15 +648,15 @@ in {
|
||||||
nsid = mkOption {
|
nsid = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
NSID identity (hex string, or "ascii_somestring").
|
NSID identity (hex string, or "ascii_somestring").
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = types.int;
|
type = types.port;
|
||||||
default = 53;
|
default = 53;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Port the service should bind do.
|
Port the service should bind do.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -611,7 +665,7 @@ in {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = pkgs.stdenv.isLinux;
|
default = pkgs.stdenv.isLinux;
|
||||||
defaultText = literalExpression "pkgs.stdenv.isLinux";
|
defaultText = literalExpression "pkgs.stdenv.isLinux";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whether to enable SO_REUSEPORT on all used sockets. This lets multiple
|
Whether to enable SO_REUSEPORT on all used sockets. This lets multiple
|
||||||
processes bind to the same port. This speeds up operation especially
|
processes bind to the same port. This speeds up operation especially
|
||||||
if the server count is greater than one and makes fast restarts less
|
if the server count is greater than one and makes fast restarts less
|
||||||
|
@ -622,18 +676,18 @@ in {
|
||||||
rootServer = mkOption {
|
rootServer = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whether this server will be a root server (a DNS root server, you
|
Whether this server will be a root server (a DNS root server, you
|
||||||
usually don't want that).
|
usually don't want that).
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
roundRobin = mkEnableOption "round robin rotation of records";
|
roundRobin = mkEnableOption (lib.mdDoc "round robin rotation of records");
|
||||||
|
|
||||||
serverCount = mkOption {
|
serverCount = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 1;
|
default = 1;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Number of NSD servers to fork. Put the number of CPUs to use here.
|
Number of NSD servers to fork. Put the number of CPUs to use here.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -641,7 +695,7 @@ in {
|
||||||
statistics = mkOption {
|
statistics = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Statistics are produced every number of seconds. Prints to log.
|
Statistics are produced every number of seconds. Prints to log.
|
||||||
If null no statistics are logged.
|
If null no statistics are logged.
|
||||||
'';
|
'';
|
||||||
|
@ -650,7 +704,7 @@ in {
|
||||||
tcpCount = mkOption {
|
tcpCount = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 100;
|
default = 100;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Maximum number of concurrent TCP connections per server.
|
Maximum number of concurrent TCP connections per server.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -658,7 +712,7 @@ in {
|
||||||
tcpQueryCount = mkOption {
|
tcpQueryCount = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 0;
|
default = 0;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Maximum number of queries served on a single TCP connection.
|
Maximum number of queries served on a single TCP connection.
|
||||||
0 means no maximum.
|
0 means no maximum.
|
||||||
'';
|
'';
|
||||||
|
@ -667,7 +721,7 @@ in {
|
||||||
tcpTimeout = mkOption {
|
tcpTimeout = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 120;
|
default = 120;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
TCP timeout in seconds.
|
TCP timeout in seconds.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -675,7 +729,7 @@ in {
|
||||||
verbosity = mkOption {
|
verbosity = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 0;
|
default = 0;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Verbosity level.
|
Verbosity level.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -683,7 +737,7 @@ in {
|
||||||
version = mkOption {
|
version = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
The version string replied for CH TXT version.server and version.bind
|
The version string replied for CH TXT version.server and version.bind
|
||||||
queries. Will use the compiled package version on null.
|
queries. Will use the compiled package version on null.
|
||||||
See hideVersion for enabling/disabling this responses.
|
See hideVersion for enabling/disabling this responses.
|
||||||
|
@ -693,7 +747,7 @@ in {
|
||||||
xfrdReloadTimeout = mkOption {
|
xfrdReloadTimeout = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 1;
|
default = 1;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Number of seconds between reloads triggered by xfrd.
|
Number of seconds between reloads triggered by xfrd.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -701,7 +755,7 @@ in {
|
||||||
zonefilesCheck = mkOption {
|
zonefilesCheck = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Whether to check mtime of all zone files on start and sighup.
|
Whether to check mtime of all zone files on start and sighup.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -713,14 +767,14 @@ in {
|
||||||
algorithm = mkOption {
|
algorithm = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "hmac-sha256";
|
default = "hmac-sha256";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Authentication algorithm for this key.
|
Authentication algorithm for this key.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
keyFile = mkOption {
|
keyFile = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Path to the file which contains the actual base64 encoded
|
Path to the file which contains the actual base64 encoded
|
||||||
key. The key will be copied into "${stateDir}/private" before
|
key. The key will be copied into "${stateDir}/private" before
|
||||||
NSD starts. The copied file is only accessibly by the NSD
|
NSD starts. The copied file is only accessibly by the NSD
|
||||||
|
@ -738,19 +792,19 @@ in {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Define your TSIG keys here.
|
Define your TSIG keys here.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
ratelimit = {
|
ratelimit = {
|
||||||
|
|
||||||
enable = mkEnableOption "ratelimit capabilities";
|
enable = mkEnableOption (lib.mdDoc "ratelimit capabilities");
|
||||||
|
|
||||||
ipv4PrefixLength = mkOption {
|
ipv4PrefixLength = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
IPv4 prefix length. Addresses are grouped by netblock.
|
IPv4 prefix length. Addresses are grouped by netblock.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -758,7 +812,7 @@ in {
|
||||||
ipv6PrefixLength = mkOption {
|
ipv6PrefixLength = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
IPv6 prefix length. Addresses are grouped by netblock.
|
IPv6 prefix length. Addresses are grouped by netblock.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -766,7 +820,7 @@ in {
|
||||||
ratelimit = mkOption {
|
ratelimit = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 200;
|
default = 200;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Max qps allowed from any query source.
|
Max qps allowed from any query source.
|
||||||
0 means unlimited. With an verbosity of 2 blocked and
|
0 means unlimited. With an verbosity of 2 blocked and
|
||||||
unblocked subnets will be logged.
|
unblocked subnets will be logged.
|
||||||
|
@ -776,7 +830,7 @@ in {
|
||||||
slip = mkOption {
|
slip = mkOption {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Number of packets that get discarded before replying a SLIP response.
|
Number of packets that get discarded before replying a SLIP response.
|
||||||
0 disables SLIP responses. 1 will make every response a SLIP response.
|
0 disables SLIP responses. 1 will make every response a SLIP response.
|
||||||
'';
|
'';
|
||||||
|
@ -785,7 +839,7 @@ in {
|
||||||
size = mkOption {
|
size = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 1000000;
|
default = 1000000;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Size of the hashtable. More buckets use more memory but lower
|
Size of the hashtable. More buckets use more memory but lower
|
||||||
the chance of hash hash collisions.
|
the chance of hash hash collisions.
|
||||||
'';
|
'';
|
||||||
|
@ -794,7 +848,7 @@ in {
|
||||||
whitelistRatelimit = mkOption {
|
whitelistRatelimit = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 2000;
|
default = 2000;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Max qps allowed from whitelisted sources.
|
Max qps allowed from whitelisted sources.
|
||||||
0 means unlimited. Set the rrl-whitelist option for specific
|
0 means unlimited. Set the rrl-whitelist option for specific
|
||||||
queries to apply this limit instead of the default to them.
|
queries to apply this limit instead of the default to them.
|
||||||
|
@ -805,12 +859,12 @@ in {
|
||||||
|
|
||||||
remoteControl = {
|
remoteControl = {
|
||||||
|
|
||||||
enable = mkEnableOption "remote control via nsd-control";
|
enable = mkEnableOption (lib.mdDoc "remote control via nsd-control");
|
||||||
|
|
||||||
controlCertFile = mkOption {
|
controlCertFile = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "/etc/nsd/nsd_control.pem";
|
default = "/etc/nsd/nsd_control.pem";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Path to the client certificate signed with the server certificate.
|
Path to the client certificate signed with the server certificate.
|
||||||
This file is used by nsd-control and generated by nsd-control-setup.
|
This file is used by nsd-control and generated by nsd-control-setup.
|
||||||
'';
|
'';
|
||||||
|
@ -819,7 +873,7 @@ in {
|
||||||
controlKeyFile = mkOption {
|
controlKeyFile = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "/etc/nsd/nsd_control.key";
|
default = "/etc/nsd/nsd_control.key";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Path to the client private key, which is used by nsd-control
|
Path to the client private key, which is used by nsd-control
|
||||||
but not by the server. This file is generated by nsd-control-setup.
|
but not by the server. This file is generated by nsd-control-setup.
|
||||||
'';
|
'';
|
||||||
|
@ -828,15 +882,15 @@ in {
|
||||||
interfaces = mkOption {
|
interfaces = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ "127.0.0.1" "::1" ];
|
default = [ "127.0.0.1" "::1" ];
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Which interfaces NSD should bind to for remote control.
|
Which interfaces NSD should bind to for remote control.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = types.int;
|
type = types.port;
|
||||||
default = 8952;
|
default = 8952;
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Port number for remote control operations (uses TLS over TCP).
|
Port number for remote control operations (uses TLS over TCP).
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -844,7 +898,7 @@ in {
|
||||||
serverCertFile = mkOption {
|
serverCertFile = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "/etc/nsd/nsd_server.pem";
|
default = "/etc/nsd/nsd_server.pem";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Path to the server self signed certificate, which is used by the server
|
Path to the server self signed certificate, which is used by the server
|
||||||
but and by nsd-control. This file is generated by nsd-control-setup.
|
but and by nsd-control. This file is generated by nsd-control-setup.
|
||||||
'';
|
'';
|
||||||
|
@ -853,7 +907,7 @@ in {
|
||||||
serverKeyFile = mkOption {
|
serverKeyFile = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "/etc/nsd/nsd_server.key";
|
default = "/etc/nsd/nsd_server.key";
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Path to the server private key, which is used by the server
|
Path to the server private key, which is used by the server
|
||||||
but not by nsd-control. This file is generated by nsd-control-setup.
|
but not by nsd-control. This file is generated by nsd-control-setup.
|
||||||
'';
|
'';
|
||||||
|
@ -886,6 +940,7 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
"example.net." = {
|
"example.net." = {
|
||||||
provideXFR = [ "10.3.2.1 NOKEY" ];
|
provideXFR = [ "10.3.2.1 NOKEY" ];
|
||||||
data = '''
|
data = '''
|
||||||
|
@ -894,7 +949,7 @@ in {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
description = ''
|
description = lib.mdDoc ''
|
||||||
Define your zones here. Zones can cascade other zones and therefore
|
Define your zones here. Zones can cascade other zones and therefore
|
||||||
inherit settings from parent zones. Look at the definition of
|
inherit settings from parent zones. Look at the definition of
|
||||||
children to learn about inheritance and child zones.
|
children to learn about inheritance and child zones.
|
||||||
|
@ -918,14 +973,15 @@ in {
|
||||||
etc."nsd/nsd.conf".source = "${configFile}/nsd.conf";
|
etc."nsd/nsd.conf".source = "${configFile}/nsd.conf";
|
||||||
};
|
};
|
||||||
|
|
||||||
users.groups.${username}.gid = config.ids.gids.nsd;
|
users = {
|
||||||
|
users."${username}" = {
|
||||||
users.users.${username} = {
|
description = "NSD service user";
|
||||||
description = "NSD service user";
|
home = stateDir;
|
||||||
home = stateDir;
|
createHome = true;
|
||||||
createHome = true;
|
uid = config.ids.uids.nsd;
|
||||||
uid = config.ids.uids.nsd;
|
group = username;
|
||||||
group = username;
|
};
|
||||||
|
groups."${username}".gid = config.ids.gids.nsd;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.nsd = {
|
systemd.services.nsd = {
|
||||||
|
@ -947,19 +1003,24 @@ in {
|
||||||
preStart = ''
|
preStart = ''
|
||||||
rm -Rf "${stateDir}/private/"
|
rm -Rf "${stateDir}/private/"
|
||||||
rm -Rf "${stateDir}/tmp/"
|
rm -Rf "${stateDir}/tmp/"
|
||||||
|
|
||||||
mkdir -m 0700 -p "${stateDir}/private"
|
mkdir -m 0700 -p "${stateDir}/private"
|
||||||
mkdir -m 0700 -p "${stateDir}/tmp"
|
mkdir -m 0700 -p "${stateDir}/tmp"
|
||||||
mkdir -m 0700 -p "${stateDir}/var"
|
mkdir -m 0700 -p "${stateDir}/var"
|
||||||
|
|
||||||
cat > "${stateDir}/don't touch anything in here" << EOF
|
cat > "${stateDir}/don't touch anything in here" << EOF
|
||||||
Everything in this directory except NSD's state in var and dnssec
|
Everything in this directory except NSD's state in var and dnssec
|
||||||
is automatically generated and will be purged and redeployed by
|
is automatically generated and will be purged and redeployed by
|
||||||
the nsd.service pre-start script.
|
the nsd.service pre-start script.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chown ${username}:${username} -R "${stateDir}/private"
|
chown ${username}:${username} -R "${stateDir}/private"
|
||||||
chown ${username}:${username} -R "${stateDir}/tmp"
|
chown ${username}:${username} -R "${stateDir}/tmp"
|
||||||
chown ${username}:${username} -R "${stateDir}/var"
|
chown ${username}:${username} -R "${stateDir}/var"
|
||||||
|
|
||||||
rm -rf "${stateDir}/zones"
|
rm -rf "${stateDir}/zones"
|
||||||
cp -rL "${nsdEnv}/zones" "${stateDir}/zones"
|
cp -rL "${nsdEnv}/zones" "${stateDir}/zones"
|
||||||
|
|
||||||
${copyKeys}
|
${copyKeys}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -968,6 +1029,7 @@ in {
|
||||||
description = "Automatic DNSSEC key rollover";
|
description = "Automatic DNSSEC key rollover";
|
||||||
|
|
||||||
wantedBy = [ "nsd.service" ];
|
wantedBy = [ "nsd.service" ];
|
||||||
|
before = [ "nsd.service" ];
|
||||||
|
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
OnActiveSec = cfg.dnssecInterval;
|
OnActiveSec = cfg.dnssecInterval;
|
||||||
|
@ -981,6 +1043,22 @@ in {
|
||||||
wantedBy = [ "nsd.service" ];
|
wantedBy = [ "nsd.service" ];
|
||||||
before = [ "nsd.service" ];
|
before = [ "nsd.service" ];
|
||||||
|
|
||||||
|
preStart = let
|
||||||
|
zoneRotateCmd = zone:
|
||||||
|
let zoneDir = "${stateDir}/dnssec/${zone}";
|
||||||
|
in ''
|
||||||
|
mkdir -p ${zoneDir}
|
||||||
|
${pkgs.nsdRotateKeys}/bin/nsd-rotate-keys \
|
||||||
|
--key-directory=${zoneDir} \
|
||||||
|
--validity-period=30 \
|
||||||
|
--period-overlap=10 \
|
||||||
|
--metadata=${zoneDir}/metadata.json \
|
||||||
|
--verbose \
|
||||||
|
${zone}
|
||||||
|
'';
|
||||||
|
zoneRotateCmds = map zoneRotateCmd (lib.attrNames dnssecZones);
|
||||||
|
in lib.concatStringsSep "\n" zoneRotateCmds;
|
||||||
|
|
||||||
script = signZones;
|
script = signZones;
|
||||||
|
|
||||||
postStop = ''
|
postStop = ''
|
||||||
|
|
|
@ -151,6 +151,12 @@ in {
|
||||||
options.fudo.postgresql = with types; {
|
options.fudo.postgresql = with types; {
|
||||||
enable = mkEnableOption "Fudo PostgreSQL Server";
|
enable = mkEnableOption "Fudo PostgreSQL Server";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = package;
|
||||||
|
description = "Which package to use for Postgresql server.";
|
||||||
|
default = pkgs.postgresql_15_gssapi;
|
||||||
|
};
|
||||||
|
|
||||||
ssl-private-key = mkOption {
|
ssl-private-key = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = "Location of the server SSL private key.";
|
description = "Location of the server SSL private key.";
|
||||||
|
@ -247,7 +253,7 @@ in {
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 5432 ];
|
networking.firewall.allowedTCPPorts = [ 5432 ];
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [ postgresql_11_gssapi ];
|
environment.systemPackages = with pkgs; [ cfg.package ];
|
||||||
|
|
||||||
users.groups = {
|
users.groups = {
|
||||||
${cfg.socket-group} = { members = [ "postgres" ] ++ cfg.local-users; };
|
${cfg.socket-group} = { members = [ "postgres" ] ++ cfg.local-users; };
|
||||||
|
@ -255,7 +261,7 @@ in {
|
||||||
|
|
||||||
services.postgresql = {
|
services.postgresql = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.postgresql_11_gssapi;
|
package = cfg.package;
|
||||||
enableTCPIP = true;
|
enableTCPIP = true;
|
||||||
ensureDatabases = mapAttrsToList (name: value: name) cfg.databases;
|
ensureDatabases = mapAttrsToList (name: value: name) cfg.databases;
|
||||||
ensureUsers = ((mapAttrsToList (username: attrs: {
|
ensureUsers = ((mapAttrsToList (username: attrs: {
|
||||||
|
|
|
@ -39,13 +39,27 @@ let
|
||||||
{ source-file, target-file, user, group, permissions, ... }: {
|
{ source-file, target-file, user, group, permissions, ... }: {
|
||||||
description =
|
description =
|
||||||
"decrypt secret ${secret-name} at ${target-host}:${target-file}.";
|
"decrypt secret ${secret-name} at ${target-host}:${target-file}.";
|
||||||
wantedBy = [ cfg.secret-target ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requiredBy = [ cfg.secret-target ];
|
||||||
|
requires = [ "local-fs.target" ];
|
||||||
before = [ cfg.secret-target ];
|
before = [ cfg.secret-target ];
|
||||||
|
after = [ "local-fs.target" ];
|
||||||
|
restartIfChanged = true;
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
|
ExecStartPre =
|
||||||
|
pkgs.writeShellScript "fudo-secret-prep-${secret-name}.sh" ''
|
||||||
|
if [ ! -d ${dirOf target-file} ]; then
|
||||||
|
mkdir -p ${dirOf target-file}
|
||||||
|
chown ${user}:${group} ${dirOf target-file}
|
||||||
|
chmod ${if (group == null) then "0550" else "0500"} ${
|
||||||
|
dirOf target-file
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
'';
|
||||||
ExecStart =
|
ExecStart =
|
||||||
let host-master-key = config.fudo.hosts.${target-host}.master-key;
|
let host-master-key = config.fudo.hosts."${target-host}".master-key;
|
||||||
in decrypt-script {
|
in decrypt-script {
|
||||||
inherit secret-name source-file target-host target-file
|
inherit secret-name source-file target-host target-file
|
||||||
host-master-key user group permissions;
|
host-master-key user group permissions;
|
||||||
|
@ -218,11 +232,11 @@ in {
|
||||||
cfg.secret-paths;
|
cfg.secret-paths;
|
||||||
|
|
||||||
in {
|
in {
|
||||||
tmpfiles.rules = host-secret-paths ++ build-secret-paths;
|
tmpfiles.rules = unique (host-secret-paths ++ build-secret-paths);
|
||||||
|
|
||||||
services = host-secret-services // {
|
services = host-secret-services // {
|
||||||
fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) {
|
fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) {
|
||||||
wantedBy = [ "default.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
description =
|
description =
|
||||||
"Ensure access for group ${cfg.secret-group} to fudo secret paths.";
|
"Ensure access for group ${cfg.secret-group} to fudo secret paths.";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
@ -241,12 +255,12 @@ in {
|
||||||
${strip-ext cfg.secret-target} = {
|
${strip-ext cfg.secret-target} = {
|
||||||
description =
|
description =
|
||||||
"Target indicating that all Fudo secrets are available.";
|
"Target indicating that all Fudo secrets are available.";
|
||||||
wantedBy = [ "default.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
paths.fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) {
|
paths.fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) {
|
||||||
wantedBy = [ "default.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
description = "Watch fudo secret paths, and correct perms on changes.";
|
description = "Watch fudo secret paths, and correct perms on changes.";
|
||||||
pathConfig = {
|
pathConfig = {
|
||||||
PathChanged = cfg.secret-paths;
|
PathChanged = cfg.secret-paths;
|
||||||
|
|
|
@ -139,9 +139,31 @@ let
|
||||||
|
|
||||||
local-gateway = mkOption {
|
local-gateway = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = "If this is a NAT site, this should point to the host acting as network gateway.";
|
description =
|
||||||
|
"If this is a NAT site, this should point to the host acting as network gateway.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
nexus = {
|
||||||
|
public-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "Nexus domains to which hosts in this domain belong.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
private-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"Nexus private domains to which hosts in this domain belong.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
tailscale-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"Nexus tailscale domains to which hosts in this domain belong.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,21 +4,22 @@ with lib;
|
||||||
let
|
let
|
||||||
cfg = config.fudo.slynk;
|
cfg = config.fudo.slynk;
|
||||||
|
|
||||||
initScript = port: load-paths: let
|
initScript = port: load-paths:
|
||||||
load-path-string =
|
let
|
||||||
concatStringsSep " " (map (path: "\"${path}\"") load-paths);
|
load-path-string =
|
||||||
in pkgs.writeText "slynk.lisp" ''
|
concatStringsSep " " (map (path: ''"${path}"'') load-paths);
|
||||||
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
|
in pkgs.writeText "slynk.lisp" ''
|
||||||
(ql:quickload :slynk)
|
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
|
||||||
(setf asdf:*central-registry*
|
(ql:quickload :slynk)
|
||||||
(append asdf:*central-registry*
|
(setf asdf:*central-registry*
|
||||||
(list ${load-path-string})))
|
(append asdf:*central-registry*
|
||||||
(slynk:create-server :port ${toString port} :dont-close t)
|
(list ${load-path-string})))
|
||||||
(dolist (var '("LD_LIBRARY_PATH"))
|
(slynk:create-server :port ${toString port} :dont-close t)
|
||||||
(format t "~S: ~S~%" var (sb-unix::posix-getenv var)))
|
(dolist (var '("LD_LIBRARY_PATH"))
|
||||||
|
(format t "~S: ~S~%" var (sb-unix::posix-getenv var)))
|
||||||
|
|
||||||
(loop (sleep 60))
|
(loop (sleep 60))
|
||||||
'';
|
'';
|
||||||
|
|
||||||
lisp-libs = with pkgs.lispPackages; [
|
lisp-libs = with pkgs.lispPackages; [
|
||||||
alexandria
|
alexandria
|
||||||
|
@ -47,14 +48,15 @@ in {
|
||||||
systemd.user.services.slynk = {
|
systemd.user.services.slynk = {
|
||||||
description = "Slynk Common Lisp server.";
|
description = "Slynk Common Lisp server.";
|
||||||
|
|
||||||
serviceConfig = let
|
serviceConfig =
|
||||||
load-paths = (map (pkg: "${pkg}/lib/common-lisp/") lisp-libs);
|
let load-paths = (map (pkg: "${pkg}/lib/common-lisp/") lisp-libs);
|
||||||
in {
|
in {
|
||||||
ExecStartPre = "${pkgs.lispPackages.quicklisp}/bin/quicklisp init";
|
ExecStartPre = "${pkgs.lispPackages.quicklisp}/bin/quicklisp init";
|
||||||
ExecStart = "${pkgs.sbcl}/bin/sbcl --load ${initScript cfg.port load-paths}";
|
ExecStart =
|
||||||
Restart = "on-failure";
|
"${pkgs.sbcl}/bin/sbcl --load ${initScript cfg.port load-paths}";
|
||||||
PIDFile = "/run/slynk.$USERNAME.pid";
|
Restart = "on-failure";
|
||||||
};
|
PIDFile = "/run/slynk.$USERNAME.pid";
|
||||||
|
};
|
||||||
|
|
||||||
path = with pkgs; [
|
path = with pkgs; [
|
||||||
gcc
|
gcc
|
||||||
|
@ -62,9 +64,7 @@ in {
|
||||||
file
|
file
|
||||||
];
|
];
|
||||||
|
|
||||||
environment = {
|
environment = { LD_LIBRARY_PATH = "${pkgs.openssl.out}/lib"; };
|
||||||
LD_LIBRARY_PATH = "${pkgs.openssl_1_1.out}/lib";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,12 @@ let
|
||||||
|
|
||||||
cfg = config.fudo.webmail;
|
cfg = config.fudo.webmail;
|
||||||
|
|
||||||
|
base-data-path = cfg.state-directory;
|
||||||
|
|
||||||
webmail-user = cfg.user;
|
webmail-user = cfg.user;
|
||||||
webmail-group = cfg.group;
|
webmail-group = cfg.group;
|
||||||
|
|
||||||
base-data-path = "/run/rainloop";
|
concatMapAttrs = f: attrs: foldr (a: b: a // b) { } (mapAttrsToList f attrs);
|
||||||
|
|
||||||
concatMapAttrs = f: attrs:
|
|
||||||
foldr (a: b: a // b) {} (mapAttrsToList f attrs);
|
|
||||||
|
|
||||||
fastcgi-conf = builtins.toFile "fastcgi.conf" ''
|
fastcgi-conf = builtins.toFile "fastcgi.conf" ''
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
@ -57,142 +56,144 @@ let
|
||||||
'';
|
'';
|
||||||
})) cfg.sites;
|
})) cfg.sites;
|
||||||
|
|
||||||
siteOpts = { site-host, ... }: with types; {
|
siteOpts = { name, ... }:
|
||||||
options = {
|
with types; {
|
||||||
title = mkOption {
|
options = {
|
||||||
type = str;
|
title = mkOption {
|
||||||
description = "Webmail site title";
|
type = str;
|
||||||
example = "My Webmail";
|
description = "Webmail site title";
|
||||||
};
|
example = "My Webmail";
|
||||||
|
|
||||||
debug = mkOption {
|
|
||||||
type = bool;
|
|
||||||
description = "Turn debug logs on.";
|
|
||||||
default = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
mail-server = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "Mail server from which to send & recieve email.";
|
|
||||||
default = "mail.fudo.org";
|
|
||||||
};
|
|
||||||
|
|
||||||
favicon = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "URL of the site favicon";
|
|
||||||
example = "https://www.somepage.com/fav.ico";
|
|
||||||
};
|
|
||||||
|
|
||||||
messages-per-page = mkOption {
|
|
||||||
type = int;
|
|
||||||
description = "Default number of messages to show per page";
|
|
||||||
default = 30;
|
|
||||||
};
|
|
||||||
|
|
||||||
max-upload-size = mkOption {
|
|
||||||
type = int;
|
|
||||||
description = "Size limit in MB for uploaded files";
|
|
||||||
default = 30;
|
|
||||||
};
|
|
||||||
|
|
||||||
theme = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "Default theme to use for this webmail site.";
|
|
||||||
default = "Default";
|
|
||||||
};
|
|
||||||
|
|
||||||
domain = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "Domain for which the server acts as webmail server";
|
|
||||||
};
|
|
||||||
|
|
||||||
edit-mode = mkOption {
|
|
||||||
type = enum [ "Plain" "Html" "PlainForced" "HtmlForced" ];
|
|
||||||
description = "Default text editing mode for email";
|
|
||||||
default = "Html";
|
|
||||||
};
|
|
||||||
|
|
||||||
layout-mode = mkOption {
|
|
||||||
type = enum [ "side" "bottom" ];
|
|
||||||
description = "Layout mode to use for email preview.";
|
|
||||||
default = "side";
|
|
||||||
};
|
|
||||||
|
|
||||||
enable-threading = mkOption {
|
|
||||||
type = bool;
|
|
||||||
description = "Whether to enable threading for email.";
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
enable-mobile = mkOption {
|
|
||||||
type = bool;
|
|
||||||
description = "Whether to enable a mobile site view.";
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
database = mkOption {
|
|
||||||
type = nullOr (submodule databaseOpts);
|
|
||||||
description = "Database configuration for storing contact data.";
|
|
||||||
example = {
|
|
||||||
name = "my_db";
|
|
||||||
host = "db.domain.com";
|
|
||||||
user = "my_user";
|
|
||||||
password-file = /path/to/some/file.pw;
|
|
||||||
};
|
};
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
admin-email = mkOption {
|
debug = mkOption {
|
||||||
type = str;
|
type = bool;
|
||||||
description = "Email of administrator of this site.";
|
description = "Turn debug logs on.";
|
||||||
default = "admin@fudo.org";
|
default = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
mail-server = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Mail server from which to send & recieve email.";
|
||||||
|
default = "mail.fudo.org";
|
||||||
|
};
|
||||||
|
|
||||||
|
favicon = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "URL of the site favicon";
|
||||||
|
example = "https://www.somepage.com/fav.ico";
|
||||||
|
};
|
||||||
|
|
||||||
|
messages-per-page = mkOption {
|
||||||
|
type = int;
|
||||||
|
description = "Default number of messages to show per page";
|
||||||
|
default = 30;
|
||||||
|
};
|
||||||
|
|
||||||
|
max-upload-size = mkOption {
|
||||||
|
type = int;
|
||||||
|
description = "Size limit in MB for uploaded files";
|
||||||
|
default = 30;
|
||||||
|
};
|
||||||
|
|
||||||
|
theme = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Default theme to use for this webmail site.";
|
||||||
|
default = "Default";
|
||||||
|
};
|
||||||
|
|
||||||
|
domain = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Domain for which the server acts as webmail server";
|
||||||
|
};
|
||||||
|
|
||||||
|
edit-mode = mkOption {
|
||||||
|
type = enum [ "Plain" "Html" "PlainForced" "HtmlForced" ];
|
||||||
|
description = "Default text editing mode for email";
|
||||||
|
default = "Html";
|
||||||
|
};
|
||||||
|
|
||||||
|
layout-mode = mkOption {
|
||||||
|
type = enum [ "side" "bottom" ];
|
||||||
|
description = "Layout mode to use for email preview.";
|
||||||
|
default = "side";
|
||||||
|
};
|
||||||
|
|
||||||
|
enable-threading = mkOption {
|
||||||
|
type = bool;
|
||||||
|
description = "Whether to enable threading for email.";
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
enable-mobile = mkOption {
|
||||||
|
type = bool;
|
||||||
|
description = "Whether to enable a mobile site view.";
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
database = mkOption {
|
||||||
|
type = nullOr (submodule databaseOpts);
|
||||||
|
description = "Database configuration for storing contact data.";
|
||||||
|
example = {
|
||||||
|
name = "my_db";
|
||||||
|
host = "db.domain.com";
|
||||||
|
user = "my_user";
|
||||||
|
password-file = /path/to/some/file.pw;
|
||||||
|
};
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
admin-email = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Email of administrator of this site.";
|
||||||
|
default = "admin@fudo.org";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
databaseOpts = { ... }: with types; {
|
databaseOpts = { ... }:
|
||||||
options = {
|
with types; {
|
||||||
type = mkOption {
|
options = {
|
||||||
type = enum [ "pgsql" "mysql" ];
|
type = mkOption {
|
||||||
description = "Driver to use when connecting to the database.";
|
type = enum [ "pgsql" "mysql" ];
|
||||||
default = "pgsql";
|
description = "Driver to use when connecting to the database.";
|
||||||
};
|
default = "pgsql";
|
||||||
|
};
|
||||||
|
|
||||||
hostname = mkOption {
|
hostname = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Name of host running the database.";
|
description = "Name of host running the database.";
|
||||||
example = "my-db.domain.com";
|
example = "my-db.domain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = int;
|
type = int;
|
||||||
description = "Port on which the database server is listening.";
|
description = "Port on which the database server is listening.";
|
||||||
default = 5432;
|
default = 5432;
|
||||||
};
|
};
|
||||||
|
|
||||||
name = mkOption {
|
name = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description =
|
description =
|
||||||
"Name of the database containing contact info. <user> must have access.";
|
"Name of the database containing contact info. <user> must have access.";
|
||||||
default = "rainloop_webmail";
|
default = "rainloop_webmail";
|
||||||
};
|
};
|
||||||
|
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "User as which to connect to the database.";
|
description = "User as which to connect to the database.";
|
||||||
default = "webmail";
|
default = "webmail";
|
||||||
};
|
};
|
||||||
|
|
||||||
password-file = mkOption {
|
password-file = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = ''
|
description = ''
|
||||||
Password to use when connecting to the database.
|
Password to use when connecting to the database.
|
||||||
|
|
||||||
If unset, a random password will be generated.
|
If unset, a random password will be generated.
|
||||||
'';
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
in {
|
in {
|
||||||
options.fudo.webmail = with types; {
|
options.fudo.webmail = with types; {
|
||||||
|
@ -211,6 +212,11 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state-directory = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "The path at which to store server state.";
|
||||||
|
};
|
||||||
|
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "User as which webmail will run.";
|
description = "User as which webmail will run.";
|
||||||
|
@ -240,9 +246,8 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
security.acme.certs = mapAttrs
|
security.acme.certs =
|
||||||
(site: site-cfg: { email = site-cfg.admin-email; })
|
mapAttrs (site: site-cfg: { email = site-cfg.admin-email; }) cfg.sites;
|
||||||
cfg.sites;
|
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
phpfpm = {
|
phpfpm = {
|
||||||
|
@ -298,11 +303,12 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fudo.secrets.host-secrets.${hostname} = concatMapAttrs
|
fudo.secrets.host-secrets.${hostname} = concatMapAttrs (site: site-cfg:
|
||||||
(site: site-cfg: let
|
let
|
||||||
|
|
||||||
site-config-file = builtins.toFile "${site}-rainloop.cfg"
|
site-config-file = builtins.toFile "${site}-rainloop.cfg"
|
||||||
(import ./include/rainloop.nix lib site site-cfg site-packages.${site}.version);
|
(import ./include/rainloop.nix lib site site-cfg
|
||||||
|
site-packages.${site}.version);
|
||||||
|
|
||||||
domain-config-file = builtins.toFile "${site}-domain.cfg" ''
|
domain-config-file = builtins.toFile "${site}-domain.cfg" ''
|
||||||
imap_host = "${site-cfg.mail-server}"
|
imap_host = "${site-cfg.mail-server}"
|
||||||
|
@ -325,13 +331,13 @@ in {
|
||||||
in {
|
in {
|
||||||
"${site}-site-config" = {
|
"${site}-site-config" = {
|
||||||
source-file = site-config-file;
|
source-file = site-config-file;
|
||||||
target-file = "/var/run/webmail/rainloop/site-${site}-rainloop.cfg";
|
target-file = "/run/webmail/rainloop/site-${site}-rainloop.cfg";
|
||||||
user = cfg.user;
|
user = cfg.user;
|
||||||
};
|
};
|
||||||
|
|
||||||
"${site}-domain-config" = {
|
"${site}-domain-config" = {
|
||||||
source-file = domain-config-file;
|
source-file = domain-config-file;
|
||||||
target-file = "/var/run/webmail/rainloop/domain-${site}-rainloop.cfg";
|
target-file = "/run/webmail/rainloop/domain-${site}-rainloop.cfg";
|
||||||
user = cfg.user;
|
user = cfg.user;
|
||||||
};
|
};
|
||||||
}) cfg.sites;
|
}) cfg.sites;
|
||||||
|
@ -341,8 +347,10 @@ in {
|
||||||
webmail-init = let
|
webmail-init = let
|
||||||
link-configs = concatStringsSep "\n" (mapAttrsToList (site: site-cfg:
|
link-configs = concatStringsSep "\n" (mapAttrsToList (site: site-cfg:
|
||||||
let
|
let
|
||||||
cfg-file = config.fudo.secrets.host-secrets.${hostname}."${site}-site-config".target-file;
|
cfg-file =
|
||||||
domain-cfg-file = config.fudo.secrets.host-secrets.${hostname}."${site}-domain-config".target-file;
|
config.fudo.secrets.host-secrets.${hostname}."${site}-site-config".target-file;
|
||||||
|
domain-cfg-file =
|
||||||
|
config.fudo.secrets.host-secrets.${hostname}."${site}-domain-config".target-file;
|
||||||
in ''
|
in ''
|
||||||
${pkgs.coreutils}/bin/mkdir -p ${base-data-path}/${site}/_data_/_default_/configs
|
${pkgs.coreutils}/bin/mkdir -p ${base-data-path}/${site}/_data_/_default_/configs
|
||||||
${pkgs.coreutils}/bin/cp ${cfg-file} ${base-data-path}/${site}/_data_/_default_/configs/application.ini
|
${pkgs.coreutils}/bin/cp ${cfg-file} ${base-data-path}/${site}/_data_/_default_/configs/application.ini
|
||||||
|
|
|
@ -1,27 +1,32 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let zoneOpts = import ../types/zone-definition.nix { inherit lib; };
|
||||||
zoneOpts =
|
|
||||||
import ../types/zone-definition.nix { inherit lib; };
|
|
||||||
in {
|
in {
|
||||||
options.fudo.zones = with types; mkOption {
|
options.fudo.zones = with types;
|
||||||
type = attrsOf (submodule zoneOpts);
|
mkOption {
|
||||||
description = "A map of network zone to zone definition.";
|
type = attrsOf (submodule zoneOpts);
|
||||||
default = { };
|
description = "A map of network zone to zone definition.";
|
||||||
};
|
default = { };
|
||||||
|
};
|
||||||
|
|
||||||
config = let
|
# config = let
|
||||||
domain-name = config.instance.local-domain;
|
# domainName = config.instance.local-domain;
|
||||||
# FIXME: ipv6?
|
# zoneName = config.fudo.domains."${domainName}".zone;
|
||||||
local-networks = config.instance.local-networks;
|
# isLocal = network: hasPrefix "::1/" network || hasPrefix "127." network;
|
||||||
net-names = map (network: "ipv4:${network}")
|
# localNetworks =
|
||||||
local-networks;
|
# filter (network: !(isLocal network)) config.instance.local-networks;
|
||||||
local-net-string = concatStringsSep " " net-names;
|
# makeName = network:
|
||||||
in {
|
# if !isNull (builtins.match ":" network) then
|
||||||
fudo.zones.${domain-name}.verbatim-dns-records = [
|
# "ip6:${network}"
|
||||||
''@ IN TXT "v=spf1 mx ${local-net-string} -all"''
|
# else
|
||||||
''@ IN SPF "v=spf1 mx ${local-net-string} -all"''
|
# "ip4:${network}";
|
||||||
];
|
# netNames = map makeName localNetworks;
|
||||||
};
|
# localNetString = concatStringsSep " " netNames;
|
||||||
|
# in {
|
||||||
|
# fudo.zones."${zoneName}".verbatim-dns-records = [
|
||||||
|
# ''@ IN TXT "v=spf1 mx ${localNetString} -all"''
|
||||||
|
# ''@ IN SPF "v=spf1 mx ${localNetString} -all"''
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,47 +4,38 @@ with lib;
|
||||||
let
|
let
|
||||||
cfg = config.informis.cl-gemini;
|
cfg = config.informis.cl-gemini;
|
||||||
|
|
||||||
feedOpts = { ... }: with types; {
|
feedOpts = { ... }:
|
||||||
options = {
|
with types; {
|
||||||
url = mkOption {
|
options = {
|
||||||
type = str;
|
url = mkOption {
|
||||||
description = "Base URI of the feed, i.e. the URI corresponding to the feed path.";
|
type = str;
|
||||||
example = "gemini://my.server/path/to/feedfiles";
|
description =
|
||||||
};
|
"Base URI of the feed, i.e. the URI corresponding to the feed path.";
|
||||||
|
example = "gemini://my.server/path/to/feedfiles";
|
||||||
|
};
|
||||||
|
|
||||||
title = mkOption {
|
title = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Title of given feed.";
|
description = "Title of given feed.";
|
||||||
example = "My Fancy Feed";
|
example = "My Fancy Feed";
|
||||||
};
|
};
|
||||||
|
|
||||||
path = mkOption {
|
path = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Path to Gemini files making up the feed.";
|
description = "Path to Gemini files making up the feed.";
|
||||||
example = "/path/to/feed";
|
example = "/path/to/feed";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
ensure-certificates = hostname: user: key: cert: pkgs.writeShellScript "ensure-gemini-certificates.sh" ''
|
|
||||||
if [[ ! -e ${key} ]]; then
|
|
||||||
TARGET_CERT_DIR=$(${pkgs.coreutils}/bin/dirname ${cert})
|
|
||||||
TARGET_KEY_DIR=$(${pkgs.coreutils}/bin/dirname ${key})
|
|
||||||
if [[ ! -d $TARGET_CERT_DIR ]]; then mkdir -p $TARGET_CERT_DIR; fi
|
|
||||||
if [[ ! -d $TARGET_KEY_DIR ]]; then mkdir -p $TARGET_KEY_DIR; fi
|
|
||||||
${pkgs.openssl}/bin/openssl req -new -subj "/CN=.${hostname}" -addext "subjectAltName = DNS:${hostname}, DNS:.${hostname}" -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 3650 -nodes -out ${cert} -keyout ${key}
|
|
||||||
${pkgs.coreutils}/bin/chown -R ${user}:nogroup ${cert}
|
|
||||||
${pkgs.coreutils}/bin/chown -R ${user}:nogroup ${key}
|
|
||||||
${pkgs.coreutils}/bin/chmod 0444 ${cert}
|
|
||||||
${pkgs.coreutils}/bin/chmod 0400 ${key}
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
generate-feeds = feeds:
|
generate-feeds = feeds:
|
||||||
let
|
let
|
||||||
feed-strings = mapAttrsToList (feed-name: opts:
|
feed-strings = mapAttrsToList (feed-name: opts:
|
||||||
"(cl-gemini:register-feed :name \"${feed-name}\" :title \"${opts.title}\" :path \"${opts.path}\" :base-uri \"${opts.url}\")") feeds;
|
''
|
||||||
in pkgs.writeText "gemini-local-feeds.lisp" (concatStringsSep "\n" feed-strings);
|
(cl-gemini:register-feed :name "${feed-name}" :title "${opts.title}" :path "${opts.path}" :base-uri "${opts.url}")'')
|
||||||
|
feeds;
|
||||||
|
in pkgs.writeText "gemini-local-feeds.lisp"
|
||||||
|
(concatStringsSep "\n" feed-strings);
|
||||||
|
|
||||||
in {
|
in {
|
||||||
options.informis.cl-gemini = with types; {
|
options.informis.cl-gemini = with types; {
|
||||||
|
@ -58,7 +49,8 @@ in {
|
||||||
|
|
||||||
hostname = mkOption {
|
hostname = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Hostname at which the server is available (for generating the SSL certificate).";
|
description =
|
||||||
|
"Hostname at which the server is available (for generating the SSL certificate).";
|
||||||
example = "my.hostname.com";
|
example = "my.hostname.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,7 +100,8 @@ in {
|
||||||
|
|
||||||
feeds = mkOption {
|
feeds = mkOption {
|
||||||
type = attrsOf (submodule feedOpts);
|
type = attrsOf (submodule feedOpts);
|
||||||
description = "Feeds to generate and make available (as eg. /feed/name.xml).";
|
description =
|
||||||
|
"Feeds to generate and make available (as eg. /feed/name.xml).";
|
||||||
example = {
|
example = {
|
||||||
diary = {
|
diary = {
|
||||||
title = "My Diary";
|
title = "My Diary";
|
||||||
|
@ -116,7 +109,7 @@ in {
|
||||||
url = "gemini://my.host/blog-path/";
|
url = "gemini://my.host/blog-path/";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
textfiles-archive = mkOption {
|
textfiles-archive = mkOption {
|
||||||
|
@ -141,18 +134,22 @@ in {
|
||||||
|
|
||||||
systemd.services = {
|
systemd.services = {
|
||||||
cl-gemini = {
|
cl-gemini = {
|
||||||
description = "cl-gemini Gemini server (https://gemini.circumlunar.space/)";
|
description =
|
||||||
|
"cl-gemini Gemini server (https://gemini.circumlunar.space/)";
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStartPre = "${ensure-certificates cfg.hostname cfg.user cfg.ssl-private-key cfg.ssl-certificate}";
|
ExecStartPre =
|
||||||
ExecStart = "${pkgs.cl-gemini}/bin/launch-server.sh";
|
"${ensure-certificates cfg.hostname cfg.user cfg.ssl-private-key
|
||||||
|
cfg.ssl-certificate}";
|
||||||
|
ExecStart = "${pkgs.cl-gemini}/bin/cl-gemini-launcher";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
PIDFile = "/run/cl-gemini.$USERNAME.uid";
|
PIDFile = "/run/cl-gemini.$USERNAME.uid";
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
};
|
};
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
GEMINI_SLYNK_PORT = mkIf (cfg.slynk-port != null) (toString cfg.slynk-port);
|
GEMINI_SLYNK_PORT =
|
||||||
|
mkIf (cfg.slynk-port != null) (toString cfg.slynk-port);
|
||||||
GEMINI_LISTEN_IP = cfg.server-ip;
|
GEMINI_LISTEN_IP = cfg.server-ip;
|
||||||
GEMINI_PRIVATE_KEY = cfg.ssl-private-key;
|
GEMINI_PRIVATE_KEY = cfg.ssl-private-key;
|
||||||
GEMINI_CERTIFICATE = cfg.ssl-certificate;
|
GEMINI_CERTIFICATE = cfg.ssl-certificate;
|
||||||
|
@ -161,14 +158,11 @@ 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; [ gcc file getent ];
|
||||||
gcc
|
|
||||||
file
|
|
||||||
getent
|
|
||||||
];
|
|
||||||
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
};
|
};
|
||||||
|
|
182
lib/lib/dns.nix
182
lib/lib/dns.nix
|
@ -7,16 +7,17 @@ let
|
||||||
pthru = obj: builtins.trace obj obj;
|
pthru = obj: builtins.trace obj obj;
|
||||||
|
|
||||||
remove-blank-lines = str:
|
remove-blank-lines = str:
|
||||||
concatStringsSep "\n\n"
|
concatStringsSep "\n\n" (filter builtins.isString (builtins.split ''
|
||||||
(filter builtins.isString
|
|
||||||
(builtins.split "\n\n\n+" str));
|
|
||||||
|
|
||||||
n-spaces = n:
|
|
||||||
concatStringsSep "" (builtins.genList (_: " ") n);
|
|
||||||
|
|
||||||
pad-to-length = strlen: str: let
|
|
||||||
spaces = n-spaces (strlen - (stringLength str));
|
+'' str));
|
||||||
in str + spaces;
|
|
||||||
|
n-spaces = n: concatStringsSep "" (builtins.genList (_: " ") n);
|
||||||
|
|
||||||
|
pad-to-length = strlen: str:
|
||||||
|
let spaces = n-spaces (strlen - (stringLength str));
|
||||||
|
in str + spaces;
|
||||||
|
|
||||||
record-matcher = builtins.match "^([^;].*) IN ([A-Z][A-Z0-9]*) (.+)$";
|
record-matcher = builtins.match "^([^;].*) IN ([A-Z][A-Z0-9]*) (.+)$";
|
||||||
|
|
||||||
|
@ -24,32 +25,35 @@ let
|
||||||
|
|
||||||
max-int = foldr (a: b: if (a < b) then b else a) 0;
|
max-int = foldr (a: b: if (a < b) then b else a) 0;
|
||||||
|
|
||||||
make-zone-formatter = zonedata: let
|
make-zone-formatter = zonedata:
|
||||||
lines = splitString "\n" zonedata;
|
let
|
||||||
records = filter is-record lines;
|
lines = splitString "\n" zonedata;
|
||||||
split-records = map record-matcher records;
|
records = filter is-record lines;
|
||||||
index-strlen = i: record: stringLength (elemAt record i);
|
split-records = map record-matcher records;
|
||||||
record-index-maxlen = i: max-int (map (index-strlen i) split-records);
|
index-strlen = i: record: stringLength (elemAt record i);
|
||||||
in record-formatter (record-index-maxlen 0) (record-index-maxlen 1);
|
record-index-maxlen = i: max-int (map (index-strlen i) split-records);
|
||||||
|
in record-formatter (record-index-maxlen 0) (record-index-maxlen 1);
|
||||||
|
|
||||||
record-formatter = name-max: type-max: let
|
record-formatter = name-max: type-max:
|
||||||
name-padder = pad-to-length name-max;
|
let
|
||||||
type-padder = pad-to-length type-max;
|
name-padder = pad-to-length name-max;
|
||||||
in record-line: let
|
type-padder = pad-to-length type-max;
|
||||||
record-parts = record-matcher record-line;
|
in record-line:
|
||||||
in
|
let record-parts = record-matcher record-line;
|
||||||
if (record-parts == null) then
|
in if (record-parts == null) then
|
||||||
record-line
|
record-line
|
||||||
else (let
|
else
|
||||||
name = elemAt record-parts 0;
|
(let
|
||||||
type = elemAt record-parts 1;
|
name = elemAt record-parts 0;
|
||||||
data = elemAt record-parts 2;
|
type = elemAt record-parts 1;
|
||||||
in "${name-padder name} IN ${type-padder type} ${data}");
|
data = elemAt record-parts 2;
|
||||||
|
in "${name-padder name} IN ${type-padder type} ${data}");
|
||||||
|
|
||||||
format-zone = zonedata: let
|
format-zone = zonedata:
|
||||||
formatter = make-zone-formatter zonedata;
|
let
|
||||||
lines = splitString "\n" zonedata;
|
formatter = make-zone-formatter zonedata;
|
||||||
in concatStringsSep "\n" (map formatter lines);
|
lines = splitString "\n" zonedata;
|
||||||
|
in concatStringsSep "\n" (map formatter lines);
|
||||||
|
|
||||||
makeSrvRecords = protocol: service: records:
|
makeSrvRecords = protocol: service: records:
|
||||||
join-lines (map (record:
|
join-lines (map (record:
|
||||||
|
@ -61,13 +65,10 @@ let
|
||||||
join-lines (mapAttrsToList (makeSrvRecords protocol) services);
|
join-lines (mapAttrsToList (makeSrvRecords protocol) services);
|
||||||
|
|
||||||
makeMetricRecords = metric-type: records:
|
makeMetricRecords = metric-type: records:
|
||||||
join-lines
|
join-lines (map (record:
|
||||||
(map (record:
|
"${metric-type}._metrics._tcp IN SRV ${toString record.priority} ${
|
||||||
"${metric-type}._metrics._tcp IN SRV ${
|
toString record.weight
|
||||||
toString record.priority
|
} ${toString record.port} ${record.host}.") records);
|
||||||
} ${
|
|
||||||
toString record.weight
|
|
||||||
} ${toString record.port} ${record.host}.") records);
|
|
||||||
|
|
||||||
srvRecordOpts = with types; {
|
srvRecordOpts = with types; {
|
||||||
options = {
|
options = {
|
||||||
|
@ -96,33 +97,31 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
hostRecords = hostname: nethost-data: let
|
hostRecords = hostname: nethost-data:
|
||||||
sshfp-records = map (sshfp: "${hostname} IN SSHFP ${sshfp}")
|
let
|
||||||
nethost-data.sshfp-records;
|
sshfp-records =
|
||||||
a-record = optional (nethost-data.ipv4-address != null)
|
map (sshfp: "${hostname} IN SSHFP ${sshfp}") nethost-data.sshfp-records;
|
||||||
"${hostname} IN A ${nethost-data.ipv4-address}";
|
a-record = optional (nethost-data.ipv4-address != null)
|
||||||
aaaa-record = optional (nethost-data.ipv6-address != null)
|
"${hostname} IN A ${nethost-data.ipv4-address}";
|
||||||
"${hostname} IN AAAA ${nethost-data.ipv6-address}";
|
aaaa-record = optional (nethost-data.ipv6-address != null)
|
||||||
description-record = optional (nethost-data.description != null)
|
"${hostname} IN AAAA ${nethost-data.ipv6-address}";
|
||||||
''${hostname} IN TXT "${nethost-data.description}"'';
|
description-record = optional (nethost-data.description != null)
|
||||||
in join-lines (a-record ++
|
''${hostname} IN TXT "${nethost-data.description}"'';
|
||||||
aaaa-record ++
|
in join-lines
|
||||||
sshfp-records ++
|
(a-record ++ aaaa-record ++ sshfp-records ++ description-record);
|
||||||
description-record);
|
|
||||||
|
|
||||||
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
|
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
|
||||||
|
|
||||||
dmarcRecord = dmarc-email:
|
dmarcRecord = dmarc-email:
|
||||||
optionalString (dmarc-email != null)
|
optionalString (dmarc-email != null) ''
|
||||||
''_dmarc IN TXT "v=DMARC1;p=quarantine;sp=quarantine;rua=mailto:${dmarc-email};"'';
|
_dmarc IN TXT "v=DMARC1;p=quarantine;sp=quarantine;rua=mailto:${dmarc-email};"'';
|
||||||
|
|
||||||
mxRecords = mxs: map (mx: "@ IN MX 10 ${mx}.") mxs;
|
mxRecords = mxs: map (mx: "@ IN MX 10 ${mx}.") mxs;
|
||||||
|
|
||||||
nsRecords = map (ns-host: "@ IN NS ${ns-host}");
|
nsRecords = map (ns-host: "@ IN NS ${ns-host}");
|
||||||
|
|
||||||
flatmapAttrsToList = f: attrs:
|
flatmapAttrsToList = f: attrs:
|
||||||
foldr (a: b: a ++ b) [] (mapAttrsToList f attrs);
|
foldr (a: b: a ++ b) [ ] (mapAttrsToList f attrs);
|
||||||
|
|
||||||
|
|
||||||
srvRecordPair = domain: protocol: service: record: {
|
srvRecordPair = domain: protocol: service: record: {
|
||||||
"_${service}._${protocol}.${domain}" =
|
"_${service}._${protocol}.${domain}" =
|
||||||
|
@ -131,38 +130,44 @@ let
|
||||||
} ${record.host}.";
|
} ${record.host}.";
|
||||||
};
|
};
|
||||||
|
|
||||||
domain-records = dom: zone: ''
|
domain-records = dom: zone:
|
||||||
$ORIGIN ${dom}.
|
let
|
||||||
$TTL ${zone.default-ttl}
|
defaultHostRecords = optionalString (zone.default-host != null)
|
||||||
|
(hostRecords "@" zone.default-host);
|
||||||
|
in ''
|
||||||
|
$ORIGIN ${dom}.
|
||||||
|
$TTL ${zone.default-ttl}
|
||||||
|
|
||||||
${optionalString (zone.default-host != null)
|
${defaultHostRecords}
|
||||||
"@ IN A ${zone.default-host}"}
|
|
||||||
|
|
||||||
${join-lines (mxRecords zone.mx)}
|
${join-lines (mxRecords zone.mx)}
|
||||||
|
|
||||||
${dmarcRecord zone.dmarc-report-address}
|
${dmarcRecord zone.dmarc-report-address}
|
||||||
|
|
||||||
${optionalString (zone.gssapi-realm != null)
|
${optionalString (zone.gssapi-realm != null)
|
||||||
''_kerberos IN TXT "${zone.gssapi-realm}"''}
|
''_kerberos IN TXT "${zone.gssapi-realm}"''}
|
||||||
|
|
||||||
${join-lines (nsRecords zone.nameservers)}
|
${join-lines (nsRecords zone.nameservers)}
|
||||||
|
|
||||||
${join-lines (mapAttrsToList makeSrvProtocolRecords zone.srv-records)}
|
${join-lines (mapAttrsToList makeSrvProtocolRecords zone.srv-records)}
|
||||||
|
|
||||||
${join-lines (mapAttrsToList makeMetricRecords zone.metric-records)}
|
${join-lines (mapAttrsToList makeMetricRecords zone.metric-records)}
|
||||||
|
|
||||||
$TTL ${zone.host-record-ttl}
|
$TTL ${zone.host-record-ttl}
|
||||||
|
|
||||||
${join-lines (mapAttrsToList hostRecords zone.hosts)}
|
${join-lines (mapAttrsToList hostRecords 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}
|
||||||
|
|
||||||
${join-lines (mapAttrsToList
|
${join-lines (mapAttrsToList
|
||||||
(subdom: subdomCfg: domain-records "${subdom}.${dom}" subdomCfg)
|
(subdom: subdomCfg: domain-records "${subdom}.${dom}" subdomCfg)
|
||||||
zone.subdomains)}
|
zone.subdomains)}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
concatMapAttrs = f: attrs:
|
||||||
|
concatMap (x: x) (mapAttrsToList (key: val: f key val) attrs);
|
||||||
|
|
||||||
in rec {
|
in rec {
|
||||||
|
|
||||||
|
@ -171,27 +176,24 @@ in rec {
|
||||||
srvRecordsToBindZone = srvRecords:
|
srvRecordsToBindZone = srvRecords:
|
||||||
join-lines (mapAttrsToList makeSrvProtocolRecords srvRecords);
|
join-lines (mapAttrsToList makeSrvProtocolRecords srvRecords);
|
||||||
|
|
||||||
concatMapAttrs = f: attrs:
|
srvRecordsToPairs = domain: srvRecords:
|
||||||
concatMap (x: x) (mapAttrsToList (key: val: f key val) attrs);
|
|
||||||
|
|
||||||
srvRecordsToPairs = domain: srvRecords:
|
|
||||||
listToAttrs (concatMapAttrs (protocol: services:
|
listToAttrs (concatMapAttrs (protocol: services:
|
||||||
concatMapAttrs
|
concatMapAttrs
|
||||||
(service: records: map (srvRecordPair domain protocol service) records) services)
|
(service: records: map (srvRecordPair domain protocol service) records)
|
||||||
srvRecords);
|
services) srvRecords);
|
||||||
|
|
||||||
zoneToZonefile = timestamp: dom: zone:
|
zoneToZonefile = timestamp: dom: zone:
|
||||||
remove-blank-lines (format-zone ''
|
remove-blank-lines (format-zone ''
|
||||||
$ORIGIN ${dom}.
|
$ORIGIN ${dom}.
|
||||||
$TTL ${zone.default-ttl}
|
$TTL ${zone.default-ttl}
|
||||||
|
|
||||||
@ IN SOA ns1.${dom}. hostmaster.${dom}. (
|
@ IN SOA ns1.${dom}. hostmaster.${dom}. (
|
||||||
${toString timestamp}
|
${toString timestamp}
|
||||||
30m
|
30m
|
||||||
2m
|
2m
|
||||||
3w
|
3w
|
||||||
5m)
|
5m)
|
||||||
|
|
||||||
${domain-records dom zone}
|
${domain-records dom zone}
|
||||||
'');
|
'');
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,8 +39,10 @@ in rec {
|
||||||
in foldr (a: b: a + b) 0 (imap0 (i: el: (leftShift el (i * 8))) els);
|
in foldr (a: b: a + b) 0 (imap0 (i: el: (leftShift el (i * 8))) els);
|
||||||
|
|
||||||
intToIpv4 = int:
|
intToIpv4 = int:
|
||||||
concatStringsSep "."
|
let
|
||||||
(map (i: toString (bitAnd (rightShift int (i * 8)) 255)) [ 3 2 1 0 ]);
|
toComponent = i: toString (bitAnd (rightShift int (i * 8)) 255);
|
||||||
|
components = map toComponent [ 3 2 1 0 ];
|
||||||
|
in concatStringsSep "." components;
|
||||||
|
|
||||||
maskFromV32Network = network:
|
maskFromV32Network = network:
|
||||||
let
|
let
|
||||||
|
@ -49,7 +51,7 @@ in rec {
|
||||||
in intToIpv4
|
in intToIpv4
|
||||||
(leftShift (rightShift fullMask insignificantBits) insignificantBits);
|
(leftShift (rightShift fullMask insignificantBits) insignificantBits);
|
||||||
|
|
||||||
networkMinIp = network: intToIpv4 (1 + (ipv4ToInt (getNetworkBase network)));
|
networkMinIp = network: intToIpv4 (ipv4ToInt (getNetworkBase network));
|
||||||
|
|
||||||
networkMaxIp = network:
|
networkMaxIp = network:
|
||||||
intToIpv4 (rightPadBits (ipv4ToInt (getNetworkBase network))
|
intToIpv4 (rightPadBits (ipv4ToInt (getNetworkBase network))
|
||||||
|
@ -57,15 +59,14 @@ in rec {
|
||||||
|
|
||||||
# To avoid broadcast IP...
|
# To avoid broadcast IP...
|
||||||
networkMaxButOneIp = network:
|
networkMaxButOneIp = network:
|
||||||
intToIpv4 ((rightPadBits (ipv4ToInt (getNetworkBase network))
|
intToIpv4 ((ipv4ToInt (networkMaxIp network)) - 1);
|
||||||
(32 - (getNetworkMask network))) - 1);
|
|
||||||
|
|
||||||
ipv4OnNetwork = ip: network:
|
ipv4OnNetwork = ip: network:
|
||||||
let
|
let
|
||||||
ip-int = ipv4ToInt ip;
|
ip-int = ipv4ToInt ip;
|
||||||
net-min = networkMinIp network;
|
netMin = networkMinIp network;
|
||||||
net-max = networkMaxIp network;
|
netMax = networkMaxIp network;
|
||||||
in (ip-int >= networkMinIp) && (ip-int <= networkMaxIp);
|
in (ip-int >= (ipv4ToInt netMin)) && (ip-int <= (ipv4ToInt netMax));
|
||||||
|
|
||||||
getNetworkMask = network: toInt (elemAt (splitString "/" network) 1);
|
getNetworkMask = network: toInt (elemAt (splitString "/" network) 1);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
|
|
||||||
with pkgs.lib;
|
with pkgs.lib; rec {
|
||||||
rec {
|
gather-dependencies = pkg:
|
||||||
gather-dependencies = pkg: unique (pkg.propagatedBuildInputs ++ (concatMap gather-dependencies pkg.propagatedBuildInputs));
|
unique (pkg.propagatedBuildInputs
|
||||||
|
++ (concatMap gather-dependencies pkg.propagatedBuildInputs));
|
||||||
lisp-source-registry = pkg: concatStringsSep ":" (map (p: "${p}//") (gather-dependencies pkg));
|
|
||||||
|
lisp-source-registry = pkg:
|
||||||
|
concatStringsSep ":" (map (p: "${p}//") (gather-dependencies pkg));
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,54 +2,55 @@
|
||||||
|
|
||||||
with pkgs.lib;
|
with pkgs.lib;
|
||||||
let
|
let
|
||||||
hash-ldap-passwd-pkg = name: passwd-file: pkgs.stdenv.mkDerivation {
|
hash-ldap-passwd-pkg = name: passwd-file:
|
||||||
name = "${name}-ldap-passwd";
|
pkgs.stdenv.mkDerivation {
|
||||||
|
name = "${name}-ldap-passwd";
|
||||||
|
|
||||||
phases = [ "installPhase" ];
|
phases = [ "installPhase" ];
|
||||||
|
|
||||||
buildInputs = with pkgs; [ openldap ];
|
buildInputs = with pkgs; [ openldap ];
|
||||||
|
|
||||||
installPhase = let
|
installPhase = let passwd = removeSuffix "\n" (readFile passwd-file);
|
||||||
passwd = removeSuffix "\n" (readFile passwd-file);
|
in ''
|
||||||
in ''
|
slappasswd -s ${passwd} | tr -d '\n' > $out
|
||||||
slappasswd -s ${passwd} | tr -d '\n' > $out
|
'';
|
||||||
'';
|
};
|
||||||
};
|
|
||||||
|
|
||||||
hash-ldap-passwd = name: passwd-file:
|
hash-ldap-passwd = name: passwd-file:
|
||||||
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:
|
||||||
name = "${name}-random-passwd";
|
pkgs.stdenv.mkDerivation {
|
||||||
|
name = "${name}-random-passwd";
|
||||||
|
|
||||||
phases = [ "installPhase" ];
|
phases = [ "installPhase" ];
|
||||||
|
|
||||||
buildInputs = with pkgs; [ pwgen ];
|
buildInputs = with pkgs; [ pwgen ];
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
pwgen --secure --num-passwords=1 ${toString length} | tr -d '\n' > $out
|
pwgen --secure --num-passwords=1 ${toString length} | tr -d '\n' > $out
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
bcrypt-passwd-pkg = name: passwd-file: pkgs.stdenv.mkDerivation {
|
bcrypt-passwd-pkg = name: passwd-file:
|
||||||
name = "${name}-bcrypt";
|
pkgs.stdenv.mkDerivation {
|
||||||
|
name = "${name}-bcrypt";
|
||||||
|
|
||||||
phases = [ "installPhase" ];
|
phases = [ "installPhase" ];
|
||||||
|
|
||||||
buildInputs = with pkgs; [ apacheHttpd ];
|
buildInputs = with pkgs; [ apacheHttpd ];
|
||||||
|
|
||||||
installPhase = let
|
installPhase = let passwd = removeSuffix "\n" (readFile passwd-file);
|
||||||
passwd = removeSuffix "\n" (readFile passwd-file);
|
in ''
|
||||||
in ''
|
htpasswd -bnBC 10 "" ${passwd} | tr -d ':\n' | sed 's/$2y/$2a/' > $out
|
||||||
htpasswd -bnBC 10 "" ${passwd} | tr -d ':\n' | sed 's/$2y/$2a/' > $out
|
'';
|
||||||
'';
|
};
|
||||||
};
|
|
||||||
|
|
||||||
bcrypt-passwd = name: passwd-file:
|
bcrypt-passwd = name: passwd-file:
|
||||||
readFile "${bcrypt-passwd-pkg name passwd-file}";
|
readFile "${bcrypt-passwd-pkg name passwd-file}";
|
||||||
|
|
||||||
|
generate-stablerandom-passwd = name:
|
||||||
generate-stablerandom-passwd = name: { seed, length ? 20, ... }:
|
{ seed, length ? 20, ... }:
|
||||||
pkgs.stdenv.mkDerivation {
|
pkgs.stdenv.mkDerivation {
|
||||||
name = "${name}-stablerandom-passwd";
|
name = "${name}-stablerandom-passwd";
|
||||||
|
|
||||||
|
@ -59,7 +60,9 @@ let
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
echo "${name}-${seed}" > seedfile
|
echo "${name}-${seed}" > seedfile
|
||||||
pwgen --secure --num-passwords=1 -H seedfile ${toString length} | tr -d '\n' > $out
|
pwgen --secure --num-passwords=1 -H seedfile ${
|
||||||
|
toString length
|
||||||
|
} | tr -d '\n' > $out
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,12 @@ let
|
||||||
get-basename-without-hash = filename:
|
get-basename-without-hash = filename:
|
||||||
head (builtins.match "^[a-zA-Z0-9]+-(.+)$" (baseNameOf filename));
|
head (builtins.match "^[a-zA-Z0-9]+-(.+)$" (baseNameOf filename));
|
||||||
|
|
||||||
format-json-file = filename: pkgs.stdenv.mkDerivation {
|
format-json-file = filename:
|
||||||
name = "formatted-${get-basename-without-hash filename}";
|
pkgs.stdenv.mkDerivation {
|
||||||
phases = [ "installPhase" ];
|
name = "formatted-${get-basename-without-hash filename}";
|
||||||
buildInputs = with pkgs; [ python ];
|
phases = [ "installPhase" ];
|
||||||
installPhase = "python -mjson.tool ${filename} > $out";
|
buildInputs = with pkgs; [ python3 ];
|
||||||
};
|
installPhase = "python -mjson.tool ${filename} > $out";
|
||||||
|
};
|
||||||
|
|
||||||
in {
|
in { inherit format-json-file; }
|
||||||
inherit format-json-file;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,20 +4,21 @@ with lib;
|
||||||
let passwd = import ../passwd.nix { inherit lib; };
|
let passwd = import ../passwd.nix { inherit lib; };
|
||||||
|
|
||||||
in rec {
|
in rec {
|
||||||
encryptedFSOpts = { ... }:
|
encryptedFSOpts = { name, ... }:
|
||||||
let
|
let
|
||||||
mountpoint = { mp, ... }: {
|
mountpoint = { name, ... }: {
|
||||||
options = with types; {
|
options = with types; {
|
||||||
mountpoint = mkOption {
|
mountpoint = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Path at which to mount the filesystem.";
|
description = "Path at which to mount the filesystem.";
|
||||||
default = mp;
|
default = name;
|
||||||
};
|
};
|
||||||
|
|
||||||
options = mkOption {
|
options = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description =
|
description =
|
||||||
"List of filesystem options specific to this mountpoint (eg: subvol).";
|
"List of filesystem options specific to this mountpoint (eg: subvol).";
|
||||||
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
group = mkOption {
|
group = mkOption {
|
||||||
|
@ -59,6 +60,18 @@ in rec {
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type = mkOption {
|
||||||
|
type = enum [ "luks" "luks2" ];
|
||||||
|
description = "Type of the LUKS encryption.";
|
||||||
|
default = "luks";
|
||||||
|
};
|
||||||
|
|
||||||
|
remove-key = mkOption {
|
||||||
|
type = bool;
|
||||||
|
description = "Remove key once the filesystem is decrypted.";
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
|
||||||
filesystem-type = mkOption {
|
filesystem-type = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Filesystem type of the decrypted filesystem.";
|
description = "Filesystem type of the decrypted filesystem.";
|
||||||
|
@ -67,6 +80,7 @@ in rec {
|
||||||
options = mkOption {
|
options = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of filesystem options with which to mount.";
|
description = "List of filesystem options with which to mount.";
|
||||||
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
mountpoints = mkOption {
|
mountpoints = mkOption {
|
||||||
|
@ -243,6 +257,13 @@ in rec {
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fudo-system = mkOption {
|
||||||
|
type = bool;
|
||||||
|
description =
|
||||||
|
"Whether the host is a Fudo host (i.e. needs generated keys) without being a NixOS system.";
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
|
||||||
arch = mkOption {
|
arch = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "System architecture of the system.";
|
description = "System architecture of the system.";
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib; rec {
|
||||||
rec {
|
|
||||||
systemUserOpts = { name, ... }: {
|
systemUserOpts = { name, ... }: {
|
||||||
options = with lib.types; {
|
options = with lib.types; {
|
||||||
username = mkOption {
|
username = mkOption {
|
||||||
|
@ -23,93 +22,105 @@ rec {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
userOpts = { name, ... }: let
|
userOpts = { name, ... }:
|
||||||
username = name;
|
let username = name;
|
||||||
in {
|
in {
|
||||||
options = with lib.types; {
|
options = with lib.types; {
|
||||||
username = mkOption {
|
username = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "The user's login name.";
|
description = "The user's login name.";
|
||||||
default = username;
|
default = username;
|
||||||
};
|
};
|
||||||
|
|
||||||
uid = mkOption {
|
uid = mkOption {
|
||||||
type = int;
|
type = int;
|
||||||
description = "Unique UID number for the user.";
|
description = "Unique UID number for the user.";
|
||||||
};
|
};
|
||||||
|
|
||||||
common-name = mkOption {
|
common-name = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "The user's common or given name.";
|
description = "The user's common or given name.";
|
||||||
};
|
};
|
||||||
|
|
||||||
primary-group = mkOption {
|
primary-group = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Primary group to which the user belongs.";
|
description = "Primary group to which the user belongs.";
|
||||||
};
|
};
|
||||||
|
|
||||||
login-shell = mkOption {
|
login-shell = mkOption {
|
||||||
type = nullOr shellPackage;
|
type = nullOr shellPackage;
|
||||||
description = "The user's preferred shell.";
|
description = "The user's preferred shell.";
|
||||||
};
|
};
|
||||||
|
|
||||||
description = mkOption {
|
description = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
default = "Fudo Member";
|
default = "Fudo Member";
|
||||||
description = "A description of this user's role.";
|
description = "A description of this user's role.";
|
||||||
};
|
};
|
||||||
|
|
||||||
ldap-hashed-passwd = mkOption {
|
ldap-hashed-passwd = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description =
|
description =
|
||||||
"LDAP-formatted hashed password, used for email and other services. Use slappasswd to generate the properly-formatted password.";
|
"LDAP-formatted hashed password, used for email and other services. Use slappasswd to generate the properly-formatted password.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
login-hashed-passwd = mkOption {
|
login-hashed-passwd = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description =
|
description =
|
||||||
"Hashed password for shell, used for shell access to hosts. Use mkpasswd to generate the properly-formatted password.";
|
"Hashed password for shell, used for shell access to hosts. Use mkpasswd to generate the properly-formatted password.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
ssh-authorized-keys = mkOption {
|
ssh-authorized-keys = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "SSH public keys this user can use to log in.";
|
description = "SSH public keys this user can use to log in.";
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
home-directory = mkOption {
|
home-directory = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = "Default home directory for the given user.";
|
description = "Default home directory for the given user.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
k5login = mkOption {
|
k5login = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of Kerberos principals that map to this user.";
|
description = "List of Kerberos principals that map to this user.";
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
ssh-keys = mkOption {
|
ssh-keys = mkOption {
|
||||||
type = listOf (submodule sshKeyOpts);
|
type = listOf (submodule sshKeyOpts);
|
||||||
description = "Path to the user's public and private key files.";
|
description = "Path to the user's public and private key files.";
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
email = mkOption {
|
email = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = "User's primary email address.";
|
description = "User's primary email address.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
email-aliases = mkOption {
|
email-aliases = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "Email aliases that should map to this user.";
|
description = "Email aliases that should map to this user.";
|
||||||
default = [];
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
given-name = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
description = "User's given name.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
surname = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
description = "User's surname.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
groupOpts = { name, ... }: {
|
groupOpts = { name, ... }: {
|
||||||
options = with lib.types; {
|
options = with lib.types; {
|
||||||
|
|
|
@ -50,11 +50,8 @@ let
|
||||||
nameservers = mkOption {
|
nameservers = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
description = "List of zone nameservers.";
|
description = "List of zone nameservers.";
|
||||||
example = [
|
example = [ "ns1.fudo.org." "10.0.0.1" ];
|
||||||
"ns1.fudo.org."
|
default = [ ];
|
||||||
"10.0.0.1"
|
|
||||||
];
|
|
||||||
default = [];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
srv-records = mkOption {
|
srv-records = mkOption {
|
||||||
|
@ -91,12 +88,10 @@ let
|
||||||
port = 443;
|
port = 443;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
rspamd = [
|
rspamd = [{
|
||||||
{
|
host = "mail-host.my-domain.com";
|
||||||
host = "mail-host.my-domain.com";
|
port = 443;
|
||||||
port = 443;
|
}];
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
|
@ -127,7 +122,7 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
default-host = mkOption {
|
default-host = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr (submodule networkHostOpts);
|
||||||
description =
|
description =
|
||||||
"IP of the host which will act as the default server for this domain, if any.";
|
"IP of the host which will act as the default server for this domain, if any.";
|
||||||
default = null;
|
default = null;
|
||||||
|
@ -165,7 +160,7 @@ let
|
||||||
subdomains = mkOption {
|
subdomains = mkOption {
|
||||||
type = attrsOf (submodule zoneOpts);
|
type = attrsOf (submodule zoneOpts);
|
||||||
description = "Subdomains of the current zone.";
|
description = "Subdomains of the current zone.";
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue