blue 与 green 网络范围分离、部分上下文共享;删除 blue 名称后,进程引用仍连接命名空间。

网络命名空间隔离网络资源,名称并不拥有其中所有进程

Linux 网络命名空间分离接口、路由、邻居表、套接字端口和包过滤规则等网络资源。它与其他命名空间共享内核,不是独立操作系统。进程通常继承父进程的网络命名空间,工具也可以在另一个范围中执行进程。这种隔离针对网络资源,不会自动涵盖全部进程身份和文件系统访问,因此需要按实际机制说明边界。 坏掉的样子 运行 ip netns exec 不会自动获得完整容器隔离。PID、用户身份和主机名隔离属于其他机制。应用可能看到不同路由,却仍与父环境共享普通文件和主机名。如果把一个网络命名空间当成完整沙箱,就会赋予它从未配置过的保证。应区分实际改变了哪类资源,以及哪些属性仍然来自原有运行环境。 新命名空间也不会自动得到有用的外部连接。接口需要有意放置和配置,本地回环可能需要启用。移动接口会改变管理它的命名空间,原范围不再把该设备列为自己的接口。因此,从错误范围执行配置命令,可能报告接口不存在,尽管预期设备仍位于另一个命名空间。诊断前应先确认命令的执行上下文。 名称还有独立生命周期。ip netns 按约定在 /run/netns 暴露引用,通常也能通过 /var/run/netns 访问。删除名称会移除这项引用,但进程或其他引用仍可维持命名空间存活。如果清理流程把名称列表中消失,直接等同于所有服务终止,就可能留下网络资源和运行中的进程。名称只是管理入口,不是进程所有权的完整表达。 网络范围不同,而选定上下文仍共享。删除名称后,自有进程引用仍可存活;当前运行仍待验证。 快速验证 文中结果属于历史文章报告;保留验证文件中未找到原始集成日志,本轮没有重新执行特权操作。 将脚本保存为 check-network-namespace.py,只在获准的一次性虚拟机中准备 Docker,并启用 Python 断言运行 python3 check-network-namespace.py。固定的 Python 3.11.16 与 iproute2 6.1.0 软件包仍需可取得。NET_ADMIN 与 SYS_ADMIN 是前提,不保证越过其他安全限制;不要削弱宿主保护。临时 proc 挂载支持其中的网络参数设置,不移动宿主接口,也不发布宿主端口: ...

二月 19, 2028 · 2 分钟 · XB Field Notes
两个命名空间边界内的 left0 与 right0 构成一对链路;右侧还拥有回环目的地址。

veth 对在两个端点之间搬运帧,路由决定这些帧承载的路径

veth 对由两个互联的虚拟以太网设备组成。从一端发送的帧到达另一端,再由接收网络栈或网桥处理。两端可以放在不同 Linux 网络命名空间,因此常用于容器网络。创建这一对设备只提供链路,地址、路由、桥接关系、转发和访问策略都需要另外配置。不能把接口已经出现,等同于完整网络已经准备好。 坏掉的样子 接口显示管理状态 UP,仍可能因为 veth 对端关闭而没有载波。只读取 UP 标志,会让尚未完成的配置看起来已经就绪。应检查两个端点,并区分管理状态与 LOWER_UP。路由也可能已经指向这个接口,但链路仍无法承载有效流量,所以路由列表本身不是连通性检查,更不能代替一次真实应用交换。 veth 对不会把 IP 载荷限制为对端接口自身地址。路由选择报文经过哪个链路,随后以太网转发或接收网络栈决定下一步处理。发往容器直接邻居之外的流量,也可以通过这一对设备到达路由器或网桥。声称“不发给对端的包都会留在本地”,混淆了链路交付与 IP 目的地选择,容易误导容器出口排障。 两端生命周期也相互关联。删除一个 veth 端点,会移除另一个端点,即使对端由另一个命名空间管理。因此宿主侧清理动作可能删除容器接口。影响活跃工作负载时,应有意识地安排停止流量、调整路由和删除设备的顺序。命名空间分离了管理视图,却没有让成对设备变成两项互不影响的资源。 veth:跨命名空间设备对:概念机制与预期检查,不是新增运行证据。 快速验证 将脚本保存为 check-veth.py,仅在已获授权的一次性 Linux 实验环境中,使用核实为本地的 Docker daemon,并保留 Python 断言。配方注明 Python 3.11.16、iproute2 6.1.0,但镜像架构、包与宿主内核仍需核对。NET_ADMIN、SYS_ADMIN 无法覆盖强制策略的挂载拒绝,Docker 默认 AppArmor 配置即存在此限制。应使用兼容且获批的实验环境,不应为运行示例削弱宿主保护。嵌套命名空间与 proc 挂载需要实际内核权限,运行时隔离不代表拉取镜像和构建离线;清理命令仅尝试执行,未全部核对退出码。 ...

十一月 21, 2027 · 2 分钟 · XB Field Notes
受约束进程不能写只读根文件系统,但仍可使用指定的可写工作卷。

安全上下文需要运行时证据来验证

Kubernetes 的 securityContext 配置进程身份及部分操作系统约束,可以缩小被攻陷应用允许执行的操作,但本身不会建立完整安全边界。应先确定应用实际需要什么操作,再配置对应字段,并在最终容器中同时测试允许与拒绝的行为。只有一份看起来严格的清单,还不能说明运行中的进程已经按预期受到限制,也不能代替应用本身的权限设计。 原文 Kubernetes 1.34.11 历史样例,本次未重跑。结果反映组合设置;RuntimeDefault 取决于运行时。没有测试容器逃逸抵抗、全部 seccomp 规则、敌对多租户隔离或应用授权。执行仍为受阻。 把设置放在正确层级 Pod 级配置可以提供用户和组身份、共享卷的组权限行为,以及 seccomp 配置。容器级字段可以覆盖适用的默认值,并设置 capabilities、权限提升和只读根文件系统。需要核对字段真实 API 范围,把熟悉的名称写在错误位置,并不会获得等价约束。相同的 securityContext 名称之下,不代表所有字段都能互换放置。 ...

十一月 20, 2027 · 3 分钟 · XB Field Notes
摘要与签名者绑定,构建来源则保留为独立证据。

镜像签名把摘要绑定到受信任签名者

容器镜像标签是可变指针,摘要则标识内容。有效签名把一份关于该摘要的声明绑定到签名密钥;这把密钥是否代表获授权的人或工作负载,要由信任策略判断。单靠签名,不能证明谁构建了镜像、构建是否遵循所宣称的流程,也不能证明软件内容安全。这些问题需要不同证据,不能因为校验成功就合并成一个结论。 真正有用的边界,是部署之前的验证决定。校验方需要检查内容绑定和预期信任根,再执行身份与来源策略。registry 地址、熟悉的标签,或者存在某份签名,都不足以独立构成放行依据。必须知道自己信任谁、允许其声明什么,以及这份声明究竟覆盖哪一个产物。 摘要是身份,标签是指针 OCI 清单摘要对精确清单字节进行哈希,清单中的描述符再通过各自摘要标识配置与层,客户端也须核对这些绑定。在哈希抗碰撞假设下,清单改变会得到不同摘要。如果只修改存储层中的字节却不更新描述符,应触发内容完整性失败,而不是悄悄把原镜像重新定义成另一份内容。内容寻址依赖实际校验,不只是路径看起来像摘要。 Cosign 的普通镜像签名载荷包含镜像摘要,验证时会把它与请求的镜像比较。不同标签可以指向同一个已签名摘要;标签一旦移动,先前的校验不能覆盖新对象。应解析并验证目标摘要,再部署同一个摘要。如果校验之后重新按标签拉取,就留下检查与使用之间的竞争窗口;重复查询标签也不能彻底关闭这个窗口。 标签字符串不能绑定镜像内容 对字面字符串 app:v2 签名,认证的是这个声明,并不是名字后面不断变化的对象。签名也可以包含其他上下文,所以需要检查真实载荷格式与校验器行为,而不是把所有方案都说成只签一个裸哈希。不要为了让不匹配的载荷通过,就关闭摘要声明校验,否则签名有效与目标镜像之间的关键联系会丢失。 基于密钥的校验,需要由策略说明为什么信任所给公钥。无密钥校验还要检查预期证书身份与 OIDC 颁发者,以及证书链和所要求的透明日志证据。Fulcio 证书把身份关联到密钥,不会检查构建过程或认证软件质量。要判断构建声明,还需要适当的来源证明、受信任的构建者,以及证明中主体摘要与待运行产物一致的检查。 校验才是控制,签名不是 强制检查可以放在准入或部署前,但效果取决于实际覆盖范围。需要确认哪些工作负载与 registry 被纳入策略,以及校验失败时如何处理。只记录摘要不能证明发布获授权;即使签名正确、身份也获授权,仍不能据此排除所有漏洞或恶意行为。安全测试与构建来源验证应各自提供证据,不能相互替代。 密码学签名有效与当前策略接受,是两回事。退役密钥需要更新所有相关验证方的信任策略,仅生成一把新密钥不会自动完成撤销。短期证书配合可信签名时间证据,可以支持证书到期之后验证历史签名。透明日志让活动可检查,却不会自动撤销泄露身份或阻挡恶意发布。事故响应仍须决定保留哪些身份、时间窗口、摘要和证明,不能把日志存在等同于风险已解决。 坏掉的样子 部署步骤先验证一个摘要,随后又按可变标签拉取;另一次推送就可能让最终运行的字节变掉。需要把经过验证的摘要一直传到部署交接处,并明确它指向单平台清单还是多平台索引,确认运行时怎样选择具体平台镜像。这样才能说明实际运行对象与被检查对象之间的关系。 另一种失败是接受任何能通过密码学检查的签名者。应把信任限制到获授权密钥,或明确的身份与颁发者组合,并测试错误签名者和未批准摘要会被拒绝。发现泄露后需要调查并改变策略;这并不自动证明该密钥以前签过的所有产物都恶意,也不能承诺继续接受它们一定安全。 镜像签名信任:机制与证据边界。 快速验证 下面是命令模板,不是可直接运行的夹具:registry 是示例,摘要也被省略。进行获授权的 registry 检查之前,需提供已有测试镜像的完整摘要、可信公钥,并确认所安装的 Cosign 与 Skopeo 版本。本次源码审阅没有签名、操作 registry 或执行部署,因此没有新的集成运行结果。 cosign verify --key cosign.pub registry.example.com/app@sha256:9ab5a134... cosign tree registry.example.com/app@sha256:9ab5a134... skopeo inspect --format '{{.Digest}}' docker://registry.example.com/app:latest 预期结果是指定摘要在选定密钥下通过校验,不一定打印可读的人类身份。cosign tree 发现关联对象,列出对象不等于验证其中声明。Skopeo 可以查看标签当下指向;如果不同,只说明它与刚才检查的摘要不同,另一个摘要仍可能有自己的签名。即使两者相同,也只是观察时刻的快照,无法阻止标签随后移动。 ...

十月 21, 2027 · 1 分钟 · XB Field Notes
ENTRYPOINT 保留,带叉号的 CMD 默认参数块由运行参数替换。

ENTRYPOINT 和 CMD 定义可被运行者覆盖的启动默认值

镜像的 ENTRYPOINT 和 CMD 共同参与容器创建时的启动命令。使用 exec 形式 ENTRYPOINT 时,CMD 通常提供追加给入口的默认参数;没有入口时,CMD 可以提供可执行程序及其参数。这些是启动默认值,不是强制执行边界。有权限创建容器的运行者,可以替换入口或参数,因此不能把 ENTRYPOINT 描述成无论如何都不可改变的程序锁。 坏掉的样子 在 docker run image ... 中写到镜像名之后的参数,会替换 CMD,不是自动加到原 CMD 默认值之后。exec 形式入口仍然保留,除非显式覆盖。用户以为自己只是追加一个选项,却可能丢掉全部默认参数。因此,命令行工具需要说明哪些值可省略、哪些必须重新提供,以及自身参数解析器如何处理覆盖。 exec 形式使用 JSON 数组,不会隐式运行 shell。$DEMO 这样的字符串会原样传给程序,而不是自动展开环境变量。管道、重定向和通配符也需要真正的解释器,才会获得相应含义。shell 形式会改变参数和进程语义,所以两种形式之间的切换并不只是语法美化,不能只看命令表面似乎相同。 启动包装脚本还可能让服务藏在不转发停止信号的 shell 后面。Docker 通常向容器主进程发送停止信号,等待配置的超时,必要时强制终止。即使服务自身正确安装了 SIGTERM 处理函数,如果信号只到达另一个不会转发的进程,这个处理函数也无法发挥作用。排查优雅退出失败时,应先确认信号实际发给了谁。 运行参数替换 CMD 默认值;exec 改变信号送达路径,却不实现应用清理。 快速验证 使用启用断言的宿主 Python 3.7 或更新版本,以及能够取得固定基础镜像的 Docker 构建器。早期记录报告 Docker 29.3.1、Linux arm64,镜像内 Python 3.11.16;这里没有复现该次运行。PID 断言要求容器使用私有 PID 命名空间,且没有注入 init。实验关闭容器网络,等待服务就绪输出后测试停止,并尝试清理容器和镜像标签,不依赖盲目等待固定时长猜测就绪: ...

十月 3, 2027 · 2 分钟 · XB Field Notes
先前层仍保存文件字节,后续 whiteout 只在合并视图中隐藏路径。

后续镜像层删除文件,并不会擦除先前层的原始字节

容器镜像层描述按顺序应用的文件系统变化。一层可以新增或替换文件,也可以记录某个先前路径应从合并文件系统中消失。普通容器进程看到的是最终合并视图,但这个视图不等于随镜像分发的全部字节历史。因此,仅检查运行中的容器,可能遗漏早期层仍然保留的数据,尤其容易误判删除操作的实际效果。 原文历史样例,本次未重跑。image-save 归档不同于快照存储内部表示和物理磁盘分配。后续删除不能修复已经暴露的凭据。 坏掉的样子 把凭据复制进镜像,再通过后续 RUN 删除,不会从早期不可变层中移除凭据。能够取得相关层的人,仍可能恢复原始字节。同样,把很大的构建压缩包放进某层,再在下一条指令里删除,最终路径虽然不存在,分发内容仍可能包含整个压缩包。目录看起来干净,并不能证明镜像已经没有这份数据。 镜像层是文件系统变更集,不一定是紧凑的二进制差分。替换大文件,可能把替换后的完整内容放入另一层。删除通过 whiteout 标记,在应用层时屏蔽原有路径。OCI 还规定了隐藏下层目录内容的不透明目录规则,所以不能把正确应用镜像层简化成反复普通解压 tar;删除和覆盖语义需要被专门处理。 不是每条 Dockerfile 指令都会创建文件系统层。ENV、LABEL 和 CMD 改变镜像配置,可以产生标记为 empty_layer 的历史条目。因此,Dockerfile 行数、构建历史条目数,都不能可靠地替代根文件系统层数。即使没有增加应用文件字节,这些配置变化仍然影响执行方式和产物身份,不应被当成无关信息。 快速验证 使用 Python、Docker 构建器和固定摘要的基础镜像运行脚本。实测为 Docker 29.3.1,采用 containerd 镜像存储,基础镜像内为 Linux arm64 的 Python 3.11.16。标记内容特意设为公开文本。脚本构建并保存一个临时镜像,直接读取归档成员,不把归档解压到宿主文件系统,结束时删除自己的标签: ...

八月 16, 2027 · 2 分钟 · XB Field Notes
两个不同应用输出共享同一固定基础,所选构建参数不同;只固定 FROM 并不能确定完整发布。

固定基础镜像只固定一个输入,完整发布还需要其他记录

按摘要固定容器基础镜像,可以选择确定的镜像对象,避免依赖会移动的标签。这消除了一类意外构建变化,但不会让所有派生镜像或运行实例都完全相同。应用源码、构建参数、下载依赖、目标平台、运行配置和持久数据,仍可能改变构建结果及其行为。因此,基础镜像不可变是一个明确的输入约束,不是完整发布已经可复现的证明。 坏掉的样子 两个 Dockerfile 可以使用相同 FROM 摘要,却生成不同应用。即使 Dockerfile 一样,构建参数变化或未固定的下载取得新内容,也可能改变输出。所以,仅声明基础镜像不可变,无法证明完整构建是确定性的。决定这个性质的是全部输入,以及施加在输入上的转换过程;遗漏任何真正影响结果的部分,都会留下解释缺口。 完全相同的应用镜像,在不同环境变量下运行,也可能选择不同端点、功能行为或存储。可写绑定挂载还能替换镜像内已经测试过的文件。把配置留在镜像外通常有价值,但只要配置参与定义发布行为,它仍然需要版本管理和审阅。外置不等于无须控制,更不意味着配置变化不会影响同一镜像的实际表现。 普通容器还拥有可写文件系统层。紧急编辑可以改变一个实例,而镜像和其他实例保持原样。之后从未修改的镜像替换实例,编辑就会消失。因此,不可变部署实践需要把产物替换与受控运行状态结合起来;只在 FROM 后写摘要,无法自动强制团队遵守这种操作方式,也无法阻止容器内手工修补。 固定基础只控制一个输入。只读根约束特定写入,而挂载、数据与设置仍相互独立。基础更新需要主动审阅、重建和测试。Docker 示例执行仍受阻,本次未重跑。 快速验证 使用宿主 Python、Docker 构建器和已取得的固定基础镜像运行脚本。此前记录的实测环境为 Docker 29.3.1、Linux arm64,镜像内 Python 3.11.16;本轮没有重跑 Docker。容器网络与构建 RUN 网络被关闭,但镜像解析仍可能访问仓库;构建步骤没有下载软件包。实验结束删除自己的临时容器和标签,不修改任何既有服务,以便把观察限定在明确的受控输入上: from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,uuid BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=120) if check:assert result.returncode==0,result.stderr return result def inspect(image):return json.loads(docker('image','inspect',image).stdout)[0] name='xbcatm-pinning-'+uuid.uuid4().hex[:10];tags=[name+':one',name+':two'];container=name+'-running' try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory) (root/'Dockerfile').write_text(f'FROM {BASE}\nARG MESSAGE\nRUN printf "%s" "$MESSAGE" > /built-message\nENV MODE=default\nCOPY app.py /app.py\nCMD ["python", "/app.py"]\n') (root/'app.py').write_text('from pathlib import Path\nimport os\nprint(Path("/built-message").read_text(),os.environ["MODE"])\n') layers=inspect(BASE)['RootFS']['Layers'] for tag,message in zip(tags,['one','two']): docker('build','--network','none','--build-arg','MESSAGE='+message,'-t',tag,str(root)) assert inspect(tag)['RootFS']['Layers'][:len(layers)]==layers assert inspect(tags[0])['Id']!=inspect(tags[1])['Id'] assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='one default' assert docker('run','--rm','--network','none',tags[1]).stdout.strip()=='two default' assert docker('run','--rm','--network','none','-e','MODE=changed',tags[0]).stdout.strip()=='one changed' print('both builds use the same pinned base layers; different build arguments produce different artifacts') print('the identical application image behaves differently when runtime configuration changes') docker('run','-d','--name',container,'--network','none',tags[0],'python','-c','import time;time.sleep(60)') docker('exec',container,'python','-c','from pathlib import Path;Path("/built-message").write_text("edited")') assert docker('exec',container,'python','/app.py').stdout.strip()=='edited default' assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='one default' failed=docker('run','--rm','--network','none','--read-only',tags[0],'python','-c','from pathlib import Path;Path("/built-message").write_text("edited")',check=False) assert failed.returncode!=0 and 'Read-only file system' in failed.stderr print('a writable container can drift while its image stays unchanged; a read-only root rejects that particular write') finally: docker('rm','-f',container,check=False);docker('image','rm',*tags,check=False) 两次构建使用相同基础引用和应用源码,选择的构建差异只有 MESSAGE;Dockerfile 把它写入 /built-message。第一份镜像输出 one default,第二份输出 two default。完整镜像身份不同,但检查确认它们有序的基础文件系统层前缀完全一致。这里同时对照了公共输入和最终行为,避免把基础相同误当成全部相同。 ...

四月 30, 2027 · 2 分钟 · XB Field Notes
同一 Pod 内,普通 prepare 初始化容器重试后完成,再运行 transform,最后启动应用;三者挂载同一个 emptyDir。

普通初始化容器建立启动前置条件

普通 init container 在 Pod 的应用容器启动之前执行准备工作。它有自己的命令、镜像和文件系统视图,也可以挂载应用稍后读取的共享卷。这让前置步骤与主进程分开,但不意味着任意初始化操作都会自动安全,也不意味着重试默认存在固定次数上限。应根据真实重试方式及产物生命周期设计这个步骤,而不是把启动顺序当成完整容错机制。 原文历史样例,本次未重跑;执行仍受阻。普通初始化顺序仅限单个 Pod。显式 emptyDir 在容器重启后保留,但随 Pod 消失;不表示持久化或整组副本串行化。 顺序约束只属于一个 Pod Kubelet 按声明顺序运行普通初始化容器。每一步必须成功结束,下一步才开始,应用容器则等待整个序列完成。检查 initContainerStatuses,才能区分正在等待的步骤、运行中的步骤和已经终止的某次尝试。仅查看 Pod 的阶段,无法说明哪条命令失败,也无法说明新的尝试是否正在进行;排查需要结合具体容器状态和日志。 共享存储需要显式挂载。emptyDir 可以在初始化容器与应用容器之间传递生成文件,并在同一 Pod 内的容器重启期间保留,但会随着 Pod 消失。不同容器不会自动共享镜像根文件系统,只写在初始化容器可写层中的文件,不会自然出现在应用容器里面。产物的写入位置和读取位置必须通过真实卷定义对应起来。 ...

一月 3, 2027 · 3 分钟 · XB Field Notes