Last active
April 3, 2026 12:06
-
-
Save sprytnyk/c0e72172354f9481d5c183905dc9dcb5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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" |
Author
Author
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."
Author
to set pihole password
pihole setpassword
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
2nd step, apply rules
install iptables-persistent
apt install -y iptables-persistent3rd step