🛡️ Access Shield Pro v1.2 released - Now with Spamhaus & Emerging Threats feeds
🤖 AI Support Assistant 2.1 - GPT-5 & Claude Opus 4.1 support, auto-signatures, improved error handling.
🧮 Recalculate Prices v1.2 - Fixed addon pricing, enhanced safety checks, full billing cycle support.
🔧 PowerDNS Manager v1.11 - Enhanced logging security with better data sanitization, improved debug output handling, bug fixes.

PowerDNS Master-Slave Configuration with DNSSEC Support Print

  • 37

Introduction

This guide provides a detailed walk-through for setting up a PowerDNS master-slave configuration on Debian 12, including installation, configuration, DNSSEC support, and implementation of automatic zone management and import scripts. This setup is ideal for managing multiple DNS zones with automatic replication between servers, efficient zone imports, and secure DNSSEC signing.


Pre-requisites

  • SSH Access: Ensure passwordless SSH access from your master server (ns1) to your slave server (ns2). This is crucial for automated zone transfers and management. You can achieve this by setting up SSH keys.
  • Network Connectivity: Verify that both servers (ns1 and ns2) can communicate with each other on the required ports (primarily port 53 for DNS and port 8081 for the API, if enabled). Use tools like ping and telnet or nc to test connectivity.
  • Accurate Server IP Addresses: Have the correct IP addresses for both ns1 and ns2 readily available, as you'll need these for configuration.

Part 1: Installing PowerDNS

On both Master (ns1) and Slave (ns2) servers:

  1. Update your system:

    sudo apt update && sudo apt upgrade -y
  2. Install PowerDNS and the MySQL backend:

    sudo apt install pdns-server pdns-backend-mysql mariadb-server -y
  3. Secure your MySQL installation:

    sudo mysql_secure_installation
  4. Create a database and user for PowerDNS:

    sudo mysql -u root -p

    In the MySQL prompt:

    CREATE DATABASE pdns;
    CREATE USER 'pdns'@'localhost' IDENTIFIED BY 'your_secure_password';
    GRANT ALL PRIVILEGES ON pdns.* TO 'pdns'@'localhost';
    FLUSH PRIVILEGES;
    EXIT;
  5. Import the PowerDNS schema:

    sudo mysql -u root -p pdns < /usr/share/doc/pdns-backend-mysql/schema.mysql.sql

Part 2: Configuring PowerDNS

On the Master server (ns1):

  1. Edit the PowerDNS configuration file:

    sudo nano /etc/powerdns/pdns.conf
  2. Add or modify these lines:

    launch=gmysql
    gmysql-host=localhost
    gmysql-user=pdns
    gmysql-dbname=pdns
    gmysql-password=your_secure_password
    gmysql-dnssec=yes
    api=yes
    api-key=generate_a_secure_api_key
    webserver=yes
    webserver-port=8081
    webserver-address=0.0.0.0
    webserver-allow-from=127.0.0.1,ANOTHER_IP,ANOTHER_IP
    allow-axfr-ips=IP_OF_SLAVE_SERVER
    master=yes
    slave=no
    also-notify=IP_OF_SLAVE_SERVER
  3. Here's how you might generate a secure API key:

    openssl rand -base64 30

On the Slave server (ns2):

  1. Edit the PowerDNS configuration file:

    sudo nano /etc/powerdns/pdns.conf
  2. Add or modify these lines:

    launch=gmysql
    gmysql-host=localhost
    gmysql-user=pdns
    gmysql-dbname=pdns
    gmysql-password=your_secure_password
    gmysql-dnssec=yes
    slave=yes
    superslave=yes
  3. Add the master server as a supermaster:

    sudo mysql -u pdns -p pdns

    In the MySQL prompt:

    INSERT INTO supermasters (ip, nameserver, account) VALUES ('IP_OF_MASTER_SERVER', 'ns1.yourdomain.com', 'admin');
    EXIT;

Part 3: Firewall Configuration

On both servers, configure the firewall to allow only necessary traffic from trusted sources. We will allow port 53 from anywhere since setup is for public Authoritative Nameservers:

# Allow DNS traffic
sudo ufw allow 53/tcp
sudo ufw allow 53/udp

# Allow API traffic on port 8081 (restricted to specific IPs)
# Replace 192.0.2.1 with the actual IP address that needs access
sudo ufw allow from 192.0.2.1 to any port 8081 proto tcp

# If multiple IPs need access, add each one separately
# sudo ufw allow from 10.0.0.1 to any port 8081 proto tcp

# Allow SSH access only from specific jumphost/trusted IPs
# Replace 198.51.100.5 with your jumphost/management server IP
sudo ufw allow from 198.51.100.5 to any port 22 proto tcp

# Deny all other SSH connections
sudo ufw deny 22/tcp

# Enable the firewall
sudo ufw enable

# Verify the rules
sudo ufw status verbose

Part 4: Starting PowerDNS

On both servers:

sudo systemctl start pdns
sudo systemctl enable pdns

Part 5: Testing the Setup

  1. On the master server, create a test zone:

    sudo pdnsutil create-zone example.com ns1.example.com
    sudo pdnsutil add-record example.com www A 192.0.2.1
  2. Notify the slave:

    sudo pdns_control notify example.com
  3. On the slave, check if the zone transferred:

    sudo pdnsutil list-zone example.com

Part 6: DNSSEC Configuration

Enabling DNSSEC for a zone (on Master server):

  1. Secure a zone with DNSSEC:

    sudo pdnsutil secure-zone example.com
  2. CRITICAL: After securing a zone, you MUST increase the SOA serial for slave synchronization:

    sudo pdnsutil increase-serial example.com
    sudo pdns_control notify example.com
  3. Verify DNSSEC is working:

    sudo pdnsutil show-zone example.com
  4. Get DS records for parent zone delegation:

    sudo pdnsutil export-zone-ds example.com
  5. Test DNSSEC validation:

    dig DNSKEY example.com @ns1.yourdomain.com
    dig +dnssec SOA example.com @ns1.yourdomain.com

Important DNSSEC Behavior in Master-Slave Setup:

Master (ns1) behavior:

  • Generates DNSSEC signatures (RRSIG) on-the-fly during queries
  • Zone database contains only base records + DNSKEY records
  • No RRSIG records stored in database
  • Higher CPU usage but lower storage

Slave (ns2) behavior:

  • Receives pre-signed zone data via AXFR including all RRSIG records
  • Stores complete signed zone in database
  • Very fast query responses (no on-the-fly signing)
  • Higher storage usage but lower CPU

This is the preferred setup for master-slave configurations as it provides:

  • Load distribution (master handles signing, slave handles fast queries)
  • Redundancy (if master fails, slave continues serving signed responses)
  • Optimal performance (slave can serve high query volumes efficiently)

Restoring DNSSEC-enabled Zones

Complete zone restoration process:

  1. Restore zone data:

    pdnsutil load-zone example.com example.com.txt
  2. Import DNSSEC keys (if .key files exist):

    # Import each key file (key ID will be in filename)
    pdnsutil import-zone-key example.com example.com.key3
  3. Rectify the zone:

    pdnsutil rectify-zone example.com
  4. Increase serial for slave sync:

    pdnsutil increase-serial example.com
    pdns_control notify example.com
  5. Update DS records at your registrar:

    # Use content from example.com.ds file
    cat example.com.ds

    Copy the DS records and update them at your domain registrar.

  6. Verify restoration:

    pdnsutil show-zone example.com
    dig DNSKEY example.com @localhost
    dig +dnssec SOA example.com @localhost

Important DNSSEC Notes:

  • DS Records: After securing a zone, you must update the DS records at your domain registrar using the output from export-zone-ds
  • Serial Management: Always increase SOA serial after DNSSEC operations to ensure slave synchronization
  • Key Management: PowerDNS automatically handles key rotation and signing
  • Slave Synchronization: DNSSEC keys and signatures are automatically transferred to slave servers
  • Validation: Use online DNSSEC validators to verify your setup is working correctly
  • Backup Strategy: Always backup both zone data AND DNSSEC keys for complete restoration capability

Part 7: Implementing the Zone Management Script

  1. Install jq on the master server:

    sudo apt install jq -y
  2. Create the script file:

    sudo nano /usr/local/bin/pdns_zone_monitor.sh
  3. Paste the following content:

    #!/bin/bash
    
    # Configuration
    API_KEY="API_KEY"
    API_URL="http://IP_OF_MASTER_SERVER:8081"
    NS1="ns1.example.com"
    NS2="ns2.example.com"
    LOG_FILE="/var/log/pdns_zone_monitor.log"
    STATE_FILE="/var/log/pdns_processed_zones.txt"
    SERIAL_FILE="/var/log/pdns_zone_serials.txt"
    
    # Function to log messages
    log_message() {
        echo "$(date): $1" >> $LOG_FILE
    }
    
    # Function to get zone serial from API
    get_zone_serial() {
        local zone=$1
        curl -s -H "X-API-Key: $API_KEY" "$API_URL/api/v1/servers/localhost/zones/$zone" | jq -r '.serial // empty'
    }
    
    # Function to fix SOA and NS records for a zone
    fix_zone_records() {
        local zone=$1
        local serial=$(date +%Y%m%d%H)
        
        # Update SOA and NS records
        pdnsutil set-meta "$zone" SOA-EDIT-API INCREMENT-WEEKS
        pdnsutil set-kind "$zone" MASTER
        pdnsutil add-record "$zone" @ SOA "$NS1 hostmaster.$zone $serial 10800 3600 604800 3600"
        pdnsutil add-record "$zone" @ NS "$NS1"
        pdnsutil add-record "$zone" @ NS "$NS2"
        
        # Increase serial and notify slaves
        pdnsutil increase-serial "$zone"
        pdns_control notify "$zone"
        
        # Small delay to ensure changes are committed
        sleep 2
        
        # Trigger retrieval on ns2
        ssh root@ns2 "pdns_control retrieve $zone"
        
        log_message "Fixed records for zone $zone and triggered retrieval on ns2"
    }
    
    # Function to sync zone to ns2
    sync_zone_to_ns2() {
        local zone=$1
        
        # Notify and retrieve
        pdns_control notify "$zone"
        sleep 2
        ssh root@ns2 "pdns_control retrieve $zone"
        
        log_message "Synced zone $zone to ns2"
    }
    
    # Function to remove zone from ns2
    remove_zone_from_ns2() {
        local zone=$1
        ssh root@ns2 "pdnsutil delete-zone $zone"
        log_message "Removed zone $zone from ns2"
    }
    
    # Function to clean ns2 by removing zones not present on ns1
    clean_ns2() {
        log_message "Starting cleanup process on ns2"
        local primary_zones=$(pdnsutil list-all-zones | awk '{print $1}')
        local secondary_zones=$(ssh root@ns2 "pdnsutil list-all-zones" | awk '{print $1}')
        
        log_message "Primary zones on ns1: $primary_zones"
        log_message "Secondary zones on ns2: $secondary_zones"
        
        # Ensure the zones are compared correctly
        primary_zones_array=($primary_zones)
        secondary_zones_array=($secondary_zones)
        
        for zone in "${secondary_zones_array[@]}"; do
            if [[ ! " ${primary_zones_array[*]} " =~ " $zone " ]]; then
                log_message "Cleaning up zone $zone from ns2"
                remove_zone_from_ns2 "$zone"
            fi
        done
    }
    
    # Load processed zones
    if [ -f "$STATE_FILE" ]; then
        mapfile -t processed_zones < "$STATE_FILE"
    else
        touch "$STATE_FILE"
        processed_zones=()
    fi
    
    # Load zone serials
    declare -A zone_serials
    if [ -f "$SERIAL_FILE" ]; then
        while IFS='=' read -r zone serial; do
            zone_serials["$zone"]="$serial"
        done < "$SERIAL_FILE"
    fi
    
    # Get list of current zones
    current_zones=$(curl -s -H "X-API-Key: $API_KEY" "$API_URL/api/v1/servers/localhost/zones" | jq -r '.[].name')
    
    # Convert current_zones to an array
    current_zones_array=($current_zones)
    
    # Main loop
    while true; do
        # Get new list of zones
        new_zones=$(curl -s -H "X-API-Key: $API_KEY" "$API_URL/api/v1/servers/localhost/zones" | jq -r '.[].name')
        
        # Convert new_zones to an array
        new_zones_array=($new_zones)
        
        # Check for new zones and process them
        for zone in "${new_zones_array[@]}"; do
            if [[ ! " ${current_zones_array[*]} " =~ " $zone " ]] && [[ ! " ${processed_zones[*]} " =~ " $zone " ]]; then
                log_message "New zone detected: $zone"
                fix_zone_records "$zone"
                echo "$zone" >> "$STATE_FILE"
                processed_zones+=("$zone")
                
                # Store initial serial
                serial=$(get_zone_serial "$zone")
                if [ -n "$serial" ]; then
                    zone_serials["$zone"]="$serial"
                fi
            fi
        done
        
        # Check for serial number changes in existing zones
        for zone in "${new_zones_array[@]}"; do
            current_serial=$(get_zone_serial "$zone")
            if [ -n "$current_serial" ]; then
                stored_serial="${zone_serials[$zone]}"
                
                # If serial changed or no stored serial, sync the zone
                if [ -z "$stored_serial" ] || [ "$current_serial" != "$stored_serial" ]; then
                    log_message "Serial change detected for $zone: $stored_serial -> $current_serial"
                    sync_zone_to_ns2 "$zone"
                    zone_serials["$zone"]="$current_serial"
                fi
            fi
        done
        
        # Check for removed zones and process them
        for zone in "${current_zones_array[@]}"; do
            if [[ ! " ${new_zones_array[*]} " =~ " $zone " ]]; then
                log_message "Zone removed: $zone"
                remove_zone_from_ns2 "$zone"
                processed_zones=("${processed_zones[@]/$zone}")
                sed -i "\|$zone|d" "$STATE_FILE"
                unset zone_serials["$zone"]
            fi
        done
        
        # Save current serials to file
        > "$SERIAL_FILE"
        for zone in "${!zone_serials[@]}"; do
            echo "$zone=${zone_serials[$zone]}" >> "$SERIAL_FILE"
        done
        
        # Clean ns2 to remove any zones not present on ns1
        clean_ns2
        
        # Update current zones list
        current_zones_array=("${new_zones_array[@]}")
        
        # Wait for 1 minute before checking again
        sleep 60
    done

    If you use the ArkHost PowerDNS Manager for WHMCS, what should you do?
    Remove or Comment Out the Zone Record Additions:

    You should remove or comment out these lines from the fix_zone_records function, since the module handles initial zone records creation:
    # pdnsutil add-record "$zone" @ SOA "$NS1 hostmaster.$zone $serial 10800 3600 604800 3600"
    # pdnsutil add-record "$zone" @ NS "$NS1"
    # pdnsutil add-record "$zone" @ NS "$NS2"
  4. Make the script executable:

    sudo chmod +x /usr/local/bin/pdns_zone_monitor.sh
  5. Create a systemd service file:

    sudo nano /etc/systemd/system/pdns-zone-monitor.service
  6. Add the following content:

    [Unit]
    Description=PowerDNS Zone Monitor
    After=pdns.service
    
    [Service]
    ExecStart=/usr/local/bin/pdns_zone_monitor.sh
    Restart=always
    User=root
    
    [Install]
    WantedBy=multi-user.target
  7. Enable and start the service:

    sudo systemctl daemon-reload
    sudo systemctl enable pdns-zone-monitor
    sudo systemctl start pdns-zone-monitor

Part 8: Implementing the Zone Import Script

  1. Create the PowerDNS importer script:

    sudo nano /usr/local/bin/powerdns_importer.py
  2. Paste the following content:

    #!/usr/bin/env python3
    import os
    import sys
    import subprocess
    import logging
    
    # Set up logging
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    
    def run_command(command):
        try:
            result = subprocess.run(command, capture_output=True, text=True, check=True)
            return result.stdout.strip()
        except subprocess.CalledProcessError as e:
            logging.error(f"Error running command {' '.join(command)}. Error: {e.stderr.strip()}")
            return None
    
    def import_zone(zone_file):
        with open(zone_file, 'r') as f:
            lines = f.readlines()
    
        zone_name = None
        ttl = "3600"
    
        for line in lines:
            line = line.strip()
            if line.startswith('$ORIGIN'):
                zone_name = line.split()[1].rstrip('.')
                break
    
        if not zone_name:
            logging.error(f"Could not determine zone name from {zone_file}")
            return False
    
        try:
            # Delete the zone if it exists
            run_command(['pdnsutil', 'delete-zone', zone_name])
            logging.info(f"Deleted existing zone: {zone_name}")
        except:
            logging.warning(f"Zone {zone_name} did not exist, continuing with creation")
    
        # Create the zone
        run_command(['pdnsutil', 'create-zone', zone_name])
        logging.info(f"Created new zone: {zone_name}")
    
        for line in lines:
            line = line.strip()
            if not line or line.startswith('$ORIGIN'):
                continue
            if line.startswith('$TTL'):
                ttl = line.split()[1]
                continue
    
            parts = line.split(maxsplit=5)
            if len(parts) < 5:
                continue
    
            name, record_ttl, _, record_type, content = parts[:5]
            
            # Handle domain name correctly
            if name == zone_name or name == "@":
                name = "@"
            else:
                name = name.rstrip('.')
                if name.endswith(zone_name):
                    name = name[:-len(zone_name)-1]  # Remove zone name from the end
                if not name:
                    name = "@"
    
            if record_type == 'MX':
                if len(parts) == 6:
                    priority, mx = parts[4], parts[5]
                else:
                    priority, mx = "10", parts[4]
                content = f"{priority} {mx}"
            elif record_type == 'TXT':
                content = f'"{" ".join(parts[4:])}"'
            elif record_type in ['CNAME', 'NS', 'MX'] and not content.endswith('.'):
                content = f"{content}."
    
            result = run_command(['pdnsutil', 'add-record', zone_name, name, record_type, record_ttl, content])
            if result is not None:
                logging.info(f"Added {record_type} record: {name} {record_ttl} IN {record_type} {content}")
            else:
                logging.error(f"Failed to add record: {line}")
    
        # Rectify the zone
        run_command(['pdnsutil', 'rectify-zone', zone_name])
        logging.info(f"Rectified zone: {zone_name}")
    
        return True
    
    def sync_to_ns2(zone_name):
        try:
            run_command(['pdns_control', 'notify', zone_name])
            logging.info(f"Triggered notification for {zone_name} to ns2")
            return True
        except Exception as e:
            logging.error(f"Error syncing {zone_name} to ns2: {str(e)}")
            return False
    
    def main():
        if len(sys.argv) < 2:
            print("Usage: python powerdns_importer.py <input_directory>")
            sys.exit(1)
    
        input_directory = sys.argv[1]
        if not os.path.isdir(input_directory):
            logging.error(f"Error: {input_directory} is not a valid directory")
            sys.exit(1)
    
        for filename in os.listdir(input_directory):
            if filename.endswith('.txt'):
                zone_file = os.path.join(input_directory, filename)
                logging.info(f"Processing {zone_file}")
                if import_zone(zone_file):
                    sync_to_ns2(os.path.splitext(filename)[0])
    
    if __name__ == "__main__":
        main()
  3. Make the script executable:

    sudo chmod +x /usr/local/bin/powerdns_importer.py

Zone Migration from CloudFlare

If you're migrating from CloudFlare to PowerDNS, see our separate guide:
Migrating DNS Zones from CloudFlare to PowerDNS

This covers:

  • Exporting all zones from CloudFlare via API
  • Converting to PowerDNS-compatible format
  • Bulk import process using the PowerDNS importer script

Part 9: Implementing the Slave Update Script

  1. Create the update all slaves script:

    sudo nano /usr/local/bin/update_all_slaves.py
  2. Paste the following content:

    #!/usr/bin/env python3
    import subprocess
    import logging
    
    # Set up logging
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    
    def run_command(command):
        try:
            result = subprocess.run(command, capture_output=True, text=True, check=True)
            return result.stdout.strip()
        except subprocess.CalledProcessError as e:
            logging.error(f"Error running command {' '.join(command)}. Error: {e.stderr.strip()}")
            return None
    
    def update_all_slaves():
        try:
            # Get list of all zones
            zones_output = run_command(['pdnsutil', 'list-all-zones'])
            if zones_output is None:
                logging.error("Failed to get list of zones")
                return False
    
            zones = zones_output.split('\n')
    
            # Notify each zone
            for zone in zones:
                result = run_command(['pdns_control', 'notify', zone])
                if result is not None:
                    logging.info(f"Triggered notification for {zone} to slaves")
                else:
                    logging.warning(f"Failed to notify for zone: {zone}")
    
            logging.info("Completed notifications for all zones")
            return True
        except Exception as e:
            logging.error(f"Error updating all slaves: {str(e)}")
            return False
    
    if __name__ == "__main__":
        if update_all_slaves():
            logging.info("Successfully updated all slave servers")
        else:
            logging.error("Failed to update all slave servers")
  3. Make the script executable:

    sudo chmod +x /usr/local/bin/update_all_slaves.py

Part 10: Using the Scripts

  1. To monitor and manage zones automatically: The pdns_zone_monitor.sh script will run as a service, continuously monitoring for new zones and managing them.
  2. To import zones from text files:

    python3 /usr/local/bin/powerdns_importer.py /path/to/zone/files/directory
  3. To update all slave servers:

    python3 /usr/local/bin/update_all_slaves.py
  4. To enable DNSSEC for a zone:

    sudo pdnsutil secure-zone example.com
    sudo pdnsutil export-zone-ds example.com

Part 11: Monitoring and Maintenance

  1. Check the status of PowerDNS:

    sudo systemctl status pdns
  2. Monitor the zone management script:

    sudo journalctl -u pdns-zone-monitor -f
  3. View the script's log:

    tail -f /var/log/pdns_zone_monitor.log
  4. Regularly backup your PowerDNS database:

    sudo mysqldump pdns > pdns_backup_$(date +%Y%m%d).sql
  5. Check DNSSEC status for all zones:

    sudo pdnsutil check-all-zones

Part 12: Zone Backups with DNSSEC Support

Create the enhanced backup script that includes DNSSEC keys:

sudo nano /root/scripts/zone_backups.py
import subprocess
import os
import re
from datetime import datetime

def run_pdnsutil(command):
    result = subprocess.run(['pdnsutil'] + command.split(),
                          capture_output=True,
                          text=True)
    return result.stdout.strip()

# Get the current date and time
current_date = datetime.now().strftime("%Y-%m-%d")

# Get all zones
zones = run_pdnsutil('list-all-zones').split('\n')
zones = [zone.strip() for zone in zones if zone.strip()]

# Create output directory
output_dir = f'/root/backups/output_{current_date}'
os.makedirs(output_dir, exist_ok=True)

# Backup each zone
for zone in zones:
    print(f"Backing up zone {zone}")
    
    # Backup zone records
    content = run_pdnsutil(f'list-zone {zone}')
    with open(os.path.join(output_dir, f'{zone}.txt'), 'w') as f:
        f.write(content)
    
    # Check for DNSSEC keys
    zone_info = run_pdnsutil(f'show-zone {zone}')
    
    # Extract key IDs using regex
    key_ids = re.findall(r'ID = (\d+)', zone_info)
    
    if key_ids:
        print(f"  DNSSEC keys found for {zone}: {key_ids}")
        
        # Export each key
        for key_id in key_ids:
            key_content = run_pdnsutil(f'export-zone-key {zone} {key_id}')
            if key_content.strip():
                with open(os.path.join(output_dir, f'{zone}.key{key_id}'), 'w') as f:
                    f.write(key_content)
        
        # Export DS records
        ds_records = run_pdnsutil(f'export-zone-ds {zone}')
        if ds_records.strip():
            with open(os.path.join(output_dir, f'{zone}.ds'), 'w') as f:
                f.write(ds_records)

print(f"Backup complete. Files saved in {output_dir}.")

# Call the transfer and cleanup bash script
subprocess.run(['/root/scripts/transfer_cleanup.sh'], check=True)
print("Backup transfer and cleanup process initiated.")

Set up the cron job:

crontab -e

0 4 * * * /usr/bin/python3 /root/scripts/zone_backups.py

Restoring from DNSSEC Backups

To restore a zone with DNSSEC:

# Load zone
pdnsutil load-zone example.com example.com.txt

# Import DNSSEC key (if .key files exist)
pdnsutil import-zone-key example.com example.com.key3

# Rectify zone
pdnsutil rectify-zone example.com

# Update DS records at registrar using content from example.com.ds file

Your backup files will now include:

  • example.com.txt - DNS records
  • example.com.key3 - Private DNSSEC key
  • example.com.ds - DS records for parent zone

Was this answer helpful?

« Back

WHOIS Information

×
Loading WHOIS information...