Skip to content

Add pluggable credential helper source (HTTP endpoint) for ProviderConfig #393

@giepa

Description

@giepa

Problem

Today, every ProviderConfig in provider-sql requires master database credentials to be materialized as a Kubernetes Secret (referenced via spec.credentials.connectionSecretRef). For control planes that broker access to upstream secret stores (AWS Secrets Manager, GCP Secret Manager, Vault, etc.), this means the master password ends up sitting in plaintext in a user-visible namespace, exposed to anyone with namespace-scoped read access — even though the upstream system already enforces stricter access controls.

There is no way today to instruct provider-sql to fetch credentials from somewhere other than a Secret in the cluster.

Proposal

Add a new credential Source value that points provider-sql at a configurable HTTP endpoint. The operator hosts a small "credential helper" — typically a sidecar bound to localhost — that resolves credentials from whatever upstream backend they want (Secrets Manager, Vault, etc.) and returns them over HTTP. Provider-sql never sees the upstream credentials and they never land in a Secret.

API shape

apiVersion: postgresql.sql.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: PostgreSQLConnectionHelper
    connectionHelperConfig:
      endpoint: http://localhost:8080/credentials
  sslMode: require

Mirrored for mysql.sql.crossplane.io and mssql.sql.crossplane.io with MySQLConnectionHelper / MSSQLConnectionHelper to match the existing per-engine Source naming convention.

Helper response contract

The helper returns JSON whose keys map directly to the existing xpv1.ResourceCredentialsSecret*Key constants the connection clients already consume:

{
  "endpoint": "db.example.internal",
  "port": "5432",
  "username": "master",
  "password": "..."
}

This keeps pkg/clients/{postgresql,mysql,mssql}.New() unchanged — they continue to receive a flat map[string][]byte of credentials and don't need to know whether the source was a Secret or a helper.

v1 scope (kept deliberately small)

  • Localhost sidecar only — no auth on the helper endpoint in v1; security boundary is the pod network namespace
  • All three engines (postgres, mysql, mssql), cluster + namespaced ProviderConfig variants
  • Additive only — no changes to the existing *ConnectionSecret source; existing users unaffected
  • No new dependenciesnet/http + encoding/json from stdlib are sufficient

Auth on the helper endpoint (bearer token, mTLS) and non-localhost endpoints are deliberately out of scope for v1, but the API shape leaves room to add them compatibly later (e.g. optional fields on ConnectionHelperConfig).

Implementation footprint

  • API types: apis/{namespaced,cluster}/{postgresql,mysql,mssql}/v1alpha1/provider_types.go — new enum const + optional ConnectionHelperConfig field on ProviderCredentials
  • Connectors: each pkg/controller/.../reconciler.go Connect() gains a switch on pc.Spec.Credentials.Source with a new branch that calls the HTTP helper client
  • New file: pkg/clients/credentialhelper/http.go — HTTP GET + JSON decode → map[string][]byte
  • Tests: httptest.Server-based fixtures in the existing reconciler test suites

No refactoring of existing code is required.

Prior art / related work

Questions for maintainers before I open a PR

  1. PR granularity — would you prefer one PR per engine (postgres first, mysql/mssql follow), or a single PR covering all three? The reconciler change is mechanical but replicated across ~24 files.
  2. Naming — per-engine *ConnectionHelper constants matches the existing *ConnectionSecret convention, but a single shared ConnectionHelper value across all three engines is also defensible. Preference?
  3. Helper response shape — is JSON-with-stringly-typed-fields acceptable, or would you prefer a versioned content-type (e.g. application/vnd.crossplane.provider-sql.credentials.v1+json) from day one?
  4. Coordination with feat: Add AWS IAM database authentication support for MySQL and PostgreSQL #347 — is there an active implementer there I should sync with?

Happy to iterate on the design here before any code lands. If reception is positive I'll open a draft PR scoped per your preference above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions