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

  1. Deploy the configurations to your master and slave servers
  2. Initialize DNSSEC for each zone on the master:
    init-dnssec example.com
    init-dnssec example.org
    
  3. 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
    
  4. 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:

  1. Zone transfer monitoring
  2. DNSSEC signature expiration monitoring
  3. Response time monitoring
  4. 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.

Additional Resources