在 Kubernetes 的日常排障中,网络问题往往最耗时间。这次我在飞牛 NAS 上用 Docker 跑了一个 K3s(单节点 master),准备给 CI 跑 argocd-diff-preview。结果 Argo CD 部署完成后一直报 DNS 解析失败,日志里看起来像“在 Pod 里用 localhost 查 DNS”,非常反直觉。

最后定位到的根因也很“隐形”:宿主机目录的 Default ACL 影响了 containerd 为 Pod 生成并挂载的 resolv.conf,导致非 root 容器无法读取 /etc/resolv.conf,从而触发了 libc resolver 的兜底逻辑,盲目向 127.0.0.1/[::1]:53 发起查询。

背景

我本地用 Argo CD 管理和发布应用。这次想引入 argocd-diff-preview 做 CI 检查,把 diff 结果作为 GitHub 评论回写到 PR。

效果大概是这样:

1774962665949.png

为了跑这类任务,我需要一个轻量的 Kubernetes 集群。正好手头有飞牛 NAS,就在上面用 Docker 起了一个 K3s server(禁用 servicelb),docker-compose 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.8"
services:
argocd-k3s:
image: k3s:v1.35.1-k3s1
container_name: argocd-k3s
privileged: true
network_mode: bridge # 防止本机的 Docker 网络的 DNS 干扰
restart: unless-stopped
ports:
- "9444:6443"
command:
- "server"
- "--prefer-bundled-bin"
- "--disable=servicelb"
- "--node-name=argocd-k3s"
- "--kube-apiserver-arg=event-ttl=10m"
volumes:
- /vol1/1000/K3sData/argocd-k3s/data:/var/lib/rancher/k3s
- /dev:/dev:ro

然后在 CI 里执行 argocd-diff-preview 时就开始报错:

1
2
3
4
5
6
7
8
9
10
11
argocd-diff-preview  \
--create-cluster=False --keep-cluster-alive \
-d \
--argocd-chart-url ${chart_url} \
--repo ${repo} \
--target-branch ${env.CHANGE_BRANCH} \
--argocd-chart-name argocd-apps

Tue, 31 Mar 2026 12:42:20 UTC DBG AppsetGenerateWithRetry attempt 1/5 to Argo CD...
Tue, 31 Mar 2026 12:42:21 UTC DBG Waiting 1s before next appset generate attempt (2/5)...
Tue, 31 Mar 2026 12:42:21 UTC WRN Appset generate attempt 1/5 failed. error="failed to run argocd appset generate: argocd command failed: {\"level\":\"fatal\",\"msg\":\"rpc error: code = Internal desc = unable to resolve git revision : failed to list refs: dial tcp: lookup argocd-redis on [::1]:53: read udp [::1]:59984-\\u003e[::1]:53: read: connection refused\",\"time\":\"2026-03-31T12:42:21Z\"}\n: exit status 20"

现象:Pod 里用 [::1]:53 查 DNS

Argo CD 无法正常同步应用。查看 argocd-server 日志,发现大量类似报错:

1
Failed to resync revoked tokens... dial tcp: lookup argocd-redis on [::1]:53: read udp [::1]:34952->[::1]:53: read: connection refused

第一眼看到这个日志,我的反应是:为什么 Pod 内部会用 IPv6 的本地环回地址([::1])去查 DNS?

按常规思路我先检查 CoreDNS,但 CoreDNS 运行完全正常。这就更怪了:dnsPolicy: ClusterFirst 的 Pod,理论上应该把查询发给 CoreDNS(例如 10.43.0.10),而不是对着本地 53 端口重试。

排查:从 /etc/resolv.conf 入手

既然外部看不出问题,就直接进入容器确认 DNS 配置:

1
kubectl -n argocd exec -ti argocd-server-fbbdf5d4d-c8z5q -- /bin/bash

然后查看 /etc/resolv.conf

1
2
argocd@argocd-server:~$ cat /etc/resolv.conf
cat: /etc/resolv.conf: Permission denied (os error 13)

到这里基本就能解释日志了:应用进程读不到 /etc/resolv.conf,就拿不到 nameserver 配置。

在这种情况下,libc resolver 会走兜底逻辑,直接尝试向本地 localhost127.0.0.1[::1])发 DNS 查询;如果本地没有 DNS 服务监听 53 端口,就会出现 connection refused

所以这并不是 IPv6 本身的问题,而是容器“看不见”正确的 nameserver。

根因:文件权限末尾的 “+”(ACL)

继续用 ls -l 看权限:

1
2
argocd@argocd-server:~$ ls -alh /etc/resolv.conf 
-rw-r-----+ 1 root root 102 Mar 31 11:56 /etc/resolv.conf

我把这里的内容帖给 AI 之后, 它就提到了 ACL 问题:

注意权限末尾的 +:这表示该文件启用了 ACL(Access Control List)。即使基础权限看起来没问题,ACL 也可能额外收紧“其他用户(other)”的权限。由于 Argo CD 默认以非 root 用户(例如 UID 999)运行,它就会被挡在门外。

/etc/resolv.conf 不是镜像里的文件,而是运行时挂载进来的。为了确认它在宿主机上对应哪个文件,我回到宿主机用 crictl inspect 查了容器挂载信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
~ # crictl ps  # 找到 argocd-server 容器的 ID
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
c2722b3817277 86219ff1fefea 2 seconds ago Running argocd-server 3 d32169958ed68 argocd-server-fbbdf5d4d-tjsnf argocd

~ # crictl inspect <container_id> # 查看 /etc/resolv.conf 的真实 source
# {
#   "destination": "/etc/resolv.conf",
#   "options": [
#     "rbind",
#     "rprivate",
#     "ro"
#   ],
#   "source": "/var/lib/rancher/k3s/agent/containerd/io.containerd.grpc.v1.cri/sandboxes/fd2373cfd801e2f95267286af155df3ca0cd912563bc9a668cc5e1b3a4956d3e/resolv.conf",
#   "type": "bind"
# }

~ # cat /var/lib/rancher/k3s/agent/containerd/io.containerd.grpc.v1.cri/sandboxes/fd2373cfd801e2f95267286af155df3ca0cd912563bc9a668cc5e1b3a4956d3e/resolv.conf
search argocd.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.43.0.10
options ndots:5

由于我的 K3s 数据目录在 /vol1/1000/K3sData(NAS 挂载盘)上,顺着这个路径找过去,再用 getfacl 查看 ACL:

1
2
3
4
5
6
root@Host:~# getfacl /vol1/.../sandboxes/.../resolv.conf
# owner: root
# group: root
user::rw-
group::--x
other::--- <--- 真凶在这里!

真相大白:/vol1 这类 NAS 挂载盘默认可能带有严格的 Default ACL。当 containerd 在这个目录下为 Pod 动态生成 resolv.conf 时,会继承父目录的 Default ACL,把 other 权限收紧为 ---。Pod 里的非 root 进程就无法读取 /etc/resolv.conf,从而触发“查 localhost DNS”的兜底行为。

解决方案:清理 Default ACL 继承规则

我最终没有改 K3s 配置,也没有去关闭整块盘的 ACL(这通常会影响 Samba 等服务)。更稳妥的做法是:只清理 K3s/containerd 工作目录上导致继承的 Default ACL 规则。

在宿主机执行:

1
2
3
4
5
# 1. 清除沙盒目录上导致问题的 Default ACL 遗传规则
sudo setfacl -k /vol1/1000/K3sData/

# 2. (可选) 给当前存在的目录批量重置基础权限
sudo setfacl -R -b /vol1/1000/K3sData/

做完后把 Argo CD 的 Pod 删掉让它重建。新的 Pod 启动后,containerd 生成的 resolv.conf 恢复为正常的可读权限;再进入容器执行 cat /etc/resolv.conf 也不再报 Permission denied。至此,Argo CD 立刻恢复正常,应用也能正常 Sync。

自动持久化:使用 tmpfiles.d (进阶方案)

如果你担心 NAS 系统在重启或通过 Web UI 操作后会重新“遗传”错误的 ACL 规则,可以使用 systemd 的 tmpfiles.d 机制。这是一种比 Crontab 脚本更优雅的“声明式”配置。

在宿主机创建配置文件 /etc/tmpfiles.d/k3s-nas-acl.conf

1
2
3
4
5
6
# 确保目录及其子目录基础权限为 0755
# 类型 路径 模式 用户 组 时间 参数
z /vol1/1000/K3sData/ 0755 root root - -

# 递归强制应用规则:确保 others 可读,且新生成的子项也继承该权限
A+ /vol1/1000/K3sData/ - - - - other::r-x,default:other::r-x

配置完成后,可以执行以下命令立即生效(无需重启):

1
sudo systemd-tmpfiles --create /etc/tmpfiles.d/k3s-nas-acl.conf

系统会扫描该路径,并按照配置自动纠正所有不符合要求的 ACL 项。

总结与避坑指南

  • 别被 [::1]:53 误导:Pod 内查 localhost DNS,优先检查是否能读取 /etc/resolv.conf
  • 留意非 root 容器:越来越多组件默认非 root 运行,宿主机生成/挂载的文件必须保证“other 可读”或至少对应用用户可读。
  • 小心 NAS 默认 ACL:在 NAS 挂载盘上跑 K3s/Docker 时,ls -l 权限末尾的 + 往往是权限问题的信号;遇到诡异现象优先用 getfacl 定位。
  • AI 真的太强大了, 问对合适的问题, 能让调试更高效。

希望这次记录能给你一个更快的排查切入点。