Skip to content

feat(broker): store provider client secrets in GCP Secret Manager - #113

Merged
sangalo20 merged 19 commits into
mainfrom
feat/secret-backend-gcp
Oct 10, 2026
Merged

sangalo20 merged 19 commits into
mainfrom
feat/secret-backend-gcp

Conversation

@sangalo20

@sangalo20 sangalo20 commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request

Description

First external secret backend for #63. With SECRET_BACKEND=gcp-secret-manager, the Broker stores each OAuth2 provider's client_secret as its own Secret Manager secret and leaves provider_profiles.client_secret NULL, so application credentials no longer sit next to metadata or in database backups. The default internal backend is unchanged.

Design deviations from the issue, posted there for discussion: only client_secret moves (not client_id), secrets are keyed by provider UUID (not name), and GCP ships first (not HashiCorp Vault).

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor

Changes Made

  • nexus-broker/pkg/vault/tokens: existing AES-GCM token encryption, moved from pkg/vault (first commit, no behaviour change).
  • nexus-broker/pkg/vault/providers: Backend interface, ParseKind, in-memory backend for tests.
  • nexus-broker/pkg/vault/providers/gcp: Secret Manager implementation. One secret per provider, automatic replication, managed-by=nexus-broker label; Set adds a version and destroys earlier enabled versions; Ping runs Set twice, Get and Delete on a random probe key under the prefix at startup, so every permission a registration needs (secret create/delete, version add/access/list/destroy) is verified before the Broker serves traffic; the probe secret lives for under a second and is removed on every exit path.
  • nexus-broker/pkg/provider/store.go: NewStore(db, opts...) with WithSecretBackend. Register writes the row then the secret and removes the row if the secret write fails. Patch and update rotate. Delete removes the secret. Reads resolve from the backend only when the column is NULL, so pre-existing rows keep working.
  • nexus-broker/pkg/config/broker.go: SECRET_BACKEND, GCP_PROJECT_ID, GCP_SECRET_PREFIX, validated at boot. main.go pings the backend and refuses to start on failure.
  • deploy/terraform/gcp/secret-backend: custom role with secretmanager.secrets.create plus roles/secretmanager.admin conditioned on resource.name.startsWith(".../secrets/<prefix>-").
  • Docs: docs/guides/provider-secret-backends.md, config table rows, deploy page note, broker README, changelog 0.5.0. docker-compose.yml and .env.example wire SECRET_BACKEND, GCP_PROJECT_ID, GCP_SECRET_PREFIX.
  • Review hardening (later commits): typed backend and validation errors mapped to 503/502/400, missing-secret state that refuses to send an empty secret, concurrent-safe rotation and delete-by-name, bounded startup probe, Go 1.26 toolchain for the broker.
  • VERSION 0.5.0 (minor: new feature), stamped.

How to Test

  1. cd nexus-broker && go test ./... — unit tests cover register/rollback, resolve, missing-secret, non-OAuth skip, patch, update, delete and delete-by-name against the in-memory backend; the internal path is asserted unchanged.
  2. NEXUS_TEST_GCP_PROJECT=<project> go test ./pkg/vault/providers/gcp/ -run TestBackend_RoundTrip -v with ADC that can write secrets — create, rotate, read, delete. Skips in CI.
  3. Run the Broker with SECRET_BACKEND=gcp-secret-manager GCP_PROJECT_ID=<project> against local Postgres and Redis, POST /providers with a client secret: the row's client_secret is NULL, the secret nexus-providers-<id> exists, GET /providers/by-name/... is still redacted, PATCH with a new client_secret leaves one enabled version, DELETE removes the secret. Done against nexus-cloud-dae3 on 2026-10-10.
  4. terraform validate in deploy/terraform/gcp/secret-backend.

Migration / Breaking Changes

  • Database migration required
  • No migration required

Opt-in via SECRET_BACKEND. Existing deployments are unaffected. Existing rows are not migrated into the backend; that is a follow-up.

Checklist

  • Code follows the Go styleguide (gofmt applied)
  • Commit messages use present tense, imperative mood, ≤72 chars
  • Documentation updated where applicable
  • No secrets or credentials committed

pkg/vault becomes the umbrella for the broker's two secret concerns: the
AES-GCM token vault (now pkg/vault/tokens) and the provider client-secret
backends that follow in the next commit. Call sites import the package as
tokenvault. No behaviour change.

Refs #63
Add SECRET_BACKEND with two implementations: internal (the existing
provider_profiles.client_secret column, still the default) and
gcp-secret-manager, which keeps one Secret Manager secret per provider
named <GCP_SECRET_PREFIX>-<provider id> and leaves the column NULL.

The provider store takes the backend as an option. Register writes the
row then the secret and removes the row again if the secret write fails.
Patch rotates the secret and destroys earlier versions. Delete removes
the secret. Reads resolve the secret from the backend only when the
column is NULL, so rows written before the switch keep working. Secrets
are keyed by provider id, not name, so renames never orphan them. Only
client_secret moves; client_id is not a secret.

The broker pings the backend at startup and refuses to boot if it cannot
reach Secret Manager under the prefix.

deploy/terraform/gcp/secret-backend grants the broker service account
secretmanager.secrets.create on the project (create cannot be scoped by
name) plus Secret Manager admin conditioned on the secret-name prefix,
so infrastructure secrets in the same project stay unreadable to it.

Unit tests cover the store against an in-memory backend. The GCP
round-trip test runs only when NEXUS_TEST_GCP_PROJECT is set.

Closes #63 for GCP; Vault and AWS backends follow on the same interface.
Minor bump for the GCP Secret Manager provider secret backend.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 10:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Credential-loss, orphaned-secret, rotation-concurrency, and cleanup reliability issues must be resolved before approval.

3 open findings
What changed in this PR

Adds GCP Secret Manager as an opt-in backend for OAuth provider client secrets while retaining PostgreSQL as the default.

Changes:

  • Introduces provider-secret backend abstractions and GCP implementation.
  • Integrates external secrets into provider lifecycle and startup validation.
  • Moves token encryption helpers and updates documentation, Terraform, dependencies, and versions.
File Description
VERSION Bumps release to 0.5.0.
openapi.yaml Updates Gateway API version.
nexus-sdk-ts/​package.json Updates TypeScript SDK version.
nexus-sdk-ts/​package-lock.json Synchronizes package-lock version.
nexus-sdk-python/​pyproject.toml Updates Python SDK version.
nexus-sdk-python/​nexus_sdk/​__init__.py Updates runtime SDK version.
nexus-broker/​tests/​integration/​soc_test.go Updates token-vault imports.
nexus-broker/​tests/​integration/​soc_livedb_test.go Updates live database encryption imports.
nexus-broker/​README.md Documents GCP secret backend setup.
nexus-broker/​pkg/​vault/​tokens/​encrypt.go Moves encryption into the tokens package.
nexus-broker/​pkg/​vault/​tokens/​encrypt_test.go Moves encryption tests.
nexus-broker/​pkg/​vault/​providers/​providers_test.go Tests backend parsing and memory storage.
nexus-broker/​pkg/​vault/​providers/​memory.go Adds an in-memory test backend.
nexus-broker/​pkg/​vault/​providers/​gcp/​gcp.go Implements GCP Secret Manager storage.
nexus-broker/​pkg/​vault/​providers/​gcp/​gcp_integration_test.go Adds opt-in GCP integration testing.
nexus-broker/​pkg/​vault/​providers/​backend.go Defines provider-secret backend contracts.
nexus-broker/​pkg/​vault/​doc.go Documents vault package responsibilities.
nexus-broker/​pkg/​provider/​store.go Integrates external secrets with provider persistence.
nexus-broker/​pkg/​provider/​store_secrets_test.go Tests provider-store backend behavior.
nexus-broker/​pkg/​handlers/​soc2_compliance_test.go Updates encryption package references.
nexus-broker/​pkg/​config/​broker.go Adds secret-backend configuration.
nexus-broker/​openapi.yaml Updates Broker API version.
nexus-broker/​internal/​service/​saml.go Updates token encryption imports.
nexus-broker/​internal/​service/​revoke.go Updates token decryption imports.
nexus-broker/​internal/​service/​revoke_test.go Updates revocation test imports.
nexus-broker/​internal/​service/​revoke_integration_test.go Updates integration-test encryption imports.
nexus-broker/​internal/​service/​credential.go Updates credential encryption imports.
nexus-broker/​internal/​service/​connection.go Updates token encryption imports.
nexus-broker/​internal/​service/​connection_test.go Updates connection test imports.
nexus-broker/​go.sum Records GCP dependency checksums.
nexus-broker/​go.mod Adds GCP dependencies and updates Go version.
nexus-broker/​cmd/​nexus-broker/​main.go Initializes and verifies the configured backend.
mkdocs.yml Adds guide navigation and version stamp.
docs/​infrastructure/​deploying-nexus.md Links deployment guidance.
docs/​guides/​provider-secret-backends.md Documents backend behavior and permissions.
docs/​getting-started/​configuration.md Documents new environment variables.
docs/​CHANGELOG.md Records the 0.5.0 feature.
deploy/​terraform/​gcp/​secret-backend/​variables.tf Defines Terraform module inputs.
deploy/​terraform/​gcp/​secret-backend/​README.md Documents Terraform usage.
deploy/​terraform/​gcp/​secret-backend/​outputs.tf Exposes backend environment settings.
deploy/​terraform/​gcp/​secret-backend/​main.tf Provisions scoped Secret Manager IAM.
.gitignore Ignores Terraform state and working files.
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread nexus-broker/pkg/vault/providers/gcp/gcp.go
Comment thread nexus-broker/pkg/config/broker.go Outdated
Comment thread nexus-broker/pkg/provider/store.go Outdated
- Set reports a failure to retire earlier secret versions instead of
  returning success; the new version is live either way, and the caller
  can retry since Set is idempotent.
- GCP_SECRET_PREFIX is validated only when the GCP backend is selected,
  so a stale value cannot stop an internal deployment from booting.
- A registration whose secret write fails removes the external secret
  as well as the row, since the write may have failed part way.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Secret loss, orphan cleanup, invalid-prefix handling, and concurrent rotation defects must be resolved before approval.

1 open finding
3 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (4)

In code that hasn't changed since last review

Medium severity Terraform allows prefixes that exceed Secret Manager ID limits

deploy/​terraform/​gcp/​secret-backend/​variables.tf:18

The Terraform module also accepts a prefix longer than 218 characters, even though appending -<provider UUID> then exceeds Secret Manager's 255-character secret ID limit. Mirror the broker's generated-name length constraint so invalid deployments fail during validation.

Medium severity Prefix validation allows Secret Manager IDs over 255 characters

nexus-broker/​pkg/​config/​broker.go:182

The charset check accepts prefixes that can never form a valid Secret Manager ID. Secret IDs are limited to 255 characters, and the generated prefix + "-" + UUID adds 37 characters, so prefixes longer than 218 pass startup validation but every registration fails. Enforce the generated-ID length here.

Medium severity Missing secrets are treated as successful reads

nexus-broker/​pkg/​provider/​store.go:75

Treating a missing external secret as a successful read causes OAuth flows to send an empty client_secret. It also makes the health worker classify the provider's typical invalid_client 400/401 response as healthy, contrary to the stated missing-secret behavior. Preserve a typed missing-secret state so execution and health paths fail clearly while metadata-only reads can remain available.

This issue also appears in the following locations of the same file:

  • line 392
  • line 397
  • line 435
Medium severity Soft delete leaves secrets behind after backend failures

nexus-broker/​pkg/​provider/​store.go:556

Once the soft delete succeeds, a backend failure returns immediately and leaves this and all later secrets behind. Retrying by name cannot clean them up because the ID query only selects non-deleted rows. Include historical IDs for idempotent cleanup and attempt every deletion before returning the joined errors.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/vault/providers/gcp/gcp.go Outdated
- Rotation retires only versions numerically older than the one just
  added, so two concurrent rotations cannot destroy each other's value.
- GCP_SECRET_PREFIX is capped at 218 characters in the broker and in the
  Terraform module, so <prefix>-<uuid> always fits Secret Manager's
  255-character id limit.
- A provider whose secret is absent from the backend is flagged
  ClientSecretMissing: metadata reads still work, but exchange, refresh
  and revocation refuse with provider_secret_missing and the health
  probe reports unhealthy, instead of sending an empty secret that the
  provider would reject in a way that looked healthy.
- DeleteProfileByName drops the secret of every row with that name,
  including rows soft-deleted earlier, attempts every drop, and joins
  the errors, so a retry after a backend failure cleans up.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 16:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Legacy PUT handling can lose secrets, missing providers can create orphaned secrets, and the Go 1.26 requirement conflicts with existing CI and Docker toolchains.

2 open findings
1 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (1)

In code that hasn't changed since last review

Medium severity PATCH creates orphaned secrets for inactive providers

nexus-broker/​pkg/​provider/​store.go:466

A PATCH containing client_secret creates the external secret before confirming that this UUID identifies an active provider. For a missing or soft-deleted ID, the later UPDATE affects zero rows without error, so the request succeeds and leaves an orphaned secret. Verify the row first or check RowsAffected and clean up the secret on zero rows.

🧠 Review effort: Balanced

Comment thread nexus-broker/go.mod
Comment thread nexus-broker/pkg/provider/store.go Outdated
…1.26

- UpdateProfile with an external backend clears the column only when a
  new secret was written; a PUT that omits client_secret leaves a
  legacy row's column value in place instead of nulling it.
- PATCH and PUT that wrote a secret verify the UPDATE matched an active
  row; on zero rows the secret is removed again and the caller gets
  not-found, so an unknown or soft-deleted id cannot orphan a secret.
- The Secret Manager client and the x/* modules it pulls in require Go
  1.26, so the broker Dockerfile builds with golang:1.26-alpine and CI
  installs the toolchain from nexus-broker/go.mod, the highest directive
  in the repo, rather than relying on toolchain auto-download.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 16:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Empty secrets can be rotated into the backend, the startup probe can hang indefinitely, and the vault package move breaks its exported API.

1 open finding
2 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (2)

In code that hasn't changed since last review

Medium severity Preserve deprecated vault API wrappers

nexus-broker/​pkg/​vault/​tokens/​encrypt.go:1

Moving these exported helpers removes vault.Encrypt and vault.Decrypt, so consumers of the public pkg/vault package no longer compile despite this being described as a no-behavior-change refactor. Keep deprecated forwarding wrappers in pkg/vault while introducing the new package, or explicitly mark this as a breaking API change.

Medium severity Bound startup probe with a timeout

nexus-broker/​cmd/​nexus-broker/​main.go:102

Bound the startup probe with a deadline. context.Background() has no cancellation, so an unreachable Secret Manager endpoint or prolonged client retries can hang startup indefinitely instead of failing fast as documented.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/provider/store.go Outdated
- PUT and PATCH with an external backend refuse an empty client_secret;
  clearing a secret is done by deleting the provider.
- pkg/vault keeps Encrypt and Decrypt as deprecated wrappers around
  pkg/vault/tokens, so the move does not break importers.
- The startup backend probe runs under a 15s deadline so an unreachable
  Secret Manager fails fast instead of hanging in client retries.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 16:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Startup permission validation and backend-error HTTP handling can currently produce late failures and misleading client responses.

0 open findings

1 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (4)

In code that hasn't changed since last review

Medium severity Missing-secret status is checked after premature discovery failures

nexus-broker/​pkg/​provider/​health.go:126

The missing-secret check runs after discovery and the empty-token-URL return. Consequently, a profile flagged ClientSecretMissing can be reported as a discovery failure or unknown instead of the promised missing-secret unhealthy result. Check this flag immediately after identifying an OAuth provider, before either early return.

Medium severity Backend read failures are misclassified as missing providers

nexus-broker/​pkg/​provider/​store.go:80

Backend read failures now share the same GetProfile error channel as a missing database row. Existing callers classify any such error as provider_not_found/404 (and GetByName returns the raw error), so a Secret Manager timeout is falsely reported as a missing provider and may expose cloud error details. Return a distinguishable backend-unavailable error and map it to a sanitized 5xx response.

Medium severity Registration maps backend failures to 400 and exposes internal errors

nexus-broker/​pkg/​provider/​store.go:285

A putSecret or cleanup failure is operational, but ProvidersHandler.Register maps every RegisterProfile error to HTTP 400 and returns its text. A transient GCP outage will therefore look like a non-retryable client error and can expose backend details. Use a typed backend error and have registration return a sanitized 5xx response.

Medium severity Startup probe omits required secret creation permission

nexus-broker/​pkg/​vault/​providers/​gcp/​gcp.go:199

This startup probe only exercises secretmanager.secrets.get. The Terraform module deliberately grants secretmanager.secrets.create through a separate project-level custom role, so a deployment missing that binding will pass Ping and then fail on its first registration. Either verify every permission needed by Set/Delete (for example via IAM permission checks or a create/delete probe), or narrow the documented fail-fast guarantee.

🧠 Review effort: Balanced

…artup

- Store errors from the secret backend wrap provider.ErrSecretBackend.
  Handlers map it to a sanitized 503 instead of a 400 or 404 carrying
  the cloud error; the exchange and refresh services map it to 502
  instead of provider_not_found.
- The health prober checks ClientSecretMissing before discovery and the
  token-URL check, so the missing-secret verdict is never masked.
- The startup probe creates, reads and deletes a probe secret under the
  prefix (nil UUID, never versioned, never billed), so a missing create
  binding fails at boot rather than on the first registration.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 16:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Registration has a crash-consistency gap, and the startup probe is incomplete and unsafe during concurrent replica startup.

1 open finding
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (2)

In code that hasn't changed since last review

Medium severity Map GetProfile backend outages to secret_backend_unavailable

nexus-broker/​internal/​service/​connection.go:294

The backend-unavailable mapping is only applied to token exchange. CreateConsentSpec also calls GetProfile, which can now return ErrSecretBackend, but its existing branch at connection.go:149-152 reports that outage as provider_not_found. Apply the same secret_backend_unavailable mapping there so starting an OAuth flow does not return a false 404 during a Secret Manager outage.

Medium severity Ping does not verify secret version permissions

nexus-broker/​pkg/​vault/​providers/​gcp/​gcp.go:200

This startup check does not prove the permissions required by Set and Get: it never adds, accesses, or destroys a secret version. An IAM setup with create/get/delete but without version permissions passes Ping and then fails on the first registration or OAuth exchange, contrary to the fail-fast contract. Exercise a temporary version through add/access/destroy before deleting the probe secret.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/vault/providers/gcp/gcp.go Outdated
The startup probe now runs the backend's own Set, Get and Delete on a
random key, which exercises every permission a registration needs,
including version add, access, list and destroy. A per-call key means
replicas starting together cannot delete each other's probe, and the
probe is removed on every exit path.

CreateConsentSpec maps a backend outage to secret_backend_unavailable
like the exchange path, instead of a false provider_not_found.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 16:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The startup probe, cleanup timeout, deletion concurrency, and validation-status issues can cause runtime failures or orphaned secrets.

1 open finding
1 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (3)

In code that hasn't changed since last review

Medium severity Client secret validation errors incorrectly return HTTP 500

nexus-broker/​pkg/​handlers/​providers.go:37

The new client_secret validation errors from UpdateProfile and PatchProfile fall through this helper using the callers' 500 status. Thus an empty or non-string client secret is reported as an internal failure rather than invalid input. Introduce a typed/sentinel validation error and map it to HTTP 400 here.

Medium severity Non-atomic ID lookup can orphan concurrently registered secrets

nexus-broker/​pkg/​provider/​store.go:614

The ID lookup and soft-delete are separate statements. On a retry where only historical rows exist, a concurrent registration can insert the same name after this SELECT; the later UPDATE deletes that new row, but its ID is absent from ids, leaving its backend secret orphaned. Obtain active IDs from UPDATE ... RETURNING id atomically, while retaining a separate retry path for already-deleted rows.

Medium severity Probe does not verify DestroySecretVersion permission

nexus-broker/​pkg/​vault/​providers/​gcp/​gcp.go:207

This probe creates only one version, so Set lists the new version but never calls DestroySecretVersion. Startup can therefore succeed without the destroy permission and the first real rotation will fail. Write the probe twice so the second Set must retire the first version.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/vault/providers/gcp/gcp.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Concurrent rotations can mismatch client metadata and secrets, and blank backend values do not fail closed.

1 open finding
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (1)

In code that hasn't changed since last review

Medium severity Treat blank backend secrets as missing

nexus-broker/​pkg/​provider/​store.go:92

A successful backend read is treated as a usable secret even when the backend returns an empty or whitespace-only payload. That leaves ClientSecretMissing false, so RequireClientSecret and the health probe send the blank value instead of failing closed (the Backend contract does not prohibit such values, and secrets can be changed out of band). Mark blank values as missing before assigning ClientSecret.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/provider/store.go Outdated
- A rotation through PUT or PATCH writes the backend and the row inside
  one transaction holding pg_advisory_xact_lock on the provider id, so
  two concurrent updates cannot pair one request's metadata with the
  other's secret, across replicas.
- A blank value read from the backend is treated as missing.
- A rotation that matches no active row returns ErrProviderNotFound and
  the handler answers 404 instead of 500.

Verified end to end against a real Postgres with the GCP backend:
register, PATCH and PUT rotation, PUT without secret preserving it,
empty secret rejected with 400, unknown id leaving no orphan, delete.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Registration consistency, probe cleanup, and compound error handling can leave unusable rows or orphaned secrets.

2 open findings
1 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (1)

In code that hasn't changed since last review

Medium severity Probe cleanup failures leak resources

nexus-broker/​pkg/​vault/​providers/​gcp/​gcp.go:214

Cleanup failures are discarded even though this probe is specifically meant to detect incomplete permissions. For example, if the project-level create grant exists but the prefixed admin grant is missing, the first Set can create a secret container, fail adding its version, and this deferred delete also fails with permission denied; every restart then leaks another random probe resource. Verify delete access before creating the probe and surface cleanup failures, or use a probe naming/cleanup strategy that can recover leftovers.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/provider/store.go Outdated
Comment thread nexus-broker/pkg/handlers/providers.go Outdated
- With an external backend, registration chooses the id, inserts the row
  inside a transaction, writes the secret, and commits only then. No
  reader sees a row without its secret, a crash in between leaves no
  row, and a failed commit removes the secret it had written.
- Handlers check backend failures before not-found, so a rotation that
  missed its row and also failed to clean up reads as retryable.
- The startup probe reports a cleanup failure with the probe secret id
  instead of discarding it.

Verified against a real Postgres: register, read, delete.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An ambiguous database commit failure can delete a successfully committed provider’s external secret.

1 open finding
2 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (2)

In code that hasn't changed since last review

Low severity Document secret rotation or deletion before re-registering

docs/​guides/​provider-secret-backends.md:33

An active missing-secret provider cannot be re-registered directly because RegisterProfile rejects duplicate names. Document the actual recovery path: rotate the secret with PATCH/PUT, or delete the existing provider before registering it again.

Low severity Document supported recovery instead of re-registering active providers

nexus-broker/​pkg/​provider/​health.go:101

The suggested re-register action fails while this active row exists because registration rejects duplicate provider names. Point users to the supported in-place recovery (PATCH a new client_secret) or explicitly tell them to delete before re-registering.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/provider/store.go
A commit error may mean the row was committed and only the
acknowledgement was lost. The secret is dropped only after the database
confirms the row is absent; if it exists or cannot be checked, it is
kept. Health and docs now name the supported recovery for a missing
secret: PATCH a new client_secret, or delete and register again.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

External registration accepts unusable whitespace secrets, and rotations can expose new secrets with stale metadata to concurrent readers.

0 open findings

1 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (2)

In code that hasn't changed since last review

Medium severity Reject whitespace-only client secrets during registration

nexus-broker/​pkg/​provider/​store.go:362

Registration rejects only the exact empty string before this branch, while reads treat any whitespace-only backend value as missing. A client_secret such as " " therefore returns success, commits a NULL column, and becomes unusable on the next read. Reject the same trimmed-empty values here that rotation already rejects.

Medium severity Prevent inconsistent reads during credential rotation

nexus-broker/​pkg/​provider/​store.go:528

The advisory lock serializes writers, but GetProfile/GetAllProfiles do not acquire it. Since putSecret publishes the new latest version before this transaction updates fields such as client_id and auth_header, an OAuth request can read the new secret together with the old metadata during a PUT/PATCH. Use an immutable secret version/key referenced atomically by the database row, or make reads participate in the same lock across row loading and secret resolution; otherwise credential rotation is not read-consistent.

🧠 Review effort: Balanced

Reads already treat a blank backend value as missing; registration now
rejects the same trimmed-empty values rotation does.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:41
@sangalo20

Copy link
Copy Markdown
Contributor Author

Review status after fourteen Copilot passes: every thread is resolved with a code change and verified against a real Postgres plus the live project. One summary note is deliberately not addressed here: read consistency during a rotation (new secret visible with previous row metadata for the milliseconds between the backend write and the row update). That needs an immutable version reference on the row and a migration, and is tracked as #114.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Ambiguous registration commits can still leave a committed provider row after its external secret has been deleted.

1 open finding
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (2)

In code that hasn't changed since last review

Medium severity Secret Manager lifecycle lacks deterministic test coverage

nexus-broker/​pkg/​vault/​providers/​gcp/​gcp.go:123

The core Secret Manager lifecycle is not exercised by an always-run test: the unit test only covers version-name comparison, while TestBackend_RoundTrip skips whenever cloud credentials are absent. Regressions in create/AlreadyExists handling, rotation, partial failures, and version retirement can therefore reach production unnoticed. Wrap the client calls behind a small interface/fake and add deterministic unit coverage for these paths.

Low severity Internal backend recovery leaves orphaned GCP secrets

docs/​guides/​provider-secret-backends.md:38

This recovery guidance omits that PATCH under the internal backend only repopulates PostgreSQL; it cannot delete the existing GCP secret. Deleting the provider while internal is active also leaves that GCP secret behind. Document the required Secret Manager cleanup (or a switch-back procedure performed while the GCP backend is still configured) so operators do not retain a duplicate credential indefinitely.

🧠 Review effort: Balanced

Comment thread nexus-broker/pkg/provider/store.go Outdated
An existence check right after a failed commit can run while the commit
is still completing and see no row. The secret is now kept on any commit
error and the error names it, so an operator can remove it if the row
turns out to be absent. Document Secret Manager cleanup when recovering
under the internal backend.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Metadata reads unnecessarily depend on Secret Manager, and the Terraform role grants permissions beyond those required.

1 open finding
1 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (2)

In code that hasn't changed since last review

Medium severity Avoid secret resolution for metadata-only provider operations

nexus-broker/​pkg/​provider/​store.go:389

Resolving the secret in every GetProfile call makes metadata-only operations depend on Secret Manager. ProvidersHandler.Get immediately redacts this value, and CreateConsentSpec uses only authorization metadata/client ID, yet either operation now fails during a backend outage and performs an unnecessary secret access. Split metadata loading from credential resolution (or resolve explicitly only in exchange, refresh, revocation, and health paths).

This issue also appears on line 446 of the same file.

Low severity Test fail-closed health behavior when client secret is missing

nexus-broker/​pkg/​provider/​health.go:102

The new fail-closed branch is not exercised by health_test.go. Add a test with ClientSecretMissing: true that asserts an unhealthy result and verifies the token endpoint is never contacted, so a regression cannot silently send an empty secret or report the provider healthy.

🧠 Review effort: Balanced

Comment thread deploy/terraform/gcp/secret-backend/main.tf Outdated
The Terraform module binds a custom role with only get, delete and
version add/access/list/destroy under the prefix condition, instead of
roles/secretmanager.admin, which also carried IAM-policy and metadata
administration. A health test asserts a provider with a missing secret
is unhealthy and the token endpoint is never contacted.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Update existence checks, health isolation, and commit-failure HTTP classification remain incorrect.

0 open findings

1 resolved since last review
Files not reviewed (1)
  • nexus-sdk-ts/package-lock.json: Generated file
Previously missed (4)

In code that hasn't changed since last review

Medium severity Untyped commit failure incorrectly returns HTTP 400

nexus-broker/​pkg/​provider/​store.go:370

This new operational commit failure is untyped, so ProvidersHandler.Register falls through to its default HTTP 400 response. A database commit with an unknown outcome is a server failure, not an invalid registration; returning 400 can prevent retries and leaves clients unable to distinguish the possible committed row/orphaned secret. Add a typed database/commit error and map it to a sanitized 5xx response.

Medium severity PUT ignores missing provider when client_secret is omitted

nexus-broker/​pkg/​provider/​store.go:510

When client_secret is omitted, this external-backend PUT never checks RowsAffected, so an unknown or soft-deleted provider returns success (and the handler emits an update audit event). The same PUT with a secret returns ErrProviderNotFound, making existence semantics depend on whether a secret was supplied. Check the result and return ErrProviderNotFound when no active row matched.

Medium severity PATCH ignores missing provider without secret rotation

nexus-broker/​pkg/​provider/​store.go:659

A PATCH that does not rotate the secret also ignores RowsAffected, so PATCHing an unknown or deleted provider returns 200 and produces a misleading audit event, while a secret-bearing PATCH correctly returns 404. Check the update result and return ErrProviderNotFound when no active row matched.

Medium severity One secret resolution error prevents all health checks

nexus-broker/​pkg/​provider/​store.go:786

resolveSecrets joins operational errors from every provider, and this all-or-nothing return makes HealthWorker.runChecks exit before updating any status. Thus one per-secret permission problem or transient access failure leaves every provider's previous health status stale. Preserve per-profile resolution errors so the worker can mark only affected providers unhealthy and continue checking the rest.

🧠 Review effort: Balanced

@sangalo20
sangalo20 merged commit 8f8815d into main Oct 10, 2026
11 checks passed
@sangalo20
sangalo20 deleted the feat/secret-backend-gcp branch October 11, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants