Raspberry Pi Headless Server from Scratch
WHY
Since ISP (Internet Service Provider) started to change public IPs at will, I wanted to have sort of a control center network appliance that would monitor things like Public IP changes and possibly other things (like speed tests, checking if certain external sites are up, etc.) utilizing existing, unused, and, probably soon, outdated hardware.
WHAT
- Raspberry Pi 1 Model B+ Board
- Clear acrylic case for RPi Model B+
- 8GB SD card
- Old iPhone 1A adapter (works fine for Model 1b)
- Micro USB to USB Cable
- RJ45 Ethernet cable
HOW
Prepare RPi SD Card
- Download Raspberry Pi OS Lite (at the time of writing Kernel version 5.00 released on March 4th 2021)
- Download Balena Etcher or similar software to copy image to SD card (Raspberry Pi Imager did not work for me but was my first choice)
- Plug SD Card into your Windows / OSX / Linux host machine and copy downloaded image to SD Card
- Navigate to the SD Card (you might have more than one drive mounted / available under windows) and create an empty file named ssh (this will ensure that SSH server on headless Raspberry Pi (RPi) will start on the first boot)
First Time Setup
- On windows I have download and installed wonderful Cygwin that provides a large collection of GNU and Open Source tools to provide functionality similar to a Linux distribution on windows. If using Windows 10, you might want to look into much heavier but “out of box” option called WSL2.
- Unmount SD Card and plug it into RPi (via SD adapter if required), plug in RJ45 for wired internet connection (other side should go to your router), and then plug in Power cable.
- RPi lights will be lit.
- Identify an IP address of newly built RPi using either your Router’s DHCP list of IPs – RPi MAC addresses would start with B8-27-EB or use one of the utilities like Adafruit Pi Finder, nmap, Angry IP Scanner (try older version with a single executable ipscan.exe), or just type in something like “ping -4 raspberrypi.local” at the command prompt.
Sample Naming
- RPi hostname: raspberry ==> headless_horseman
- RPi IP Address: 192.168.1.123
- SSH Port: 22 ==> 7522
- Sample Sudo User / Pass: mysudouser / Pwd.926!
- Sample SSH User / Pass: mysshuser / T3n.F0ur!
- SSH Public key file on Host machine: ~/.ssh/id_rsa-headless_horseman.pub
First Time Login
-
Using Cygwin or WSL2 (from windows) or Terminal (on OSX / Linux), connect to RPi (default user “pi” with default password “raspberry”):
user@host $ ssh pi@192.168.1.123 -
To enable SSH for the future boots one can use “sudo raspi-config” or via command line:
pi@192.168.1.123 $ sudo systemctl enable ssh # enable ssh -
Change default “pi” password (optionally - user “pi” can be removed in the future if another user is created):
pi@192.168.1.123 $ passwd -
Change default hostname:
pi@192.168.1.123 $ sudo vi /etc/hostname -
To add users:
pi@192.168.1.123 $ sudo adduser mysshuser #add user for non-sudo SSH access
pi@192.168.1.123 $ sudo adduser mysudouser sudo #add user for sudo non-SSH access
pi@192.168.1.123 $ sudo usermod -aG sudo mysudouser #OPTIONAL: add user to sudo group (covered by adduser with ‘sudo’ group name at the end)
Remove ability for ssh user to browse sudo user folder:
pi@192.168.1.123 $ sudo chmod 0750 /home/mysudouser
- Modify SSH Config to Allow / deny certain users and change default port (append at the end):
pi@192.168.1.123 $ sudo vi /etc/ssh/sshd_config
Port 7522 # Change Port to 7522 from default 22
AllowUsers mysshuser # allow SSH
DenyUsers mysudouser # sudo user to be denied SSH
PermitRootLogin=no # do not allow root login via ssh
SSH Server needs to be restarted for changes to take effect. Status can be queried as well:
pi@192.168.1.123 $ sudo systemctl restart ssh
pi@192.168.1.123 $ sudo systemctl status ssh
Setup Host machine for Passwordless SSH login
- In terminal in the host machine (cygwin, wsl2, terminal, etc.) key needs to be available or generated (accept default empty value for passphrase by pressing ENTER):
user@host $ mkdir -p ~/.ssh && cd ~/.ssh
user@host $ ssh-keygen -f id_rsa-headless_horseman
- Copy public key to RPi (target is ~/.ssh/authorized_keys):
user@host $ ssh-copy-id -i ~/.ssh/id_rsa-headless_horseman -p 7522 mysshuser@192.168.1.123
- Create configuration file for SSH to avoid supplying parameters every time:
user@host $ vi ~/.ssh/config
Host headless_horseman
HostName 192.168.1.123
User mysshuser
Port 7522
IdentityFile ~/.ssh/id_rsa-headless_horseman
Connect to RPi:
user@host $ ssh headless_horseman
mysshuser@headless_horseman $ su - mysudouser
Delete pi user
mysudouser@headless_horseman $ sudo userdel -r pi
Create scripts folder:
mysudouser@headless_horseman $ mkdir -p ~/scripts
Install fail2ban
mysudouser@headless_horseman $ sudo apt-get update
mysudouser@headless_horseman $ sudo apt-get -y
install fail2ban
mysudouser@headless_horseman $ sudo vi /etc/fail2ban/jail.local
[ssh]
enabled = true
port = 7522
filter = sshd
logpath = /var/log/auth.log
bantime = 900
banaction = iptables-allports
findtime = 900
maxretry = 3
mysudouser@headless_horseman $ sudo systemctl restart fail2ban
mysudouser@headless_horseman $ sudo iptables -L -n --line #check banned addresses
mysudouser@headless_horseman $ sudo iptables -D fail2ban-ssh 1 #unban line number 1
mysudouser@headless_horseman $ ln -s -T /var/log/fail2ban.log ~/scripts/fail2ban.log
Modify prompt and ls alias
mysudouser@headless_horseman $ vi ~/.profile
export PS1="\[\033[0;33m\][\D{%Y-%m-%d}][\t]\[\033[0;36m\] [\u\[\033[0;37m\]@\[\033[0;36m\]\h:\[\033[0;32m\]\w]\[\033[0m\] \n$ "
alias ls='ls -laFh --color=auto'
Script: update.sh
mysudouser@headless_horseman $ vi ~/scripts/update.sh
#!/bin/bash
sudo apt update;
sudo apt -y upgrade;
sudo apt -y autoclean;
sudo apt -y autoremove;
sudo shutdown -r
mysudouser@headless_horseman $ chmod +x ~/scripts/update.sh
mysudouser@headless_horseman $ sudo crontab -e
30 23 * * WED /bin/bash /home/mysudouser/scripts/update.sh
Script: myutil.py
mysudouser@headless_horseman $ vi ~/scripts/myutil.py && chmod +x ~/scripts/myutil.py
#!/usr/bin/env python3
def getip(networkCardName):
import subprocess
return subprocess.getoutput("ip addr show " + networkCardName + " | grep -Po 'inet \K[\d.]+'")
def getips():
import re
import subprocess
# get ip(s) from ifconfig
found_ips = []
ips = re.findall( r'[0-9]+(?:\.[0-9]+){3}', subprocess.getoutput("/sbin/ifconfig"))
for ip in ips:
if ip.startswith("255") or ip.startswith("127") or ip.endswith("255"):
continue
found_ips.append(ip)
return ", ".join(found_ips)
def emailsend(subjectPart, messagePart):
import time
import smtplib
import socket
from datetime import datetime
current_utc = datetime.utcnow().isoformat() + 'Z'
####--[CONFIGURATION]
server = 'smtp.gmail.com'
server_port = '587'
username = 'MYEMAIL@gmail.com'
password = 'MYGMAILPA$$'
fromaddr = 'DEVOPS <MYEMAIL@gmail.com>'
toaddr = 'TARGET_EMAIL@domain.com'
subject = 'DEVOPS | ' + socket.gethostname() + ' | ' + subjectPart
message = 'DEVOPS | ' + socket.gethostname() + ' | <br>' + messagePart + '<br><br><br>Email Generated On: ' + current_utc
####--[/CONFIGURATION]
headers = [
"Subject: " + subject,
"From: " + fromaddr,
"To: " + toaddr,
"MIME-Version: 1.0",
"Content-Type: text/html"
]
headers = "\r\n".join(headers)
server = smtplib.SMTP(server + ":" + server_port)
server.ehlo()
server.starttls()
server.ehlo()
server.login(username,password)
server.sendmail(fromaddr, toaddr, headers + "\r\n\r\n" + message)
server.quit()
def get_script_path():
import os
import sys
return os.path.dirname(os.path.realpath(sys.argv[0]))
Script: ipcheck.py
mysudouser@headless_horseman $ vi ~/scripts/ipcheck.py && chmod +x ~/scripts/ipcheck.py
#!/usr/bin/env python3
import json, os
import urllib.request
import myutil
from datetime import date
today = date.today()
formatted_date = today.strftime("%Y%m%d")
script_dir = myutil.get_script_path()
ipcheck_url = "https://mydomain.com/ipcheck.php"
ipcheck_log = script_dir + "/ipcheck_changes-py.log"
lastip_filename = script_dir + "/ipcheck_lastip-py.txt"
lastip = "unknown"
if os.path.isfile(lastip_filename):
with open(lastip_filename, 'r') as f:
lastip = f.read()
data = json.loads(urllib.request.urlopen(ipcheck_url).read())
currentip = data["ip"]
if lastip != currentip:
with open(lastip_filename, 'w') as f:
f.write(currentip)
with open(ipcheck_log, 'a') as f:
f.write(formatted_date + " " + currentip + '\n')
subject = "Public IP address change"
message = "Old IP: " + lastip + " / New IP: " + currentip
myutil.emailsend(subject, message)
Create a script on external web site (mydomain.com) named ipcheck.php as follows
<?php
header('Content-Type: application/json');
$ip=$_SERVER["REMOTE_ADDR"];
$arr=array( "ip" => $ip );
echo json_encode($arr);
?>
mysudouser@headless_horseman $ sudo crontab -e
*/1 * * * * /usr/bin/python3 /home/mysudouser/scripts/ipcheck.py
Script: i_am_alive_email.py
mysudouser@headless_horseman $ vi ~/scripts/i_am_alive_email.py && chmod +x ~/scripts/i_am_alive_email.py
#!/usr/bin/env python3
import time
import myutil
# wait for interface to initialize before parsing ifconfig
time.sleep(1)
subject = "IP address(es)"
# ifconfig will show networkCardName
networkCardName = "eth0"
message = myutil.getip(networkCardName)
myutil.emailsend(subject, message)
mysudouser@headless_horseman $ sudo crontab -e
33 03 * * * /usr/bin/python3 /home/mysudouser/scripts/i_am_alive_email.py