From 126a19e2828f52b2a510e107ef66a9ef1d1e88cf Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 15 Jun 2026 17:50:00 +0800 Subject: [PATCH] feat(security): add SSH hardening, fail2ban tasks, connection check helper, and doc --- docs/tldr-ssh-security.md | 108 +++++++++++++++++++++++ roles/vhosts/common/files/ssh_check.exp | 45 ++++++++++ roles/vhosts/common/handlers/main.yml | 10 +++ roles/vhosts/common/tasks/fail2ban.yml | 32 +++++++ roles/vhosts/common/tasks/harden_ssh.yml | 32 +++++++ roles/vhosts/common/tasks/main.yml | 8 ++ 6 files changed, 235 insertions(+) create mode 100644 docs/tldr-ssh-security.md create mode 100644 roles/vhosts/common/files/ssh_check.exp create mode 100644 roles/vhosts/common/tasks/fail2ban.yml create mode 100644 roles/vhosts/common/tasks/harden_ssh.yml diff --git a/docs/tldr-ssh-security.md b/docs/tldr-ssh-security.md new file mode 100644 index 0000000..c36d4df --- /dev/null +++ b/docs/tldr-ssh-security.md @@ -0,0 +1,108 @@ +# TLDR: SSH Security & Hardening Playbook + +Quick reference for SSH security hardening, firewall controls, Fail2ban management, and connection checking. + +## 1. SSH Hardening (Key-Only Auth) +Password login is completely disabled for all users. Direct root login is restricted to key-only. + +### Configuration file +Drop-in config is deployed to: +`/etc/ssh/sshd_config.d/00-disable-password.conf` + +```text +PasswordAuthentication no +PubkeyAuthentication yes +KbdInteractiveAuthentication no +PermitRootLogin prohibit-password +``` + +### Apply Changes +If you update SSH configurations, reload sshd: +```bash +# Debian/Ubuntu +sudo systemctl reload ssh + +# RedHat/CentOS +sudo systemctl reload sshd +``` + +--- + +## 2. Fail2ban Management +Fail2ban monitors SSH authentication failures and bans offensive IPs. + +### Default Settings +* **Bantime**: 24 hours (`86400` seconds) +* **Findtime**: 10 minutes (`600` seconds) +* **Maxretry**: 3 attempts + +### Useful Commands +```bash +# Check Fail2ban service status +sudo systemctl status fail2ban + +# Check sshd jail status (banned IPs) +sudo fail2ban-client status sshd + +# Unban a specific IP +sudo fail2ban-client set sshd unbanip + +# Manually ban a specific IP +sudo fail2ban-client set sshd banip + +# View fail2ban logs +sudo tail -f /var/log/fail2ban.log +``` + +--- + +## 3. SSH Proxy Connection Helper (`ssh_check.exp`) +A generic `expect` helper script to verify ProxyJump-ed SSH connectivity. + +### Usage +To prevent password leaks in shell history (`~/.bash_history` or `~/.zsh_history`), **never** pass the password as a command-line argument. Instead, use one of the secure methods below: + +#### Option A: Read securely from input (Recommended) +```bash +# Type your password securely (input will not echo on screen) +read -s SSH_CHECK_PASSWORD +export SSH_CHECK_PASSWORD + +# Run the helper script (picks up password from env var) +ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129 +``` + +#### Option B: Set via env var with leading space +If your shell is configured to ignore commands starting with a space (e.g. `HISTCONTROL=ignorespace` in bash or `setopt HIST_IGNORE_SPACE` in zsh), you can set the variable with a leading space: +```bash + export SSH_CHECK_PASSWORD="your_password" + ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129 +``` + +#### Option C: Legacy/Direct (Not recommended, leaves history trace) +```bash +ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129 "your_password" +``` + +--- + +## 4. Firewall (UFW) quick-ref +Used on hosts to manage ports (e.g. 80, 443, 1443). + +```bash +# View firewall rules with line numbers +sudo ufw status numbered + +# Allow a port to Anywhere +sudo ufw allow 443/tcp + +# Delete a rule by rule number +sudo ufw delete + +# Restrict port 22 to a specific IP (e.g. Proxy IP) +sudo ufw allow from 43.207.194.92 to any port 22 proto tcp +sudo ufw delete allow 22/tcp + +# Reload firewall +sudo ufw reload +``` diff --git a/roles/vhosts/common/files/ssh_check.exp b/roles/vhosts/common/files/ssh_check.exp new file mode 100644 index 0000000..fc27b40 --- /dev/null +++ b/roles/vhosts/common/files/ssh_check.exp @@ -0,0 +1,45 @@ +#!/usr/bin/expect -f +set timeout 30 +set proxy [lindex $argv 0] +set target [lindex $argv 1] + +# Retrieve password from environment variable (secure) +# Fallback to the third argument if environment variable is not set +if { [info exists ::env(SSH_CHECK_PASSWORD)] } { + set password $::env(SSH_CHECK_PASSWORD) +} else { + set password [lindex $argv 2] +} + +if { $proxy == "" || $target == "" || $password == "" } { + send_user "Error: Missing required parameters.\n" + send_user "Usage (Recommended): export SSH_CHECK_PASSWORD=\"your_password\"\n" + send_user " ssh_check.exp \n" + send_user "Usage (Legacy): ssh_check.exp \n" + exit 1 +} + +# Use UserKnownHostsFile=/dev/null to avoid modifying the local known_hosts file +spawn ssh -J $proxy -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $target +expect { + "password:" { + # Temporarily disable logging to hide the password from being echoed in stdout/logs + log_user 0 + send "$password\r" + log_user 1 + exp_continue + } + -re "(#|\\\$)" { + send_user "SUCCESS\n" + send "exit\n" + expect eof + } + timeout { + send_user "TIMEOUT\n" + exit 1 + } + eof { + send_user "EOF_CLOSED\n" + exit 1 + } +} diff --git a/roles/vhosts/common/handlers/main.yml b/roles/vhosts/common/handlers/main.yml index 4a4bf1d..11a7d84 100644 --- a/roles/vhosts/common/handlers/main.yml +++ b/roles/vhosts/common/handlers/main.yml @@ -12,3 +12,13 @@ - name: apt-update-cache ansible.builtin.apt: update_cache: true + +- name: Restart SSH + ansible.builtin.service: + name: "{{ 'ssh' if ansible_facts.os_family == 'Debian' else 'sshd' }}" + state: reloaded + +- name: Restart Fail2ban + ansible.builtin.service: + name: fail2ban + state: restarted diff --git a/roles/vhosts/common/tasks/fail2ban.yml b/roles/vhosts/common/tasks/fail2ban.yml new file mode 100644 index 0000000..8820be2 --- /dev/null +++ b/roles/vhosts/common/tasks/fail2ban.yml @@ -0,0 +1,32 @@ +--- +- name: Fail2ban | Install Fail2ban package + ansible.builtin.package: + name: fail2ban + state: present + become: true + +- name: Fail2ban | Deploy jail.local configuration + ansible.builtin.copy: + dest: /etc/fail2ban/jail.local + mode: "0644" + owner: root + group: root + content: | + [DEFAULT] + bantime = 86400 + findtime = 600 + maxretry = 3 + + [sshd] + enabled = true + port = ssh + become: true + notify: Restart Fail2ban + +- name: Fail2ban | Ensure service is started and enabled + ansible.builtin.service: + name: fail2ban + state: started + enabled: true + become: true + when: not ansible_check_mode diff --git a/roles/vhosts/common/tasks/harden_ssh.yml b/roles/vhosts/common/tasks/harden_ssh.yml new file mode 100644 index 0000000..569be9d --- /dev/null +++ b/roles/vhosts/common/tasks/harden_ssh.yml @@ -0,0 +1,32 @@ +--- +- name: SSH | Ensure sshd drop-in directory exists + ansible.builtin.file: + path: /etc/ssh/sshd_config.d + state: directory + mode: "0755" + owner: root + group: root + become: true + +- name: SSH | Write sshd hardening drop-in (Disable password authentication for all users) + ansible.builtin.copy: + dest: /etc/ssh/sshd_config.d/00-disable-password.conf + mode: "0644" + owner: root + group: root + content: | + PasswordAuthentication no + PubkeyAuthentication yes + KbdInteractiveAuthentication no + PermitRootLogin prohibit-password + become: true + notify: Restart SSH + +- name: SSH | Deploy ssh_check.exp helper script + ansible.builtin.copy: + src: files/ssh_check.exp + dest: /usr/local/bin/ssh_check.exp + mode: "0755" + owner: root + group: root + become: true diff --git a/roles/vhosts/common/tasks/main.yml b/roles/vhosts/common/tasks/main.yml index 940b4ef..bd54941 100644 --- a/roles/vhosts/common/tasks/main.yml +++ b/roles/vhosts/common/tasks/main.yml @@ -32,6 +32,14 @@ ansible.builtin.script: files/secure_ssh.sh become: true +- name: Base | harden ssh config + ansible.builtin.import_tasks: harden_ssh.yml + tags: [ssh, security] + +- name: Base | configure fail2ban + ansible.builtin.import_tasks: fail2ban.yml + tags: [fail2ban, security] + - name: Base | file limits ansible.builtin.import_tasks: limits.yml when: