Master-Slave PowerDNS Configuration and Domain Migration from BIND Using API and AXFR

One of my latest projects was a fully dockerized name server infrastructure based on PowerDNS: one master and two slaves — one in the same subnet and the second running in a cloud on a basic virtual machine.

Why PowerDNS? Because I needed an API, a proper admin-friendly web interface, user management, and LDAP integration. PowerDNS fully matched my requirements.

Why Docker? Because I wanted some level of automation and IaC — Docker fits nicely in the middle, where you have several configs and docker-compose files fully describing the container configuration.

Here are my domain import command-line tools, written in Bash: powerdns-api

This script performs the main actions needed to migrate from almost any name server (by reading zones via AXFR) into PowerDNS. You need both pdns-master and pdns-admin, because the PowerDNS API alone does not provide all the required functionality.

And then you just use the command line to migrate domains:
cat domains.txt | awk '{print "./pdns-migrator.sh --create-slave-zone="$1}'
cat domains.txt | awk '{print "./pdns-migrator.sh --axfr-retrieve="$1}'
cat domains.txt | awk '{print "./pdns-migrator.sh --convert-to-master="$1}'
cat domains.txt | awk '{print "./pdns-migrator.sh --replace-ns-soa="$1}'
I really like using a startup service for Docker that removes all data on stop/start — just to make sure I don’t forget to move changes out of containers 😉
# cat docker-compose.service 
#
# /etc/systemd/system/docker-compose.service
#

[Unit]
Description=Docker compose start service
Requires=docker.service network-online.target
After=docker.service network-online.target

[Service]
PIDFile=/run/docker-compose.pid

WorkingDirectory=/srv/docker-compose

ExecStartPre=/usr/bin/docker compose -f docker-compose.yaml down -v
ExecStartPre=/usr/bin/docker compose -f docker-compose.yaml rm -fv

ExecStart=/usr/bin/docker compose -f docker-compose.yaml up
ExecStop=/usr/bin/docker compose down -v

StandardOutput=null
StandardError=null

[Install]
WantedBy=multi-user.target


Example of master name server:
# cat master-docker-compose.yaml

x-all-defaults: &all-defaults
  logging:
    driver: fluentd

services:

  fluentd:
    image: fluent/fluentd:v1.18-debian-1
    hostname: fluentd
    container_name: fluentd
    networks:
      - pdns
    ports:
      - '127.0.0.1:24224:24224'
      - '127.0.0.1:24224:24224/udp'
    volumes:
      - 'logs:/fluentd/log'

  db:
    << : *all-defaults
    image: mariadb:11.7.2-ubi
    hostname: db
    container_name: db
    networks:
      pdns:
        ipv4_address: 172.6.0.20
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - db:/var/lib/mysql:Z
    environment:
      - MARIADB_ROOT_PASSWORD=PASSWORD
    depends_on:
      - fluentd
    healthcheck:
      test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
      timeout: 10s
      retries: 5

  pdns-master:
    << : *all-defaults
    image: pschiffe/pdns-mysql:4.9
    hostname: ns1.domain.com
    container_name: pdns-master
    networks:
      pdns:
        ipv4_address: 172.6.0.30
    ports:
      - '53:53'
      - '53:53/udp'
      - '8987:8987'
    extra_hosts:
      - 'ns1.domain.com:111.111.111.111'
      - 'ns2.domain.com:10.0.0.10'
      - 'ns3.domain.com:10.0.100.55'
    volumes:
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PDNS_gmysql_host=172.6.0.20
      - PDNS_gmysql_password=PASSWORD
      - PDNS_primary=yes
      - PDNS_api=yes
      - PDNS_api_key=PASSWORD
      - PDNS_webserver=yes
      - PDNS_webserver_password=PASSWORD
      - PDNS_webserver_address=0.0.0.0
      - PDNS_webserver_port=8987
      - PDNS_webserver_allow_from=1.2.3.4/32
      - PDNS_version_string=anonymous
      - PDNS_default_ttl=1500
      - PDNS_allow_axfr_ips=10.0.0.10/32,111.111.111.111/32
      - PDNS_allow_notify_from=10.0.0.10/32,111.111.111.111/32
      - PDNS_only_notify=10.0.0.10/32,111.111.111.111/32
      - PDNS_also_notify=10.0.0.10/32,111.111.111.111/32
      - PDNS_default_soa_content=ns1.domain.com. hostmaster.@ 0 3600 3600 1209600 86400
      - PDNS_log_dns_details=yes
      - PDNS_loglevel=4
    depends_on:
      - fluentd
      - db

  pdns-admin:
    << : *all-defaults
    image: pschiffe/pdns-admin:0.4.1
    hostname: pdns-admin
    container_name: pdns-admin
    networks:
      pdns:
        ipv4_address: 172.6.0.35
    ports:
      - '8989:8080'
    volumes:
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PDNS_ADMIN_SQLA_DB_HOST=172.6.0.20
      - PDNS_ADMIN_SQLA_DB_PASSWORD=PASSWORD
      - PDNS_ADMIN_SALT=$$11$$11$$SALT
      - PDNS_API_KEY=PASSWORD
      - PDNS_API_URL=http://172.6.0.30:8987
      - PDNS_VERSION=4.9.0
    depends_on:
      - fluentd
      - db
      - pdns-master

  phpmyadmin:
    << : *all-defaults
    image: phpmyadmin:5
    hostname: phpmyadmin
    container_name: phpmyadmin
    networks:
      pdns:
        ipv4_address: 172.6.0.40
    ports:
      - '8988:80'
    volumes:
      - /etc/localtime:/etc/localtime:ro
    depends_on:
      - fluentd
      - db
    healthcheck:
      test: ['CMD', 'curl', '-fsSL', 'http://127.0.0.1:80']
      timeout: 10s
      retries: 5

networks:
  pdns:
    ipam:
      config:
        - subnet: 172.6.0.0/16
          gateway: 172.6.0.1

volumes:
  db:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/db_data
  logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/logs_data


Example of slave name server:
# cat slave-docker-compose.yaml

x-all-defaults: &all-defaults
  logging:
    driver: fluentd

services:

  fluentd:
    image: fluent/fluentd:v1.18-debian-1
    hostname: fluentd
    container_name: fluentd
    networks:
      - pdns
    ports:
      - '127.0.0.1:24224:24224'
      - '127.0.0.1:24224:24224/udp'
    volumes:
      - 'logs:/fluentd/log'

  db:
    << : *all-defaults
    image: mariadb:11.7.2-ubi
    hostname: db
    container_name: db
    networks:
      pdns:
        ipv4_address: 172.6.0.20
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - db:/var/lib/mysql:Z
    environment:
      - MARIADB_ROOT_PASSWORD=PASSWORD
    depends_on:
      - fluentd
    healthcheck:
      test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
      timeout: 10s
      retries: 5

  pdns-slave:
    << : *all-defaults
    image: pschiffe/pdns-mysql:4.9
    hostname: ns2.domain.com
    container_name: pdns-slave
    networks:
      pdns:
        ipv4_address: 172.6.0.30
    ports:
      - '53:53'
      - '53:53/udp'
      - '8987:8987'
    extra_hosts:
      - 'ns1.domain.com:111.111.111.111'
      - 'ns2.domain.com:10.0.0.10'
      - 'ns3.domain.com:10.0.100.55'
    volumes:
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PDNS_gmysql_host=172.6.0.20
      - PDNS_gmysql_dbname=powerdnsslave
      - PDNS_gmysql_password=PASSWORD
      - PDNS_secondary=yes
      - PDNS_autosecondary=yes
      - PDNS_webserver=yes
      - PDNS_webserver_password=PASSWORD
      - PDNS_webserver_address=0.0.0.0
      - PDNS_webserver_port=8987
      - PDNS_webserver_allow_from=1.2.3.4/32
      - PDNS_version_string=anonymous
      - PDNS_disable_axfr=yes
      - PDNS_allow_notify_from=10.0.0.10/32,111.111.111.111/32
      - PDNS_log_dns_details=yes
      - PDNS_loglevel=4
      - SUPERMASTER_IPS=10.0.0.123
      - SUPERMASTER_HOSTS=ns1.domain.com
    depends_on:
      - fluentd
      - db

  phpmyadmin:
    << : *all-defaults
    image: phpmyadmin:5
    hostname: phpmyadmin
    container_name: phpmyadmin
    networks:
      pdns:
        ipv4_address: 172.6.0.40
    ports:
      - '8988:80'
    volumes:
      - /etc/localtime:/etc/localtime:ro
    depends_on:
      - fluentd
      - db
    healthcheck:
      test: ['CMD', 'curl', '-fsSL', 'http://127.0.0.1:80']
      timeout: 10s
      retries: 5

networks:
  pdns:
    ipam:
      config:
        - subnet: 172.6.0.0/16
          gateway: 172.6.0.1

volumes:
  db:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/db_data
  logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/logs_data
Easy as 1-2-3: Build → Migrate → Use 😉

Comments

Popular posts from this blog

FreeRadius and Google Workspace LDAP

pssh (parallel-ssh) problems on Debian 10 with Python 3.7