mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 20:15:19 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -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.
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
- op: add
|
||||
path: /spec/egress/-
|
||||
value:
|
||||
to:
|
||||
- ipBlock:
|
||||
cidr: 192.0.2.1/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6443
|
||||
+16
@@ -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
|
||||
+15
@@ -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
|
||||
+11
@@ -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
|
||||
+31
@@ -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
|
||||
+17
@@ -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
|
||||
+10
@@ -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
|
||||
+31
@@ -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
|
||||
+24
@@ -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
|
||||
+26
@@ -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
|
||||
+31
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user