Git Server for Bare

自建 Git Server 时,单人场景最佳方案是使用裸仓。

结论:如果没有网络环境或数据隐私要求,使用免费的私有或公共仓库吧。比如:github.com,另外,也是有免费的组织。

由于 Git 协议本身只负责数据同步,没有远程创建仓库的 API。所以,客户端无法直接远程新建仓库,只有安装 gitea 等服务器后,才可以在 WEB 上创建仓库。

单人场景:祼仓

使用 git init --bare 命令,再通过 SSH 协议直接访问服务器上的裸仓库文件,无任何常驻服务进程。

优点缺点
– 零额外内存占用:没有后台服务,只有 SSH 连接时才短暂调用 git 进程
– 零安装成本,系统自带 git 即可
– 绝对稳定,不存在服务崩溃问题
– 无 Web 界面,无法浏览器浏览代码、看提交记录
– 无细粒度权限:只能靠 Linux 系统用户 / 文件权限控制,所有人要么全读写要么全只读
– 仓库创建、密钥管理全部手动操作

快速搭建与使用

服务器配置与创建 bare 仓库

  1. 服务器:安装 git 客户端
$ git --version
# 使用PPA源安装吧,Ubuntu 是23年的2.43版本,而ppa里是2.53。
# 如果提示无 add-apt-repository 命令,需要先 `sudo apt install software-properties-common` 。另外,PPA 的源,在国内下载速度,都很慢。
$ sudo add-apt-repository ppa:git-core/ppa
$ sudo apt update
$ sudo apt install git

# 可选:Git LFS 用于二进制大文件支持,见附录
$ git lfs version
  1. 服务器:创建专用 git 账号
# 创建 git 用户,且切换 git
$ sudo useradd -m -s /bin/bash git
$ sudo passwd git
$ su - git

# git 用户下执行
$ mkdir .ssh/
$ touch .ssh/authorized_keys
$ chmod u=rw,g=,o= .ssh/authorized_keys
$ chmod u=rwx,g=,o= .ssh/
$ vim .ssh/authorized_keys      # 把公钥放进去
$ exit
  1. 服务器:使用 git 登录后,创建 bare 仓库
# 配置默认分支名,执行一次就可以了,避免 git init --bare 的提示
$ git config --global init.defaultBranch master
# 创建祼仓
$ mkdir -p ~/repo/demo.git/
$ cd ~/repo/demo.git
$ git init --bare    # 创建祼仓
$ exit

客户端:连接与首次同步

  1. [可跳过]配置 ~/.ssh/config 文件,作用是执行 ssh git@t725.cn 会自动去找相关配置,不需要在命令中指定。
Host t725.cn
    HostName git.t725.cn                  # Host 是 ssh 命令中的域名,而 HostName 是实际SSH服务域名,如果一样可以省略
    User git                              # ssh 命令中的用户
    #Port 443                             # 无则为22,有则以指定为准
    PreferredAuthentications publickey    # 指定认证方法=publickey
    IdentityFile D:\vps_git.pem           # 指定认证的私钥文件
    IdentitiesOnly yes                    # 忽略 ssh-agent
  1. 和 Github 等不一样(init 时,会有添加 LICENSE 与 README.md 文件),而祼仓 git init --bare 在创建后是空的,没有工作区,也没有分支名,所以:
    • 首次不能使用 git clone git@t725.cn:repo/demo.git d:\demo 命令
    • 首次需要先从客户端做一次 push 后,服务上才有内容,才会有分支名的,以及其他客户才可以使用 git cloen 命令
# 本地仓库已初化完成
PS D:> git remote add origin git@t725.cn:repo/BookmarkSync
PS D:> git push -u origin master   # -u 首次必须要有
PS D:> git branch -r               # 在没有上传前,是无值的。

安全配置

注册 git-shell 及两种安全方式

无论哪一种安全方式,都要先让  git-shell 注册为合法 shell(否则 SSH 会拒绝登录),因为默认是不注册的。

$ cat /etc/shells                     # 没有注册前的内容
# /etc/shells: valid login shells
/bin/sh
/usr/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/usr/bin/dash
/usr/bin/screen
/usr/bin/tmux

# 确认路径 /usr/bin/git-shell
$ command -v git-shell
# 注册为合法的 shell
$ grep -qxF '/usr/bin/git-shell' /etc/shells || echo '/usr/bin/git-shell' | sudo tee -a /etc/shells

方式一:git 用户的 shell 为 /usr/bin/git-shell

  • 优点:git 用户权限被严格限制
  • 缺点:未来要调整 git 用户配置不太方便,因为 git-shell 里能运行命令,只有git相关的,文件管理等bash命令都没有。
# 创用户时,指定shell
$ sudo adduser --system --group --home /var/lib/git --shell /usr/bin/git-shell --comment "Git service" git
# 修改已有用户 
$ sudo usermod -s /usr/bin/git-shell git

注意:

  1. home 是 /var/lib/git,目录默认权限是 drwxr-xr-x 2 root root 4.0K Apr 23 2024 ./
  2. 后续,这个账号在终端里登录后,也只有git-shell
  3. 另外,Linux 目录权限规则:要进入一个子目录,必须对路径上每一级父目录都拥有 x(execute/search)权限,所以也要注意上级目录的权限。可以没有r权限,但要有x权限。

方式二:authorized_keys 里限制命令,也是 gitea 等 git 服务器的标准做法

  1. 正常添加用户:
    • git 用户是可以正常使用 bash shell,但此证书,不做第2步的先处理,就是只有原PUB证书内容。
# -p '!' 是代表不能使用密码登录
$ sudo useradd -r -s /bin/bash -c 'Git Version Control' -m -d /home/git -U -p '!' git
  1. 在 .ssh/authorized_keys 添加 git 客户端证书时,要先处理。比如,原PUB证书 ssh-ed25519 xxxCret-Pubxxx Git@T725.CN,则完整内容如下:
command="git-shell -c \"$SSH_ORIGINAL_COMMAND\"",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 xxxCret-Pubxxx Git@T725.CN

echo 命令追加:

$ echo 'command="git-shell -c \"$SSH_ORIGINAL_COMMAND\"",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA... user@host' | sudo tee /var/lib/git/.ssh/authorized_keys
选项作用
command="..."强制无论客户端请求什么命令,都只执行引号内的命令;$SSH_ORIGINAL_COMMAND 保存了客户端实际请求的原始命令(如 git-upload-pack 'repo.git')
no-port-forwarding禁止 SSH 端口转发
no-X11-forwarding禁止 X11 图形转发
no-agent-forwarding禁止 SSH Agent 转发
no-pty禁止分配伪终端(阻止交互式 shell)

在其他用户里,以 git 用户来执行命令

  • 命令 sudo -u git cat ~/.ssh/authorized_keys 的写法是错误的,因为 shell 展开 ~ 发生在 sudo 之前,所以 ~ 仍然被展开为当前用户 $home 的路径,而非 git 用户的家目录。正确写法:
# 任选其一,分别是:使用绝对路径、让目标用户的 shell 来展开、直接以 root 读取(绕过权限检查)
$ sudo -u git cat /var/lib/git/.ssh/authorized_keys
$ sudo -u git bash -c 'cat ~/.ssh/authorized_keys'
$ sudo cat /var/lib/git/.ssh/authorized_keys

 git-shell 特点与自定义命令

  1. 默认的白名单命令,且不能交互;其它一律拒绝(包括 git log、git status、scp、sftp)。
服务端命令客户端在做什么
git-receive-pack / git receive-packgit push
git-upload-pack / git upload-packgit fetch / git clone / git pull
git-upload-archive / git upload-archivegit archive --remote
cvs server模拟 CVS 服务(git-cvsserver,很少用)
  1. 可扩展自定义命令:有 ~/git-shell-commands 之后,但默认是没有的。
    • 可执行该目录里的可执行文件:git-shell-commands/<命令> <参数>,或者在 ssh git@t725.cn "<命令> <参数>" 直接执行(注意:<命令>前不需要 git-shell-commands)。
    • 无参数登录会进 git> 提示符;有 help 就先跑它;输入 exit 断开
    • 若存在 no-interactive-login:先跑它,然后禁止交互登录(官方示例用来打印「认证成功但不提供 shell」)
  2. 注意:只要建了 git-shell-commands 目录,交互登录就打开了;若仍不想给人 shell,务必放上 no-interactive-login。官方禁用交互登录的写法:
mkdir -p ~/git-shell-commands
cat > ~/git-shell-commands/no-interactive-login <<'EOF'
#!/bin/sh
printf '%s\n' "Hi $USER! You've successfully authenticated, but I do not"
printf '%s\n' "provide interactive shell access."
exit 128
EOF
chmod +x ~/git-shell-commands/no-interactive-login
  1. 为什么自定义命令可以进入交互模式
    • 登录 Shell ≠ 脚本解释器,用户的 git-shell 限制是会话级别的,不是进程级别的。而这些自定义命令,是以用户身份,在创建的子进程中执行,所以自定义命令中,可以使用bash shell里所有命令,比如:文件管理等。

非交互登录的环境

如果想执行有x权限的 ~/xxx.sh 文件,需要使用 ssh user@ip "~/xxx.sh"。如果使用 ssh user@ip "xxx.sh" 不会被执行,因为裸命令只按 PATH 查找,不会自动看当前目录或HOME目录。

非交互登录的环境:ecs@t725.cn的 shell 是 /bin/bash,且无 authorized_keys 限制

# ssh ecs@t725.cn "printenv" 看 SHELL/PWD/SHLVL/PATH 的值。
_=/usr/bin/printenv
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
HOME=/home/ecs
LANG=C.UTF-8
LOGNAME=ecs
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
PWD=/home/ecs
SHELL=/bin/bash
SHLVL=0
SSH_CLIENT=x.x.x.x 12962 22
SSH_CONNECTION=x.x.x.x 12962 x.x.x.x 22
USER=ecs
XDG_RUNTIME_DIR=/run/user/1001
XDG_SESSION_CLASS=user
XDG_SESSION_ID=13951
XDG_SESSION_TYPE=tty

非交互登录的环境:git@t725.cn的 shell 是 /bin/bash,但有 authorized_keys 的 git-shell 限制

# ~/git-shell-commands/printEnv 的内容,且有执行权限;注意:第一行很重要!!!
#!/bin/bash
printenv

# ssh git@t725.cn "printEnv" 看 SHELL/PWD/SHLVL/PATH 的值。
_=/usr/bin/printenv
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/108/bus
HOME=/home/git
LANG=C.UTF-8
LOGNAME=git
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
PWD=/home/git
SHELL=/bin/bash
SHLVL=1
SSH_CLIENT=x.x.x.x 13232 22
SSH_CONNECTION=x.x.x.x 13232 x.x.x.x 22
SSH_ORIGINAL_COMMAND=printEnv
USER=git
XDG_RUNTIME_DIR=/run/user/108
XDG_SESSION_CLASS=user
XDG_SESSION_ID=13961
XDG_SESSION_TYPE=tty

便捷部署脚本 init

这个脚本,让你不需要登录到服务器上交互执行,可以通过 ssh user@server "init youRepoName" 直接非交互执行。

# 方式一(git 用户 shell 限制)的文件目录,但注意:下面命令不能直接使用的,只是表达意思
$ vim ~/git-shell-commands/init
$ chmod +x ~/git-shell-commands/init

# 方式二(authorized_keys 限制)的 init 文件目录,但注意:要使用没有限制证书,否则连接后没有shell
$ vim ~/init
$ chmod +x ~/init

init内容

#!/bin/bash

set -euo pipefail
REPO_BASE="$HOME/repo"   # 后续可在这里传入参数 $PUB_KEY_USER

if [[ $# -ne 1 ]]; then
    echo "Usage: init <repo-name>" >&2
    exit 1
fi
REPO_NAME="$1"
# 安全校验:禁止路径遍历、绝对路径、特殊字符
if [[ ! "$REPO_NAME" =~ ^[a-zA-Z0-9._-]+$ || "$REPO_NAME" == "." || "$REPO_NAME" == ".." ]]; then
    echo "Error: Invalid repository name '${REPO_NAME}'" >&2
    echo "Only alphanumeric characters, dots, hyphens and underscores are allowed." >&2
    exit 1
fi
REPO_PATH="${REPO_BASE}/${REPO_NAME}"
# 检查是否已存在
if [[ -e "$REPO_PATH" ]]; then
    echo "Error: Path already exists: ${REPO_PATH}" >&2
    exit 1
fi
# 检查基础目录是否可写
if [[ ! -w "$REPO_BASE" ]]; then
    echo "Error: No write permission to ${REPO_BASE}" >&2
    exit 1
fi
# 检查并设置 init.defaultBranch
DEFAULT_BRANCH=$(git config init.defaultBranch)
if [[ -z "$DEFAULT_BRANCH" ]]; then
    echo "INFO: init.defaultBranch masterr"
    git config --global init.defaultBranch master
fi

# ========== 创建裸仓库 ==========
mkdir -p "$REPO_PATH"
git init --bare "$REPO_PATH"

REPO_URL="git@t725.cn:${REPO_PATH#$HOME/}"
echo "✔ Initialized bare repository: ${REPO_PATH}"
echo "First sync, choose one of the two: "
echo "git clone $REPO_URL D:\$REPO_NAME"
echo "git remote add origin $REPO_URL && git push -u origin master"

在客户端上,远程执行

PS D:\Downloads> ssh git@t725.cn "init youRepoName"    # 注意不走 git-shell 是 ssh git@t725.cn "~/init youRepoName"
Initialized empty Git repository in /data/www/gitBare/youRepoName/
/home/git
[INFO] 仓库创建完成,目录内容如下:
HEAD  config  description  hooks/  info/  objects/  refs/

首次同步: git remote add origin git@t725.cn:repo/youRepoName && git push -u origin master
PS D:\Downloads>

git-shell 查看仓库名称的脚本 list

#!/bin/bash

set -euo pipefail

if [[ -z "${PUB_KEY_USER:-}" ]]; then
    echo "Error: PUB_KEY_USER is not set" >&2
    exit 1
fi
REPO_BASE="$HOME/$PUB_KEY_USER"
ls -C "$REPO_BASE"

发表回复