Dynamic DNS written with Bash
Motivation
Due to hosting my Nginx webserver at home my IP is subject to change as my ISP does not afford me a static one
Said webserver hosts tobiastime.xyz and the pages you are currently viewing
Initially I wanted to create a Bash script to interact with Namecheap's API allowing me to automatically update my A record when needs be
However Namecheap charges $50 to interact with their API, and I have heard even if you cough up the money it is very poor and limited
Consequently I began to run my own nameservers (ns1/2.tobiastime.xyz) on headless Debian, with PowerDNS, for full autonomy and control over my domain utilizing PowerDNS as the backend
Dependencies
- SSH/SFTP
- PowerDNS
- Bash
Prerequisites
For security purposes all of my standard ssh keys are password protected
However persistently storing the password to a protected key in non-volatile memory and allowing it to be used for automated scripts is a difficult and risky endeavour
Consequently I generated a new SSH key without password protection and linked it to a user with nologin shell
Said user is appropriately named jaileduser and their sole purpose is to transfer the public IP of my Nginx server to my master nameserver
Set up on the nameserver
Create jaileduser as a system user
useradd -r -s /usr/sbin/nologin jaileduser
If it hasn't been automatically created, then create jaileduser's home directory and give root ownership
Root must be given ownership due to chroot modifications we will make in the SSH config
mkdir /home/jaileduser
chown root:root /home/jaileduser
Create a subdirectory within jaileduser's home directory where the file storing the public IP of the Nginx server will reside
And give jaileduser only read/execute permissions for the directory (no write so they cannot create more files)
mkdir /home/jaileduser/nginx
chown jaileduser:jaileduser /home/jaileduser/nginx
chmod 500 /home/jaileduser/nginx
Create the file within that directory with which the public IP of the Nginx server will be stored
Then give jaileduser read and write permissions for the file so it can be modified via SFTP
Then create a file storing the current IP within the records of PowerDNS
touch /home/jaileduser/nginx/homeip
chmod 600 /home/jaileduser/nginx/homeip
touch /home/jaileduser/nginx/recordip
echo "9.8.7.6 (use the actual ip)" > /home/jaileduser/nginx/recordip
Lastly we generate the SSH key for jaileduser so it can be used to authenticate and transfer files using sftp
And create the appropriate files within jaileduser's home directory
The private key will need to be transfered and stored onto the Nginx server
mkdir /home/jaileduser/.ssh
touch /home/jaileduser/.ssh/authorized_keys
ssh-keygen -f jailedkey
cat jailedkey.pub > /home/jaileduser/.ssh/authorized_keys
rm jailedkey.pub
Due to the user having no login shell if the less secure key were ever compromised it would not provide an attack vector for remote code execution on the server
However a user with nologin shell is not able to be used to transfer files with sftp without first making some modifications to the sshd configuration file
Changes to be made in /etc/ssh/sshd_config:
Match User jaileduser
ChrootDirectory /home/jaileduser
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no
In reference to the above changes
-
Match user applies the following configuration changes only to jaileduser
-
ChrootDirectory sets their root directory to their home directory when connected via ssh so they will not be able to navigate or view directories outside of it
-
ForceCommand internal-sftp stops the user starting an interactive shell or executing any other commands over ssh
-
The rest are standardized deny permissions to further bolster security and prevent jaileduser bypassing the restrictions in place
With all of this done the preliminary steps are complete and we can place the scripts on the Nginx server and nameserver
Script ran on Nginx server to send public IP to nameserver:
#!/bin/bash
#define path to private key
keypath=/path/to/jailed/key
#define where you want your publicip file to be stored on the Nginx server
thetext=/home/user/publicip
ip1=$(curl icanhazip.com 2>/dev/null)
#backup for if curl icanhazip.com fails utilizing dig and opendns
backupcheck() {
local ip2=$(dig +short myip.opendns.com @resolver1.opendns.com) 2>/dev/null \
&& echo "$ip2" > "$thetext"
}
#waiting 60 seconds before trying the dig fall back method
#this is in case the initial failure was due to a network blip
[ -z "$ip1" ] && sleep 60 && backupcheck || echo "$ip1" > "$thetext"
#update scp to use the actual remote ip of your nameserver
#the -z check paired with the logical OR does nothing if both curl and dig failed to return an ip
[ -z "$(cat "$thetext")" ] || scp -i "$keypath" "$thetext" jaileduser@1.2.3.4:/nginx/homeip
Script ran on master nameserver to update DNS records:
#!/bin/bash
homeip=/home/jaileduser/nginx/homeip
recordip=/home/jaileduser/nginx/recordip
#define where you want the bashddns log to live
bashddnslog=/var/log/bashddns.log
changetime() {
local newip=$(cat "$homeip")
#here i use @ because i want to update the root of my domain and a TTL of 3600 (standard for A records)
pdnsutil replace-rrset yourdomain.name @ A 3600 "$newip"
#increasing the serial number in the SOA so the slave nameserver is notified of changes to the domain
pdnsutil increase-serial yourdomain.name
#updating the record ip file with the new ip
cat "$homeip" > "$recordip"
#the following last line is optional
#it uses ssmtp to send an email notification when changes to the A record are made
#ssmtp or any other command line compatible mail sending utility will work
echo -e "Subject: domain.name updated A record\nYour home ip has changed!" | ssmtp mail@site.name
}
#though we made sure on the Nginx server not to send the file if it was empty
#we will implement a double check here for redundancy
#only run the following *if* homeip is not empty
if ! [ -z "$(cat "$homeip")" ] ; then
#if homeip/recordip files are the same do nothing, else run changetime
#log all actions to a centralized file for auditing
diff "$homeip" "$recordip" && \
echo "A record matches homeip at $(date -u) no action taken" >> "$bashddnslog" \
&& exit 0 \
|| changetime && echo "changed A record at $(date -u) to "$(cat "$recordip")"" >> "$bashddnslog"
else
echo "no action taken due to homeip file being empty at $(date -u)" >> "$bashddnslog"
fi
Scheduling automatic execution
Finally we need to schedule these scripts to automatically run using cronjobs
I will configure the DDNS to run its check once every hour matching up with the 3600 TTL
This means the public ip of the Nginx server at home will be checked and the record will be changed if needed every hour
I have made a 2 minute gap between when the IP sending script runs on the Nginx server and when the record updating script runs on the nameserver
This is to afford time for transferring the files and in case the dig fall back method runs, which first waits for 60 seconds
By default without further permission modifications root must run the script on the nameserver as root is required to modify the SQLite3 database PowerDNS utilizes
In /etc/crontab for the Nginx server place:
0 * * * * youruser /path/to/ip/sending/script
On the master nameserver in /etc/crontab place:
2 * * * * root /path/to/bashddns/script
Rotating logs
Assuming your system has logrotate installed (as most modern distros do
And you don't want a giant wall of text singular log file for all bashddns actions on the master nameserver
A log rotate entry can be made as follows:
touch /etc/logrotate.d/bashddns
nano /etc/logrotate.d/bashddns
Insert these configuration options:
/var/log/bashddns.log {
rotate 7
daily
compress
missingok
notifempty
create 644 youruser youruser
}
This will keep logs for a week, rotate them daily, compress old ones, and ensure the log file is readable and writebale by your user