2023-12-11 08:30:00 +00:00
|
|
|
# The DNS server for my domains. Take note it uses a hidden master setup with
|
|
|
|
# the secondary nameservers of the service (as of 2023-10-05, we're using
|
|
|
|
# Hetzner's secondary nameservers).
|
2024-02-11 07:16:25 +00:00
|
|
|
{ config, lib, pkgs, foodogsquaredLib, ... }:
|
2023-12-11 08:30:00 +00:00
|
|
|
|
|
|
|
let
|
|
|
|
hostCfg = config.hosts.plover;
|
|
|
|
cfg = hostCfg.services.dns-server;
|
|
|
|
|
|
|
|
inherit (config.networking) domain fqdn;
|
2024-09-20 04:33:26 +00:00
|
|
|
|
|
|
|
zonesDir = "/etc/bind/zones";
|
|
|
|
getZoneFile = domain: "${zonesDir}/${domain}.zone";
|
|
|
|
|
|
|
|
zonefile = pkgs.substituteAll {
|
|
|
|
src = ../setups/dns/zones/${domain}.zone;
|
|
|
|
ploverWANIPv4 = config.state.network.ipv4;
|
|
|
|
ploverWANIPv6 = config.state.network.ipv6;
|
2023-12-11 08:30:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
fqdnZone = pkgs.substituteAll {
|
2024-09-20 04:33:26 +00:00
|
|
|
src = ../setups/dns/zones/${fqdn}.zone;
|
|
|
|
ploverWANIPv4 = config.state.network.ipv4;
|
|
|
|
ploverWANIPv6 = config.state.network.ipv6;
|
2023-12-11 08:30:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
dnsSubdomain = "ns1.${domain}";
|
|
|
|
in
|
|
|
|
{
|
2023-12-15 05:27:12 +00:00
|
|
|
options.hosts.plover.services.dns-server.enable =
|
|
|
|
lib.mkEnableOption "preferred DNS server";
|
2023-12-11 08:30:00 +00:00
|
|
|
|
|
|
|
config = lib.mkIf cfg.enable (lib.mkMerge [
|
|
|
|
{
|
2024-09-20 04:33:26 +00:00
|
|
|
state.ports = {
|
|
|
|
bindStatistics.value = 9423;
|
|
|
|
dns.value = 53;
|
|
|
|
dnsOverHTTPS.value = 8443;
|
|
|
|
dnsOverTLS.value = 853;
|
|
|
|
};
|
|
|
|
|
2023-12-11 08:30:00 +00:00
|
|
|
sops.secrets =
|
|
|
|
let
|
|
|
|
dnsFileAttribute = {
|
|
|
|
owner = config.users.users.named.name;
|
|
|
|
group = config.users.users.named.group;
|
|
|
|
mode = "0400";
|
|
|
|
};
|
|
|
|
in
|
2024-09-20 04:33:26 +00:00
|
|
|
foodogsquaredLib.sops-nix.getSecrets ./secrets.yaml {
|
2023-12-11 08:30:00 +00:00
|
|
|
"dns/${domain}/rfc2136-key" = dnsFileAttribute // {
|
|
|
|
reloadUnits = [ "bind.service" ];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
# Install the utilities.
|
|
|
|
environment.systemPackages = [ config.services.bind.package ];
|
|
|
|
|
|
|
|
services.bind = {
|
|
|
|
enable = true;
|
|
|
|
forward = "first";
|
|
|
|
|
|
|
|
cacheNetworks = [
|
|
|
|
"127.0.0.1"
|
|
|
|
"::1"
|
|
|
|
];
|
|
|
|
|
|
|
|
listenOn = [
|
|
|
|
"127.0.0.1"
|
2024-09-20 04:33:26 +00:00
|
|
|
config.state.network.ipv4
|
2023-12-11 08:30:00 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
listenOnIpv6 = [
|
|
|
|
"::1"
|
2024-09-20 04:33:26 +00:00
|
|
|
config.state.network.ipv6
|
2023-12-11 08:30:00 +00:00
|
|
|
];
|
|
|
|
|
2024-09-20 04:33:26 +00:00
|
|
|
extraConfig = ''
|
|
|
|
include "${config.state.paths.dataDir}/dns/*-dnskeys.conf";
|
|
|
|
'';
|
|
|
|
|
2023-12-11 08:30:00 +00:00
|
|
|
# Welp, since the template is pretty limited, we'll have to go with our
|
|
|
|
# own. This is partially based from the NixOS Bind module except without
|
|
|
|
# the template for filling in zones since we use views.
|
|
|
|
configFile =
|
|
|
|
let
|
|
|
|
cfg = config.services.bind;
|
|
|
|
certDir = path: "/run/credentials/bind.service/${path}";
|
|
|
|
listenInterfaces = lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOn;
|
|
|
|
listenInterfacesIpv6 = lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6;
|
|
|
|
in
|
|
|
|
pkgs.writeText "named.conf" ''
|
|
|
|
include "/etc/bind/rndc.key";
|
|
|
|
include "${config.sops.secrets."dns/${domain}/rfc2136-key".path}";
|
|
|
|
|
|
|
|
controls {
|
|
|
|
inet 127.0.0.1 allow {localhost;} keys {"rndc-key";};
|
|
|
|
};
|
|
|
|
|
|
|
|
tls ${dnsSubdomain} {
|
|
|
|
key-file "${certDir "key.pem"}";
|
|
|
|
cert-file "${certDir "cert.pem"}";
|
|
|
|
dhparam-file "${config.security.dhparams.params.bind.path}";
|
|
|
|
ciphers "HIGH:!kRSA:!aNULL:!eNULL:!RC4:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!SHA1:!SHA256:!SHA384";
|
|
|
|
prefer-server-ciphers yes;
|
|
|
|
session-tickets no;
|
|
|
|
};
|
|
|
|
|
|
|
|
http ${dnsSubdomain} {
|
|
|
|
endpoints { "/dns-query"; };
|
|
|
|
};
|
|
|
|
|
2024-09-20 04:33:26 +00:00
|
|
|
acl trusted { ${lib.concatStringsSep "; " [ "10.0.0.0/8" ]}; localhost; };
|
2023-12-11 08:30:00 +00:00
|
|
|
acl cachenetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} };
|
|
|
|
acl badnetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} };
|
|
|
|
|
|
|
|
options {
|
|
|
|
# Native DNS.
|
|
|
|
listen-on { ${listenInterfaces} };
|
|
|
|
listen-on-v6 { ${listenInterfacesIpv6} };
|
|
|
|
|
|
|
|
# DNS-over-TLS.
|
|
|
|
listen-on tls ${dnsSubdomain} { ${listenInterfaces} };
|
|
|
|
listen-on-v6 tls ${dnsSubdomain} { ${listenInterfacesIpv6} };
|
|
|
|
|
|
|
|
# DNS-over-HTTPS.
|
2024-09-20 04:33:26 +00:00
|
|
|
tls-port ${builtins.toString config.state.ports.dnsOverTLS.value};
|
|
|
|
https-port ${builtins.toString config.state.ports.dnsOverHTTPS.value};
|
2023-12-11 08:30:00 +00:00
|
|
|
listen-on tls ${dnsSubdomain} http ${dnsSubdomain} { ${listenInterfaces} };
|
|
|
|
listen-on-v6 tls ${dnsSubdomain} http ${dnsSubdomain} { ${listenInterfacesIpv6} };
|
|
|
|
|
|
|
|
allow-query { cachenetworks; };
|
|
|
|
blackhole { badnetworks; };
|
|
|
|
forward ${cfg.forward};
|
|
|
|
forwarders { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
|
|
|
|
directory "${cfg.directory}";
|
|
|
|
pid-file "/run/named/named.pid";
|
|
|
|
};
|
|
|
|
|
|
|
|
view internal {
|
|
|
|
match-clients { trusted; };
|
|
|
|
|
|
|
|
allow-query { any; };
|
|
|
|
allow-recursion { any; };
|
|
|
|
|
|
|
|
// We'll use systemd-resolved as our forwarder.
|
|
|
|
forwarders { 127.0.0.53 port 53; };
|
|
|
|
|
|
|
|
zone "${fqdn}" {
|
|
|
|
type primary;
|
2024-09-20 04:33:26 +00:00
|
|
|
file "${getZoneFile fqdn}";
|
2023-12-11 08:30:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
zone "${domain}" {
|
|
|
|
type primary;
|
|
|
|
|
2024-09-20 04:33:26 +00:00
|
|
|
file "${getZoneFile domain}";
|
|
|
|
allow-transfer { ${lib.concatStringsSep "; " config.state.network.secondaryNameservers}; };
|
2023-12-11 08:30:00 +00:00
|
|
|
update-policy {
|
|
|
|
grant rfc2136key.${domain}. zonesub TXT;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
view external {
|
|
|
|
match-clients { any; };
|
|
|
|
|
|
|
|
forwarders { };
|
|
|
|
empty-zones-enable yes;
|
|
|
|
allow-query { any; };
|
|
|
|
allow-recursion { none; };
|
|
|
|
|
|
|
|
zone "${domain}" {
|
|
|
|
in-view internal;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
${cfg.extraConfig}
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.bind = {
|
|
|
|
path = with pkgs; [ replace-secret ];
|
|
|
|
preStart =
|
|
|
|
let
|
2024-09-20 04:33:26 +00:00
|
|
|
domainZone' = getZoneFile domain;
|
|
|
|
fqdnZone' = getZoneFile fqdn;
|
2023-12-11 08:30:00 +00:00
|
|
|
in
|
|
|
|
lib.mkAfter ''
|
|
|
|
# Install the domain zone.
|
2024-09-20 04:33:26 +00:00
|
|
|
[ -f ${lib.escapeShellArg domainZone'} ] && install -Dm0600 ${zonefile} ${lib.escapeShellArg domainZone'}
|
2023-12-11 08:30:00 +00:00
|
|
|
|
|
|
|
# Install the internal DNS zones.
|
2024-09-20 04:33:26 +00:00
|
|
|
[ -f ${lib.escapeShellArg fqdnZone'} ] && install -Dm0600 '${fqdnZone}' ${lib.escapeShellArg fqdnZone'}
|
2023-12-11 08:30:00 +00:00
|
|
|
'';
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
# Additional service hardening. You can see most of the options from
|
|
|
|
# systemd.exec(5) manual. Run it as an unprivileged user.
|
|
|
|
User = config.users.users.named.name;
|
|
|
|
Group = config.users.users.named.group;
|
|
|
|
UMask = "0037";
|
|
|
|
|
|
|
|
# Get the credentials into the service.
|
|
|
|
LoadCredential =
|
|
|
|
let
|
|
|
|
certDirectory = config.security.acme.certs."${dnsSubdomain}".directory;
|
|
|
|
certCredentialPath = path: "${path}:${certDirectory}/${path}";
|
|
|
|
in
|
|
|
|
[
|
|
|
|
(certCredentialPath "cert.pem")
|
|
|
|
(certCredentialPath "key.pem")
|
|
|
|
(certCredentialPath "fullchain.pem")
|
|
|
|
];
|
|
|
|
|
|
|
|
LogFilterPatterns = [
|
|
|
|
# systemd-resolved doesn't have DNS cookie support, it seems.
|
|
|
|
"~missing expected cookie from 127.0.0.53#53"
|
|
|
|
];
|
|
|
|
|
|
|
|
# Lock and protect various system components.
|
|
|
|
LockPersonality = true;
|
|
|
|
PrivateTmp = true;
|
|
|
|
NoNewPrivileges = true;
|
|
|
|
RestrictSUIDSGID = true;
|
|
|
|
ProtectHome = true;
|
|
|
|
ProtectHostname = true;
|
|
|
|
ProtectClock = true;
|
|
|
|
ProtectKernelModules = true;
|
|
|
|
ProtectKernelTunables = true;
|
|
|
|
ProtectKernelLogs = true;
|
|
|
|
ProtectControlGroups = true;
|
|
|
|
ProtectProc = "invisible";
|
|
|
|
|
|
|
|
# Make the filesystem invisible to the service.
|
|
|
|
ProtectSystem = "strict";
|
|
|
|
ReadWritePaths = [
|
|
|
|
config.services.bind.directory
|
|
|
|
"/etc/bind"
|
|
|
|
];
|
|
|
|
ReadOnlyPaths = [
|
|
|
|
config.security.dhparams.params.bind.path
|
|
|
|
config.security.acme.certs."${dnsSubdomain}".directory
|
|
|
|
];
|
|
|
|
|
|
|
|
# Set up writable directories.
|
|
|
|
RuntimeDirectory = "named";
|
|
|
|
RuntimeDirectoryMode = "0750";
|
|
|
|
CacheDirectory = "named";
|
|
|
|
CacheDirectoryMode = "0750";
|
|
|
|
ConfigurationDirectory = "bind";
|
|
|
|
ConfigurationDirectoryMode = "0755";
|
|
|
|
|
|
|
|
# Filtering system calls.
|
|
|
|
SystemCallFilter = [ "@system-service" ];
|
|
|
|
SystemCallErrorNumber = "EPERM";
|
|
|
|
SystemCallArchitectures = "native";
|
|
|
|
|
|
|
|
# Granting and restricting its capabilities. Take note we're not using
|
|
|
|
# syslog for this even if the application can so no syslog capability.
|
|
|
|
# Additionally, we're using omitting the program's ability to chroot and
|
|
|
|
# chown since the user and the directories are already configured.
|
|
|
|
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
|
|
|
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
|
|
|
|
|
|
|
# Restrict what address families can it access.
|
|
|
|
RestrictAddressFamilies = [
|
|
|
|
"AF_LOCAL"
|
|
|
|
"AF_NETLINK"
|
|
|
|
"AF_BRIDGE"
|
|
|
|
"AF_INET"
|
|
|
|
"AF_INET6"
|
|
|
|
];
|
|
|
|
|
|
|
|
# Restricting what namespaces it can create.
|
|
|
|
RestrictNamespaces = true;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
# Then generate a DH parameter for the application.
|
|
|
|
security.dhparams.params.bind.bits = 4096;
|
|
|
|
}
|
|
|
|
|
2024-09-20 04:33:26 +00:00
|
|
|
(lib.mkIf hostCfg.setups.monitoring.enable {
|
|
|
|
services.bind.extraConfig = ''
|
|
|
|
statistics-channels {
|
|
|
|
inet 127.0.0.1 port ${builtins.toString config.state.ports.bindStatistics.value} allow { 127.0.0.1; };
|
|
|
|
};
|
|
|
|
'';
|
|
|
|
})
|
|
|
|
|
2023-12-11 08:30:00 +00:00
|
|
|
(lib.mkIf hostCfg.services.reverse-proxy.enable {
|
|
|
|
# Making this with nginx.
|
|
|
|
services.nginx.upstreams.local-dns = {
|
|
|
|
extraConfig = ''
|
|
|
|
zone dns 64k;
|
|
|
|
'';
|
|
|
|
servers = {
|
2024-09-20 04:33:26 +00:00
|
|
|
"127.0.0.1:${builtins.toString config.state.ports.dnsOverHTTPS.value}" = { };
|
2023-12-11 08:30:00 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
services.nginx.virtualHosts."${dnsSubdomain}" = {
|
|
|
|
forceSSL = true;
|
|
|
|
enableACME = true;
|
|
|
|
acmeRoot = null;
|
|
|
|
extraConfig = ''
|
|
|
|
add_header Strict-Transport-Security max-age=31536000;
|
|
|
|
'';
|
|
|
|
kTLS = true;
|
|
|
|
locations = {
|
|
|
|
"/".return = "444";
|
|
|
|
"/dns-query".extraConfig = ''
|
|
|
|
grpc_pass grpcs://local-dns;
|
|
|
|
grpc_socket_keepalive on;
|
|
|
|
grpc_connect_timeout 10s;
|
|
|
|
grpc_ssl_verify off;
|
|
|
|
grpc_ssl_protocols TLSv1.3 TLSv1.2;
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
services.nginx.streamConfig = ''
|
|
|
|
upstream dns_servers {
|
|
|
|
server localhost:53;
|
|
|
|
}
|
|
|
|
|
|
|
|
server {
|
|
|
|
listen 53 udp reuseport;
|
|
|
|
proxy_timeout 20s;
|
|
|
|
proxy_pass dns_servers;
|
|
|
|
}
|
|
|
|
'';
|
|
|
|
})
|
|
|
|
|
|
|
|
# Set up the firewall. Take note the ports with the transport layer being
|
|
|
|
# accepted in Bind.
|
|
|
|
(lib.mkIf hostCfg.services.firewall.enable {
|
2024-09-20 04:33:26 +00:00
|
|
|
networking.firewall = {
|
|
|
|
allowedUDPPorts = [ config.state.ports.dns.value ];
|
|
|
|
allowedTCPPorts = with config.state.ports; [
|
|
|
|
dns.value
|
|
|
|
dnsOverHTTPS.value
|
|
|
|
dnsOverTLS.value
|
|
|
|
];
|
|
|
|
};
|
2023-12-11 08:30:00 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
# Add the following to be backed up.
|
|
|
|
(lib.mkIf hostCfg.services.backup.enable {
|
|
|
|
services.borgbackup.jobs.services-backup.paths = [ zonesDir ];
|
|
|
|
})
|
|
|
|
|
|
|
|
# Set up a fail2ban which is apparently already available in the package.
|
|
|
|
(lib.mkIf hostCfg.services.fail2ban.enable {
|
|
|
|
services.fail2ban.jails."named-refused".settings = {
|
|
|
|
enabled = true;
|
|
|
|
backend = "systemd";
|
|
|
|
filter = "named-refused[journalmatch='_SYSTEMD_UNIT=bind.service']";
|
|
|
|
maxretry = 3;
|
|
|
|
};
|
|
|
|
})
|
|
|
|
]);
|
|
|
|
}
|