资讯动态

Windows通过SSH搭建Git服务器:OpenSSH与裸仓库实战

发布时间:2026/10/27 15:15:05 来源:尧图企业网站定制
局域网里想给几个人搭个 Git 远程仓库机器是台 Windows 台式机或者闲置的 NUC没打算上容器、也没打算买云服务——这个场景其实非常普遍。标题里这套「Windows 系统上通过 SSH 搭建 Git 服务器」的方案说白了就是用 Windows 自带或可选安装的 OpenSSH 服务端做入口用 Git 的裸仓库bare repository做存储客户端凭密钥登录、用git push/pull完成协作全程不依赖任何第三方服务端软件也不写一行后端代码。它能解决的核心问题有三个一是让团队摆脱「拷贝压缩包 微信传文件」的版本混乱二是把代码、脚本、设计稿这类文本资产集中到一台可控的机器上三是成本几乎为零一台常开的 Windows 机器加两块硬盘就能跑起来。适合谁来参考中小团队的开发同学、实验室里管设备的人、独立开发者给自己搭私人仓库、以及想搞明白 SSH 认证链路到底怎么走的技术爱好者。下面我把这套东西从选型到落地、从踩坑到加固完整拆一遍。1. 方案设计Windows OpenSSH Git 裸仓库这套组合拳怎么打1.1 先把需求摊开这套东西到底给谁用动手之前我习惯先把需求写在纸上因为「搭 Git 服务器」这句话在不同人嘴里含义差得很远。有人要的是完整的 Web 界面、 issue、合并请求、CI 流水线那是另一套系统该干的活有人要的只是「一个能 push 能 pull 的远程地址」。后者才是 SSH 裸仓库的舒适区它的本质非常朴素SSH 负责把人送进门Git 负责在门里读写一个没有工作区的仓库目录。所以判断标准很直接如果你的团队规模在 3 到 20 人之间日常以代码、配置、文档为主仓库总量在几十 GB 以内不需要代码评审平台那套流程那这套方案就是性价比最高的选择。反过来如果你需要精细到分支级别的权限控制、需要审计每一行代码的变更人、需要和工单系统联动那趁早别在 SSH 上硬堆直接上成熟的服务端软件别把时间浪费在造轮子上。我见过太多人一开始雄心勃勃要「自建全套」结果三个月后仓库里躺着两个提交机器因为断电再也没开过。所以第一步不是装软件是想清楚这台机器的定位它是不是能长期通电、有没有人负责备份、出问题谁去重启。这三件事想不明白后面所有技术细节都是白费。1.2 四种常见做法的横向对比在 Windows 上让人共享 Git 仓库主流有四条路。我把它们放在一张表里你可以对着自己团队的实际情况打勾。方案访问方式权限粒度部署成本适用规模主要短板共享文件夹 本地仓库UNC 路径靠文件系统 ACL极低1-3 人极易冲突几乎不可用HTTP(S) 服务端软件http(s)://用户/仓库级中等5-50 人需要额外服务、证书维护SSH 裸仓库ssh:// 或 scp 风格系统账号 文件 ACL低3-20 人权限模型偏粗托管平台私有仓库https/ssh平台提供极低付费任意数据不在自己手里选 SSH 裸仓库的核心理由是「链路短」。它不需要额外跑一个常驻服务进程去解析 HTTP 协议不需要处理证书续期不需要考虑反向代理SSH 服务本身是操作系统的一部分稳定性由系统保证。代价是权限模型只有「账号级」这一层你很难做到「A 只能读这个仓库、B 只能推那个分支」想做这种控制就得靠钩子脚本自己写或者干脆换方案。我个人的经验是团队人数乘以仓库数量小于 100 的时候账号级权限完全够用因为真正的权限边界在「谁有这台机器的账号」而不是「谁能读哪个目录」。等这个数字撑不住了说明团队规模已经该换方案了。1.3 目录与账号规划动手前先定规矩这一步最容易被跳过但它是后面所有麻烦的源头。我建议至少把三件事定死仓库根目录放哪、用几个系统账号、客户端怎么命名远程。仓库根目录别放在 C 盘用户目录下。原因很实际系统盘容量紧张、重装系统容易丢、杀毒软件对用户目录扫描更频繁。我的习惯是单独一块数据盘路径短、无空格、无中文比如D:\gitrepo。路径里带空格是给自己找麻烦某些钩子脚本和命令行拼接会在这里翻车带中文则是踩编码坑的快捷方式虽然现代工具大多能处理但没必要给自己增加变量。账号方面最省事的做法是所有协作用一个专用账号比如gituser。注意这个账号只用来做 Git 访问不要给它管理员权限不要拿它当日常登录账号用。如果团队里有人需要区分提交身份这个区分交给 Git 的user.name和user.email配置去做不需要在系统层面拆账号。真正需要拆账号的场景只有一种不同的人对仓库有完全不同的可见范围那才需要多账号配合目录 ACL。客户端侧我建议统一约定一个 SSH 别名比如gitbox所有人克隆时都用git clone gitbox:项目名.git。这样做的好处是以后服务器换 IP、换端口只需要改自己本地的 config 文件不用通知所有人改远程地址。这个小心思在长期维护里能省下大量沟通成本。2. 服务端落地把 sshd 从「能连」调到「好用」2.1 Git 与 OpenSSH Server 的安装路径选择Windows 上装 Git 没什么争议官方安装包一路下一步即可但有一处选项值得停下来看一眼安装向导里问「Adjusting your PATH environment」时选「Git from the command line and also from 3rd-party software」。这个选项会把 Git 的cmd目录写进系统 PATH而 SSH 服务端在执行远程命令时依赖的正是系统 PATH。选错了后面会看到一类很迷惑的报错本地命令行敲git一切正常但远程git clone就提示找不到仓库。安装路径也提一句默认的C:\Program Files\Git完全没问题但如果你的机器上路径里有空格或者特殊字符记得后面配置默认 shell 时要给路径加引号否则参数会被拆开。OpenSSH Server 在 Windows 10 1809 之后的版本里是可选功能两条安装路径一条是图形界面的「设置 → 应用 → 可选功能 → 添加功能 → OpenSSH 服务器」另一条是管理员 PowerShell 里的命令我更推荐后者因为可以写进脚本、重装系统后直接复用# 查看当前是否已安装 Get-WindowsCapability -Online | Where-Object Name -like OpenSSH* # 安装服务端客户端一般已自带 Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0这里有个现实问题Add-WindowsCapability需要从系统更新源拉取组件包如果这台机器在完全隔离的环境里命令会卡住或者报错。这种环境下要么提前把组件包准备好要么直接用 Git for Windows 自带的sshd.exe在usr\bin目录下手工注册成服务。后者稍麻烦一点但完全离线可用我在几个封闭环境里都是这么干的。安装完成后建议立刻验证一下二进制是否就位 C:\Windows\System32\OpenSSH\sshd.exe -V能打印出版本号说明服务端程序本身没问题接下来的问题都在配置和权限上。2.2 服务自启、端口与防火墙的一步到位新装好的 sshd 服务默认是「手动」启动状态重启一次机器就没了。这一步必须做否则你会在某次断电之后对着「连接被拒绝」发呆半小时。Set-Service -Name sshd -StartupType Automatic Start-Service sshd Get-Service sshd端口方面默认是 22。我不建议为了「安全」而盲目改端口因为改端口挡不住真正想进来的人只会增加自己团队的配置成本。但有两种情况确实该改一是这台机器上已经有别的服务占用了 22 端口二是所在网络环境的接入策略对 22 端口有统一限制。改端口就是在sshd_config里把Port 22换成别的比如Port 2222然后同步改防火墙规则。防火墙这一步经常被遗漏。Windows 的防火墙对入站连接默认拦截如果安装 OpenSSH Server 时系统没有自动放行外网访问就会一直超时。手动加规则的写法New-NetFirewallRule -Name OpenSSH-Server-In-TCP -DisplayName OpenSSH Server (sshd) -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22提示如果之前改过端口这里的-LocalPort要跟着改。规则名重复执行会报错加规则前先用Get-NetFirewallRule -Name OpenSSH*看一眼有没有现成的。配完这三件事从同一局域网的另一台机器上ssh 用户名服务器IP应该能弹出密码输入提示了。这一步通了说明网络链路和服务状态都是好的后面再出问题就只可能是认证和权限层面的事——把排查范围压缩到最小这就是分步验证的价值。2.3 默认 shell 的选择决定了后面 90% 的坑这是我踩得最深的一脚。Windows 上 sshd 的登录 shell 默认是cmd.exeGit 客户端通过 SSH 执行远端命令时实际发过去的是类似git-upload-pack demo.git这样一条命令由默认 shell 解释执行。如果 shell 找不到git或者 shell 在启动时输出了欢迎信息就会直接破坏 Git 的协议流。这里有一个非常隐蔽的失败模式Git 在 SSH 通道上传输数据时用的是自定义协议开头几个字节必须是长度前缀。如果 shell 在启动时打印了任何一句「欢迎使用某某系统」客户端收到的第一个字节就不是数字于是报出fatal: protocol error: bad line length character: Welc看到这个报错第一反应应该是「远端 shell 在启动时说话了」而不是「Git 坏了」。有两种应对方式。第一种是保持默认 shell 为cmd只保证 Git 在系统 PATH 里这样最稳因为cmd是非交互执行时不会输出任何东西。缺点是远程登录后你拿到的是 cmd 环境用惯了 bash 的人会别扭。第二种是切换默认 shell 到 Git 自带的 bash。改注册表即可New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value C:\Program Files\Git\bin\bash.exe -PropertyType String -Force Restart-Service sshd注意这里选的是bin\bash.exe而不是bin\sh.exe两者的启动行为有差别sh.exe更容易触发 profile 加载。即便如此仍要确认 Git 的etc\profile里没有会打印内容的语句有些第三方工具比如版本管理器、提示符美化脚本会在 profile 里插一段输出那就会重演上面那个报错。我自己的选择是第二种理由是团队里大多数人熟悉 bash远程上去排查问题时手更顺而且 Git 钩子脚本用#!/bin/sh写配上 bash 默认 shell 能直接跑。代价就是每次升级 Git 或者改环境变量后都要回来验一遍ssh 用户名服务器 echo ok的输出是不是干净的只有一个ok。这个自检命令我建议写进备忘出问题时它能一秒定位。2.4 专用账号与仓库根目录的权限落位账号建议单独建一个不要用日常登录账号也不要给管理员权限$pw Read-Host -AsSecureString 输入访问密码 New-LocalUser -Name gituser -Password $pw -Description Git over SSH建完记得确认它只在Users组里不在Administrators组里。这一点非常关键因为它直接决定了后面公钥文件该放在哪个位置——管理员账号和普通账号的authorized_keys路径完全不同这是 Windows 上 OpenSSH 最容易让人抓狂的设计。仓库根目录的权限要手工收紧。Windows 默认的继承权限会把Users组带进来意味着这台机器上任何登录的人都能翻你的仓库。正确做法是先断开继承再显式授权New-Item -ItemType Directory -Path D:\gitrepo -Force icacls D:\gitrepo /inheritance:r icacls D:\gitrepo /grant gituser:(OI)(CI)(F) SYSTEM:(OI)(CI)(F) Administrators:(OI)(CI)(F)/inheritance:r是移除继承来的权限项(OI)(CI)表示子对象和容器都继承(F)是完整控制。执行完用icacls D:\gitrepo复查一遍应该只剩下三条授权。这三条的含义分别是服务账号可读写、系统进程可读写备份、索引需要、管理员可读写你需要人工维护时用。其他组一律不该出现。注意icacls的授权顺序和继承关系在 Windows 上是「拒绝优先于允许」的但显式拒绝权限项很容易在后期维护时忘掉导致莫名其妙的访问失败。我的做法是永远不用显式拒绝只做白名单授权。3. 密钥体系把密码登录彻底关掉3.1 客户端生成密钥与上传公钥的正确姿势密码登录在 SSH 上是可以用的但我强烈建议在生产环境里关掉。原因不是「密码弱」而是密码认证无法防止暴力尝试一台暴露在局域网里的机器日志里每天刷几千条失败记录是常态既吵又占资源。密钥认证则是一次性配置、长期有效。客户端生成密钥现在一律推荐 ed25519ssh-keygen -t ed25519 -C dev-ateam -f ~/.ssh/id_ed25519_git-C后面的注释一定要写写清楚是谁的、干什么用的。半年后你在服务器上看到一串公钥没有注释根本分不清哪条能删。-f指定文件名是为了避免和已有的个人密钥冲突我个人习惯给每台服务器或每套环境单独一对密钥出问题的时候可以直接作废某一对不影响其他。生成过程中会问要不要设 passphrase。我的建议是设尤其是笔记本电脑。虽然每次用都要输一遍可以用系统的密钥代理缓存但万一机器丢了别人拿到你的私钥也进不去。如果这台机器是固定工位、物理环境可控也可以空着图省事这是风险和便利的取舍没有标准答案。上传公钥这一步很多人习惯复制粘贴到服务器上手工追加。如果只有一两个人这么干没问题人一多就容易出错——少一个字符、多一个换行、粘贴时被编辑器自动折行都会导致「密钥明明传了却登不上」。更稳的做法是用ssh-copy-idssh-copy-id -i ~/.ssh/id_ed25519_git.pub gituser192.168.1.50如果 Windows 客户端上没有这个命令Git Bash 里通常有也可以自己拼一条命令完成追加type $env:USERPROFILE\.ssh\id_ed25519_git.pub | ssh gituser192.168.1.50 cat ~/.ssh/authorized_keys用管道而不是复制粘贴能保证内容一字不差也不会被编辑器改动换行符。3.2 authorized_keys 的两个位置与 ACL 陷阱这是整个方案里最反直觉的一点Windows 的 OpenSSH 服务端对「管理员账号」和「普通账号」使用两套不同的公钥文件位置。普通账号C:\Users\用户名\.ssh\authorized_keys管理员组账号C:\ProgramData\ssh\administrators_authorized_keys默认的sshd_config末尾有一段匹配规则Match Group administrators AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys所以如果你图省事用了一个管理员账号来做 Git 服务公钥写进用户目录是绝对不生效的服务端压根不去那儿找。这也是「密钥配了但一直要密码」的头号原因。即便是普通账号权限也不能马虎。OpenSSH 在 Windows 上会检查密钥文件的所有者和 ACL如果发现「其他用户可写」会直接拒绝使用这个文件日志里写的是Authentication refused: bad ownership or modes。修复方法icacls $env:USERPROFILE\.ssh\authorized_keys /inheritance:r icacls $env:USERPROFILE\.ssh\authorized_keys /grant $env:USERNAME:F SYSTEM:F管理员那份文件则需要授权给管理员组和 SYSTEMicacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r icacls C:\ProgramData\ssh\administrators_authorized_keys /grant Administrators:F SYSTEM:F注意如果用 PowerShell 的Out-File或重定向去写authorized_keys默认编码可能是 UTF-16文件里会带 BOM服务端解析时第一行就废了。要么用-Encoding ascii要么直接用Add-Content或者干脆从 Linux 侧用cat 追加。我自己的一套惯例是先确认账号是不是管理员组再决定往哪写写完立刻用ssh -v从客户端连一次看日志里到底走了哪条认证路径。-v参数打印的协商过程非常详细能直接告诉你「服务端接受了哪把密钥」「试了哪些方法被拒」比在服务器上翻日志快得多。3.3 sshd_config 的关键参数逐条调配置文件在C:\ProgramData\ssh\sshd_config。改之前先备份一份改完用sshd -t做语法检查再重启服务——跳过检查直接重启一个拼写错误就能让服务起不来而这时候你还没法远程连进去看日志只能跑到机器跟前处理非常狼狈。下面是我常用的一份精简配置逐条说理由Port 2222 PasswordAuthentication no PubkeyAuthentication yes PermitEmptyPasswords no AllowUsers gituser X11Forwarding no AllowTcpForwarding no PermitTunnel no MaxAuthTries 3 MaxStartups 20 ClientAliveInterval 60 ClientAliveCountMax 3 Subsystem sftp sftp-server.exePasswordAuthentication no关掉密码登录这是安全基线。AllowUsers gituser白名单化只有这一个账号能登录其他系统账号哪怕有密码也进不来这是防止误配置最有效的一条。X11Forwarding、AllowTcpForwarding、PermitTunnel全部关掉因为做 Git 服务根本用不上这些东西开着只会扩大暴露面。ClientAliveInterval 60配ClientAliveCountMax 3的含义是服务端每 60 秒探测一次客户端连续 3 次没响应就断开。这个配置在机房网络不稳或者客户端休眠时特别有用否则你会看到一堆「僵尸会话」把连接数占满新的克隆请求直接被拒。MaxStartups 20限制同时处于未认证状态的连接数。默认的10:30:100表示超过 10 个未认证连接开始随机丢弃、超过 100 个全丢。小团队用默认值就行我写 20 只是留点余量避免早高峰大家一起推代码时互相踩。改完执行 C:\Windows\System32\OpenSSH\sshd.exe -t Restart-Service sshd-t无输出就是语法通过。想确认最终生效的配置用sshd -T打印全部解析后的参数比对着文件猜要可靠得多。3.4 客户端 config把长命令压缩成四个字母服务端配好之后每次敲ssh -p 2222 gituser192.168.1.50太啰嗦而且团队里每个人都要记住这些参数。用客户端的~/.ssh/config做一层封装Host gitbox HostName 192.168.1.50 Port 2222 User gituser IdentityFile ~/.ssh/id_ed25519_git IdentitiesOnly yes ServerAliveInterval 30 ServerAliveCountMax 3配好之后ssh gitbox直接登录git clone gitbox:demo.git直接克隆git remote add origin gitbox:demo.git直接推送。IdentitiesOnly yes这一行值得单独说它的作用是「只使用这里指定的密钥」不去尝试代理里那些乱七八糟的其他密钥。不写这一行当你本机有五六把密钥时服务端可能会在你试到第三把时触发MaxAuthTries报出「Too many authentication failures」让人完全摸不着头脑。ServerAliveInterval放在客户端是心跳保活和服务端的ClientAliveInterval是一对。两边都开长连接就不会因为中间设备的空闲超时被悄悄掐断。如果团队人多这份 config 片段可以直接发到群里让人粘贴比口头描述参数可靠得多。部署新机器的时候我一般会顺手把它写进团队的新人上手文档里省下大量重复答疑。4. 裸仓库建立与团队协作流程4.1 初始化 bare 仓库与共享权限裸仓库和普通仓库的区别只有一点它没有工作区只有一个.git目录里的内容所以它天生就适合做「中心仓库」。创建方式有两种。从零开始新建一个ssh gitbox cd /d/gitrepo git init --bare --sharedgroup demo.git--sharedgroup的作用是把仓库内部文件的权限设为同组可读写。在 Linux 上是调chmod在 Windows 上 Git 会尽量映射到 ACL。这个参数在多账号环境下很重要单账号环境下影响不大但我习惯加上因为将来要加人时不用回头改一遍所有仓库。把已有的本地仓库变成中心仓库更常用的方式是这样git clone --bare /path/to/old-project demo.git或者直接在原仓库里操作cd /path/to/old-project git clone --bare . /d/gitrepo/demo.git目录命名统一加.git后缀这是我坚持的习惯好处是任何人看到demo.git就知道它是个裸仓库不会手贱进去git status。命名上只用小写字母、数字和连字符避免大小写敏感问题——Windows 文件系统不区分大小写但 Git 的引用是区分的一个叫Demo.git一个叫demo.git在这种环境下会非常难受。权限方面裸仓库目录建好之后确认gituser对整个目录有完全控制权icacls D:\gitrepo\demo.git /grant gituser:(OI)(CI)(F)如果不放心继承关系就再跑一次/inheritance:r重新授权。这一步做完从客户端git clone gitbox:demo.git应该就能拿到一个空仓库的提示了。4.2 首次推送、默认分支与引用保护空仓库的第一次推送有个小坑。Git 在 2.28 之后可以用init.defaultBranch配置默认分支名但老仓库里可能还叫master。如果服务端仓库是空的时候大家各自推不同名字的分支会同时出现main和master两条线后面合并起来很烦。我的处理办法是在服务端仓库上直接定死 HEAD 指向cd /d/gitrepo/demo.git git symbolic-ref HEAD refs/heads/main然后在客户端的全局配置里统一git config --global init.defaultBranch main这样新建的仓库、克隆下来的仓库、推上去的分支名全部一致不会出现两套命名并存。另一个常见报错是往非裸仓库推送! [remote rejected] main - main (branch is currently checked out)提示里还会带一句receive.denyCurrentBranch。这个配置的默认值是refuse含义是「不允许推送到当前检出分支」因为推送会让工作区和索引与实际文件对不上。遇到这个报错九成是因为你把仓库建成了普通仓库而不是裸仓库。正确做法是重新用--bare建一个而不是去改配置绕过去——把denyCurrentBranch改成ignore虽然能推上去但服务端工作区会一直是旧内容将来某天有人直接在那儿改文件就会产生难以理清的冲突。引用保护这块裸仓库本身不提供分支级别的写保护但可以用update钩子实现。比如禁止删除主干分支#!/bin/sh # hooks/update refname$1 if [ $refname refs/heads/main ] [ $2 0000000000000000000000000000000000000000 ]; then echo main 分支不允许删除 2 exit 1 fi exit 0钩子文件放在hooks/update不带扩展名需要可执行权限。这个脚本在每次引用更新前执行返回值非零就拒绝。用它可以做很多事检查提交信息格式、拦截大文件、限制谁能推主干——虽然「谁能推」在纯 SSH 方案里只能靠脚本里写死的名单比较笨但应付小团队够用了。4.3 用钩子打通「推送即部署」和「推送即通知」post-receive是这个方案里最实用的一环。它在一次推送完成、引用全部更新之后执行常见用途有两个自动把代码同步到运行目录、把这次推送的信息发到群里。自动部署的写法#!/bin/sh # hooks/post-receive TARGET/d/www/demo GIT_DIR/d/gitrepo/demo.git while read oldrev newrev refname; do branch$(echo $refname | sed s#^refs/heads/##) if [ $branch main ]; then echo [deploy] 开始同步 $branch 到 $TARGET unset GIT_INDEX_FILE git --work-tree$TARGET --git-dir$GIT_DIR checkout -f main echo [deploy] 完成 fi done几个关键点必须注意。unset GIT_INDEX_FILE这行不能省钩子运行时环境里会带着一个临时索引文件路径不解除的话checkout会报索引损坏。checkout -f的-f也不能省它保证工作区被强制覆盖否则遇到本地修改就会中断。--work-tree和--git-dir两个参数都要给全因为在钩子里当前目录是hooks不是仓库根目录。注意部署目录里的东西会被checkout -f覆盖所以千万别把用户上传的文件、日志、配置文件直接放在那个目录里。我的做法是把部署目录拆成「代码区」和「数据区」钩子只动代码区数据区通过软链或配置引用。推送通知的钩子可以更简单只把信息打到日志里再由别的任务去读#!/bin/sh while read oldrev newrev refname; do user$(git config --get user.name) echo $(date %F %T) $user $refname $oldrev..$newrev /d/gitrepo/push.log done至于「钩子脚本为什么改了不生效」这个问题我在第 5 节专门说它的原因和 Git 本身没关系是文件权限和换行符的锅。4.4 备份、迁移与仓库体检自建服务最容易被忽略的就是备份。Windows 机器不比服务器一次系统更新、一次意外断电、一次硬盘坏道都可能让几个月的工作消失。备份方案至少要有一种是「异地」的。最简单的是用git bundle打离线包它把整个仓库的引用和对象打包成单个文件恢复时可以直接git clonecd /d/gitrepo/demo.git git bundle create /d/backup/demo-$(date %Y%m%d).bundle --all打包完成后验证一下完整性这个习惯很重要我见过太多人备了一年真要用的时候发现包是坏的git bundle verify /d/backup/demo-20250101.bundle另一种是镜像克隆到另一台机器适合有两台机器的场景git clone --mirror gitbox:demo.git /d/backup/demo.git cd /d/backup/demo.git git remote update--mirror会连 refs、备注、配置一起复制适合做热备。定期在备份机上执行git remote update拉增量。仓库体检用git fsck和git count-objectscd /d/gitrepo/demo.git git fsck --full git count-objects -vHfsck输出里如果有dangling commit属于正常现象那是被 reset 掉的历史有missing或corrupt才是真问题。count-objects能看仓库占了多少空间、有多少松散对象松散对象长期累积会拖慢操作数量超过几千就该跑一次git gc。但gc --aggressive在大仓库上会跑很久、吃很多内存生产仓库别在白天跑安排在夜里用计划任务执行更稳妥。5. 实战排障报错速查表与踩坑记录5.1 连接阶段的典型报错速查大部分问题在第一次连接时就暴露了我把常见的几张「脸」整理成表出问题先对号入座能省下大量瞎猜的时间。现象/报错大概率原因处理方向Connection refused服务没起 / 端口不通Get-Service sshd检查防火墙规则Connection timed out防火墙拦截 / IP 不对Test-NetConnection 主机 -Port 端口一直提示输密码公钥没生效确认账号类型、文件位置、ACLPermission denied (publickey)密钥不匹配或文件权限错误客户端ssh -v看试了哪把钥Too many authentication failures客户端尝试了太多密钥config 里加IdentitiesOnly yesbad line length character远端 shell 启动时有输出检查 profile或改用cmd作 shelldoes not appear to be a git repositoryPATH 里没有 git / 路径写错查系统 PATH确认仓库相对路径Host key verification failed服务端重装后指纹变了删known_hosts中对应条目排查顺序我总结成一句话先通网络再通认证最后通 Git。网络层用Test-NetConnection加端口能直接确认认证层用ssh -v能看到完整的密钥协商Git 层则用ssh gitbox git-upload-pack demo.git手工触发一次服务端命令看返回的是不是一串二进制乱码——如果是说明 Git 链路是通的问题在客户端如果返回的是错误信息说明命令根本没执行到 Git。这个手工触发命令非常有用举例说明如果返回bash: git-upload-pack: command not found那就锁定在 PATH如果返回空内容说明 shell 有问题或者仓库路径不对。它把一个模糊的「克隆失败」拆成了具体的两个方向我个人觉得这是整个排障过程里最值得记住的一招。5.2 权限、路径与文件系统的隐形坑Windows 上有几类坑是 Linux 用户完全想象不到的提前知道能少走很多弯路。第一类是钩子脚本不执行。症状是post-receive明明写了推完什么反应都没有也不报错。原因通常两个一是文件从 Windows 编辑器里保存带 CRLF 换行bash 在解析#!/bin/sh时可能因为\r找不到解释器二是 ACL 里没有执行权限。解决办法是写钩子时用支持 LF 的编辑器并在保存后跑一遍icacls确认权限或者干脆用 Git Bash 里的vim编辑。这个坑我踩过不止一次每次都是先怀疑逻辑最后一个file hooks/post-receive才看到with CRLF line terminators。第二类是大小写问题。Windows 文件系统不区分大小写Git 是区分的。如果两个人分别提交了Utils.js和utils.js本地看起来一切正常但另一个人的机器上检出时就会互相覆盖git status显示有改不完的差异。预防的办法是在仓库根目录放一份命名约定同时开启git config core.ignorecase true这个配置只是让 Git 在当前平台表现一致并不能解决跨平台问题。真正要防的是「同一目录下只差大小写的两个文件」代码评审时看到这种命名就该拦下来。第三类是文件名非法字符。Windows 上文件名不能包含: * ? |还有CON、PRN、AUX、NUL这些保留名。如果仓库里有从其他系统带过来的这类文件检出会直接失败报invalid path。这种只能改名没有别的办法越早处理越好。第四类是长路径限制。传统 API 的路径上限是 260 字符深层目录的node_modules或者 Java 项目的包路径很容易超。Git for Windows 默认已经处理了大部分场景但为了保险可以在服务端仓库里设git config --system core.longpaths true第五类是杀毒软件。这个是性能问题但表现很像功能问题克隆特别慢、git status卡十几秒、gc跑到一半超时。原因是实时扫描在反复检查成千上万个松散对象。处理办法是把仓库根目录加进杀毒软件的白名单这个改动在受管控的环境里可能需要走流程但收益非常明显我们的仓库加白之后gc耗时从二十多分钟降到三分钟。5.3 性能与并发大仓库不卡的几个调参小仓库怎么跑都快一旦仓库超过几个 GB或者对象数量上到几十万各种问题就会浮现。我遇到最典型的是克隆中途断开报early EOF或者index-pack failed。这不是网络断了而是客户端打包时内存或超时顶不住。服务端这边可以调几个 Git 参数cd /d/gitrepo/demo.git git config core.compression 0 git config gc.auto 6700 git config pack.windowMemory 256m git config pack.packSizeLimit 1gcore.compression 0是降低压缩比换取速度适合「写入频繁、读取不频繁」的仓库如果更看重存储空间就别动它。pack.windowMemory限制打包时用的内存避免一次性吃光机器内存导致其他服务被拖垮。pack.packSizeLimit把大包切成 1GB 的小包能显著降低克隆中途失败的概率代价是包数量变多、目录看起来乱一点但稳定性提升很值。SSH 层还有两个参数值得调MaxSessions默认是 10够用MaxStartups在上一节说过了。真正影响体验的是ClientAliveInterval我见过因为中间设备 5 分钟空闲断连导致大仓库克隆总是跑到一半失败的情况把心跳调到 60 秒之后问题直接消失。另外提一个反直觉的点不要在大仓库上频繁跑gc --aggressive。它会重新计算所有对象的 delta耗时随仓库规模超线性增长而且对已经打包好的仓库收益很小。我的策略是平时靠gc.auto自动触发轻量 gc只在仓库确实出现明显膨胀时才手工跑一次完整的。6. 长期可用安全加固与日常运维清单6.1 最小权限原则的具体落法前面零散提过一些加固点这里集中收一下因为这套东西一旦跑起来往往就是好几年加固做得不到位某天就会变成麻烦。账号层面只用gituser一个专用账号不给管理员权限不设空密码sshd_config里PermitEmptyPasswords no已经挡了但账号本身也别设空。这个账号除了访问仓库目录不应该有任何其他系统权限尤其不要把它加进「备份操作员」之类的组。协议层面把AllowTcpForwarding、PermitTunnel、X11Forwarding全关。这三个功能在 Git 场景里完全用不上开着等于把这台机器变成一个通用入口别人拿到密钥之后能干的事情远超你的预期。MaxAuthTries 3也是同理限制认证尝试次数防止有人在密钥列表上反复试。文件层面仓库根目录的 ACL 只留三条服务账号、SYSTEM、Administrators.ssh目录和authorized_keys的 ACL 单独收紧。这两个位置是整套方案里最敏感的地方写权限等于「能给自己发一把钥匙」。还有一条容易被忽略关掉或限制 SMB 共享。既然大家通过 SSH 访问 Git就没必要再把D:\gitrepo整个共享出去。共享出去的目录权限模型和 SSH 完全是两套一旦有人从共享路径直接改文件仓库状态就会和工作区对不上排查起来极其痛苦。6.2 日志、审计与例行检查Windows 的 OpenSSH 服务端日志在事件查看器里路径是「应用程序和服务日志 → OpenSSH → Operational」。命令行查最近记录Get-WinEvent -LogName OpenSSH/Operational -MaxEvents 50 | Select-Object TimeCreated, Id, Message默认的日志级别只记录连接和认证结果排查密钥问题时经常不够用。需要临时提高详细度可以在sshd_config里加LogLevel DEBUG3排查完记得改回去——DEBUG3会把每次数据交换都记下来日志文件涨得非常快长期开着既占空间又拖性能。审计方面Git 自己有一份天然的记录git reflog在裸仓库里同样可用能看到引用是怎么变的配合上一节写的post-receive日志谁在什么时候推了哪个分支就有据可查。如果对「谁推的」要求更严格可以在钩子里记录 SSH 连接的环境变量但要注意SSH_ORIGINAL_COMMAND这类信息并不总是可靠只能作参考。日常检查我给自己列了个清单贴在运维文档里每周花五分钟过一遍服务状态Get-Service sshd确认是 Running 且启动类型为 Automatic磁盘余量仓库盘剩余空间低于 20% 就该清理或扩容仓库满盘会导致推送失败且可能损坏对象备份是否成功git bundle verify最近一个备份包日志异常看有没有大量认证失败连续刷屏通常意味着有人在试探Git 版本服务端 Git 和客户端 Git 版本差太多时某些新协议特性会对不上比如GIT_PROTOCOL版本协商尽量让服务端版本不低于客户端这台机器上跑的虽然是「小服务」但它是团队协作的入口一旦停摆所有人都会卡住。所以我在部署时会顺手做两件事把上面的检查项写成计划任务定期跑出问题发个邮件把配置文件和钩子脚本全部丢进一个仓库做版本管理下次重装机器直接 clone 下来照着装半小时就能恢复。这两件事花的时间不多但能把「出事的时候手忙脚乱」变成「按步骤走一遍」。最后分享一个我在实际维护里觉得最值的小技巧给这台机器配一个固定的静态地址或者至少在路由器上做地址保留。动态地址导致的「突然连不上」我遇到过至少三次每次都要挨个问人「服务器 IP 是不是变了」而这个问题本身又极难被联想到因为大家会本能地怀疑密钥、怀疑服务。地址固定下来之后这类问题直接归零。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑