252 lines
8.1 KiB
Markdown
252 lines
8.1 KiB
Markdown
# 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
|