维护说明(2026 年 9 月 1 日):本页现在先提供一套可复现的现行方案,文末再原样保存完整的 2019 年源导出。旧文中的默认分支假设、排版造成的错误命令、私有局域网示例和注明出处的 CSDN 摘录都保留在档案中,请勿从档案复制执行。维护层使用占位符和官方资料,不含推广链接。
这套设计适用于运行 Raspberry Pi OS 的个人服务器或互相信任的小团队。Git 客户端可以通过 SSH 访问裸仓库,但不会得到交互式 shell。本文给出两种部署路线:
- 面向低风险静态目录的简短
post-receivehook; - 把 Git 访问和发布账号分开的 systemd 交接方案。
它们都不是完整代码托管平台:没有网页界面、Pull Request、按仓库授权或密钥扫描。若用户彼此并不完全信任,不要让大家共用一个不分仓库权限的 git 账号;应使用仍在维护的 Gitolite 或代码托管平台。
Table of Contents
1. 安装前先确定信任边界
先把树莓派放在可信局域网或 VPN 后面使用。不要为了完成教程就把 SSH 端口直接映射到公网。确需公网访问时,应先明确防火墙规则、补丁责任、监控、备份和密钥吊销流程。
以下示例假设:
- 管理账号名为
admin,拥有sudo权限; - 服务器名为
pi-git.local,或使用你控制的其他名称; - 仓库放在
/srv/git,而不是某个私人主目录; - Git 使用专用、不可交互登录的
git账号; - 默认分支被明确指定为
main。
替换名称和路径时要保持前后一致。应用密钥、上传文件、数据库和运行时可变数据必须放在部署工作树之外。
2. 安装 Git 并启用 SSH
全新 Raspberry Pi OS 默认禁用 SSH。Raspberry Pi Imager 可以在写入系统时设置仅公钥 SSH;已有系统则可以按官方文档使用 raspi-config 或系统服务。
在树莓派上用管理账号运行:
sudo apt update
sudo apt install --yes git openssh-server
sudo systemctl enable --now ssh
git --version
systemctl is-active ssh
按照操作系统的正常更新策略持续安装安全更新。在客户端首次接受 SSH 主机密钥之前,应通过可信渠道确认树莓派的地址和主机身份。
3. 创建仅用于 Git 的账号
创建不能使用密码登录的账号,找到 git-shell,再把它设为登录 shell:
sudo adduser --disabled-password --gecos '' git
command -v git-shell
grep -Fx /usr/bin/git-shell /etc/shells || printf '%sn' /usr/bin/git-shell | sudo tee -a /etc/shells
sudo chsh -s /usr/bin/git-shell git
getent passwd git
若 command -v git-shell 输出的不是 /usr/bin/git-shell,后续命令应全部换成实际路径。Git 官方把 git-shell 定义为受限登录 shell:它允许 push、fetch 和远程归档所需的服务端 Git 命令,但不提供普通交互式 shell。
不要为此直接编辑 /etc/passwd。chsh 会校验并记录 shell,无需手工改写账号数据库。
4. 添加受限制的 SSH 密钥
每台客户端各生成一把专用密钥。当前 OpenSSH 支持 Ed25519;Raspberry Pi 的 SSH 文档也把它列为基础 RSA 示例之外安全性更好的选择。
ssh-keygen -t ed25519 -a 64 -C 'laptop-to-pi-git'
除非无人值守进程另有经过记录的密钥保护方案,否则应设置口令。私钥只留在客户端,服务器只接收 .pub 公钥文件。
在服务器准备目录:
sudo install -d -m 0700 -o git -g git /home/git/.ssh
sudo touch /home/git/.ssh/authorized_keys
sudo chown git:git /home/git/.ssh/authorized_keys
sudo chmod 0600 /home/git/.ssh/authorized_keys
从客户端给公钥加上 OpenSSH 的 restrict 选项,再通过管理账号追加:
{ printf 'restrict '; cat ~/.ssh/id_ed25519.pub; }
| ssh admin@pi-git.local
'sudo tee -a /home/git/.ssh/authorized_keys >/dev/null'
restrict 会对这把密钥禁用端口转发、agent 转发、X11 转发、PTY 和 ~/.ssh/rc,但仍允许 Git 所需的远程命令。每个人和每个自动化任务都应使用带有可识别注释的独立公钥行,这样可以单独吊销一项凭证。
测试限制:
ssh -T git@pi-git.local
出现“未启用交互式 Git shell”之类的信息属于预期结果:认证成功后,shell 拒绝交互登录。
可选:全局强化 SSH
先为管理账号安装密钥,并在第二个终端验证。之后才考虑创建 /etc/ssh/sshd_config.d/00-local-hardening.conf:
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
重载前同时检查语法和最终生效值:
sudo sshd -t
sudo sshd -T | grep -E 'passwordauthentication|kbdinteractiveauthentication|permitrootlogin'
sudo systemctl reload ssh
在新建的公钥登录会话成功之前,不要关闭原来已经认证的管理会话。配置通过语法检查,并不代表访问策略一定符合你的意图。
5. 创建并测试裸仓库
裸仓库保存 refs、objects 和服务端 hooks,但没有检出的工作树。明确指定分支名,不依赖某一 Git 版本的默认值。
sudo install -d -m 0750 -o git -g git /srv/git
sudo -u git git init --bare --initial-branch=main /srv/git/sample.git
sudo -u git git --git-dir=/srv/git/sample.git config receive.denyNonFastForwards true
sudo -u git git --git-dir=/srv/git/sample.git config receive.denyDeletes true
sudo -u git git --git-dir=/srv/git/sample.git config --get-regexp '^receive.'
在已有本地仓库的客户端:
git remote add pi ssh://git@pi-git.local/srv/git/sample.git
git push -u pi HEAD:main
git ls-remote pi
也可以在另一个位置克隆:
git clone ssh://git@pi-git.local/srv/git/sample.git
这两项 receive 设置可以阻止普通分支被强制改写或删除,但它们不提供基于身份的授权。这个共享 git 账号接受的任何密钥,都能访问该账号本身有权读写的仓库。
6. 方案 A:面向小型静态目录的 Hook
只有在所有能推送 main 的人也都有权发布目标目录时,才使用此方案。目标目录只能包含可部署文件,不能混入上传内容、数据库或手工维护的配置。
创建目标目录和由服务器维护的 hook:
sudo install -d -m 0755 -o git -g git /srv/www/sample
sudoedit /srv/git/sample.git/hooks/post-receive
#!/bin/sh
set -eu
repo=/srv/git/sample.git
target=/srv/www/sample
deploy_ref=refs/heads/main
lock=/srv/git/sample.deploy.lock
while read -r oldrev newrev refname
do
[ "$refname" = "$deploy_ref" ] || continue
if ! git --git-dir="$repo" cat-file -e "$newrev^{commit}" 2>/dev/null
then
printf '%sn' 'The deploy branch was deleted or does not name a commit; skipping.' >&2
continue
fi
(
flock -x 9
git --git-dir="$repo" --work-tree="$target" checkout --force main
) 9>"$lock"
printf 'Deployed %s to %sn' "$newrev" "$target"
done
然后设置所有者和执行权限:
sudo chown git:git /srv/git/sample.git/hooks/post-receive
sudo chmod 0755 /srv/git/sample.git/hooks/post-receive
command -v flock
分支过滤可以阻止 tag 和其他分支触发部署;commit 校验可以处理删除分支的情况;路径加引号可避免分词;flock 让两次几乎同时发生的检出串行执行。
Git 在 refs 已经更新之后才调用 post-receive。它的输出会传回客户端,但非零退出码不能撤销已经接受的 push。因此,看到 Permission denied 表示“push 已存储、部署失败”,而不是“push 被拒绝”。必须拒绝 push 的策略应放在 pre-receive 或 update。
Hooks 是服务端文件,普通 clone 或 push 不会安装它们。应像基础设施配置一样备份、审查这些文件。
7. 方案 B:把 commit 入队,再由 systemd 发布
对于应用或更重要的静态站点,应把 SSH/Git 访问与 release 所有权分开。这里的 hook 只把经过验证的 commit ID 写入队列;systemd.path 发现队列后,以 sample-deploy 身份启动沙箱化 oneshot 服务。服务把 commit 导出到新的不可变 release 目录,再原子切换 current 符号链接。
这个例子仍假定所有能推送 main 的人都有权请求部署。它不会执行仓库中的构建脚本。
创建身份和目录
sudo groupadd --system sample-web
sudo useradd --system --home-dir /nonexistent --no-create-home
--shell /usr/sbin/nologin --gid sample-web --groups git sample-deploy
sudo usermod -aG sample-web www-data
sudo chmod -R g+rX /srv/git/sample.git
sudo install -d -m 2770 -o git -g git /var/lib/sample-deploy/queue
sudo install -d -m 0750 -o sample-deploy -g sample-web /srv/www/sample
sudo install -d -m 0750 -o sample-deploy -g sample-web /srv/www/sample/releases
把 www-data 替换成实际的只读 Web 服务账号;修改附加组后要重启该服务。配置服务器读取 /srv/www/sample/current,不要给它 release 写权限。
用仅入队的 Hook 替换简单 Hook
#!/bin/sh
set -eu
repo=/srv/git/sample.git
queue=/var/lib/sample-deploy/queue
deploy_ref=refs/heads/main
umask 027
while read -r oldrev newrev refname
do
[ "$refname" = "$deploy_ref" ] || continue
git --git-dir="$repo" cat-file -e "$newrev^{commit}" 2>/dev/null || continue
temporary="$queue/.${newrev}.$$"
printf '%sn' "$newrev" >"$temporary"
mv "$temporary" "$queue/$newrev"
printf 'Queued deployment of %sn' "$newrev"
done
安装部署程序
把下面内容保存为 /usr/local/sbin/deploy-sample,所有者设为 root,权限设为 0755:
#!/bin/sh
set -eu
repo=/srv/git/sample.git
queue=/var/lib/sample-deploy/queue
root=/srv/www/sample
releases=$root/releases
deploy_ref=refs/heads/main
temporary=
archive=
cleanup()
{
[ -z "$archive" ] || rm -f -- "$archive"
[ -z "$temporary" ] || rm -rf -- "$temporary"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
for job in "$queue"/*
do
[ -f "$job" ] || exit 0
commit=${job##*/}
case "$commit" in
''|*[!0-9a-fA-F]*)
printf 'Ignoring invalid deployment job: %sn' "$commit" >&2
rm -f -- "$job"
continue
;;
esac
if ! git --git-dir="$repo" cat-file -e "$commit^{commit}" 2>/dev/null
then
printf 'Commit is unavailable: %sn' "$commit" >&2
rm -f -- "$job"
continue
fi
current=$(git --git-dir="$repo" rev-parse "$deploy_ref^{commit}")
if [ "$commit" != "$current" ]
then
printf 'Skipping superseded deployment: %sn' "$commit"
rm -f -- "$job"
continue
fi
release=$releases/$commit
if [ ! -d "$release" ]
then
temporary=$releases/.${commit}.$$
archive=$releases/.${commit}.$$.tar
install -d -m 0750 "$temporary"
git --git-dir="$repo" archive --format=tar --output="$archive" "$commit"
tar -xf "$archive" -C "$temporary"
if find "$temporary" -type l -print -quit | grep -q .
then
printf '%sn' 'Symlinks are not permitted in this static release.' >&2
rm -f -- "$job"
exit 1
fi
rm -f -- "$archive"
archive=
mv "$temporary" "$release"
temporary=
fi
current=$(git --git-dir="$repo" rev-parse "$deploy_ref^{commit}")
if [ "$commit" != "$current" ]
then
printf 'Branch advanced while preparing: %sn' "$commit"
rm -f -- "$job"
continue
fi
next=$root/.current.$$
ln -s "$release" "$next"
mv -Tf "$next" "$root/current"
rm -f -- "$job"
printf 'Published %sn' "$commit"
done
这个程序只导出受 Git 跟踪的内容,并不执行它。两次分支尖端检查会丢弃已过时的队列项,避免按哈希文件名排序后反而最后发布旧提交;若推送恰好发生在最终切换符号链接时,其队列项会保留并触发下一次服务运行。若必须构建,应在明确威胁模型下加入单独审查过、具有资源限制的构建步骤;即使推送者已认证,push 仍然是不可信输入。
添加 Path 与 Service 单元
/etc/systemd/system/sample-deploy.path:
[Unit]
Description=Watch for queued sample deployments
[Path]
DirectoryNotEmpty=/var/lib/sample-deploy/queue
Unit=sample-deploy.service
[Install]
WantedBy=multi-user.target
/etc/systemd/system/sample-deploy.service:
[Unit]
Description=Publish a queued sample Git commit
[Service]
Type=oneshot
User=sample-deploy
Group=sample-web
SupplementaryGroups=git
ExecStart=/usr/local/sbin/deploy-sample
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/srv/git/sample.git
ReadWritePaths=/srv/www/sample /var/lib/sample-deploy
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX
验证并启用:
sudo systemd-analyze verify
/etc/systemd/system/sample-deploy.path
/etc/systemd/system/sample-deploy.service
sudo systemctl daemon-reload
sudo systemctl enable --now sample-deploy.path
sudo systemd-analyze security sample-deploy.service
sudo systemctl status sample-deploy.path
systemd-analyze verify 能发现单元文件错误;systemd-analyze security 会报告沙箱设置所暗示的暴露面,但两者都不能证明部署程序正确。完成一次真实 push 后检查:
sudo journalctl -u sample-deploy.service --since today
readlink -f /srv/www/sample/current
8. 验证与恢复清单
初次设置完成后以及每次有实质修改后,运行:
sudo sshd -t
sudo -u git git --git-dir=/srv/git/sample.git fsck --full
git ls-remote pi
还要验证以下事实:
- 交互式
ssh git@host会被拒绝; - 获准的 push 和 clone 正常;
- tag 或非部署分支不会发布;
- 删除部署分支不会清空站点;
- hook 或部署错误能在日志中看到,不会伪装成 push 被拒;
- 可以把
current指回旧 release 完成回滚; - 裸仓库、hooks、SSH 公钥、单元文件和部署脚本均纳入备份;
- 已在另一条路径或另一台机器上实际测试恢复。
相比 2019 年步骤,现行版本改了什么
| 2019 年档案细节 | 2026 年维护方案 |
|---|---|
| 生成 RSA 密钥,却没有口令策略 | 使用专用 Ed25519 密钥,并记录所有者、口令、限制和吊销方式 |
直接编辑 /etc/passwd | 用 chsh 和已安装的 git-shell 实际路径 |
| 仓库和部署路径混在多个主目录 | 裸仓库放 /srv/git,部署放 /srv/www,明确所有者 |
所有命令默认 master | 明确选择 main,并过滤完整 ref refs/heads/main |
占位 GIT_DIR 与前面创建的仓库不一致 | 全文使用同一个绝对仓库路径 |
| 部署报权限错误,后面却仍显示 push 成功 | 明确说明 post-receive 无法回滚已接受的 push |
把 chown git:git 当作通用权限修复 | 只在低风险静态部署中使用;更强隔离采用独立部署身份 |
| 复制的命令含排版破折号和弯引号 | 只在档案保留;维护命令使用可执行的 ASCII 选项和引号 |
| 第三方主机和私有 IP 看起来像可复制示例 | 仅视为历史字符串,不是现行推荐或背书 |
权威资料
- Raspberry Pi 文档:启用 SSH 与配置公钥认证
- Git 文档:`git init`、裸仓库与显式初始分支
- Git 文档:接收端历史保护设置
- Git 文档:`git-shell` 受限 SSH 访问
- Git 文档:Hook 执行方式与 `post-receive` 语义
- OpenSSH 手册:`ssh-keygen` 密钥类型与私钥保护
- OpenSSH 手册:`authorized_keys` 限制
- OpenSSH 手册:服务端配置
- systemd 上游手册源码:执行沙箱指令
- systemd 上游手册源码:Path 单元
- systemd 上游手册源码:Service 单元
- systemd 上游手册源码:`systemd-analyze verify` 与 `security`
软件包版本和默认值会变化。公开或自动化服务器之前,应查阅树莓派实际安装的手册(man git-init、man githooks、man sshd_config、man systemd.exec),并按真实版本验证。
2019 年源导出全文(原样存档,请勿执行)
1, Set up git envioronment
sudo apt-get install git
sudo adduser git
2, Generate public and private key
ssh-keygen -t rsa -C “user@lazying.art”
Client:
/home/pi/.ssh/id_rsa
Server: add public key of each client to the file below
/home/git/.ssh/authorized_keys
3, Initialize server end envioronment
mkdir Git
cd Git
sudo git init --bare sample.git
sudo chown -R git:git sample.git
vim /etc/passwd
git:x:1001:1001:,,,:/home/git:/bin/bash
git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell
4, Upload code to your github server
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
git add .
git commit -m "init commit"
git push -u origin master
5, Set-up Hooks
vim sapmle.git/hooks/post-receive
Add code below to post-receive
#!/bin/bash
TARGET="/home/webuser/deploy-folder"
GIT_DIR="/home/webuser/www.git"
BRANCH="master"
while read oldrev newrev ref
do
# only checking out the master (or whatever branch you would like to deploy)
if [ "$ref" = "refs/heads/$BRANCH" ];
then
echo "Ref $ref received. Deploying ${BRANCH} branch to production..."
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f $BRANCH
else
echo "Ref $ref received. Doing nothing: only the ${BRANCH} branch may be deployed on this server."
fi
done
chown git:git hooks/post-receive
chmod +x hooks/post-receive
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 237 bytes | 23.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
remote: Ref refs/heads/master received. Deploying master branch to production…
remote: error: unable to create file main.py (Permission denied)
remote: error: unable to create file test.py (Permission denied)
remote: Already on 'master'
To 192.168.1.108:/home/pi/Git/printer.git
2e6a796..6085cd0 master -> master
Causing of the hooks script cannot access deployment files, you should change the owner of deployment directory to git
chown git:git /path/to/deployment/directory/
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 239 bytes | 21.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
remote: Ref refs/heads/master received. Deploying master branch to production…
remote: Already on 'master'
To 192.168.1.108:/home/pi/Git/printer.git
6085cd0..b83c0d5 master -> master
________
1.在文件所在位置打开终端,输入如下命令,进行git全局设置:
git config –global user.name “用户名”
git config –global user.email “用户邮箱”
1. 安装如下命令创建新的仓库,网址为自己新建项目的网址,cd转到自己要上传的项目文件夹:
git clone https://git.aiiage.com:9999/song.yl/ReID.git
cd ReID
touch README.md
git add README.md
git commit -m “add README”
git push -u origin master
1. 对已经存在的文件夹进行操作, 可以不用cd命令转到文件夹,直接到文件夹下打开终端执行如下命令,其中git commit命令后引号里面的内容可以自己命名:
cd existing_folder
git init
git remote add origin https://git.aiiage.com:9999/song.yl/ReID.git
git add .
git commit -m “Initial commit”
git push -u origin master
1. 对存在的git仓库进行操作。依然可以直接在所在文件夹直接打开终端执行命令:
cd existing_foloder
git remote rename origin old-origin
git remote add origin https://git.aiiage.com:9999/song.yl/ReID.git
git push -u origin —all
git push -u origin –tags
## 作者:yllifesong
来源:CSDN
原文:https://blog.csdn.net/yllifesong/article/details/81041156
版权声明:本文为博主原创文章,转载请附上博文链接!
