Add README.md
This commit is contained in:
251
README.md
Normal file
251
README.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user