Created by: ston1th 2019-05-29 18:21:18
Updated by: ston1th 2023-04-01 13:24:58
Visibility: Public

OpenBSD DNS Server

Enable and start service:

rcctl enable nsd
rcctl start nsd

Config

Create control keys:

# crypto partition (optional)
mkdir /mnt/nsd
chmod 750 /mnt/nsd
ln -s /mnt/nsd/nsd_control.key /var/nsd/etc/nsd_control.key
ln -s /mnt/nsd/nsd_server.key /var/nsd/etc/nsd_server.key

# create keys
nsd-control-setup

# crypto partition (optional)
chgrp _nsd /mnt/nsd/*.key
cat <<EOF> /var/nsd/etc/nsd.conf
server:
	hide-version: yes
	verbosity: 1
	database: ""
	username: _nsd


remote-control:
	control-enable: yes
	control-interface: /var/run/nsd.sock

zone:
	name: "example.com"
	zonefile: "/var/nsd/etc/example.com.zone"
	notify: <secondary dns server> NOKEY
	provide-xfr: <secondary dns server> NOKEY

zone:
	name: "example.org"
	zonefile: "/var/nsd/etc/example.org.zone"
	notify: <secondary dns server> NOKEY
	provide-xfr: <secondary dns server> NOKEY
EOF
chmod 640 /var/nsd/etc/nsd.conf

Zone Files

cat <<EOF> /var/nsd/etc/example.com.zone
$ORIGIN example.com.
$TTL 3600
@ IN SOA ns.example.com. hostmaster.example.com. (
1535663699
	3600
	1200
	604800
	600 )

	IN NS ns.example.com.
	IN NS ns2.example.com.

	IN MX 10 mail.example.com.

	IN CAA 128 issue "letsencrypt.org"
	; add to deny wildcard certificates
	;IN CAA 128 issuewild ";"

www		IN A	198.51.100.1
dyn		IN A	198.51.100.1
EOF

Dynamic DNS

This is based on https://github.com/exitnode/nsd-dyndns

Web Server

cat <<'EOF'> /etc/httpd.conf
domain="ns.example.com"

server $domain {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location "/*" {
    block return 301 "https://$SERVER_NAME$DOCUMENT_URI"
  }
}

server $domain {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/ns.example.com.fullchain.pem"
    key "/etc/ssl/private/ns.example.com.key"
    ciphers ECDHE-RSA-CHACHA20-POLY1305:CHACHA20:ECDHE+AESGCM:ECDHE+AES
  }
  hsts { max-age 31536000, subdomains }
  root "/htdocs/dyndns"
  location "/robots.txt" {
    root "/htdocs"
  }
  location "/update.html" {
    log access dyndns.log
  }
}
EOF

cat <<EOF> /var/www/htdocs/robots.txt
User-agent: *
Disallow: /
EOF

mkdir /var/www/htdocs/dyndns
touch /var/www/htdocs/dyndns/update.html

rcctl enable httpd
rcctl start httpd

Lets Encrypt

See OpenBSD Lets Encrypt

Config

Create config directory: mkdir -m 700 /etc/dyndns.d

Example A Record:

cat <<EOF> /etc/dyndns.d/dyn.conf
key="$(openssl rand -hex 16)"
init="127.0.0.1"
logfile=/var/www/logs/dyndns.log
timestamp=\$(date +%s)
zonefile=/var/nsd/etc/example.com.zone
ttl=60
subdomain="dyn"
rtype=A
dnssec=0
EOF
chmod 600 /etc/dyndns.d/dyn.conf

Example TXT Record for ACME DNS01 challenge:

cat <<EOF> /etc/dyndns.d/acme.conf
key="$(openssl rand -hex 16)"
logfile=/var/www/logs/dyndns.log
timestamp=\$(date +%s)
zonefile=/var/nsd/etc/example.com.zone
ttl=600
subdomain="_acme-challenge"
rtype=TXT
dnssec=0
EOF
chmod 600 /etc/dyndns.d/acme.conf

Create update script:

cat <<'EOF'> /usr/local/sbin/dyndns-update
#!/bin/sh

i=0
for conf in $(find /etc/dyndns.d -name "*.conf" -type f); do
  . ${conf}
  changed=0
  record="${subdomain} ${ttl} IN ${rtype}"
  case ${rtype} in
  A)
    data=$(grep ${key} ${logfile} | tail -1 | cut -d" " -f2)
    if [ -z "${data}" ]; then
      grep -q "${subdomain} .*${rtype}" ${zonefile} && continue
      echo "${record} ${init}" >>${zonefile}; changed=1
    else
      grep -q "${record} ${data}" ${zonefile} && continue
      sed -i "s/^${subdomain} .*${rtype}.*/${record} ${data}/" ${zonefile}; changed=1
    fi
  ;;
  TXT)
    data="\"$(grep -o "${key}=[^ ]*" ${logfile} | tail -1 | cut -d"=" -f2)\""
    if [ "${data}" = '""' ]; then
      grep -q "${subdomain} .*${rtype}" ${zonefile} || continue
      sed -i "/^${subdomain} .*${rtype}/d" ${zonefile}; changed=1
    else
      grep -q "${record} ${data}" ${zonefile} && continue
      echo "${record} ${data}" >>${zonefile}; changed=1
    fi
  ;;
  esac
  if [[ ${changed} -eq 1 ]]; then
    sed -i "4s/.*/${timestamp}/" ${zonefile}
    [[ ${dnssec} -eq 1 ]] && /usr/local/sbin/sign-zone ${zonefile}
  fi
  logger -t dyndns-update ${conf##*/} ${rtype} set to ${data}
  i=$((i+1))
done

[ $i -ne 0 ] && { nsd-control reload >/dev/null; nsd-control notify >/dev/null; }
EOF
chmod 755 /usr/local/sbin/dyndns-update

Client

Extract the key from the config file: grep key /etc/dyndns.d/dyn.conf

A Record

Replace <KEY> with the key in your config file.

On OpenBSD

echo "*/5 * * * * /usr/bin/ftp -o /dev/null https://ns.example.com/update.html?<KEY> >/dev/null" >> /var/cron/tabs/nobody
chown nobody:crontab /var/cron/tabs/nobody
chmod 600 /var/cron/tabs/nobody

On Linux

echo "*/5 * * * * /usr/bin/curl -s https://ns.example.com/update.html?<KEY> >/dev/null" >> /var/cron/tabs/nobody
chown nobody:crontab /var/spool/cron/crontabs/nobody
chmod 600 /var/spool/cron/crontabs/nobody

TXT Record

Replace <KEY> with the key in your config file.

Replace <YOUR TXT CONTENT> with the value you want to have in the TXT record.

On OpenBSD

echo "*/5 * * * * /usr/bin/ftp -o /dev/null https://ns.example.com/update.html?<KEY>=<YOUR TXT CONTENT> >/dev/null" >> /var/cron/tabs/nobody
chown nobody:crontab /var/cron/tabs/nobody
chmod 600 /var/cron/tabs/nobody

On Linux

echo "*/5 * * * * /usr/bin/curl -s https://ns.example.com/update.html?<KEY>=<YOUR TXT CONTENT> >/dev/null" >> /var/spool/cron/crontabs/nobody
chown nobody:crontab /var/spool/cron/crontabs/nobody
chmod 600 /var/spool/cron/crontabs/nobody

DNSSEC

pkg_add ldns-utils
mkdir -p /var/nsd/etc/keys

# crypto partition (optional)
mkdir -p /mnt/nsd/keys
chmod 750 /mnt/nsd/keys
ln -s /mnt/nsd/keys /var/nsd/etc/keys

cd /var/nsd/etc/keys

domain=example.com
zsk=$(ldns-keygen -a RSASHA256 -b 2048 ${domain})
mv ${zsk}.key ${zsk%%.+*}.zsk.key
mv ${zsk}.private ${zsk%%.+*}.zsk.private
ksk=$(ldns-keygen -a RSASHA256 -b 2048 -k ${domain})
mv ${ksk}.key ${ksk%%.+*}.ksk.key
mv ${ksk}.ds ${ksk%%.+*}.ksk.ds
mv ${ksk}.private ${ksk%%.+*}.ksk.private

You need to supply the contents of Kexample.com.ksk.ds and Kexample.com.ksk.key to your domain registry.

cat <<'EOF'> /usr/local/sbin/sign-zone
#!/bin/sh

zonefile="$1"
[[ -z "${zonefile}" ]] && { echo "missing zonefile argument"; exit 1; }
domain=${zonefile##*/}
domain=${domain%.zone}

valid=6
hashes=24
dir="/var/nsd/etc"
keys="${dir}/keys"
zone="${dir}/${domain}.zone"
signed="${zone}.signed"
temp="${dir}/tmp"
now=$(date +%s)

sed -i "4s/.*/${now}/" ${zone}
/usr/local/bin/ldns-read-zone ${zone} >${temp}
cat ${keys}/K${domain}.ksk.ds ${keys}/K${domain}*.key >>${temp}

exp=$(($now+$valid * 24 * 3600))
/usr/local/bin/ldns-signzone -i $now -e $exp \
  -n -s $(openssl rand -hex 8) -t ${hashes} -o ${domain} \
  -f ${signed} ${temp} ${keys}/K${domain}.ksk ${keys}/K${domain}.zsk

rm ${temp}
EOF
chmod 755 /usr/local/sbin/sign-zone
cat <<'EOF'> /usr/local/sbin/dnssec-update
#!/bin/sh

[[ $# -eq 0 ]] && { echo "missing domain arguments"; exit 1; }

for domain in $@; do
  /usr/local/sbin/sign-zone ${domain}
done
nsd-control reload >/dev/null
nsd-control notify >/dev/null
EOF
chmod 755 /usr/local/sbin/dnssec-update
echo "0 2 * * * /usr/local/sbin/dnssec-update example.com example.net >/dev/null 2>&1" >> /var/cron/tabs/root

Test your DNSSEC domain: https://dnssec-analyzer.verisignlabs.com/

SSHFP

ssh server "ssh-keyscan -D 127.0.0.1 2>/dev/null" | grep -E "SSHFP 1 2|SSHFP 4 2"