commit 2a24576fa6778ca2d5d01561815af928dd1aa300 Author: Tobias Date: Wed Mar 11 18:02:38 2026 +0000 Add README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d38e56 --- /dev/null +++ b/README.md @@ -0,0 +1,251 @@ +# 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