# 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) 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 ``` 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 ``` Then edit their home directory in /etc/passwd so it looks something like ``` jaileduser:x:999:999::/home/jaileduser:/usr/sbin/nologin ``` 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/record/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