Repository navigation
Conversation
…t secret
The configure-azure wizard printed the freshly created Azure service
principal's client secret to stdout with a bare fmt.Printf, then asked
the operator to retype it. Under script/tee, CI logs or terminal
scrollback that leaks a live credential holding the Reservations
Administrator role.
The wizard now hands the created credentials (tenant, appId, secret and
the Step 2 subscription) straight to the Secrets Manager store step.
Only the non-secret identifiers are printed, along with the name of the
secret the credentials are stored in. If loading AWS config or storing
fails after creation, the error explains how to mint a replacement
secret ('az ad app credential reset') and re-run with --skip-setup.
Closes LeanerCloud#2049 (A10-028).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 4 billable files and costs up to $1.00. Or wait 4 minutes for your next included review. View limit details
Comment |
Merge-gate review verdict: CLEAN at 8bd9f96Independent adversarial security review by a non-author agent (Sonnet high, cold brief, full diff Secret path traced from creation to the end of the flow: Graph Non-blocking notes, @IAmJSD (your call; none block this PR):
Behaviour change: when the wizard creates the SP, the Local verification on macOS at 8bd9f96 (Azure and AWS calls are mocked; no real cloud):
Merge blockers (not code):
🤖 Generated with Claude Code |
…g the service principal Open the Secrets Manager store (load AWS config and resolve AWS credentials) before the wizard can create a service principal, so the most common failures happen before any Azure side effect and never orphan an SP whose undisplayed secret is lost. The recovery hint stays on the store step for failures that can still happen after creation. Extract configureAzure(ctx, reader) and make the store constructor a package variable so the command flow is unit-tested with mocks: AWS config failure makes zero Azure calls, a store failure after creation carries the appId recovery hint without the secret, and the end-to-end create-and-store path never prints the secret. Reword the manual-entry prompt, which referred to Service Principal output that is no longer shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up on the three non-blocking review notes: new head 7cf76afOne new commit on top of 8bd9f96 (no rebase or force-push). Files: 1. AWS config loaded after SP creation. Fixed. 2. Hint call sites untested. The command body is now
3. Stale prompt wording. Changed to "Enter the credentials of an existing Azure Service Principal:". Verification (mock/unit evidence only; no real Azure or AWS calls, no SP created)Fail-before (mutation probes on 7cf76af, each reverted and confirmed identical to the committed file):
Pass-after on macOS, go1.26.9: Gaps: I have not run this against a real AWS account. In particular, I have not confirmed with real credentials that 🤖 Generated with Claude Code |
Summary
Closes #2049 (A10-028, the CLI part of this finding. A08-016 and A08b-026 were split out to LeanerCloud/cloud-commitments-go#64 and are not touched here.)
Root cause
printAzureSPResultincmd/configure_azure.gowrote the newly created service principal's client secret to stdout with a plainfmt.Printf. It ran with no TTY check and no redaction. The wizard then asked the operator to retype the same value at the next prompt. Anything that captures stdout (script,tee, CI job logs, terminal scrollback) ended up holding a live credential with the Reservations Administrator role in plaintext.Fix: hand the secret straight to the store step and never display it
I took the issue's primary suggestion: carry the wizard's result straight into
collectAzureCredentials.azureStepCreateServicePrincipal/runAzureSetupCommandsnow return the created credentials as*AzureCredentials: tenant, appId, secret, and the subscription the SP was scoped to in Step 2. The return isnilwhen Step 3 is skipped.collectAzureCredentials(reader, created)uses those credentials as they are when non-nil and asks for nothing.storeAzureCredentialsthen writes them to<stack>-AzureCredentials. The skip path and--skip-setuppath behave as before.printAzureSPResultprints only appId, tenant ID and subscription ID, pluspassword (Client Secret): [hidden]and the name of the Secrets Manager secret it is stored in. No part of the secret is shown, not even a prefix hint.<stack>-AzureCredentials(fieldclient_secret), which is the only place CUDly reads it from. Anyone who really needs the value can read it from Secrets Manager with their AWS permissions, which is an audited path.UpdateSecretis denied),withAzureSPRecoveryHintadds recovery steps to the error. It gives the appId (not secret), says the secret was not stored and never displayed, and tells the operator to runaz ad app credential reset --id <appId>and re-runcudly configure-azure --skip-setup, typing the new secret at the existing hidden (term.ReadPassword) prompt.Alternatives I considered and rejected:
term.IsTerminalgate (the issue's fallback). It still leaks underscript, terminal scrollback and screen sharing.--show-secretflag. Nothing needs it once the handoff is direct, and it adds new surface.Behavior change: when the wizard creates an SP, the credentials from that SP replace any
--tenant-id/--client-id/--client-secret/--subscription-idflags. This is deliberate: those values belong to the SP that was just created.docs/cli/cloud-setup.mdnow describes this.To make Step 3 testable without Azure,
resolveAzureTenantIDandnewGraphSPProvisionerare now called through the package variablesresolveAzureTenantIDFn/newAzureSPProvisionerFn. The production values are unchanged. The command body isconfigureAzure(ctx, reader), and the Secrets Manager store is built throughopenAzureSecretsStoreFn, so the whole command flow is unit-tested with mocks (follow-up commit 7cf76af).Verification
All evidence comes from unit tests with mocks. The Azure provisioner is the existing
mockSPProvisionerand Secrets Manager isMockSecretsStore. No real Azure or AWS calls were made and no service principals were created.New tests in
cmd/configure_azure_secret_test.go. Each one sendsos.Stdout,os.Stderrand the standardlogoutput to one pipe. They assert that neither the full fake secret nor any 8-character window of it appears in that output:TestAzureWizard_ClientSecretNeverPrinted: runs the full handoff (Step 3 with inputr→collectAzureCredentialswith an empty reader, so any re-prompt would fail →storeAzureCredentials). Asserts no secret appears in the output, appId /[hidden]/my-cudly-AzureCredentialsare shown, and the stored JSON has exactly the tenant, appId, secret and subscription.TestPrintAzureSPResult_OmitsSecret: tests the printer directly.TestAzureWizard_SkipCreateSP_ReturnsNil: the skip path returns nil and never builds a provisioner.TestConfigureAzure_AWSConfigFailure_BeforeSPCreation,TestConfigureAzure_StoreFailureAfterCreate_HasRecoveryHint,TestConfigureAzure_CreateAndStore_EndToEnd,TestCollectAzureCredentials_PromptWording(added in 7cf76af; see the follow-up PR comment).TestAzureWizard_FailureAfterCreate_NoSecretInError: the secret does not appear in the rollback error (AssignRole failure) or in the recovery-hint error, and the hint namesaz ad app credential reset --id <appId>.Fail-before:
origin/main(detached worktree), the printer test adapted to the old 2-argument signature fails:fmt.Printf(" password (Client Secret): %s\n", result.ClientSecret)in place of the[hidden]line makes bothTestAzureWizard_ClientSecretNeverPrintedandTestPrintAzureSPResult_OmitsSecretfail withfull client secret leaked to output.Pass-after (commands run from the repo root):
I set
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullonly because the local~/.aws/configdefault profile on the verification machine is invalid. These tests don't depend on it.Gaps: I did not run the wizard against a real Azure tenant or AWS account. In particular, I did not check by hand the full interactive
cudly configure-azurerun (az login → Graph SP creation → Secrets Manager write).Out of scope
--client-secreton the command line (chore(pkg/cmd): medium/low findings from the 2026-07-28 full review #1613).setup.shprinting the same kind of secret (chore(terraform/environments): medium/low findings from the 2026-07-28 full review cloud-commitments-platform#132 item 19).cmd/configure_azure.gowas already over the 500-line guideline before this change (602 → 649 lines). I left the file split for a separate change.Maintainers: please mirror the issue's triage labels onto this PR:
triaged,priority/p3,severity/low,urgency/eventually,impact/few,effort/s,type/security.🤖 Generated with Claude Code