feat(ql3): validate cluster credential key rotation

This commit is contained in:
whyour
2026-08-26 03:31:13 +08:00
parent b45a5e04b7
commit ac0992081e
23 changed files with 1119 additions and 167 deletions
@@ -20,6 +20,7 @@ function report() {
architecture: 'amd64',
kubernetesImageId: digest('1'),
administrationImageId: digest('2'),
controlImageId: digest('4'),
cniName: 'flannel',
cniDistributionBinding: 'rancher/k3s:v1.34.3-k3s1',
controlPlaneNodes: 1,
@@ -45,13 +46,21 @@ function report() {
operations: [
'identity.register',
'audit.list',
'credential.issue',
'credential.issue.replay',
'credential.rotate',
'credential.revoke',
'credential.issue.old',
'credential.issue.old.replay',
'credential.key-references.before-activate',
'credential.issue.new',
'credential.rotate.new',
'credential.key-references.after-activate',
'credential.revoke.old',
'credential.key-references.after-converge',
],
completedJobs: 6,
completedJobs: 10,
failedJobs: 1,
authenticationProbeJobs: 5,
controlReplicas: 2,
controlRollouts: 3,
controlReplicaAntiAffinity: true,
callerDriven: true,
backoffLimit: 0,
activeDeadlineSeconds: 300,
@@ -60,6 +69,18 @@ function report() {
serviceAccountTokenMounted: false,
rbacGranted: false,
responseLossReplayObserved: true,
overlapGenerationCount: 2,
contractedGenerationCount: 1,
activeGenerationChanged: true,
oldReferencesBeforeActivation: 1,
oldReferencesAfterActivation: 1,
oldReferencesAfterConvergence: 0,
oldAuthenticationBeforeActivation: true,
oldAuthenticationDuringOverlap: true,
newAuthenticationDuringOverlap: true,
oldAuthenticationRejectedAfterConvergence: true,
newAuthenticationAfterContraction: true,
contractedToActiveGeneration: true,
sensitiveMaterialReported: false,
},
inputBoundary: {
@@ -80,7 +101,7 @@ function report() {
fixtureProvisionerRanAsRoot: true,
privateDirectoryMode: '0700',
fileMode: '0600',
fileCount: 2,
fileCount: 3,
issueDigest: digest('4'),
rotationDigest: digest('5'),
distinctRotationMaterial: true,
@@ -100,13 +121,20 @@ function report() {
durability: {
identityVersion: 1,
identityStatus: 'active',
credentialVersion: 3,
credentialState: 'revoked',
oldCredentialVersion: 2,
oldCredentialState: 'revoked',
newCredentialVersion: 2,
newCredentialState: 'active',
identityMutationCount: 1,
credentialMutationCount: 3,
credentialMutationCount: 4,
issueMutationCount: 1,
credentialVersionCount: 3,
allowedAuditCount: 4,
credentialVersionCount: 4,
oldGenerationVersionCount: 1,
newGenerationVersionCount: 3,
latestGenerationsAreNew: true,
allowedAuditCount: 5,
authenticationDeniedAuditCount: 4,
authenticationRejectedAuditCount: 1,
},
cleanup: {
jobsDeleted: true,
@@ -114,6 +142,9 @@ function report() {
evidenceJobsDeleted: true,
storageProvisionJobDeleted: true,
deliveryVolumeClaimDeleted: true,
controlDeploymentDeleted: true,
controlServiceDeleted: true,
controlRuntimeSecretDeleted: true,
},
gates: {
realThreeNodeKubernetes: true,
@@ -121,6 +152,7 @@ function report() {
realKubeletSecretProjection: true,
realAdministrationProductCommands: true,
realPersistentCredentialCustody: true,
realClusterControlAuthenticationRotation: true,
responseLossReplay: true,
failedInputStageClosed: true,
leastPrivilege: true,
@@ -142,6 +174,7 @@ test('rejects widened authority, false custody and replay duplication', () => {
candidate.ceremony.serviceAccountTokenMounted = true;
candidate.deliveryCustody.noReplaceReplayPreserved = false;
candidate.durability.issueMutationCount = 2;
candidate.ceremony.oldReferencesAfterConvergence = 1;
candidate.gates.passed = false;
const codes = validateSecurityAdministrationKubernetesLiveReport(
candidate,
@@ -6,6 +6,8 @@ const { test } = require('node:test');
const {
auditListCommand,
clusterControlResources,
credentialAuthenticationProbeSource,
credentialIssueCommand,
credentialRevokeCommand,
credentialRotateCommand,
@@ -188,6 +190,43 @@ test('waits for per-Pod network policy before mounting private material', () =>
assert.doesNotMatch(source, /readFile|process\.env|console\.log/);
});
test('runs the credential ceremony against two real anti-affine control replicas', () => {
const [service, deployment] = clusterControlResources(
'qinglong3-cluster-control:test',
);
assert.equal(service.kind, 'Service');
assert.equal(service.spec.ports[0].port, 5800);
assert.equal(deployment.kind, 'Deployment');
assert.equal(deployment.spec.replicas, 2);
assert.equal(deployment.spec.strategy.rollingUpdate.maxUnavailable, 0);
assert.equal(
deployment.spec.template.spec.affinity.podAntiAffinity
.requiredDuringSchedulingIgnoredDuringExecution[0].topologyKey,
'kubernetes.io/hostname',
);
const environment = deployment.spec.template.spec.containers[0].env;
assert.ok(
environment.some(
(entry) =>
entry.name === 'QL3_API_CREDENTIAL_PEPPER_KEYRING_FILE' &&
entry.value.endsWith('/keyring.json'),
),
);
assert.equal(
environment.some((entry) => entry.name === 'QL3_API_CREDENTIAL_PEPPER'),
false,
);
});
test('keeps the real authentication probe content-free', () => {
const source = credentialAuthenticationProbeSource();
assert.match(source, /ql3-security-live-control/);
assert.match(source, /\/api\/v3\/projects\/prj_default\/runs\?limit=1/);
assert.match(source, /observedStatus:observed/);
assert.match(source, /bytes\?\.fill\(0\)/);
assert.doesNotMatch(source, /console\.log|process\.env/);
});
test('live runner remains opt-in, reviewed, cleanup-bound and log-free', () => {
const source = fs.readFileSync(
path.resolve(
@@ -206,7 +245,15 @@ test('live runner remains opt-in, reviewed, cleanup-bound and log-free', () => {
assert.match(source, /net\.bridge\.bridge-nf-call-iptables=1/);
assert.match(source, /wait-network-policy/);
assert.match(source, /projectedMode: 0o444/);
assert.match(source, /credential\.issue\.replay/);
assert.match(source, /credential\.issue\.old\.replay/);
assert.match(source, /ql3-security-live-auth-old-before-activate/);
assert.match(source, /ql3-security-live-auth-old-overlap/);
assert.match(source, /ql3-security-live-auth-new-overlap/);
assert.match(source, /ql3-security-live-auth-old-contracted/);
assert.match(source, /ql3-security-live-auth-new-contracted/);
assert.match(source, /expectedStatus: 401/);
assert.match(source, /expectedStatus: 403/);
assert.match(source, /controlRollouts: 3/);
assert.match(source, /FallbackToLogsOnError/);
assert.match(source, /failureMessage: 'rejected'/);
assert.match(