mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
feat(ql3): prove security administration custody live
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
name: QingLong 3.0 Security Administration Kubernetes live evidence
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ql3-security-administration-live-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-administration-live:
|
||||||
|
name: Three-node Security Administration, PostgreSQL and PVC custody
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 90
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: '8.3.1'
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24.18.0'
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Require enough ephemeral disk for three isolated K3s nodes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
available_kib="$(df -Pk / | awk 'NR == 2 { print $4 }')"
|
||||||
|
minimum_kib="$((25 * 1024 * 1024))"
|
||||||
|
if (( available_kib < minimum_kib )); then
|
||||||
|
echo "Security Administration live gate requires at least 25 GiB free; found ${available_kib} KiB" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker volume ls --quiet --filter dangling=true | sort > \
|
||||||
|
"${RUNNER_TEMP}/ql3-dangling-volumes.before"
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
- name: Install workspace dependencies without lifecycle scripts
|
||||||
|
run: pnpm install --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
- name: Install verified kubectl v1.34.3
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl --fail --location --silent --show-error \
|
||||||
|
--output "${RUNNER_TEMP}/kubectl" \
|
||||||
|
https://dl.k8s.io/release/v1.34.3/bin/linux/amd64/kubectl
|
||||||
|
curl --fail --location --silent --show-error \
|
||||||
|
--output "${RUNNER_TEMP}/kubectl.sha256" \
|
||||||
|
https://dl.k8s.io/release/v1.34.3/bin/linux/amd64/kubectl.sha256
|
||||||
|
test "$(cat "${RUNNER_TEMP}/kubectl.sha256")" = \
|
||||||
|
"$(sha256sum "${RUNNER_TEMP}/kubectl" | cut -d ' ' -f 1)"
|
||||||
|
chmod 0755 "${RUNNER_TEMP}/kubectl"
|
||||||
|
|
||||||
|
- name: Fetch checksum-locked CloudNativePG operator manifest
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl --fail --location --silent --show-error \
|
||||||
|
--output "${RUNNER_TEMP}/cloudnative-pg.yaml" \
|
||||||
|
https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.30.0/cnpg-1.30.0.yaml
|
||||||
|
echo "f8bede43fe4ee0d478c2355b204a36876b2ae4faac60f2a9452280b293da3b88 ${RUNNER_TEMP}/cloudnative-pg.yaml" | sha256sum --check
|
||||||
|
|
||||||
|
- name: Recheck static Security Administration and evidence contracts
|
||||||
|
run: |
|
||||||
|
pnpm audit:security-administration-kubernetes:ql3
|
||||||
|
node --test \
|
||||||
|
test/back/ql3SecurityAdministrationKubernetesLiveContract.test.cjs \
|
||||||
|
test/back/ql3SecurityAdministrationKubernetesLiveAudit.test.cjs
|
||||||
|
|
||||||
|
- name: Preload the digest-bound K3s distribution
|
||||||
|
run: docker pull rancher/k3s:v1.34.3-k3s1
|
||||||
|
|
||||||
|
- name: Prove Security Administration through K3s, PostgreSQL and PVC
|
||||||
|
env:
|
||||||
|
QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE: '1'
|
||||||
|
QL3_KUBECTL_BIN: ${{ runner.temp }}/kubectl
|
||||||
|
QL3_CNPG_OPERATOR_MANIFEST_FILE: ${{ runner.temp }}/cloudnative-pg.yaml
|
||||||
|
QL3_SECURITY_ADMINISTRATION_REPORT: ${{ runner.temp }}/ql3-security-administration/report.json
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
mkdir -m 0700 "$(dirname "${QL3_SECURITY_ADMINISTRATION_REPORT}")"
|
||||||
|
pnpm test:security-administration-kubernetes-live:ql3 \
|
||||||
|
"--report=${QL3_SECURITY_ADMINISTRATION_REPORT}"
|
||||||
|
|
||||||
|
- name: Re-audit content-free report and isolated cleanup
|
||||||
|
env:
|
||||||
|
QL3_SECURITY_ADMINISTRATION_REPORT: ${{ runner.temp }}/ql3-security-administration/report.json
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test "$(stat -c '%a' "${QL3_SECURITY_ADMINISTRATION_REPORT}")" = '600'
|
||||||
|
pnpm audit:security-administration-kubernetes-live:ql3 \
|
||||||
|
"--report=${QL3_SECURITY_ADMINISTRATION_REPORT}"
|
||||||
|
sha256sum "${QL3_SECURITY_ADMINISTRATION_REPORT}"
|
||||||
|
test -z "$(docker ps -aq --filter name=ql3-security-live-)"
|
||||||
|
test -z "$(docker network ls -q --filter name=ql3-security-live-)"
|
||||||
|
docker volume ls --quiet --filter dangling=true | sort > \
|
||||||
|
"${RUNNER_TEMP}/ql3-dangling-volumes.after"
|
||||||
|
diff --unified \
|
||||||
|
"${RUNNER_TEMP}/ql3-dangling-volumes.before" \
|
||||||
|
"${RUNNER_TEMP}/ql3-dangling-volumes.after"
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
- name: Upload audited content-free Security Administration evidence
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ql3-security-administration-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
path: ${{ runner.temp }}/ql3-security-administration/report.json
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
compression-level: 9
|
||||||
|
overwrite: false
|
||||||
|
include-hidden-files: false
|
||||||
@@ -29,6 +29,7 @@ spec:
|
|||||||
runAsUser: 10001
|
runAsUser: 10001
|
||||||
runAsGroup: 10001
|
runAsGroup: 10001
|
||||||
fsGroup: 10001
|
fsGroup: 10001
|
||||||
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
seccompProfile:
|
seccompProfile:
|
||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
initContainers:
|
initContainers:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
最新增量证据(2026-08-25):
|
最新增量证据(2026-08-25):
|
||||||
|
|
||||||
- D-406/ADR-0501(进行中:静态部署契约已验收,live gate pending):Cluster Security Administration 现在有可选的一次性 Kubernetes Job,而不是要求每个部署者自行拼装。通用 PostgreSQL、CloudNativePG、credential delivery 与组合入口均显式 opt-in,不进入共享 operations;因此 Edge/Standalone 以及默认 Cluster 的 package、依赖、启动路径和常驻资源仍为零增量。Job 固定 non-root/read-only/drop-all、无 ServiceAccount token/RBAC、`backoffLimit=0`、300 秒 deadline、600 秒 TTL、每容器 25m/48 MiB request 和 250m/128 MiB limit;主命令仍只打开一个 admin PostgreSQL connection。新增 stager 保留 kubelet Secret symlink 兼容性,同时以 realpath confinement、`O_NOFOLLOW`、读前/读后 inode 复验和独立大小上限把四个输入复制成 memory-backed `0700/0600` 私有边界,失败时主容器不可启动。issue/rotate 才选择 PVC delivery component,token 只进入唯一 `0600` no-replace 文件。6 个 stager/CLI 测试、4 个静态审计及四种 `kubectl kustomize` 渲染均通过;18-package clean build/test 退出 0,当前 `cluster-admin` 为 `454 total / 451 pass / 3 conditional skip / 0 fail`,backend 为 `1579 total / 1577 pass / 2 conditional skip / 0 fail`,122-module Edge import、Cluster dependency、package boundary、deployment、deployment-lock 与 release-version 审计全部 compatible。本地 Admin 镜像 `sha256:5464f0bbf5aa1302c080b13c9f18aaa89ba418ab5d09a917f5b5b4937c0ede2f` 在 non-root/read-only、无网络/能力、128 MiB/0.25 CPU 下包含并运行固定 stager 入口。真实 K3s + PostgreSQL register/query/issue/replay/revoke、PVC custody、response-loss 与清理证据尚未执行,所以 D-406 不冒充完整生产 ceremony,也不关闭双人复核/break-glass、pepper rotation、audit retention/export/alert 或远程 UI/API 门禁。
|
- D-406/ADR-0501(已验收):Cluster Security Administration 现在有可选的一次性 Kubernetes Job,而不是要求每个部署者自行拼装。通用 PostgreSQL、CloudNativePG、credential delivery 与组合入口均显式 opt-in,不进入共享 operations;因此 Edge/Standalone 以及默认 Cluster 的 package、依赖、启动路径和常驻资源仍为零增量。Job 固定 non-root/read-only/drop-all、无 ServiceAccount token/RBAC、`backoffLimit=0`、300 秒 deadline、600 秒 TTL、每容器 25m/48 MiB request 和 250m/128 MiB limit;主命令仍只打开一个 admin PostgreSQL connection。stager 保留 kubelet Secret symlink 兼容性,同时以 realpath confinement、`O_NOFOLLOW`、读前/读后 inode 复验和独立大小上限把四个输入复制成 memory-backed `0700/0600` 私有边界,失败时主容器不可启动;issue/rotate 才选择 PVC delivery component,token 只进入唯一 `0600` no-replace 文件。真实 arm64 live gate 已在 1 control-plane + 2 worker 的 K3s `v1.34.3+k3s1`、Flannel、CloudNativePG 1.30.0、3 个 PostgreSQL 18.4 实例、migration 71/control-core capability 70 上完成 register、audit query、issue、response-loss exact replay、rotate、revoke;证明 kubelet `0440` immutable Secret 投影可被私有化、`0444` 扩权会在主容器启动前失败、RWO PVC 跨 Job 保留 `0700/0600` no-replace 交付、Kubernetes API/公网 egress 与 Secret/Job RBAC 均被拒绝。全部管理 Job 继续以 UID/GID 10001 运行;仅 live local-path fixture 用一个无网络、无 API token、drop-all 的 root Job 把实现特有的 `02777 root:10001` PVC 根收紧为 `02770`,不能据此声称生产 CSI 加密或 custody 已验收。content-free `0600` 报告独立审计为 `compatible=true/findings=[]`,SHA-256 为 `e5c24af77034e1a2efee062107176e218c11a9f9f0d6c8c7308fdc280b0a82cf`;18-package clean build/test 退出 0,当前 `cluster-admin` 为 `456 total / 453 pass / 3 conditional skip / 0 fail`,backend 为 `1590 total / 1588 pass / 2 conditional skip / 0 fail`。D-406 关闭单主机 K3s/CNPG/PVC 的产品命令与权限边界门,但不冒充生产 Kubernetes control-plane HA、跨主机 STONITH/DR、加密 CSI、外部 IdP、双人复核/break-glass、pepper rotation、audit retention/export/alert、并发 dispatch 或远程 UI/API。
|
||||||
|
|
||||||
- D-405/ADR-0500(已验收):Cluster Identity/API Credential/Security Audit 现在有首个受审产品入口。`ql3-security-admin` 由既有 `ql3-cluster-admin security` facade 到达同一安装内的固定 target,不新建单文件 package,也不进入常驻 `cluster-control`;每次只执行一个 exact-shape 的 identity register/enable/disable、credential issue/rotate/revoke 或最多 200 条的 keyset audit query,随后关闭单连接 admin PostgreSQL authority。命令使用独立 `ql3-security-administration+jwt` type、`security-administration` purpose、audience 与 generation/revocation ledger,拒绝其他管理面的 assertion;command、assertion、keyset 与 pepper 均来自显式有界文件,不发现 home、ambient Kubernetes context 或默认 credential。issue/rotate token 只经私有目录内 `0600`、`fsync`、hard-link no-replace 文件交付,stdout 仅含 basename 与 SHA-256;精确重放的 `token=null` 不重新生成或发布 secret。默认 PostgreSQL `verify-full`,Pool 最大一个连接,无 listener、daemon、timer、watcher 或后台 retention。`cluster-admin` 完整回归为 `448 total / 445 pass / 3 conditional skip / 0 fail`,新增聚焦测试 19/19、产品 facade 4/4、backend product contract 4/4;18-package clean build/test 退出 0,backend 为 `1575 total / 1573 pass / 2 conditional skip / 0 fail`。真实 arm64 Admin 镜像在 non-root、read-only、无网络/能力、128 MiB/0.25 CPU 下通过 12-command live contract;PostgreSQL 18.6 arm64 physical HA timeline 1→2 的 147 gates 全部通过,报告 SHA-256 为 `8fbb606773080dae15de5e31db5726abb8700862b51e4616b5f3f50e0b8374f3`。package、Cluster dependency、122-module Edge import、部署、镜像与 service bridge 审计全部 compatible,Edge/Standalone package、依赖和常驻资源不变。本切片关闭 ADR-0050“只有 application service、没有受审产品入口”的缺口,但不冒充远程 HTTP/API/UI、双人复核/break-glass、pepper rotation、audit retention/export/alert 或完整 Kubernetes Job ceremony。
|
- D-405/ADR-0500(已验收):Cluster Identity/API Credential/Security Audit 现在有首个受审产品入口。`ql3-security-admin` 由既有 `ql3-cluster-admin security` facade 到达同一安装内的固定 target,不新建单文件 package,也不进入常驻 `cluster-control`;每次只执行一个 exact-shape 的 identity register/enable/disable、credential issue/rotate/revoke 或最多 200 条的 keyset audit query,随后关闭单连接 admin PostgreSQL authority。命令使用独立 `ql3-security-administration+jwt` type、`security-administration` purpose、audience 与 generation/revocation ledger,拒绝其他管理面的 assertion;command、assertion、keyset 与 pepper 均来自显式有界文件,不发现 home、ambient Kubernetes context 或默认 credential。issue/rotate token 只经私有目录内 `0600`、`fsync`、hard-link no-replace 文件交付,stdout 仅含 basename 与 SHA-256;精确重放的 `token=null` 不重新生成或发布 secret。默认 PostgreSQL `verify-full`,Pool 最大一个连接,无 listener、daemon、timer、watcher 或后台 retention。`cluster-admin` 完整回归为 `448 total / 445 pass / 3 conditional skip / 0 fail`,新增聚焦测试 19/19、产品 facade 4/4、backend product contract 4/4;18-package clean build/test 退出 0,backend 为 `1575 total / 1573 pass / 2 conditional skip / 0 fail`。真实 arm64 Admin 镜像在 non-root、read-only、无网络/能力、128 MiB/0.25 CPU 下通过 12-command live contract;PostgreSQL 18.6 arm64 physical HA timeline 1→2 的 147 gates 全部通过,报告 SHA-256 为 `8fbb606773080dae15de5e31db5726abb8700862b51e4616b5f3f50e0b8374f3`。package、Cluster dependency、122-module Edge import、部署、镜像与 service bridge 审计全部 compatible,Edge/Standalone package、依赖和常驻资源不变。本切片关闭 ADR-0050“只有 application service、没有受审产品入口”的缺口,但不冒充远程 HTTP/API/UI、双人复核/break-glass、pepper rotation、audit retention/export/alert 或完整 Kubernetes Job ceremony。
|
||||||
|
|
||||||
@@ -10078,7 +10078,7 @@ PR-8 的本机最新增量由 ADR-0075/0076/0077/0078/0079/0080/0081/0082/0083/0
|
|||||||
|
|
||||||
ADR-0087 Owner package 更新同样适用于上段 PR-8 累计描述:现行产品 CLI 只能经 console facade 到达其内部 bootstrap/credential-recovery,三个历史 ceremony package 名都只表示旧切片;`ql3-owner-gc` 由 maintenance 直接提供,不再拥有独立 importer。
|
ADR-0087 Owner package 更新同样适用于上段 PR-8 累计描述:现行产品 CLI 只能经 console facade 到达其内部 bootstrap/credential-recovery,三个历史 ceremony package 名都只表示旧切片;`ql3-owner-gc` 由 maintenance 直接提供,不再拥有独立 importer。
|
||||||
|
|
||||||
PR-8 的 cluster ADR-0049 未完成项由 ADR-0050/0051/0500/0501 继续收敛:Identity register/enable/disable、credential issue/rotate/revoke、mutation ledger、强 actor、同事务 audit、有界 audit query,以及常驻 `/api/v3` 的认证前 overload shield 已孵化完成;D-405 新增默认无 listener、一次只执行一个命令并关闭单连接 admin authority 的 `ql3-security-admin` 产品 CLI,credential token 只向私有 no-replace 文件交付。D-406 又补齐默认不安装、无 Kubernetes API authority 的一次性 Job、私密输入 stager、CloudNativePG 与 PVC delivery 静态契约;其真实 K3s + PostgreSQL/PVC live ceremony 仍是当前 Gate。远程 API/UI、管理入口独立 rate limit、双人复核或 break-glass、pepper rotation、audit retention/export/alert 也仍未完成;不得把短生命周期 CLI、静态 Job、application service 或 process-local HTTP shield 的存在解释为 cluster-control 已获得管理 authority 或全局 quota。
|
PR-8 的 cluster ADR-0049 未完成项由 ADR-0050/0051/0500/0501 继续收敛:Identity register/enable/disable、credential issue/rotate/revoke、mutation ledger、强 actor、同事务 audit、有界 audit query,以及常驻 `/api/v3` 的认证前 overload shield 已孵化完成;D-405 新增默认无 listener、一次只执行一个命令并关闭单连接 admin authority 的 `ql3-security-admin` 产品 CLI,credential token 只向私有 no-replace 文件交付。D-406 又补齐默认不安装、无 Kubernetes API authority 的一次性 Job、私密输入 stager、CloudNativePG 与 PVC delivery 契约,并已由真实三节点 K3s、三实例 PostgreSQL 18.4、response-loss replay、PVC custody、网络/RBAC 拒绝与失败清理 live ceremony 验收。该单 Docker host fixture 仍不证明生产 control-plane HA、跨主机 STONITH/DR 或加密 CSI;远程 API/UI、管理入口独立 rate limit、双人复核或 break-glass、pepper rotation、audit retention/export/alert 也仍未完成。不得把短生命周期 CLI、一次性 Job、application service 或 process-local HTTP shield 的存在解释为 cluster-control 已获得管理 authority 或全局 quota。
|
||||||
|
|
||||||
未进入当前孵化切片的代码在通过对应 Gate 前必须保持不可达:不得仅因 schema、service 或 Primary 编排器已存在,就让旧 Controller、Scheduler、gRPC callback 或 Shell 脚本直接写入新状态表或调用新 Executor。已接入的 Shadow 观察只能通过默认关闭的 Feature Flag 和 origin owner 决策到达,不得调用 Executor、再次 spawn 或改变 Legacy 返回结果。manual `runSingle` 只增加 owner selection seam;默认没有 router。HTTP bootstrap 每次启动只读取一次 manifest,缺失、禁用、拒绝或非 primary 时保持 Legacy 且不加载重组件;显式 accepted manual primary 会在恢复门禁通过后安装唯一 owner,选中后禁止回退双跑。ADR-0445 已让 `ScheduleService.runTask` 的 subscription/system/script 在显式 origin flag 下只观察同一个 Legacy ChildProcess;system crond 的 `scheduled_system`、once/boot/grpc 与这些来源的 Primary owner 切换仍须独立门禁。
|
未进入当前孵化切片的代码在通过对应 Gate 前必须保持不可达:不得仅因 schema、service 或 Primary 编排器已存在,就让旧 Controller、Scheduler、gRPC callback 或 Shell 脚本直接写入新状态表或调用新 Executor。已接入的 Shadow 观察只能通过默认关闭的 Feature Flag 和 origin owner 决策到达,不得调用 Executor、再次 spawn 或改变 Legacy 返回结果。manual `runSingle` 只增加 owner selection seam;默认没有 router。HTTP bootstrap 每次启动只读取一次 manifest,缺失、禁用、拒绝或非 primary 时保持 Legacy 且不加载重组件;显式 accepted manual primary 会在恢复门禁通过后安装唯一 owner,选中后禁止回退双跑。ADR-0445 已让 `ScheduleService.runTask` 的 subscription/system/script 在显式 origin flag 下只观察同一个 Legacy ChildProcess;system crond 的 `scheduled_system`、once/boot/grpc 与这些来源的 Primary owner 切换仍须独立门禁。
|
||||||
|
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ Identity 变更、revoke 和 audit query 使用无 delivery 的 base。只有 `c
|
|||||||
- stager 聚焦测试覆盖真实 kubelet symlink 布局、`0700/0600` 收紧、持久 delivery 目录复验、realpath 逃逸、world-readable material、目标不可覆盖和 CLI 无敏感回显。
|
- stager 聚焦测试覆盖真实 kubelet symlink 布局、`0700/0600` 收紧、持久 delivery 目录复验、realpath 逃逸、world-readable material、目标不可覆盖和 CLI 无敏感回显。
|
||||||
- 部署审计冻结无 API token/RBAC、caller-driven/零重试/deadline/TTL、non-root/read-only/drop-all、资源上限、固定 CLI、内存私有输入、独立 admin credential、CloudNativePG egress、PVC delivery 和默认聚合不可达;失败注入覆盖权限扩大、非持久 delivery 与误入共享 aggregate。
|
- 部署审计冻结无 API token/RBAC、caller-driven/零重试/deadline/TTL、non-root/read-only/drop-all、资源上限、固定 CLI、内存私有输入、独立 admin credential、CloudNativePG egress、PVC delivery 和默认聚合不可达;失败注入覆盖权限扩大、非持久 delivery 与误入共享 aggregate。
|
||||||
- `kubectl kustomize` 已分别渲染 base、CloudNativePG、credential-delivery 和 CloudNativePG + delivery 四个入口。
|
- `kubectl kustomize` 已分别渲染 base、CloudNativePG、credential-delivery 和 CloudNativePG + delivery 四个入口。
|
||||||
- 18-package clean build/test 退出 0;当前 `cluster-admin` 为 454 total / 451 pass / 3 conditional skip / 0 fail,backend 为 1579 total / 1577 pass / 2 conditional skip / 0 fail。Edge import 仍为 122 modules,Cluster dependency、package boundary、deployment、deployment-lock source surface 与 release-version 审计均 compatible。
|
- 18-package clean build/test 退出 0;当前 `cluster-admin` 为 456 total / 453 pass / 3 conditional skip / 0 fail,backend 为 1590 total / 1588 pass / 2 conditional skip / 0 fail。
|
||||||
- 本地构建的 Cluster Admin 镜像 digest 为 `sha256:5464f0bbf5aa1302c080b13c9f18aaa89ba418ab5d09a917f5b5b4937c0ede2f`;新 stager 在 non-root、read-only rootfs、`network=none`、drop-all、no-new-privileges、32 PID、128 MiB 与 0.25 CPU 下完成独立 `--help` smoke,证明发布镜像包含该固定入口。
|
- opt-in live gate 在本机 arm64 建立 1 control-plane + 2 worker 的 K3s `v1.34.3+k3s1`/Flannel、CloudNativePG 1.30.0 和 3 个 PostgreSQL 18.4 实例;migration 71 与 control-core capability 70 通过后,6 个串行产品 Job 完成 register、audit query、issue、exact replay、rotate 与 revoke,另一个 `0444` 输入 Job 按预期在 init 阶段失败且主容器未启动。
|
||||||
- 真实 K3s Pod、PostgreSQL admin operation、PVC token custody、response loss 与清理演练尚未执行,因此不得把本 ADR 解释为 live ceremony 已验收。
|
- live gate 证明 immutable Secret 的真实 kubelet Atomic Writer 投影、memory-backed 私有 stage、PVC 跨 Job persistence/no-replace replay、不同 rotation material、TLS 与单连接最小权限;同时实测拒绝 Kubernetes API/公网 egress、Secret read/Job mutation RBAC,并在 finally 删除 Job、Secret、证据 Job、fixture root Job、PVC、K3s 容器、网络与卷。
|
||||||
|
- 所有 Security Administration Job 继续以 UID/GID 10001 运行并使用 `fsGroupChangePolicy=OnRootMismatch`,避免后续只读证据 Pod 递归改写 `0700/0600` custody。K3s local-path 实现把新 PVC 根暴露为 `02777 root:10001`,live gate 因此先用一个专用、无网络、无 API token、drop-all 的 root fixture Job 精确收紧为 `02770`;该 fixture 例外不是产品管理 authority,也不证明生产 CSI 加密或权限模型。
|
||||||
|
- content-free 报告权限为 `0600`,独立审计结果为 `compatible=true/findings=[]`,SHA-256 为 `e5c24af77034e1a2efee062107176e218c11a9f9f0d6c8c7308fdc280b0a82cf`。可手动触发的专用 CI workflow 固定 K3s/CNPG/PostgreSQL 供应链并重复同一 ceremony。
|
||||||
|
|
||||||
## 影响与剩余门禁
|
## 影响与剩余门禁
|
||||||
|
|
||||||
D-406 关闭“每个部署者都要从零编写一次性 Admin Job”的静态部署缺口,且 Edge/Standalone 和默认 Cluster 常驻资源保持不变。下一门是以临时 K3s + PostgreSQL 执行 register/query/issue/replay/revoke、证明 token 仅存在于 PVC no-replace 文件、Pod 无 API authority、失败清理和证据 content-free;之后仍有双人复核/break-glass、pepper rotation、audit retention/export/alert、并发 dispatch 和远程管理 UI/API。
|
D-406 已关闭“每个部署者都要从零编写一次性 Admin Job”及其单主机 K3s/CNPG/PVC live 缺口,且 Edge/Standalone 和默认 Cluster 常驻资源保持不变。该 gate 不代表生产 Kubernetes control-plane HA、跨主机故障隔离/STONITH、灾备恢复、加密 CSI custody 或外部 IdP 已完成;之后仍有双人复核/break-glass、pepper rotation、audit retention/export/alert、并发 dispatch 和远程管理 UI/API。
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ kubectl logs job/ql3-security-administration -n qinglong3-system \
|
|||||||
-c administrator
|
-c administrator
|
||||||
```
|
```
|
||||||
|
|
||||||
当前固定资源名只允许串行执行。收集 content-free 结果和(仅 issue/rotate)PVC 中的 `0600` delivery 文件后,删除 Job 与本次 immutable input Secret;不得重用 assertion、把 token 复制到终端输出,或以 `kubectl apply` 修改旧 Job。真实 K3s + PostgreSQL live ceremony 尚未验收,生产启用前仍需完成 ADR-0501 的 live gate。
|
当前固定资源名只允许串行执行。收集 content-free 结果和(仅 issue/rotate)PVC 中的 `0600` delivery 文件后,删除 Job 与本次 immutable input Secret;不得重用 assertion、把 token 复制到终端输出,或以 `kubectl apply` 修改旧 Job。ADR-0501 已由三节点 K3s、三实例 CloudNativePG/PostgreSQL、真实 kubelet Secret 投影和 RWO PVC 完成一次端到端 live 验收;它验证的是应用契约与权限边界,不替代生产 control-plane HA、跨主机 STONITH/DR、加密 CSI 和外部 IdP 验收。
|
||||||
|
|
||||||
## 当前边界
|
## 当前边界
|
||||||
|
|
||||||
本入口没有远程 API/UI、双人复核或 break-glass、pepper rotation、audit retention/export/alert。可选 Job 已有受审静态部署契约,但不默认安装,真实 K3s + PostgreSQL/PVC ceremony 仍待验收;admin database credential 始终不得进入常驻 Cluster Control。命令决策见 [ADR-0500](../adr/ADR-0500-short-lived-cluster-security-administration-command.md),部署决策见 [ADR-0501](../adr/ADR-0501-opt-in-kubernetes-security-administration-job.md)。
|
本入口没有远程 API/UI、双人复核或 break-glass、pepper rotation、audit retention/export/alert。可选 Job 的静态契约与单主机 K3s + PostgreSQL/PVC ceremony 已验收,但仍不默认安装,也不证明生产基础设施 HA/DR 或存储加密;admin database credential 始终不得进入常驻 Cluster Control。命令决策见 [ADR-0500](../adr/ADR-0500-short-lived-cluster-security-administration-command.md),部署决策见 [ADR-0501](../adr/ADR-0501-opt-in-kubernetes-security-administration-job.md)。
|
||||||
|
|||||||
@@ -93,6 +93,8 @@
|
|||||||
"test:postgres-backup-prompt-output-recovery-live:ql3": "pnpm --filter @qinglong/ai build && pnpm --filter @qinglong/cluster-admin build && node scripts/ql3-postgres-prompt-output-recovery-live-contract.cjs",
|
"test:postgres-backup-prompt-output-recovery-live:ql3": "pnpm --filter @qinglong/ai build && pnpm --filter @qinglong/cluster-admin build && node scripts/ql3-postgres-prompt-output-recovery-live-contract.cjs",
|
||||||
"audit:cluster-deployment:ql3": "node scripts/ql3-cluster-deployment-audit.cjs",
|
"audit:cluster-deployment:ql3": "node scripts/ql3-cluster-deployment-audit.cjs",
|
||||||
"audit:security-administration-kubernetes:ql3": "node scripts/ql3-security-administration-kubernetes-audit.cjs",
|
"audit:security-administration-kubernetes:ql3": "node scripts/ql3-security-administration-kubernetes-audit.cjs",
|
||||||
|
"test:security-administration-kubernetes-live:ql3": "node scripts/ql3-security-administration-kubernetes-live-contract.cjs",
|
||||||
|
"audit:security-administration-kubernetes-live:ql3": "node scripts/ql3-security-administration-kubernetes-live-audit.cjs",
|
||||||
"audit:cluster-copilot-console:ql3": "node scripts/ql3-cluster-copilot-console-audit.cjs",
|
"audit:cluster-copilot-console:ql3": "node scripts/ql3-cluster-copilot-console-audit.cjs",
|
||||||
"audit:cluster-copilot-console-distribution:ql3": "node scripts/ql3-cluster-copilot-console-distribution-audit.cjs",
|
"audit:cluster-copilot-console-distribution:ql3": "node scripts/ql3-cluster-copilot-console-distribution-audit.cjs",
|
||||||
"evidence:cluster-admin-release-workstation:ql3": "node scripts/ql3-cluster-admin-release-workstation-ceremony.cjs",
|
"evidence:cluster-admin-release-workstation:ql3": "node scripts/ql3-cluster-admin-release-workstation-ceremony.cjs",
|
||||||
|
|||||||
+13
-2
@@ -119,13 +119,23 @@ function sameFileState(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isClusterAdministrationProjectedSourceDirectoryAuthority(
|
||||||
|
status: Readonly<{ mode: number; uid: number }>,
|
||||||
|
): boolean {
|
||||||
|
const permissions = status.mode & 0o1777;
|
||||||
|
return (
|
||||||
|
(permissions & 0o002) === 0 ||
|
||||||
|
(status.uid === 0 && permissions === 0o1777)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function verifySourceDirectory(sourceDirectory: string): string {
|
function verifySourceDirectory(sourceDirectory: string): string {
|
||||||
const status = lstatSync(sourceDirectory, { throwIfNoEntry: false });
|
const status = lstatSync(sourceDirectory, { throwIfNoEntry: false });
|
||||||
if (
|
if (
|
||||||
status === undefined ||
|
status === undefined ||
|
||||||
!status.isDirectory() ||
|
!status.isDirectory() ||
|
||||||
status.isSymbolicLink() ||
|
status.isSymbolicLink() ||
|
||||||
(status.mode & 0o002) !== 0
|
!isClusterAdministrationProjectedSourceDirectoryAuthority(status)
|
||||||
) {
|
) {
|
||||||
throw new ClusterAdministrationKubernetesInputStageError(
|
throw new ClusterAdministrationKubernetesInputStageError(
|
||||||
'projected source directory authority is invalid',
|
'projected source directory authority is invalid',
|
||||||
@@ -245,7 +255,7 @@ function verifyWritableParent(parent: string, label: string): void {
|
|||||||
status === undefined ||
|
status === undefined ||
|
||||||
!status.isDirectory() ||
|
!status.isDirectory() ||
|
||||||
status.isSymbolicLink() ||
|
status.isSymbolicLink() ||
|
||||||
(status.mode & 0o002) !== 0
|
!isClusterAdministrationProjectedSourceDirectoryAuthority(status)
|
||||||
) {
|
) {
|
||||||
throw new ClusterAdministrationKubernetesInputStageError(
|
throw new ClusterAdministrationKubernetesInputStageError(
|
||||||
`${label} parent authority is invalid`,
|
`${label} parent authority is invalid`,
|
||||||
@@ -290,6 +300,7 @@ function createPrivateDirectory(directory: string, label: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prepareDeliveryDirectory(directory: string): void {
|
function prepareDeliveryDirectory(directory: string): void {
|
||||||
|
verifyWritableParent(dirname(directory), 'delivery directory');
|
||||||
const existing = lstatSync(directory, { throwIfNoEntry: false });
|
const existing = lstatSync(directory, { throwIfNoEntry: false });
|
||||||
if (existing === undefined) {
|
if (existing === undefined) {
|
||||||
createPrivateDirectory(directory, 'delivery directory');
|
createPrivateDirectory(directory, 'delivery directory');
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const { afterEach, test } = require('node:test');
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
ClusterAdministrationKubernetesInputStageError,
|
ClusterAdministrationKubernetesInputStageError,
|
||||||
|
isClusterAdministrationProjectedSourceDirectoryAuthority,
|
||||||
stageClusterAdministrationKubernetesInputs,
|
stageClusterAdministrationKubernetesInputs,
|
||||||
} = require('../dist/security-administration/clusterAdministrationKubernetesInputStage.js');
|
} = require('../dist/security-administration/clusterAdministrationKubernetesInputStage.js');
|
||||||
|
|
||||||
@@ -87,6 +88,44 @@ test('copies a Kubernetes projected Secret into a private immutable input bounda
|
|||||||
assert.equal(JSON.stringify(result).includes('A'.repeat(43)), false);
|
assert.equal(JSON.stringify(result).includes('A'.repeat(43)), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('accepts only the exact root-owned sticky Kubernetes mount authority', () => {
|
||||||
|
assert.equal(
|
||||||
|
isClusterAdministrationProjectedSourceDirectoryAuthority({
|
||||||
|
mode: 0o41777,
|
||||||
|
uid: 0,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isClusterAdministrationProjectedSourceDirectoryAuthority({
|
||||||
|
mode: 0o43777,
|
||||||
|
uid: 0,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isClusterAdministrationProjectedSourceDirectoryAuthority({
|
||||||
|
mode: 0o40777,
|
||||||
|
uid: 0,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isClusterAdministrationProjectedSourceDirectoryAuthority({
|
||||||
|
mode: 0o41777,
|
||||||
|
uid: 10001,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isClusterAdministrationProjectedSourceDirectoryAuthority({
|
||||||
|
mode: 0o40755,
|
||||||
|
uid: 10001,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('prepares a private persistent delivery directory without weakening it', () => {
|
test('prepares a private persistent delivery directory without weakening it', () => {
|
||||||
const fixture = projectedInput();
|
const fixture = projectedInput();
|
||||||
|
|
||||||
@@ -109,6 +148,26 @@ test('prepares a private persistent delivery directory without weakening it', ()
|
|||||||
assert.equal(mode(fixture.deliveryDirectory), 0o700);
|
assert.equal(mode(fixture.deliveryDirectory), 0o700);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects a precreated private delivery directory under an unsafe parent', () => {
|
||||||
|
const fixture = projectedInput();
|
||||||
|
const unsafeParent = join(fixture.root, 'unsafe-delivery-parent');
|
||||||
|
mkdirSync(unsafeParent, { mode: 0o777 });
|
||||||
|
chmodSync(unsafeParent, 0o777);
|
||||||
|
const deliveryDirectory = join(unsafeParent, 'private');
|
||||||
|
mkdirSync(deliveryDirectory, { mode: 0o700 });
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
stageClusterAdministrationKubernetesInputs({
|
||||||
|
sourceDirectory: fixture.sourceDirectory,
|
||||||
|
targetDirectory: fixture.targetDirectory,
|
||||||
|
deliveryDirectory,
|
||||||
|
}),
|
||||||
|
/delivery directory parent authority is invalid/,
|
||||||
|
);
|
||||||
|
assert.equal(mode(deliveryDirectory), 0o700);
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects a projected input symlink that escapes the Secret authority', () => {
|
test('rejects a projected input symlink that escapes the Secret authority', () => {
|
||||||
const fixture = projectedInput();
|
const fixture = projectedInput();
|
||||||
const external = join(fixture.root, 'external-command.json');
|
const external = join(fixture.root, 'external-command.json');
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ function auditSecurityAdministrationKubernetes(options = {}) {
|
|||||||
pod?.securityContext?.runAsUser !== 10001 ||
|
pod?.securityContext?.runAsUser !== 10001 ||
|
||||||
pod?.securityContext?.runAsGroup !== 10001 ||
|
pod?.securityContext?.runAsGroup !== 10001 ||
|
||||||
pod?.securityContext?.fsGroup !== 10001 ||
|
pod?.securityContext?.fsGroup !== 10001 ||
|
||||||
|
pod?.securityContext?.fsGroupChangePolicy !== 'OnRootMismatch' ||
|
||||||
pod?.securityContext?.seccompProfile?.type !== 'RuntimeDefault'
|
pod?.securityContext?.seccompProfile?.type !== 'RuntimeDefault'
|
||||||
) {
|
) {
|
||||||
findings.push(
|
findings.push(
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const FIXTURE =
|
||||||
|
'qinglong/security-administration-kubernetes-live-contract@v1';
|
||||||
|
const LIMITATIONS = Object.freeze([
|
||||||
|
'three privileged K3s Docker nodes are not production infrastructure or control-plane HA evidence',
|
||||||
|
'the strong-User assertion is issued by a local deterministic ceremony rather than a production external IdP',
|
||||||
|
'CloudNativePG inside one Docker host is not infrastructure STONITH or disaster-recovery evidence',
|
||||||
|
'the local-path ReadWriteOnce volume is not encrypted production CSI custody evidence',
|
||||||
|
'a dedicated root storage-fixture Job constrains the local-path volume root before every non-root administration Job',
|
||||||
|
]);
|
||||||
|
const BANNED_KEYS = new Set([
|
||||||
|
'assertion',
|
||||||
|
'authorization',
|
||||||
|
'bearer',
|
||||||
|
'connectionstring',
|
||||||
|
'dsn',
|
||||||
|
'keyset',
|
||||||
|
'kubeconfig',
|
||||||
|
'password',
|
||||||
|
'pepper',
|
||||||
|
'privatekey',
|
||||||
|
'secret',
|
||||||
|
'tlskey',
|
||||||
|
'token',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function finding(code, detail) {
|
||||||
|
return Object.freeze({ code, detail });
|
||||||
|
}
|
||||||
|
|
||||||
|
function exactKeys(value, expected) {
|
||||||
|
return (
|
||||||
|
value !== null &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
JSON.stringify(Object.keys(value).sort()) ===
|
||||||
|
JSON.stringify([...expected].sort())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function allTrue(value, keys) {
|
||||||
|
return exactKeys(value, keys) && keys.every((key) => value[key] === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSha256(value) {
|
||||||
|
return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIsoTime(value) {
|
||||||
|
return (
|
||||||
|
typeof value === 'string' &&
|
||||||
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(
|
||||||
|
value,
|
||||||
|
) &&
|
||||||
|
Number.isFinite(Date.parse(value))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsSensitiveMaterial(value, key = '') {
|
||||||
|
if (BANNED_KEYS.has(key.toLowerCase())) return true;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return (
|
||||||
|
/postgres(?:ql)?:\/\/[^/\s]+:[^@\s]+@/i.test(value) ||
|
||||||
|
/\bql3c_[A-Za-z0-9_-]{16,}\b/.test(value) ||
|
||||||
|
/\beyJ[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/.test(
|
||||||
|
value,
|
||||||
|
) ||
|
||||||
|
/-----BEGIN (?:CERTIFICATE|(?:RSA |EC |OPENSSH )?PRIVATE KEY)-----/.test(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.some((entry) => containsSensitiveMaterial(entry));
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.entries(value).some(([childKey, child]) =>
|
||||||
|
containsSensitiveMaterial(child, childKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validKubernetesVersion(value) {
|
||||||
|
const match =
|
||||||
|
typeof value === 'string'
|
||||||
|
? /^v1\.([0-9]{2,3})\.([0-9]+)(?:[-+][0-9A-Za-z](?:[0-9A-Za-z.-]{0,62}[0-9A-Za-z])?)?$/.exec(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return Boolean(match && Number(match[1]) >= 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSecurityAdministrationKubernetesLiveReport(report) {
|
||||||
|
const findings = [];
|
||||||
|
if (
|
||||||
|
!exactKeys(report, [
|
||||||
|
'schemaVersion',
|
||||||
|
'fixture',
|
||||||
|
'observedAt',
|
||||||
|
'platform',
|
||||||
|
'database',
|
||||||
|
'ceremony',
|
||||||
|
'inputBoundary',
|
||||||
|
'deliveryCustody',
|
||||||
|
'isolation',
|
||||||
|
'durability',
|
||||||
|
'cleanup',
|
||||||
|
'gates',
|
||||||
|
'limitations',
|
||||||
|
]) ||
|
||||||
|
report?.schemaVersion !== 1 ||
|
||||||
|
report?.fixture !== FIXTURE ||
|
||||||
|
!isIsoTime(report?.observedAt)
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_REPORT_SHAPE',
|
||||||
|
'the report must use the exact versioned live-contract envelope',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (containsSensitiveMaterial(report)) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_MATERIAL_EXPOSURE',
|
||||||
|
'the report must not contain credentials, assertions, database authority, kubeconfig or private key material',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = report?.platform;
|
||||||
|
if (
|
||||||
|
!exactKeys(platform, [
|
||||||
|
'distribution',
|
||||||
|
'kubernetesVersion',
|
||||||
|
'architecture',
|
||||||
|
'kubernetesImageId',
|
||||||
|
'administrationImageId',
|
||||||
|
'cniName',
|
||||||
|
'cniDistributionBinding',
|
||||||
|
'controlPlaneNodes',
|
||||||
|
'workerNodes',
|
||||||
|
'readyNodes',
|
||||||
|
]) ||
|
||||||
|
platform?.distribution !== 'k3s' ||
|
||||||
|
!validKubernetesVersion(platform?.kubernetesVersion) ||
|
||||||
|
!['amd64', 'arm64'].includes(platform?.architecture) ||
|
||||||
|
!isSha256(platform?.kubernetesImageId) ||
|
||||||
|
!isSha256(platform?.administrationImageId) ||
|
||||||
|
platform?.cniName !== 'flannel' ||
|
||||||
|
platform?.cniDistributionBinding !== 'rancher/k3s:v1.34.3-k3s1' ||
|
||||||
|
platform?.controlPlaneNodes !== 1 ||
|
||||||
|
platform?.workerNodes !== 2 ||
|
||||||
|
platform?.readyNodes !== 3
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_PLATFORM',
|
||||||
|
'the fixture must bind three real K3s nodes, Flannel and exact local runtime images',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = report?.database;
|
||||||
|
if (
|
||||||
|
!exactKeys(database, [
|
||||||
|
'operator',
|
||||||
|
'operatorVersion',
|
||||||
|
'postgresVersionNumber',
|
||||||
|
'postgresImageId',
|
||||||
|
'instances',
|
||||||
|
'readyInstances',
|
||||||
|
'administrationRole',
|
||||||
|
'roleConnectionLimit',
|
||||||
|
'commandConnectionLimit',
|
||||||
|
'migrationCount',
|
||||||
|
'controlCoreCapability',
|
||||||
|
'tlsVerified',
|
||||||
|
'leastPrivilege',
|
||||||
|
]) ||
|
||||||
|
database?.operator !== 'cloudnative-pg' ||
|
||||||
|
database?.operatorVersion !== '1.30.0' ||
|
||||||
|
database?.postgresVersionNumber !== 180004 ||
|
||||||
|
!isSha256(database?.postgresImageId) ||
|
||||||
|
database?.instances !== 3 ||
|
||||||
|
database?.readyInstances !== 3 ||
|
||||||
|
database?.administrationRole !== 'ql3_admin' ||
|
||||||
|
database?.roleConnectionLimit !== 4 ||
|
||||||
|
database?.commandConnectionLimit !== 1 ||
|
||||||
|
database?.migrationCount !== 71 ||
|
||||||
|
database?.controlCoreCapability !== 70 ||
|
||||||
|
database?.tlsVerified !== true ||
|
||||||
|
database?.leastPrivilege !== true
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_DATABASE',
|
||||||
|
'three TLS CloudNativePG instances must expose only the reviewed one-connection administration authority',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ceremony = report?.ceremony;
|
||||||
|
if (
|
||||||
|
!exactKeys(ceremony, [
|
||||||
|
'operations',
|
||||||
|
'completedJobs',
|
||||||
|
'failedJobs',
|
||||||
|
'callerDriven',
|
||||||
|
'backoffLimit',
|
||||||
|
'activeDeadlineSeconds',
|
||||||
|
'ttlSecondsAfterFinished',
|
||||||
|
'serviceAccount',
|
||||||
|
'serviceAccountTokenMounted',
|
||||||
|
'rbacGranted',
|
||||||
|
'responseLossReplayObserved',
|
||||||
|
'sensitiveMaterialReported',
|
||||||
|
]) ||
|
||||||
|
JSON.stringify(ceremony?.operations) !==
|
||||||
|
JSON.stringify([
|
||||||
|
'identity.register',
|
||||||
|
'audit.list',
|
||||||
|
'credential.issue',
|
||||||
|
'credential.issue.replay',
|
||||||
|
'credential.rotate',
|
||||||
|
'credential.revoke',
|
||||||
|
]) ||
|
||||||
|
ceremony?.completedJobs !== 6 ||
|
||||||
|
ceremony?.failedJobs !== 1 ||
|
||||||
|
ceremony?.callerDriven !== true ||
|
||||||
|
ceremony?.backoffLimit !== 0 ||
|
||||||
|
ceremony?.activeDeadlineSeconds !== 300 ||
|
||||||
|
ceremony?.ttlSecondsAfterFinished !== 600 ||
|
||||||
|
ceremony?.serviceAccount !== 'ql3-security-administration' ||
|
||||||
|
ceremony?.serviceAccountTokenMounted !== false ||
|
||||||
|
ceremony?.rbacGranted !== false ||
|
||||||
|
ceremony?.responseLossReplayObserved !== true ||
|
||||||
|
ceremony?.sensitiveMaterialReported !== false
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_CEREMONY',
|
||||||
|
'six serial caller-created commands plus one failed input stage must use the exact tokenless Job contract',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allTrue(report?.inputBoundary, [
|
||||||
|
'immutableSecret',
|
||||||
|
'projectedMode0440',
|
||||||
|
'memoryBackedPrivateStage',
|
||||||
|
'targetDirectoryMode0700',
|
||||||
|
'targetFilesMode0600',
|
||||||
|
'kubeletAtomicWriterProjectionAccepted',
|
||||||
|
'worldReadableProjectionRejected',
|
||||||
|
'mainContainerNotStartedAfterStageFailure',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_INPUT_BOUNDARY',
|
||||||
|
'the real kubelet projection must stage privately and reject widened source permissions before the main container starts',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delivery = report?.deliveryCustody;
|
||||||
|
if (
|
||||||
|
!exactKeys(delivery, [
|
||||||
|
'persistentVolumeClaim',
|
||||||
|
'accessMode',
|
||||||
|
'fixtureRootProvisioned',
|
||||||
|
'fixtureRootMode',
|
||||||
|
'fixtureProvisionerRanAsRoot',
|
||||||
|
'privateDirectoryMode',
|
||||||
|
'fileMode',
|
||||||
|
'fileCount',
|
||||||
|
'issueDigest',
|
||||||
|
'rotationDigest',
|
||||||
|
'distinctRotationMaterial',
|
||||||
|
'persistentAcrossJobs',
|
||||||
|
'noReplaceReplayPreserved',
|
||||||
|
'deliverySchemaValidated',
|
||||||
|
'bearerFormatValidatedInPod',
|
||||||
|
'sensitiveMaterialReported',
|
||||||
|
]) ||
|
||||||
|
delivery?.persistentVolumeClaim !== true ||
|
||||||
|
delivery?.accessMode !== 'ReadWriteOnce' ||
|
||||||
|
delivery?.fixtureRootProvisioned !== true ||
|
||||||
|
delivery?.fixtureRootMode !== '2770' ||
|
||||||
|
delivery?.fixtureProvisionerRanAsRoot !== true ||
|
||||||
|
delivery?.privateDirectoryMode !== '0700' ||
|
||||||
|
delivery?.fileMode !== '0600' ||
|
||||||
|
delivery?.fileCount !== 2 ||
|
||||||
|
!isSha256(delivery?.issueDigest) ||
|
||||||
|
!isSha256(delivery?.rotationDigest) ||
|
||||||
|
delivery?.issueDigest === delivery?.rotationDigest ||
|
||||||
|
![
|
||||||
|
'distinctRotationMaterial',
|
||||||
|
'persistentAcrossJobs',
|
||||||
|
'noReplaceReplayPreserved',
|
||||||
|
'deliverySchemaValidated',
|
||||||
|
'bearerFormatValidatedInPod',
|
||||||
|
].every((key) => delivery?.[key] === true) ||
|
||||||
|
delivery?.sensitiveMaterialReported !== false
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_DELIVERY',
|
||||||
|
'issue and rotation material must remain distinct, private, persistent and no-replace on the caller-owned RWO volume',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allTrue(report?.isolation, [
|
||||||
|
'dnsAndDatabaseEgressAllowed',
|
||||||
|
'kubernetesApiEgressDenied',
|
||||||
|
'publicInternetEgressDenied',
|
||||||
|
'secretReadRbacDenied',
|
||||||
|
'jobMutationRbacDenied',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_ISOLATION',
|
||||||
|
'Flannel and RBAC evidence must allow only DNS/PostgreSQL and deny ambient Kubernetes or public authority',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const durability = report?.durability;
|
||||||
|
if (
|
||||||
|
!exactKeys(durability, [
|
||||||
|
'identityVersion',
|
||||||
|
'identityStatus',
|
||||||
|
'credentialVersion',
|
||||||
|
'credentialState',
|
||||||
|
'identityMutationCount',
|
||||||
|
'credentialMutationCount',
|
||||||
|
'issueMutationCount',
|
||||||
|
'credentialVersionCount',
|
||||||
|
'allowedAuditCount',
|
||||||
|
]) ||
|
||||||
|
durability?.identityVersion !== 1 ||
|
||||||
|
durability?.identityStatus !== 'active' ||
|
||||||
|
durability?.credentialVersion !== 3 ||
|
||||||
|
durability?.credentialState !== 'revoked' ||
|
||||||
|
durability?.identityMutationCount !== 1 ||
|
||||||
|
durability?.credentialMutationCount !== 3 ||
|
||||||
|
durability?.issueMutationCount !== 1 ||
|
||||||
|
durability?.credentialVersionCount !== 3 ||
|
||||||
|
durability?.allowedAuditCount !== 4
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_DURABILITY',
|
||||||
|
'the database must retain one identity mutation and exactly three credential generations without replay duplication',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allTrue(report?.cleanup, [
|
||||||
|
'jobsDeleted',
|
||||||
|
'inputSecretsDeleted',
|
||||||
|
'evidenceJobsDeleted',
|
||||||
|
'storageProvisionJobDeleted',
|
||||||
|
'deliveryVolumeClaimDeleted',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_CLEANUP',
|
||||||
|
'all caller-created Jobs, inputs, evidence Pods and the fixture delivery claim must be removed',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allTrue(report?.gates, [
|
||||||
|
'realThreeNodeKubernetes',
|
||||||
|
'realCloudNativePg',
|
||||||
|
'realKubeletSecretProjection',
|
||||||
|
'realAdministrationProductCommands',
|
||||||
|
'realPersistentCredentialCustody',
|
||||||
|
'responseLossReplay',
|
||||||
|
'failedInputStageClosed',
|
||||||
|
'leastPrivilege',
|
||||||
|
'contentFreeEvidence',
|
||||||
|
'passed',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_GATES',
|
||||||
|
'every real ceremony gate must be explicitly and truthfully closed',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JSON.stringify(report?.limitations) !== JSON.stringify(LIMITATIONS)) {
|
||||||
|
findings.push(
|
||||||
|
finding(
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_LIMITATIONS',
|
||||||
|
'the report must retain the exact non-production fixture limitations',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1,
|
||||||
|
fixture: FIXTURE,
|
||||||
|
findings: Object.freeze(findings),
|
||||||
|
compatible: findings.length === 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportPath(argv) {
|
||||||
|
if (
|
||||||
|
argv.length !== 1 ||
|
||||||
|
!argv[0].startsWith('--report=') ||
|
||||||
|
!path.isAbsolute(argv[0].slice('--report='.length))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'usage: ql3-security-administration-kubernetes-live-audit --report=/absolute/report.json',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return argv[0].slice('--report='.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
try {
|
||||||
|
const report = JSON.parse(fs.readFileSync(reportPath(process.argv.slice(2))));
|
||||||
|
const result = validateSecurityAdministrationKubernetesLiveReport(report);
|
||||||
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||||
|
if (!result.compatible) process.exitCode = 1;
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(
|
||||||
|
`security administration Kubernetes live audit failed: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
FIXTURE,
|
||||||
|
LIMITATIONS,
|
||||||
|
validateSecurityAdministrationKubernetesLiveReport,
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,26 @@ test('rejects Kubernetes API token authority in the administration Job', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects recursive fsGroup rewrites of persistent credential custody', () => {
|
||||||
|
const report = auditSecurityAdministrationKubernetes({
|
||||||
|
root: ROOT,
|
||||||
|
readFile: intercept(
|
||||||
|
'deploy/kubernetes/ql3-cluster/operations/security-administration/base/job.yaml',
|
||||||
|
(value) => value.replace(
|
||||||
|
'fsGroupChangePolicy: OnRootMismatch',
|
||||||
|
'fsGroupChangePolicy: Always',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(report.compatible, false);
|
||||||
|
assert.ok(
|
||||||
|
report.findings.some(
|
||||||
|
({ code }) => code === 'QL3_SECURITY_ADMIN_KUBERNETES_JOB_BOUNDARY_INVALID',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects a non-persistent credential delivery boundary', () => {
|
test('rejects a non-persistent credential delivery boundary', () => {
|
||||||
const report = auditSecurityAdministrationKubernetes({
|
const report = auditSecurityAdministrationKubernetes({
|
||||||
root: ROOT,
|
root: ROOT,
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { test } = require('node:test');
|
||||||
|
|
||||||
|
const {
|
||||||
|
FIXTURE,
|
||||||
|
LIMITATIONS,
|
||||||
|
validateSecurityAdministrationKubernetesLiveReport,
|
||||||
|
} = require('../../scripts/ql3-security-administration-kubernetes-live-audit.cjs');
|
||||||
|
|
||||||
|
const digest = (value) => `sha256:${value.repeat(64)}`;
|
||||||
|
|
||||||
|
function report() {
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
fixture: FIXTURE,
|
||||||
|
observedAt: '2026-08-25T04:00:00.000Z',
|
||||||
|
platform: {
|
||||||
|
distribution: 'k3s',
|
||||||
|
kubernetesVersion: 'v1.34.3+k3s1',
|
||||||
|
architecture: 'amd64',
|
||||||
|
kubernetesImageId: digest('1'),
|
||||||
|
administrationImageId: digest('2'),
|
||||||
|
cniName: 'flannel',
|
||||||
|
cniDistributionBinding: 'rancher/k3s:v1.34.3-k3s1',
|
||||||
|
controlPlaneNodes: 1,
|
||||||
|
workerNodes: 2,
|
||||||
|
readyNodes: 3,
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
operator: 'cloudnative-pg',
|
||||||
|
operatorVersion: '1.30.0',
|
||||||
|
postgresVersionNumber: 180004,
|
||||||
|
postgresImageId: digest('3'),
|
||||||
|
instances: 3,
|
||||||
|
readyInstances: 3,
|
||||||
|
administrationRole: 'ql3_admin',
|
||||||
|
roleConnectionLimit: 4,
|
||||||
|
commandConnectionLimit: 1,
|
||||||
|
migrationCount: 71,
|
||||||
|
controlCoreCapability: 70,
|
||||||
|
tlsVerified: true,
|
||||||
|
leastPrivilege: true,
|
||||||
|
},
|
||||||
|
ceremony: {
|
||||||
|
operations: [
|
||||||
|
'identity.register',
|
||||||
|
'audit.list',
|
||||||
|
'credential.issue',
|
||||||
|
'credential.issue.replay',
|
||||||
|
'credential.rotate',
|
||||||
|
'credential.revoke',
|
||||||
|
],
|
||||||
|
completedJobs: 6,
|
||||||
|
failedJobs: 1,
|
||||||
|
callerDriven: true,
|
||||||
|
backoffLimit: 0,
|
||||||
|
activeDeadlineSeconds: 300,
|
||||||
|
ttlSecondsAfterFinished: 600,
|
||||||
|
serviceAccount: 'ql3-security-administration',
|
||||||
|
serviceAccountTokenMounted: false,
|
||||||
|
rbacGranted: false,
|
||||||
|
responseLossReplayObserved: true,
|
||||||
|
sensitiveMaterialReported: false,
|
||||||
|
},
|
||||||
|
inputBoundary: {
|
||||||
|
immutableSecret: true,
|
||||||
|
projectedMode0440: true,
|
||||||
|
memoryBackedPrivateStage: true,
|
||||||
|
targetDirectoryMode0700: true,
|
||||||
|
targetFilesMode0600: true,
|
||||||
|
kubeletAtomicWriterProjectionAccepted: true,
|
||||||
|
worldReadableProjectionRejected: true,
|
||||||
|
mainContainerNotStartedAfterStageFailure: true,
|
||||||
|
},
|
||||||
|
deliveryCustody: {
|
||||||
|
persistentVolumeClaim: true,
|
||||||
|
accessMode: 'ReadWriteOnce',
|
||||||
|
fixtureRootProvisioned: true,
|
||||||
|
fixtureRootMode: '2770',
|
||||||
|
fixtureProvisionerRanAsRoot: true,
|
||||||
|
privateDirectoryMode: '0700',
|
||||||
|
fileMode: '0600',
|
||||||
|
fileCount: 2,
|
||||||
|
issueDigest: digest('4'),
|
||||||
|
rotationDigest: digest('5'),
|
||||||
|
distinctRotationMaterial: true,
|
||||||
|
persistentAcrossJobs: true,
|
||||||
|
noReplaceReplayPreserved: true,
|
||||||
|
deliverySchemaValidated: true,
|
||||||
|
bearerFormatValidatedInPod: true,
|
||||||
|
sensitiveMaterialReported: false,
|
||||||
|
},
|
||||||
|
isolation: {
|
||||||
|
dnsAndDatabaseEgressAllowed: true,
|
||||||
|
kubernetesApiEgressDenied: true,
|
||||||
|
publicInternetEgressDenied: true,
|
||||||
|
secretReadRbacDenied: true,
|
||||||
|
jobMutationRbacDenied: true,
|
||||||
|
},
|
||||||
|
durability: {
|
||||||
|
identityVersion: 1,
|
||||||
|
identityStatus: 'active',
|
||||||
|
credentialVersion: 3,
|
||||||
|
credentialState: 'revoked',
|
||||||
|
identityMutationCount: 1,
|
||||||
|
credentialMutationCount: 3,
|
||||||
|
issueMutationCount: 1,
|
||||||
|
credentialVersionCount: 3,
|
||||||
|
allowedAuditCount: 4,
|
||||||
|
},
|
||||||
|
cleanup: {
|
||||||
|
jobsDeleted: true,
|
||||||
|
inputSecretsDeleted: true,
|
||||||
|
evidenceJobsDeleted: true,
|
||||||
|
storageProvisionJobDeleted: true,
|
||||||
|
deliveryVolumeClaimDeleted: true,
|
||||||
|
},
|
||||||
|
gates: {
|
||||||
|
realThreeNodeKubernetes: true,
|
||||||
|
realCloudNativePg: true,
|
||||||
|
realKubeletSecretProjection: true,
|
||||||
|
realAdministrationProductCommands: true,
|
||||||
|
realPersistentCredentialCustody: true,
|
||||||
|
responseLossReplay: true,
|
||||||
|
failedInputStageClosed: true,
|
||||||
|
leastPrivilege: true,
|
||||||
|
contentFreeEvidence: true,
|
||||||
|
passed: true,
|
||||||
|
},
|
||||||
|
limitations: [...LIMITATIONS],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('accepts the exact content-free Security Administration live report', () => {
|
||||||
|
const result = validateSecurityAdministrationKubernetesLiveReport(report());
|
||||||
|
assert.equal(result.compatible, true);
|
||||||
|
assert.deepEqual(result.findings, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects widened authority, false custody and replay duplication', () => {
|
||||||
|
const candidate = report();
|
||||||
|
candidate.ceremony.serviceAccountTokenMounted = true;
|
||||||
|
candidate.deliveryCustody.noReplaceReplayPreserved = false;
|
||||||
|
candidate.durability.issueMutationCount = 2;
|
||||||
|
candidate.gates.passed = false;
|
||||||
|
const codes = validateSecurityAdministrationKubernetesLiveReport(
|
||||||
|
candidate,
|
||||||
|
).findings.map((finding) => finding.code);
|
||||||
|
assert.ok(
|
||||||
|
codes.includes('QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_CEREMONY'),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
codes.includes('QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_DELIVERY'),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
codes.includes('QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_DURABILITY'),
|
||||||
|
);
|
||||||
|
assert.ok(codes.includes('QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_GATES'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects any credential or assertion material in evidence', () => {
|
||||||
|
const candidate = report();
|
||||||
|
candidate.deliveryCustody.proof = {
|
||||||
|
token: 'ql3c_example_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||||
|
};
|
||||||
|
const result = validateSecurityAdministrationKubernetesLiveReport(candidate);
|
||||||
|
assert.equal(result.compatible, false);
|
||||||
|
assert.ok(
|
||||||
|
result.findings.some(
|
||||||
|
(finding) =>
|
||||||
|
finding.code ===
|
||||||
|
'QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE_MATERIAL_EXPOSURE',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { test } = require('node:test');
|
||||||
|
|
||||||
|
const {
|
||||||
|
auditListCommand,
|
||||||
|
credentialIssueCommand,
|
||||||
|
credentialRevokeCommand,
|
||||||
|
credentialRotateCommand,
|
||||||
|
custodyEvidenceSource,
|
||||||
|
deliveryVolumeProvisionSource,
|
||||||
|
identity,
|
||||||
|
identityRegisterCommand,
|
||||||
|
inputAuthorityEvidenceSource,
|
||||||
|
} = require('../../scripts/ql3-security-administration-kubernetes-live-contract.cjs');
|
||||||
|
|
||||||
|
const values = Object.freeze({
|
||||||
|
subject: Object.freeze({ type: 'api_app', id: 'd406-test-client' }),
|
||||||
|
credentialId: 'd406-test-client',
|
||||||
|
identityMutationId: '10000000-0000-4000-8000-000000000001',
|
||||||
|
issueMutationId: '10000000-0000-4000-8000-000000000002',
|
||||||
|
rotateMutationId: '10000000-0000-4000-8000-000000000003',
|
||||||
|
revokeMutationId: '10000000-0000-4000-8000-000000000004',
|
||||||
|
registerRequestId: 'd406-register-test',
|
||||||
|
issueRequestId: 'd406-issue-test',
|
||||||
|
rotateRequestId: 'd406-rotate-test',
|
||||||
|
revokeRequestId: 'd406-revoke-test',
|
||||||
|
notBeforeAtMs: 2_000_000_000_000,
|
||||||
|
expiresAtMs: 2_000_003_600_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requires a private report path before any Docker or Kubernetes mutation', () => {
|
||||||
|
const script = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../scripts/ql3-security-administration-kubernetes-live-contract.cjs',
|
||||||
|
);
|
||||||
|
const result = spawnSync(process.execPath, [script], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(result.status, 1);
|
||||||
|
assert.match(result.stderr, /--report=\/absolute\/private-report\.json/);
|
||||||
|
assert.doesNotMatch(result.stderr, /Docker\/Kubernetes/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds the exact register, audit, issue, rotate and revoke commands', () => {
|
||||||
|
assert.deepEqual(identityRegisterCommand(values), {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'identity.register',
|
||||||
|
request: {
|
||||||
|
mutationId: values.identityMutationId,
|
||||||
|
requestId: values.registerRequestId,
|
||||||
|
expectedCurrentVersion: 0,
|
||||||
|
subject: values.subject,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(auditListCommand(), {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'audit.list',
|
||||||
|
request: { limit: 25, filter: { outcome: 'allowed' } },
|
||||||
|
});
|
||||||
|
assert.equal(credentialIssueCommand(values).request.expectedCurrentVersion, 0);
|
||||||
|
assert.equal(credentialRotateCommand(values).request.expectedCurrentVersion, 1);
|
||||||
|
assert.equal(credentialRevokeCommand(values).request.expectedCurrentVersion, 2);
|
||||||
|
assert.equal('notBeforeAtMs' in credentialRevokeCommand(values).request, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('binds the live assertion to the isolated Security Administration purpose', () => {
|
||||||
|
const key = identity.reviewedKey('security-administration-unit-key');
|
||||||
|
const document = identity.keyset(1, [key]);
|
||||||
|
assert.equal(document.audience, 'qinglong3-security-administration');
|
||||||
|
const assertion = identity.assertion(key, 'unit-test');
|
||||||
|
const header = JSON.parse(Buffer.from(assertion.split('.')[0], 'base64url'));
|
||||||
|
const payload = JSON.parse(Buffer.from(assertion.split('.')[1], 'base64url'));
|
||||||
|
assert.equal(header.typ, 'ql3-security-administration+jwt');
|
||||||
|
assert.equal(payload.ql3_purpose, 'security-administration');
|
||||||
|
assert.equal(payload.sub, 'security-owner');
|
||||||
|
assert.equal(payload.acr, 'urn:ql3:mfa');
|
||||||
|
assert.deepEqual(payload.amr, ['pwd', 'otp']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the in-Pod custody verifier content-free and fail-closed', () => {
|
||||||
|
const source = custodyEvidenceSource();
|
||||||
|
assert.match(source, /bytes\.fill\(0\)/);
|
||||||
|
assert.match(source, /kubernetesApiConnected/);
|
||||||
|
assert.match(source, /publicInternetConnected/);
|
||||||
|
assert.match(source, /status\.nlink!==1/);
|
||||||
|
assert.doesNotMatch(source, /value\.token[,}]/);
|
||||||
|
assert.doesNotMatch(source, /console\.log/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('inspects projected input authority without reading private material', () => {
|
||||||
|
const source = inputAuthorityEvidenceSource();
|
||||||
|
assert.match(source, /lstatSync/);
|
||||||
|
assert.match(source, /realpathSync/);
|
||||||
|
assert.match(source, /confined/);
|
||||||
|
assert.doesNotMatch(source, /readFile/);
|
||||||
|
assert.doesNotMatch(source, /createReadStream/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constrains only the exact local-path fixture root without network authority', () => {
|
||||||
|
const source = deliveryVolumeProvisionSource();
|
||||||
|
assert.match(source, /before\.mode!==['"]2777['"]/);
|
||||||
|
assert.match(source, /chmodSync\(root,0o2770\)/);
|
||||||
|
assert.match(source, /after\.mode!==['"]2770['"]/);
|
||||||
|
assert.doesNotMatch(source, /child_process/);
|
||||||
|
assert.doesNotMatch(source, /net|fetch|http/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('live runner remains opt-in, reviewed, cleanup-bound and log-free', () => {
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../scripts/ql3-security-administration-kubernetes-live-contract.cjs',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/QL3_SECURITY_ADMINISTRATION_KUBERNETES_LIVE !== '1'/,
|
||||||
|
);
|
||||||
|
assert.match(source, /reviewedOperatorManifest\(operatorManifestFile\)/);
|
||||||
|
assert.match(source, /validateSecurityAdministrationKubernetesLiveReport/);
|
||||||
|
assert.match(source, /projectedMode: 0o444/);
|
||||||
|
assert.match(source, /credential\.issue\.replay/);
|
||||||
|
assert.match(source, /FallbackToLogsOnError/);
|
||||||
|
assert.match(source, /failureMessage: 'rejected'/);
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/deploy\/kubernetes\/ql3-cluster\/base\/service-account\.yaml/,
|
||||||
|
);
|
||||||
|
assert.match(source, /persistentvolumeclaim\/\$\{DELIVERY_CLAIM\}/);
|
||||||
|
assert.match(source, /await fixture\.cleanup\(\)/);
|
||||||
|
assert.doesNotMatch(source, /kubectl\([^)]*logs/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user