mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-05 22:24:04 +00:00
fix: mask Docker key rotation banner
This commit is contained in:
+1
-1
@@ -131,7 +131,7 @@ docker exec ccs-cliproxy supervisorctl -c /etc/supervisord.conf restart ccs-dash
|
|||||||
|
|
||||||
On first startup, the integrated container generates per-install CLIProxy API and management secrets when the config is missing custom values. If you have already configured `cliproxy.auth.api_key` or `cliproxy.auth.management_secret`, Docker preserves those custom values.
|
On first startup, the integrated container generates per-install CLIProxy API and management secrets when the config is missing custom values. If you have already configured `cliproxy.auth.api_key` or `cliproxy.auth.management_secret`, Docker preserves those custom values.
|
||||||
|
|
||||||
If you upgraded from an older Docker deployment that used the historical `ccs-internal-managed` API key, CCS keeps that legacy key valid beside the new per-install key for 14 days by default. During the grace period, every `ccs docker up` prints the new key and expiry date to stderr. Override the window with `CCS_DOCKER_LEGACY_KEY_GRACE_DAYS`.
|
If you upgraded from an older Docker deployment that used the historical `ccs-internal-managed` API key, CCS keeps that legacy key valid beside the new per-install key for 14 days by default. During the grace period, every `ccs docker up` prints the masked new key and expiry date to stderr. Reveal the full key only with `ccs docker show-key --full`. Override the window with `CCS_DOCKER_LEGACY_KEY_GRACE_DAYS`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ccs docker show-key # masked
|
ccs docker show-key # masked
|
||||||
|
|||||||
@@ -173,10 +173,14 @@ export function renderDockerKeyRotationBanner(status = getDockerKeyRotationStatu
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maskedReplacementKey =
|
||||||
|
maskDockerApiKey(status.legacyGrace.replacementKey) ?? '(not configured)';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'[!] Docker CLIProxy API key rotation grace period is active.',
|
'[!] Docker CLIProxy API key rotation grace period is active.',
|
||||||
`[i] New CLIProxy API key: ${status.legacyGrace.replacementKey}`,
|
`[i] New CLIProxy API key: ${maskedReplacementKey}`,
|
||||||
`[i] Legacy key ${status.legacyGrace.legacyKey} remains valid until ${status.legacyGrace.expiresAt}.`,
|
`[i] Legacy key ${status.legacyGrace.legacyKey} remains valid until ${status.legacyGrace.expiresAt}.`,
|
||||||
|
'[i] Reveal the full key with `ccs docker show-key --full`.',
|
||||||
'[i] Update existing clients, then run `ccs docker finalize-key-rotation`.',
|
'[i] Update existing clients, then run `ccs docker finalize-key-rotation`.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ describe('docker up subcommand', () => {
|
|||||||
dockerModule.DockerExecutor.prototype.getKeyRotationBanner = function () {
|
dockerModule.DockerExecutor.prototype.getKeyRotationBanner = function () {
|
||||||
return [
|
return [
|
||||||
'[!] Docker CLIProxy API key rotation grace period is active.',
|
'[!] Docker CLIProxy API key rotation grace period is active.',
|
||||||
'[i] New CLIProxy API key: new-secret',
|
'[i] New CLIProxy API key: new-...cret',
|
||||||
'[i] Legacy key ccs-internal-managed remains valid until 2026-06-05T00:00:00.000Z.',
|
'[i] Legacy key ccs-internal-managed remains valid until 2026-06-05T00:00:00.000Z.',
|
||||||
|
'[i] Reveal the full key with `ccs docker show-key --full`.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +52,8 @@ describe('docker up subcommand', () => {
|
|||||||
expect(renderCapturedLines(capture.errorLines)).toContain(
|
expect(renderCapturedLines(capture.errorLines)).toContain(
|
||||||
'Docker CLIProxy API key rotation grace period is active'
|
'Docker CLIProxy API key rotation grace period is active'
|
||||||
);
|
);
|
||||||
expect(renderCapturedLines(capture.errorLines)).toContain('New CLIProxy API key: new-secret');
|
expect(renderCapturedLines(capture.errorLines)).toContain('New CLIProxy API key: new-...cret');
|
||||||
|
expect(renderCapturedLines(capture.errorLines)).toContain('ccs docker show-key --full');
|
||||||
expect(process.exitCode).toBe(0);
|
expect(process.exitCode).toBe(0);
|
||||||
} finally {
|
} finally {
|
||||||
dockerModule.DockerExecutor.prototype.up = originalUp;
|
dockerModule.DockerExecutor.prototype.up = originalUp;
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { mkdtempSync, rmSync } from 'fs';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { afterEach, describe, expect, it } from 'bun:test';
|
||||||
|
import { ensureDockerCliproxyAuth } from '../../../src/docker/docker-bootstrap';
|
||||||
|
import { mutateConfig } from '../../../src/config/config-loader-facade';
|
||||||
|
import { CCS_INTERNAL_API_KEY } from '../../../src/cliproxy/config/config-generator';
|
||||||
|
import {
|
||||||
|
maskDockerApiKey,
|
||||||
|
readDockerBootstrapState,
|
||||||
|
renderDockerKeyRotationBanner,
|
||||||
|
} from '../../../src/docker/docker-key-rotation';
|
||||||
|
|
||||||
|
const originalCcsHome = process.env.CCS_HOME;
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
function useTempCcsHome(): void {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'ccs-rotation-banner-'));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
process.env.CCS_HOME = dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalCcsHome === undefined) {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
} else {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
}
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderDockerKeyRotationBanner - key masking', () => {
|
||||||
|
it('does not leak the full replacement API key into the banner', () => {
|
||||||
|
useTempCcsHome();
|
||||||
|
mutateConfig((config) => {
|
||||||
|
config.cliproxy.auth = { api_key: CCS_INTERNAL_API_KEY };
|
||||||
|
});
|
||||||
|
ensureDockerCliproxyAuth();
|
||||||
|
|
||||||
|
const replacementKey = readDockerBootstrapState().state?.legacyKeyGrace?.replacementKey;
|
||||||
|
expect(replacementKey).toBeTruthy();
|
||||||
|
|
||||||
|
const banner = renderDockerKeyRotationBanner();
|
||||||
|
expect(banner).not.toBe('');
|
||||||
|
expect(banner).not.toContain(replacementKey ?? '');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the masked replacement key in the banner', () => {
|
||||||
|
useTempCcsHome();
|
||||||
|
mutateConfig((config) => {
|
||||||
|
config.cliproxy.auth = { api_key: CCS_INTERNAL_API_KEY };
|
||||||
|
});
|
||||||
|
ensureDockerCliproxyAuth();
|
||||||
|
|
||||||
|
const replacementKey = readDockerBootstrapState().state?.legacyKeyGrace?.replacementKey;
|
||||||
|
const masked = maskDockerApiKey(replacementKey);
|
||||||
|
expect(masked).toBeTruthy();
|
||||||
|
|
||||||
|
const banner = renderDockerKeyRotationBanner();
|
||||||
|
expect(banner).toContain(masked!);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('points at `ccs docker show-key --full` to reveal the key', () => {
|
||||||
|
useTempCcsHome();
|
||||||
|
mutateConfig((config) => {
|
||||||
|
config.cliproxy.auth = { api_key: CCS_INTERNAL_API_KEY };
|
||||||
|
});
|
||||||
|
ensureDockerCliproxyAuth();
|
||||||
|
|
||||||
|
const banner = renderDockerKeyRotationBanner();
|
||||||
|
expect(banner).toContain('ccs docker show-key --full');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still shows the legacy key value (it is the public-known default, not a secret)', () => {
|
||||||
|
useTempCcsHome();
|
||||||
|
mutateConfig((config) => {
|
||||||
|
config.cliproxy.auth = { api_key: CCS_INTERNAL_API_KEY };
|
||||||
|
});
|
||||||
|
ensureDockerCliproxyAuth();
|
||||||
|
|
||||||
|
const banner = renderDockerKeyRotationBanner();
|
||||||
|
expect(banner).toContain(CCS_INTERNAL_API_KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty when grace is not active', () => {
|
||||||
|
useTempCcsHome();
|
||||||
|
const banner = renderDockerKeyRotationBanner();
|
||||||
|
expect(banner).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user