From 0115f54b0423614b457649761e8a1d48ea716204 Mon Sep 17 00:00:00 2001 From: chunzhi Date: Sun, 12 Apr 2026 12:31:47 +0800 Subject: [PATCH] feat: add Cloudflare browser SSH + short-lived certificate setup script --- setup-cf-browser-ssh.sh | 513 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 setup-cf-browser-ssh.sh diff --git a/setup-cf-browser-ssh.sh b/setup-cf-browser-ssh.sh new file mode 100644 index 0000000..ffd326a --- /dev/null +++ b/setup-cf-browser-ssh.sh @@ -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 Cloudflare short-lived CA 公钥(内容或文件路径) +# --login-user 浏览器终端登录的 Linux 用户(默认:root) +# --tunnel-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] 将安装 cloudflared(apt 方式)" + 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 " + 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 " + 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 " + 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 "$@"