feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,59 @@
# Caller-driven Cluster Prompt output key rotation
This operation activates one externally staged 32-byte key while retaining all
existing keys for historical decryption. It never provisions the target Secret
and is not part of the default Cluster Kustomization.
Before creating the Job:
1. Read the current target Secret UID, active key ID, generation and catalog
digest through the reviewed deployment ceremony.
2. Ask the deployment Secret manager or KMS boundary to create the immutable
`ql3-prompt-output-key-rotation-material` Secret in `qinglong3-system`. It
must contain exactly one 32-byte `material.bin` data item. Do not commit it,
use `stringData`, or grant this Job API access to that staging Secret.
3. Copy `command.example.yaml` into private configuration and replace every
placeholder. The new key ID must be unique. Keep the fixed staged-material
path unchanged.
4. Patch `network-policy.yaml` with the real Kubernetes API server `/32` and
TCP port by using `api-server-egress-patch.example.yaml`; never widen it to
a public or cluster-wide CIDR. Replace the deny-canary placeholder only with
a separately proven reachable in-cluster canary.
5. Pin the independently verified Cluster Admin image digest, apply the private
command ConfigMap and staged Secret, then create the resources in `base`.
The process first appends a content-free PostgreSQL preparation through the
dedicated `ql3_ai_maintenance` role, then performs the target Secret CAS, and
finally appends the completion. Reusing the same command and staged material
after any process or response-loss window converges from those durable facts.
The CloudNativePG overlay binds only the writer endpoint, maintenance identity,
private CA and exact database Pod egress.
The ServiceAccount can get/update only `ql3-prompt-output-keyring` and create
SelfSubjectAccessReview requests. The Pod and ServiceAccount disable automatic
tokens; a tokenless init container proves API allow plus deny-canary blocking
before the main container receives a 600-second projected token. The material
and command are separate read-only single-file `subPath` mounts. A lost update
response is resolved by rereading the target and comparing the exact staged
material proof; blind Job retries remain disabled.
Delete the completed Job, command ConfigMap and staged material Secret after
retaining content-free evidence. First target-Secret provision, KMS wrapping
and lost-key recovery remain separate release gates.
The opt-in product evidence gate is:
```sh
QL3_PROMPT_OUTPUT_KEY_ROTATION_KUBERNETES_LIVE=1 \
QL3_CNPG_OPERATOR_MANIFEST_FILE=/owner-private/cloudnative-pg-1.30.0.yaml \
pnpm test:prompt-output-key-rotation-kubernetes-live:ql3
```
It creates a random three-node K3s/Flannel fixture, runs CloudNativePG with
three PostgreSQL instances, applies the main and AI migrations, executes the
rotation twice, and proves the same tokenless runtime Pod reloads generation
1→2 while retaining historical Artifact decrypt. It also requires one target
Secret resourceVersion change, one content-free preparation/completion pair,
exact RBAC/egress, a read-only `0440` staging projection and zero fixture/image
residue. This single-host, dynamic local-path fixture is not production CSI,
control-plane HA, infrastructure STONITH, KMS wrapping or HSM evidence.
@@ -0,0 +1,9 @@
- op: add
path: /spec/egress/-
value:
to:
- ipBlock:
cidr: 192.0.2.1/32
ports:
- protocol: TCP
port: 6443
@@ -0,0 +1,16 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ql3-prompt-output-key-rotation-access-review
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
subjects:
- kind: ServiceAccount
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: ql3-prompt-output-key-rotation-access-review
@@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ql3-prompt-output-key-rotation-access-review
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
rules:
- apiGroups:
- authorization.k8s.io
resources:
- selfsubjectaccessreviews
verbs:
- create
@@ -0,0 +1,204 @@
apiVersion: batch/v1
kind: Job
metadata:
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
qinglong.io/execution-model: caller-driven
spec:
backoffLimit: 0
activeDeadlineSeconds: 180
ttlSecondsAfterFinished: 600
template:
metadata:
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
qinglong.io/execution-model: caller-driven
spec:
serviceAccountName: ql3-prompt-output-key-rotation
automountServiceAccountToken: false
enableServiceLinks: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
initContainers:
- name: network-policy-ready
image: qinglong3-cluster-admin:3.0.0-alpha.0
imagePullPolicy: IfNotPresent
command:
- node
- -e
- |
const dns = require('node:dns').promises;
const fs = require('node:fs');
const net = require('node:net');
const apiHost = process.env.KUBERNETES_SERVICE_HOST;
const apiPort = Number(process.env.KUBERNETES_SERVICE_PORT_HTTPS);
const canaryHost = process.env.QL3_NETWORK_POLICY_DENY_CANARY_HOST;
const canaryPort = Number(process.env.QL3_NETWORK_POLICY_DENY_CANARY_PORT);
const validPort = (value) => Number.isInteger(value) && value >= 1 && value <= 65535;
const connect = (host, port, timeout) => new Promise((resolve) => {
let settled = false;
const socket = net.createConnection({ host, port });
const finish = (connected) => {
if (settled) return;
settled = true;
socket.destroy();
resolve(connected);
};
socket.setTimeout(timeout);
socket.once('connect', () => finish(true));
socket.once('timeout', () => finish(false));
socket.once('error', () => finish(false));
});
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const finish = (ready, code) => {
fs.writeFileSync('/dev/termination-log', JSON.stringify({ schemaVersion: 1, ready, code }), { encoding: 'utf8', mode: 0o600 });
process.exitCode = ready ? 0 : 1;
};
(async () => {
if (!apiHost || !validPort(apiPort) || !canaryHost || canaryHost.startsWith('replace-with-') || !validPort(canaryPort)) {
finish(false, 'CONFIG_INVALID');
return;
}
try {
await dns.lookup(canaryHost);
} catch {
finish(false, 'CANARY_UNRESOLVED');
return;
}
const deadline = Date.now() + 30000;
while (Date.now() < deadline) {
const apiAllowed = await connect(apiHost, apiPort, 500);
const canaryAllowed = await connect(canaryHost, canaryPort, 250);
if (apiAllowed && !canaryAllowed) {
finish(true, 'POLICY_READY');
return;
}
await sleep(50);
}
finish(false, 'POLICY_NOT_READY');
})().catch(() => finish(false, 'PROBE_FAILED'));
env:
- name: QL3_NETWORK_POLICY_DENY_CANARY_HOST
value: replace-with-reachable-deny-canary
- name: QL3_NETWORK_POLICY_DENY_CANARY_PORT
value: '443'
terminationMessagePolicy: File
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 5m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
containers:
- name: rotation
image: qinglong3-cluster-admin:3.0.0-alpha.0
imagePullPolicy: IfNotPresent
command:
- node
- /opt/qinglong/node_modules/@qinglong/cluster-admin/dist/prompt-output/key-management/promptOutputKeyRotationCli.js
args:
- run
- --command-file
- /var/run/qinglong3/prompt-output-key-rotation/command.json
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
env:
- name: QL3_POSTGRES_TLS_MODE
value: verify-full
- name: QL3_POSTGRES_TLS_CA_FILE
value: /var/run/secrets/qinglong3/postgres-ai-maintenance/ca.crt
- name: QL3_POSTGRES_AI_MAINTENANCE_URL
valueFrom:
secretKeyRef:
name: ql3-cluster-ai-maintenance
key: postgres-ai-maintenance-url
- name: QL3_POSTGRES_TLS_SERVERNAME
valueFrom:
secretKeyRef:
name: ql3-cluster-ai-maintenance
key: postgres-tls-servername
resources:
requests:
cpu: 25m
memory: 48Mi
limits:
cpu: 250m
memory: 128Mi
volumeMounts:
- name: command
mountPath: /var/run/qinglong3/prompt-output-key-rotation/command.json
subPath: command.json
readOnly: true
- name: staged-material
mountPath: /var/run/secrets/qinglong3/prompt-output-key-rotation/material.bin
subPath: material.bin
readOnly: true
- name: postgres-ca
mountPath: /var/run/secrets/qinglong3/postgres-ai-maintenance
readOnly: true
- name: kubernetes-api-token
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: command
configMap:
name: ql3-prompt-output-key-rotation-command
defaultMode: 292
items:
- key: command.json
path: command.json
- name: staged-material
secret:
secretName: ql3-prompt-output-key-rotation-material
defaultMode: 288
items:
- key: material.bin
path: material.bin
- name: postgres-ca
secret:
secretName: ql3-cluster-ai-maintenance
defaultMode: 292
items:
- key: postgres-ca.crt
path: ca.crt
- name: kubernetes-api-token
projected:
defaultMode: 256
sources:
- serviceAccountToken:
path: token
expirationSeconds: 600
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- downwardAPI:
items:
- path: namespace
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- service-account.yaml
- role.yaml
- role-binding.yaml
- access-review-cluster-role.yaml
- access-review-cluster-role-binding.yaml
- job.yaml
- network-policy.yaml
@@ -0,0 +1,31 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
policyTypes:
- Ingress
- Egress
ingress: []
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
@@ -0,0 +1,17 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
subjects:
- kind: ServiceAccount
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ql3-prompt-output-key-rotation
@@ -0,0 +1,19 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
rules:
- apiGroups:
- ''
resources:
- secrets
resourceNames:
- ql3-prompt-output-keyring
verbs:
- get
- update
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
automountServiceAccountToken: false
@@ -0,0 +1,31 @@
- op: replace
path: /spec/template/spec/containers/0/env
value:
- name: QL3_POSTGRES_TLS_MODE
value: verify-full
- name: QL3_POSTGRES_TLS_CA_FILE
value: /var/run/secrets/qinglong3/postgres-ai-maintenance/ca.crt
- name: QL3_POSTGRES_AI_MAINTENANCE_HOST
value: ql3-postgres-rw.qinglong3-system.svc
- name: QL3_POSTGRES_AI_MAINTENANCE_PORT
value: '5432'
- name: QL3_POSTGRES_AI_MAINTENANCE_DATABASE
value: qinglong
- name: QL3_POSTGRES_AI_MAINTENANCE_USER
valueFrom:
secretKeyRef:
name: ql3-postgres-ai-maintenance-auth
key: username
- name: QL3_POSTGRES_AI_MAINTENANCE_PASSWORD
valueFrom:
secretKeyRef:
name: ql3-postgres-ai-maintenance-auth
key: password
- name: QL3_POSTGRES_TLS_SERVERNAME
value: ql3-postgres-rw.qinglong3-system.svc
- op: replace
path: /spec/template/spec/volumes/2/secret/secretName
value: ql3-postgres-ca
- op: replace
path: /spec/template/spec/volumes/2/secret/items/0/key
value: ca.crt
@@ -0,0 +1,24 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: job-patch.yaml
target:
group: batch
version: v1
kind: Job
name: ql3-prompt-output-key-rotation
- path: network-policy-patch.yaml
target:
group: networking.k8s.io
version: v1
kind: NetworkPolicy
name: ql3-prompt-output-key-rotation
images:
- name: qinglong3-cluster-admin
newName: registry.example.com/qinglong/qinglong3-cluster-admin
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ql3-prompt-output-key-rotation
namespace: qinglong3-system
spec:
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- podSelector:
matchLabels:
cnpg.io/cluster: ql3-postgres
ports:
- protocol: TCP
port: 5432
@@ -0,0 +1,31 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ql3-prompt-output-key-rotation-command
namespace: qinglong3-system
labels:
app.kubernetes.io/name: ql3-prompt-output-key-rotation
app.kubernetes.io/component: ai-maintenance
app.kubernetes.io/part-of: qinglong3
immutable: true
data:
command.json: |
{
"schemaVersion": 1,
"operation": "cluster.prompt-output-key.rotate",
"kubernetes": {
"namespace": "qinglong3-system",
"secretName": "ql3-prompt-output-keyring",
"expectedSecretUid": "replace-with-live-secret-uid",
"dataKey": "keyring.json"
},
"stagedMaterialFile": "/var/run/secrets/qinglong3/prompt-output-key-rotation/material.bin",
"request": {
"rotationId": "replace-with-unique-rotation-id",
"requestId": "replace-with-unique-request-id",
"mutationId": "replace-with-unique-mutation-id",
"expectedActiveKeyId": "replace-with-current-active-key-id",
"expectedCatalogDigest": "replace-with-current-catalog-digest",
"newKeyId": "replace-with-unique-new-key-id"
}
}