Repository navigation
Conversation
A stored external_account config names the endpoints the resolver talks to when it exchanges and impersonates tokens. Only Google's STS and IAM Credentials endpoints are ever expected there (gcloud and the gcp-target IaC write exactly those, and the secret-free federated path hard-codes them), so the resolver now checks them before it builds a token source: token_url must be https://sts.googleapis.com/..., and service_account_impersonation_url, when set, must be https://iamcredentials.googleapis.com/..., with no user info or port. The check decodes the JSON the same way golang.org/x/oauth2/google does, so it sees the values that library would use. A config that fails it makes the load return a clear error naming the field, without the stored value, instead of a token source. ValidateGCPWIFEndpoints is exported so the API can apply the same rule when a config is submitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validateGCPWIFPayload checked the shape of a gcp_workload_identity_config but not its URLs. It now runs credentials.ValidateGCPWIFEndpoints on token_url and service_account_impersonation_url, the same rule the resolver applies before using a stored config, so a config that could never be used is refused with a 400 when it is saved. The optional impersonation URL must also be a non-empty string when present. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/register accepts an optional credential_type and credential_payload, which approval saves into account_credentials. They were stored as sent, while POST /api/accounts/:id/credentials checks the type and the payload shape. Registrations now get the same checks: - credential_type and credential_payload come together or not at all; - the type must be a known one and belong to the registration's provider; - the payload must be a JSON object that passes validateCredentialPayload (including the GCP WIF endpoint check). The stored payload is re-encoded from the checked object, so the saved bytes are the ones that were checked. Registrations without credentials are accepted as before. Approval checks the stored credential again, against the provider of the account being created, before the registration leaves "pending". A credential that fails stops the approval with a 400 telling the admin to reject the registration; nothing is created or saved. Previously a decrypt failure or a type-less payload was skipped with a log line while the account was still created and enabled. Without a credential store the payload was already dropped at submit; the type is now dropped with it so the record stays consistent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
Collaborator
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Credentials can be submitted two ways: with a registration (
POST /api/register, saved when an admin approves the registration) and throughPOST /api/accounts/:id/credentials. The second path checks the credential type and the payload shape. The first path didn't, so a registration's credential was saved exactly as sent. This PR runs the same checks on both paths. It also adds an endpoint check for GCP workload identity federation (WIF) configs, which runs when a config is saved and again before a stored config is used.This complements #1308, which pins the JSON type each stored GCP credential slot may hold. The two branches merge cleanly, and the combined tree passes
go test -race ./internal/credentials/ ./internal/api/.What is checked now
Registration credentials (
internal/api/handler_registrations_credentials.go)credential_typeandcredential_payloadmust be sent together or not at all. Registrations without credentials are still accepted.credential_typemust be one of the four known types, and it must belong to the registration's provider (aws_access_keys→aws,azure_client_secret→azure,gcp_*→gcp).credential_payloadmust be a JSON object that passesvalidateCredentialPayload, the same checkPOST /api/accounts/:id/credentialsruns.pending. A credential that fails gets a 400 telling the admin to reject the registration. Nothing is transitioned, created or saved. Before this change, a decrypt failure or a payload with no type was logged and skipped, and the account was still created and enabled.GCP WIF endpoints (
internal/credentials/gcp_endpoints.go, used byvalidateGCPWIFPayload)token_urlis required and must behttps://sts.googleapis.com/....service_account_impersonation_urlis optional. When set, it must be a non-empty string of the formhttps://iamcredentials.googleapis.com/....sts.googleapis.com.example.netandmy-sts.googleapis.com,http://URLs, and IP literals.loadStoredGCPTokenSourceruns the same check on any storedexternal_accountJSON before building a token source. It decodes the JSON the same waygolang.org/x/oauth2/googledoes (case-insensitive keys, last duplicate wins), so it checks the value that library would use. A failing config makes the load return a clear error that names the field but not the stored value. The load does not panic or fall back. A stored WIF config still takes priority over the secret-free federated path, and the check covers that case.Compatibility
token_urlorservice_account_impersonation_urlpoints anywhere other than Google's public STS / IAM Credentials endpoints, for example a private or regional STS endpoint, a proxy, or a URL with an explicit:443. gcloud-generated configs and the gcp-target IaC (iac/federation/gcp-target/terraform/registration.tf) use exactlyhttps://sts.googleapis.com/v1/tokenandhttps://iamcredentials.googleapis.com/..., and both pass (tested). An affected account's GCP calls fail with "stored gcp workload identity config for account … is not usable: …; save the config again" until the config is saved again with Google endpoints.POST /api/accounts/:id/credentialswould also reject now get a 400 at submit. Pending registrations that already hold such a credential can't be approved. The admin gets a 400 and can reject the registration and ask for a new one.Verification
All commands were run from the worktree on Linux, with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true. Nothing calls a real cloud API: building a token source makes no network calls, and no test callsToken().go build ./... && go vet ./... && go vet -tags integration ./... && (cd tests/e2e && go vet -tags e2e ./...): cleangolangci-lint runv2.10.1 on./internal/api/... ./internal/credentials/..., with and without--build-tags integration: 0 issuesgocyclo -over 10on the changed non-test files: nonego test -race -short -count=1 ./internal/api/ ./internal/credentials/: okgo test -race -tags integration -count=1 -timeout 45m ./internal/api/ ./internal/credentials/: credentials ok. api: every test passed exceptTestIntegration_HistorySummary_TotalsCoverAllInScopeRows, whose Postgres test container did not become ready within the timeout. The same test fails the same way on unmodifiedorigin/mainon this VM (Docker was heavily loaded at the time), and it does not touch the code changed here. CI's Integration Tests job should confirm it.handler_registrations.go,validation.goandresolver.gowere restored fromorigin/main, the new handler file was removed, andValidateGCPWIFEndpointswas made a no-op. On that tree, every new rejection test failed (35 failing api subtests, 76 failing credentials subtests). The acceptance and compatibility tests passed before and after: no credentials, gcloud-style configs, and the empty / malformed load errors. With the fix, all new tests pass.sts.googleapis.com.example.net,my-sts.googleapis.com, trailing dot, other Google host),http://and other schemes, user info (sts.googleapis.com@host,user:pass@), explicit ports (:443,:8443, empty), IP literals, case-variant and duplicate keys, a valid gcloud-generated WIF config (AWSaws1source and file source), registrations with a mismatched provider/type, junk/array/null/string JSON, type-only or payload-only submissions, credentials omitted (still accepted), and approval of a pending registration whose stored credential fails (400, nothing created).secrets-scan.shon the changed files: "clean; planted key flagged".scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.🤖 Generated with Claude Code