feat(ql3): distribute copilot console via signed admin image

This commit is contained in:
whyour
2026-08-16 05:14:17 +08:00
parent c4a1238a92
commit fba8dfb602
22 changed files with 1176 additions and 29 deletions
+70 -6
View File
@@ -1,20 +1,50 @@
# Cluster Copilot read-only Console
This Console is an operator-workstation process, not a resident QingLong
service. It serves digest-bound assets on an ephemeral `127.0.0.1` port and
service. Native execution serves digest-bound assets on an ephemeral
`127.0.0.1` port and
forwards only `inspect` and explicit `output` reads to the existing Cluster
Copilot API. Do not deploy it as a Kubernetes workload, Ingress, shared LAN
listener, Edge component or legacy 2.x Web route.
Use `ql3-cluster-admin` from the same independently verified Admin release as
the Cluster deployment. The Console intentionally runs directly on the trusted
operator workstation. A container port mapping is not a supported substitute:
the process binds container loopback and must not be widened to `0.0.0.0`.
the Cluster deployment. D-328 also supports the image-carried
`docker-loopback.sh`: it uses an explicit container-only listener but publishes
the same port exclusively on host `127.0.0.1`. Arbitrary `0.0.0.0`, host
networking and LAN publication remain forbidden.
## Verify the distribution
The multi-architecture `qinglong3-cluster-admin@sha256:…` OCI image is the
distribution artifact. It already carries the exact launcher, examples and
this document under `/opt/qinglong/share/ql3-copilot-console/`; there is no
second Node archive or package dependency graph to trust.
From the exact reviewed source tag, run `verify-release.sh` with the immutable
image digest, repository, 40-hex source revision and full tag ref. The verifier
requires `cosign` and authenticated `gh`, then independently checks the keyless
release-workflow identity, SLSA provenance, CycloneDX SBOM and digest-bound OS
vulnerability evidence. It rejects tags and mutable image references.
```sh
deploy/console/ql3-cluster-copilot/verify-release.sh \
ghcr.io/replace-owner/qinglong3-cluster-admin@sha256:REPLACE_64_HEX \
replace-owner/qinglong \
REPLACE_40_HEX_SOURCE_REVISION \
refs/tags/v3.0.0-alpha.0
```
After verification, pull that exact digest. The signature covers the embedded
host launcher and templates as part of the image filesystem. Operators may
either use the launcher from the matching reviewed tag or extract its exact
image-carried copy with `docker create` plus `docker cp` before execution.
## Prepare private authority
Create an absolute canonical directory owned by the current operator with mode
`0700`. Copy `client-config.example.json` to `client.json`, install the reviewed
Create an absolute canonical directory with mode `0700`. For native execution
it is owned by the current operator; for the image-carried launcher it and all
files are owned by UID/GID `10001:10001`. Copy `client-config.example.json` to
`client.json`, install the reviewed
Cluster API CA as `ca.pem`, and install a separately issued `ql3c_` Project API
credential as `credential`. Give the credential only `run.read` and
`artifact.read`; the Console has no route for diagnosis creation or
@@ -72,3 +102,37 @@ approximately 2 MiB, disables cache/cookies/frames/workers, and never polls.
Model text is rendered as plain text and remains untrusted advice. These limits
keep the workstation surface bounded, but this Cluster-only product is still
excluded from small router Edge/Standalone artifacts.
## Run the verified image
Create a dedicated Docker network whose egress is restricted by the host
firewall to DNS and the exact Cluster API destination. Copy
`host-environment.example.json` values into the launcher environment, replacing
the image with the verified digest and selecting one unused host port. The
launcher rejects `bridge|default|host|none`, mutable tags, noncanonical private
roots, ports outside `1024..65535` and unknown resource classes.
| Resource class | Memory | CPU | PIDs | Console reads |
| --- | ---: | ---: | ---: | ---: |
| `compact` | 192 MiB | 0.25 | 32 | 2, no queue |
| `standard` | 512 MiB | 1 | 64 | 2, no queue |
Validate private authority and the upstream unauthenticated TLS 1.3 readiness
route without opening or publishing a listener:
```sh
deploy/console/ql3-cluster-copilot/docker-loopback.sh check
```
Then start the foreground session:
```sh
deploy/console/ql3-cluster-copilot/docker-loopback.sh serve
```
The launcher fixes non-root UID, read-only root, no capabilities,
no-new-privileges, bounded memory/CPU/PIDs, an 8 MiB noexec tmpfs, one read-only
private mount and `--pull never`. `serve` alone adds
`--publish 127.0.0.1:<port>:<port>/tcp`; `check` publishes nothing. The
container listener is reachable only through this reviewed publication and
continues to require the 256-bit browser session token plus exact Host/Origin.
+88
View File
@@ -0,0 +1,88 @@
#!/bin/sh
set -eu
usage() {
printf '%s\n' 'Usage: docker-loopback.sh check|serve' >&2
exit 64
}
fail() {
printf '%s\n' '{"schemaVersion":1,"component":"qinglong3-cluster-copilot-console-launcher","event":"launch_failed"}' >&2
exit 78
}
[ "$#" -eq 1 ] || usage
mode=$1
case "$mode" in
check|serve) ;;
*) usage ;;
esac
image=${QL3_COPILOT_CONSOLE_IMAGE-}
private_root=${QL3_COPILOT_CONSOLE_PRIVATE_ROOT-}
network=${QL3_COPILOT_CONSOLE_NETWORK-}
port=${QL3_COPILOT_CONSOLE_PORT-}
resource_class=${QL3_COPILOT_CONSOLE_RESOURCE_CLASS-compact}
printf '%s' "$image" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._/-]{0,191}@sha256:[0-9a-f]{64}$' || fail
printf '%s' "$network" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$' || fail
case "$network" in
bridge|default|host|none) fail ;;
esac
printf '%s' "$port" | grep -Eq '^[1-9][0-9]{3,4}$' || fail
[ "$port" -ge 1024 ] 2>/dev/null || fail
[ "$port" -le 65535 ] 2>/dev/null || fail
case "$private_root" in
/*) ;;
*) fail ;;
esac
case "$private_root" in
*','*|*':'*) fail ;;
esac
[ -d "$private_root" ] || fail
canonical_root=$(CDPATH= cd -- "$private_root" 2>/dev/null && pwd -P) || fail
[ "$canonical_root" = "$private_root" ] || fail
case "$resource_class" in
compact)
memory=192m
cpus=0.25
pids=32
;;
standard)
memory=512m
cpus=1
pids=64
;;
*) fail ;;
esac
set -- docker run --rm --pull never --init --read-only \
--network "$network" \
--cap-drop ALL \
--security-opt no-new-privileges \
--user 10001:10001 \
--pids-limit "$pids" \
--memory "$memory" \
--cpus "$cpus" \
--stop-timeout 3 \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=8m,mode=700,uid=10001,gid=10001 \
--mount "type=bind,src=$private_root,dst=/var/run/secrets/qinglong3/copilot-console,readonly"
if [ "$mode" = serve ]; then
set -- "$@" --publish "127.0.0.1:$port:$port/tcp"
fi
set -- "$@" "$image" copilot-console \
--container-published-loopback \
"--port=$port" \
--config /var/run/secrets/qinglong3/copilot-console/client.json \
--credential /var/run/secrets/qinglong3/copilot-console/credential \
--session /var/run/secrets/qinglong3/copilot-console/session
if [ "$mode" = check ]; then
set -- "$@" --check
fi
exec "$@"
@@ -0,0 +1,7 @@
{
"QL3_COPILOT_CONSOLE_IMAGE": "ghcr.io/replace-owner/qinglong3-cluster-admin@sha256:0000000000000000000000000000000000000000000000000000000000000000",
"QL3_COPILOT_CONSOLE_PRIVATE_ROOT": "/absolute/private/ql3-copilot-console",
"QL3_COPILOT_CONSOLE_NETWORK": "qinglong3-copilot-console-egress",
"QL3_COPILOT_CONSOLE_PORT": "5701",
"QL3_COPILOT_CONSOLE_RESOURCE_CLASS": "compact"
}
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
set -eu
usage() {
printf '%s\n' 'Usage: verify-release.sh IMAGE@sha256:DIGEST OWNER/REPO SOURCE_REVISION refs/tags/v3.VERSION' >&2
exit 64
}
fail() {
printf '%s\n' '{"schemaVersion":1,"component":"qinglong3-cluster-admin-release-verifier","event":"verification_failed"}' >&2
exit 78
}
[ "$#" -eq 4 ] || usage
image=$1
repository=$2
source_revision=$3
source_ref=$4
printf '%s' "$repository" | grep -Eq '^[a-z0-9][a-z0-9-]{0,38}/[A-Za-z0-9_.-]{1,100}$' || fail
owner=${repository%%/*}
printf '%s' "$image" | grep -Eq "^ghcr.io/$owner/qinglong3-cluster-admin@sha256:[0-9a-f]{64}$" || fail
printf '%s' "$source_revision" | grep -Eq '^[0-9a-f]{40}$' || fail
printf '%s' "$source_ref" | grep -Eq '^refs/tags/v3\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$' || fail
command -v cosign >/dev/null 2>&1 || fail
command -v gh >/dev/null 2>&1 || fail
workflow="$repository/.github/workflows/ql3-image-release.yml"
certificate_identity="https://github.com/$workflow@$source_ref"
cosign verify \
--certificate-identity "$certificate_identity" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$image" >/dev/null || fail
verify_attestation() {
predicate_type=$1
if [ -n "$predicate_type" ]; then
set -- --predicate-type "$predicate_type"
else
set --
fi
gh attestation verify "oci://$image" \
--repo "$repository" \
--signer-workflow "$workflow" \
--source-digest "$source_revision" \
--source-ref "$source_ref" \
"$@" \
--deny-self-hosted-runners \
--bundle-from-oci >/dev/null || fail
}
verify_attestation ''
verify_attestation https://cyclonedx.org/bom
verify_attestation https://qinglong.dev/attestations/image-os-vulnerability/v1
printf '%s\n' '{"schemaVersion":1,"component":"qinglong3-cluster-admin-release-verifier","signature":true,"provenance":true,"sbom":true,"osVulnerabilityEvidence":true,"compatible":true}'
@@ -76,6 +76,17 @@ COPY --from=workspace /workspace/packages/ql3-cluster-admin/dist \
node_modules/@qinglong/cluster-admin/dist
COPY --from=workspace /workspace/packages/ql3-cluster-admin/assets/copilot-console \
node_modules/@qinglong/cluster-admin/assets/copilot-console
RUN mkdir -p share/ql3-copilot-console
COPY --chmod=0555 deploy/console/ql3-cluster-copilot/docker-loopback.sh \
share/ql3-copilot-console/docker-loopback.sh
COPY --chmod=0555 deploy/console/ql3-cluster-copilot/verify-release.sh \
share/ql3-copilot-console/verify-release.sh
COPY --chmod=0444 deploy/console/ql3-cluster-copilot/README.md \
share/ql3-copilot-console/README.md
COPY --chmod=0444 deploy/console/ql3-cluster-copilot/client-config.example.json \
share/ql3-copilot-console/client-config.example.json
COPY --chmod=0444 deploy/console/ql3-cluster-copilot/host-environment.example.json \
share/ql3-copilot-console/host-environment.example.json
FROM node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d AS runtime