Skip to content

Instantly share code, notes, and snippets.

@sprytnyk
Last active April 3, 2026 12:06
Show Gist options
  • Select an option

  • Save sprytnyk/c0e72172354f9481d5c183905dc9dcb5 to your computer and use it in GitHub Desktop.

Select an option

Save sprytnyk/c0e72172354f9481d5c183905dc9dcb5 to your computer and use it in GitHub Desktop.
#!/bin/bash
# VPN bootstrap script for Debian 12
# Step 1: System preparation and Unbound DNS setup
set -eu
trap 'echo "❌ Error on line $LINENO"; exit 1' ERR
echo "🚀 Starting VPN bootstrap setup..."
# Ensure sudo exists and update system
echo "🔧 Updating system and installing base packages..."
apt-get update
apt-get install -y sudo
apt-get upgrade -y
# Install necessary tools (ufw excluded — firewall managed via raw iptables)
apt-get install -y make git pwgen htop lnav wget curl openssl rsync \
gnupg locales unbound fail2ban
# Configure locale (en_GB)
echo "🌐 Configuring locales..."
sed -i '/en_GB.UTF-8/s/^# //' /etc/locale.gen
locale-gen
update-locale LANG=en_GB.UTF-8
# Create a user and add to sudo group
echo "👤 Enter your desired username:"
read -r USER_NAME
# Guard against empty username
if [[ -z "${USER_NAME}" ]]; then
echo "❌ Username cannot be empty."
exit 1
fi
if id "${USER_NAME}" &>/dev/null; then
echo "⚠️ User '${USER_NAME}' already exists, skipping creation."
else
adduser --disabled-password --gecos "" "${USER_NAME}"
usermod -aG sudo "${USER_NAME}"
echo "✅ User '${USER_NAME}' created and added to sudo group."
fi
# Set secure password — written to user home only (never echoed to terminal)
USER_PASSWORD="$(pwgen -r ',;' -s 25 -y)"
USER_CREDS_FILE="/home/${USER_NAME}/credentials.txt"
echo "Password: ${USER_PASSWORD}" > "${USER_CREDS_FILE}"
chown "${USER_NAME}:${USER_NAME}" "${USER_CREDS_FILE}"
chmod 600 "${USER_CREDS_FILE}"
echo "${USER_NAME}:${USER_PASSWORD}" | chpasswd
echo "🔐 Credentials saved to ${USER_CREDS_FILE}"
echo " Retrieve locally with: scp ${USER_NAME}@<vps-ip>:~/credentials.txt ./credentials.txt"
# Sync SSH keys from root to new user
echo "🔐 Syncing SSH keys from root to /home/${USER_NAME}/.ssh..."
rsync --archive --chown="${USER_NAME}:${USER_NAME}" ~/.ssh "/home/${USER_NAME}"
# Verify authorized_keys exists before hardening SSH
# (prevents lockout if key sync failed)
if [ ! -f "/home/${USER_NAME}/.ssh/authorized_keys" ]; then
echo "❌ No authorized_keys found for '${USER_NAME}' — aborting SSH hardening to prevent lockout!"
exit 1
fi
echo "✅ authorized_keys verified for '${USER_NAME}'."
# Harden SSH configuration
echo "🔒 Configuring SSH..."
sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config
if grep -q "^#\?PasswordAuthentication" /etc/ssh/sshd_config; then
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
else
echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
fi
if grep -q "^#\?PermitEmptyPasswords" /etc/ssh/sshd_config; then
sed -i 's/^#\?PermitEmptyPasswords.*/PermitEmptyPasswords no/' /etc/ssh/sshd_config
else
echo "PermitEmptyPasswords no" >> /etc/ssh/sshd_config
fi
rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf
systemctl restart ssh
# Setup /etc/hosts entry for Unbound
echo "📘 Adding Unbound to /etc/hosts..."
if ! grep -q "127.1.1.2 unbound" /etc/hosts; then
echo -e "\n# Local DNS resolvers\n127.1.1.2 unbound" >> /etc/hosts
fi
# Download root hints file
echo "🌍 Downloading root hints file..."
wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
# Configure Unbound
echo "🛠️ Configuring Unbound DNS resolver..."
cat > /etc/unbound/unbound.conf.d/pi-hole.conf <<EOF
server:
verbosity: 3
interface: 127.1.1.2
port: 5353
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
prefer-ip6: no
root-hints: "/var/lib/unbound/root.hints"
# Deny all by default, then allow loopback only
access-control: 0.0.0.0/0 deny
access-control: ::0/0 deny
access-control: 127.0.0.0/8 allow
access-control: ::1 allow
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
edns-buffer-size: 1232
prefetch: yes
num-threads: 1
so-rcvbuf: 1m
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
EOF
# Ensure dnsmasq doesn't break EDNS
mkdir -p /etc/dnsmasq.d
echo 'edns-packet-max=1232' > /etc/dnsmasq.d/99-edns.conf
# Start Unbound and wait properly (no fragile sleep)
echo "🔁 Restarting Unbound..."
systemctl restart unbound
if ! systemctl is-active --quiet unbound; then
echo "❌ Unbound failed to start. Check: journalctl -u unbound"
exit 1
fi
echo "✅ Unbound is running."
# Setup Unbound logging
mkdir -p /var/log/unbound
touch /var/log/unbound/unbound.log
chown unbound /var/log/unbound/unbound.log
# Verification — also validate DNSSEC behaviour
echo "🔎 Running test DNS queries..."
dig pi-hole.net @127.1.1.2 -p 5353
echo "🔎 Testing DNSSEC validation..."
if dig sigfail.verteiltesysteme.net @127.1.1.2 -p 5353 | grep -q "SERVFAIL"; then
echo "✅ DNSSEC rejection working correctly (SERVFAIL as expected)"
else
echo "⚠️ WARNING: DNSSEC rejection test did not return SERVFAIL — check Unbound config"
fi
if dig sigok.verteiltesysteme.net @127.1.1.2 -p 5353 | grep -q "NOERROR"; then
echo "✅ DNSSEC validation working correctly (NOERROR as expected)"
else
echo "⚠️ WARNING: DNSSEC validation test did not return NOERROR — check Unbound config"
fi
# Configure fail2ban for SSH protection
echo "🛡️ Configuring fail2ban for SSH..."
cat > /etc/fail2ban/jail.d/ssh.conf <<EOF
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
findtime = 300
bantime = 3600
EOF
systemctl enable fail2ban
systemctl restart fail2ban
echo "✅ fail2ban configured and running."
# Show Unbound status
systemctl status unbound.service --no-pager
echo ""
echo "✅ VPN bootstrap setup completed successfully."
echo "📋 Next steps:"
echo " 1. scp credentials to local machine: scp ${USER_NAME}@<vps-ip>:~/credentials.txt ./credentials.txt"
echo " 2. Delete from server after saving to password manager: rm ${USER_CREDS_FILE}"
echo " 3. Apply iptables rules (your separate iptables script)"
echo " 4. Install iptables-persistent to persist rules across reboots"
echo " 5. Install and configure Pi-hole pointing to 127.1.1.2:5353"
@sprytnyk
Copy link
Copy Markdown
Author

sprytnyk commented Jan 11, 2022

2nd step, apply rules

#!/usr/bin/env sh

# iptables ruleset for Debian 12 VPS running OpenVPN + Pi-hole + Unbound
# Apply with: sh iptables.sh
# Requires: iptables-persistent (apt-get install iptables-persistent netfilter-persistent)

set -eu

echo "🚀 Applying iptables rules..."

# ---------------------------------------------------------------------------
# IPv4
# ---------------------------------------------------------------------------

# Use iptables-restore for atomic application — avoids the brief window
# where rules are flushed but new ones not yet in place
iptables-restore --noflush <<'EOF'
*filter

# Default policies
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# --- Loopback ---
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT

# --- Block invalid packets early ---
-A INPUT -m state --state INVALID -j DROP
-A FORWARD -m state --state INVALID -j DROP

# --- Allow established/related ---
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT

# --- ICMP (ping) — rate limited to avoid amplification ---
-A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/s --limit-burst 10 -j ACCEPT
-A INPUT -p icmp --icmp-type echo-request -j DROP

# --- OpenVPN (TCP + UDP on 1194) ---
-A INPUT -p tcp --dport 1194 -j ACCEPT
-A INPUT -p udp --dport 1194 -j ACCEPT

# --- SSH: rate limiting handled by fail2ban ---
-A INPUT -p tcp --dport 22 -j ACCEPT

# --- Block external DNS (eth0) — must come before VPN DNS rules ---
# Prevents internet scanners from reaching dnsmasq/Pi-hole
-A INPUT -i eth0 -p udp --dport 53 -j DROP
-A INPUT -i eth0 -p tcp --dport 53 -j DROP

# --- VPN clients: DNS, HTTP and HTTPS to Pi-hole admin ---
-A INPUT -i tun0 -p udp --dport 53 -j ACCEPT
-A INPUT -i tun0 -p tcp --dport 53 -j ACCEPT
-A INPUT -i tun0 -p tcp --dport 80 -j ACCEPT
-A INPUT -i tun0 -p tcp --dport 443 -j ACCEPT

# --- Unbound local resolver (loopback only) ---
-A INPUT -i lo -p udp --dport 5353 -j ACCEPT
-A INPUT -i lo -p tcp --dport 5353 -j ACCEPT

# --- Block direct HTTP/HTTPS from internet (only accessible via VPN) ---
-A INPUT -i eth0 -p tcp --dport 80 -j REJECT --reject-with tcp-reset
-A INPUT -i eth0 -p tcp --dport 443 -j REJECT --reject-with tcp-reset
-A INPUT -i eth0 -p udp --dport 443 -j REJECT --reject-with icmp-port-unreachable

# --- FORWARD: block VPN clients from bypassing Pi-hole DNS ---
-A FORWARD -i tun0 -o eth0 -p udp --dport 53 -j DROP
-A FORWARD -i tun0 -o eth0 -p tcp --dport 53 -j DROP

# --- FORWARD: allow VPN clients HTTP/HTTPS to internet ---
-A FORWARD -i tun0 -o eth0 -p tcp --dport 80 -j ACCEPT
-A FORWARD -i tun0 -o eth0 -p tcp --dport 443 -j ACCEPT

# --- FORWARD: allow VPN clients general outbound (NEW + established) ---
-A FORWARD -i tun0 -o eth0 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT

COMMIT

*nat

:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]

# NAT for VPN client traffic
-A POSTROUTING -o eth0 -j MASQUERADE

COMMIT
EOF

echo "✅ IPv4 rules applied."

# ---------------------------------------------------------------------------
# IPv6 — block everything (VPN stack is IPv4 only)
# ---------------------------------------------------------------------------

ip6tables-restore --noflush <<'EOF'
*filter

:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Allow loopback
-A INPUT -i lo -j ACCEPT

# Allow established/related
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

# Block external DNS on IPv6 as well
-A INPUT -p udp --dport 53 -j DROP
-A INPUT -p tcp --dport 53 -j DROP

# Drop everything else inbound (VPN is IPv4-only)
-A INPUT -j DROP
-A FORWARD -j DROP

COMMIT
EOF

echo "✅ IPv6 rules applied."

# ---------------------------------------------------------------------------
# Persist rules
# ---------------------------------------------------------------------------

mkdir -p /etc/iptables
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6

echo "💾 Rules saved to /etc/iptables/rules.v4 and rules.v6"
echo "✅ iptables rules applied and saved successfully."

install iptables-persistent

apt install -y iptables-persistent

3rd step

$ wget https://git.io/vpn -O openvpn-install.sh && bash openvpn-install.sh # choose your static, public IP address
$ curl -sSL https://install.pi-hole.net | bash  # choose your tun0 interface
$ vim /etc/openvpn/server/server.conf
#and
## and replace `push "dhcp-option DNS 8.8.4.4"` with `push "dhcp-option DNS 10.8.0.1"`

@sprytnyk
Copy link
Copy Markdown
Author

sprytnyk commented Jan 11, 2022

4th step

#!/usr/bin/env bash
# Sets up a systemd timer to download Unbound root hints monthly

set -eu
trap 'echo "❌ Error on line $LINENO"; exit 1' ERR

echo "🌍 Setting up root hints update timer..."

# Create the update script
mkdir -p /root/.scripts
cat > /root/.scripts/update-hints.sh <<'EOF'
#!/usr/bin/env sh
# Download root hints
wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
EOF
chmod 700 /root/.scripts/update-hints.sh

# Create the systemd service
cat > /lib/systemd/system/hints.service <<'EOF'
[Unit]
Description=Download Unbound root hints

[Service]
Type=oneshot
ExecStart=/root/.scripts/update-hints.sh
PrivateTmp=true
EOF

# Create the systemd timer
cat > /lib/systemd/system/hints.timer <<'EOF'
[Unit]
Description=Download root hints monthly

[Timer]
OnCalendar=monthly
RandomizedDelaySec=2h
Persistent=true

[Install]
WantedBy=timers.target
EOF

# Reload systemd, enable and start the timer, restart Unbound
systemctl daemon-reload
systemctl restart unbound
systemctl enable hints.timer
systemctl start hints.timer

# Verify
systemctl status hints.timer --no-pager

echo "✅ Root hints timer set up successfully."

@sprytnyk
Copy link
Copy Markdown
Author

sprytnyk commented Jan 11, 2022

to set pihole password

pihole setpassword

@sprytnyk
Copy link
Copy Markdown
Author

sprytnyk commented Jun 9, 2025

Enable forwarding in /etc/sysctl.conf with setting net.ipv4.ip_forward=1
REBOOT THE SYSTEM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment