feat(ql3): harden copilot mcp host deployment

This commit is contained in:
whyour
2026-08-16 03:41:38 +08:00
parent 58025ede55
commit da4e7cf688
25 changed files with 734 additions and 48 deletions
+67
View File
@@ -0,0 +1,67 @@
# Cluster Copilot MCP stdio container
This is an explicit host-side deployment for the bounded Cluster Copilot MCP.
It is not a Kubernetes Deployment or Service: stdio must be owned by the MCP
host that launches the process. A resident Pod without that parent session
would be unreachable while still retaining a credential and image attack
surface.
The launcher uses the existing independently released Cluster Admin image and
its default `ql3-cluster-admin` entrypoint. It selects the reviewed
`copilot-mcp` subcommand, never overrides the entrypoint and never mounts a
Kubernetes token, database credential, Docker socket or writable directory.
## Prepare the private projection
Create an absolute canonical directory owned by UID/GID `10001:10001`, mode
`0700`. Copy `mcp-config.example.json` to `mcp.json` and
`client-config.example.json` to `client.json`; install the reviewed API CA as
`ca.pem` and the separately issued `ql3c_` Project API credential as
`credential`. All four files must be regular, non-symlink, UID 10001-owned,
canonical files with mode `0600`. Never put the credential value in the MCP
host config, argv, environment or image.
Replace the endpoint, DNS server name and CA. The client uses TLS 1.3, does not
use a client certificate, proxy, redirect or ambient CA, and rereads
`credential` for every Tool call.
## Select resources and egress
Create a dedicated Docker network whose host firewall permits only DNS and the
reviewed Cluster API destination. The launcher rejects `bridge`, `default`,
`host`, `none` and an implicit network, but Docker network naming alone is not
an egress allowlist.
| Resource class | Memory | CPU | PIDs | Maximum configured concurrency |
| --- | ---: | ---: | ---: | ---: |
| `compact` | 192 MiB | 0.25 | 32 | 1 |
| `standard` | 512 MiB | 1 | 64 | 4 |
| `dense` | 1 GiB | 2 | 96 | 16 |
The launcher passes a second concurrency ceiling to the process. Startup and
preflight fail closed if `mcp.json` requests more concurrency than its resource
class. There is no queue, retry, poller, watcher or resident health timer.
Export only the immutable image digest, private directory path, dedicated
network name and resource class, then validate before registering the host:
```sh
deploy/mcp/ql3-cluster-copilot/docker-stdio.sh check
```
The check validates all mounted path/credential authority and makes one
unauthenticated `GET /readyz`. Its JSON contains no endpoint, path, credential
or cluster identity. A not-ready response exits 69; invalid local authority or
transport failure emits only a low-sensitive failure fact.
Copy `mcp-host.example.json` into the external MCP host's private
configuration, replace its launcher path, image digest, private root and
network, and map its `command`/`args`/`env` fields to the host's equivalent
stdio process adapter. `serve` runs Docker attached to stdin/stdout with a
read-only root filesystem, no capabilities, no-new-privileges, a fixed
non-root UID, bounded memory/CPU/PIDs and `--pull never`.
Do not compose this directory into Edge/Standalone, `cluster-control`, the
Cluster AI Pod or any shared Kubernetes operations Kustomization. Small router
profiles continue to use the separately bounded Local MCP artifact only when
explicitly selected; otherwise they carry no MCP dependency at all.
@@ -0,0 +1,7 @@
{
"schema": "qinglong/cluster-copilot-client-config@v1",
"endpoint": "https://replace-cluster-api.example.com:5800/",
"servername": "replace-cluster-api.example.com",
"caFile": "/var/run/secrets/qinglong3/copilot-mcp/ca.pem",
"requestTimeoutMs": 30000
}
+83
View File
@@ -0,0 +1,83 @@
#!/bin/sh
set -eu
usage() {
printf '%s\n' 'Usage: docker-stdio.sh check|serve' >&2
exit 64
}
fail() {
printf '%s\n' '{"schemaVersion":1,"component":"qinglong3-cluster-copilot-mcp-launcher","event":"launch_failed"}' >&2
exit 78
}
[ "$#" -eq 1 ] || usage
mode=$1
case "$mode" in
check|serve) ;;
*) usage ;;
esac
image=${QL3_COPILOT_MCP_IMAGE-}
private_root=${QL3_COPILOT_MCP_PRIVATE_ROOT-}
network=${QL3_COPILOT_MCP_NETWORK-}
resource_class=${QL3_COPILOT_MCP_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
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
concurrency_ceiling=1
;;
standard)
memory=512m
cpus=1
pids=64
concurrency_ceiling=4
;;
dense)
memory=1g
cpus=2
pids=96
concurrency_ceiling=16
;;
*) fail ;;
esac
set -- docker run --rm -i --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" \
--mount "type=bind,src=$private_root,dst=/var/run/secrets/qinglong3/copilot-mcp,readonly" \
"$image" \
copilot-mcp
if [ "$mode" = check ]; then
exec "$@" --check --config /var/run/secrets/qinglong3/copilot-mcp/mcp.json \
"--concurrency-ceiling=$concurrency_ceiling"
fi
exec "$@" --config /var/run/secrets/qinglong3/copilot-mcp/mcp.json \
"--concurrency-ceiling=$concurrency_ceiling"
@@ -0,0 +1,6 @@
{
"schema": "qinglong/cluster-copilot-mcp-server@v1",
"clientConfigFile": "/var/run/secrets/qinglong3/copilot-mcp/client.json",
"credentialFile": "/var/run/secrets/qinglong3/copilot-mcp/credential",
"maxConcurrentRequests": 1
}
@@ -0,0 +1,14 @@
{
"mcpServers": {
"qinglong-cluster-copilot": {
"command": "/absolute/path/to/docker-stdio.sh",
"args": ["serve"],
"env": {
"QL3_COPILOT_MCP_IMAGE": "ghcr.io/replace-owner/qinglong3-cluster-admin@sha256:0000000000000000000000000000000000000000000000000000000000000000",
"QL3_COPILOT_MCP_PRIVATE_ROOT": "/absolute/private/qinglong3-cluster-copilot",
"QL3_COPILOT_MCP_NETWORK": "qinglong3-copilot-egress",
"QL3_COPILOT_MCP_RESOURCE_CLASS": "compact"
}
}
}
}