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

sprytnyk commented Jun 9, 2025

Copy link
Copy Markdown
Author

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