文章是本站的原创长文。每篇都保持无脚本、来源可溯,并与本站记录的系统现实一致:一个小型、静态、公网服务如何被构建、加固并保持诚实。
进程的 umask 在普通模式位创建规则下,为新文件和目录移除权限位。应用先提供请求模式,掩码再限制这份请求。它既不是完整访问策略,也不会重写所有既有文件。如果父目录具有默认访问控制列表,创建计算方式还会改变,因此只检查进程掩码,可能无法准确预测最后得到的权限。排查时需要同时看到应用请求和目录环境。
坏掉的样子 没有默认 ACL 时,普通权限结果是请求模式与 umask 按位取反后的值进行按位与。例如请求 0666、掩码 0022,得到 0644。这个计算不是算术减法:相同掩码下请求 0600,仍然得到 0600。应用没有请求执行权限时,宽松掩码也不会主动添加执行位;掩码负责移除,而不是凭空授予应用未请求的普通权限。
限制严格的 umask 不会修复已经存在的全员可写文件。打开并截断该文件,会改变内容,却保留既有模式。后续 chmod 也可以不受当前掩码限制地扩大权限。因此,把 umask 当作权限永远不能超过的上界,会遗漏既有文件与之后权限修改两类情况。必须区分文件首次创建,以及此后围绕同一 inode 发生的操作。
父目录具有默认 ACL 时,Linux 从它派生新对象的访问 ACL,再按应用请求模式限制权限,不再按普通方式应用进程 umask。因此,即使创建者掩码是 0077,共享目录里仍可能生成组可访问文件。遇到与预期不同的模式时,应检查继承的 ACL,而不能只看到严格掩码就断言内核没有遵守权限设置。
普通掩码与默认 ACL 继承是不同创建分支。已有文件截断与后续 chmod 另行处理;案例属于实验预期。 快速验证 把脚本保存为 mask.py,在安装 Python 3 和 setfacl 的 Linux 环境中运行 python3 mask.py。应使用获准且没有既有默认 ACL 的临时目录基址,所在文件系统须支持默认 ACL,并启用断言。历史文章报告 Python 3.11.2、acl 2.3.1,以及先在 tmpfs 被拒绝、再在 ext4 执行的过程;但未找到原始日志,本轮没有更改存储或重跑。该次拒绝不代表所有 tmpfs 配置都不支持 ACL:
...
聚合可以用一个覆盖前缀,替代若干更具体的路由通告。只有更具体前缀确实受到抑制、且其他对等体没有继续提供它们时,下游表项才会减少。成员不必填满整个汇总范围:几个稀疏子网也能形成一个较大的覆盖块,但该通告同时会吸引发往空洞地址的流量。因此,聚合既是规模问题,也是可达性和地址授权问题。
聚合究竟减少了什么 对等体同时收到三个成员前缀和一条汇总时,仍然持有四条路由。FRR 的 aggregate-address 10.1.0.0/16 summary-only 抑制被覆盖的更具体前缀通告,并不会把它们从聚合路由器自己的 BGP 表删除。生成汇总的前提是 BGP 表中至少有一个符合条件的更具体前缀,仅在其他路由表中存在一条记录还不够。
稳定的汇总可以把部分成员变化挡在下游之外,但这个收益有前提。最后一个成员消失、汇总属性改变,或者出口策略变化,都可能继续产生更新。抑制通告不会修复客户链路,只会改变其他网络获得的故障信息。聚合者仍然需要监控内部成员,不能因为上游只看到一条稳定路由,就认为内部全部可达。
汇总覆盖的地址通常也比列出来的几个子网更多。宣告之前,应检查整个范围的使用授权以及预期的汇总起源 AS。地址对齐、数值相邻、能节省表项,都不是拥有宣告权限的证据。提供商可以在自己的地址分配范围内合法汇总客户网络;成员来自不同起源 AS,并不自动意味着这次聚合非法,关键是整体授权和出口契约。
保留清晰的起源 FRR 的 suppress-map 用路由映射选择哪些更具体前缀受到抑制。as-set 处理的是另一个维度,即怎样表示成员路径中的 AS 信息。AS_SET 是无序集合,并非只包含客户起源号码的清单。历史上的 RFC 4271 在计算 AS 路径长度时,将整个集合计作一个,不会按集合成员数量逐个增加长度。
RFC 9774 已经废弃 AS_SET 与 AS_CONFED_SET。除非运营者为了过渡显式选择其他行为,发言者不得继续通告这些集合,收到含集合的路由时应按撤回处理。应选择明确、获得授权的汇总起源,并配置相应 ROA,不能把 AS_SET 当作常规修补办法。邻居的起源过滤器也不会因为路由器生成了一条汇总,就自动接受这个范围。
坏掉的样子 在选定转发表中的普通目的地址查找,使用最长的已安装匹配前缀。因此,有效的 /24 会覆盖其上方的 /16,不论两者的 BGP 本地优先级谁更高。不过,路由首先必须通过导入策略、下一跳解析和安装过程。BGP 为同一前缀选择候选路径,与转发时在不同前缀之间做匹配,是两个不同阶段。
...
DNS over TLS 和 DNS over HTTPS 保护其端点之间加密连接上的 DNS 流量。对于典型客户端,这条连接终止于选定解析器;解析器能够读取问题与回答,它随后发出的查询还可能使用不同传输。加密这一跳,不会自动加密整个递归过程,也不会让解析器失去记录所处理名称的能力。隐私范围应按真实连接边界说明,而不是笼统称为整个 DNS 都已保密。
分别观察加密边界与认证证据。853/443 为常用协议端口;8531/8443 属于文章的回环实验。证书失败诊断不能被零退出码掩盖。本图仅依据来源制作:加密 DNS 运行审阅仍待完成,实验未重跑。 坏掉的样子 加密套接字如果没有按预期验证证书,也可能连接到错误端点。应同时验证证书信任链和客户端期望的解析器身份;握手成功本身不能证明这两项检查已启用。失败时悄悄改用其他解析器或明文路径的配置,也会改变隐私契约。需要通过真实调用方了解这些行为,不能只看到配置文件中有一个 TLS 或 HTTPS 地址,就认定所有查询都符合预期。
DoH 使用 HTTPS,但四百四十三端口不会让它在所有网络中都无法识别、无法阻止或完全等同于任意网页流量。目的地址、连接时间和其他元信息仍可观察,TLS 终止点还可以检查明文 HTTP。选择解析器和传输策略时,应考虑这些边界,不能假定 HTTPS URL 会消除所有与 DNS 活动有关的信息暴露,也不能由加密推导解析器不会保存日志。
...
IPv6 路由器在向目的地转发报文时,不会替它进行分片。如果报文大于下一条链路所能承载的大小,路由器通常丢弃它,并发送 ICMPv6 Packet Too Big 反馈。源端随后可以调整报文大小,或在适当情况下通过 IPv6 分片扩展头发送分片;这些分片在目的端重组。明确各阶段由谁负责,是判断超大报文故障的基础。
坏掉的样子 假定路由器会自动拆开过大的 IPv6 包,会掩盖与大小相关的故障。相同地址与路由上,小 echo 可以成功,大 echo 却失败。端点本地接口可能支持原始大小,中间链路却不支持。应检查整条路径约束,不能仅因接口配置了 MTU 1500,就推断该大小能够端到端通过,也不能用一次小探测成功替代完整大小检查。
Packet Too Big 反馈缺失,会使经典发现更难学到这个限制。分片被过滤,或者其中一片丢失,也可能阻止正确重组。能够生成分片扩展头,不证明沿途策略都允许相应报文。不能把源端分片当作正确路径大小与必要控制流量的万能替代,它有自己的交付条件,需要通过对应流量验证,而不是只检查发送选项已经开启。
反方向也有自己的路径估计。请求发送端学到 MTU,不会自动教会响应端怎样发送较大回复。因此双向 echo 测试需要分别了解两个方向。TCP 分段还不同于 IP 分片:TCP 发送端学到限制后,可以构造更小的段,而不必把每个 IP 包再分片。应先确定观察的是哪一层的切分,避免看到多个包就统称为分片。
路由器不会对转发的 IPv6 流量分片。源端分片、目的端重组与学习路径上限属于不同阶段。报文和开口形状为示意,未按字节比例绘制。网络示例执行仍受阻,本次未重跑。 快速验证 历史报告使用 Linux 6.12.76、Python 3.11.16、iproute2 6.1.0 与 tcpdump 4.99.3;本轮没有重跑。脚本名为 check-ipv6-fragmentation.py,只应留给另外获得授权的可丢弃 Linux 虚拟机实验。容器共享该虚拟机的内核,NET_ADMIN、SYS_ADMIN 与 proc 挂载需要比生产容器更明确的隔离边界。之后运行时的 --network none 不会阻止镜像构建下载软件包。保持断言开启,不要为通过设置而放宽宿主安全策略。新建 veth、地址与路由仅应存在于实验内部:
...
TCP keepalive 询问的是:连接安静了一段时间后,对面的 TCP 端点是否仍然响应。它不询问 HTTP handler 是否正常、数据库查询是否推进、后台工作线程是否卡住。即使应用已经停顿,内核仍然可能确认收到的数据。因此,“传输对端能够回应”和“应用给出了有用结果”必须作为两个不同的观察项。
TCP 内核确认与应用进展是不同观察项。原有 Go 实验设置空闲 30 秒、间隔 10 秒、3 次探针,随后 200 毫秒读取期限到达。对端稍后读取原始 ping 并回复 pong;客户端重设读取期限后在同一连接收到回复。该实验没有测试真实保活探针。 仅为原有实验示意,本轮配图未重跑。 传输层探针不是应用心跳 保活有助于暴露远端连接状态已经消失,或者网络路径悄悄停止传递报文的情况。实现会在满足空闲条件后发送探针,并按照未响应探针的策略最终报告失败;但单个探针丢失不等于对端死亡,不能把网络中的临时丢包直接解释成确定性故障。
探针在应用字节流以下处理,不会变成 HTTP 请求、数据库查询或 WebSocket 心跳。如果连接终止在代理上,探测对象就是代理自身的 TCP 端点,不是代理后面的源站。某些中间设备的连接跟踪可能受探针影响,但不能由此承诺刷新应用的空闲计时,更不能保证所有 NAT 映射都会一直保留。
...
别名在 Bash 读取命令时,把命令位置的一个词替换成一段 Shell 文本。alias ll='ls -l' 这种个人快捷方式很方便,但别名并不是一个可执行文件,也没有独立的位置参数作用域。非交互 Bash 默认关闭别名展开,脚本却可以明确启用它;“默认不展开”与“脚本中绝对不能使用”是两回事。理解这个区别,才能解释为什么终端里好用的简称到了自动化环境就找不到。
别名在读取输入时替换。调用者的引用可以保留,但别名没有独立参数作用域;已解析的函数可能保留旧的替换文本。此图呈现原文解释与预期行为,本轮没有重新运行示例。 替换发生在读取输入时 通常,命令位置上没有被引用的词会被检查是否需要别名展开。定义 ll 后,ll 'two words' 仍会把带空格的文本作为一个参数传给命令,不能笼统地说别名必然破坏参数引用。真正的限制是:别名没有属于自己的 $1,也不能像函数那样拥有并重新排列一组独立的参数。调用者后面追加的参数,只是继续参与替换后的命令解析。
替换文本甚至可以包含管道或多条命令,因此别名在语法上并不局限于单一程序。代价是读者更难直接看出简称背后的行为。Bash 会继续检查替换文本的第一个词,但不会再次展开当前正在展开的同名别名,所以 alias ls='ls -F' 不会无限递归。还有一个容易忽略的规则:别名值末尾如果留有空白,紧随其后的词即使位于参数位置而非命令名称位置,也会接受别名检查,不应把尾部空格当成无意义的排版。
定义本身的引号还决定另一层求值时机。单引号让美元符号保留到替换文本以后被解释;双引号却可能在定义别名时就展开变量。原本想跟随目录变量变化的快捷方式,于是可能固定住旧值。遇到这种差异时,应检查实际保存的别名定义,而不只是回看自己最初输入的那一行。
...
镜像的 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。实验关闭容器网络,等待服务就绪输出后测试停止,并尝试清理容器和镜像标签,不依赖盲目等待固定时长猜测就绪:
...
Linux 可执行文件上的 setuid 位,可以在 exec 成功时把进程的有效用户 ID 改为文件属主的 ID。它不会把进程全部凭据都替换成属主的登录身份。特权辅助程序因此能够代表另一个账户完成受控操作,但输入处理和特权持续时间都会成为安全边界的一部分。模式位本身既不能证明身份转换已经发生,也不能证明这个辅助程序安全。
文件位与进程凭据分开观察 通常的 set-user-ID 转换会改变有效 UID,再将结果复制到保存的 set-user-ID;真实 UID 保持不变。组凭据是另一套状态:setuid 不会自动装入文件属主的附加组。Linux 还存在文件系统身份和能力集合,因此真实、有效与保存 UID 是有用的观察,却不是权限的完整描述。不能看到有效身份变成某个账户,就假设已经拥有该账户登录会话的全部上下文。
应检查实际部署的文件,不要推断整个发行版都采用同一权限。用于修改密码的特权程序,在容器或定制镜像里可能有不同安装方式。属主执行位位置的小写 s 表示同时具备 setuid 和属主执行位,大写 S 则表示存在 setuid 但没有那个执行位。两种显示都不能替代属主核验、目标文件核验,以及对真实启动上下文的检查。
执行环境可以抑制转换 Linux 文档列出了 no_new_privs、nosuid 挂载和被跟踪等条件,它们可能抑制特权转换;某些文档记录的组合也可能直接让执行失败。文件能力同样受到执行约束。不能为了让演示成功,就关闭这些保护。应确定目标路径与调用进程实际适用哪些政策,如实说明限制,而不是把受保护环境当作必须修复的故障。
Linux 忽略解释器脚本上的 set-user-ID 与 set-group-ID 位。因此,把原生辅助程序改成脚本并保留模式,不会保留原先的提权行为。给通用解释器增加特权也不是安全替代方案。特权程序另外启动的可执行文件,还可能主动重置凭据,所以运行独立的 id 命令,不一定忠实显示原辅助程序内部的身份。观测点必须与要验证的进程一致。
...