Bind9 DNS on a LXC (Proxmox)

Why run Bind9 inside an LXC?

Putting Bind inside an unprivileged Linux container keeps the Proxmox host surface small while letting us roll snapshots, firewall the service separately and move the DNS stack to another node in seconds. Arch Linux was picked for its tiny base image and bleeding‑edge packages — ideal when you actually want the latest CVE fixes. If you prefer a long‑term‑support distro (Debian, Alma …), the steps are 95 % identical: just swap pacman for apt/dnf and enjoy.

1 · Create the container (Proxmox host)

Download an official Arch template:

pveam update
pveam available | grep archlinux
pveam download local archlinux-20240301_amd64.tar.zst

Spin up an unprivileged container (ID 130, 512 MiB RAM, static IP 10.0.0.3):

pct create 130 local:vztmpl/archlinux-20240301_amd64.tar.zst \
     --hostname dns01 \
     --unprivileged 1 \
     --net0 name=eth0,bridge=vmbr0,ip=10.0.0.3/24,gw=10.0.0.1 \
     --memory 512 --swap 512 --rootfs local-lvm:5
pct start 130
pct console 130

Need systemd-nspawn or FUSE later? Enable nesting capabilities:

pct set 130 -features nesting=1

2 · Install Bind9 & helper tools

Refresh keys, fully upgrade Arch

pacman -Sy archlinux-keyring --noconfirm
pacman -Syu --noconfirm

Install Bind and dig / nsupdate helpers

pacman -S --noconfirm bind bind-tools

Free port 53 – disable the default stub resolver:

systemctl disable --now systemd-resolved.service

Enable & start named

systemctl enable --now named.service

Quick firewall (optional but recommended)

Arch’s iptables preset is blank. If your LXC is bridged straight to the LAN it will happily answer the whole internet. The one‑liner below allows only TCP/UDP 53 plus SSH from the LAN range:

pacman -S --noconfirm nftables
cat > /etc/nftables.conf <<'EOF'
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0;
    ct state established,related accept
    iif "lo" accept
    ip saddr 10.0.0.0/24 udp dport 53 accept   # DNS UDP
    ip saddr 10.0.0.0/24 tcp dport 53 accept   # DNS TCP
    counter drop
  }
}
EOF
systemctl enable --now nftables

3 · Directory layout & defaults on Arch

/etc/named.conf       # main configuration, world‑readable
/etc/named/           # custom include files (our views)
/var/named/           # zone files, owned by named:named (chroot‑safe)
/run/named/           # PID & runtime artefacts
/var/log/named.log    # we create this in logging section

The Arch package does not chroot Bind. That makes debugging easier. If you want a real chroot use named -t or systemd‑nspawn.

4 · Configuration: minimal named.conf (split‑horizon)

Below is a hardened template: recursion disabled by default, query logging on its own file and two views (“lan”/“wan”) for split‑horizon. Adapt CIDRs and domain.

options {
    directory "/var/named";
    pid-file  "/run/named/named.pid";

    listen-on { any; };
    listen-on-v6 { any; };

    recursion no;                 # safety first
    allow-recursion { 10.0.0.0/24; };
    allow-transfer  { none; };
    allow-update    { none; };

    dnssec-validation auto;

    version   "not disclosed";
    hostname  "dns01";
    server-id none;
};

logging {
    channel querylog {
        file "/var/log/named.log" versions 3 size 5m;
        severity info;
        print-category yes;
        print-time yes;
        print-severity yes;
    };
    category queries { querylog; };
};

view "lan" {
    match-clients { 10.0.0.0/24; localhost; };
    include "/etc/named/lan-zones.conf";
};

view "wan" {
    match-clients { any; };
    recursion no;              # public must not recurse
    include "/etc/named/wan-zones.conf";
};

5 · Create zone files

Add a per‑view include so you can host many domains without editing the root config each time:

# /etc/named/lan-zones.conf
zone "example.com" IN { type master; file "lan/example.com.zone"; };

# /etc/named/wan-zones.conf
zone "example.com" IN { type master; file "wan/example.com.zone"; };

LAN (private) zone /var/named/lan/example.com.zone

$TTL 2h
@   IN SOA ns1.example.com. hostmaster.example.com. (
            2025051701 ; Serial (YYYYMMDDnn)
            8h         ; Refresh
            30m        ; Retry
            1w         ; Expire
            1h )       ; Negative Cache TTL
    IN NS  ns1.example.com.
ns1 IN A   10.0.0.3
@   IN A   10.0.0.20
www IN A   10.0.0.20

WAN (public) zone /var/named/wan/example.com.zone

$TTL 2h
@   IN SOA ns1.example.com. hostmaster.example.com. (
            2025051701 ; Serial (YYYYMMDDnn)
            8h         ; Refresh
            30m        ; Retry
            1w         ; Expire
            1h )       ; Negative Cache TTL
    IN NS  ns1.example.com.
ns1 IN A   203.0.113.15
@   IN A   203.0.113.15
www IN A   203.0.113.15

Validate syntax & zone integrity

named-checkconf
named-checkzone example.com /var/named/lan/example.com.zone
named-checkzone example.com /var/named/wan/example.com.zone

Reload without downtime

rndc reload

6 · Smoke test

From a LAN client:

dig @10.0.0.3 www.example.com +short   # → 10.0.0.20

From an external VPS:

dig @203.0.113.15 www.example.com +short   # → 203.0.113.15

Verify AXFR is blocked

dig -t AXFR example.com @203.0.113.15    # → Transfer failed.

Confirm recursion disabled for WAN / LAN

dig @203.0.113.15 google.com +short   # no answer section
dig @10.0.0.3 google.com +short   # no answer section

7 · Proxmox LXC caveats

  • Map container DNS port to host only if you use NAT; bridged mode exposes it automatically.
  • Enable IPv6: pct set 130 -net0 ...,ip6=2a02:ffff::3/64 and add a AAAA record.
  • Take a snapshot after every zone addition: pct snapshot 130 "after-example.com".
  • Backups: vzdump 130 --mode snapshot --compress zstd includes /etc & /var/named by default.

8 · Maintenance cheat‑sheet

Add a record, bump serial, reload

vim /var/named/lan/example.com.zone    # edit + bump serial
rndc reload example.com

Flush full cache

rndc flush

Dump statistics

rndc stats && tail /var/named/named.stats

9 · Troubleshooting quickies

  • Crash on start → check journalctl -u named.service and named-checkconf.
  • Zone loads but queries fail → verify firewall and listen-on directives.
  • Random SERVFAIL → ensure the container clock syncs (timedatectl).
  • Slow answers → enable query logging and look for TCP fallback loops.

10 · References