Linux 服务器 SSH 加固相关脚本。
| Script | Description |
|---|---|
| install.sh | 一行 bootstrap 安装器:从 GitHub 拉取并安装 install-duo-ssh.sh 到 /usr/local/sbin/,可选择直接转发参数完成安装。 |
| install-duo-ssh.sh | 一键安装、配置、卸载和更新 Duo SSH 2FA。 |
最快的方式:
curl -fsSL https://raw.githubusercontent.com/KazuhaHub/ops-scripts/master/ssh/install.sh | sudo bashbootstrap 会下载 install-duo-ssh.sh 到 /usr/local/sbin/、做 bash -n 校验、安装 kh-duo 快捷方式。安装完后跑 sudo kh-duo 进交互菜单。
完全自动化部署(适合 Ansible / cloud-init / MDM)—— 把 Duo 凭据通过 -- 转发给主脚本:
curl -fsSL https://raw.githubusercontent.com/KazuhaHub/ops-scripts/master/ssh/install.sh \
| sudo bash -s -- \
--ikey DIXXXXXXXXXXXXXXXXXX \
--skey your-secret-key \
--host api-xxxxxxxx.duosecurity.com \
--yesbootstrap 会下载、安装、然后 exec 主脚本,把 -- 之后的所有参数转发过去,一气呵成。
⚠️ curl ... | sudo bash的 stdin 是 pipe 不是 TTY,所以主脚本的菜单不会自动触发(它要[[ -t 0 && -t 1 ]])。这是为啥不传参数时 bootstrap 装好就退出,让你下次单独跑sudo kh-duo。
raw.githubusercontent.com 在大陆经常不可达。把 bootstrap 自身和主脚本都走 jsDelivr CDN:
curl -fsSL https://cdn.jsdelivr.net/gh/KazuhaHub/ops-scripts@master/ssh/install.sh \
| sudo bash -s -- --cdn jsdelivr--cdn jsdelivr 让 bootstrap:
- 用 jsDelivr 下载主脚本(v1.2.0+ bootstrap 内置 jsDelivr / Statically / canonical 三个 prefix 的白名单)
- 自动持久化 jsDelivr 到
/etc/duo/install-duo-ssh.conf,未来kh-duo --update也走 CDN
支持的 provider:jsdelivr / statically / github(默认 = canonical)。
下载完都会跑多锚点 SHA256 quorum 校验(v1.2.0+),任意单一 CDN 被污染都会被另两个识别。
也可以组合 --channel beta:
curl -fsSL https://cdn.jsdelivr.net/gh/KazuhaHub/ops-scripts@beta/ssh/install.sh \
| sudo bash -s -- --cdn jsdelivr --channel betaKH_DUO_BOOTSTRAP_URL 环境变量可以覆盖下载源,但必须匹配下面三个 prefix 之一(v1.2.0+):
https://raw.githubusercontent.com/KazuhaHub/ops-scripts/https://cdn.jsdelivr.net/gh/KazuhaHub/ops-scripts@https://cdn.statically.io/gh/KazuhaHub/ops-scripts/
绝大多数场景用 --cdn 就够了,env 覆盖只在用自建镜像/企业代理时才需要。
把 SSH 登录改为:
SSH public key + Duo Push/Passcode
默认情况下,纯密码登录会被拒绝。脚本会安装 Duo 官方软件包,写入 /etc/duo/pam_duo.conf,修改 /etc/pam.d/sshd 和 /etc/ssh/sshd_config,并在成功校验后重启 SSH 服务。
- Debian 11+
- Ubuntu 20.04+
- RHEL 8+
- CentOS Stream 8+
- Rocky Linux / AlmaLinux / Oracle Linux
- Fedora 39+
- Amazon Linux 2023
- 在 Duo Admin Panel 创建或打开
UNIX Application。 - 准备以下三个值:
Integration key(ikey)Secret key(skey)API hostname(host)
- 确认当前 SSH 用户已经配置可用的 public key。
- 安装时保持一个现有 SSH 会话不要关闭,直到确认新会话可以正常登录。
下载脚本后执行交互式安装:
chmod +x install-duo-ssh.sh
sudo ./install-duo-ssh.sh使用参数进行非交互安装:
sudo ./install-duo-ssh.sh \
--ikey DIXXXXXXXXXXXXXXXXXX \
--skey your-secret-key \
--host api-xxxxxxxx.duosecurity.com \
--yes也可以通过环境变量传入 Duo 凭据:
sudo DUO_IKEY=DIXXXXXXXXXXXXXXXXXX \
DUO_SKEY=your-secret-key \
DUO_HOST=api-xxxxxxxx.duosecurity.com \
./install-duo-ssh.sh --yes| Option | Description |
|---|---|
--ikey VALUE |
Duo Integration Key。 |
--skey VALUE |
Duo Secret Key。 |
--host VALUE |
Duo API hostname。 |
--breakglass USER |
指定一个紧急用户绕过 Duo,仅允许 public key。 |
--bypass-local |
对 localhost 连接绕过 Duo,默认开启。 |
--no-bypass-local |
关闭 localhost 绕过。 |
--bypass-addr CIDR |
指定额外绕过 Duo 的来源网段,可重复使用。 |
--allow-password |
强制开启 password + Duo fallback(即使有 SSH key)。 |
--strict-publickey |
强制 publickey-only,没找到 key 时直接 abort(不允许密码登录)。 |
--skip-key-check |
跳过 authorized_keys 检查。 |
--use-cdn PROVIDER |
一键切换更新源到指定 CDN:jsdelivr / statically / github(v1.6.1+)。 |
--set-mirror URL |
持久化任意 https 更新 URL(覆盖 channel 默认)。 |
--clear-mirror |
移除持久化的 mirror,恢复 channel 默认。 |
--install-auto-update |
单独注册每天 04:00 自动更新(systemd timer,cron fallback)。 |
--remove-auto-update |
移除自动更新任务(systemd 和 cron 两边都清)。 |
--no-auto-update |
安装时不要自动注册更新任务(适合用 Ansible 等外部工具管理)。 |
--uninstall |
卸载 Duo SSH 2FA 并恢复 SSH 配置。 |
--no-menu |
即使在 TTY 中也不进入交互菜单。 |
-y, --yes |
自动确认提示,适合自动化执行。 |
-h, --help |
显示帮助。 |
直接跑 sudo kh-duo(或 sudo ./install-duo-ssh.sh)会进交互菜单:
What would you like to do?
1) Install / configure Duo 2FA (default — full setup with new credentials)
2) Adjust settings (keep credentials; change auth/bypass/breakglass)
3) Show current configuration
4) Uninstall Duo 2FA
5) Check for script updates
6) Install / refresh the 'kh-duo' shortcut
7) Quit without changes
Adjust settings (选项 2) 是 v1.2.0 新增的:自动从 /etc/duo/pam_duo.conf 和 /etc/ssh/sshd_config 的 Duo 块读出当前 ikey/skey/host 和所有开关,让你在不重新输凭据的情况下切换:
- 是否允许 password fallback
- 是否绕过 localhost
- 额外的 bypass CIDR(多个用逗号隔开)
- breakglass user
子菜单里多次切换会先在内存里累积,最后选 "5) Apply pending changes" 一次性写文件 + 重启 sshd。期间任何时候 q 都能放弃改动退出。
Show current configuration (选项 3) 不动任何东西,只 dump 当前生效的设置(含 ikey 前 8 位脱敏)。
安装快捷命令:
sudo ./install-duo-ssh.sh --install-shortcut
sudo kh-duo检查脚本版本(v1.1.1 起 --version 和 --check-update 不需要 root):
./install-duo-ssh.sh --version
./install-duo-ssh.sh --check-update更新脚本(写 $SCRIPT_PATH,需要 root):
sudo ./install-duo-ssh.sh --update
# 或保留兼容性,--self-update 是 alias:
sudo ./install-duo-ssh.sh --self-update默认更新源是 canonical GitHub:
https://raw.githubusercontent.com/KazuhaHub/ops-scripts/master/ssh/install-duo-ssh.sh
完整性由 多锚点 SHA256 quorum 校验 保证(即使 raw.github 自身被污染,也会被另两个独立锚点检测到)。
raw.githubusercontent.com 在大陆经常不可达。一行命令切到 jsDelivr CDN(公共服务,国内 POP 可达,内容直接从 GitHub 拉):
sudo kh-duo --use-cdn jsdelivr脚本会按当前 channel(stable / beta)自动拼对应分支的 jsDelivr URL,写到 /etc/duo/install-duo-ssh.conf。下次 --update 和每天 04:00 的自动更新都走 jsDelivr。
支持的 provider:
| provider | 用途 |
|---|---|
jsdelivr |
jsDelivr CDN,CN POP 可达,推荐 |
statically |
Statically CDN,备用 |
github |
等价于 --clear-mirror,回到 canonical |
不用担心 CDN 被攻破——下载完都会从三个独立 CDN(raw.github / jsDelivr / Statically)并发拉
.sha256做 quorum 校验。任意一个 CDN 单独被污染,都会被另外两个识别并拒绝。详见下面的防篡改设计。
首次安装就要 CN 加速?bootstrap 也支持,见 中国大陆一行安装(推荐 jsDelivr)。
如果你不想用 --use-cdn 提供的预设——例如想走企业内部镜像、自建 OSS、或某个特定加速器(ghproxy / ghfast):
# 设置一次(写到 root-owned 600 的 /etc/duo/install-duo-ssh.conf)
sudo kh-duo --set-mirror https://ghproxy.com/https://raw.githubusercontent.com/KazuhaHub/ops-scripts/master/ssh/install-duo-ssh.sh
# 看现在用什么 URL(任何用户都能看,纯只读)
kh-duo --show-config
# 撤销,回到默认 GitHub
sudo kh-duo --clear-mirror无论镜像怎么换,下载完都会跑多锚点 SHA256 quorum 校验,被污染的镜像会被立刻识别并拒绝。
URL 解析顺序(trust priority 由高到低):
/etc/duo/install-duo-ssh.conf里的update_url = ...KH_DUO_UPDATE_URL环境变量 —— 只在SUDO_USER为空时生效(裸 root,比如 cron / 系统服务)。sudo调用时被拒。- 默认 canonical GitHub URL
也可以直接编辑配置文件(任何能写 /etc/duo/ 的方式都行,比如 Ansible template):
# /etc/duo/install-duo-ssh.conf
update_url = https://cdn.jsdelivr.net/gh/KazuhaHub/ops-scripts@master/ssh/install-duo-ssh.sh两个独立威胁,分别防:
| 层 | 怎么防 |
|---|---|
| URL 来源 | 优先读 /etc/duo/install-duo-ssh.conf(root-owned 600)。KH_DUO_UPDATE_URL / KH_DUO_CHANNEL 在 sudo 上下文(SUDO_USER 非空)被拒,避免 sudo -E KH_DUO_UPDATE_URL=evil kh-duo 攻击。 |
$SCRIPT_PATH 所有权 |
--update 前要求脚本文件 owner 是 root。否则用户把脚本放 /home/<user>/ 里执行就能在 root 写下去之前换文件。 |
| PATH pin | 脚本顶部 PATH=/usr/sbin:/usr/bin:/sbin:/bin,curl/awk/install 等不会被攻击者的 $HOME/bin/ 同名脚本劫持。 |
威胁场景:脚本从某个 mirror 下载(默认 canonical github,或 admin 用 --use-cdn / --set-mirror 设的 CDN/代理),mirror 被攻破,attacker 替换 install-duo-ssh.sh。仅靠 shebang + bash -n 拦不住——attacker 写合法 bash 即可。
分层防御模型:canonical 权威,CDN 仅做后备。
| Tier | Anchor | 行为 | 用途 |
|---|---|---|---|
| 1 (authoritative) | raw.githubusercontent.com/<repo>/<branch>/.../install-duo-ssh.sh.sha256 |
可达即一锤定音:匹配 → 通过;不匹配 → 直接拒绝(不咨询 CDN) | github 是源头,它的 .sha256 就是信任边界 |
| 2 (fallback quorum) | cdn.jsdelivr.net + cdn.statically.io |
仅 Tier 1 不可达时启用:所有可达 fallback 必须一致 + 匹配下载内容 | CN 网络封 raw.github 的兜底 |
┌─ Tier 1 ────────────────────────────────────┐
│ raw.github → AUTHORITATIVE │ ← 可达即一锤定音
└─────────────────────────────────────────────┘
↓ (仅当 Tier 1 不可达)
┌─ Tier 2 ────────────────────────────────────┐
│ jsDelivr + Statically (quorum) │ ← 都可达且一致才通过
└─────────────────────────────────────────────┘
设计要点:
- canonical 权威:raw.github 已经是源代码本身的 source of truth,它的
.sha256就是同一个信任边界。可达且匹配 → 立刻通过;可达但不匹配 → 直接拒绝(不再咨询 CDN,避免 CDN 缓存陈旧"投票颠覆"权威源的失败模式)。 - CDN 只做 fallback:仅当 raw.github 不可达(CN 大陆典型场景)才退到 CDN quorum。jsDelivr + Statically 都返回 hash 且一致 → 通过;不一致 → 拒绝。
- 为什么不简单用扁平 quorum:jsDelivr / Statically 的
@<branch>边缘缓存有 ~12h TTL。每次 release push 后,多个 CDN 边缘可能同时返回陈旧.sha256,反而以多数派压过新鲜的 raw.github——这是 CDN 一致性问题,不是攻击。分层模型从源头消除这个 false positive。 - 3 个 anchor 并发拉取,每个 8s 超时;总耗时 < 1 秒。
.sha256 由 GitHub Actions (.github/workflows/update-sha256.yml) 在每次 install-duo-ssh.sh 提交后自动重生,并主动调用 jsDelivr 的 purge API 让 CDN 边缘尽快刷新(异步、尽力而为,不阻塞)。
信任优先级(高到低):
KH_DUO_PIN_SHA256环境变量 —— 带外信任,最高优先级。适合所有 anchor 全不可达 的极端隔离环境,hash 通过 VPN/sneakernet 拿到后塞 env 里。- Tier 1:raw.github canonical —— 可达即权威。匹配则通过,不匹配立即拒(明确建议用
--use-cdn github重新走 canonical 下载)。 - Tier 2:CDN fallback quorum —— 仅 Tier 1 不可达时启用。所有可达 fallback anchor 必须一致 + 匹配下载内容。
| 攻击者控制 | 结果 |
|---|---|
| 任一单一 CDN(默认下载源被攻破) | ❌ 拒绝(Tier 1 raw.github 权威不匹配) |
用户自定义 --set-mirror 被攻破 |
❌ 拒绝(同上) |
| 同时攻破 jsDelivr 和 Statically(且 CN 用户 raw.github 不可达) | |
| 同时控制 GitHub 仓库本身(PAT 泄露) | |
本地 KH_DUO_PIN_SHA256 被普通用户篡改 |
❌ 拒绝(sudo 上下文 env 被忽略) |
| CDN 缓存陈旧(v1.6.2 老 bug) | ✅ 不再误报(Tier 1 直接拍板,不看 CDN) |
sudo ./install-duo-ssh.sh --uninstall卸载会尽量移除脚本写入的 Duo PAM 配置、SSH AuthenticationMethods 配置、Duo 软件包和仓库配置,并重启 SSH 服务。
脚本每次运行都会创建时间戳备份目录:
/root/duo-install-backup-YYYYMMDD-HHMMSS
如果配置校验或重启失败,脚本会尝试自动恢复备份并重启 SSH。
为了避免 v1.4.x 自更新崩溃那种悲剧波及整个 fleet,仓库分两条分支:
| 分支 | 用途 | 谁在用 |
|---|---|---|
master |
stable,fleet 默认 | 所有生产服务器 |
beta |
预览,新版本先在这里 soak | 一台/几台测试机 |
工作流:commit 先进 beta → 测试机自动更新到 beta 版本 → 跑几天没问题 → merge 到 master → fleet 第二天 04:00 全量更新。
切换通道(菜单或 flag):
# 把这台机器切到 beta(持久写到 /etc/duo/install-duo-ssh.conf)
sudo kh-duo --set-channel beta
# 或在菜单里:sudo kh-duo → 5) Update settings → 1) Switch update channel
# 切回 stable
sudo kh-duo --set-channel stablebootstrap 时直接选 beta:
curl -fsSL https://raw.githubusercontent.com/KazuhaHub/ops-scripts/beta/ssh/install.sh \
| sudo bash -s -- --channel betabootstrap 会从 beta 分支拉脚本 + 持久化 channel = beta 到配置文件,之后 kh-duo --update 自动跟 beta。
每次成功安装后,脚本会自动注册一个每天 04:00 跑 --update --no-menu --yes 的任务,跟 Windows 那边的 Kazuha Hub Auto Update 行为对齐。优先级:
- systemd timer —— 检测到
systemctl时安装/etc/systemd/system/kh-duo-update.{timer,service},Persistent=true+RandomizedDelaySec=15min(错过窗口会补跑、最多 15 分钟随机延迟避免 fleet 同时打满 GitHub) - cron fallback —— 没 systemd 时写
/etc/cron.d/kh-duo-update,跳点 sleep0–900秒再跑,同样的 fleet 错峰目的 - 两个都没 —— 报错让用户手动调度
kh-duo --show-config 顶部会显示当前用的是哪种、下次什么时候跑。--uninstall 会同时清两边(即使一开始用的 systemd,将来切到 cron 后再卸载也能干净)。
控制 flag:
sudo kh-duo --install-auto-update # 单独注册(不重装 Duo 的话)
sudo kh-duo --remove-auto-update # 单独移除
sudo kh-duo --no-auto-update # 安装 Duo 时跳过注册(外部工具管理时用)立即触发(不等 04:00):
# systemd
sudo systemctl start kh-duo-update.service
# cron
sudo /usr/local/sbin/install-duo-ssh.sh --update --no-menu --yes脚本会自动读取 /root/.ssh/authorized_keys 和 $SUDO_USER 主目录:
| 现状 | 默认行为 |
|---|---|
| 找到 SSH key | publickey + Duo(password 拒绝)—— 推荐配置 |
| 没有 SSH key | publickey OR password + Duo(避免管理员锁死自己),同时大字提醒强烈建议加 key |
--allow-password 仍然能强制打开 password fallback(即使有 key);--strict-publickey 则强制走 key-only,没有 key 时直接 abort。这两个 flag 互斥,优先级高于自动检测。
向导(sudo kh-duo 进菜单选 1)的 Step 2/5 也会按这个逻辑选择默认值,并在没找到 key 时把"强烈建议加 key"放在最显眼的位置。
- 不要把 Duo
skey提交到 Git 仓库或写进共享日志。 - 首次安装时保留一个已登录的 SSH 会话。
- 推荐配置一个受控的
--breakglass用户,用于 Duo 服务异常时的紧急恢复。 - 对生产服务器执行前,先在测试机验证登录流程。
普通用户没法直接跑这个脚本(require_root 拦截),但如果通过 sudo -E 或 sudoers env_keep 让 env 透传,仍可能被利用。v1.1.1 加了这些防御:
- PATH 固定为
/usr/sbin:/usr/bin:/sbin:/bin,curl/awk 等命令不会被攻击者放在$HOME/bin/的同名脚本劫持。 KH_DUO_UPDATE_URL白名单:必须以https://raw.githubusercontent.com/开头,否则在任何网络 I/O 之前拒绝。KH_DUO_SHORTCUT白名单:只允许/usr/local/{bin,sbin}/和/usr/{bin,sbin}/,防止把 symlink 投到/etc/cron.hourly/之类的自动执行路径。$SCRIPT_PATH必须 root-owned 才允许--self-update和--install-shortcut。否则把脚本放到/home/<user>/再 sudo 调用,等于把后续sudo kh-duo引向用户可写的目标。--check-update不再需要 root:纯只读,UX 改善的同时不增加风险。--version/--help也不需要 root(一直如此)。
如果你的 sudoers 里没有 Defaults env_keep += "...",并且团队不用 sudo -E 跑这个脚本,上述风险面其实很小;这些加固是 defense-in-depth。
检查 shell 脚本语法:
bash -n install-duo-ssh.sh