无需开放 SSH 端口!使用 Cloudflare Tunnel 构建家用 Ubuntu Desktop 的安全 CD 流水线!

日本語|English|中国语
| 22 min read
Author: kosuke-uematsu kosuke-uematsuの画像
Information

为了覆盖更广泛的受众,这篇文章已从日语翻译而来。
您可以在这里找到原始版本。

引言

#

最近,我购买了一台用于 Ubuntu Desktop 的笔记本。想在家里托管 Web 应用是契机。

购买之后,我立即安装了 Apache Web 服务器,直接编辑 /var/www/html/index.html 并通过浏览器访问。我记得看到按预期显示时,心想“这太有趣了!”。

但是,这样下去会有问题。**如果在这种状态下向外部公开页面,编辑的内容会立即反映到生产环境中。**也就是说,访客可能会看到开发中的不完整代码或测试用数据。

“我想要将开发环境与生产环境分离……!”

因此,我创建了一个与 Apache Web 服务器的公开目录分离的开发目录,在那里进行开发。同时引入了 Git/GitHub 版本管理,并采用了从 dev 分支到 main 分支的拉取请求(Pull Request)及合并的通用开发流程。

然而,又出现了新的问题。引入这个开发流程后,每次开发完成都需要手动将开发目录的内容复制粘贴到公开目录。虽然在 GitHub 上合并了拉取请求,但还要在本地仓库的 main 分支的基础上,手动执行开发目录到公开目录的复制粘贴。这实在太麻烦了。

“希望一合并拉取请求,更新内容就能自动反映到 Apache Web 服务器的公开目录……!”

为了解决这繁琐的手动操作,我决定构建一个基于 GitHub Actions 的 CD 管道。

本文将结合具体的配置步骤,讲解如何使用 GitHub Actions 构建面向家庭局域网内 Ubuntu Desktop 的持续部署(CD)流水线。

构建的 CD 流水线

#

本文构建的 CD 流水线将按照以下机制运行。

实现的操作流程

#
graph TD
    A[代码编辑] --> B[提交到 dev 分支]
    B --> C[在 GitHub 创建 PR]
    C --> D[将 PR 合并到 main]
    D --> E[GitHub Actions 自动触发]
    E --> F[自动部署到家用服务器]
    F --> G[生产环境反映完成]

    style A fill:#e1f5fe
    style D fill:#fff3e0
    style G fill:#c8e6c9

手动进行的“从开发目录复制粘贴到公开目录”操作将以合并拉取请求为触发器实现完全自动化


系统架构

#
graph TD
    A[GitHub Actions Runner] --> B[Cloudflare Edge]
    B --> C[Cloudflare Tunnel]
    C --> D[家用 Ubuntu Desktop]
    D --> E[Apache Web Server]

    style A fill:#e1f5fe
    style B fill:#fff3e0
    style C fill:#fff3e0
    style D fill:#c8e6c9
    style E fill:#c8e6c9

组件 作用
GitHub Actions 以合并为触发器执行部署工作流程
Cloudflare Tunnel 提供无需公开家用 IP 的安全通信通道
Cloudflare Access 实现基于 Service Token 的自动认证
SSH + rsync 仅高效同步已更改的文件

完成后可以做什么

#
  • ✅ 只需合并拉取请求即可自动反映到生产环境
  • ✅ 在不公开家用公网 IP 的情况下安全运行
  • ✅ 部署历史会自动记录在 GitHub Actions 日志中
  • ✅ 完全解放手动部署操作

本文的前提条件

#

本文面向具备以下知识和环境的读者。

所需知识

#
  • GitHub Actions 基础:workflow 文件的编写方式及基本 Action 的使用方法
  • Linux 基本操作:命令行操作、文件权限、SSH 连接概念
  • Apache Web 服务器基础:DocumentRoot 概念及基本配置
  • Git 基础:分支、拉取请求、合并概念

所需环境

#
  • GitHub 仓库:私有或公共均可
  • Ubuntu Desktop 环境:本次以 Ubuntu 24.04.3 LTS 为例
  • Apache Web 服务器:已安装并配置
  • Cloudflare 账号:预期使用 Cloudflare Zero Trust
  • 自有域名:DNS 服务器已设置为 Cloudflare

所需技术概念与知识

#

在搭建流水线前,先梳理相关技术概念。

GitHub Actions 工作流

#

GitHub Actions 是在 GitHub 仓库中执行 CI/CD 流水线的一个平台。

重要概念

  • 工作流:在 .github/workflows/ 目录下通过 YAML 文件定义并执行一个或多个作业的流程
  • 事件:触发工作流的仓库内特定活动(如合并拉取请求)
  • 作业:在同一 Runner 上执行的工作流中一系列步骤
  • Action:在工作流中执行特定任务的组件(可以使用 GitHub Actions 提供的或自定义的)
  • Runner:执行工作流的虚拟环境。每个 Runner 同时只能执行一个作业。(本例中使用 ubuntu-latest)

SSH 连接与安全

#

SSH(Secure Shell)基础

SSH 是一种使用加密通信路径安全连接远程服务器的协议。

主机认证

SSH 客户端通过验证,确认连接的目标是真正的 SSH 服务器

主机认证的机制

SSH 服务器会设置一对称为 ホスト鍵(host key) 的唯一的私钥/公钥对。
SSH 客户端将此公钥保存在 ~/.ssh/known_hosts 文件中。

首次连接时,将 SSH 服务器的主机密钥(公钥)添加到 SSH 客户端的 ~/.ssh/known_hosts 文件。
之后每次连接时,将该文件中登记的主机密钥(公钥)与 SSH 服务器的主机密钥(私钥)进行比对,以确认连接目标是真正的 SSH 服务器。

用户认证

SSH 服务器端通过验证,确认接入的 SSH 客户端用户是真实的用户

用户认证的机制

使用公钥认证方式。
大致流程如下。

  1. SSH 服务器在 ~/.ssh/authorized_keys 文件中保存被允许的 SSH 客户端的公钥。
  2. SSH 客户端使用其私钥生成签名。
  3. SSH 服务器使用 SSH 客户端的公钥验证签名。
  4. 签名验证通过后,SSH 服务器允许以该 SSH 客户端用户身份连接。

Information

本次 SSH 连接中的角色

  • SSH 客户端: GitHub Actions 的 Runner 环境 (ubuntu-latest)
  • SSH 服务器: 家庭 Ubuntu Desktop

为什么会是这种关系?

GitHub Actions 的工作流在 GitHub 的云环境运行。
因此,为了从云环境向外部服务器(家用 Ubuntu Desktop)部署文件,必须由 GitHub Actions Runner 向家用服务器发起 SSH 连接。
也就是说,连接方向是“云 → 家”,GitHub Actions Runner 是发起连接的一方(客户端),家用 Ubuntu Desktop 是接收连接的一方(服务器)。


Cloudflare Tunnel

#

什么是 Cloudflare Tunnel

Cloudflare Tunnel 是一项无需公开公网 IP,便可将本地服务器安全暴露到互联网的服务


与传统的家庭服务器公开方法的区别

传统方法(端口映射):

互联网 → 路由器(开放端口22) → 家用 Ubuntu Desktop
                    ↑ 安全风险
  • ❌️ 需要在路由器上开放 SSH 端口(22 端口)
  • ❌ 公网 IP 地址直接暴露
  • ❌ 易成为 DDoS 攻击和暴力破解攻击的目标
  • ❌ 家庭 IP 地址有被识别的风险

使用 Cloudflare Tunnel 的方法:

GitHub Actions → Cloudflare Edge → Cloudflare Tunnel → 家用 Ubuntu Desktop
                                       ↑
                               加密隧道
  • ✅️ 无需在路由器上开放端口
  • ✅️ 隐藏公网 IP 地址
  • ✅️ 利用 Cloudflare 的安全功能
  • ✅️ 流量通过 Cloudflare Edge 进行过滤

本次架构中的角色

graph TD
    A[GitHub Actions Runner] --> B[Cloudflare Edge]
    B --> C[Cloudflare Tunnel]
    C --> D[家用 Ubuntu Desktop]
    
    style A fill:#e1f5fe
    style D fill:#c8e6c9
    style C fill:#fff3e0
  • 家用 Ubuntu Desktop 端cloudflared 守护进程持续连接到 Cloudflare
  • GitHub Actions 端:通过 cloudflared access ssh 命令经代理连接
  • Cloudflare:在双方之间提供安全隧道

安全性优势

  • IP 隐藏:家庭公网 IP 地址不会暴露
  • DDoS 防护:利用 Cloudflare 的安全基础设施
  • 访问控制:通过 Cloudflare Access 实现详细控制

Cloudflare Access

#

什么是 Cloudflare Access

Cloudflare Access 是 Cloudflare 提供的实现零信任网络访问(ZTNA)的安全服务。它取代传统 VPN,在应用层面提供认证与授权。

零信任网络访问(ZTNA)的概念

传统边界安全模型:

外部(危险) | 防火墙 | 内部(安全)
  • 在企业内部网络和互联网之间划定边界,通过防火墙阻断外部威胁
  • 以“内部网络可被信任”为前提
  • 一旦入侵内部,存在横向扩散风险

零信任方法:

验证所有访问 → 认证 → 授权 → 允许访问
  • “不信任,始终验证”(Never Trust, Always Verify)
  • 无论网络位置如何,对所有连接进行认证与授权

本次架构中 Cloudflare Access 的角色

sequenceDiagram
    participant GA as GitHub Actions
    participant CF as Cloudflare Access
    participant CT as Cloudflare Tunnel
    participant UD as Ubuntu Desktop

    GA->>CF: 1. SSH 连接请求 + 认证信息
    CF->>CF: 2. Access Policy 验证
    CF->>GA: 3. 认证成功
    GA->>CT: 4. 通过 cloudflared 代理进行 SSH 连接
    CT->>UD: 5. 通过安全隧道进行 SSH 连接
    UD->>GA: 6. SSH 连接建立

安全性优势

  • 细粒度访问控制:按资源和用户进行详细权限设置
  • 审计日志:记录所有访问尝试
  • 支持 MFA:可强制多因素认证(针对人类用户)
  • 实时控制:策略更改即时生效
  • 会话管理:对连接会话进行详细管理与监控

与传统 VPN 的区别

项目 传统 VPN Cloudflare Access
连接范围 整个网络 按应用划分
认证 仅在连接时 每次访问时验证
配置复杂度 复杂 相对简单
可扩展性 有限制
审计日志 有限 详细

基于 Service Token 的自动认证

什么是 Service Token

Service Token 是面向自动化系统和机器间通信的认证机制。它不同于需要人类用户交互的(OAuth、SAML 等)认证,支持完全编程化访问。

与传统用户认证的区别:

项目 用户认证 (OAuth/SAML) Service Token
对象 人类用户 自动化系统与应用
认证流程 基于浏览器・交互式 基于 API・编程式
认证信息 用户名・密码・MFA Client ID・Client Secret
有效期 以会话为单位(数小时) 长期

GitHub Actions 中的角色
1. 实现自动化的 SSH 连接

在 GitHub Actions 工作流中无法进行人为认证操作,因此 Service Token 是必不可少的。

GitHub Actions Runner → Service Token 认证 → Cloudflare Access → SSH 连接
2. CD 流水线的最佳理由
  • 无人执行:可在无人干预下 24 小时运行
  • 可扩展性:可供多个工作流同时使用
  • 可靠性:无需担心会话失效或超时
  • 安全性:通过 GitHub Secrets 进行安全管理
3. 自动化工作流中的安全优势
  • 最小权限原则:仅访问特定应用
  • 可审计性:所有访问操作都有日志记录
  • 集中管理:可在 Cloudflare 仪表盘中统一管理
认证流程概念

Service Token 认证流程:

sequenceDiagram
    participant GA as GitHub Actions
    participant CF as Cloudflare Access
    participant App as Protected Application

    GA->>CF: 1. Service Token(Client ID + Secret)
    CF->>CF: 2. Token 有效性验证
    CF->>CF: 3. Access Policy 评估
    CF->>GA: 4. 认证成功(Access Token)
    GA->>App: 5. 访问应用(Access Token)
    App->>GA: 6. 响应

通过 Cloudflare Access 的验证流程:

  1. Token 有效性验证:验证提供的 Client ID 与 Secret
  2. 策略评估:验证对应 Access Policy 的条件
  3. 访问许可判定:只有满足所有条件时才允许访问
  4. 审计日志记录:记录认证尝试及结果的详细日志

通过这一基于 Service Token 的认证机制,可以在不损害安全性的前提下,实现完全自动化的 CD 流水线。


完整版流水线整体概览

#

deploy.yml 文件全貌

#
name: Deploy to Server

on:
  pull_request:
   types: [closed]
   branches:
     - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    if: github.event.pull_request.merged == true

    env:
      REMOTE_HOST: ssh.your-domain.com
      REMOTE_USER: deploy-user
      REMOTE_DIR: /var/www/html
      SSH_PROXY_COMMAND: /tmp/cloudflared/cloudflared access ssh --id ${{ secrets.CLOUDFLARED_SSH_ID }} --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} --hostname %h

    steps:
      - name: Install cloudflared
        run: |
          latest_version=$(curl -s $GITHUB_API_URL/repos/cloudflare/cloudflared/releases/latest | jq -r '.tag_name')
          mkdir -p /tmp/cloudflared
          curl -sL -o /tmp/cloudflared/cloudflared $GITHUB_SERVER_URL/cloudflare/cloudflared/releases/download/$latest_version/cloudflared-linux-amd64
          chmod +x /tmp/cloudflared/cloudflared
          /tmp/cloudflared/cloudflared --version

      - name: Prepare .ssh/known_hosts from secrets
        run: |
          mkdir -p $HOME/.ssh
          chmod 700 $HOME/.ssh
          echo "${{ secrets.SSH_KNOWN_HOSTS }}" > $HOME/.ssh/known_hosts
          chmod 644 $HOME/.ssh/known_hosts

      - name: Set up SSH key
        run: |
          SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > "$SSH_KEY_PATH"
          chmod 600 "$SSH_KEY_PATH"

      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Deploy application via rsync
        run: |
          SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions
          rsync -rvz --no-group --no-perms --omit-dir-times \
            -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" \
            --include='public/***' \
            --include='src/***' \
            --include='views/***' \
            --include='composer.json' \
            --include='composer.lock' \
            --exclude='*' \
            ./ \
            $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/

      - name: Run composer install on remote
        run: |
          echo "Installing composer dependencies..."
          SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions
          ssh -i "$SSH_KEY_PATH" \
            -o StrictHostKeyChecking=yes \
            -o ProxyCommand="$SSH_PROXY_COMMAND" \
            $REMOTE_USER@$REMOTE_HOST "
              set -euo pipefail
              cd \"$REMOTE_DIR\"
              if command -v composer >/dev/null 2>&1; then
                COMPOSER_NO_INTERACTION=1 \
                composer install \
                  --no-dev \
                  --prefer-dist \
                  --no-interaction \
                  --no-progress \
                  --optimize-autoloader
                if sudo -n true 2>/dev/null; then
                  sudo chown -R www-data:www-data vendor
                  sudo chmod -R 755 vendor
                else
                  echo '⚠️ 由于没有 sudo 权限,跳过了 vendor 的所有权/权限更改'
                fi
                echo '✅ Composer install completed.'
              else
                echo '❌ composer not found on remote host.'
                exit 1
              fi
            "

流水线的操作流程

#
graph TD
    A[拉取请求合并到 main 分支] --> B[启动 GitHub Actions]
    B --> C[检出仓库代码]
    C --> D[安装 cloudflared]
    D --> E[SSH 配置与主机密钥注册]
    E --> F[通过 rsync 同步文件]
    F --> G[部署完成]
    F -.-> H[通过 Composer 安装依赖包<br/>※ 仅限 PHP 环境]
    H -.-> G
    
    style A fill:#e1f5fe
    style G fill:#c8e6c9
    style H fill:#fff2cc

各步骤详解

#

1. 触发条件

on:
  pull_request:
    types: [closed]
    branches: [main]
  • 在拉取请求关闭并针对 main 分支时执行
  • 仅在 github.event.pull_request.merged == true 时(已合并)才处理

2. 安装 cloudflared

latest_version=$(curl -s $GITHUB_API_URL/repos/cloudflare/cloudflared/releases/latest | jq -r '.tag_name')
mkdir -p /tmp/cloudflared
curl -sL -o /tmp/cloudflared/cloudflared $GITHUB_SERVER_URL/cloudflare/cloudflared/releases/download/$latest_version/cloudflared-linux-amd64
chmod +x /tmp/cloudflared/cloudflared
/tmp/cloudflared/cloudflared --version
  • 动态从 GitHub API 获取最新版本

3. 在 SSH 连接中预先注册主机密钥

mkdir -p $HOME/.ssh
chmod 700 $HOME/.ssh
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > $HOME/.ssh/known_hosts
chmod 644 $HOME/.ssh/known_hosts
  • 使用预先注册的主机密钥以建立安全的 SSH 连接
Caution

重视安全性的设计决策

在自动化系统中进行 SSH 连接时,可以使用 StrictHostKeyChecking=no 选项来省略首次连接的主机验证,从而简化设置。

# 简单但存在安全风险的做法
ssh -o StrictHostKeyChecking=no user@hostname "command"

然而,这种做法会带来以下安全风险:

  • ❌️ 中间人攻击(MITM)风险:恶意服务器可能冒充合法服务器
  • ❌️ 无法验证目标主机真实性:无法保证连接的是预期的正确服务器
  • ❌️ 妥协安全策略:为自动化而禁用了基本的安全检查

预先注册主机密钥的优点:

# 安全的做法
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
ssh -o StrictHostKeyChecking=yes user@hostname "command"  # 默认行为
  • ✅️ 验证目标主机:通过与预先注册的合法主机密钥比对,确保连接目标
  • ✅️ 防范中间人攻击:自动拒绝连接到不合法的服务器
  • ✅️ 自动化与安全并重:无需人工干预的安全连接
  • ✅️ 符合审计要求:构建满足安全标准的自动化系统

在此流水线中,我们优先考虑安全性而非自动化便利性,宁可承担事先获取和管理主机密钥的额外工作,也要采用更安全的做法。


4. 检出源代码

- name: Checkout repository
  uses: actions/checkout@v4
  • actions/checkout@v4:使用 GitHub Actions 官方 Action 获取仓库源代码
  • 准备要部署的文件:在通过 rsync 传输前,将最新的已合并代码放置到 Runner 环境

5. 文件同步

SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions
rsync -rvz --no-group --no-perms --omit-dir-times \
  -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" \
  --include='public/***' \
  --include='src/***' \
  --include='views/***' \
  --include='composer.json' \
  --include='composer.lock' \
  --exclude='*' \
  ./ \
  $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/

rsync 命令详解
  • SSH_KEY_PATH 变量:将 SSH 私钥路径变量化,提高可读性和可维护性
  • ed25519 密钥类型:使用比 RSA 更快速且安全的椭圆曲线加密
  • GitHub Actions 专用密钥:部署专用,便于识别的密钥名称

rsync 参数详解
rsync -rvz --no-group --no-perms --omit-dir-times
参数 说明 原因
-r 递归复制 保持目录结构传输所有文件
-v 详细输出 可在日志中查看传输状态
-z 压缩传输 高效利用网络带宽
--no-group 跳过组更改 避免权限错误
--no-perms 跳过权限更改 保持服务器上现有权限
--omit-dir-times 跳过目录时间戳 防止与时间戳相关的错误


SSH 连接设置
-e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'"
  • -e 标志:显式指定 rsync 使用的 SSH 命令
  • -i "$SSH_KEY_PATH":使用指定的私钥文件进行用户认证
  • StrictHostKeyChecking=yes:强制主机密钥验证(确保安全)
  • ProxyCommand='$SSH_PROXY_COMMAND':通过 Cloudflare Access 建立 SSH 连接

SSH_PROXY_COMMAND 详解
SSH_PROXY_COMMAND: /tmp/cloudflared/cloudflared access ssh --id ${{ secrets.CLOUDFLARED_SSH_ID }} --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} --hostname %h
参数 说明 作用
/tmp/cloudflared/cloudflared cloudflared 二进制路径 预先安装的 cloudflared 可执行文件
access ssh SSH 访问模式 指定通过 Cloudflare Access 进行 SSH 连接
--id ${{ secrets.CLOUDFLARED_SSH_ID }} Service Token ID 用于 Cloudflare Access 认证的客户端 ID
--secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} Service Token Secret Service Token 的客户端密钥(在 GitHub Secrets 中管理)
--hostname %h 动态主机名指定 在 SSH 连接时动态获取主机名(%h 为 SSH 占位变量)

为什么需要此命令?

GitHub Actions Runner(SSH 客户端)无法像人类一样通过浏览器输入 ID/密码或 MFA。要通过 Cloudflare Access 的网关,需要一种编程式的认证机制。

此命令的作用:

该命令会在 SSH 连接前自动执行 Cloudflare Access 认证,作为 ProxyCommand 提供必要的认证信息。具体来说,它使用 Service Token 通过 Cloudflare Access 认证,并通过 Cloudflare Tunnel 与家用服务器建立 SSH 连接。

SSH 代理命令的操作流程:

sequenceDiagram
    participant SSH as SSH Client
    participant CF as cloudflared
    participant CA as Cloudflare Access
    participant CT as Cloudflare Tunnel
    participant Server as Ubuntu Server

    SSH->>CF: 执行 ProxyCommand
    CF->>CA: 使用 Service Token 认证
    CA->>CF: 认证成功
    CF->>CT: 发起隧道中继连接请求
    CT->>Server: 转发 SSH 连接
    Server->>SSH: 建立 SSH 会话

通过此代理命令,GitHub Actions Runner(SSH 客户端)可以经 Cloudflare Access 认证并通过隧道安全地连接到家用服务器(SSH 服务器)。


文件选择逻辑

包含模式(Include):

--include='public/***'      # Web 公共文件
--include='src/***'         # 应用源代码  
--include='views/***'       # 模板文件
--include='composer.json'   # PHP 依赖定义
--include='composer.lock'   # 依赖锁定文件

排除模式(Exclude):

--exclude='*'              # 默认排除所有文件

选择性同步的优势:

  • 安全性:防止传输机密文件(.env, .git 等)
  • 效率:排除不必要文件(node_modules, vendor 等),实现高速传输
  • 安全性:控制可能影响生产环境的文件传输

指定源与目标
./ $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
  • ./:当前目录(检出的仓库根目录)
  • $REMOTE_USER:远程服务器用户名(例如:deploy-user
  • $REMOTE_HOST:目标主机名(例如:ssh.your-domain.com
  • $REMOTE_DIR:部署目标目录(例如:/var/www/html

通过此配置,实现了安全、高效且可控的文件同步

通过上述 4 个基本步骤,即可实现从 GitHub Actions 经 Cloudflare Access 到家用服务器的安全文件同步。


详细的配置步骤

#

下面介绍在相同环境中进行搭建的详细步骤。

1. Cloudflare 配置

#

参考

Cloudflare login


1.1 创建 Cloudflare Tunnel

  1. 进入 Zero Trust > 网络 > Tunnels
  2. 点击 创建隧道


  1. 点击 Cloudflared

  1. 输入隧道名称
  2. 点击 保存隧道

  1. 复制显示的安装命令

  1. 填写以下项目后,点击 完成设置

    项目 说明
    サブドメイン (可选)ssh
    ドメイン 已获取的自有域名
    タイプ SSH
    URL localhost:22

1.3 配置 Cloudflare Access

1.3.1 创建 Service Token
  1. 进入 Zero Trust > Access > 服务认证
  2. 点击 创建服务令牌

  1. 填写以下项目后,点击 创建令牌

    项目 说明
    サービストークン名 服务令牌名称
    サービストークンの有効期間 有效期,可自行选择

  1. 复制创建后的服务令牌的 Client IDClient Secret(稍后在 GitHub Secrets 中配置)
  2. 点击 保存


1.3.2 创建策略
  1. 进入 Zero Trust > Access > 策略
  2. 点击 添加策略

  1. 填写以下项目后,点击 保存

    项目 说明
    ポリシー名 自定义名称
    アクション Service Auth
    セッション時間 可自定义
    セレクター Service Auth
    1.3.1 创建 Service Token 中创建的服务令牌


1.3.3 创建应用
  1. 进入 Zero Trust > Access > 应用程序
  2. 点击 添加应用程序

  1. 点击 Self-hosted(自托管)

  1. 填写以下项目

    项目 说明
    アプリケーション名 任意的应用名称
    セッション時間 自定义会话时长
    サブドメイン (可选)ssh
    ドメイン 已获取的自有域名

    点击“添加公共主机名”可显示“公共主机名”输入框。

  2. 点击“选择现有策略”,并选择在 1.3.2 创建策略 中创建的策略

  3. 点击“下一步”

  1. 保持“体验设置”和“详细设置”为默认,根据个人环境调整后,点击“下一步”/“保存”


2. 在 Ubuntu Desktop 端的设置

#

2.1 安装及配置 SSH 服务器

# 安装 OpenSSH Server
sudo apt update
sudo apt install openssh-server

# 编辑 SSH 配置
sudo nano /etc/ssh/sshd_config

配置项:

# 禁用密码认证
PasswordAuthentication no

# 启用公钥认证
PubkeyAuthentication yes

# 禁用 root 登录
PermitRootLogin no

# 禁止使用空密码的账户登录
PermitEmptyPasswords no

# 最大认证尝试次数
MaxAuthTries 6

# 主机密钥
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key

配置后重启:

sudo systemctl restart ssh.socket
sudo systemctl enable ssh.socket

2.2 防火墙设置

# 启用防火墙
sudo ufw enable
# 默认拒绝传入 (Incoming) 流量
sudo ufw default deny incoming
# 默认允许传出 (Outgoing) 流量
sudo ufw default allow outgoing
# 开放 SSH 端口
sudo ufw allow ssh
# 防止 SSH 暴力破解攻击
sudo ufw limit ssh
# 查看防火墙状态
sudo ufw status

セキュアなSSHサーバの構築と運用ガイド(Ubuntu上)


2.3 创建部署专用用户

# 创建部署专用用户
sudo adduser deploy-user

# 为部署专用用户创建存放公钥的目录
sudo mkdir -p /home/deploy-user/.ssh
# 创建公钥文件(此时为空文件)
sudo touch /home/deploy-user/.ssh/authorized_keys
# 设置目录权限
sudo chmod 700 /home/deploy-user/.ssh
# 设置公钥文件权限
sudo chmod 600 /home/deploy-user/.ssh/authorized_keys
# 设置目录所有者
sudo chown -R deploy-user:deploy-user /home/deploy-user/.ssh

2.4 Apache Web 服务器配置

# 安装 Apache
sudo apt install apache2

# 设置 DocumentRoot 权限
find /var/www/html -type d -exec chmod 750 {} \;
find /var/www/html -type f -exec chmod 640 {} \;
sudo chown -R www-data:www-data /var/www/html

# 允许 deploy-user 在 www-data 组中写入
sudo usermod -aG www-data deploy-user

2.5 安装包管理器

Information

关于此步骤

包管理器的安装不属于本次主题(CD 流水线搭建),故省略详细内容。

本例中针对 PHP 环境,安装 Composer,其他开发语言请根据需要选择合适的包管理器。

  • PHP:Composer
  • Node.js:npm / yarn / pnpm
  • Python:pip / poetry / pipenv
  • Ruby:gem / bundler
  • Go:go mod
  • Rust:cargo
  • Java:Maven / Gradle

请参考各语言官方文档进行安装。


2.6 安装及配置 cloudflared

执行在 1.1 创建 Cloudflare Tunnel 中复制的安装命令。

# 安装 cloudflared
# 添加 Cloudflare GPG 密钥
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | sudo tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null

# 将此仓库添加到 apt 源
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# 安装 cloudflared
sudo apt-get update && sudo apt-get install cloudflared

服务化:

sudo cloudflared service install [TOKEN]
sudo systemctl start cloudflared
sudo systemctl enable cloudflared

3. 配置 GitHub Secrets

#

3.1 生成 SSH 密钥对

Caution
关于此步骤

SSH 密钥的生成及公钥的部署需在 2.3 创建部署专用用户 中创建的用户主目录下进行。

# 确认用户名
$ whoami
deploy-user

# 确认当前目录
$ pwd
/home/deploy-user

# 为 GitHub Actions 生成 SSH 密钥
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_github_actions -C "GitHub Actions"

# 将公钥部署到服务器
cat ~/.ssh/id_ed25519_github_actions.pub >> ~/.ssh/authorized_keys

3.2 获取主机密钥

连接到 Ubuntu Desktop 的 SSH 服务器,获取主机密钥。复制以下命令的输出。

ssh-keyscan localhost 2>/dev/null | sed 's/localhost/ssh.your-domain.com/'

请将 ssh-keyscan 命令获取到的主机密钥中包含的 localhost 替换为 Cloudflare Tunnel 的主机名,sed 命令中的 your-domain.com 请根据个人环境替换。


3.3 在 GitHub 仓库设置中的配置

SettingsSecrets and variablesActions 中点击 New repository secret,并配置以下内容。

名称
SSH_PRIVATE_KEY ~/.ssh/id_ed25519_github_actions 的内容
SSH_KNOWN_HOSTS 3.2 获取主机密钥 中复制的主机密钥内容
CLOUDFLARED_SSH_ID 1.3.1 创建 Service Token 中创建的服务令牌的 Client ID(不含前缀,直接粘贴)
CLOUDFLARED_SSH_SECRET 1.3.1 创建 Service Token 中创建的服务令牌的 Client Secret(不含前缀,直接粘贴)

4. 放置工作流文件

#

在仓库中创建 .github/workflows/deploy.yml,并放置前面介绍的完整版 YAML。


5. 检查运行

#

5.1 通过拉取请求进行测试

  1. 创建功能分支
  2. 提交一些更改
  3. 创建针对 main 分支的拉取请求
  4. 合并拉取请求
  5. 查看 GitHub Actions 运行日志

总结

#

本文介绍了如何使用 GitHub Actions 构建面向家用 Ubuntu Desktop 的持续部署(CD)流水线。

已达成内容

#
  • 完全自动化手动部署:以拉取请求合并为触发器,实现从开发目录到公开目录的文件同步自动化
  • 安全连接:利用 Cloudflare Tunnel 和 Cloudflare Access,在不公开家用公网 IP 的情况下实现安全 SSH 连接
  • 实现零信任架构:通过 Service Token 进行认证和访问控制,构建不降低安全性的自动化

重要要点

#
  1. 使用 Cloudflare Tunnel:无需在路由器上开放端口,即可安全地将家用服务器暴露给外部
  2. Service Token 认证:无需人类干预,实现 GitHub Actions 的安全自动访问
  3. 预先注册主机密钥:避免轻率使用 StrictHostKeyChecking=no,优先考虑安全性

此方法的优点

#
  • 提升开发效率:部署操作自动化,可专注开发
  • 确保安全性:通过多重安全层,对家用服务器进行安全运维
  • 可扩展性:可将相同机制应用于多个项目或服务器
  • 可审计性:所有部署操作都会记录在 GitHub Actions 和 Cloudflare 日志中

后续扩展

#

基于此流水线,还可考虑以下功能扩展。

  • 部署前的自动化测试执行
  • 支持多环境(预发布/生产)
  • 部署失败时自动回滚
  • 集成 Slack/Discord 通知

我认为,使用家用服务器进行 Web 托管是十分适合学习和实验的环境,过程非常有趣☺️


豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。