Authoritative implementation

Configure NSD narrowly, then confine the process in layers

NSD has a deliberately small authoritative-only role. A secure deployment preserves that narrow scope in its zone configuration, filesystem permissions, AppArmor policy, systemd sandbox and network firewall.

Security model

Each layer should stop a different class of mistake

NSD serves authoritative data and does not perform recursive resolution. Its public network surface is normally UDP and TCP port 53, while administrative control should remain local.

systemd starts NSD as an unprivileged account, grants only the capability needed to bind a privileged port, removes broad kernel-facing features and makes most of the filesystem read-only.

AppArmor adds path-oriented mandatory access control. Even if an NSD process is compromised, the profile can prevent it from reading or modifying files outside the explicitly allowed NSD locations.

The firewall remains a separate boundary.

RestrictAddressFamilies= permits protocol families, not individual addresses or ports. Packet filtering must still expose only the intended DNS listeners and keep the control interface private.

Layered NSD security model with network filtering, systemd sandboxing, AppArmor confinement and restricted data directories

NSD configuration

A primary, a secondary and authenticated transfers

This Debian-style example uses documentation-only addresses. Paths and package defaults vary, so validate them against the installed NSD version.

/etc/nsd/nsd.conf
server:
    ip-address: 192.0.2.53
    ip-address: 2001:db8:53::53
    server-count: 2

    zonesdir: "/etc/nsd"
    xfrdfile: "/var/lib/nsd/xfrd.state"
    zonelistfile: "/var/lib/nsd/zone.list"

    logfile: ""
    log-only-syslog: yes
    verbosity: 1
    hide-version: yes
    hide-identity: yes
    minimal-responses: yes

    # Warning!
    # Necessary and safe when running NSD as user with systemd, but DANGEROUS when NSD is running as root!
    # Only set (uncomment) this when you have made sure systemd is running NSD as a user!
    #username: ""


remote-control:
    control-enable: yes
    control-interface: 127.0.0.1
    control-interface: ::1

key:
    name: "xfr-key.example."
    algorithm: hmac-sha256
    secret: "BASE64-ENCODED-SECRET"

# This server is primary for example.com.
zone:
    name: "example.com"
    zonefile: "master/example.com.zone"
    notify: 198.51.100.53 xfr-key.example.
    provide-xfr: 198.51.100.53 xfr-key.example.
    store-ixfr: yes
    create-ixfr: yes

# This server is secondary for example.net.
zone:
    name: "example.net"
    zonefile: "slaves/example.net.zone"
    allow-notify: 203.0.113.53 xfr-key.example.
    request-xfr: 203.0.113.53 xfr-key.example.

Explicit listeners

Binding named IPv4 and IPv6 addresses avoids accidentally serving on an unintended interface. Wildcard listeners may be preferable on hosts whose addresses change, but require equally deliberate firewall policy.

State outside the source tree

Primary source zones remain under /etc/nsd/master; transferable state and the dynamic zone list live under /var/lib/nsd; secondary zone files are the only writable content under /etc/nsd/slaves.

Local control only

nsd-control uses mutually authenticated TLS by default. Keeping its interfaces on loopback avoids publishing an administrative listener. A Unix control socket can reduce the network and certificate surface further.

Two ACL statements per role

A primary uses notify and provide-xfr. A secondary separately uses allow-notify and request-xfr. TSIG authenticates those messages but does not encrypt zone contents.

Do not publish the example secret.

Generate a random TSIG secret, distribute it over a protected channel and restrict its file permissions. Use XFR-over-TLS as well when transfer confidentiality is required.

Primary and secondary files

Make the intended write boundary visible in the directory layout

The daemon should read configuration, keys and primary zones, but only write transfer state and secondary data. That distinction lets both systemd and AppArmor enforce a useful policy.

/etc/nsd/nsd.confRead-only configuration
/etc/nsd/master/Read-only primary zones
/etc/nsd/slaves/Writable secondary zones
/var/lib/nsd/Writable transfer and runtime state
/run/nsd/Ephemeral PID or control socket data
Create narrow data directories
install -d -o root -g nsd -m 0750 /etc/nsd/master
install -d -o nsd  -g nsd -m 0700 /etc/nsd/slaves
install -d -o nsd  -g nsd -m 0700 /var/lib/nsd
Validate before loading
nsd-checkconf /etc/nsd/nsd.conf
nsd-checkzone example.com   /etc/nsd/master/example.com.zone
systemctl reload nsd

AppArmor

Permit the executable, its configuration and only the required writes

The attached profile below is automatically applied whenever the kernel executes /usr/sbin/nsd.

/etc/apparmor.d/usr.sbin.nsd
# Last reviewed for an AppArmor 4 policy ABI
abi <abi/4.0>,

include <tunables/global>

/usr/sbin/nsd {
  include <abstractions/base>
  include <abstractions/nameservice>
  include <abstractions/ssl_certs>
  include <abstractions/user-tmp>

  capability net_bind_service,

  /etc/nsd/** r,
  /usr/sbin/nsd mr,
  owner /etc/nsd/slaves/* rw,
  owner /var/lib/nsd/* rw,
}
Executable

mr

Allows NSD to be read and mapped into memory. The profile attaches to the exact executable path.

Capability

net_bind_service

Permits binding below port 1024. AppArmor capability permission and the process capability granted by systemd are independent checks.

Configuration

/etc/nsd/** r

Allows recursive read access to NSD configuration, zone files, control certificates and secrets. It is simple, but can be narrowed further when the exact file set is stable.

Writable state

owner … rw

Only files owned by the NSD user match an owner rule. Existing root-owned secondary or state files can therefore produce denials even when the pathname looks correct.

Globs and abstractions deserve review.

* covers direct children, while ** also covers deeper paths. Distribution abstractions can change and may grant more than their name suggests. Inspect the resolved profile whenever least privilege matters.

systemd sandbox

Use a drop-in instead of editing the vendor unit

The packaged service already supplies Type=notify, restart behaviour and the NSD command line. A local drop-in can tighten the service while remaining separate from package upgrades.

/etc/systemd/system/nsd.service.d/override.conf
[Unit]
After=network-online.target
# Optional local integration, only when this template exists:
# OnFailure=notify-error@%n

[Service]
# Ask all configured secondary zones to check their primaries after startup.
ExecStartPost=/usr/sbin/nsd-control transfer

# Run the daemon and ExecStartPost helper as the dedicated account.
User=nsd
Group=nsd
NoNewPrivileges=yes

# Make the filesystem read-only, then reopen only required state paths.
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ReadWritePaths=
ReadWritePaths=/var/lib/nsd /etc/nsd/slaves /run/nsd

ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectKernelLogs=yes
RemoveIPC=yes

RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes

PrivateNetwork=no
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX

# Reset inherited lists before granting the single required capability.
CapabilityBoundingSet=
AmbientCapabilities=
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

ProtectProc=invisible
ProcSubset=pid
MemoryDenyWriteExecute=yes
UMask=0077

SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=~@clock @cpu-emulation @debug @module @mount   @obsolete @privileged @raw-io @reboot @resources @swap

Unprivileged from the first instruction

User=nsd avoids starting a general root process. The ambient CAP_NET_BIND_SERVICE capability is enough for UDP and TCP port 53 without retaining CAP_NET_ADMIN, CAP_NET_RAW, CAP_SETUID or CAP_SYS_CHROOT.

Read-only filesystem view

ProtectSystem=strict makes the filesystem read-only inside the service mount namespace. Resetting the inherited ReadWritePaths list prevents a vendor rule that exposes all of /etc/nsd as writable from silently remaining in force.

Reduced kernel surface

Namespace, device, kernel, clock, hostname, realtime and executable-memory restrictions remove facilities that an authoritative DNS server normally does not require.

Restricted process view

ProtectProc=invisible and ProcSubset=pid reduce what NSD can learn about unrelated processes while preserving the process information needed for its own operation.

Denylisted syscall groups

The leading ~ makes this a denylist. This is generally more upgrade-tolerant than a tight allowlist, but every syscall not covered by the denied groups remains available.

Startup transfer check

nsd-control transfer checks secondary zones for newer versions. NSD already performs normal refresh processing; this hook simply requests an immediate check after a successful start.

Ordering is not dependency.

After=network-online.target only orders NSD after that target when the target is already part of the transaction. Add Wants=network-online.target only when the host genuinely needs the online target and provides a working wait-online service.

Verification workflow

Prove that both service and confinement still work

  1. Check the NSD configuration and every changed zone.

    Configuration syntax, zone syntax and runtime loading are separate checks.

    nsd-checkconf /etc/nsd/nsd.conf
    nsd-checkzone example.com /etc/nsd/master/example.com.zone
  2. Reload the AppArmor policy.

    Parse the profile before restarting the service, then confirm it is in enforce mode.

    apparmor_parser -r /etc/apparmor.d/usr.sbin.nsd
    aa-status | grep -F /usr/sbin/nsd
  3. Reload systemd and inspect the merged unit.

    systemctl cat reveals inherited vendor settings as well as the drop-in.

    systemctl daemon-reload
    systemd-analyze verify nsd.service
    systemctl cat nsd.service
    systemd-analyze security nsd.service
  4. Restart and inspect failures immediately.

    Look at both service logs and kernel AppArmor denials.

    systemctl restart nsd
    systemctl --no-pager --full status nsd
    journalctl -u nsd -b
    journalctl -k -b | grep 'apparmor="DENIED"'
  5. Test authoritative behaviour externally.

    Query both transports and verify that the answer is authoritative and recursion is unavailable.

    dig @192.0.2.53 example.com SOA +norecurse
    dig @192.0.2.53 example.com SOA +norecurse +tcp
    dig @192.0.2.53 unrelated.example A +recurse
  6. Inspect transfer state.

    For secondary zones, confirm the loaded serial and force a check when diagnosing.

    nsd-control status
    nsd-control zonestatus example.net
    nsd-control transfer example.net

Boundaries and caveats

Hardening is effective only when its assumptions remain true

nsd-control is a separate executable.

The AppArmor profile attached to /usr/sbin/nsd does not automatically confine a helper that systemd directly executes as /usr/sbin/nsd-control. The helper still inherits the unit’s systemd sandbox. Add a separate AppArmor profile if path-level confinement of the helper is required.

Control keys must be readable by the service account.

Starting NSD as User=nsd means both server and control credentials need carefully chosen ownership and mode bits. Do not solve a permission failure by making private keys world-readable.

The AppArmor read rule is intentionally broad.

/etc/nsd/** r includes TSIG secrets and control private keys. That is acceptable only because it applies to the confined NSD process; a more mature policy can name individual directories and files.

Secondary ownership must match owner.

If a deployment tool creates files as root inside /etc/nsd/slaves, the path matches but the AppArmor owner condition does not. Align the deployment workflow with the policy.

System call filters require regression testing.

Library, resolver, logging or NSD upgrades may introduce a previously unused syscall. Test upgrades before broad deployment and treat an EPERM in the journal as a possible sandbox denial.

Host hardening does not authenticate zone data.

AppArmor and systemd constrain the daemon. DNSSEC authenticates published RRsets; TSIG authenticates transfers and control transactions; TLS can add transfer confidentiality. These controls are complementary.