fix: mask Docker key rotation banner

This commit is contained in:
Tam Nhu Tran
2026-05-22 17:04:19 -04:00
parent 72688b6eff
commit bbcf9e8b15
4 changed files with 102 additions and 4 deletions
+1 -1
View File
@@ -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
+5 -1
View File
@@ -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('');
});
});