|
#!/usr/bin/env bash |
|
set -euo pipefail |
|
|
|
echo "=== Duo SSH (pam_duo) 互動式安裝腳本 ===" |
|
|
|
######################################## |
|
# 0. 權限與必備工具檢查 |
|
######################################## |
|
|
|
if [[ "$EUID" -ne 0 ]]; then |
|
if ! command -v sudo >/dev/null 2>&1; then |
|
echo "❌ 需要 root 或 sudo,但系統沒有 sudo" |
|
echo " 請先執行:apt update && apt install -y sudo" |
|
exit 1 |
|
fi |
|
SUDO="sudo" |
|
else |
|
SUDO="" |
|
fi |
|
|
|
REQUIRED_CMDS=(curl wget gpg) |
|
MISSING=() |
|
|
|
for cmd in "${REQUIRED_CMDS[@]}"; do |
|
if ! command -v "$cmd" >/dev/null 2>&1; then |
|
MISSING+=("$cmd") |
|
fi |
|
done |
|
|
|
if ((${#MISSING[@]} > 0)); then |
|
echo "⚠️ 缺少必要工具: ${MISSING[*]}" |
|
echo "📦 自動安裝中 ..." |
|
$SUDO apt-get update -y |
|
$SUDO apt-get install -y "${MISSING[@]}" |
|
else |
|
echo "✔️ curl / wget / gpg 已安裝" |
|
fi |
|
|
|
echo "----------------------------------------" |
|
|
|
######################################## |
|
# 1. 互動輸入資訊 |
|
######################################## |
|
|
|
read -rp "請輸入 Duo integration key (ikey): " IKEY |
|
read -rp "請輸入 Duo secret key (skey): " SKEY |
|
read -rp "請輸入 Duo API host (例如 api-xxxx.duosecurity.com): " HOST |
|
|
|
read -rp "SSH Port (預設 22): " PORT |
|
PORT="${PORT:-22}" |
|
|
|
echo |
|
echo "是否啟用 autopush?" |
|
echo " y = 自動推送 Duo Push,不顯示 passcode 輸入框" |
|
echo " n = 顯示選單,可輸入 Duo OTP / passcode(建議)" |
|
read -rp "[y/N]: " AUTOPUSH_ANS |
|
AUTOPUSH_VALUE=$( [[ "${AUTOPUSH_ANS:-n}" =~ ^[Yy]$ ]] && echo "yes" || echo "no" ) |
|
|
|
echo |
|
echo "登入模式選擇:" |
|
echo " 1) 所有使用者:密碼 + Duo (keyboard-interactive → PAM 密碼 + pam_duo)" |
|
echo " 2) 所有使用者:Key + Duo (publickey + pam_duo,不問密碼)" |
|
echo " 3) root:Key + Duo;其他:密碼 + Duo" |
|
read -rp "請選擇 1/2/3(預設 1): " MODE |
|
MODE="${MODE:-1}" |
|
|
|
echo |
|
echo "設定確認:" |
|
echo " ikey = $IKEY" |
|
echo " host = $HOST" |
|
echo " SSH Port = $PORT" |
|
echo " autopush = $AUTOPUSH_VALUE" |
|
echo " 模式 = $MODE" |
|
read -rp "確認開始安裝?(y/N): " CONFIRM |
|
CONFIRM="${CONFIRM:-n}" |
|
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then |
|
echo "🚫 已取消。" |
|
exit 0 |
|
fi |
|
|
|
######################################## |
|
# 2. 自動備份現有設定 |
|
######################################## |
|
|
|
BACKUP_DIR="/root/duo-backup-$(date +%F_%H-%M-%S)" |
|
$SUDO mkdir -p "$BACKUP_DIR" |
|
|
|
FILES_TO_BACKUP=( |
|
/etc/duo/pam_duo.conf |
|
/etc/pam.d/sshd |
|
/etc/ssh/sshd_config |
|
/etc/apt/sources.list.d/duosecurity.list |
|
) |
|
|
|
echo |
|
echo "📁 備份檔案到:$BACKUP_DIR" |
|
for f in "${FILES_TO_BACKUP[@]}"; do |
|
if [[ -f "$f" ]]; then |
|
$SUDO cp "$f" "$BACKUP_DIR"/ |
|
echo " ✔ 已備份 $f" |
|
fi |
|
done |
|
|
|
######################################## |
|
# 3. 設定 Duo APT repository(無 apt-key) |
|
######################################## |
|
|
|
echo |
|
echo "🔑 設定 Duo APT repo..." |
|
|
|
$SUDO mkdir -p /usr/share/keyrings |
|
|
|
curl -fsSL https://duo.com/DUO-GPG-PUBLIC-KEY.asc \ |
|
| $SUDO gpg --dearmor -o /usr/share/keyrings/duo-archive-keyring.gpg |
|
|
|
CODENAME=$($SUDO bash -lc 'lsb_release -cs 2>/dev/null || echo "stable"') |
|
|
|
cat <<EOF | $SUDO tee /etc/apt/sources.list.d/duosecurity.list >/dev/null |
|
deb [signed-by=/usr/share/keyrings/duo-archive-keyring.gpg] https://pkg.duosecurity.com/Debian ${CODENAME} main |
|
EOF |
|
|
|
echo " ✔ 已寫入 /etc/apt/sources.list.d/duosecurity.list" |
|
|
|
######################################## |
|
# 4. 安裝 duo-unix |
|
######################################## |
|
|
|
echo |
|
echo "📦 安裝 duo-unix..." |
|
$SUDO apt-get update -y |
|
$SUDO apt-get install -y duo-unix |
|
|
|
######################################## |
|
# 5. 找 pam_duo.so 路徑 |
|
######################################## |
|
|
|
echo |
|
echo "🔍 搜尋 pam_duo.so 路徑..." |
|
PAM_DUO=$($SUDO bash -lc 'find / -name pam_duo.so 2>/dev/null | head -n 1 || true') |
|
|
|
if [[ -z "$PAM_DUO" ]]; then |
|
echo "❌ 找不到 pam_duo.so,可能 duo-unix 安裝失敗。" |
|
echo " 備份位置:$BACKUP_DIR" |
|
exit 1 |
|
fi |
|
|
|
echo " ✔ 使用 pam_duo.so: $PAM_DUO" |
|
|
|
######################################## |
|
# 6. 寫入 /etc/duo/pam_duo.conf |
|
######################################## |
|
|
|
echo |
|
echo "📝 寫入 /etc/duo/pam_duo.conf..." |
|
|
|
cat <<EOF | $SUDO tee /etc/duo/pam_duo.conf >/dev/null |
|
[duo] |
|
ikey = ${IKEY} |
|
skey = ${SKEY} |
|
host = ${HOST} |
|
|
|
failmode = safe |
|
autopush = ${AUTOPUSH_VALUE} |
|
pushinfo = yes |
|
EOF |
|
|
|
$SUDO chown root:root /etc/duo/pam_duo.conf |
|
$SUDO chmod 600 /etc/duo/pam_duo.conf |
|
|
|
echo " ✔ pam_duo.conf 權限設定為 root:root 600" |
|
|
|
######################################## |
|
# 7. 依模式產生 /etc/pam.d/sshd |
|
######################################## |
|
|
|
echo |
|
echo "🔐 產生 /etc/pam.d/sshd ..." |
|
|
|
if [[ "$MODE" == "1" || "$MODE" == "3" ]]; then |
|
# 模式 1 & 3:需要「密碼 + Duo」 |
|
cat <<EOF | $SUDO tee /etc/pam.d/sshd >/dev/null |
|
# PAM configuration for the Secure Shell service |
|
auth required pam_nologin.so |
|
@include common-auth |
|
auth required ${PAM_DUO} |
|
|
|
account required pam_nologin.so |
|
@include common-account |
|
|
|
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close |
|
session required pam_loginuid.so |
|
session optional pam_keyinit.so force revoke |
|
@include common-session |
|
session optional pam_motd.so motd=/run/motd.dynamic |
|
session optional pam_motd.so noupdate |
|
session optional pam_mail.so standard noenv |
|
session required pam_limits.so |
|
session required pam_env.so |
|
session required pam_env.so user_readenv=1 envfile=/etc/default/locale |
|
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open |
|
|
|
@include common-password |
|
EOF |
|
|
|
else |
|
# 模式 2:只要 Duo,不要系統密碼(公鑰 + Duo) |
|
cat <<EOF | $SUDO tee /etc/pam.d/sshd >/dev/null |
|
# PAM configuration for the Secure Shell service |
|
auth required pam_nologin.so |
|
auth sufficient ${PAM_DUO} |
|
auth required pam_deny.so |
|
|
|
account required pam_nologin.so |
|
@include common-account |
|
|
|
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close |
|
session required pam_loginuid.so |
|
session optional pam_keyinit.so force revoke |
|
@include common-session |
|
session optional pam_motd.so motd=/run/motd.dynamic |
|
session optional pam_motd.so noupdate |
|
session optional pam_mail.so standard noenv |
|
session required pam_limits.so |
|
session required pam_env.so |
|
session required pam_env.so user_readenv=1 envfile=/etc/default/locale |
|
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open |
|
EOF |
|
fi |
|
|
|
echo " ✔ /etc/pam.d/sshd 已更新" |
|
|
|
######################################## |
|
# 8. 依模式產生 /etc/ssh/sshd_config |
|
######################################## |
|
|
|
echo |
|
echo "⚙️ sshd_config 設定模式:" |
|
echo " 1) 覆蓋為精簡配置(新主機 / 想重置設定時建議)" |
|
echo " 2) 在現有 sshd_config 末尾追加 Duo 設定" |
|
read -rp "請選擇 1/2(預設 1): " SSHD_MODE |
|
SSHD_MODE="${SSHD_MODE:-1}" |
|
|
|
if [[ "$MODE" == "1" ]]; then |
|
# 所有人:密碼 + Duo (keyboard-interactive → PAM) |
|
if [[ "$SSHD_MODE" == "1" ]]; then |
|
cat <<EOF | $SUDO tee /etc/ssh/sshd_config >/dev/null |
|
Port ${PORT} |
|
PermitRootLogin yes |
|
|
|
PubkeyAuthentication yes |
|
PasswordAuthentication yes |
|
KbdInteractiveAuthentication yes |
|
AuthenticationMethods keyboard-interactive |
|
|
|
UsePAM yes |
|
UseDNS no |
|
MaxAuthTries 10 |
|
|
|
AcceptEnv LANG LC_* |
|
Subsystem sftp /usr/lib/openssh/sftp-server |
|
EOF |
|
else |
|
cat <<EOF | $SUDO tee -a /etc/ssh/sshd_config >/dev/null |
|
|
|
# Duo SSH settings (mode 1: password + Duo) |
|
Port ${PORT} |
|
PasswordAuthentication yes |
|
KbdInteractiveAuthentication yes |
|
AuthenticationMethods keyboard-interactive |
|
UsePAM yes |
|
EOF |
|
fi |
|
|
|
elif [[ "$MODE" == "2" ]]; then |
|
# 所有人:Key + Duo (publickey + keyboard-interactive → pam_duo) |
|
if [[ "$SSHD_MODE" == "1" ]]; then |
|
cat <<EOF | $SUDO tee /etc/ssh/sshd_config >/dev/null |
|
Port ${PORT} |
|
PermitRootLogin yes |
|
|
|
PubkeyAuthentication yes |
|
PasswordAuthentication no |
|
KbdInteractiveAuthentication yes |
|
AuthenticationMethods publickey,keyboard-interactive |
|
|
|
UsePAM yes |
|
UseDNS no |
|
MaxAuthTries 10 |
|
|
|
AcceptEnv LANG LC_* |
|
Subsystem sftp /usr/lib/openssh/sftp-server |
|
EOF |
|
else |
|
cat <<EOF | $SUDO tee -a /etc/ssh/sshd_config >/dev/null |
|
|
|
# Duo SSH settings (mode 2: key + Duo) |
|
Port ${PORT} |
|
PasswordAuthentication no |
|
KbdInteractiveAuthentication yes |
|
AuthenticationMethods publickey,keyboard-interactive |
|
UsePAM yes |
|
EOF |
|
fi |
|
|
|
else |
|
# MODE 3: root = key + Duo, 其他 = 密碼 + Duo |
|
if [[ "$SSHD_MODE" == "1" ]]; then |
|
cat <<EOF | $SUDO tee /etc/ssh/sshd_config >/dev/null |
|
Port ${PORT} |
|
PermitRootLogin yes |
|
|
|
PubkeyAuthentication yes |
|
PasswordAuthentication yes |
|
KbdInteractiveAuthentication yes |
|
AuthenticationMethods keyboard-interactive |
|
|
|
UsePAM yes |
|
UseDNS no |
|
MaxAuthTries 10 |
|
|
|
AcceptEnv LANG LC_* |
|
Subsystem sftp /usr/lib/openssh/sftp-server |
|
|
|
Match User root |
|
PasswordAuthentication no |
|
AuthenticationMethods publickey,keyboard-interactive |
|
EOF |
|
else |
|
cat <<EOF | $SUDO tee -a /etc/ssh/sshd_config >/dev/null |
|
|
|
# Duo SSH settings (mode 3: root key+duo, others password+duo) |
|
Port ${PORT} |
|
PasswordAuthentication yes |
|
KbdInteractiveAuthentication yes |
|
AuthenticationMethods keyboard-interactive |
|
UsePAM yes |
|
|
|
Match User root |
|
PasswordAuthentication no |
|
AuthenticationMethods publickey,keyboard-interactive |
|
EOF |
|
fi |
|
fi |
|
|
|
######################################## |
|
# 9. 驗證 & 重啟 SSH |
|
######################################## |
|
|
|
echo |
|
echo "🧪 驗證 sshd 語法..." |
|
if $SUDO sshd -t 2>/tmp/sshd_check.log; then |
|
echo "🚀 語法 OK,重啟 ssh ..." |
|
$SUDO systemctl restart ssh |
|
echo "🎉 Duo 已成功啟用" |
|
else |
|
echo "❌ sshd 語法錯誤,ssh 未重啟。" |
|
echo "🔎 詳細錯誤:" |
|
cat /tmp/sshd_check.log |
|
echo "📁 備份檔案在:$BACKUP_DIR" |
|
exit 1 |
|
fi |
|
|
|
echo |
|
echo "✅ 完成!備份位置:$BACKUP_DIR" |
|
echo |
|
echo "👉 模式 1(密碼 + Duo):直接 ssh 登入,會先問密碼,再跳 Duo" |
|
echo "👉 模式 2(Key + Duo):必須先有公鑰登入成功,再跑 Duo" |
|
echo "👉 模式 3:root 只能 Key + Duo;其他帳號用 密碼 + Duo" |
|
echo |
|
echo "🍻 Done." |