feat: add Cloudflare browser SSH + short-lived certificate setup script

This commit is contained in:
chunzhi 2026-04-12 12:31:47 +08:00
commit 0115f54b04

513
setup-cf-browser-ssh.sh Normal file
View File

@ -0,0 +1,513 @@
#!/usr/bin/env bash
# =============================================================================
# setup-cf-browser-ssh.sh
# Cloudflare Browser-rendered SSH + Short-lived Certificate 服务器端一键配置
# 支持系统Ubuntu / Debian
#
# 功能:
# 1. 安装 cloudflared如未安装
# 2. 可选:用 Tunnel Token 注册 cloudflared 系统服务
# 3. 写入 Cloudflare CA 公钥到 /etc/ssh/ca.pub
# 4. 配置 sshd_config 信任 Cloudflare CA
# 5. 配置用户名映射(支持 root 共享登录)
# 6. 校验 sshd 配置并 reload
#
# 用法:
# 交互式(推荐):
# sudo bash setup-cf-browser-ssh.sh
#
# 非交互式(全部参数传入):
# sudo bash setup-cf-browser-ssh.sh \
# --ca-pubkey "ecdsa-sha2-nistp256 AAAA... open-ssh-ca@cloudflareaccess.org" \
# --login-user root \
# --tunnel-token "eyJhIjoiNz..."
#
# 选项:
# --ca-pubkey <string|filepath> Cloudflare short-lived CA 公钥(内容或文件路径)
# --login-user <username> 浏览器终端登录的 Linux 用户默认root
# --tunnel-token <token> cloudflared Tunnel Token可选不传则跳过服务注册
# --skip-cloudflared 跳过 cloudflared 安装
# --allow-all-principals 允许任意 Access 用户登录 --login-user默认开启 root 时自动启用)
# --dry-run 只打印将要执行的操作,不实际修改
# -h, --help 显示帮助
#
# 回滚:
# 脚本会在修改前备份 sshd_config备份文件名如 sshd_config.bak.20260412_120000
# 如果 sshd -t 校验失败,脚本会自动还原备份并拒绝 reload。
# =============================================================================
set -euo pipefail
# ----- 颜色输出 -----
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "\n${CYAN}===> $*${NC}"; }
# ----- 默认值 -----
CA_PUBKEY=""
LOGIN_USER="root"
TUNNEL_TOKEN=""
SKIP_CLOUDFLARED=false
ALLOW_ALL_PRINCIPALS=false
DRY_RUN=false
SSHD_CONFIG="/etc/ssh/sshd_config"
CA_PUB_FILE="/etc/ssh/ca.pub"
BACKUP_SUFFIX="bak.$(date +%Y%m%d_%H%M%S)"
# ----- 参数解析 -----
while [[ $# -gt 0 ]]; do
case "$1" in
--ca-pubkey) CA_PUBKEY="$2"; shift 2 ;;
--login-user) LOGIN_USER="$2"; shift 2 ;;
--tunnel-token) TUNNEL_TOKEN="$2"; shift 2 ;;
--skip-cloudflared) SKIP_CLOUDFLARED=true; shift ;;
--allow-all-principals) ALLOW_ALL_PRINCIPALS=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help)
awk '/^# =====/{ if(n++) exit } n{ sub(/^# ?/,""); print }' "$0"
exit 0
;;
*) err "未知参数: $1"; exit 1 ;;
esac
done
# ----- 前置检查 -----
check_root() {
if [[ $EUID -ne 0 ]]; then
err "请使用 root 或 sudo 执行此脚本"
exit 1
fi
}
check_os() {
if [[ ! -f /etc/os-release ]]; then
err "无法检测操作系统,此脚本仅支持 Ubuntu/Debian"
exit 1
fi
# shellcheck source=/dev/null
source /etc/os-release
case "$ID" in
ubuntu|debian) info "检测到系统: $PRETTY_NAME" ;;
*)
err "此脚本仅支持 Ubuntu/Debian当前系统: $ID"
exit 1
;;
esac
}
# ----- 交互式收集缺失参数 -----
prompt_missing_params() {
# CA 公钥
if [[ -z "$CA_PUBKEY" ]]; then
echo ""
warn "未提供 Cloudflare CA 公钥。"
echo " 获取方式Cloudflare One → Access controls → Service credentials → SSH"
echo " → 选择对应 Application → 复制 CA public key"
echo ""
read -rp "请粘贴 CA 公钥(一行,以 ecdsa-sha2 或 ssh-rsa 开头),或输入本地文件路径: " CA_PUBKEY
fi
# 如果是文件路径,读取内容
if [[ -f "$CA_PUBKEY" ]]; then
info "从文件读取 CA 公钥: $CA_PUBKEY"
CA_PUBKEY="$(cat "$CA_PUBKEY")"
fi
# 校验公钥格式
if [[ ! "$CA_PUBKEY" =~ ^(ecdsa-sha2-nistp256|ssh-rsa|ssh-ed25519)[[:space:]] ]]; then
err "CA 公钥格式不正确,应以 ecdsa-sha2-nistp256 / ssh-rsa / ssh-ed25519 开头"
exit 1
fi
# 登录用户
if [[ -z "$LOGIN_USER" ]]; then
read -rp "浏览器终端登录的 Linux 用户 [root]: " LOGIN_USER
LOGIN_USER="${LOGIN_USER:-root}"
fi
# root 模式自动启用 allow-all-principals
if [[ "$LOGIN_USER" == "root" ]]; then
ALLOW_ALL_PRINCIPALS=true
fi
# Tunnel Token
if [[ -z "$TUNNEL_TOKEN" && "$SKIP_CLOUDFLARED" != true ]]; then
echo ""
echo -e " ${CYAN}Tunnel Token 获取方式:${NC}"
echo " Cloudflare One → Networks → Tunnels → 选择/创建 Tunnel → Install connector"
echo ""
echo " 控制台会给你一条类似这样的命令:"
echo -e " ${GREEN}sudo cloudflared service install eyJhIjoiZDZjN2MwNT...${NC}"
echo ""
echo " 你只需要复制 eyJ 开头的那串 Token 粘贴到下面即可。"
echo " (留空则跳过隧道服务注册,之后可手动执行)"
echo ""
read -rp "请输入 Tunnel Token: " TUNNEL_TOKEN
fi
}
# ----- 安装 cloudflared -----
install_cloudflared() {
step "安装 cloudflared"
if command -v cloudflared &>/dev/null; then
local ver
ver=$(cloudflared --version 2>&1 | head -1)
info "cloudflared 已安装: $ver(跳过安装)"
return 0
fi
if [[ "$DRY_RUN" == true ]]; then
info "[DRY-RUN] 将安装 cloudflaredapt 方式)"
return 0
fi
info "添加 Cloudflare GPG key 和 apt 源..."
mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg \
| tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main" \
| tee /etc/apt/sources.list.d/cloudflared.list >/dev/null
info "正在安装 cloudflared全程无交互..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq cloudflared
if command -v cloudflared &>/dev/null; then
info "cloudflared 安装成功: $(cloudflared --version 2>&1 | head -1)"
else
err "cloudflared 安装失败"
exit 1
fi
}
# ----- 注册 cloudflared 系统服务 -----
register_cloudflared_service() {
step "注册 cloudflared 系统服务"
if [[ -z "$TUNNEL_TOKEN" ]]; then
warn "未提供 Tunnel Token跳过服务注册"
echo ""
echo " 后续手动注册命令:"
echo " sudo cloudflared service install <YOUR_TUNNEL_TOKEN>"
echo ""
return 0
fi
if systemctl is-active --quiet cloudflared 2>/dev/null; then
info "cloudflared 服务已在运行(跳过)"
return 0
fi
if [[ "$DRY_RUN" == true ]]; then
info "[DRY-RUN] 将执行: cloudflared service install <TOKEN>"
return 0
fi
info "执行 cloudflared service install ..."
cloudflared service install "$TUNNEL_TOKEN"
if systemctl is-active --quiet cloudflared 2>/dev/null; then
info "cloudflared 服务已启动"
else
warn "cloudflared 服务注册完成但未自动启动,尝试启动..."
systemctl start cloudflared
systemctl enable cloudflared
fi
}
# ----- 写入 CA 公钥 -----
write_ca_pubkey() {
step "写入 Cloudflare CA 公钥 → $CA_PUB_FILE"
if [[ "$DRY_RUN" == true ]]; then
info "[DRY-RUN] 将写入 CA 公钥到 $CA_PUB_FILE"
return 0
fi
# 幂等:检查是否已包含相同公钥
if [[ -f "$CA_PUB_FILE" ]]; then
if grep -qF "$CA_PUBKEY" "$CA_PUB_FILE" 2>/dev/null; then
info "CA 公钥已存在于 $CA_PUB_FILE(跳过)"
return 0
fi
# 文件存在但内容不同,追加
info "$CA_PUB_FILE 已存在,追加新公钥"
echo "$CA_PUBKEY" >> "$CA_PUB_FILE"
else
echo "$CA_PUBKEY" > "$CA_PUB_FILE"
fi
chmod 644 "$CA_PUB_FILE"
info "CA 公钥已写入"
}
# ----- 配置 sshd_config -----
configure_sshd() {
step "配置 $SSHD_CONFIG"
if [[ ! -f "$SSHD_CONFIG" ]]; then
err "$SSHD_CONFIG 不存在"
exit 1
fi
# 备份
local backup="${SSHD_CONFIG}.${BACKUP_SUFFIX}"
cp "$SSHD_CONFIG" "$backup"
info "已备份: $backup"
if [[ "$DRY_RUN" == true ]]; then
info "[DRY-RUN] 将修改 $SSHD_CONFIG"
info "[DRY-RUN] PubkeyAuthentication yes"
info "[DRY-RUN] TrustedUserCAKeys $CA_PUB_FILE"
if [[ "$ALLOW_ALL_PRINCIPALS" == true ]]; then
info "[DRY-RUN] AuthorizedPrincipalsCommand (allow all for $LOGIN_USER)"
fi
return 0
fi
local changed=false
# --- PubkeyAuthentication yes ---
if grep -qE '^\s*PubkeyAuthentication\s' "$SSHD_CONFIG"; then
# 如果存在但不是 yes替换
if ! grep -qE '^\s*PubkeyAuthentication\s+yes' "$SSHD_CONFIG"; then
sed -i 's/^\s*PubkeyAuthentication\s.*/PubkeyAuthentication yes/' "$SSHD_CONFIG"
info "已修改 PubkeyAuthentication → yes"
changed=true
else
info "PubkeyAuthentication yes 已存在"
fi
else
# 不存在,添加到文件顶部(跳过注释)
sed -i '1i PubkeyAuthentication yes' "$SSHD_CONFIG"
info "已添加 PubkeyAuthentication yes"
changed=true
fi
# --- TrustedUserCAKeys ---
local ca_directive="TrustedUserCAKeys $CA_PUB_FILE"
if grep -qE '^\s*TrustedUserCAKeys\s' "$SSHD_CONFIG"; then
if ! grep -qF "$ca_directive" "$SSHD_CONFIG"; then
sed -i "s|^\s*TrustedUserCAKeys\s.*|$ca_directive|" "$SSHD_CONFIG"
info "已修改 TrustedUserCAKeys → $CA_PUB_FILE"
changed=true
else
info "TrustedUserCAKeys 已正确配置"
fi
else
# 添加到 PubkeyAuthentication 之后
sed -i "/^PubkeyAuthentication/a $ca_directive" "$SSHD_CONFIG"
info "已添加 $ca_directive"
changed=true
fi
# --- 用户名映射root / 共享账号模式)---
if [[ "$ALLOW_ALL_PRINCIPALS" == true ]]; then
configure_principals "$LOGIN_USER"
fi
# --- 确保 PermitRootLogin 允许公钥登录(仅 root 模式)---
if [[ "$LOGIN_USER" == "root" ]]; then
ensure_root_login
fi
if [[ "$changed" == true ]]; then
info "sshd_config 已修改"
else
info "sshd_config 无需修改"
fi
# --- 校验 ---
validate_and_reload "$backup"
}
# 配置 AuthorizedPrincipalsCommand让任意 Access 用户都可以登录指定用户
configure_principals() {
local user="$1"
# 标记行,用于幂等检查
local marker="# --- Cloudflare Access short-lived cert: $user ---"
if grep -qF "$marker" "$SSHD_CONFIG" 2>/dev/null; then
info "AuthorizedPrincipalsCommand$user)已配置(跳过)"
return 0
fi
warn "⚠️ 将允许任意通过 Cloudflare Access 认证的用户以 '$user' 身份登录"
warn "⚠️ 请确保你的 Access Policy 已正确限制可访问人员!"
# 对于 root使用全局 AuthorizedPrincipalsCommand
# 对于其他用户,使用 Match User 块
if [[ "$user" == "root" ]]; then
cat >> "$SSHD_CONFIG" << EOF
$marker
# 允许任意 Cloudflare Access 短期证书用户以 root 登录
# 安全性完全依赖 Cloudflare Access Policy请务必正确配置
Match User root
AuthorizedPrincipalsCommand /bin/bash -c "echo '%t %k' | ssh-keygen -L -f - | grep -A1 Principals"
AuthorizedPrincipalsCommandUser nobody
EOF
else
cat >> "$SSHD_CONFIG" << EOF
$marker
Match User $user
AuthorizedPrincipalsCommand /bin/bash -c "echo '%t %k' | ssh-keygen -L -f - | grep -A1 Principals"
AuthorizedPrincipalsCommandUser nobody
EOF
fi
info "已添加 AuthorizedPrincipalsCommand$user"
}
# 确保 root 可以通过公钥登录
ensure_root_login() {
# PermitRootLogin 需要为 yes 或 prohibit-password 或 without-password
if grep -qE '^\s*PermitRootLogin\s+(no|forced-commands-only)\b' "$SSHD_CONFIG"; then
sed -i 's/^\s*PermitRootLogin\s.*/PermitRootLogin prohibit-password/' "$SSHD_CONFIG"
info "已修改 PermitRootLogin → prohibit-password允许证书登录禁止密码"
elif grep -qE '^\s*PermitRootLogin\s' "$SSHD_CONFIG"; then
info "PermitRootLogin 当前设置允许公钥登录"
else
echo "PermitRootLogin prohibit-password" >> "$SSHD_CONFIG"
info "已添加 PermitRootLogin prohibit-password"
fi
}
# ----- 校验并 reload -----
validate_and_reload() {
local backup="$1"
step "校验 sshd 配置"
if sshd -t 2>&1; then
info "sshd -t 校验通过 ✓"
else
err "sshd -t 校验失败!正在回滚..."
cp "$backup" "$SSHD_CONFIG"
info "已回滚到备份: $backup"
err "请手动检查 $SSHD_CONFIG 后重试"
exit 1
fi
step "重新加载 SSH 服务"
if systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null; then
info "SSH 服务已 reload ✓"
else
warn "systemctl reload 失败,尝试 service ssh reload..."
service ssh reload 2>/dev/null || service sshd reload 2>/dev/null || {
err "SSH 服务 reload 失败,请手动执行: systemctl reload ssh"
exit 1
}
fi
}
# ----- 打印后续手工步骤 -----
print_next_steps() {
echo ""
echo -e "${CYAN}============================================================${NC}"
echo -e "${CYAN} 服务器端配置完成!以下是你需要在 Cloudflare 控制台完成的步骤:${NC}"
echo -e "${CYAN}============================================================${NC}"
echo ""
echo " 1. 创建 / 确认 Self-hosted Application"
echo " → Cloudflare One → Access controls → Applications"
echo " → 添加 Self-hosted Application域名指向你的 SSH 服务"
echo ""
echo " 2. 开启 Browser Rendering"
echo " → 在 Application 设置中Browser rendering → 选择 SSH"
echo ""
echo " 3. 生成 Short-lived Certificate如果你还没生成 CA 公钥)"
echo " → Access controls → Service credentials → SSH"
echo " → Add a certificate → 选择你的 Application → Generate"
echo ""
echo " 4. 配置 Access Policy"
echo " → 在 Application 中配置谁可以访问(邮箱、域名、组等)"
echo ""
echo " 5. 测试"
echo " → 浏览器访问你配置的域名"
echo " → 完成 SSO 登录后,应出现网页 SSH 终端"
echo " → 用户名输入: $LOGIN_USER"
echo ""
if [[ "$LOGIN_USER" == "root" ]]; then
echo -e " ${YELLOW}⚠️ 重要提醒:${NC}"
echo -e " ${YELLOW} 你配置的是 root 登录模式,服务器安全性完全依赖 Access Policy。${NC}"
echo -e " ${YELLOW} 务必确保 Access Policy 只允许受信任的用户访问!${NC}"
echo ""
fi
if [[ -z "$TUNNEL_TOKEN" && "$SKIP_CLOUDFLARED" != true ]]; then
echo " ⓘ cloudflared 已安装但 Tunnel 服务未注册。"
echo " 如需注册,请在控制台获取 Token 后执行:"
echo " sudo cloudflared service install <YOUR_TUNNEL_TOKEN>"
echo ""
fi
echo -e "${GREEN} 配置文件位置:${NC}"
echo " CA 公钥: $CA_PUB_FILE"
echo " sshd_config: $SSHD_CONFIG"
echo " sshd 备份: ${SSHD_CONFIG}.${BACKUP_SUFFIX}"
echo ""
}
# =============================================================================
# 主流程
# =============================================================================
main() {
echo ""
echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Cloudflare Browser SSH + Short-lived Certificates Setup ║${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
check_root
check_os
prompt_missing_params
echo ""
info "配置摘要:"
info " 登录用户: $LOGIN_USER"
info " 允许任意 principal: $ALLOW_ALL_PRINCIPALS"
info " 安装 cloudflared: $( [[ "$SKIP_CLOUDFLARED" == true ]] && echo '跳过' || echo '是' )"
info " Tunnel Token: $( [[ -n "$TUNNEL_TOKEN" ]] && echo '已提供' || echo '未提供' )"
info " Dry Run: $DRY_RUN"
echo ""
if [[ "$DRY_RUN" != true ]]; then
read -rp "确认以上配置并继续?[Y/n] " confirm
if [[ "${confirm,,}" =~ ^n ]]; then
info "已取消"
exit 0
fi
fi
# 1. cloudflared
if [[ "$SKIP_CLOUDFLARED" != true ]]; then
install_cloudflared
register_cloudflared_service
else
info "跳过 cloudflared 安装(--skip-cloudflared"
fi
# 2. CA 公钥
write_ca_pubkey
# 3. sshd 配置
configure_sshd
# 4. 后续步骤
print_next_steps
}
main "$@"