mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): distribute copilot console via signed admin image
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user