Introduction
In this post, I’ll walk through setting up a complete, redundant DNS infrastructure using NixOS, Knot DNS, and DNSSEC. I’ll provide ready-to-use configurations for both master and slave servers.
Directory Structure
nix-config/
├── hosts/
│ ├── master/
│ │ ├── default.nix
│ │ └── zones/
│ │ ├── example.com.zone
│ │ └── example.org.zone
│ └── slave/
│ └── default.nix
Zone Files
First, let’s look at our zone files that will be stored in version control:
; example.com.zone
example.com. 300 A 192.0.2.1
example.com. 300 NS ns01.example.com.
example.com. 300 NS ns02.example.com.
example.com. 300 SOA ns01.example.org. ns02.example.com. 2024011502 300 1800 604800 86400
blog.example.com. 300 CNAME www.example.com.
docs.example.com. 300 CNAME www.example.com.
loopback.example.com. 300 A 192.0.2.1
www.example.com. 300 A 192.0.2.1
; example.org.zone
example.org. 300 A 192.0.2.1
example.org. 300 NS ns01.example.org.
example.org. 300 NS ns02.example.org.
example.org. 300 SOA ns01.example.org. ns02.example.org. 2024011502 300 1800 604800 86400
blog.example.org. 300 CNAME www.example.org.
docs.example.org. 300 CNAME www.example.org.
loopback.example.org. 300 A 192.0.2.1
www.example.org. 300 A 192.0.2.1
Master Server Configuration
Here’s the complete NixOS configuration for the master DNS server:
{ pkgs, ... }: let
zonesDir = ./zones;
in {
# Basic network configuration
networking = {
hostName = "dns-master";
firewall.allowedTCPPorts = [ 22 53 ];
firewall.allowedUDPPorts = [ 53 ];
};
# Knot DNS configuration
services.knot = {
enable = true;
settings = {
database = {
storage = "/var/lib/knot/keys";
};
server = {
listen = ["0.0.0.0@53" "::@53"];
user = "knot";
};
remote = [
{
id = "slave1";
address = "192.0.2.2@53"; # Slave server IP
}
];
log = [
{
target = "stderr";
any = "debug";
}
];
acl = [
{
id = "acl_transfer";
action = "transfer";
address = ["192.0.2.2/32"];
}
{
id = "acl_notify";
action = "notify";
address = ["192.0.2.2"];
}
{
id = "acl_query";
action = "query";
address = ["0.0.0.0/0"];
}
];
template = [
{
id = "default";
file = "%d.zone";
storage = "/var/lib/knot/zones";
dnssec-signing = "on";
dnssec-policy = "default";
semantic-checks = "on";
}
];
policy = [
{
id = "default";
algorithm = "ECDSAP256SHA256";
ksk-size = 256;
zsk-size = 256;
zsk-lifetime = "30d";
ksk-lifetime = "365d";
propagation-delay = "1h";
rrsig-lifetime = "14d";
rrsig-refresh = "7d";
reproducible-signing = "on";
}
];
zone = [
{
domain = "example.com";
acl = ["acl_transfer" "acl_query"];
file = "/var/lib/knot/zones/example.com.zone";
template = "default";
dnssec-signing = "on";
dnssec-policy = "default";
journal-content = "all";
zonefile-load = "difference-no-serial";
notify = "slave1";
}
{
domain = "example.org";
acl = ["acl_transfer" "acl_query"];
file = "/var/lib/knot/zones/example.org.zone";
template = "default";
dnssec-signing = "on";
dnssec-policy = "default";
journal-content = "all";
zonefile-load = "difference-no-serial";
notify = "slave1";
}
];
};
};
# Automated zone deployment
system.activationScripts.knotSetup = ''
# Create directories
if [ ! -d /var/lib/knot/zones ]; then
mkdir -p /var/lib/knot/zones
fi
if [ ! -d /var/lib/knot/keys ]; then
mkdir -p /var/lib/knot/keys
fi
# Copy zone files
cp -f ${zonesDir}/* /var/lib/knot/zones/
# Set permissions
chown -R knot:knot /var/lib/knot
chmod -R 0770 /var/lib/knot
# Reload zones if knot is running
if ${pkgs.systemd}/bin/systemctl is-active knot >/dev/null; then
${pkgs.systemd}/bin/systemctl reload knot
fi
'';
# DNSSEC key management tool
environment.systemPackages = with pkgs; [
(writeScriptBin "init-dnssec" ''
#!${pkgs.bash}/bin/bash
# Function to display usage
usage() {
echo "Usage: $0 <zone-name>"
echo "Example: $0 example.com"
exit 1
}
# Check if zone parameter is provided
if [ $# -ne 1 ]; then
usage
fi
ZONE="$1"
KEY_DIR="/var/lib/knot/keys"
# Check if keys already exist
if [ -f "$KEY_DIR/$ZONE.keys" ]; then
echo "DNSSEC keys already exist for zone $ZONE"
echo "To generate new keys, remove existing ones first:"
echo "rm $KEY_DIR/$ZONE.keys"
exit 1
fi
# Generate keys
echo "Generating DNSSEC keys for zone $ZONE..."
${pkgs.knot-dns}/bin/keymgr -D "$KEY_DIR" "$ZONE" generate default
if [ $? -eq 0 ]; then
chown -R knot:knot "$KEY_DIR"
systemctl restart knot
echo "DNSSEC keys generated successfully for $ZONE"
echo "To get DS records, run: keymgr -d $KEY_DIR $ZONE ds"
else
echo "Error generating DNSSEC keys for $ZONE"
exit 1
fi
'')
];
# System users
users.users.knot = {
isSystemUser = true;
group = "knot";
};
users.groups.knot = {};
# Backup configuration
services.borgbackup.jobs.system = {
paths = ["/etc" "/var/lib/knot"];
exclude = ["*.tmp"];
repo = "/backup/system";
encryption.mode = "none";
compression = "auto,lz4";
startAt = "daily";
};
}
Slave Server Configuration
And here’s the complete configuration for the slave server:
{ pkgs, ... }: {
networking = {
hostName = "dns-slave";
firewall.allowedTCPPorts = [ 22 53 ];
firewall.allowedUDPPorts = [ 53 ];
};
services.knot = {
enable = true;
settings = {
database = {
storage = "/var/lib/knot/keys";
};
server = {
listen = ["0.0.0.0@53" "::@53"];
user = "knot";
};
remote = [
{
id = "master1";
address = "192.0.2.1@53";
}
];
log = [
{
target = "stderr";
any = "debug";
}
];
acl = [
{
id = "acl_transfer";
action = "transfer";
address = ["192.0.2.1"];
}
{
id = "acl_notify";
action = "notify";
address = ["192.0.2.1"];
}
{
id = "acl_query";
action = "query";
address = ["0.0.0.0/0"];
}
];
template = [
{
id = "default";
storage = "/var/lib/knot/zones";
master = ["master1"];
semantic-checks = "on";
}
];
zone = [
{
domain = "example.com";
template = "default";
acl = ["acl_transfer" "acl_notify" "acl_query"];
}
{
domain = "example.org";
template = "default";
acl = ["acl_transfer" "acl_notify" "acl_query"];
}
];
};
};
users.users.knot = {
isSystemUser = true;
group = "knot";
};
users.groups.knot = {};
}
Setup Process
- Deploy the configurations to your master and slave servers
- Initialize DNSSEC for each zone on the master:
init-dnssec example.com init-dnssec example.org
- Get the DS records for your parent zone:
keymgr -d /var/lib/knot/keys example.com ds keymgr -d /var/lib/knot/keys example.org ds
- Add the DS records to your parent zones through your registrar
Key Features
- Version-controlled zone files
- Automated zone deployment
- DNSSEC with modern algorithms
- Automatic key rotation
- Daily backups
- Master-slave replication
- Proper access controls
Monitoring Considerations
While this configuration provides a solid foundation, you should consider adding:
- Zone transfer monitoring
- DNSSEC signature expiration monitoring
- Response time monitoring
- Query logging for security analysis
Conclusion
This configuration provides a complete, production-ready DNS setup with DNSSEC support. The combination of version control, automation, and security features creates a robust and maintainable system.