Skip to the content.

Security notes

← Back to README

VPS hardening checklist

After your VPS is up and the bot is working, lock it down. The order below matters: skipping the verification steps is the easiest way to lock yourself out of a fresh VPS.

[!WARNING] Keep your current SSH session open through the whole process. Verify each step from a new terminal window before closing the old one. If something breaks, the open session is your only way back in.

0. Verify SSH key auth works before touching anything

If you used the GitHub Actions deploy flow (see docs/auto-deploy.md), you already have a key. Confirm it works from a fresh terminal:

ssh -i ~/.ssh/your-deploy-key -o IdentitiesOnly=yes root@<VPS_IP>

Must log in without prompting for a password. If it asks for one or fails — stop, fix the key first.

1. Enable the firewall (allow SSH first!)

sudo apt update && sudo apt install -y ufw

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw status numbered
sudo ufw enable

The bot has no inbound ports — only OpenSSH is needed.

2. Disable password auth (key-only SSH)

sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
sudo sed -i 's/^#*KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config

sudo grep -E '^(PasswordAuthentication|PermitRootLogin|KbdInteractive|ChallengeResponse)' /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl restart sshd

Use PermitRootLogin prohibit-password (not no) so the GitHub Actions deploy can still log in as root via key. If you’re running deploys as a non-root user, set this to no.

3. Verify (from a new terminal — don’t close the existing one!)

ssh -i ~/.ssh/your-deploy-key -o IdentitiesOnly=yes root@<VPS_IP> "echo OK"

ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no root@<VPS_IP>
# expected: "Permission denied (publickey)"

Only after both checks pass, close the original SSH session.

4. Automatic security updates

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

Default config blocks an IP after 5 failed SSH attempts. Tune in /etc/fail2ban/jail.conf if needed.

6. Switch from root to a deploy user (optional, more proper)

If you’d rather not run deploys as root, create a non-privileged user:

sudo adduser deploy
sudo usermod -aG sudo,docker deploy
sudo mkdir -p /home/deploy/.ssh
sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh && sudo chmod 600 /home/deploy/.ssh/authorized_keys
echo 'deploy ALL=(ALL) NOPASSWD: /usr/bin/docker' | sudo tee /etc/sudoers.d/deploy

Then move the project from /root/obsidian-telegram-agent to /home/deploy/obsidian-telegram-agent, update VPS_USER=deploy in GitHub Secrets, and tighten PermitRootLogin no in /etc/ssh/sshd_config.


After all steps the VPS has zero inbound surface beyond SSH, accepts only key-based auth, blocks brute-force attempts, and pulls security patches automatically.