feat: add ssh-key-only
This commit is contained in:
Executable
+323
@@ -0,0 +1,323 @@
|
||||
#!/bin/bash
|
||||
# 交互式设置网卡静态 IPv4 地址或切回 DHCP
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common/help.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: ip-switch
|
||||
|
||||
交互选择网络服务/网卡,并设置临时静态 IPv4 地址或切换回 DHCP。
|
||||
|
||||
支持:
|
||||
macOS 使用 networksetup 管理网络服务
|
||||
Linux 优先使用 NetworkManager (nmcli);否则使用 ip + DHCP 客户端
|
||||
|
||||
选项:
|
||||
-h, --help 显示此帮助
|
||||
|
||||
说明:
|
||||
修改网络配置通常需要 sudo。Linux 下通过 nmcli 或 ip 做出的修改仅作用于
|
||||
当前连接,不会写入连接配置文件;重新连接后会恢复原配置。
|
||||
ip 命令本身不能获取 DHCP 租约;切回 DHCP 时需要 dhclient、udhcpc 或 dhcpcd。
|
||||
设置静态地址时,子网掩码默认 255.255.255.0,默认网关按 IP 推导为 x.x.x.1。
|
||||
远程 SSH 连接中运行时,修改当前网卡可能会导致连接中断。
|
||||
EOF
|
||||
}
|
||||
|
||||
dotfile_help_requested "${1:-}" && dotfile_show_help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OS="$(uname -s)"
|
||||
|
||||
die() {
|
||||
echo "错误: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
is_ipv4() {
|
||||
local ip="$1" octet
|
||||
[[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 1
|
||||
IFS='.' read -r -a octets <<<"$ip"
|
||||
for octet in "${octets[@]}"; do
|
||||
((10#$octet <= 255)) || return 1
|
||||
done
|
||||
}
|
||||
|
||||
netmask_to_prefix() {
|
||||
case "$1" in
|
||||
255.255.255.255) echo 32 ;;
|
||||
255.255.255.254) echo 31 ;;
|
||||
255.255.255.252) echo 30 ;;
|
||||
255.255.255.248) echo 29 ;;
|
||||
255.255.255.240) echo 28 ;;
|
||||
255.255.255.224) echo 27 ;;
|
||||
255.255.255.192) echo 26 ;;
|
||||
255.255.255.128) echo 25 ;;
|
||||
255.255.255.0) echo 24 ;;
|
||||
255.255.254.0) echo 23 ;;
|
||||
255.255.252.0) echo 22 ;;
|
||||
255.255.248.0) echo 21 ;;
|
||||
255.255.240.0) echo 20 ;;
|
||||
255.255.224.0) echo 19 ;;
|
||||
255.255.192.0) echo 18 ;;
|
||||
255.255.128.0) echo 17 ;;
|
||||
255.255.0.0) echo 16 ;;
|
||||
255.254.0.0) echo 15 ;;
|
||||
255.252.0.0) echo 14 ;;
|
||||
255.248.0.0) echo 13 ;;
|
||||
255.240.0.0) echo 12 ;;
|
||||
255.224.0.0) echo 11 ;;
|
||||
255.192.0.0) echo 10 ;;
|
||||
255.128.0.0) echo 9 ;;
|
||||
255.0.0.0) echo 8 ;;
|
||||
254.0.0.0) echo 7 ;;
|
||||
252.0.0.0) echo 6 ;;
|
||||
248.0.0.0) echo 5 ;;
|
||||
240.0.0.0) echo 4 ;;
|
||||
224.0.0.0) echo 3 ;;
|
||||
192.0.0.0) echo 2 ;;
|
||||
128.0.0.0) echo 1 ;;
|
||||
0.0.0.0) echo 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local answer
|
||||
read -r -p "确认应用此网络配置?[y/N] " answer
|
||||
[[ "$answer" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
select_item() {
|
||||
local prompt="$1" item index=1 choice
|
||||
shift
|
||||
local -a items=("$@")
|
||||
|
||||
echo
|
||||
for item in "${items[@]}"; do
|
||||
printf ' %d) %s\n' "$index" "$item"
|
||||
((index += 1))
|
||||
done
|
||||
echo " 0) 取消"
|
||||
|
||||
while true; do
|
||||
read -r -p "$prompt" choice
|
||||
[[ "$choice" == "0" || "$choice" == "q" || "$choice" == "Q" ]] && return 1
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && ((choice >= 1 && choice <= ${#items[@]})); then
|
||||
SELECTED_INDEX=$((choice - 1))
|
||||
return 0
|
||||
fi
|
||||
echo "请输入列表中的编号。"
|
||||
done
|
||||
}
|
||||
|
||||
prompt_static_config() {
|
||||
local default_gateway
|
||||
|
||||
while true; do
|
||||
read -r -p "IPv4 地址: " STATIC_IP
|
||||
is_ipv4 "$STATIC_IP" && break
|
||||
echo "请输入有效的 IPv4 地址。"
|
||||
done
|
||||
|
||||
while true; do
|
||||
read -r -p "子网掩码 [255.255.255.0]: " STATIC_MASK
|
||||
STATIC_MASK="${STATIC_MASK:-255.255.255.0}"
|
||||
if STATIC_PREFIX=$(netmask_to_prefix "$STATIC_MASK"); then
|
||||
break
|
||||
fi
|
||||
echo "请输入连续的 IPv4 子网掩码,例如 255.255.255.0。"
|
||||
done
|
||||
|
||||
default_gateway="${STATIC_IP%.*}.1"
|
||||
while true; do
|
||||
read -r -p "默认网关 [$default_gateway]: " STATIC_GATEWAY
|
||||
STATIC_GATEWAY="${STATIC_GATEWAY:-$default_gateway}"
|
||||
is_ipv4 "$STATIC_GATEWAY" && break
|
||||
echo "请输入有效的 IPv4 默认网关。"
|
||||
done
|
||||
}
|
||||
|
||||
run_macos() {
|
||||
local service device current action
|
||||
local -a services=() devices=() labels=()
|
||||
|
||||
command -v networksetup >/dev/null || die "找不到 networksetup"
|
||||
|
||||
while IFS=$'\t' read -r service device; do
|
||||
[ -n "$service" ] || continue
|
||||
services+=("$service")
|
||||
devices+=("$device")
|
||||
labels+=("$service ($device)")
|
||||
done < <(networksetup -listnetworkserviceorder | awk '
|
||||
/^\([0-9]+\) / { service = $0; sub(/^\([0-9]+\) \*?/, "", service); next }
|
||||
/Hardware Port:.*Device:/ {
|
||||
device = $0; sub(/^.*Device: /, "", device); sub(/\).*/, "", device)
|
||||
if (service != "") print service "\t" device
|
||||
}
|
||||
')
|
||||
|
||||
((${#services[@]} > 0)) || die "未发现可配置的网络服务"
|
||||
select_item "选择网络服务: " "${labels[@]}" || exit 0
|
||||
service="${services[$SELECTED_INDEX]}"
|
||||
device="${devices[$SELECTED_INDEX]}"
|
||||
current="$(networksetup -getinfo "$service" 2>&1)"
|
||||
|
||||
echo
|
||||
echo "当前配置 ($service / $device):"
|
||||
echo "$current"
|
||||
select_item "选择操作: " "设置静态 IPv4" "切换为 DHCP" || exit 0
|
||||
action="$SELECTED_INDEX"
|
||||
|
||||
if ((action == 0)); then
|
||||
prompt_static_config
|
||||
echo
|
||||
printf '将 %s (%s) 设为静态地址: %s / %s,网关: %s\n' \
|
||||
"$service" "$device" "$STATIC_IP" "$STATIC_MASK" "$STATIC_GATEWAY"
|
||||
confirm || { echo "已取消。"; exit 0; }
|
||||
sudo networksetup -setmanual "$service" "$STATIC_IP" "$STATIC_MASK" "$STATIC_GATEWAY"
|
||||
else
|
||||
echo
|
||||
echo "将 $service ($device) 切换为 DHCP。"
|
||||
confirm || { echo "已取消。"; exit 0; }
|
||||
sudo networksetup -setdhcp "$service"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "已应用。当前配置:"
|
||||
networksetup -getinfo "$service"
|
||||
}
|
||||
|
||||
run_linux() {
|
||||
local device type state action
|
||||
local -a devices=() labels=()
|
||||
|
||||
while IFS=: read -r device type state; do
|
||||
[ -n "$device" ] || continue
|
||||
[ "$type" = "loopback" ] && continue
|
||||
devices+=("$device")
|
||||
labels+=("$device ($type, $state)")
|
||||
done < <(nmcli -t -f DEVICE,TYPE,STATE device status)
|
||||
|
||||
((${#devices[@]} > 0)) || die "未发现可配置的网络接口"
|
||||
select_item "选择网络接口: " "${labels[@]}" || exit 0
|
||||
device="${devices[$SELECTED_INDEX]}"
|
||||
|
||||
echo
|
||||
echo "当前配置 ($device):"
|
||||
nmcli device show "$device" | awk '/GENERAL.CONNECTION|IP4\.ADDRESS|IP4\.GATEWAY|IP4\.DNS/ { print }'
|
||||
select_item "选择操作: " "设置静态 IPv4" "切换为 DHCP" || exit 0
|
||||
action="$SELECTED_INDEX"
|
||||
|
||||
if ((action == 0)); then
|
||||
prompt_static_config
|
||||
echo
|
||||
printf '将 %s 设为静态地址: %s/%s,网关: %s\n' \
|
||||
"$device" "$STATIC_IP" "$STATIC_PREFIX" "$STATIC_GATEWAY"
|
||||
confirm || { echo "已取消。"; exit 0; }
|
||||
sudo nmcli device modify "$device" ipv4.method manual \
|
||||
ipv4.addresses "$STATIC_IP/$STATIC_PREFIX" ipv4.gateway "$STATIC_GATEWAY"
|
||||
else
|
||||
echo
|
||||
echo "将 $device 切换为 DHCP。"
|
||||
confirm || { echo "已取消。"; exit 0; }
|
||||
sudo nmcli device modify "$device" ipv4.method auto ipv4.addresses "" ipv4.gateway ""
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "已应用。当前配置:"
|
||||
nmcli device show "$device" | awk '/GENERAL.CONNECTION|IP4\.ADDRESS|IP4\.GATEWAY|IP4\.DNS/ { print }'
|
||||
}
|
||||
|
||||
linux_dhcp_client() {
|
||||
if command -v dhclient >/dev/null; then
|
||||
echo dhclient
|
||||
elif command -v udhcpc >/dev/null; then
|
||||
echo udhcpc
|
||||
elif command -v dhcpcd >/dev/null; then
|
||||
echo dhcpcd
|
||||
fi
|
||||
}
|
||||
|
||||
run_linux_ip() {
|
||||
local device action dhcp_client
|
||||
local -a devices=() labels=()
|
||||
|
||||
command -v ip >/dev/null || die "找不到 ip 命令"
|
||||
|
||||
while IFS= read -r device; do
|
||||
[ -n "$device" ] || continue
|
||||
devices+=("$device")
|
||||
labels+=("$device ($(ip -br link show dev "$device" | awk '{ print $2 }'))")
|
||||
done < <(ip -o link show | awk -F': ' '$2 != "lo" { sub(/@.*/, "", $2); print $2 }')
|
||||
|
||||
((${#devices[@]} > 0)) || die "未发现可配置的网络接口"
|
||||
select_item "选择网络接口: " "${labels[@]}" || exit 0
|
||||
device="${devices[$SELECTED_INDEX]}"
|
||||
|
||||
echo
|
||||
echo "当前配置 ($device):"
|
||||
ip -br -4 addr show dev "$device"
|
||||
ip route show default dev "$device" || true
|
||||
select_item "选择操作: " "设置静态 IPv4" "切换为 DHCP" || exit 0
|
||||
action="$SELECTED_INDEX"
|
||||
|
||||
if ((action == 0)); then
|
||||
prompt_static_config
|
||||
echo
|
||||
printf '将 %s 设为静态地址: %s/%s,网关: %s\n' \
|
||||
"$device" "$STATIC_IP" "$STATIC_PREFIX" "$STATIC_GATEWAY"
|
||||
confirm || { echo "已取消。"; exit 0; }
|
||||
sudo ip link set dev "$device" up
|
||||
sudo ip -4 addr flush dev "$device"
|
||||
sudo ip -4 addr add "$STATIC_IP/$STATIC_PREFIX" dev "$device"
|
||||
sudo ip route replace default via "$STATIC_GATEWAY" dev "$device"
|
||||
else
|
||||
dhcp_client="$(linux_dhcp_client)"
|
||||
[ -n "$dhcp_client" ] || die "切回 DHCP 需要 dhclient、udhcpc 或 dhcpcd;请安装与现有网络管理方式兼容的 DHCP 客户端"
|
||||
echo
|
||||
echo "将 $device 切换为 DHCP(使用 $dhcp_client)。"
|
||||
confirm || { echo "已取消。"; exit 0; }
|
||||
sudo ip link set dev "$device" up
|
||||
|
||||
case "$dhcp_client" in
|
||||
dhclient)
|
||||
sudo dhclient -4 -r "$device" 2>/dev/null || true
|
||||
sudo ip -4 addr flush dev "$device"
|
||||
sudo ip route flush default dev "$device" 2>/dev/null || true
|
||||
sudo dhclient -4 -v "$device"
|
||||
;;
|
||||
udhcpc)
|
||||
sudo ip -4 addr flush dev "$device"
|
||||
sudo ip route flush default dev "$device" 2>/dev/null || true
|
||||
sudo udhcpc -i "$device" -q -n
|
||||
;;
|
||||
dhcpcd)
|
||||
sudo ip -4 addr flush dev "$device"
|
||||
sudo ip route flush default dev "$device" 2>/dev/null || true
|
||||
sudo dhcpcd -4 "$device"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "已应用。当前配置:"
|
||||
ip -br -4 addr show dev "$device"
|
||||
ip route show default dev "$device" || true
|
||||
}
|
||||
|
||||
case "$OS" in
|
||||
Darwin) run_macos ;;
|
||||
Linux)
|
||||
if command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager 2>/dev/null; then
|
||||
run_linux
|
||||
else
|
||||
run_linux_ip
|
||||
fi
|
||||
;;
|
||||
*) die "不支持的操作系统: $OS" ;;
|
||||
esac
|
||||
Executable
+309
@@ -0,0 +1,309 @@
|
||||
#!/bin/bash
|
||||
# 快速检查服务器 SSH、端口、防火墙、特权账号与近期登录风险
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common/help.sh"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common/os-arch.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: server-security-check
|
||||
|
||||
快速、只读地检查以下服务器安全项:
|
||||
- SSH 是否允许 root 或密码登录
|
||||
- 当前监听端口,以及常见明文服务端口
|
||||
- 本机防火墙是否启用
|
||||
- UID 0 异常账号和 sudoers 中的免密 sudo 规则
|
||||
- 最近 24 小时 SSH 失败登录摘要和最近成功登录来源
|
||||
|
||||
选项:
|
||||
-h, --help 显示此帮助
|
||||
|
||||
说明:
|
||||
不扫描网络、不查询漏洞库、不修改配置,通常可在数秒内完成。
|
||||
建议使用 sudo 运行,以读取 sudoers 和系统认证日志:
|
||||
sudo server-security-check
|
||||
EOF
|
||||
}
|
||||
|
||||
dotfile_help_requested "${1:-}" && dotfile_show_help
|
||||
|
||||
if [[ $# -ne 0 ]]; then
|
||||
echo "错误: 不支持参数: $*" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS_COUNT=0
|
||||
WARN_COUNT=0
|
||||
HIGH_COUNT=0
|
||||
INFO_COUNT=0
|
||||
|
||||
pass() {
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
printf '[通过] %s\n' "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
WARN_COUNT=$((WARN_COUNT + 1))
|
||||
printf '[警告] %s\n' "$*"
|
||||
}
|
||||
|
||||
high() {
|
||||
HIGH_COUNT=$((HIGH_COUNT + 1))
|
||||
printf '[高风险] %s\n' "$*"
|
||||
}
|
||||
|
||||
info() {
|
||||
INFO_COUNT=$((INFO_COUNT + 1))
|
||||
printf '[信息] %s\n' "$*"
|
||||
}
|
||||
|
||||
section() {
|
||||
printf '\n== %s ==\n' "$1"
|
||||
}
|
||||
|
||||
find_sshd() {
|
||||
local candidate
|
||||
if command -v sshd >/dev/null 2>&1; then
|
||||
command -v sshd
|
||||
return
|
||||
fi
|
||||
|
||||
for candidate in /usr/sbin/sshd /usr/local/sbin/sshd; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_sshd_setting() {
|
||||
local setting="$1" value
|
||||
value=$(printf '%s\n' "$SSHD_EFFECTIVE_CONFIG" | awk -v key="$setting" '$1 == key { print $2; exit }')
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
check_ssh() {
|
||||
local sshd_bin root_login password_login
|
||||
|
||||
section "SSH 登录"
|
||||
sshd_bin=$(find_sshd)
|
||||
if [[ -z "$sshd_bin" ]]; then
|
||||
info "未安装 sshd,跳过 SSH 配置检查"
|
||||
return
|
||||
fi
|
||||
|
||||
SSHD_EFFECTIVE_CONFIG=$("$sshd_bin" -T 2>/dev/null)
|
||||
if [[ -z "$SSHD_EFFECTIVE_CONFIG" ]]; then
|
||||
warn "无法读取 sshd 生效配置;请使用 sudo 重试或检查 sshd 配置"
|
||||
return
|
||||
fi
|
||||
|
||||
root_login=$(check_sshd_setting permitrootlogin)
|
||||
password_login=$(check_sshd_setting passwordauthentication)
|
||||
|
||||
case "$root_login" in
|
||||
no|forced-commands-only)
|
||||
pass "SSH 已禁止 root 交互式登录(PermitRootLogin: $root_login)"
|
||||
;;
|
||||
prohibit-password)
|
||||
pass "SSH root 登录仅允许密钥认证(PermitRootLogin: prohibit-password)"
|
||||
;;
|
||||
yes)
|
||||
high "SSH 允许 root 直接登录;建议设为 PermitRootLogin no"
|
||||
;;
|
||||
*)
|
||||
warn "无法确定 PermitRootLogin 的安全状态(当前: ${root_login:-未设置})"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$password_login" in
|
||||
no)
|
||||
pass "SSH 已禁用密码登录"
|
||||
;;
|
||||
yes)
|
||||
warn "SSH 允许密码登录;公网服务器建议改用密钥认证"
|
||||
;;
|
||||
*)
|
||||
warn "无法确定 PasswordAuthentication 的安全状态(当前: ${password_login:-未设置})"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_listening_ports() {
|
||||
local listeners risky
|
||||
|
||||
section "监听端口"
|
||||
if [[ "$OS_NAME" == "linux" ]] && command -v ss >/dev/null 2>&1; then
|
||||
listeners=$(ss -H -lntu 2>/dev/null)
|
||||
elif [[ "$OS_NAME" == "darwin" ]] && command -v lsof >/dev/null 2>&1; then
|
||||
listeners=$(lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | sed 1d)
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
listeners=$(netstat -lnt 2>/dev/null || netstat -an 2>/dev/null)
|
||||
else
|
||||
warn "找不到 ss、lsof 或 netstat,无法列出监听端口"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "$listeners" ]]; then
|
||||
pass "未发现 TCP/UDP 监听端口"
|
||||
return
|
||||
fi
|
||||
|
||||
printf '%s\n' "$listeners" | sed -n '1,30p'
|
||||
local listener_count
|
||||
listener_count=$(printf '%s\n' "$listeners" | wc -l | tr -d ' ')
|
||||
if ((listener_count > 30)); then
|
||||
info "仅显示前 30 个监听项,共 ${listener_count} 项"
|
||||
fi
|
||||
|
||||
risky=$(printf '%s\n' "$listeners" | grep -E '(:21|:23|:69|:111|:512|:513|:514)([[:space:]]|$)' || true)
|
||||
if [[ -n "$risky" ]]; then
|
||||
warn "发现 FTP/Telnet/rsh 等常见高风险或明文服务端口:$(printf '%s' "$risky" | tr '\n' ';')"
|
||||
else
|
||||
pass "未发现常见明文服务端口(21、23、69、111、512-514)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_firewall() {
|
||||
local ufw_output zones
|
||||
|
||||
section "防火墙"
|
||||
case "$OS_NAME" in
|
||||
linux)
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
ufw_output=$(ufw status verbose 2>&1)
|
||||
if printf '%s\n' "$ufw_output" | grep -qi '^Status: active'; then
|
||||
if printf '%s\n' "$ufw_output" | grep -qi 'Default: deny (incoming)'; then
|
||||
pass "UFW 已启用,默认拒绝入站连接"
|
||||
else
|
||||
warn "UFW 已启用,但未确认默认拒绝入站连接"
|
||||
fi
|
||||
else
|
||||
warn "UFW 未启用;若未使用云安全组或其他防火墙,请启用入站访问控制"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null | grep -qx running; then
|
||||
zones=$(firewall-cmd --get-active-zones 2>/dev/null | tr '\n' ';')
|
||||
pass "firewalld 正在运行(活动区域: ${zones:-未知})"
|
||||
info "请确认活动区域只放行实际需要的服务"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nftables 2>/dev/null; then
|
||||
pass "nftables 服务正在运行"
|
||||
info "请确认 nftables 的入站默认策略为 drop 或 reject"
|
||||
return
|
||||
fi
|
||||
|
||||
warn "未检测到正在运行的 UFW、firewalld 或 nftables;若依赖云安全组,请确认其已限制入站端口"
|
||||
;;
|
||||
darwin)
|
||||
if [[ -x /usr/libexec/ApplicationFirewall/socketfilterfw ]]; then
|
||||
if /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null | grep -qi enabled; then
|
||||
pass "macOS 应用防火墙已启用"
|
||||
else
|
||||
warn "macOS 应用防火墙未启用"
|
||||
fi
|
||||
else
|
||||
warn "无法读取 macOS 防火墙状态"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
warn "不支持的系统,无法检测防火墙"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_privileged_accounts() {
|
||||
local uid_zero sudo_rules
|
||||
|
||||
section "高权限账号"
|
||||
uid_zero=$(awk -F: '$3 == 0 && $1 != "root" {print $1}' /etc/passwd 2>/dev/null)
|
||||
if [[ -n "$uid_zero" ]]; then
|
||||
high "发现非 root 的 UID 0 账号:$(printf '%s' "$uid_zero" | tr '\n' ' ')"
|
||||
else
|
||||
pass "未发现非 root 的 UID 0 账号"
|
||||
fi
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
info "未以 root 运行,跳过完整 sudoers 免密规则检查;可用 sudo 重跑"
|
||||
return
|
||||
fi
|
||||
|
||||
sudo_rules=$(grep -REh '^[[:space:]]*[^#].*NOPASSWD' /etc/sudoers /etc/sudoers.d 2>/dev/null || true)
|
||||
if [[ -n "$sudo_rules" ]]; then
|
||||
warn "发现免密 sudo 规则:$(printf '%s' "$sudo_rules" | tr '\n' ';')"
|
||||
else
|
||||
pass "未发现 sudoers 中的 NOPASSWD 免密 sudo 规则"
|
||||
fi
|
||||
}
|
||||
|
||||
check_recent_logins() {
|
||||
local auth_log log_text failed_count successful
|
||||
|
||||
section "近期 SSH 登录"
|
||||
auth_log=""
|
||||
for candidate in /var/log/auth.log /var/log/secure; do
|
||||
if [[ -r "$candidate" ]]; then
|
||||
auth_log="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$auth_log" ]]; then
|
||||
log_text=$(tail -n 5000 "$auth_log" 2>/dev/null)
|
||||
elif command -v journalctl >/dev/null 2>&1; then
|
||||
log_text=$(journalctl --no-pager --since '24 hours ago' -u ssh -u sshd 2>/dev/null)
|
||||
else
|
||||
log_text=""
|
||||
fi
|
||||
|
||||
if [[ -n "$log_text" ]]; then
|
||||
failed_count=$(printf '%s\n' "$log_text" | grep -Eic 'Failed password|Invalid user|authentication failure|Failed publickey' || true)
|
||||
if ((failed_count == 0)); then
|
||||
pass "最近 24 小时未发现 SSH 失败登录记录"
|
||||
elif ((failed_count < 20)); then
|
||||
warn "最近 24 小时发现 ${failed_count} 条 SSH 失败登录记录"
|
||||
else
|
||||
high "最近 24 小时发现 ${failed_count} 条 SSH 失败登录记录,可能存在暴力破解"
|
||||
fi
|
||||
else
|
||||
info "无法读取最近 24 小时 SSH 日志;可用 sudo 重跑,或确认日志服务正常"
|
||||
fi
|
||||
|
||||
if command -v last >/dev/null 2>&1; then
|
||||
successful=$(last -i -n 5 2>/dev/null | sed -n '/^wtmp begins/q;p' | sed '/^$/d')
|
||||
if [[ -n "$successful" ]]; then
|
||||
printf '最近成功登录(最多 5 条):\n%s\n' "$successful"
|
||||
else
|
||||
info "未读取到成功登录记录"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
OS_NAME=$(get_os)
|
||||
|
||||
echo "服务器快速安全检查(系统: ${OS_NAME}/$(get_arch))"
|
||||
echo "只读检查;建议使用 sudo 获取完整结果。"
|
||||
|
||||
check_ssh
|
||||
check_listening_ports
|
||||
check_firewall
|
||||
check_privileged_accounts
|
||||
check_recent_logins
|
||||
|
||||
printf '\n== 汇总 ==\n'
|
||||
printf '通过: %d 警告: %d 高风险: %d 信息: %d\n' "$PASS_COUNT" "$WARN_COUNT" "$HIGH_COUNT" "$INFO_COUNT"
|
||||
if ((HIGH_COUNT > 0)); then
|
||||
echo "请优先处理“高风险”项。"
|
||||
elif ((WARN_COUNT > 0)); then
|
||||
echo "建议逐项确认“警告”项是否符合服务器的实际部署方式。"
|
||||
else
|
||||
echo "未发现需要立即处理的项。"
|
||||
fi
|
||||
Executable
+216
@@ -0,0 +1,216 @@
|
||||
#!/bin/bash
|
||||
# 验证已有公钥后关闭 SSH 密码登录,仅保留公钥认证
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common/help.sh"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common/os-arch.sh"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: sudo ssh-key-only [--user <用户名>] [--yes]
|
||||
|
||||
在确认目标用户已有 SSH 公钥后,写入 /etc/ssh/sshd_config.d/99-dotfile-key-only.conf:
|
||||
PubkeyAuthentication yes
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
ChallengeResponseAuthentication no
|
||||
|
||||
选项:
|
||||
-u, --user <用户名> 要确认公钥的用户;默认使用调用 sudo 前的用户
|
||||
-y, --yes 跳过交互确认
|
||||
-h, --help 显示此帮助
|
||||
|
||||
示例:
|
||||
sudo ssh-key-only
|
||||
sudo ssh-key-only --user deploy
|
||||
|
||||
重要说明:
|
||||
- 仅支持使用 systemd 管理 SSH 服务的 Linux 服务器。
|
||||
- 脚本不会创建 SSH 密钥;目标用户的 ~/.ssh/authorized_keys 中必须已有有效公钥。
|
||||
- 写入前会校验 sshd 配置,且重载后会再次核对生效设置;失败会恢复原文件。
|
||||
- 请保持当前 SSH 会话不要退出,另开一个终端验证密钥登录成功后再关闭。
|
||||
EOF
|
||||
}
|
||||
|
||||
TARGET_USER="${SUDO_USER:-}"
|
||||
ASSUME_YES=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
dotfile_show_help
|
||||
;;
|
||||
-u|--user)
|
||||
[[ $# -ge 2 ]] || { echo "错误: $1 需要用户名" >&2; exit 1; }
|
||||
TARGET_USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-y|--yes)
|
||||
ASSUME_YES=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "错误: 不支持参数: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
die() {
|
||||
echo "错误: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
find_sshd() {
|
||||
local candidate
|
||||
if command -v sshd >/dev/null 2>&1; then
|
||||
command -v sshd
|
||||
return
|
||||
fi
|
||||
|
||||
for candidate in /usr/sbin/sshd /usr/local/sbin/sshd; do
|
||||
[[ -x "$candidate" ]] && { printf '%s\n' "$candidate"; return; }
|
||||
done
|
||||
}
|
||||
|
||||
user_home() {
|
||||
local user="$1" home
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
home=$(getent passwd "$user" | awk -F: 'NR == 1 {print $6}')
|
||||
fi
|
||||
printf '%s' "${home:-}"
|
||||
}
|
||||
|
||||
setting_value() {
|
||||
local setting="$1"
|
||||
printf '%s\n' "$EFFECTIVE_CONFIG" | awk -v key="$setting" '$1 == key {print $2; exit}'
|
||||
}
|
||||
|
||||
check_ssh_path_permissions() {
|
||||
local path="$1" owner mode
|
||||
owner=$(stat -c '%U' "$path")
|
||||
mode=$(stat -c '%a' "$path")
|
||||
|
||||
[[ "$owner" == "$TARGET_USER" || "$owner" == "root" ]] || die "$path 的属主为 $owner,应为 $TARGET_USER 或 root"
|
||||
(( (8#$mode & 022) == 0 )) || die "$path 对组或其他用户可写(权限: $mode),sshd 可能会忽略其中的公钥"
|
||||
}
|
||||
|
||||
config_is_key_only() {
|
||||
EFFECTIVE_CONFIG=$("$SSHD_BIN" -T -f "$SSHD_CONFIG" 2>/dev/null || true)
|
||||
[[ "$(setting_value pubkeyauthentication)" == "yes" ]] || return 1
|
||||
[[ "$(setting_value passwordauthentication)" == "no" ]] || return 1
|
||||
[[ "$(setting_value kbdinteractiveauthentication)" == "no" ]] || return 1
|
||||
}
|
||||
|
||||
reload_sshd() {
|
||||
local unit
|
||||
for unit in ssh sshd; do
|
||||
if systemctl reload "$unit" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
die "请使用 sudo 运行,例如: sudo $0${TARGET_USER:+ --user "$TARGET_USER"}"
|
||||
fi
|
||||
|
||||
[[ "$(get_os)" == "linux" ]] || die "当前仅支持 systemd 管理 SSH 服务的 Linux 服务器"
|
||||
command -v systemctl >/dev/null 2>&1 || die "未找到 systemctl,无法安全重载 SSH 服务"
|
||||
[[ -n "$TARGET_USER" ]] || die "无法识别目标用户;请指定 --user <用户名>"
|
||||
id "$TARGET_USER" >/dev/null 2>&1 || die "用户不存在: $TARGET_USER"
|
||||
|
||||
TARGET_HOME=$(user_home "$TARGET_USER")
|
||||
[[ -n "$TARGET_HOME" && -d "$TARGET_HOME" ]] || die "无法确定用户主目录: $TARGET_USER"
|
||||
AUTHORIZED_KEYS="$TARGET_HOME/.ssh/authorized_keys"
|
||||
[[ -r "$AUTHORIZED_KEYS" ]] || die "未找到可读取的公钥文件: $AUTHORIZED_KEYS"
|
||||
[[ -d "$TARGET_HOME/.ssh" ]] || die "未找到 SSH 配置目录: $TARGET_HOME/.ssh"
|
||||
check_ssh_path_permissions "$TARGET_HOME"
|
||||
check_ssh_path_permissions "$TARGET_HOME/.ssh"
|
||||
check_ssh_path_permissions "$AUTHORIZED_KEYS"
|
||||
|
||||
if ! grep -Eq '^[[:space:]]*(ssh-(rsa|ed25519|dss)|ecdsa-sha2-nistp(256|384|521)|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com|[[:alnum:]_-]+-cert-v01@openssh\.com)[[:space:]]+' "$AUTHORIZED_KEYS"; then
|
||||
die "$AUTHORIZED_KEYS 中未发现有效的 SSH 公钥,已取消以避免锁定访问"
|
||||
fi
|
||||
|
||||
SSHD_BIN=$(find_sshd)
|
||||
[[ -n "$SSHD_BIN" ]] || die "未找到 sshd"
|
||||
SSHD_CONFIG=/etc/ssh/sshd_config
|
||||
[[ -r "$SSHD_CONFIG" ]] || die "无法读取 $SSHD_CONFIG"
|
||||
|
||||
if ! grep -Eq '^[[:space:]]*Include[[:space:]].*sshd_config\.d' "$SSHD_CONFIG"; then
|
||||
die "$SSHD_CONFIG 未包含 sshd_config.d;为避免覆盖现有规则,未作修改"
|
||||
fi
|
||||
|
||||
DROP_IN_DIR=/etc/ssh/sshd_config.d
|
||||
DROP_IN="$DROP_IN_DIR/99-dotfile-key-only.conf"
|
||||
mkdir -p "$DROP_IN_DIR"
|
||||
|
||||
echo "将为 SSH 启用公钥认证,并关闭密码与交互式认证。"
|
||||
echo "已确认 $TARGET_USER 的公钥文件: $AUTHORIZED_KEYS"
|
||||
echo "配置文件: $DROP_IN"
|
||||
if [[ $ASSUME_YES -ne 1 ]]; then
|
||||
read -r -p "确认继续?[y/N] " answer
|
||||
[[ "$answer" =~ ^[Yy]$ ]] || { echo "已取消,未修改配置。"; exit 0; }
|
||||
fi
|
||||
|
||||
STAMP=$(date +%Y%m%d%H%M%S)
|
||||
BACKUP=""
|
||||
if [[ -f "$DROP_IN" ]]; then
|
||||
BACKUP="${DROP_IN}.bak.${STAMP}"
|
||||
cp -p "$DROP_IN" "$BACKUP"
|
||||
fi
|
||||
|
||||
TMP_FILE=$(mktemp "${DROP_IN}.tmp.XXXXXX")
|
||||
VALIDATION_ERROR=$(mktemp)
|
||||
trap 'rm -f "$TMP_FILE" "$VALIDATION_ERROR"' EXIT
|
||||
|
||||
cat >"$TMP_FILE" <<'EOF'
|
||||
# Managed by ~/.dotfile/script/ssh-key-only.sh
|
||||
PubkeyAuthentication yes
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
ChallengeResponseAuthentication no
|
||||
EOF
|
||||
install -m 0600 -o root -g root "$TMP_FILE" "$DROP_IN"
|
||||
|
||||
restore_drop_in() {
|
||||
if [[ -n "$BACKUP" ]]; then
|
||||
cp -p "$BACKUP" "$DROP_IN"
|
||||
else
|
||||
rm -f "$DROP_IN"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! "$SSHD_BIN" -t -f "$SSHD_CONFIG" 2>"$VALIDATION_ERROR"; then
|
||||
restore_drop_in
|
||||
echo "sshd 配置校验输出:" >&2
|
||||
cat "$VALIDATION_ERROR" >&2
|
||||
die "新配置未通过校验,已恢复原文件"
|
||||
fi
|
||||
|
||||
if ! config_is_key_only; then
|
||||
restore_drop_in
|
||||
die "新配置未成为 sshd 的生效设置(可能被更早的配置覆盖),已恢复原文件"
|
||||
fi
|
||||
|
||||
if ! reload_sshd; then
|
||||
restore_drop_in
|
||||
die "无法重载 ssh/sshd 服务,已恢复原文件;请检查 systemctl 状态"
|
||||
fi
|
||||
|
||||
if ! config_is_key_only; then
|
||||
restore_drop_in
|
||||
reload_sshd || true
|
||||
die "重载后设置未生效,已恢复原文件并尝试重载旧配置"
|
||||
fi
|
||||
|
||||
echo "已启用仅公钥 SSH 登录。"
|
||||
[[ -n "$BACKUP" ]] && echo "原配置已备份到: $BACKUP"
|
||||
echo "请保持当前会话,另开终端确认可通过密钥登录后再退出。"
|
||||
Reference in New Issue
Block a user