强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 2 章 - SSH 基础方案

第 2 章 - SSH 基础方案

使用裸仓库和 SSH 搭建最简单的 Git 服务器——零依赖、零额外服务,适合小团队快速上手。

2.1 架构概览

┌─────────────┐        SSH (22)        ┌──────────────────────┐
│  开发者 A    │ ◄────────────────────► │                      │
│  (ed25519)  │                         │   Git 服务器          │
├─────────────┤                         │   Ubuntu 22.04       │
│  开发者 B    │ ◄────────────────────► │                      │
│  (ed25519)  │                         │   /opt/git/          │
├─────────────┤                         │   ├── project-a.git  │
│  开发者 C    │ ◄────────────────────► │   ├── project-b.git  │
│  (ed25519)  │                         │   └── project-c.git  │
└─────────────┘                         └──────────────────────┘

特点

  • 无额外软件依赖,仅使用系统自带的 OpenSSH
  • 认证通过 SSH 密钥完成,安全可靠
  • 权限控制通过 Linux 文件系统权限实现
  • 无 Web 界面,纯命令行操作

2.2 服务端搭建

2.2.1 创建 Git 用户

# 创建专用的 git 用户
sudo adduser \
  --system \
  --shell /bin/bash \
  --group \
  --home /home/git \
  --disabled-password \
  git

# 确认用户创建成功
id git
# uid=1001(git) gid=1001(git) groups=1001(git)

2.2.2 初始化仓库目录

# 创建仓库根目录
sudo mkdir -p /opt/git
sudo chown git:git /opt/git
sudo chmod 755 /opt/git

# 切换到 git 用户创建裸仓库
sudo -u git bash
cd /opt/git

# 创建裸仓库
git init --bare project-a.git
git init --bare project-b.git

# 查看结构
ls -la project-a.git/
# total 24
# drwxr-xr-x 7 git git 4096 ... .
# drwxr-xr-x 3 git git 4096 ... ..
# drwxr-xr-x 2 git git 4096 ... branches
# -rw-r--r-- 1 git git   66 ... config
# -rw-r--r-- 1 git git   73 ... description
# -rw-r--r-- 1 git git   23 ... HEAD
# drwxr-xr-x 2 git git 4096 ... hooks
# drwxr-xr-x 2 git git 4096 ... info
# drwxr-xr-x 4 git git 4096 ... objects
# drwxr-xr-x 4 git git 4096 ... refs

exit

2.2.3 配置 SSH 公钥认证

# 确保 git 用户的 .ssh 目录存在
sudo mkdir -p /home/git/.ssh
sudo touch /home/git/.ssh/authorized_keys
sudo chown -R git:git /home/git/.ssh
sudo chmod 700 /home/git/.ssh
sudo chmod 600 /home/git/.ssh/authorized_keys

添加开发者的公钥:

# 假设有三个开发者,每人提供公钥
# 开发者 A
echo "ssh-ed25519 AAAAC3Nza...alice@work" | sudo tee -a /home/git/.ssh/authorized_keys

# 开发者 B
echo "ssh-ed25519 AAAAC3Nza...bob@work" | sudo tee -a /home/git/.ssh/authorized_keys

# 开发者 C
echo "ssh-ed25519 AAAAC3Nza...charlie@work" | sudo tee -a /home/git/.ssh/authorized_keys

# 确认
sudo cat /home/git/.ssh/authorized_keys

安全提醒: 每个开发者应使用独立的 SSH 密钥对,不要共享私钥。密钥注释中建议包含用户名和设备信息,如 alice@laptop

2.2.4 限制 git 用户的 SSH 权限

为防止 git 用户获得完整的 Shell 访问权限,需要限制其只能执行 Git 操作。

# 编辑 /etc/passwd,将 git 用户的 shell 改为 git-shell
# 或使用 usermod
sudo usermod -s /usr/bin/git-shell git

现在 git 用户通过 SSH 只能执行 Git 操作:

# 正常 Git 操作(允许)
ssh git@server "git-upload-pack '/opt/git/project-a.git'"

# 尝试获得 Shell(被拒绝)
ssh git@server
# fatal: Interactive git shell is not enabled.
# hint: ~/git-shell-commands should exist and have read and execute access.

2.2.5 创建受限 Shell 的自定义命令(可选)

如果需要给 git-shell 添加一些管理命令:

# 创建命令目录
sudo mkdir -p /home/git/git-shell-commands

# 创建一个简单的帮助脚本
sudo tee /home/git/git-shell-commands/help << 'EOF'
#!/bin/bash
echo "Available commands:"
echo "  help     - Show this help"
echo "  list     - List all repositories"
echo "  whoami   - Show your SSH key fingerprint"
EOF

# 创建列出仓库的脚本
sudo tee /home/git/git-shell-commands/list << 'EOF'
#!/bin/bash
echo "=== Repositories ==="
ls -1 /opt/git/*.git 2>/dev/null || ls -1 /opt/git/*/*.git 2>/dev/null
EOF

sudo chmod +x /home/git/git-shell-commands/*
sudo chown -R git:git /home/git/git-shell-commands

2.3 多用户管理

2.3.1 共享 git 用户(推荐:简单场景)

所有开发者使用同一个 git 系统用户,通过 SSH 公钥区分身份。

/home/git/.ssh/authorized_keys
├── alice 的公钥
├── bob 的公钥
└── charlie 的公钥

优点:配置简单,用户管理只需操作 authorized_keys 文件。

缺点:无法区分 Git 提交身份(需要靠 Git 的 user.name / user.email 区分);权限控制粒度有限。

管理脚本

#!/bin/bash
# manage-users.sh - 管理 Git SSH 用户

AUTHORIZED_KEYS="/home/git/.ssh/authorized_keys"

case "$1" in
  add)
    if [ -z "$2" ]; then
      echo "Usage: $0 add <pubkey-file>"
      exit 1
    fi
    cat "$2" >> "$AUTHORIZED_KEYS"
    echo "Key added from: $2"
    echo "Total keys: $(wc -l < "$AUTHORIZED_KEYS")"
    ;;
  remove)
    if [ -z "$2" ]; then
      echo "Usage: $0 remove <comment>"
      echo "Current keys:"
      awk '{print NR, $3}' "$AUTHORIZED_KEYS"
      exit 1
    fi
    grep -v "$2" "$AUTHORIZED_KEYS" > /tmp/ak_tmp
    mv /tmp/ak_tmp "$AUTHORIZED_KEYS"
    echo "Removed keys matching: $2"
    ;;
  list)
    echo "=== Authorized Keys ==="
    awk '{printf "%3d  %s\n", NR, $3}' "$AUTHORIZED_KEYS"
    ;;
  *)
    echo "Usage: $0 {add|remove|list} [args]"
    exit 1
    ;;
esac
# 使用
sudo ./manage-users.sh list
sudo ./manage-users.sh add /tmp/alice-key.pub
sudo ./manage-users.sh remove alice@laptop

2.3.2 独立系统用户(推荐:需要严格隔离)

为每个开发者创建独立的系统用户,通过 git-shell 或自定义 SSH 命令限制访问。

# 创建用户
sudo adduser --disabled-password --shell /usr/bin/git-shell alice
sudo adduser --disabled-password --shell /usr/bin/git-shell bob

# 为每个用户添加 SSH 公钥
sudo mkdir -p /home/alice/.ssh
sudo cp alice-key.pub /home/alice/.ssh/authorized_keys
sudo chown -R alice:alice /home/alice/.ssh
sudo chmod 700 /home/alice/.ssh
sudo chmod 600 /home/alice/.ssh/authorized_keys

# bob 同理
sudo mkdir -p /home/bob/.ssh
sudo cp bob-key.pub /home/bob/.ssh/authorized_keys
sudo chown -R bob:bob /home/bob/.ssh
sudo chmod 700 /home/bob/.ssh
sudo chmod 600 /home/bob/.ssh/authorized_keys

2.3.3 使用 gitgroup 统一权限

# 创建 git 组
sudo groupadd gitgroup

# 将 git 用户和开发者加入组
sudo usermod -aG gitgroup git
sudo usermod -aG gitgroup alice
sudo usermod -aG gitgroup bob

# 设置仓库目录的组权限
sudo chown -R git:gitgroup /opt/git
sudo chmod -R 775 /opt/git

# 设置 SGID 位(新建文件自动继承组)
sudo find /opt/git -type d -exec chmod g+s {} \;

2.4 权限控制

2.4.1 基于文件系统权限

Linux 文件系统权限是最基础的权限控制方式:

# 只读仓库(所有人都只能读)
sudo chmod -R 755 /opt/git/public-repo.git

# 可读写仓库(组内成员可读写)
sudo chmod -R 775 /opt/git/team-repo.git

# 私有仓库(仅 owner 可读写)
sudo chmod -R 700 /opt/git/private-repo.git

2.4.2 权限矩阵

场景ownergroupothers命令
公开只读rwxr-xr-xchmod 755
团队读写rwxrwxr-xchmod 775
私有仓库rwxchmod 700
管理员读写/团队只读rwxr-xchmod 750

2.4.3 按团队组织仓库

/opt/git/
├── team-frontend/          # 前端团队
│   ├── web-app.git
│   └── shared-ui.git
├── team-backend/           # 后端团队
│   ├── api-server.git
│   └── auth-service.git
└── shared/                 # 公共仓库
    ├── docs.git
    └── scripts.git
# 创建团队目录
sudo mkdir -p /opt/git/{team-frontend,team-backend,shared}
sudo chown git:gitgroup /opt/git/{team-frontend,team-backend,shared}

# 创建前端团队组
sudo groupadd team-frontend
sudo usermod -aG team-frontend alice
sudo usermod -aG team-frontend bob

# 设置前端团队目录权限
sudo chown git:team-frontend /opt/git/team-frontend
sudo chmod 775 /opt/git/team-frontend

# 在前端团队目录下创建仓库
sudo -u git git init --bare /opt/git/team-frontend/web-app.git

# 公共仓库所有人都能读
sudo chown git:gitgroup /opt/git/shared
sudo chmod 775 /opt/git/shared

2.4.4 使用 ACL 进行细粒度权限控制

当标准的 Unix 权限不够用时,可以使用 ACL(Access Control Lists)。

# 安装 ACL 工具
sudo apt install acl -y

# 给 charlie 单独授予 team-frontend 目录的读取权限
sudo setfacl -R -m u:charlie:rx /opt/git/team-frontend

# 查看 ACL 设置
sudo getfacl /opt/git/team-frontend
# # file: opt/git/team-frontend
# # owner: git
# # group: team-frontend
# user::rwx
# user:charlie:r-x
# group::rwx
# mask::rwx
# other::r-x

# 设置默认 ACL(新建文件自动继承)
sudo setfacl -d -m u:charlie:rx /opt/git/team-frontend

2.5 客户端使用

2.5.1 克隆仓库

# 使用绝对路径
git clone git@server:/opt/git/project-a.git

# 如果配置了 git 用户的 home 目录,可用相对路径
git clone git@server:project-a.git

# 指定本地目录名
git clone git@server:/opt/git/project-a.git my-project

2.5.2 配置 SSH 简化连接

编辑 ~/.ssh/config

# ~/.ssh/config

Host git-server
    HostName 192.168.1.100
    User git
    Port 22
    IdentityFile ~/.ssh/id_ed25519

# 如果使用自定义端口
Host git-server-alt
    HostName git.example.com
    User git
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

现在可以简化命令:

# 之前
git clone [email protected]:/opt/git/project-a.git

# 现在
git clone git-server:/opt/git/project-a.git

2.5.3 设置远程仓库

# 为已有项目添加远程仓库
cd existing-project
git remote add origin git@server:/opt/git/project-a.git
git push -u origin main

# 查看远程仓库
git remote -v
# origin  git@server:/opt/git/project-a.git (fetch)
# origin  git@server:/opt/git/project-a.git (push)

2.6 仓库管理脚本

2.6.1 仓库创建脚本

#!/bin/bash
# create-repo.sh - 创建新的 Git 裸仓库

set -euo pipefail

GIT_ROOT="/opt/git"
GROUP="${1:?Usage: $0 <group> <repo-name> [description]}"
REPO="${2:?Usage: $0 <group> <repo-name> [description]}"
DESC="${3:-No description}"

REPO_PATH="${GIT_ROOT}/${GROUP}/${REPO}.git"

if [ -d "$REPO_PATH" ]; then
    echo "错误: 仓库已存在 - $REPO_PATH"
    exit 1
fi

# 创建目录
mkdir -p "${GIT_ROOT}/${GROUP}"

# 初始化裸仓库
git init --bare "$REPO_PATH" > /dev/null

# 设置描述
echo "$DESC" > "$REPO_PATH/description"

# 设置权限
chown -R git:git "${GIT_ROOT}/${GROUP}"
chmod -R 775 "${GIT_ROOT}/${GROUP}"

# 设置 SGID
find "$REPO_PATH" -type d -exec chmod g+s {} \;

echo "仓库创建成功: ${GROUP}/${REPO}.git"
echo "克隆命令: git clone git@server:${REPO_PATH}"
# 使用
sudo ./create-repo.sh team-frontend new-app "前端应用仓库"

2.6.2 仓库列表脚本

#!/bin/bash
# list-repos.sh - 列出所有仓库

GIT_ROOT="/opt/git"

echo "=== Git 仓库列表 ==="
echo ""
printf "%-30s %-20s %-15s %s\n" "仓库路径" "最后提交" "大小" "描述"
printf "%-30s %-20s %-15s %s\n" "--------" "--------" "----" "----"

find "$GIT_ROOT" -name "*.git" -type d | sort | while read repo; do
    rel_path="${repo#$GIT_ROOT/}"
    
    # 获取最后提交时间
    last_commit=""
    if [ -f "$repo/refs/heads/main" ] || [ -f "$repo/refs/heads/master" ]; then
        branch="main"
        [ -f "$repo/refs/heads/master" ] && branch="master"
        last_commit=$(cd "$repo" && git log -1 --format="%ai" "$branch" 2>/dev/null | cut -d' ' -f1,2 | cut -c1-16)
    fi
    
    # 获取大小
    size=$(du -sh "$repo" 2>/dev/null | cut -f1)
    
    # 获取描述
    desc=$(cat "$repo/description" 2>/dev/null || echo "")
    
    printf "%-30s %-20s %-15s %s\n" "$rel_path" "${last_commit:-N/A}" "$size" "$desc"
done

2.6.3 备份脚本

#!/bin/bash
# backup-repos.sh - 备份所有 Git 仓库

set -euo pipefail

GIT_ROOT="/opt/git"
BACKUP_DIR="/var/backups/git"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/git_backup_${DATE}.tar.gz"

mkdir -p "$BACKUP_DIR"

echo "开始备份 Git 仓库..."
echo "源目录: $GIT_ROOT"
echo "备份文件: $BACKUP_FILE"

# 创建压缩备份
tar czf "$BACKUP_FILE" -C "$(dirname $GIT_ROOT)" "$(basename $GIT_ROOT)"

# 显示备份信息
SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
echo "备份完成: $BACKUP_FILE ($SIZE)"

# 清理超过 30 天的旧备份
find "$BACKUP_DIR" -name "git_backup_*.tar.gz" -mtime +30 -delete
echo "已清理 30 天前的旧备份"

2.7 Git 客户端配置优化

2.7.1 凭据和 SSH 缓存

# 缓存 SSH 连接(避免反复握手)
# 在 ~/.ssh/config 中添加:
# Host *
#     ControlMaster auto
#     ControlPath ~/.ssh/sockets/%r@%h-%p
#     ControlPersist 600

mkdir -p ~/.ssh/sockets

2.7.2 推送和拉取优化

# 全局配置
git config --global push.default simple
git config --global pull.rebase true
git config --global fetch.prune true
git config --global core.compression 6

# SSH 连接复用
git config --global core.sshCommand "ssh -o ControlMaster=auto -o ControlPersist=600 -o ControlPath=~/.ssh/sockets/%r@%h-%p"

2.8 业务场景示例

场景一:三人开发团队

需求:3 个开发者,2 个共享项目,1 个私有项目。

# 服务端操作
sudo ./create-repo.sh shared project-web "Web 应用"
sudo ./create-repo.sh shared project-api "API 服务"
sudo ./create-repo.sh alice private-notes "Alice 的私有笔记"

# 设置私有仓库权限(仅 Alice 可访问)
sudo chmod 700 /opt/git/alice/private-notes.git

场景二:开源项目托管

需求:代码对所有人可读,只有维护者可推送。

# 创建公开仓库
sudo ./create-repo.sh public awesome-project "开源项目"

# 设置权限:所有人可读,git 用户(维护者)可写
sudo chmod 755 /opt/git/public/awesome-project.git

2.9 扩展阅读


本章小结

学到了什么关键要点
服务端搭建创建 git 用户、裸仓库、配置 SSH 公钥
用户管理共享 git 用户 vs 独立系统用户,各有适用场景
权限控制文件系统权限、ACL、按团队组织目录
客户端使用SSH config 简化连接、仓库管理脚本
安全加固git-shell 限制、SSH 密钥管理、备份策略

局限性提醒: 纯 SSH + 裸仓库方案无法提供 Web 界面、Issue 管理、Code Review 等功能。如果需要这些能力,请阅读后续章节了解 Gitolite、Gitea、GitLab 等方案。

下一章:第 3 章 - Gitolite 权限管理 — 使用 Gitolite 实现更细粒度的仓库和分支权限控制。