mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +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.
|
||||
|
||||
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
|
||||
ccs docker show-key # masked
|
||||
|
||||
@@ -173,10 +173,14 @@ export function renderDockerKeyRotationBanner(status = getDockerKeyRotationStatu
|
||||
return '';
|
||||
}
|
||||
|
||||
const maskedReplacementKey =
|
||||
maskDockerApiKey(status.legacyGrace.replacementKey) ?? '(not configured)';
|
||||
|
||||
return [
|
||||
'[!] 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] Reveal the full key with `ccs docker show-key --full`.',
|
||||
'[i] Update existing clients, then run `ccs docker finalize-key-rotation`.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -31,8 +31,9 @@ describe('docker up subcommand', () => {
|
||||
dockerModule.DockerExecutor.prototype.getKeyRotationBanner = function () {
|
||||
return [
|
||||
'[!] 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] Reveal the full key with `ccs docker show-key --full`.',
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
@@ -51,7 +52,8 @@ describe('docker up subcommand', () => {
|
||||
expect(renderCapturedLines(capture.errorLines)).toContain(
|
||||
'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);
|
||||
} finally {
|
||||
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