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