docker镜像仓库如果不做加密和认证,基本等于把服务器大门敞开,谁都能往里塞镜像,谁都能把私有镜像拉走。这一节我主要讲清楚两件事:一是怎么给仓库套上HTTPS的壳,让传输过程不被截胡;二是怎么把账密体系和权限卡在仓库前面,让不该进的人进不来。适合刚搭完Registry、准备往生产环境迁的开发者参考,也适合想搞明白Harbor底层到底在做什么的运维同学。
1. 仓库为什么不能“裸奔”:加密与认证要解决的两件事
1.1 不加密的后果:镜像内容明文传输、中间人篡改
很多人在本机或内网搭好Registry之后,第一步就是往daemon.json里写insecure-registries,图省事,跳过HTTPS。这在局域网里临时验证一下问题不大,但一旦镜像要被跨网络传输,或者仓库要暴露到非完全可信的网络环境,裸奔的后果就很直接。
首先,镜像层是明文传输。容器镜像里经常带着业务代码、配置文件、数据库连接信息,甚至一些写死在构建参数里的密钥。没有TLS加密,这些内容在网络上等于透明,谁在网络链路上抓包就能还原出完整的镜像内容。其次,还存在中间人篡改风险。攻击者可以在客户端和仓库之间插入一个伪造节点,把镜像层替换成恶意代码,客户端拉下来之后毫无感知,因为没有任何完整性校验能发现镜像被换过了。
有同学会说,镜像不是有digest校验吗?对,digest能保证你拉到的内容和你请求的内容一致,但它没法保证你请求到的那个“仓库”本身就是真的。这就像你用导航软件,软件能保证你按它给的路线走,但如果地图数据本身被换掉了,导航只会把你带到更远的地方。
1.2 认证的本质:谁有权限推拉镜像,防线怎么划
加密解决的是“能不能偷看”的问题,认证解决的是“能不能进来”的问题。一个仓库如果只有加密没有认证,等于门锁升级成了指纹锁,但小区大门从来不查访客登记——谁都能按指纹进去。
Docker仓库的认证体系通常分两层。第一层是基础账密认证,就是Registry自带的htpasswd方案,简单直接,适合小团队、少量账号的场景。第二层是更完整的用户权限模型,比如Harbor里的项目级权限、角色控制、审计日志,适合多团队、多项目并行的生产环境。
我在实际项目里的建议是:单机或几个人的小团队,直接用Registry加TLS和htpasswd就够用了,复杂度低,出问题好排查。一旦仓库要服务多个部门或者对外交付镜像,尽早迁到Harbor,否则后期补权限和审计的成本比迁移本身还高。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搭一个带HTTPS加密的Registry
2.1 自签证书的生成与域名规划
搭建HTTPS加密仓库的第一步不是写配置,而是先把证书和域名想清楚。这里有个很关键的点:Docker仓库的TLS证书是绑定域名的,而不是绑定IP的,所以你得先有一个域名,哪怕是内网DNS能解析的假域名也行。
我在项目里会先在内部DNS加一条A记录,比如registry.internal.example.com指向仓库服务器的IP。如果没有内网DNS,可以直接在每台需要访问仓库的客户机/etc/hosts里写映射。这里强烈不建议直接用IP签发证书,后续客户端配置ca.crt时会遇到各种奇怪的问题,尤其是Docker Desktop这种对证书校验比较严格的客户端。
证书生成推荐用OpenSSL,分两步走。第一步先生成CA私钥和CA根证书:
bash复制openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/CN=Docker Registry CA"
第二步生成仓库服务端的私钥和证书签名请求。这里要注意,服务端证书必须带上域名对应的SAN(Subject Alternative Name),否则新版客户端在校验证书时会直接报错。我通常会在服务器上放一个openssl.cnf配置文件,里面显式声明SAN:
code复制[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = registry.internal.example.com
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = registry.internal.example.com
DNS.2 = registry.example.com
然后用这个配置生成CSR并用CA签发:
bash复制openssl genrsa -out registry.key 2048
openssl req -new -key registry.key -out registry.csr \
-config openssl.cnf
openssl x509 -req -in registry.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out registry.crt -days 825 \
-extensions v3_req -extfile openssl.cnf
这里-days 825不是随便写的,这是当前CA/Browser Forum建议的短期证书上限,虽然内网用不到这个约束,但养成习惯没坏处。
2.2 Registry与Docker daemon的TLS配置联动
证书准备好之后,Registry本身的配置很简单。用官方registry:2镜像启动时,只需要把证书和私钥以volume方式挂载进去,并告诉容器这两个文件的位置。完整启动命令大概长这样:
bash复制docker run -d \
--name registry \
--restart=always \
-p 443:443 \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/certs:/certs \
-e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
registry:2
这一步的难点不在Registry本身,而在客户端的Docker daemon。Docker daemon在拉取或推送镜像时,会像浏览器一样去校验仓库服务端的证书是否可信。由于我们用的是内网自签CA签发的证书,客户端默认是不信任的,必须把CA根证书打进系统的信任链。
Linux下做法是把ca.crt复制到/etc/docker/certs.d/registry.internal.example.com:443/ca.crt,目录名字必须是完整的“域名加端口”格式。Docker daemon会优先从这个目录读取CA来验证仓库的证书,而不是走系统全局信任链。做好这件事之后重启docker服务,再执行docker login registry.internal.example.com,如果没报x509: certificate signed by unknown authority,说明证书链路已经通了。
2.3 客户端信任自签CA的关键一步
上面讲的/etc/docker/certs.d/路径是Docker daemon专用的,但如果你在开发机上用Docker Desktop(比如Mac或Windows),处理方式会稍有不同。Docker Desktop启动的Docker Engine跑在虚拟机里,它读不到宿主机系统自带的证书库,所以你必须把ca.crt添加到“Docker Desktop的受信任证书”里。
Mac版在Docker Desktop的设置里找“Troubleshoot”或“Resources”,Windows版在“Settings”的“Docker Engine”页面里,本质上都是让你把CA证书塞进虚拟机的系统信任链。还有一个更通用的办法是把ca.crt放到位,然后重启Docker Desktop。最直观的验证方式是打开浏览器访问https://registry.internal.example.com/v2/,如果浏览器不报警,说明CA已经进了系统信任链,Docker Desktop大概率也能认。
这里有个我踩过的坑:有时候在Mac上把CA加进了系统钥匙串,Docker Desktop还是报x509错误。后来发现必须把CA同时加到“系统”钥匙串,而不是“登录”钥匙串。也就是说,双击ca.crt导入的时候,要选择“系统”而非默认的“登录”,保存的时候还要输入一次本机密码。这个问题在多数教程里都不会提,但网上搜索Docker Desktop证书不生效的案例,八成是这个原因。
3. 账密认证:htpasswd基础认证与docker login流程
3.1 htpasswd生成账密文件的细节
有了HTTPS加密之后,下一步就是给Registry加认证,让只有拿到账户密码的人才能推送和拉取镜像。Registry官方镜像内置的支持方式是htpasswd认证文件,它用的是HTTP Basic Auth,账密文件里保存的是用户名和bcrypt散列后的密码。
生成账密文件最简单的方法是用httpd:2这个镜像来执行命令,避免在宿主机安装额外的工具:
bash复制mkdir -p /opt/registry/auth
docker run --rm --entrypoint htpasswd \
httpd:2-alpine -Bbn admin 'your-strong-password' \
> /opt/registry/auth/htpasswd
用-B参数强制使用bcrypt散列算法,很重要。老版本htpasswd默认可能用MD5或crypt散列,Docker Registry新版本对这类弱散列有兼容性问题,有时候认证会莫名失败。这个命令生成的文件里是一行用户名:散列值的形式,不要手动编辑这个文件,新增用户就再执行一次并追加进去:
bash复制docker run --rm --entrypoint htpasswd \
httpd:2-alpine -Bbn dev-lee 'another-password' \
>> /opt/registry/auth/htpasswd
每次追加完账密,不用重启Registry,因为Registry在每次请求时会重新读取文件,这个是实测过的高效维护账号的方式。
3.2 Registry的认证配置与客户端登录
启动命令里加两个环境变量就够:
bash复制 -e REGISTRY_AUTH=htpasswd \
-e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
-v /opt/registry/auth:/auth
注意这里不需要重启后清空已推送的镜像,认证只影响“请求是否被允许”,不影响已存储的数据。启动完Registry之后,客户端在拉取或推送前要先执行一次登录:
bash复制docker login registry.internal.example.com
输入用户名密码后,Docker会把这些凭证缓存到~/.docker/config.json里。这里有个细节值得关注:Docker默认会用系统自带的pass或钥匙串来存凭证,第一次登录可能会提示你创建或输入钥匙串密码。如果是在CI/CD这种无人值守的机器上,建议直接在~/.docker/config.json里写"credsStore": "",取消凭证存储代理,让Docker直接保存Base64编码的账密,避免CI流水线里卡在钥匙串交互上。
做完认证之后,没有登录的客户端执行docker pull会直接报unauthorized: authentication required,推送更是会报denied: requested access to the resource is denied。这两个报错都是认证成功生效的直观信号。
3.3 基础认证的局限
htpasswd方案能挡住绝大多数“手滑访问”和“误操作”,但它的局限性也很明显。首先,所有账号权限是平级的,只要通过认证就能推送任意镜像,没法做到“这个项目只有A团队能推,那个项目只有B团队能拉”。其次,没有操作审计,出了问题根本不知道是谁在什么时候改了哪个镜像。最后,账号管理办法原始,添加和删除账号要靠命令行操作,还得登录到服务器上执行。所以我的经验是:htpasswd适合个人项目、小团队内部共享、或者做技术验证;一旦镜像要服务正式业务,尽早切Harbor。
4. 生产级方案:Harbor的认证体系与TLS配置
4.1 Harbor解决了基础认证解决不了的问题
Harbor本质上是一个基于Registry封装的企业级镜像仓库,它保留了原版Registry的存储和分发能力,在其上叠加了完整的Web管理界面、基于项目的权限模型、LDAP/AD对接、镜像签名和漏洞扫描这些能力。我用Harbor之后最大的感受是:它把“仓库管理”从“命令行操作服务器”变成了“HR系统里开个账号”,管理成本直线下降。
它的认证体系分为两层。底层是Harbor内置的用户库,默认管理员是admin,初始密码在安装时通过配置文件指定。上层是项目权限,每个项目可以设置为公开或私有,然后把用户绑定到不同角色上。这里有一个重要的概念迁移:在原始Registry里,登录成功就等于所有权限都放开了;在Harbor里,登录成功只代表你进了系统,真正能对哪些镜像做什么操作,还要看你在具体项目里的角色。这个机制和我们熟悉的Linux权限模型很像,先有用户,再谈组和权限。
4.2 Harbor安装中的关键配置项
Harbor的安装方式在它官网文档里写得很详细,我建议直接使用离线安装包。离线包体积较大,但不需要在服务器上额外拉取基础镜像,版本也更好控制。安装过程其实就是改一个harbor.yml,然后执行./install.sh。
这里分享几个配置文件里不得不注意的点。hostname字段建议写内网域名,而不是IP。这不仅仅是证书校验的问题,Harbor生成的对外URL和推送地址都会基于这个字段,如果写成IP,后续更换服务器IP会造成所有客户端配置失效。harbor_admin_password必须改,而且强烈建议设置成强密码。官方默认是Harbor12345,这个在大多数扫描工具里是被直接收录的默认口令,一旦仓库暴露到公网,等于把admin权限送了人。
TLS证书的配置方式和前面Registry一样,在harbor.yml里指定certificate和private_key的路径:
yaml复制https:
port: 443
certificate: /opt/harbor/certs/registry.crt
private_key: /opt/harbor/certs/registry.key
安装完成后,Harbor会生成一个nginx容器来做端口和流量的转发,所以外部访问的入口看起来非常像一个标准Web服务。这里有个小细节:如果你要开放的是8443端口,就要同时修改https.port和nginx对应的映射,否则访问会超时。我第一次部署Harbor的时候只改了harbor.yml里的端口号,没注意nginx容器还需要重新生成配置,结果端口一直不通,排查了半小时。执行过./install.sh之后,Harbor会用当前配置重新生成docker-compose文件,所以端口改动后必须重新跑一次安装脚本,不能只重启容器。
4.3 用户、项目与权限模型
Harbor的典型使用流程是这样的:管理员创建用户,然后创建项目,把用户加入项目并分配角色。项目里的角色分为“项目管理员”、“维护者”、“开发者”和“访客”几个级别。开发者角色能推送镜像,访客只能拉取公开项目的镜像,维护者还能管理镜像标签和扫描结果,项目管理员则拥有该项目的全部管理权限。
实际使用中,我经常看到团队把这些角色关系搞混。最典型的问题是,开发者角色往项目里推送镜像时报denied错误,明明已经登录成功了。这个多半是用户在项目里的角色不对,或者项目本身设置了只读策略。排查思路很简单:登录Web界面,打开对应项目的“成员”页面,确认当前账号的角色至少是开发者。还有一类问题是,镜像拉取时要求登录,但项目明明是公开的。Harbor的公开项目确实允许匿名拉取,但如果你在docker pull时带了之前缓存的旧凭证,Docker会优先用凭证访问,一旦凭证对应的账号没有该项目的拉取权限,匿名本来能拉取的也会变得拉不动。这种状态下把~/.docker/config.json里对应条目删掉再拉一次,往往就恢复正常了。
5. 常见问题与排查实录
5.1 x509证书错误
客户端执行docker login时报x509: certificate signed by unknown authority,是最常见的问题。原因基本可以锁定为客户端没有信任签发仓库证书的那个CA。处理步骤分三类:
- Linux下,把CA复制到
/etc/docker/certs.d/域名:端口/ca.crt并重启docker服务。 - Mac下,把CA导入系统钥匙串,并确认Docker Desktop能读取系统信任链。
- 测试环境临时方案,在
daemon.json里把仓库地址加进insecure-registries,但这会把TLS降级为“不校验”,仅限演示时用,生产环境不要这么做。
另一种x509报错是certificate relies on legacy Common Name field,这说明证书没有SAN。这种问题只能回到签发环节重新生成带SAN的证书,客户端再怎么配置也没用。
5.2 401 unauthorized、403 denied与登录失败
401或403报错要区分阶段。如果执行docker pull时直接报unauthorized,前提是仓库开了认证但客户端没登录,执行一次docker login即可。如果已经登录了还报403,优先去Harbor后台查角色和项目权限。对于htpasswd方案,有一个不起眼但很常见的坑:htpasswd文件里账户名和密码某个字符用了单引号包裹的shell变量,而变量没有正确展开,导致写入文件的密码和实际输入的密码不一致。建议生成文件后执行docker run --rm httpd:2-alpine htpasswd -bv 用户名 密码来验证下账密是否真的匹配。
5.3 推送速度慢、镜像分层多的问题
仓库启用加密和认证之后,有些同学反馈推送镜像变慢了。首先要区分是网络问题还是认证导致的开销。TLS握手和认证本身的开销很小,基本可以忽略。真正影响推送速度的是镜像层数和每个层的大小。如果镜像构建时没有优化层数,一个镜像几十个层,推送时每层都要单独做HTTPS请求,慢是很正常的。
优化手段主要有两个层面。第一,构建镜像时尽量合并RUN指令,减少中间层;第二,针对同一应用的多个版本,尽量复用基础层,不要在每一层里塞入大文件。还有一个容易被忽略的点:Docker推送时默认是串行推层的,但新版Docker支持--max-concurrent-uploads配置,默认值是5,如果你的网络带宽足够,可以在daemon.json里适当调高并发数。
5.4 Docker Desktop下自签证书的兼容问题
Docker Desktop用户在使用自签证书仓库时,经常遇到浏览器能打开,但Docker CLI死活连不上的情况。原因在前面提过:Docker CLI跑在Docker Desktop内置的虚拟机里,宿主机浏览器信任的证书链和虚拟机里Docker看到的证书链不是同一套。解决办法是把ca.crt放到Docker Desktop的虚拟机系统信任链里,具体路径是:
Mac上可以在Docker Desktop的“Settings → Resources → Advanced”里找到证书管理入口;Windows在“Settings → Docker Engine”页面里,可以直接编辑daemon配置并指定insecure-registries或通过图形界面导入证书。导入后务必重启Docker Desktop,而不是只重启Docker Engine。如果重启后还是x509报错,再检查一下证书文件权限,Docker的内部服务有时会因为证书文件权限过宽而拒绝读取。
5.5 忘记Harbor管理员密码
这个坑几乎每个用过Harbor的人都会踩一次。Harbor v2.x之后,admin的密码散列存放在数据库里,而数据库凭证在harbor.yml中配置。如果彻底忘了密码,可行的方法是停掉Harbor服务,进入Harbor的数据库容器,直接更新用户表的密码散列。实际操作中我建议更快的思路:查看harbor.yml里配置的数据库密码,然后通过docker exec进入harbor-db容器,手动重置密码。
但这里我想反向提醒一句:与其折腾数据库改密码,不如在Harbor初始化时就把管理员密码保存到团队密码管理工具里,同时关闭默认管理员登录名或限制其IP来源。Harbor高版本支持配置“仅允许通过HTTP Header认证”之类的选项,你也可以把admin密码保存到密钥管理服务,不给员工直接暴露这个账号。
6. 认证方案选型:先想清楚你要管到什么程度
很多人问,到底用Registry加htpasswd还是直接上Harbor。我的回答是看你未来半年镜像仓库的定位。如果只是给自己和两三个同事用,分支简单、项目不多,Registry加HTTPS加htpasswd足够,维护成本低到可以忽略。如果仓库要承担团队级别的镜像分发,或者要对接CI流水线,再或者有安全审计需求,请直接上Harbor,不要先搭Registry再迁移,迁移的成本比一开始就上Harbor高得多。
再补充一个很多人忽视的选型点:Harbor对仓库的存储也有自己的抽象,底层既可以用本机目录,也可以用S3、OSS这类对象存储。如果你已经用了云厂商的对象存储,Harbor的存储后端配置非常值得提前了解,它能帮你把镜像存储和计算节点解耦,实现真正的容灾和扩容。这一块听起来和加密认证无关,但你要知道,镜像仓库的认证和存储是两件独立的事,很多团队在选型时把它们绑在一起考虑,反而限制了后续扩展。
7. 从加密认证再往前一步:镜像签名与供应链安全
仓库的加密和认证,本质上是在解决“传输安全”和“访问控制”。但如果你真正关心生产环境的供应链安全,还得再往前走一步,做镜像签名和完整性校验。Harbor自带的内容信任功能就基于Notary实现的,启用之后,客户端需要在构建和推送镜像时对镜像进行签名,拉取时也默认校验签名。
我建议不要跳过这一部分。镜像签名解决的是仓库被攻破后、恶意镜像被推送到合法仓库里的问题。认证只能保证“只有合法用户能推送”,保证不了“合法用户推送的镜像一定合法”。如果你所在的团队服务的是金融、政务这类对合规有要求的业务,镜像签名几乎是硬性需求。
这里配套一个实用的操作习惯:在CI流水线里,推送镜像时用DOCKER_CONTENT_TRUST环境变量开启内容信任模式:
bash复制export DOCKER_CONTENT_TRUST=1
docker push registry.internal.example.com/project/app:v1.0
这样每次推送到Harbor的镜像都会自动生成签名元数据,拉取侧在受信任模式下也能发现镜像是否被篡改过。老实说,这套机制在对接第三方镜像(比如从Docker Hub拉基础镜像)时会有兼容性摩擦,所以我一般只在推送到自有仓库时开启,拉取外部基础镜像时按需关闭或单独配置例外。
8. 实操心得与给新手的建议
这类基础设施相关的内容,我最后多说几句,算是个人的一些体会。
第一,证书和域名这种基础设施层面的东西,一旦定下来,后面再改的代价远超你想象。我见过一个团队用IP方式搭好了整个Registry和几十台客户机的信任配置,后面要迁到新服务器时,所有客户端都要改CA和insecure-registries配置,连着折腾了两天。所以一开始就规划好域名,哪怕买一个便宜的域名只用于内网解析,后续迁移是完全无感的。
第二,认证体系一定要跟着“最小权限原则”走。哪怕小团队也尽量别用共享账号,每个开发者的账号独立开来,日志里才能定位到人。Harbor支持对接LDAP和OIDC,公司里有现成的目录服务就直接对接,用起来远比在每个系统里建一套独立账号省心。
第三,这份配置里藏得最深的一个问题是,仓库的可用性。很多团队在加密认证做完之后就以为万事大吉了,结果仓库服务一挂,所有依赖镜像拉取的环境全部瘫痪。Registry或Harbor本身没有高可用能力,真要保证可靠性,要么做多副本和负载均衡,要么借助底层对象存储的多区域能力,给镜像仓库加一层冗余。这是另一个大话题,但建议在选型的时候就一并纳入考虑。
回到加密与认证本身,Docker仓库的这层防护做到位,镜像的传输和访问控制就有了基本盘。之后再往上层叠加内容签名、扫描、审计,整个容器供应链的安全轮廓就比较完整了。按我这些年的运维经验,基础设施越是早期投入到位,后面业务跑起来就越省心。这批配置当你拉一个镜像只需要一条命令、推送一个新版本只需要一条流水线的时候,回头看这些操作,会发现它们全都是值得的。
