Loading...
时易うさぎのBlog
教程

SakuraFrp 使用自有域名穿透 Gitea,并配置 SSL 证书与 SSH

上班也有三年了,手里做过的项目慢慢多了起来。

之前代码基本都是散着放,时间长了以后越来越觉得,自己还是有必要弄一个专门的代码仓库。

并且所有的数据都只存在公司的服务器,也让我觉得没有安全感。

GitHub、码云之类的公共平台当然很好用,但对我来说总感觉不是特别适合存放一些私人项目,而且部分平台对单文件大小、仓库容量之类也会有一些限制。

而我的需求其实很简单:

  • 版本管理

  • 代码备份

  • 能在外面访问

  • 最好完全由自己控制

所以最后还是决定,在家里的服务器上自建一个 Git 服务。

为什么选择 Gitea

关于 GitLab 和 Gitea 的对比,网上已经有很多文章了,这里就不再赘述。

我最后选择 Gitea 的理由也非常简单:

够轻量,而且功能完全满足我的需求。

我的服务器上本身安装了 1Panel,所以部署 Gitea 基本没什么难度,直接在应用商店里一键安装即可。

本质上也是通过 Docker Compose 来部署。

Snipaste_2026-08-10_15-04-53

安装完成以后,在内网里直接访问对应 IP 和端口就可以进入 Gitea。

具体的安装配置没什么特别的,这里就不再单独贴截图了。

接下来真正需要解决的问题是:

怎么让这个运行在家里的 Gitea,在外网也能访问。

使用 SakuraFrp 进行内网穿透

我之前已经在家里的软路由 iKuai 上安装了 SakuraFrp,所以这里就直接继续用它。

另外,我注册了一个域名:

usagi.ink

并且已经完成备案。

这次准备用:

git.usagi.ink

作为私人 Git 服务的访问域名。

当然,如果不想使用自己的域名,也可以直接使用 SakuraFrp 分配的穿透地址。

功能上没有什么区别。

就是看着多少有点不太优雅(

Snipaste_2026-08-10_15-08-24

创建 Gitea 的 HTTP 隧道

首先进入 SakuraFrp 后台,新建一条隧道。

Snipaste_2026-08-10_15-10-44

本地 IP本地端口 中填写 Gitea 在局域网中的访问地址。

同时,在自动 HTTPS 的域名位置填写:

git.usagi.ink

需要注意的是,自己的域名要提前在 SakuraFrp 后台完成过白,否则无法用于穿透。

Snipaste_2026-08-10_15-12-06

配置完成后,重启 SakuraFrp 客户端,并启动刚刚创建的隧道。

随后查看运行日志,可以找到 SakuraFrp 分配给这条隧道的域名。

将自己的:

git.usagi.ink

添加一条 CNAME 解析,指向日志中提供的域名即可。

Snipaste_2026-08-10_15-15-40
Snipaste_2026-08-10_15-16-48

做到这里以后,实际上就已经可以通过自己的域名访问 Gitea 了。

不过现在还有一个问题:

SSL 证书。

给域名申请自己的 SSL 证书

此时直接访问的话,使用的是 SakuraFrp 自动生成的证书,浏览器可能会提示证书不受信任。

所以接下来还需要给:

git.usagi.ink

单独申请一张自己的 SSL 证书。

因为我使用的 1Panel 本身就带有证书管理功能,所以这里直接使用 1Panel 来申请和管理证书。

首先需要创建:

  • ACME 账户

  • DNS 账户

ACME 账户主要用于申请证书,DNS 账户则用于自动完成 DNS 验证。

ACME 账户配置很简单,填写自己的邮箱即可。

DNS 账户则需要根据域名当前使用的 DNS 服务商来选择。

我这里已经把域名接入了阿里云 ESA,所以使用对应的阿里云账户配置。

然后进入阿里云后台,获取对应的:

  • Access Key

  • Secret Key

填写进去即可。

Snipaste_2026-08-10_15-23-55
Snipaste_2026-08-10_15-25-45

在 1Panel 中申请证书

接下来点击申请证书。

填写需要申请的域名:

git.usagi.ink

然后依次选择:

  • 刚刚创建的 ACME 账户

  • DNS 验证

  • 对应的 DNS 账户

同时记得勾选:

自动续签

这样以后证书即将过期时,1Panel 就会自动使用刚刚配置的账户重新申请证书,不需要再手动操作。

Snipaste_2026-08-10_15-27-58

接着继续往下翻。

勾选:

推送证书到本地目录

然后选择一个用于保存证书的路径。

同时再勾选:

申请证书后执行脚本

Snipaste_2026-08-10_15-29-58

这里为什么需要执行脚本,就要说到 SakuraFrp 的证书加载方式了。

将证书同步到 SakuraFrp

证书申请完成后,我们需要将:

fullchain.pem

重命名为:

域名.crt

同时将:

privkey.pem

重命名为:

域名.key

以我这里的域名为例,也就是:

git.usagi.ink.crt
git.usagi.ink.key

然后将这两个文件复制到 SakuraFrp 的工作目录:

FrpcWorkingDirectory

之后重启隧道,SakuraFrp 就会自动加载这套证书。

手动操作的话,到这里其实就已经够用了。

但问题也很明显。

以后每次证书续签,都得再手动复制一次。

既然 1Panel 本身就支持在证书申请完成以后执行脚本,那自然没必要每次自己去折腾。

于是我让 Codex 帮我写了一个自动同步证书的脚本。

自动同步证书脚本

我的 SakuraFrp 是运行在 iKuai 上的,因此这里通过 SMB 共享,将 SakuraFrp 的工作目录暴露出来。

脚本负责:

  1. 临时挂载 iKuai 的 SMB 共享;

  2. 读取 1Panel 生成的新证书;

  3. 在临时目录中将证书重命名为 SakuraFrp 所需的格式;

  4. 将证书同步到 FrpcWorkingDirectory

  5. 校验证书文件是否复制成功;

  6. 最后自动卸载 SMB。

脚本如下:

#!/usr/bin/env bash
set -Eeuo pipefail

SOURCE_DIR="/opt/1panel/Certificate/git"

SMB_SHARE="//10.0.0.1/ikuai_skfrp"
MOUNT_POINT="/mnt/ikuai-skfrp"
TARGET_DIR="${MOUNT_POINT}/Docker-config/sakurafrp/FrpcWorkingDirectory"

CREDENTIAL_FILE="/root/.ikuai-skfrp-credentials"
LOCK_FILE="/run/lock/push-cert-to-sakurafrp.lock"

DOMAIN_NAME="git.usagi.ink"
TARGET_KEY="${DOMAIN_NAME}.key"
TARGET_CERT="${DOMAIN_NAME}.crt"

mounted_here=0
staging_dir=""

log() {
    printf '[%s] %s\n' "$(date '+%F %T')" "$*"
}

cleanup() {
    if [[ -n "${staging_dir}" && -d "${staging_dir}" ]]; then
        rm -rf -- "${staging_dir}"
    fi

    if [[ "${mounted_here}" -eq 1 ]] && mountpoint -q "${MOUNT_POINT}"; then
        log "卸载临时 SMB 挂载"

        timeout 15 umount "${MOUNT_POINT}" ||
            umount -l "${MOUNT_POINT}" ||
            true
    fi
}

trap cleanup EXIT

# 防止多个证书任务同时运行
mkdir -p "$(dirname "${LOCK_FILE}")"
exec 9>"${LOCK_FILE}"

if ! flock -n 9; then
    log "已有证书推送任务正在运行,本次退出"
    exit 0
fi

# 检查源目录
if [[ ! -d "${SOURCE_DIR}" ]]; then
    log "错误:证书目录不存在:${SOURCE_DIR}"
    exit 1
fi

if [[ ! -f "${SOURCE_DIR}/privkey.pem" ]]; then
    log "错误:找不到私钥:${SOURCE_DIR}/privkey.pem"
    exit 1
fi

if [[ ! -f "${SOURCE_DIR}/fullchain.pem" ]]; then
    log "错误:找不到完整证书链:${SOURCE_DIR}/fullchain.pem"
    exit 1
fi

# 检查凭据文件
if [[ ! -f "${CREDENTIAL_FILE}" ]]; then
    log "错误:SMB 凭据文件不存在:${CREDENTIAL_FILE}"
    exit 1
fi

if [[ "$(stat -c '%a' "${CREDENTIAL_FILE}")" != "600" ]]; then
    log "错误:SMB 凭据文件权限必须是 600"
    exit 1
fi

# 在临时目录准备待推送文件,避免修改 1Panel 原始证书
staging_dir="$(mktemp -d /tmp/push-sakurafrp-cert.XXXXXX)"
chmod 700 "${staging_dir}"

log "准备证书文件"

# 复制所有文件,并跟随可能存在的符号链接
cp -aL "${SOURCE_DIR}/." "${staging_dir}/"

# 只在临时目录中重命名
mv -- \
    "${staging_dir}/privkey.pem" \
    "${staging_dir}/${TARGET_KEY}"

mv -- \
    "${staging_dir}/fullchain.pem" \
    "${staging_dir}/${TARGET_CERT}"

# 私钥仅允许 root 读取
chmod 600 "${staging_dir}/${TARGET_KEY}"
chmod 644 "${staging_dir}/${TARGET_CERT}"

mkdir -p "${MOUNT_POINT}"

# 如果尚未挂载,则临时挂载
if ! mountpoint -q "${MOUNT_POINT}"; then
    log "挂载 iKuai SMB 共享"

    timeout 30 mount -t cifs \
        "${SMB_SHARE}" \
        "${MOUNT_POINT}" \
        -o "credentials=${CREDENTIAL_FILE},vers=2.0,sec=ntlmssp,iocharset=utf8,noserverino,uid=0,gid=0,file_mode=0600,dir_mode=0700"

    mounted_here=1
fi

# 防止挂载失败后误写入本地目录
filesystem_type="$(findmnt -n -o FSTYPE --target "${MOUNT_POINT}")"

if [[ "${filesystem_type}" != "cifs" ]]; then
    log "错误:${MOUNT_POINT} 不是 CIFS 挂载点,拒绝复制"
    exit 1
fi

mounted_source="$(findmnt -n -o SOURCE --target "${MOUNT_POINT}")"

if [[ "${mounted_source}" != "${SMB_SHARE}" ]]; then
    log "错误:挂载点连接的是其他共享:${mounted_source}"
    exit 1
fi

mkdir -p "${TARGET_DIR}"

log "开始推送证书到:${TARGET_DIR}"

timeout 300 rsync \
    -rLt \
    --checksum \
    --no-owner \
    --no-group \
    --no-perms \
    --omit-dir-times \
    --partial \
    --partial-dir=".rsync-partial" \
    --delay-updates \
    --itemize-changes \
    "${staging_dir}/" \
    "${TARGET_DIR}/"

# 校验关键文件是否完整
if ! cmp -s \
    "${staging_dir}/${TARGET_KEY}" \
    "${TARGET_DIR}/${TARGET_KEY}"; then
    log "错误:私钥复制后校验失败"
    exit 1
fi

if ! cmp -s \
    "${staging_dir}/${TARGET_CERT}" \
    "${TARGET_DIR}/${TARGET_CERT}"; then
    log "错误:证书复制后校验失败"
    exit 1
fi

log "证书推送和校验完成"
log "私钥:${TARGET_DIR}/${TARGET_KEY}"
log "证书:${TARGET_DIR}/${TARGET_CERT}"

这样设置以后,每次 1Panel 自动续签证书,脚本都会跟着执行。

新的证书会自动同步到 SakuraFrp 的工作目录中。

Snipaste_2026-08-10_15-58-40

也就是说,只要第一次配置完成,以后的证书续签基本就不需要再人工干预了。

到这里,Gitea 的 HTTPS 访问部分就算配置完成。

配置 Git SSH

如果只是通过网页访问 Gitea,到上一步其实已经结束了。

但平时使用 Git 的时候,我还是更习惯通过 SSH 进行 Push 和 Pull。

因此还需要在 SakuraFrp 中额外创建一条 TCP 隧道

在隧道配置中填写:

  • Gitea 所在服务器的 IP

  • Gitea 的 SSH 端口

这里需要注意:

外部端口和内部端口需要保持一致。

Snipaste_2026-08-10_15-41-46

创建完成以后,和前面的 HTTP 隧道一样,从 SakuraFrp 日志中找到对应的域名。

然后再给 SSH 使用的域名添加一条 CNAME 记录。

Snipaste_2026-08-10_15-50-03

修改 Gitea 的 SSH 配置

隧道建好以后,还需要修改 Gitea 本身的配置文件。

首先将 Gitea 中配置的 SSH 端口修改为刚刚在 SakuraFrp 中填写的端口。

同时不要忘记修改域名。

否则以后在 Gitea 仓库页面中直接复制出来的 Clone 地址还是原来的地址,每次都得自己手动修改。

Snipaste_2026-08-10_15-43-27
Snipaste_2026-08-10_15-46-11

最后,容器本身对应的端口配置也不要忘记修改。

Snipaste_2026-08-10_15-48-06

修改完成并重新加载配置以后,网页访问和 Git SSH 基本就都可以正常使用了。

最后

到这里,我这套 Gitea 的内网穿透就算彻底配置完成了。

最后的结构大概就是:

同时 SSL 证书也通过:

实现了自动更新。

对于我这种需求比较简单、只是想给自己弄一个私人代码仓库的人来说,目前这套方案用起来还是挺舒服的。

平时在家里可以直接走内网,在外面则通过 SakuraFrp 访问。

证书续签也不用再手动管。

最后再提醒一句:

如果 Gitea 只是自己使用,记得关闭新用户注册。

毕竟都已经叫私人 Git 了,就没必要在公网给自己多留一个入口了(

分享到

评论