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,98 @@
|
||||
# Isolated Prompt output lost-key recovery verifier
|
||||
|
||||
This caller-driven Job verifies one externally recovered Prompt output key in a
|
||||
separate `qinglong3-recovery` namespace. It is not part of the default Cluster
|
||||
Kustomization and does not provision, wrap, unwrap, export or reactivate key
|
||||
material.
|
||||
|
||||
The deployment authority must prepare a private read-only PVC named
|
||||
`ql3-prompt-output-external-recovery-workspace`. Copy `command.example.json` to
|
||||
`command.json` and provide exactly the referenced files:
|
||||
|
||||
- one provider-neutral atomic custody bundle containing the signed content-free
|
||||
receipt and its digest-bound wrapped blob;
|
||||
- the 32-byte material produced by the external KMS/HSM unwrap ceremony;
|
||||
- the exact durable `keyId/materialProof/catalogDigest` fact from an isolated
|
||||
PostgreSQL/CNPG restore;
|
||||
- the immutable encrypted Artifact from that same restore generation;
|
||||
- the pinned custody signing public key;
|
||||
- an unexpired recovery authorization signed by two different strong Users,
|
||||
plus both pinned approver public keys.
|
||||
|
||||
The verifier never accepts the receipt and wrapped blob as separate files, so a
|
||||
partial copy or cross-generation substitution cannot create a mixed recovery
|
||||
input. Every workspace file must be a non-symlink, single-link, read-only
|
||||
regular file.
|
||||
Private files must not be readable by `other`; the recovered material must be
|
||||
exactly 32 bytes. Provisioning the PVC, copying backup evidence and invoking the
|
||||
KMS/HSM are deployment-authority responsibilities outside this Job.
|
||||
|
||||
The repository includes a concrete Vault Transit adapter without making Vault
|
||||
part of the QingLong runtime. Copy `vault-transit-wrap-command.example.json`
|
||||
or `vault-transit-unwrap-command.example.json` into a private authority,
|
||||
replace every placeholder, make commands/keys/token/material read-only, and run:
|
||||
|
||||
```sh
|
||||
pnpm custody:vault-transit:ql3 -- wrap --command-file /owner-private/wrap.json
|
||||
pnpm custody:vault-transit:ql3 -- unwrap --command-file /owner-private/unwrap.json
|
||||
```
|
||||
|
||||
Production mode requires HTTPS plus an explicit CA file and reads the Vault
|
||||
token only from a private file. Wrap creates one no-replace `0400`
|
||||
provider-neutral bundle; replay verifies that bundle without another Vault
|
||||
call. Unwrap verifies the signing authority, bundle digest,
|
||||
provider/key-version authority and exact material proof before creating one
|
||||
no-replace `0400` recovered material file. Copy that bundle unchanged into the
|
||||
verifier workspace as `custody-bundle.json`. The Vault token, endpoint and key
|
||||
name are never embedded in the bundle. Do not run either Vault command in the
|
||||
tokenless, deny-all verifier Job.
|
||||
|
||||
The opt-in live gate uses the exact reviewed Vault image digest and removes its
|
||||
random container and private workspace on both success and failure:
|
||||
|
||||
```sh
|
||||
QL3_RUN_VAULT_TRANSIT_LIVE=true pnpm test:vault-transit-custody-live:ql3
|
||||
```
|
||||
|
||||
That gate proves the actual Vault 1.21.4 Transit API over TLS 1.3 with an
|
||||
explicit private CA. It initializes a persistent file barrier with three seal
|
||||
shares and a threshold of two, wraps the key, replaces the entire non-root
|
||||
read-only-rootfs container, observes the persisted server sealed, re-unseals
|
||||
it, verifies the Transit key survived and then unwraps the bundle. A different
|
||||
CA is rejected before API access. This remains a single-host file-storage
|
||||
fixture, not HA integrated storage, HSM auto-unseal, enterprise PKI/external
|
||||
IdP or CNPG restore evidence.
|
||||
|
||||
The separate opt-in PostgreSQL composition gate proves that the exact durable
|
||||
key fact and encrypted Artifact can cross a real logical backup boundary before
|
||||
the offline verifier receives them:
|
||||
|
||||
```sh
|
||||
QL3_RUN_POSTGRES_BACKUP_RECOVERY_LIVE=true \
|
||||
pnpm test:postgres-backup-prompt-output-recovery-live:ql3
|
||||
```
|
||||
|
||||
It uses the digest-pinned PostgreSQL 18 image on a random loopback-only port,
|
||||
runs the complete QL3 core and AI migration streams, and persists a canonical
|
||||
Package publication plus Prompt admission/start/completion/finalization,
|
||||
encrypted Artifact and key-rotation record through production repositories. It
|
||||
then creates a custom-format backup, removes the whole source container and
|
||||
anonymous volume, restores into a different container/volume, reopens the
|
||||
production Artifact and rotation repositories, and only then exports the
|
||||
restored fact and Artifact to the existing two-User verifier. The restored
|
||||
lineage includes the exact 52 core and 16 AI migration histories and one row for
|
||||
each required publication and Prompt chain fact. The gate removes both random
|
||||
containers, their anonymous volumes and its private directory on success or
|
||||
failure. This is full production-schema logical PostgreSQL backup composition
|
||||
evidence; it is not CloudNativePG Barman WAL/PITR or an external IdP ceremony.
|
||||
|
||||
The Pod has no Role or RoleBinding, disables ServiceAccount token projection,
|
||||
mounts the PVC read-only and is selected by an ingress/egress deny-all
|
||||
NetworkPolicy. It receives no PostgreSQL URL, Kubernetes credential, cloud
|
||||
credential, KMS endpoint or HSM session. Successful stdout is a content-free,
|
||||
authorization-bound recovery proof. Failure output contains only stable error
|
||||
metadata. The CLI wipes its owned recovered and wrapped buffers before exit.
|
||||
|
||||
This verifies recoverability only. It does not authorize plaintext export,
|
||||
production Secret mutation, retirement reversal, keyring reconstruction or
|
||||
bulk re-encryption. Those operations require separate policy and evidence.
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ql3-prompt-output-external-recovery-verifier
|
||||
namespace: qinglong3-recovery
|
||||
labels:
|
||||
app.kubernetes.io/name: ql3-prompt-output-external-recovery-verifier
|
||||
app.kubernetes.io/component: ai-recovery-verifier
|
||||
app.kubernetes.io/part-of: qinglong3
|
||||
qinglong.io/execution-model: caller-driven
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
activeDeadlineSeconds: 120
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: ql3-prompt-output-external-recovery-verifier
|
||||
app.kubernetes.io/component: ai-recovery-verifier
|
||||
app.kubernetes.io/part-of: qinglong3
|
||||
qinglong.io/execution-model: caller-driven
|
||||
spec:
|
||||
serviceAccountName: ql3-prompt-output-external-recovery-verifier
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
restartPolicy: Never
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
fsGroup: 10001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: verifier
|
||||
image: qinglong3-cluster-admin:3.0.0-alpha.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- node
|
||||
- /opt/qinglong/node_modules/@qinglong/cluster-admin/dist/prompt-output/external-recovery/promptOutputExternalRecoveryCli.js
|
||||
args:
|
||||
- run
|
||||
- --command-file
|
||||
- /var/run/qinglong3/prompt-output-external-recovery/command.json
|
||||
terminationMessagePolicy: File
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
volumeMounts:
|
||||
- name: recovery-workspace
|
||||
mountPath: /var/run/qinglong3/prompt-output-external-recovery
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: recovery-workspace
|
||||
persistentVolumeClaim:
|
||||
claimName: ql3-prompt-output-external-recovery-workspace
|
||||
readOnly: true
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- service-account.yaml
|
||||
- network-policy.yaml
|
||||
- job.yaml
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: qinglong3-recovery
|
||||
labels:
|
||||
app.kubernetes.io/part-of: qinglong3
|
||||
qinglong.io/security-domain: isolated-recovery
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ql3-prompt-output-external-recovery-verifier-deny-all
|
||||
namespace: qinglong3-recovery
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: ql3-prompt-output-external-recovery-verifier
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress: []
|
||||
egress: []
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: ql3-prompt-output-external-recovery-verifier
|
||||
namespace: qinglong3-recovery
|
||||
labels:
|
||||
app.kubernetes.io/name: ql3-prompt-output-external-recovery-verifier
|
||||
app.kubernetes.io/part-of: qinglong3
|
||||
automountServiceAccountToken: false
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "cluster.prompt-output-key.verify-recovery",
|
||||
"authorizationFile": "/var/run/qinglong3/prompt-output-external-recovery/authorization.json",
|
||||
"custodyBundleFile": "/var/run/qinglong3/prompt-output-external-recovery/custody-bundle.json",
|
||||
"recoveredMaterialFile": "/var/run/qinglong3/prompt-output-external-recovery/recovered-material.bin",
|
||||
"durableKeyFactFile": "/var/run/qinglong3/prompt-output-external-recovery/durable-key-fact.json",
|
||||
"artifactFile": "/var/run/qinglong3/prompt-output-external-recovery/artifact.json",
|
||||
"custodyPublicKeyFile": "/var/run/qinglong3/prompt-output-external-recovery/custody-public-key.pem",
|
||||
"approverPublicKeyFiles": [
|
||||
{
|
||||
"userId": "replace-with-first-reviewer-user-id",
|
||||
"filePath": "/var/run/qinglong3/prompt-output-external-recovery/first-reviewer-public-key.pem"
|
||||
},
|
||||
{
|
||||
"userId": "replace-with-second-reviewer-user-id",
|
||||
"filePath": "/var/run/qinglong3/prompt-output-external-recovery/second-reviewer-public-key.pem"
|
||||
}
|
||||
]
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "external.prompt-output-key.vault-transit.unwrap",
|
||||
"vault": {
|
||||
"transport": "https",
|
||||
"endpoint": "https://vault.example.invalid:8200",
|
||||
"caFile": "/owner-private/vault/ca.pem",
|
||||
"tokenFile": "/owner-private/vault/unwrap-token",
|
||||
"transitMount": "prompt-output-transit",
|
||||
"keyName": "prompt-output-custody"
|
||||
},
|
||||
"bundleFile": "/owner-private/prompt-output/custody-bundle.json",
|
||||
"custodyPublicKeyFile": "/owner-private/prompt-output/custody-signing-public.pem",
|
||||
"recoveredMaterialOutputFile": "/owner-private/prompt-output/recovered-material.bin"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "external.prompt-output-key.vault-transit.wrap",
|
||||
"vault": {
|
||||
"transport": "https",
|
||||
"endpoint": "https://vault.example.invalid:8200",
|
||||
"caFile": "/owner-private/vault/ca.pem",
|
||||
"tokenFile": "/owner-private/vault/wrap-token",
|
||||
"transitMount": "prompt-output-transit",
|
||||
"keyName": "prompt-output-custody"
|
||||
},
|
||||
"materialFile": "/owner-private/prompt-output/material.bin",
|
||||
"bundleOutputFile": "/owner-private/prompt-output/custody-bundle.json",
|
||||
"custody": {
|
||||
"custodyId": "replace-custody-id",
|
||||
"keyId": "replace-key-id",
|
||||
"sourceGeneration": 1,
|
||||
"sourceCatalogDigest": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"receiptSigningPrivateKeyFile": "/owner-private/prompt-output/custody-signing-private.pem",
|
||||
"receiptSigningPublicKeyFile": "/owner-private/prompt-output/custody-signing-public.pem"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user