Release Notes — CLI credential storage at rest
Sprint: SPRINT_20260501_004_Cli_token_encryption Module: CLI (src/Cli/) Audit finding closed: B4 (Pass-2 audit)
Summary
The StellaOps CLI no longer writes access tokens, refresh tokens, or any other secret-bearing field to ~/.stellaops/profiles.json in plaintext. Secrets are now persisted through an OS-native per-user secret store:
- Windows — DPAPI (
ProtectedData.Protect) bound to the current user’s SID; ciphertext lives at~/.stellaops/profiles.<name>.secrets.bin. - Linux — libsecret (Secret Service API). The OS keyring owns the ciphertext.
- macOS — Keychain (Generic-Password class).
- Linux fallback —
/etc/machine-id-derived AES-256-GCM file encryption when libsecret/dbus is unavailable. Degraded mode: a warning is logged on every use; refuse withSTELLAOPS_REQUIRE_OS_SECRET_STORE=true.
A new stella login --no-save flag (and matching STELLAOPS_LOGIN_NO_SAVE=1 env-var default) keeps tokens in process memory only — required for CI pipelines and kiosk runs.
Breaking changes
On-disk format. The CLI now writes a sibling secrets blob next to profiles.json. The cleartext file no longer carries token-shaped fields (accessToken, refreshToken, dpopPrivateKeyPem, tenantBearerTokens). External tools that grepped the cleartext file for a token will now find nothing.
This is not a CLI major-version bump because the migration is automatic and one-way:
- On first CLI run after upgrade,
CliProfileManager.LoadStoreAsyncdetects any legacy plaintext token field, re-encrypts it via the platform’sISecretProtector, and atomically rewrites the cleartext file with those fields scrubbed. - The migration is idempotent: a crash mid-run leaves the legacy file untouched and the next run completes cleanly.
Migration
No operator action is required for the encryption change — the CLI migrates on first read.
For CI / kiosk users who never want secrets persisted at all:
export STELLAOPS_LOGIN_NO_SAVE=1
stella auth login --client-id=ci-runner ...
For Linux headless servers without libsecret/dbus:
- Recommended: install
gnome-keyring+ run with a dbus session, OR - Set
STELLAOPS_REQUIRE_OS_SECRET_STORE=trueand use--no-save, OR - Accept the machine-id fallback (logs a warning every run).
What changed under the hood
- New crypto contract
IAeadAlgorithminsrc/__Libraries/StellaOps.Cryptography/IAeadAlgorithm.cs(mirrorsIHmacAlgorithm). AEAD operations across the repo must route through this; the architecture-conformance testAeadBclCallSiteConformanceTestsenforces it. - New CLI abstraction
ISecretProtectorinsrc/Cli/StellaOps.Cli/Configuration/Secrets/. Four backends:WindowsDpapiSecretProtector,LinuxLibsecretSecretProtector,MacOsKeychainSecretProtector,MachineIdFallbackSecretProtector, plus a process-onlyInMemorySecretProtectorfor--no-save. CliProfileManager(src/Cli/StellaOps.Cli/Configuration/CliProfile.cs) gained:GetSecretsAsync(profileName)/SaveSecretsAsync(profileName, ...)/RemoveSecretsAsync(profileName).MigrateLegacyPlaintextAsync— runs automatically on firstLoadStoreAsync.IsNoSaveModeandSecretBackendIdfor diagnostics.
stella auth logingained--no-saveand honoursSTELLAOPS_LOGIN_NO_SAVE.
Test coverage
- Round-trip + tamper-detection:
DefaultAeadAlgorithmTests,AeadBclCallSiteConformanceTests(Cryptography test project). - Per-platform protector round-trip:
SecretProtectorTests(Windows/Linux/macOS gated; non-matching platforms skip). - Migration:
CliProfileMigrationTestscovers happy-path, idempotency, crash-mid-migration recovery, and--no-saveno-disk-touch. --no-saveenv-var resolution:NoSaveLoginTests.
Related docs
docs/modules/cli/operations/credential-storage.md— operator-facing reference for backend selection, fallback policy, and migration.docs-archive/qa/audits/microservice-audit-pass2-2026-04-29.md§B4 — original audit finding.
