#!/bin/bash
# =============================================================================
# PowerDNS + PowerDNS Admin Installer
# Targets: Ubuntu 22.04/24.04/26.04, Trisquel 11/12/13
# =============================================================================
set -euo pipefail
# --- Configuration ---
ROOT_MYSQL_PASSWORD="penguin"
POWER_DNS_MYSQL_PASSWORD="penguin"
POWERDNS_ADMIN_VERSION="3.8.1"
domain_name="tuxmail.io"
wireguard_static_ip="94.158.244.150"
time_zone="America/New_York"
zone_info="US/Eastern"
# --- Debugging helpers ---
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a /var/log/powerdns-install.log
}
die() {
log "FATAL: $*" >&2
exit 1
}
# =============================================================================
# Section 0: OS Detection
# =============================================================================
detect_os() {
log "=== SECTION: OS Detection ==="
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
die "Cannot detect OS"
fi
log "Detected OS: $OS_ID $OS_VERSION"
}
# =============================================================================
# Section 1: Timezone
# =============================================================================
section_timezone() {
log "=== SECTION: Timezone ==="
export DEBIAN_FRONTEND=noninteractive
apt-get install -y tzdata
ln -fs "/usr/share/zoneinfo/$zone_info" /etc/localtime
echo "$time_zone" > /etc/timezone
log "Timezone set to $time_zone"
}
# =============================================================================
# Section 2: DNS / Resolver
# =============================================================================
section_dns() {
log "=== SECTION: DNS Resolver ==="
# Stop systemd-resolved (it interferes with PowerDNS on port 53)
if systemctl is-active systemd-resolved &>/dev/null; then
systemctl stop systemd-resolved
fi
systemctl disable systemd-resolved 2>/dev/null || true
# Write a static resolv.conf with public DNS only
# (127.0.0.1 will be added after PowerDNS is confirmed running in Section 4)
if [ -L /etc/resolv.conf ]; then
rm /etc/resolv.conf
fi
cat > /etc/resolv.conf << 'RESOLV'
nameserver 9.9.9.9
nameserver 1.1.1.1
options edns0
RESOLV
chmod 644 /etc/resolv.conf
# Configure dhclient so DHCP reboots don't overwrite our file
mkdir -p /etc/dhcp
echo "supersede domain-name-servers 9.9.9.9, 1.1.1.1;" > /etc/dhcp/dhclient.conf
log "DNS configured (public resolvers)"
}
# =============================================================================
# Section 3: MariaDB + Apache
# =============================================================================
section_mariadb_apache() {
log "=== SECTION: MariaDB & Apache ==="
apt-get update
apt-get install -y apache2 mariadb-server
# Ensure MariaDB is running
systemctl start mariadb
systemctl enable mariadb
# Set root password (only if still using unix_socket auth)
if mysql -u root -e "SELECT 1" 2>/dev/null; then
mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$ROOT_MYSQL_PASSWORD'; FLUSH PRIVILEGES;"
log "MariaDB root password set"
else
log "MariaDB root password already set"
fi
# Create PowerDNS database and user (idempotent)
mysql --user="root" --password="$ROOT_MYSQL_PASSWORD" </dev/null; then
sed -i "s/^launch=.*/launch=gmysql/" /etc/powerdns/pdns.conf
else
echo "launch=gmysql" >> /etc/powerdns/pdns.conf
fi
# Append gmysql settings (avoid duplicates)
if ! grep -q "^gmysql-host=" /etc/powerdns/pdns.conf; then
cat >> /etc/powerdns/pdns.conf << GMYSQL
gmysql-host=127.0.0.1
gmysql-dbname=powerdns
gmysql-user=pdns
gmysql-password=$POWER_DNS_MYSQL_PASSWORD
GMYSQL
fi
# Import schema only if tables don't already exist
local table_count
table_count=$(mysql -u root -p"$ROOT_MYSQL_PASSWORD" -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='powerdns';")
if [ "$table_count" -eq 0 ]; then
local schema_path
schema_path=$(find /usr/share -name "schema.mysql.sql" 2>/dev/null | head -1)
if [ -z "$schema_path" ]; then
die "Cannot find PowerDNS MySQL schema file"
fi
log "Schema path: $schema_path"
mysql -u root -p"$ROOT_MYSQL_PASSWORD" powerdns < "$schema_path"
log "Schema imported"
else
log "Schema already present ($table_count tables), skipping import"
fi
# Start PowerDNS
systemctl restart pdns
systemctl enable pdns
# Wait for PowerDNS to be ready
sleep 2
if ! systemctl is-active pdns &>/dev/null; then
die "PowerDNS failed to start. Check: journalctl -xeu pdns.service"
fi
log "PowerDNS running"
# Now that PowerDNS is up, add 127.0.0.1 to resolv.conf
cat > /etc/resolv.conf << 'RESOLV'
nameserver 127.0.0.1
nameserver 9.9.9.9
options edns0
RESOLV
chmod 644 /etc/resolv.conf
log "resolv.conf updated to use local PowerDNS (127.0.0.1)"
}
# =============================================================================
# Section 5: PowerDNS Admin (Web GUI)
# =============================================================================
section_powerdns_admin() {
log "=== SECTION: PowerDNS Admin ==="
# Determine PHP version based on OS
local php_pkg_ver
if [ "$OS_VERSION" = "22.04" ] || [ "$OS_VERSION" = "11.0" ]; then
php_pkg_ver="8.1"
elif [ "$OS_VERSION" = "24.04" ] || [ "$OS_VERSION" = "12.0" ]; then
php_pkg_ver="8.3"
elif [ "$OS_VERSION" = "26.04" ] || [ "$OS_VERSION" = "13.0" ]; then
php_pkg_ver="8.5"
else
php_pkg_ver=$(dpkg-query -f '${Version}' -W php-common 2>/dev/null | cut -d'.' -f1,2 || echo "8.3")
log "Detected PHP version: $php_pkg_ver"
fi
log "Using PHP packages for version $php_pkg_ver"
# Install PHP dependencies (idempotent)
apt-get install -y \
php-net-ldap2 \
php-imagick \
"php${php_pkg_ver}-common" \
"php${php_pkg_ver}-gd" \
"php${php_pkg_ver}-mysql" \
"php${php_pkg_ver}-curl" \
"php${php_pkg_ver}-zip" \
"php${php_pkg_ver}-xml" \
"php${php_pkg_ver}-mbstring" \
"php${php_pkg_ver}-bz2" \
"php${php_pkg_ver}-intl" \
"php${php_pkg_ver}-gmp" \
"php${php_pkg_ver}-redis" \
php-pear \
wget \
"php${php_pkg_ver}-fpm" \
libapache2-mod-php
# Download and extract PowerDNS Admin (idempotent)
if [ -d /var/www/powerdns ]; then
log "PowerDNS Admin already present at /var/www/powerdns, skipping download"
else
local url="https://codeload.github.com/poweradmin/poweradmin/tar.gz/refs/tags/v${POWERDNS_ADMIN_VERSION}"
log "Downloading PowerDNS Admin v${POWERDNS_ADMIN_VERSION}"
local tmp_tar="/tmp/poweradmin.tar.gz"
if ! wget -qO "$tmp_tar" "$url"; then
rm -f "$tmp_tar"
die "Failed to download PowerDNS Admin from $url"
fi
tar -xzf "$tmp_tar" -C /var/www/
rm -f "$tmp_tar"
mv /var/www/poweradmin-* /var/www/powerdns
log "PowerDNS Admin extracted to /var/www/powerdns"
fi
}
# =============================================================================
# Section 6: DNS Zone Records
# =============================================================================
section_zone_records() {
log "=== SECTION: Zone Records ==="
# Only create zone if it doesn't exist
if pdnsutil list-all-zones 2>/dev/null | grep -q "^${domain_name}$"; then
log "Zone $domain_name already exists, skipping"
else
pdnsutil create-zone "$domain_name"
pdnsutil set-kind "$domain_name" MASTER
pdnsutil add-record "$domain_name" @ NS "ns1.$domain_name"
pdnsutil add-record "$domain_name" ns1 A "$wireguard_static_ip"
pdnsutil replace-rrset "$domain_name" . SOA "ns1.$domain_name. user1@$domain_name 1 10800 3600 604800 300"
pdnsutil add-record "$domain_name" mail A "$wireguard_static_ip"
pdnsutil add-record "$domain_name" @ MX "10 mail.$domain_name"
pdnsutil add-record "$domain_name" admin A "$wireguard_static_ip"
pdnsutil add-record "$domain_name" powerdns A "$wireguard_static_ip"
log "Zone records created"
fi
}
# =============================================================================
# Section 7: Apache Virtual Host
# =============================================================================
section_apache_vhost() {
log "=== SECTION: Apache VirtualHost ==="
cat > "/etc/apache2/sites-available/powerdns.conf" << EOF
ServerName powerdns.$domain_name
DocumentRoot /var/www/powerdns
Options FollowSymLinks MultiViews
AllowOverride All
Require all granted
EOF
a2ensite powerdns.conf
a2dissite 000-default.conf 2>/dev/null || true
systemctl restart apache2
log "Apache configured"
}
# =============================================================================
# Main: Run all sections or a specific one
# =============================================================================
ALL_SECTIONS=(
detect_os
section_timezone
section_dns
section_mariadb_apache
section_powerdns
section_powerdns_admin
section_zone_records
section_apache_vhost
)
if [ $# -gt 0 ]; then
for section in "$@"; do
case "$section" in
detect_os|section_*)
log "Running: $section"
"$section"
;;
all)
for s in "${ALL_SECTIONS[@]}"; do
log "Running: $s"
"$s"
done
;;
*)
die "Unknown section: $section"
;;
esac
done
else
for s in "${ALL_SECTIONS[@]}"; do
log "Running: $s"
"$s"
done
fi
log "=== Installation complete ==="
