feat(ql3): add direct Vault KV worker secret custody

This commit is contained in:
whyour
2026-08-25 00:16:59 +08:00
parent a64d08bda1
commit 686873f8ec
25 changed files with 2830 additions and 37 deletions
+16
View File
@@ -1032,6 +1032,22 @@ Kubernetes API permission. Version-pinned SecretRefs use a different hash and
remain immutable by policy; retain their files until referenced Run/Artifact
retention has completed.
For deployments that require direct external custody, use the optional
`vault-kv-worker-secret` overlay instead of projecting value files:
```bash
kubectl kustomize \
deploy/kubernetes/ql3-cluster/vault-kv-worker-secret >/dev/null
```
That overlay mounts only a private Vault CA and a short-lived, orphan,
non-renewable read token. The adapter revalidates the token and fetches exact
digest-derived KV v2 paths over TLS 1.3 for every authorized delivery; it has no
cache, watcher, renewal loop or fallback to `mounted-files`. Follow the
overlay's README for the exact policy, payload and token-rotation contract.
External Vault HA, seal custody, audit devices, backup and disaster recovery
remain deployment-specific gates.
Create a distinct migration Secret:
```yaml
@@ -0,0 +1,50 @@
# Vault KV v2 Worker Secret custody
This overlay replaces the base `mounted-files` value projection with direct,
TLS 1.3 Vault KV v2 resolution. The control Pods mount only the private Vault
CA and one short-lived read token; Secret values and opaque Legacy Env bundles
remain in Vault and are fetched only after the durable Worker delivery authority
has been validated.
The exact Vault policy is read-only:
```hcl
path "worker-secrets/data/values/production/*" {
capabilities = ["read"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
```
Issue an orphan, non-renewable service token with only that policy and a TTL no
greater than 15 minutes. Rotate the projected `token` atomically before expiry;
the adapter rereads and revalidates it for every authorized resolution. A Vault
Agent or external Secret operator may own this projection, but must not project
the actual Worker Secret values.
Store each value at
`worker-secrets/data/values/production/<sha256(canonical SecretRef)>` with this
exact KV payload:
```json
{
"schemaVersion": 1,
"secretRefDigest": "<same 64-hex path key>",
"encoding": "base64",
"value": "<canonical base64 bytes>"
}
```
Create the two private Kubernetes Secret objects from
`credentials.example.yaml` through the deployment authority, then render:
```sh
kubectl kustomize deploy/kubernetes/ql3-cluster/vault-kv-worker-secret
```
The overlay intentionally does not create a NetworkPolicy because the base
control-plane deployment has no universal egress policy and external Vault
topologies differ. If the namespace is default-deny, explicitly allow DNS,
PostgreSQL, the artifact store and TCP 8200 only to the reviewed Vault identity.
@@ -0,0 +1,20 @@
# Create these objects through the deployment's Secret authority; do not commit
# real CA or token material. The Vault token must be an orphan, non-renewable
# service token with only ql3-worker-secret-read and at most a 15-minute TTL.
apiVersion: v1
kind: Secret
metadata:
name: ql3-cluster-worker-vault-trust
namespace: qinglong3-system
type: Opaque
stringData:
ca.pem: REPLACE_WITH_PRIVATE_VAULT_CA_PEM
---
apiVersion: v1
kind: Secret
metadata:
name: ql3-cluster-worker-vault-auth
namespace: qinglong3-system
type: Opaque
stringData:
token: REPLACE_WITH_SHORT_LIVED_ORPHAN_VAULT_TOKEN
@@ -0,0 +1,58 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ql3-cluster-control
spec:
template:
spec:
containers:
- name: cluster-control
env:
- name: QL3_WORKER_SECRET_PROVIDER
value: vault-kv-v2
- name: QL3_WORKER_SECRET_ROOT_DIRECTORY
$patch: delete
- name: QL3_WORKER_SECRET_VAULT_ENDPOINT
value: https://vault.vault.svc.cluster.local:8200
- name: QL3_WORKER_SECRET_VAULT_CA_FILE
value: /var/run/secrets/qinglong3/worker-vault-trust/ca.pem
- name: QL3_WORKER_SECRET_VAULT_TOKEN_FILE
value: /var/run/secrets/qinglong3/worker-vault-auth/token
- name: QL3_WORKER_SECRET_VAULT_KV_MOUNT
value: worker-secrets
- name: QL3_WORKER_SECRET_VAULT_PATH_PREFIX
value: values/production
- name: QL3_WORKER_SECRET_VAULT_EXPECTED_POLICY
value: ql3-worker-secret-read
- name: QL3_WORKER_SECRET_VAULT_MAX_TOKEN_TTL_SECONDS
value: '900'
- name: QL3_WORKER_SECRET_VAULT_REQUEST_TIMEOUT_MS
value: '5000'
- name: QL3_WORKER_SECRET_VAULT_MAX_CONCURRENCY
value: '4'
volumeMounts:
- name: worker-secret-values
$patch: delete
- name: worker-vault-trust
mountPath: /var/run/secrets/qinglong3/worker-vault-trust
readOnly: true
- name: worker-vault-auth
mountPath: /var/run/secrets/qinglong3/worker-vault-auth
readOnly: true
volumes:
- name: worker-secret-values
$patch: delete
- name: worker-vault-trust
secret:
secretName: ql3-cluster-worker-vault-trust
defaultMode: 292
items:
- key: ca.pem
path: ca.pem
- name: worker-vault-auth
secret:
secretName: ql3-cluster-worker-vault-auth
defaultMode: 288
items:
- key: token
path: token
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: deployment-patch.yaml