From c81d8bca81fedb58f89165a1ba9ccffe37bc9d64 Mon Sep 17 00:00:00 2001 From: michielvha Date: Sat, 20 Jun 2026 02:53:33 +0200 Subject: [PATCH 1/6] feat: add AWS IAM authentication client package Add pkg/clients/awsiam: generates short-lived RDS IAM auth tokens and injects them as the database password, with region resolution and a test seam (injectable TokenBuilder) so the logic is unit-tested without AWS credentials or network. Part of #347. Signed-off-by: michielvha --- go.mod | 3 + go.sum | 6 + pkg/clients/awsiam/awsiam.go | 93 +++++++++++++++ pkg/clients/awsiam/awsiam_test.go | 182 ++++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 pkg/clients/awsiam/awsiam.go create mode 100644 pkg/clients/awsiam/awsiam_test.go diff --git a/go.mod b/go.mod index f5047592..8262c0fd 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.26.1 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/aws/aws-sdk-go-v2 v1.42.0 + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.29 github.com/crossplane/crossplane-runtime/v2 v2.2.1 github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec github.com/crossplane/upjet/v2 v2.2.0 @@ -28,6 +30,7 @@ require ( github.com/antchfx/htmlquery v1.2.4 // indirect github.com/antchfx/xpath v1.3.6 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aws/smithy-go v1.27.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect diff --git a/go.sum b/go.sum index 12632af9..3e1c8c0a 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,12 @@ github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/ github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.29 h1:1Hbcvm9a/7hBCAM5y5SAvSKyFUsULrMgmS+XBx32u68= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.29/go.mod h1:qIWWBwh4Wp7HU4E8AkMtu9pqHJ6DxmqLFX7zV8ZX4lM= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= diff --git a/pkg/clients/awsiam/awsiam.go b/pkg/clients/awsiam/awsiam.go new file mode 100644 index 00000000..727f9d26 --- /dev/null +++ b/pkg/clients/awsiam/awsiam.go @@ -0,0 +1,93 @@ +/* +Copyright 2024 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package awsiam generates short-lived AWS RDS IAM authentication tokens and +// injects them in place of a static database password. It lets provider-sql +// connect to Aurora/RDS using an IAM identity (EKS Pod Identity or IRSA) +// instead of a password stored in a Kubernetes Secret. +package awsiam + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/rds/auth" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/pkg/errors" +) + +// regionKey is the optional connection-secret key used to resolve the AWS +// region when it is not set on the ProviderConfig. +const regionKey = "region" + +const ( + errMissingConnDetails = "connection secret must contain endpoint, port and username for AWS IAM authentication" + errNoRegion = "AWS region could not be resolved: set spec.credentials.region, a \"region\" key in the connection secret, or the controller's AWS region (e.g. AWS_REGION)" + errBuildToken = "cannot generate AWS RDS IAM authentication token" +) + +// TokenBuilder has the same signature as auth.BuildAuthToken from the AWS SDK. +// InjectToken takes it as a parameter so production code passes the real +// auth.BuildAuthToken while tests pass a stub that returns a fixed token +// without calling AWS. +type TokenBuilder func(ctx context.Context, endpoint, region, dbUser string, + creds aws.CredentialsProvider, optFns ...func(*auth.BuildAuthTokenOptions)) (string, error) + +// ResolveRegion picks the AWS region to sign the token with, in priority order: +// the ProviderConfig field, then a "region" key in the connection secret, then +// the region the AWS SDK discovered from the environment (cfgRegion). The result +// may be empty if none of the three is set; callers must treat that as an error, +// because the SDK does not reject an empty region at token-generation time. +func ResolveRegion(specRegion *string, creds map[string][]byte, cfgRegion string) string { + if specRegion != nil && *specRegion != "" { + return *specRegion + } + if r := string(creds[regionKey]); r != "" { + return r + } + return cfgRegion +} + +// InjectToken generates an RDS IAM authentication token and writes it into creds +// as the password. It reads the endpoint, port and username already present in +// creds (populated from the connection secret) and combines endpoint and port +// into the "host:port" form the AWS SDK requires. awsCreds are the AWS +// credentials used to sign the token; build is the token generator +// (auth.BuildAuthToken in production). +// +// On success the password entry of creds holds the token; the database client +// then uses it exactly as it would a static password. +func InjectToken(ctx context.Context, creds map[string][]byte, region string, + awsCreds aws.CredentialsProvider, build TokenBuilder) error { + endpoint := string(creds[xpv1.ResourceCredentialsSecretEndpointKey]) + port := string(creds[xpv1.ResourceCredentialsSecretPortKey]) + username := string(creds[xpv1.ResourceCredentialsSecretUserKey]) + + if endpoint == "" || port == "" || username == "" { + return errors.New(errMissingConnDetails) + } + if region == "" { + return errors.New(errNoRegion) + } + + token, err := build(ctx, endpoint+":"+port, region, username, awsCreds) + if err != nil { + return errors.Wrap(err, errBuildToken) + } + + creds[xpv1.ResourceCredentialsSecretPasswordKey] = []byte(token) + return nil +} diff --git a/pkg/clients/awsiam/awsiam_test.go b/pkg/clients/awsiam/awsiam_test.go new file mode 100644 index 00000000..23285ff9 --- /dev/null +++ b/pkg/clients/awsiam/awsiam_test.go @@ -0,0 +1,182 @@ +/* +Copyright 2024 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awsiam + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/rds/auth" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" +) + +// builderCall records what a stub TokenBuilder was invoked with, so a test can +// assert on it afterwards. +type builderCall struct { + endpoint string + region string + dbUser string + called bool +} + +// recordingBuilder returns a TokenBuilder that records its inputs and returns +// the supplied token/err. This is the "fake AWS": it never calls AWS, so the +// tests are deterministic and need no credentials or network. +func recordingBuilder(token string, err error, rec *builderCall) TokenBuilder { + return func(_ context.Context, endpoint, region, dbUser string, _ aws.CredentialsProvider, _ ...func(*auth.BuildAuthTokenOptions)) (string, error) { + rec.endpoint, rec.region, rec.dbUser, rec.called = endpoint, region, dbUser, true + return token, err + } +} + +func TestResolveRegion(t *testing.T) { + ptr := func(s string) *string { return &s } + + cases := map[string]struct { + specRegion *string + creds map[string][]byte + cfgRegion string + want string + }{ + "SpecFieldWins": { + specRegion: ptr("eu-west-1"), + creds: map[string][]byte{regionKey: []byte("us-east-1")}, + cfgRegion: "ap-south-1", + want: "eu-west-1", + }, + "SecretWhenSpecNil": { + specRegion: nil, + creds: map[string][]byte{regionKey: []byte("us-east-1")}, + cfgRegion: "ap-south-1", + want: "us-east-1", + }, + "SecretWhenSpecEmpty": { + specRegion: ptr(""), + creds: map[string][]byte{regionKey: []byte("us-east-1")}, + cfgRegion: "ap-south-1", + want: "us-east-1", + }, + "ConfigWhenNothingElse": { + specRegion: nil, + creds: nil, + cfgRegion: "ap-south-1", + want: "ap-south-1", + }, + "EmptyWhenAllEmpty": { + specRegion: nil, + creds: nil, + cfgRegion: "", + want: "", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + if got := ResolveRegion(tc.specRegion, tc.creds, tc.cfgRegion); got != tc.want { + t.Errorf("ResolveRegion(...): want %q, got %q", tc.want, got) + } + }) + } +} + +func TestInjectToken(t *testing.T) { + ctx := context.Background() + errBoom := errors.New("boom") + + // The stub builder ignores the credentials argument, so any value works. + var noCreds aws.CredentialsProvider + + // fullCreds is a connection-secret map with everything InjectToken needs. + fullCreds := func() map[string][]byte { + return map[string][]byte{ + xpv1.ResourceCredentialsSecretEndpointKey: []byte("db.example.rds.amazonaws.com"), + xpv1.ResourceCredentialsSecretPortKey: []byte("5432"), + xpv1.ResourceCredentialsSecretUserKey: []byte("crossplane_admin"), + } + } + + t.Run("Success", func(t *testing.T) { + creds := fullCreds() + rec := &builderCall{} + + err := InjectToken(ctx, creds, "eu-west-1", noCreds, recordingBuilder("FAKE_TOKEN", nil, rec)) + if err != nil { + t.Fatalf("InjectToken(...): unexpected error: %v", err) + } + + // The token must be written into the password slot. + if got := string(creds[xpv1.ResourceCredentialsSecretPasswordKey]); got != "FAKE_TOKEN" { + t.Errorf("password: want %q, got %q", "FAKE_TOKEN", got) + } + // The builder must have received endpoint as host:port, plus region and user. + if rec.endpoint != "db.example.rds.amazonaws.com:5432" { + t.Errorf("endpoint passed to builder: want host:port, got %q", rec.endpoint) + } + if rec.region != "eu-west-1" { + t.Errorf("region passed to builder: want %q, got %q", "eu-west-1", rec.region) + } + if rec.dbUser != "crossplane_admin" { + t.Errorf("dbUser passed to builder: want %q, got %q", "crossplane_admin", rec.dbUser) + } + }) + + t.Run("MissingUsername", func(t *testing.T) { + creds := fullCreds() + delete(creds, xpv1.ResourceCredentialsSecretUserKey) + rec := &builderCall{} + + err := InjectToken(ctx, creds, "eu-west-1", noCreds, recordingBuilder("x", nil, rec)) + if diff := cmp.Diff(errors.New(errMissingConnDetails), err, test.EquateErrors()); diff != "" { + t.Errorf("InjectToken(...): -want error, +got error:\n%s", diff) + } + if rec.called { + t.Error("builder must not be called when connection details are incomplete") + } + }) + + t.Run("EmptyRegion", func(t *testing.T) { + creds := fullCreds() + rec := &builderCall{} + + err := InjectToken(ctx, creds, "", noCreds, recordingBuilder("x", nil, rec)) + if diff := cmp.Diff(errors.New(errNoRegion), err, test.EquateErrors()); diff != "" { + t.Errorf("InjectToken(...): -want error, +got error:\n%s", diff) + } + if rec.called { + t.Error("builder must not be called when region is empty") + } + }) + + t.Run("BuilderError", func(t *testing.T) { + creds := fullCreds() + rec := &builderCall{} + + err := InjectToken(ctx, creds, "eu-west-1", noCreds, recordingBuilder("", errBoom, rec)) + if diff := cmp.Diff(errors.Wrap(errBoom, errBuildToken), err, test.EquateErrors()); diff != "" { + t.Errorf("InjectToken(...): -want error, +got error:\n%s", diff) + } + // The password must remain unset when token generation fails. + if _, ok := creds[xpv1.ResourceCredentialsSecretPasswordKey]; ok { + t.Error("password must not be set when token generation fails") + } + }) +} From 7343053c5617e2f1618e776ec22b5e5732790fa4 Mon Sep 17 00:00:00 2001 From: michielvha Date: Sat, 20 Jun 2026 03:28:19 +0200 Subject: [PATCH 2/6] feat: add AWSIAMAuth credential source to API types Add the AWSIAMAuth credential source value and an optional Region field to the MySQL and PostgreSQL ProviderConfig (and namespaced ClusterProviderConfig) credential types, for both cluster and namespaced API groups. Regenerate deepcopy and CRDs. Part of #347. Signed-off-by: michielvha --- apis/cluster/mysql/v1alpha1/provider_types.go | 13 ++++++++++++- .../cluster/mysql/v1alpha1/zz_generated.deepcopy.go | 5 +++++ apis/cluster/postgresql/v1alpha1/provider_types.go | 13 ++++++++++++- .../postgresql/v1alpha1/zz_generated.deepcopy.go | 5 +++++ .../mysql/v1alpha1/cluster_provider_types.go | 8 +++++++- apis/namespaced/mysql/v1alpha1/provider_types.go | 13 ++++++++++++- .../mysql/v1alpha1/zz_generated.deepcopy.go | 10 ++++++++++ .../postgresql/v1alpha1/cluster_provider_types.go | 8 +++++++- .../postgresql/v1alpha1/provider_types.go | 13 ++++++++++++- .../postgresql/v1alpha1/zz_generated.deepcopy.go | 10 ++++++++++ .../mysql.sql.crossplane.io_providerconfigs.yaml | 7 +++++++ ....sql.m.crossplane.io_clusterproviderconfigs.yaml | 7 +++++++ .../mysql.sql.m.crossplane.io_providerconfigs.yaml | 7 +++++++ ...ostgresql.sql.crossplane.io_providerconfigs.yaml | 7 +++++++ ....sql.m.crossplane.io_clusterproviderconfigs.yaml | 7 +++++++ ...tgresql.sql.m.crossplane.io_providerconfigs.yaml | 7 +++++++ 16 files changed, 134 insertions(+), 6 deletions(-) diff --git a/apis/cluster/mysql/v1alpha1/provider_types.go b/apis/cluster/mysql/v1alpha1/provider_types.go index 4762126b..1d6fac12 100644 --- a/apis/cluster/mysql/v1alpha1/provider_types.go +++ b/apis/cluster/mysql/v1alpha1/provider_types.go @@ -59,12 +59,17 @@ const ( // should acquire credentials from a connection secret written by a managed // resource that represents a MySQL server. CredentialsSourceMySQLConnectionSecret xpv1.CredentialsSource = "MySQLConnectionSecret" + + // CredentialsSourceAWSIAMAuth indicates that the provider should + // authenticate to an AWS RDS/Aurora instance using an IAM authentication + // token generated at connection time, instead of a static password. + CredentialsSourceAWSIAMAuth xpv1.CredentialsSource = "AWSIAMAuth" ) // ProviderCredentials required to authenticate. type ProviderCredentials struct { // Source of the provider credentials. - // +kubebuilder:validation:Enum=MySQLConnectionSecret + // +kubebuilder:validation:Enum=MySQLConnectionSecret;AWSIAMAuth Source xpv1.CredentialsSource `json:"source"` // A CredentialsSecretRef is a reference to a MySQL connection secret @@ -77,6 +82,12 @@ type ProviderCredentials struct { // standard Crossplane keys are used: "endpoint", "port", "username", "password". // +optional SecretKeyMapping *SecretKeyMapping `json:"secretKeyMapping,omitempty"` + + // Region is the AWS region used to generate the IAM authentication token + // when source is AWSIAMAuth. When unset it falls back to a "region" key in + // the connection secret, then to the controller's AWS environment. + // +optional + Region *string `json:"region,omitempty"` } // SecretKeyMapping allows overriding the default secret key names used to diff --git a/apis/cluster/mysql/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/mysql/v1alpha1/zz_generated.deepcopy.go index fff8a6cc..a3b8ed31 100644 --- a/apis/cluster/mysql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/cluster/mysql/v1alpha1/zz_generated.deepcopy.go @@ -499,6 +499,11 @@ func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { *out = new(SecretKeyMapping) **out = **in } + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. diff --git a/apis/cluster/postgresql/v1alpha1/provider_types.go b/apis/cluster/postgresql/v1alpha1/provider_types.go index 66cf03dd..b301c31f 100644 --- a/apis/cluster/postgresql/v1alpha1/provider_types.go +++ b/apis/cluster/postgresql/v1alpha1/provider_types.go @@ -43,12 +43,17 @@ const ( // should acquire credentials from a connection secret written by a managed // resource that represents a PostgreSQL server. CredentialsSourcePostgreSQLConnectionSecret xpv1.CredentialsSource = "PostgreSQLConnectionSecret" + + // CredentialsSourceAWSIAMAuth indicates that the provider should + // authenticate to an AWS RDS/Aurora instance using an IAM authentication + // token generated at connection time, instead of a static password. + CredentialsSourceAWSIAMAuth xpv1.CredentialsSource = "AWSIAMAuth" ) // ProviderCredentials required to authenticate. type ProviderCredentials struct { // Source of the provider credentials. - // +kubebuilder:validation:Enum=PostgreSQLConnectionSecret + // +kubebuilder:validation:Enum=PostgreSQLConnectionSecret;AWSIAMAuth Source xpv1.CredentialsSource `json:"source"` // A CredentialsSecretRef is a reference to a PostgreSQL connection secret @@ -61,6 +66,12 @@ type ProviderCredentials struct { // standard Crossplane keys are used: "endpoint", "port", "username", "password". // +optional SecretKeyMapping *SecretKeyMapping `json:"secretKeyMapping,omitempty"` + + // Region is the AWS region used to generate the IAM authentication token + // when source is AWSIAMAuth. When unset it falls back to a "region" key in + // the connection secret, then to the controller's AWS environment. + // +optional + Region *string `json:"region,omitempty"` } // SecretKeyMapping allows overriding the default secret key names used to diff --git a/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go index dedd46c2..30fce570 100644 --- a/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -864,6 +864,11 @@ func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { *out = new(SecretKeyMapping) **out = **in } + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. diff --git a/apis/namespaced/mysql/v1alpha1/cluster_provider_types.go b/apis/namespaced/mysql/v1alpha1/cluster_provider_types.go index ee61542a..9f356041 100644 --- a/apis/namespaced/mysql/v1alpha1/cluster_provider_types.go +++ b/apis/namespaced/mysql/v1alpha1/cluster_provider_types.go @@ -45,7 +45,7 @@ type ClusterProviderConfigSpec struct { // ClusterProviderCredentials required to authenticate. type ClusterProviderCredentials struct { // Source of the provider credentials. - // +kubebuilder:validation:Enum=MySQLConnectionSecret + // +kubebuilder:validation:Enum=MySQLConnectionSecret;AWSIAMAuth Source MySQLConnectionSecretSource `json:"source"` // A CredentialsSecretRef is a reference to a MySQL connection secret @@ -58,6 +58,12 @@ type ClusterProviderCredentials struct { // standard Crossplane keys are used: "endpoint", "port", "username", "password". // +optional SecretKeyMapping *SecretKeyMapping `json:"secretKeyMapping,omitempty"` + + // Region is the AWS region used to generate the IAM authentication token + // when source is AWSIAMAuth. When unset it falls back to a "region" key in + // the connection secret, then to the controller's AWS environment. + // +optional + Region *string `json:"region,omitempty"` } // A ClusterProviderConfigStatus reflects the observed state of a ClusterProviderConfig. diff --git a/apis/namespaced/mysql/v1alpha1/provider_types.go b/apis/namespaced/mysql/v1alpha1/provider_types.go index 280e2fe2..2ddf82f1 100644 --- a/apis/namespaced/mysql/v1alpha1/provider_types.go +++ b/apis/namespaced/mysql/v1alpha1/provider_types.go @@ -62,12 +62,17 @@ const ( // should acquire credentials from a connection secret written by a managed // resource that represents a MySQL server. CredentialsSourceMySQLConnectionSecret MySQLConnectionSecretSource = "MySQLConnectionSecret" + + // CredentialsSourceAWSIAMAuth indicates that the provider should + // authenticate to an AWS RDS/Aurora instance using an IAM authentication + // token generated at connection time, instead of a static password. + CredentialsSourceAWSIAMAuth MySQLConnectionSecretSource = "AWSIAMAuth" ) // ProviderCredentials required to authenticate. type ProviderCredentials struct { // Source of the provider credentials. - // +kubebuilder:validation:Enum=MySQLConnectionSecret + // +kubebuilder:validation:Enum=MySQLConnectionSecret;AWSIAMAuth Source MySQLConnectionSecretSource `json:"source"` // A CredentialsSecretRef is a reference to a MySQL connection secret @@ -80,6 +85,12 @@ type ProviderCredentials struct { // standard Crossplane keys are used: "endpoint", "port", "username", "password". // +optional SecretKeyMapping *SecretKeyMapping `json:"secretKeyMapping,omitempty"` + + // Region is the AWS region used to generate the IAM authentication token + // when source is AWSIAMAuth. When unset it falls back to a "region" key in + // the connection secret, then to the controller's AWS environment. + // +optional + Region *string `json:"region,omitempty"` } // SecretKeyMapping allows overriding the default secret key names used to diff --git a/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go index fae51bbd..97529333 100644 --- a/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/mysql/v1alpha1/zz_generated.deepcopy.go @@ -123,6 +123,11 @@ func (in *ClusterProviderCredentials) DeepCopyInto(out *ClusterProviderCredentia *out = new(SecretKeyMapping) **out = **in } + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProviderCredentials. @@ -617,6 +622,11 @@ func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { *out = new(SecretKeyMapping) **out = **in } + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. diff --git a/apis/namespaced/postgresql/v1alpha1/cluster_provider_types.go b/apis/namespaced/postgresql/v1alpha1/cluster_provider_types.go index a2f0db07..74feab7d 100644 --- a/apis/namespaced/postgresql/v1alpha1/cluster_provider_types.go +++ b/apis/namespaced/postgresql/v1alpha1/cluster_provider_types.go @@ -41,7 +41,7 @@ type ClusterProviderConfigSpec struct { // ClusterProviderCredentials required to authenticate. type ClusterProviderCredentials struct { // Source of the provider credentials. - // +kubebuilder:validation:Enum=PostgreSQLConnectionSecret + // +kubebuilder:validation:Enum=PostgreSQLConnectionSecret;AWSIAMAuth Source PostgreSQLConnectionSource `json:"source"` // A CredentialsSecretRef is a reference to a PostgreSQL connection secret @@ -54,6 +54,12 @@ type ClusterProviderCredentials struct { // standard Crossplane keys are used: "endpoint", "port", "username", "password". // +optional SecretKeyMapping *SecretKeyMapping `json:"secretKeyMapping,omitempty"` + + // Region is the AWS region used to generate the IAM authentication token + // when source is AWSIAMAuth. When unset it falls back to a "region" key in + // the connection secret, then to the controller's AWS environment. + // +optional + Region *string `json:"region,omitempty"` } // A ClusterProviderConfigStatus reflects the observed state of a ClusterProviderConfig. diff --git a/apis/namespaced/postgresql/v1alpha1/provider_types.go b/apis/namespaced/postgresql/v1alpha1/provider_types.go index e1597550..2ab3b129 100644 --- a/apis/namespaced/postgresql/v1alpha1/provider_types.go +++ b/apis/namespaced/postgresql/v1alpha1/provider_types.go @@ -46,12 +46,17 @@ const ( // should acquire credentials from a connection secret written by a managed // resource that represents a PostgreSQL server. CredentialsSourcePostgreSQLConnectionSecret PostgreSQLConnectionSource = "PostgreSQLConnectionSecret" + + // CredentialsSourceAWSIAMAuth indicates that the provider should + // authenticate to an AWS RDS/Aurora instance using an IAM authentication + // token generated at connection time, instead of a static password. + CredentialsSourceAWSIAMAuth PostgreSQLConnectionSource = "AWSIAMAuth" ) // ProviderCredentials required to authenticate. type ProviderCredentials struct { // Source of the provider credentials. - // +kubebuilder:validation:Enum=PostgreSQLConnectionSecret + // +kubebuilder:validation:Enum=PostgreSQLConnectionSecret;AWSIAMAuth Source PostgreSQLConnectionSource `json:"source"` // A CredentialsSecretRef is a reference to a PostgreSQL connection secret @@ -64,6 +69,12 @@ type ProviderCredentials struct { // standard Crossplane keys are used: "endpoint", "port", "username", "password". // +optional SecretKeyMapping *SecretKeyMapping `json:"secretKeyMapping,omitempty"` + + // Region is the AWS region used to generate the IAM authentication token + // when source is AWSIAMAuth. When unset it falls back to a "region" key in + // the connection secret, then to the controller's AWS environment. + // +optional + Region *string `json:"region,omitempty"` } // SecretKeyMapping allows overriding the default secret key names used to diff --git a/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go index 7a62990c..537165dd 100644 --- a/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -118,6 +118,11 @@ func (in *ClusterProviderCredentials) DeepCopyInto(out *ClusterProviderCredentia *out = new(SecretKeyMapping) **out = **in } + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProviderCredentials. @@ -977,6 +982,11 @@ func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { *out = new(SecretKeyMapping) **out = **in } + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. diff --git a/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml b/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml index e1271ea2..8c9d0b09 100644 --- a/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml +++ b/package/crds/mysql.sql.crossplane.io_providerconfigs.yaml @@ -70,6 +70,12 @@ spec: - name - namespace type: object + region: + description: |- + Region is the AWS region used to generate the IAM authentication token + when source is AWSIAMAuth. When unset it falls back to a "region" key in + the connection secret, then to the controller's AWS environment. + type: string secretKeyMapping: description: |- SecretKeyMapping allows overriding the default secret key names used @@ -97,6 +103,7 @@ spec: description: Source of the provider credentials. enum: - MySQLConnectionSecret + - AWSIAMAuth type: string required: - source diff --git a/package/crds/mysql.sql.m.crossplane.io_clusterproviderconfigs.yaml b/package/crds/mysql.sql.m.crossplane.io_clusterproviderconfigs.yaml index 4d878d84..1aea8cd8 100644 --- a/package/crds/mysql.sql.m.crossplane.io_clusterproviderconfigs.yaml +++ b/package/crds/mysql.sql.m.crossplane.io_clusterproviderconfigs.yaml @@ -71,6 +71,12 @@ spec: - name - namespace type: object + region: + description: |- + Region is the AWS region used to generate the IAM authentication token + when source is AWSIAMAuth. When unset it falls back to a "region" key in + the connection secret, then to the controller's AWS environment. + type: string secretKeyMapping: description: |- SecretKeyMapping allows overriding the default secret key names used @@ -98,6 +104,7 @@ spec: description: Source of the provider credentials. enum: - MySQLConnectionSecret + - AWSIAMAuth type: string required: - source diff --git a/package/crds/mysql.sql.m.crossplane.io_providerconfigs.yaml b/package/crds/mysql.sql.m.crossplane.io_providerconfigs.yaml index 58d6ee69..3dd867fe 100644 --- a/package/crds/mysql.sql.m.crossplane.io_providerconfigs.yaml +++ b/package/crds/mysql.sql.m.crossplane.io_providerconfigs.yaml @@ -66,6 +66,12 @@ spec: required: - name type: object + region: + description: |- + Region is the AWS region used to generate the IAM authentication token + when source is AWSIAMAuth. When unset it falls back to a "region" key in + the connection secret, then to the controller's AWS environment. + type: string secretKeyMapping: description: |- SecretKeyMapping allows overriding the default secret key names used @@ -93,6 +99,7 @@ spec: description: Source of the provider credentials. enum: - MySQLConnectionSecret + - AWSIAMAuth type: string required: - source diff --git a/package/crds/postgresql.sql.crossplane.io_providerconfigs.yaml b/package/crds/postgresql.sql.crossplane.io_providerconfigs.yaml index 64c3abfe..e1431cb4 100644 --- a/package/crds/postgresql.sql.crossplane.io_providerconfigs.yaml +++ b/package/crds/postgresql.sql.crossplane.io_providerconfigs.yaml @@ -70,6 +70,12 @@ spec: - name - namespace type: object + region: + description: |- + Region is the AWS region used to generate the IAM authentication token + when source is AWSIAMAuth. When unset it falls back to a "region" key in + the connection secret, then to the controller's AWS environment. + type: string secretKeyMapping: description: |- SecretKeyMapping allows overriding the default secret key names used @@ -97,6 +103,7 @@ spec: description: Source of the provider credentials. enum: - PostgreSQLConnectionSecret + - AWSIAMAuth type: string required: - source diff --git a/package/crds/postgresql.sql.m.crossplane.io_clusterproviderconfigs.yaml b/package/crds/postgresql.sql.m.crossplane.io_clusterproviderconfigs.yaml index 5b2484f3..732d6ef1 100644 --- a/package/crds/postgresql.sql.m.crossplane.io_clusterproviderconfigs.yaml +++ b/package/crds/postgresql.sql.m.crossplane.io_clusterproviderconfigs.yaml @@ -71,6 +71,12 @@ spec: - name - namespace type: object + region: + description: |- + Region is the AWS region used to generate the IAM authentication token + when source is AWSIAMAuth. When unset it falls back to a "region" key in + the connection secret, then to the controller's AWS environment. + type: string secretKeyMapping: description: |- SecretKeyMapping allows overriding the default secret key names used @@ -98,6 +104,7 @@ spec: description: Source of the provider credentials. enum: - PostgreSQLConnectionSecret + - AWSIAMAuth type: string required: - source diff --git a/package/crds/postgresql.sql.m.crossplane.io_providerconfigs.yaml b/package/crds/postgresql.sql.m.crossplane.io_providerconfigs.yaml index 0996d2d3..52d61807 100644 --- a/package/crds/postgresql.sql.m.crossplane.io_providerconfigs.yaml +++ b/package/crds/postgresql.sql.m.crossplane.io_providerconfigs.yaml @@ -66,6 +66,12 @@ spec: required: - name type: object + region: + description: |- + Region is the AWS region used to generate the IAM authentication token + when source is AWSIAMAuth. When unset it falls back to a "region" key in + the connection secret, then to the controller's AWS environment. + type: string secretKeyMapping: description: |- SecretKeyMapping allows overriding the default secret key names used @@ -93,6 +99,7 @@ spec: description: Source of the provider credentials. enum: - PostgreSQLConnectionSecret + - AWSIAMAuth type: string required: - source From 22a26efecdb0004fd4ba7abe72efed2f5e57c757 Mon Sep 17 00:00:00 2001 From: michielvha Date: Sat, 20 Jun 2026 03:33:17 +0200 Subject: [PATCH 3/6] feat(mysql): plumb allowCleartextPasswords flag through the client Add a cleartext parameter to mysql.New/DSN that appends allowCleartextPasswords=true to the DSN when set. AWS RDS IAM authentication needs this because the AWSAuthenticationPlugin uses MySQL's cleartext client plugin, which go-sql-driver refuses by default. This commit only plumbs the flag (all existing callers pass false, so behaviour is unchanged); the IAM reconciler changes that set it follow. Part of #347. Signed-off-by: michielvha --- pkg/clients/mysql/mysql.go | 32 ++++++++----------- pkg/clients/mysql/mysql_test.go | 17 ++++++++-- .../cluster/mysql/database/reconciler.go | 4 +-- .../cluster/mysql/database/reconciler_test.go | 2 +- .../cluster/mysql/grant/reconciler.go | 4 +-- .../cluster/mysql/grant/reconciler_test.go | 2 +- .../cluster/mysql/user/reconciler.go | 4 +-- .../cluster/mysql/user/reconciler_test.go | 2 +- .../namespaced/mysql/database/reconciler.go | 4 +-- .../mysql/database/reconciler_test.go | 2 +- .../namespaced/mysql/grant/reconciler.go | 4 +-- .../namespaced/mysql/grant/reconciler_test.go | 2 +- .../namespaced/mysql/user/reconciler.go | 4 +-- .../namespaced/mysql/user/reconciler_test.go | 2 +- 14 files changed, 47 insertions(+), 38 deletions(-) diff --git a/pkg/clients/mysql/mysql.go b/pkg/clients/mysql/mysql.go index 4ab55902..ccc83734 100644 --- a/pkg/clients/mysql/mysql.go +++ b/pkg/clients/mysql/mysql.go @@ -25,8 +25,10 @@ type mySQLDB struct { tls string } -// New returns a new MySQL database client. -func New(creds map[string][]byte, tls *string, binlog *bool) xsql.DB { +// New returns a new MySQL database client. When cleartext is true the DSN sets +// allowCleartextPasswords=true, which is required for AWS RDS IAM authentication +// (the AWSAuthenticationPlugin uses MySQL's cleartext client plugin). +func New(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB { endpoint := string(creds[xpv1.ResourceCredentialsSecretEndpointKey]) port := string(creds[xpv1.ResourceCredentialsSecretPortKey]) username := string(creds[xpv1.ResourceCredentialsSecretUserKey]) @@ -35,7 +37,7 @@ func New(creds map[string][]byte, tls *string, binlog *bool) xsql.DB { defaultTLS := "preferred" tls = &defaultTLS } - dsn := DSN(username, password, endpoint, port, *tls, binlog) + dsn := DSN(username, password, endpoint, port, *tls, binlog, cleartext) return mySQLDB{ dsn: dsn, @@ -45,26 +47,20 @@ func New(creds map[string][]byte, tls *string, binlog *bool) xsql.DB { } } -// DSN returns the DSN URL -func DSN(username, password, endpoint, port, tls string, binlog *bool) string { +// DSN returns the DSN URL. When cleartext is true, allowCleartextPasswords=true +// is appended (required for AWS RDS IAM authentication). +func DSN(username, password, endpoint, port, tls string, binlog *bool, cleartext bool) string { // Use net/url UserPassword to encode the username and password // This will ensure that any special characters in the username or password // are percent-encoded for use in the user info portion of the DSN URL + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/?tls=%s", username, password, endpoint, port, tls) if binlog != nil { - return fmt.Sprintf("%s:%s@tcp(%s:%s)/?tls=%s&sql_log_bin=%s", - username, - password, - endpoint, - port, - tls, - strconv.FormatBool(*binlog)) + dsn += "&sql_log_bin=" + strconv.FormatBool(*binlog) } - return fmt.Sprintf("%s:%s@tcp(%s:%s)/?tls=%s", - username, - password, - endpoint, - port, - tls) + if cleartext { + dsn += "&allowCleartextPasswords=true" + } + return dsn } // ExecTx is unsupported in MySQL. diff --git a/pkg/clients/mysql/mysql_test.go b/pkg/clients/mysql/mysql_test.go index 13aacdc6..18548eab 100644 --- a/pkg/clients/mysql/mysql_test.go +++ b/pkg/clients/mysql/mysql_test.go @@ -13,7 +13,7 @@ func TestDSNURLEscaping(t *testing.T) { rawPass := "password^" tls := "true" binlog := false - dsn := DSN(user, rawPass, endpoint, port, tls, &binlog) + dsn := DSN(user, rawPass, endpoint, port, tls, &binlog, false) if dsn != fmt.Sprintf("%s:%s@tcp(%s:%s)/?tls=%s&sql_log_bin=%s", user, rawPass, @@ -31,7 +31,7 @@ func TestDSNURLEscapingWithoutBinLog(t *testing.T) { user := "username" rawPass := "password^" tls := "true" - dsn := DSN(user, rawPass, endpoint, port, tls, nil) + dsn := DSN(user, rawPass, endpoint, port, tls, nil, false) if dsn != fmt.Sprintf("%s:%s@tcp(%s:%s)/?tls=%s", user, rawPass, @@ -41,3 +41,16 @@ func TestDSNURLEscapingWithoutBinLog(t *testing.T) { t.Errorf("DSN string did not match expected output with URL encoded") } } + +func TestDSNCleartext(t *testing.T) { + base := "username:password@tcp(endpoint:3306)/?tls=true" + + // cleartext=true appends allowCleartextPasswords=true (required for IAM auth). + if got := DSN("username", "password", "endpoint", "3306", "true", nil, true); got != base+"&allowCleartextPasswords=true" { + t.Errorf("cleartext DSN: want %q, got %q", base+"&allowCleartextPasswords=true", got) + } + // cleartext=false must NOT append it (guards existing non-IAM users). + if got := DSN("username", "password", "endpoint", "3306", "true", nil, false); got != base { + t.Errorf("non-cleartext DSN: want %q, got %q", base, got) + } +} diff --git a/pkg/controller/cluster/mysql/database/reconciler.go b/pkg/controller/cluster/mysql/database/reconciler.go index 86443b59..fcb4c6d8 100644 --- a/pkg/controller/cluster/mysql/database/reconciler.go +++ b/pkg/controller/cluster/mysql/database/reconciler.go @@ -94,7 +94,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } var _ managed.TypedExternalConnector[*v1alpha1.Database] = &connector{} @@ -131,7 +131,7 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Database) (managed } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog)}, nil + return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/cluster/mysql/database/reconciler_test.go b/pkg/controller/cluster/mysql/database/reconciler_test.go index 2928bda1..48f90c36 100644 --- a/pkg/controller/cluster/mysql/database/reconciler_test.go +++ b/pkg/controller/cluster/mysql/database/reconciler_test.go @@ -69,7 +69,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client track func(context.Context, resource.LegacyManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } type args struct { diff --git a/pkg/controller/cluster/mysql/grant/reconciler.go b/pkg/controller/cluster/mysql/grant/reconciler.go index 50546675..35eea9bd 100644 --- a/pkg/controller/cluster/mysql/grant/reconciler.go +++ b/pkg/controller/cluster/mysql/grant/reconciler.go @@ -102,7 +102,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } var _ managed.TypedExternalConnector[*v1alpha1.Grant] = &connector{} @@ -139,7 +139,7 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Grant) (managed.Ty } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog)}, nil + return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/cluster/mysql/grant/reconciler_test.go b/pkg/controller/cluster/mysql/grant/reconciler_test.go index 39d95b3a..6e343523 100644 --- a/pkg/controller/cluster/mysql/grant/reconciler_test.go +++ b/pkg/controller/cluster/mysql/grant/reconciler_test.go @@ -79,7 +79,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client track func(context.Context, resource.LegacyManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } type args struct { diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index 3eba36a6..dbb206da 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -97,7 +97,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } var _ managed.TypedExternalConnector[*v1alpha1.User] = &connector{} @@ -135,7 +135,7 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.User) (managed.Typ secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) return &external{ - db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog), + db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, false), kube: c.kube, }, nil } diff --git a/pkg/controller/cluster/mysql/user/reconciler_test.go b/pkg/controller/cluster/mysql/user/reconciler_test.go index 8d88abd0..916b7528 100644 --- a/pkg/controller/cluster/mysql/user/reconciler_test.go +++ b/pkg/controller/cluster/mysql/user/reconciler_test.go @@ -77,7 +77,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client track func(context.Context, resource.LegacyManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } type args struct { diff --git a/pkg/controller/namespaced/mysql/database/reconciler.go b/pkg/controller/namespaced/mysql/database/reconciler.go index 55d94991..f9df7899 100644 --- a/pkg/controller/namespaced/mysql/database/reconciler.go +++ b/pkg/controller/namespaced/mysql/database/reconciler.go @@ -90,7 +90,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client track func(ctx context.Context, mg resource.ModernManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } var _ managed.TypedExternalConnector[*namespacedv1alpha1.Database] = &connector{} @@ -112,7 +112,7 @@ func (c *connector) Connect(ctx context.Context, mg *namespacedv1alpha1.Database return nil, errors.Wrap(err, errTLSConfig) } - return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog)}, nil + return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/namespaced/mysql/database/reconciler_test.go b/pkg/controller/namespaced/mysql/database/reconciler_test.go index 8d0abd2c..35d6e9b7 100644 --- a/pkg/controller/namespaced/mysql/database/reconciler_test.go +++ b/pkg/controller/namespaced/mysql/database/reconciler_test.go @@ -71,7 +71,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client track func(context.Context, resource.ModernManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } type args struct { diff --git a/pkg/controller/namespaced/mysql/grant/reconciler.go b/pkg/controller/namespaced/mysql/grant/reconciler.go index fd431e85..f76eddfa 100644 --- a/pkg/controller/namespaced/mysql/grant/reconciler.go +++ b/pkg/controller/namespaced/mysql/grant/reconciler.go @@ -99,7 +99,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client track func(ctx context.Context, mg resource.ModernManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } var _ managed.TypedExternalConnector[*namespacedv1alpha1.Grant] = &connector{} @@ -121,7 +121,7 @@ func (c *connector) Connect(ctx context.Context, mg *namespacedv1alpha1.Grant) ( return nil, errors.Wrap(err, errTLSConfig) } - return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog)}, nil + return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/namespaced/mysql/grant/reconciler_test.go b/pkg/controller/namespaced/mysql/grant/reconciler_test.go index 41fb1802..7a89ee32 100644 --- a/pkg/controller/namespaced/mysql/grant/reconciler_test.go +++ b/pkg/controller/namespaced/mysql/grant/reconciler_test.go @@ -80,7 +80,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client track func(context.Context, resource.ModernManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } type args struct { diff --git a/pkg/controller/namespaced/mysql/user/reconciler.go b/pkg/controller/namespaced/mysql/user/reconciler.go index de6f2189..bdc3b64d 100644 --- a/pkg/controller/namespaced/mysql/user/reconciler.go +++ b/pkg/controller/namespaced/mysql/user/reconciler.go @@ -94,7 +94,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client track func(ctx context.Context, mg resource.ModernManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } var _ managed.TypedExternalConnector[*namespacedv1alpha1.User] = &connector{} @@ -117,7 +117,7 @@ func (c *connector) Connect(ctx context.Context, mg *namespacedv1alpha1.User) (m } return &external{ - db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog), + db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, false), kube: c.kube, }, nil } diff --git a/pkg/controller/namespaced/mysql/user/reconciler_test.go b/pkg/controller/namespaced/mysql/user/reconciler_test.go index 23e9cb56..8de79bc0 100644 --- a/pkg/controller/namespaced/mysql/user/reconciler_test.go +++ b/pkg/controller/namespaced/mysql/user/reconciler_test.go @@ -79,7 +79,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client track func(context.Context, resource.ModernManaged) error - newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsql.DB } type args struct { From 5b5970c23381a45ccc37479093d9c231f565cc9c Mon Sep 17 00:00:00 2001 From: michielvha Date: Sat, 20 Jun 2026 03:45:09 +0200 Subject: [PATCH 4/6] feat: inject AWS IAM auth token in cluster reconcilers Add awsiam.Inject (load AWS config, resolve region, generate and inject the IAM token) and call it from each cluster reconciler's Connect() when the credentials source is AWSIAMAuth. For MySQL this also enables the cleartext flag and ensures TLS is on; for PostgreSQL it forces sslmode=require. Certificate verification relies on the operator supplying the RDS CA (e.g. via trust-manager or a mounted secret). Part of #347. Signed-off-by: michielvha --- go.mod | 12 +++++ go.sum | 24 +++++++++ pkg/clients/awsiam/awsiam.go | 15 ++++++ pkg/clients/mysql/mysql.go | 13 +++++ .../cluster/mysql/database/reconciler.go | 21 +++++++- .../cluster/mysql/database/reconciler_test.go | 52 +++++++++++++++++++ .../cluster/mysql/grant/reconciler.go | 21 +++++++- .../cluster/mysql/user/reconciler.go | 21 +++++++- .../cluster/postgresql/database/reconciler.go | 19 ++++++- .../postgresql/database/reconciler_test.go | 49 +++++++++++++++++ .../default_privileges/reconciler.go | 17 +++++- .../postgresql/extension/reconciler.go | 19 ++++++- .../cluster/postgresql/grant/reconciler.go | 17 +++++- .../cluster/postgresql/role/reconciler.go | 17 +++++- .../cluster/postgresql/schema/reconciler.go | 17 +++++- 15 files changed, 324 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 8262c0fd..a737e999 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/aws/aws-sdk-go-v2 v1.42.0 + github.com/aws/aws-sdk-go-v2/config v1.32.25 github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.29 github.com/crossplane/crossplane-runtime/v2 v2.2.1 github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec @@ -30,6 +31,17 @@ require ( github.com/antchfx/htmlquery v1.2.4 // indirect github.com/antchfx/xpath v1.3.6 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect github.com/aws/smithy-go v1.27.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect diff --git a/go.sum b/go.sum index 3e1c8c0a..056f7b93 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,32 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.29 h1:1Hbcvm9a/7hBCAM5y5SAvSKyFUsULrMgmS+XBx32u68= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.29/go.mod h1:qIWWBwh4Wp7HU4E8AkMtu9pqHJ6DxmqLFX7zV8ZX4lM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/pkg/clients/awsiam/awsiam.go b/pkg/clients/awsiam/awsiam.go index 727f9d26..1fc4dad5 100644 --- a/pkg/clients/awsiam/awsiam.go +++ b/pkg/clients/awsiam/awsiam.go @@ -24,6 +24,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/rds/auth" xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" "github.com/pkg/errors" @@ -37,6 +38,7 @@ const ( errMissingConnDetails = "connection secret must contain endpoint, port and username for AWS IAM authentication" errNoRegion = "AWS region could not be resolved: set spec.credentials.region, a \"region\" key in the connection secret, or the controller's AWS region (e.g. AWS_REGION)" errBuildToken = "cannot generate AWS RDS IAM authentication token" + errLoadConfig = "cannot load AWS configuration" ) // TokenBuilder has the same signature as auth.BuildAuthToken from the AWS SDK. @@ -91,3 +93,16 @@ func InjectToken(ctx context.Context, creds map[string][]byte, region string, creds[xpv1.ResourceCredentialsSecretPasswordKey] = []byte(token) return nil } + +// Inject loads AWS configuration from the environment, resolves the region +// (ProviderConfig field > secret "region" key > environment) and injects an RDS +// IAM authentication token into creds as the password. It is the entry point a +// reconciler's Connect() calls when the credentials source is AWS IAM auth. +func Inject(ctx context.Context, specRegion *string, creds map[string][]byte) error { + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return errors.Wrap(err, errLoadConfig) + } + region := ResolveRegion(specRegion, creds, cfg.Region) + return InjectToken(ctx, creds, region, cfg.Credentials, auth.BuildAuthToken) +} diff --git a/pkg/clients/mysql/mysql.go b/pkg/clients/mysql/mysql.go index ccc83734..c6d8f362 100644 --- a/pkg/clients/mysql/mysql.go +++ b/pkg/clients/mysql/mysql.go @@ -47,6 +47,19 @@ func New(creds map[string][]byte, tls *string, binlog *bool, cleartext bool) xsq } } +// EnsureTLS returns a TLS mode that guarantees an encrypted connection, for use +// with AWS IAM authentication. If mode is unset or non-encrypting +// ("preferred"/"false") it returns "skip-verify" (encrypted but unverified); +// otherwise it returns mode unchanged so an operator-configured verifying mode +// ("true"/"custom", backed by the RDS CA) is preserved. +func EnsureTLS(mode *string) *string { + if mode == nil || *mode == "false" || *mode == "preferred" { + skipVerify := "skip-verify" + return &skipVerify + } + return mode +} + // DSN returns the DSN URL. When cleartext is true, allowCleartextPasswords=true // is appended (required for AWS RDS IAM authentication). func DSN(username, password, endpoint, port, tls string, binlog *bool, cleartext bool) string { diff --git a/pkg/controller/cluster/mysql/database/reconciler.go b/pkg/controller/cluster/mysql/database/reconciler.go index fcb4c6d8..d398df59 100644 --- a/pkg/controller/cluster/mysql/database/reconciler.go +++ b/pkg/controller/cluster/mysql/database/reconciler.go @@ -36,6 +36,7 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane-contrib/provider-sql/apis/cluster/mysql/v1alpha1" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/mysql/tls" @@ -48,6 +49,8 @@ const ( errGetSecret = "cannot get credentials Secret" errTLSConfig = "cannot load TLS config" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errSelectDB = "cannot select database" errCreateDB = "cannot create database" errDropDB = "cannot drop database" @@ -91,6 +94,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { Complete(r) } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -131,7 +138,19 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Database) (managed } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil + + cleartext := false + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + cleartext = true + // IAM auth requires TLS. The operator supplies the RDS CA (e.g. via + // trust-manager or a mounted secret); honour a verifying tls mode if set, + // otherwise fall back to an encrypted (unverified) connection. + tlsName = mysql.EnsureTLS(tlsName) + } + return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, cleartext)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/cluster/mysql/database/reconciler_test.go b/pkg/controller/cluster/mysql/database/reconciler_test.go index 48f90c36..1c2a2cf9 100644 --- a/pkg/controller/cluster/mysql/database/reconciler_test.go +++ b/pkg/controller/cluster/mysql/database/reconciler_test.go @@ -66,6 +66,14 @@ func TestConnect(t *testing.T) { errBoom := errors.New("boom") nopUsage := func(ctx context.Context, mg resource.LegacyManaged) error { return nil } + // Stub the AWS IAM token injection so the success case needs no AWS access. + origInject := injectIAM + injectIAM = func(_ context.Context, _ *string, creds map[string][]byte) error { + creds[xpv1.ResourceCredentialsSecretPasswordKey] = []byte("iam-token") + return nil + } + defer func() { injectIAM = origInject }() + type fields struct { kube client.Client track func(context.Context, resource.LegacyManaged) error @@ -161,6 +169,50 @@ func TestConnect(t *testing.T) { }, want: errors.Wrap(errBoom, errGetSecret), }, + "SuccessAWSIAMAuth": { + reason: "When source is AWSIAMAuth, a token is injected as the password, cleartext is enabled and TLS is forced.", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.Credentials.Source = v1alpha1.CredentialsSourceAWSIAMAuth + o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{Namespace: "ns", Name: "s"} + case *corev1.Secret: + o.Data = map[string][]byte{ + xpv1.ResourceCredentialsSecretEndpointKey: []byte("db.example.rds.amazonaws.com"), + xpv1.ResourceCredentialsSecretPortKey: []byte("3306"), + xpv1.ResourceCredentialsSecretUserKey: []byte("crossplane_admin"), + } + } + return nil + }), + }, + track: nopUsage, + newDB: func(creds map[string][]byte, tls *string, _ *bool, cleartext bool) xsql.DB { + if !cleartext { + t.Error("expected cleartext=true for AWS IAM auth") + } + if got := string(creds[xpv1.ResourceCredentialsSecretPasswordKey]); got != "iam-token" { + t.Errorf("expected injected token as password, got %q", got) + } + if tls == nil || *tls != "skip-verify" { + t.Errorf("expected TLS forced to skip-verify, got %v", tls) + } + return mockDB{} + }, + }, + args: args{ + mg: &v1alpha1.Database{ + Spec: v1alpha1.DatabaseSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + }, + }, + }, + want: nil, + }, } for name, tc := range cases { diff --git a/pkg/controller/cluster/mysql/grant/reconciler.go b/pkg/controller/cluster/mysql/grant/reconciler.go index 35eea9bd..24bffa4e 100644 --- a/pkg/controller/cluster/mysql/grant/reconciler.go +++ b/pkg/controller/cluster/mysql/grant/reconciler.go @@ -40,6 +40,7 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane-contrib/provider-sql/apis/cluster/mysql/v1alpha1" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/mysql/tls" @@ -52,6 +53,8 @@ const ( errGetSecret = "cannot get credentials Secret" errTLSConfig = "cannot load TLS config" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errCreateGrant = "cannot create grant" errRevokeGrant = "cannot revoke grant" errCurrentGrant = "cannot show current grants" @@ -99,6 +102,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { Complete(r) } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -139,7 +146,19 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Grant) (managed.Ty } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil + + cleartext := false + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + cleartext = true + // IAM auth requires TLS. The operator supplies the RDS CA (e.g. via + // trust-manager or a mounted secret); honour a verifying tls mode if set, + // otherwise fall back to an encrypted (unverified) connection. + tlsName = mysql.EnsureTLS(tlsName) + } + return &external{db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, cleartext)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index dbb206da..26d2861f 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -39,6 +39,7 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/resource" "github.com/crossplane-contrib/provider-sql/apis/cluster/mysql/v1alpha1" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/mysql/tls" @@ -51,6 +52,8 @@ const ( errGetSecret = "cannot get credentials Secret" errTLSConfig = "cannot load TLS config" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errSelectUser = "cannot select user" errCreateUser = "cannot create user" errDropUser = "cannot drop user" @@ -94,6 +97,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { Complete(r) } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -134,8 +141,20 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.User) (managed.Typ } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) + + cleartext := false + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + cleartext = true + // IAM auth requires TLS. The operator supplies the RDS CA (e.g. via + // trust-manager or a mounted secret); honour a verifying tls mode if set, + // otherwise fall back to an encrypted (unverified) connection. + tlsName = mysql.EnsureTLS(tlsName) + } return &external{ - db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, false), + db: c.newDB(secretData, tlsName, mg.Spec.ForProvider.BinLog, cleartext), kube: c.kube, }, nil } diff --git a/pkg/controller/cluster/postgresql/database/reconciler.go b/pkg/controller/cluster/postgresql/database/reconciler.go index 0bcafae5..329793f3 100644 --- a/pkg/controller/cluster/postgresql/database/reconciler.go +++ b/pkg/controller/cluster/postgresql/database/reconciler.go @@ -42,6 +42,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/cluster/postgresql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" ) @@ -52,6 +53,8 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errSelectDB = "cannot select database" errSelectServerVer = "cannot determine PostgreSQL server version" errCreateDB = "cannot create database" @@ -98,6 +101,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { Complete(r) } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -132,7 +139,17 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Database) (managed } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - return &external{db: c.newDB(secretData, pc.Spec.DefaultDatabase, clients.ToString(pc.Spec.SSLMode))}, nil + + sslMode := clients.ToString(pc.Spec.SSLMode) + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + // IAM auth requires TLS. Cert verification depends on the operator + // supplying the RDS CA (e.g. via trust-manager or a mounted secret). + sslMode = "require" + } + return &external{db: c.newDB(secretData, pc.Spec.DefaultDatabase, sslMode)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/cluster/postgresql/database/reconciler_test.go b/pkg/controller/cluster/postgresql/database/reconciler_test.go index 711418e1..ba519d66 100644 --- a/pkg/controller/cluster/postgresql/database/reconciler_test.go +++ b/pkg/controller/cluster/postgresql/database/reconciler_test.go @@ -73,6 +73,14 @@ func TestConnect(t *testing.T) { errBoom := errors.New("boom") nopUsage := func(ctx context.Context, mg resource.LegacyManaged) error { return nil } + // Stub the AWS IAM token injection so the success case needs no AWS access. + origInject := injectIAM + injectIAM = func(_ context.Context, _ *string, creds map[string][]byte) error { + creds[xpv1.ResourceCredentialsSecretPasswordKey] = []byte("iam-token") + return nil + } + defer func() { injectIAM = origInject }() + type fields struct { kube client.Client track func(context.Context, resource.LegacyManaged) error @@ -168,6 +176,47 @@ func TestConnect(t *testing.T) { }, want: errors.Wrap(errBoom, errGetSecret), }, + "SuccessAWSIAMAuth": { + reason: "When source is AWSIAMAuth, a token is injected as the password and sslmode is forced to require.", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.Credentials.Source = v1alpha1.CredentialsSourceAWSIAMAuth + o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{Namespace: "ns", Name: "s"} + case *corev1.Secret: + o.Data = map[string][]byte{ + xpv1.ResourceCredentialsSecretEndpointKey: []byte("db.example.rds.amazonaws.com"), + xpv1.ResourceCredentialsSecretPortKey: []byte("5432"), + xpv1.ResourceCredentialsSecretUserKey: []byte("crossplane_admin"), + } + } + return nil + }), + }, + track: nopUsage, + newDB: func(creds map[string][]byte, _ string, sslmode string) xsql.DB { + if sslmode != "require" { + t.Errorf("expected sslmode forced to require, got %q", sslmode) + } + if got := string(creds[xpv1.ResourceCredentialsSecretPasswordKey]); got != "iam-token" { + t.Errorf("expected injected token as password, got %q", got) + } + return mockDB{} + }, + }, + args: args{ + mg: &v1alpha1.Database{ + Spec: v1alpha1.DatabaseSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + }, + }, + }, + want: nil, + }, } for name, tc := range cases { diff --git a/pkg/controller/cluster/postgresql/default_privileges/reconciler.go b/pkg/controller/cluster/postgresql/default_privileges/reconciler.go index 13e3b34b..338c62fd 100644 --- a/pkg/controller/cluster/postgresql/default_privileges/reconciler.go +++ b/pkg/controller/cluster/postgresql/default_privileges/reconciler.go @@ -40,6 +40,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/cluster/postgresql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" ) @@ -50,6 +51,8 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errSelectDefaultPrivileges = "cannot select default privileges" errCreateDefaultPrivileges = "cannot create default privileges" errRevokeDefaultPrivileges = "cannot revoke default privileges" @@ -94,6 +97,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { Complete(r) } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -133,7 +140,15 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.DefaultPrivileges) } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - return &external{db: c.newDB(secretData, database, clients.ToString(pc.Spec.SSLMode))}, nil + + sslMode := clients.ToString(pc.Spec.SSLMode) + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + sslMode = "require" + } + return &external{db: c.newDB(secretData, database, sslMode)}, nil } type external struct { diff --git a/pkg/controller/cluster/postgresql/extension/reconciler.go b/pkg/controller/cluster/postgresql/extension/reconciler.go index be93e002..e11299f2 100644 --- a/pkg/controller/cluster/postgresql/extension/reconciler.go +++ b/pkg/controller/cluster/postgresql/extension/reconciler.go @@ -38,6 +38,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/cluster/postgresql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" ) @@ -48,6 +49,8 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errSelectExtension = "cannot select extension" errCreateExtension = "cannot create extension" errDropExtension = "cannot drop extension" @@ -88,6 +91,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { Complete(r) } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -123,13 +130,21 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Extension) (manage secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) + sslMode := clients.ToString(pc.Spec.SSLMode) + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + sslMode = "require" + } + // We do not want to create an extension on the default DB // if the user was expecting a database name to be resolved. if mg.Spec.ForProvider.Database != nil { - return &external{db: c.newDB(secretData, *mg.Spec.ForProvider.Database, clients.ToString(pc.Spec.SSLMode))}, nil + return &external{db: c.newDB(secretData, *mg.Spec.ForProvider.Database, sslMode)}, nil } - return &external{db: c.newDB(secretData, pc.Spec.DefaultDatabase, clients.ToString(pc.Spec.SSLMode))}, nil + return &external{db: c.newDB(secretData, pc.Spec.DefaultDatabase, sslMode)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/cluster/postgresql/grant/reconciler.go b/pkg/controller/cluster/postgresql/grant/reconciler.go index 81c716b1..178e284b 100644 --- a/pkg/controller/cluster/postgresql/grant/reconciler.go +++ b/pkg/controller/cluster/postgresql/grant/reconciler.go @@ -40,6 +40,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/cluster/postgresql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" ) @@ -50,6 +51,8 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errNotGrant = "managed resource is not a Grant custom resource" errSelectGrant = "cannot select grant" errCreateGrant = "cannot create grant" @@ -65,6 +68,10 @@ const ( maxConcurrency = 5 ) +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -118,7 +125,15 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Grant) (managed.Ty db = pc.Spec.DefaultDatabase } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - xdb := c.newDB(secretData, db, clients.ToString(pc.Spec.SSLMode)) + + sslMode := clients.ToString(pc.Spec.SSLMode) + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + sslMode = "require" + } + xdb := c.newDB(secretData, db, sslMode) serverVersion, err := xdb.GetServerVersion(ctx) if err != nil { diff --git a/pkg/controller/cluster/postgresql/role/reconciler.go b/pkg/controller/cluster/postgresql/role/reconciler.go index ae88ba5d..d95e183e 100644 --- a/pkg/controller/cluster/postgresql/role/reconciler.go +++ b/pkg/controller/cluster/postgresql/role/reconciler.go @@ -44,6 +44,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/cluster/postgresql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" ) @@ -54,6 +55,8 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errSelectRole = "cannot select role" errCreateRole = "cannot create role" errDropRole = "cannot drop role" @@ -98,6 +101,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { Complete(r) } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -132,8 +139,16 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Role) (managed.Typ } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) + + sslMode := clients.ToString(pc.Spec.SSLMode) + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + sslMode = "require" + } return &external{ - db: c.newDB(secretData, pc.Spec.DefaultDatabase, clients.ToString(pc.Spec.SSLMode)), + db: c.newDB(secretData, pc.Spec.DefaultDatabase, sslMode), kube: c.kube, }, nil } diff --git a/pkg/controller/cluster/postgresql/schema/reconciler.go b/pkg/controller/cluster/postgresql/schema/reconciler.go index 6fd4e769..f176e59d 100644 --- a/pkg/controller/cluster/postgresql/schema/reconciler.go +++ b/pkg/controller/cluster/postgresql/schema/reconciler.go @@ -40,6 +40,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/cluster/postgresql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" ) @@ -50,6 +51,8 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errGenerateIAMToken = "cannot generate AWS IAM authentication token" + errSelectSchema = "cannot select schema" errCreateSchema = "cannot create schema" errDropSchema = "cannot drop schema" @@ -94,6 +97,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { var _ managed.TypedExternalConnector[*v1alpha1.Schema] = &connector{} +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + type connector struct { kube client.Client track func(ctx context.Context, mg resource.LegacyManaged) error @@ -130,7 +137,15 @@ func (c *connector) Connect(ctx context.Context, mg *v1alpha1.Schema) (managed.T } secretData := xsql.RemapCredentialKeys(s.Data, pc.Spec.Credentials.SecretKeyMapping.ToMap()) - return &external{db: c.newDB(secretData, *mg.Spec.ForProvider.Database, clients.ToString(pc.Spec.SSLMode))}, nil + + sslMode := clients.ToString(pc.Spec.SSLMode) + if pc.Spec.Credentials.Source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, pc.Spec.Credentials.Region, secretData); err != nil { + return nil, errors.Wrap(err, errGenerateIAMToken) + } + sslMode = "require" + } + return &external{db: c.newDB(secretData, *mg.Spec.ForProvider.Database, sslMode)}, nil } var _ managed.TypedExternalClient[*v1alpha1.Schema] = &external{} From 1f1125dba5c93a58622253a0048f5cd686a3712f Mon Sep 17 00:00:00 2001 From: michielvha Date: Sat, 20 Jun 2026 03:49:07 +0200 Subject: [PATCH 5/6] feat: inject AWS IAM auth token in namespaced providers Inject the IAM token centrally in GetProviderConfig() for the namespaced MySQL and PostgreSQL providers when the credentials source is AWSIAMAuth. MySQL additionally surfaces a Cleartext flag (forwarded by its three reconcilers) and forces TLS on; PostgreSQL forces sslmode=require. The six namespaced PostgreSQL reconcilers need no change. Part of #347. Signed-off-by: michielvha --- pkg/controller/namespaced/errors/errors.go | 11 +++ .../namespaced/mysql/database/reconciler.go | 2 +- .../namespaced/mysql/grant/reconciler.go | 2 +- .../namespaced/mysql/provider/provider.go | 32 ++++++- .../mysql/provider/provider_test.go | 90 +++++++++++++++++++ .../namespaced/mysql/user/reconciler.go | 2 +- .../postgresql/provider/provider.go | 25 +++++- .../postgresql/provider/provider_test.go | 87 ++++++++++++++++++ 8 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 pkg/controller/namespaced/mysql/provider/provider_test.go create mode 100644 pkg/controller/namespaced/postgresql/provider/provider_test.go diff --git a/pkg/controller/namespaced/errors/errors.go b/pkg/controller/namespaced/errors/errors.go index 1d9dc7a3..7d026215 100644 --- a/pkg/controller/namespaced/errors/errors.go +++ b/pkg/controller/namespaced/errors/errors.go @@ -10,6 +10,7 @@ const ( errGetSecret = "cannot get credentials Secret: %s" errInvalidProviderConfigKind = "invalid ProviderConfig kind: %s" errNoSecretRef = "providerConfig does not reference a credentials Secret" + errGenerateIAMToken = "cannot generate AWS IAM authentication token: %s" ) func GetProviderConfigError(err error) error { return ErrGetProviderConfig{err} } @@ -50,6 +51,16 @@ func (e ErrInvalidProviderConfigKind) Error() string { return fmt.Sprintf(errInvalidProviderConfigKind, e.kind) } +func GenerateIAMTokenError(err error) error { return ErrGenerateIAMToken{err} } + +type ErrGenerateIAMToken struct{ error } + +func (e ErrGenerateIAMToken) Error() string { + return fmt.Sprintf(errGenerateIAMToken, e.error) +} + +func (e ErrGenerateIAMToken) Unwrap() error { return e.error } + func MissingSecretRefError() error { return ErrNoSecretRef{} } type ErrNoSecretRef struct{} diff --git a/pkg/controller/namespaced/mysql/database/reconciler.go b/pkg/controller/namespaced/mysql/database/reconciler.go index f9df7899..d5f3998f 100644 --- a/pkg/controller/namespaced/mysql/database/reconciler.go +++ b/pkg/controller/namespaced/mysql/database/reconciler.go @@ -112,7 +112,7 @@ func (c *connector) Connect(ctx context.Context, mg *namespacedv1alpha1.Database return nil, errors.Wrap(err, errTLSConfig) } - return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil + return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, providerInfo.Cleartext)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/namespaced/mysql/grant/reconciler.go b/pkg/controller/namespaced/mysql/grant/reconciler.go index f76eddfa..7efd81ea 100644 --- a/pkg/controller/namespaced/mysql/grant/reconciler.go +++ b/pkg/controller/namespaced/mysql/grant/reconciler.go @@ -121,7 +121,7 @@ func (c *connector) Connect(ctx context.Context, mg *namespacedv1alpha1.Grant) ( return nil, errors.Wrap(err, errTLSConfig) } - return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, false)}, nil + return &external{db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, providerInfo.Cleartext)}, nil } type external struct{ db xsql.DB } diff --git a/pkg/controller/namespaced/mysql/provider/provider.go b/pkg/controller/namespaced/mysql/provider/provider.go index 908dfc80..35427dd7 100644 --- a/pkg/controller/namespaced/mysql/provider/provider.go +++ b/pkg/controller/namespaced/mysql/provider/provider.go @@ -9,6 +9,8 @@ import ( "context" "github.com/crossplane-contrib/provider-sql/apis/namespaced/mysql/v1alpha1" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" + "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/errors" @@ -20,14 +22,23 @@ type ProviderInfo struct { SecretData map[string][]byte TLS *string TLSConfig *v1alpha1.TLSConfig + // Cleartext is true when the connection uses AWS IAM auth, requiring the + // MySQL client to set allowCleartextPasswords=true. + Cleartext bool } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.ModernManaged) (ProviderInfo, error) { var ( secretKey *client.ObjectKey tlsMode *string tlsConfig *v1alpha1.TLSConfig keyMapping map[string]string + source v1alpha1.MySQLConnectionSecretSource + region *string ) switch mg.GetProviderConfigReference().Kind { @@ -50,6 +61,8 @@ func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.Mode tlsMode = providerConfig.Spec.TLS tlsConfig = providerConfig.Spec.TLSConfig keyMapping = providerConfig.Spec.Credentials.SecretKeyMapping.ToMap() + source = providerConfig.Spec.Credentials.Source + region = providerConfig.Spec.Credentials.Region case v1alpha1.ClusterProviderConfigKind: clusterProviderConfig := &v1alpha1.ClusterProviderConfig{ @@ -69,6 +82,8 @@ func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.Mode tlsMode = clusterProviderConfig.Spec.TLS tlsConfig = clusterProviderConfig.Spec.TLSConfig keyMapping = clusterProviderConfig.Spec.Credentials.SecretKeyMapping.ToMap() + source = clusterProviderConfig.Spec.Credentials.Source + region = clusterProviderConfig.Spec.Credentials.Region default: return ProviderInfo{}, errors.InvalidProviderConfigKindError(mg.GetProviderConfigReference().Kind) @@ -84,10 +99,25 @@ func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.Mode return ProviderInfo{}, errors.GetSecretError(err) } + secretData := xsql.RemapCredentialKeys(s.Data, keyMapping) + + cleartext := false + if source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, region, secretData); err != nil { + return ProviderInfo{}, errors.GenerateIAMTokenError(err) + } + cleartext = true + // IAM auth requires TLS. The operator supplies the RDS CA (e.g. via + // trust-manager or a mounted secret); honour a verifying tls mode if set, + // otherwise fall back to an encrypted (unverified) connection. + tlsMode = mysql.EnsureTLS(tlsMode) + } + return ProviderInfo{ ProviderConfigName: mg.GetProviderConfigReference().Name, - SecretData: xsql.RemapCredentialKeys(s.Data, keyMapping), + SecretData: secretData, TLS: tlsMode, TLSConfig: tlsConfig, + Cleartext: cleartext, }, nil } diff --git a/pkg/controller/namespaced/mysql/provider/provider_test.go b/pkg/controller/namespaced/mysql/provider/provider_test.go new file mode 100644 index 00000000..40afd29e --- /dev/null +++ b/pkg/controller/namespaced/mysql/provider/provider_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2024 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane/crossplane-runtime/v2/apis/common" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + + "github.com/crossplane-contrib/provider-sql/apis/namespaced/mysql/v1alpha1" +) + +// TestGetProviderConfigAWSIAMAuth verifies the centralized IAM injection: when +// the source is AWSIAMAuth the token is injected as the password, the Cleartext +// flag is set, and TLS is forced on. This covers all three namespaced MySQL +// reconcilers, which obtain credentials exclusively through GetProviderConfig. +func TestGetProviderConfigAWSIAMAuth(t *testing.T) { + // Stub the AWS IAM token injection so the test needs no AWS access. + origInject := injectIAM + injectIAM = func(_ context.Context, _ *string, creds map[string][]byte) error { + creds[xpv1.ResourceCredentialsSecretPasswordKey] = []byte("iam-token") + return nil + } + defer func() { injectIAM = origInject }() + + kube := &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.Credentials.Source = v1alpha1.CredentialsSourceAWSIAMAuth + o.Spec.Credentials.ConnectionSecretRef = xpv1.LocalSecretReference{Name: "s"} + case *corev1.Secret: + o.Data = map[string][]byte{ + xpv1.ResourceCredentialsSecretEndpointKey: []byte("db.example.rds.amazonaws.com"), + xpv1.ResourceCredentialsSecretPortKey: []byte("3306"), + xpv1.ResourceCredentialsSecretUserKey: []byte("crossplane_admin"), + } + } + return nil + }), + } + + mg := &v1alpha1.Database{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, + Spec: v1alpha1.DatabaseSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{ + Kind: v1alpha1.ProviderConfigKind, + Name: "example", + }, + }, + }, + } + + info, err := GetProviderConfig(context.Background(), kube, mg) + if err != nil { + t.Fatalf("GetProviderConfig(...): unexpected error: %v", err) + } + if !info.Cleartext { + t.Error("expected Cleartext=true for AWS IAM auth") + } + if got := string(info.SecretData[xpv1.ResourceCredentialsSecretPasswordKey]); got != "iam-token" { + t.Errorf("expected injected token as password, got %q", got) + } + if info.TLS == nil || *info.TLS != "skip-verify" { + t.Errorf("expected TLS forced to skip-verify, got %v", info.TLS) + } +} diff --git a/pkg/controller/namespaced/mysql/user/reconciler.go b/pkg/controller/namespaced/mysql/user/reconciler.go index bdc3b64d..697fac80 100644 --- a/pkg/controller/namespaced/mysql/user/reconciler.go +++ b/pkg/controller/namespaced/mysql/user/reconciler.go @@ -117,7 +117,7 @@ func (c *connector) Connect(ctx context.Context, mg *namespacedv1alpha1.User) (m } return &external{ - db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, false), + db: c.newDB(providerInfo.SecretData, tlsName, mg.Spec.ForProvider.BinLog, providerInfo.Cleartext), kube: c.kube, }, nil } diff --git a/pkg/controller/namespaced/postgresql/provider/provider.go b/pkg/controller/namespaced/postgresql/provider/provider.go index 93652601..f98a87f9 100644 --- a/pkg/controller/namespaced/postgresql/provider/provider.go +++ b/pkg/controller/namespaced/postgresql/provider/provider.go @@ -9,6 +9,7 @@ import ( "context" "github.com/crossplane-contrib/provider-sql/apis/namespaced/postgresql/v1alpha1" + "github.com/crossplane-contrib/provider-sql/pkg/clients/awsiam" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" provErrors "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/errors" @@ -22,12 +23,18 @@ type ProviderInfo struct { SSLMode *string } +// injectIAM generates and injects an AWS IAM auth token. It is a package +// variable so tests can replace it with a stub. +var injectIAM = awsiam.Inject + func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.ModernManaged) (ProviderInfo, error) { var ( secretKey *client.ObjectKey defaultDatabase string sslMode *string keyMapping map[string]string + source v1alpha1.PostgreSQLConnectionSource + region *string ) switch mg.GetProviderConfigReference().Kind { @@ -51,6 +58,8 @@ func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.Mode defaultDatabase = providerConfig.Spec.DefaultDatabase sslMode = providerConfig.Spec.SSLMode keyMapping = providerConfig.Spec.Credentials.SecretKeyMapping.ToMap() + source = providerConfig.Spec.Credentials.Source + region = providerConfig.Spec.Credentials.Region case v1alpha1.ClusterProviderConfigKind: clusterProviderConfig := &v1alpha1.ClusterProviderConfig{ ObjectMeta: metav1.ObjectMeta{ @@ -70,6 +79,8 @@ func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.Mode defaultDatabase = clusterProviderConfig.Spec.DefaultDatabase sslMode = clusterProviderConfig.Spec.SSLMode keyMapping = clusterProviderConfig.Spec.Credentials.SecretKeyMapping.ToMap() + source = clusterProviderConfig.Spec.Credentials.Source + region = clusterProviderConfig.Spec.Credentials.Region default: return ProviderInfo{}, provErrors.InvalidProviderConfigKindError(mg.GetProviderConfigReference().Kind) } @@ -84,9 +95,21 @@ func GetProviderConfig(ctx context.Context, kube client.Client, mg resource.Mode return ProviderInfo{}, provErrors.GetSecretError(err) } + secretData := xsql.RemapCredentialKeys(s.Data, keyMapping) + + if source == v1alpha1.CredentialsSourceAWSIAMAuth { + if err := injectIAM(ctx, region, secretData); err != nil { + return ProviderInfo{}, provErrors.GenerateIAMTokenError(err) + } + // IAM auth requires TLS. Cert verification depends on the operator + // supplying the RDS CA (e.g. via trust-manager or a mounted secret). + requireMode := "require" + sslMode = &requireMode + } + return ProviderInfo{ ProviderConfigName: mg.GetProviderConfigReference().Name, - SecretData: xsql.RemapCredentialKeys(s.Data, keyMapping), + SecretData: secretData, DefaultDatabase: defaultDatabase, SSLMode: sslMode, }, nil diff --git a/pkg/controller/namespaced/postgresql/provider/provider_test.go b/pkg/controller/namespaced/postgresql/provider/provider_test.go new file mode 100644 index 00000000..f2039a65 --- /dev/null +++ b/pkg/controller/namespaced/postgresql/provider/provider_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2024 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane/crossplane-runtime/v2/apis/common" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + + "github.com/crossplane-contrib/provider-sql/apis/namespaced/postgresql/v1alpha1" +) + +// TestGetProviderConfigAWSIAMAuth verifies the centralized IAM injection: when +// the source is AWSIAMAuth the token is injected as the password and sslmode is +// forced to require. This covers all six namespaced PostgreSQL reconcilers, +// which obtain credentials exclusively through GetProviderConfig. +func TestGetProviderConfigAWSIAMAuth(t *testing.T) { + // Stub the AWS IAM token injection so the test needs no AWS access. + origInject := injectIAM + injectIAM = func(_ context.Context, _ *string, creds map[string][]byte) error { + creds[xpv1.ResourceCredentialsSecretPasswordKey] = []byte("iam-token") + return nil + } + defer func() { injectIAM = origInject }() + + kube := &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.Credentials.Source = v1alpha1.CredentialsSourceAWSIAMAuth + o.Spec.Credentials.ConnectionSecretRef = xpv1.LocalSecretReference{Name: "s"} + case *corev1.Secret: + o.Data = map[string][]byte{ + xpv1.ResourceCredentialsSecretEndpointKey: []byte("db.example.rds.amazonaws.com"), + xpv1.ResourceCredentialsSecretPortKey: []byte("5432"), + xpv1.ResourceCredentialsSecretUserKey: []byte("crossplane_admin"), + } + } + return nil + }), + } + + mg := &v1alpha1.Database{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, + Spec: v1alpha1.DatabaseSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{ + Kind: v1alpha1.ProviderConfigKind, + Name: "example", + }, + }, + }, + } + + info, err := GetProviderConfig(context.Background(), kube, mg) + if err != nil { + t.Fatalf("GetProviderConfig(...): unexpected error: %v", err) + } + if info.SSLMode == nil || *info.SSLMode != "require" { + t.Errorf("expected SSLMode forced to require, got %v", info.SSLMode) + } + if got := string(info.SecretData[xpv1.ResourceCredentialsSecretPasswordKey]); got != "iam-token" { + t.Errorf("expected injected token as password, got %q", got) + } +} From d7dfa8548cd6f5283e45d5a0a560deb60e6d6998 Mon Sep 17 00:00:00 2001 From: michielvha Date: Sat, 20 Jun 2026 03:51:12 +0200 Subject: [PATCH 6/6] docs: add AWS IAM auth examples and RDS CA setup guide Add example AWSIAMAuth ProviderConfigs for cluster and namespaced MySQL and PostgreSQL, and docs/aws-iam-auth.md covering prerequisites, the operator-supplied RDS CA trust options (trust-manager / mounted secret), region resolution and the TLS behaviour. Part of #347. Signed-off-by: michielvha --- docs/aws-iam-auth.md | 90 +++++++++++++++++++ examples/cluster/mysql/config_iam.yaml | 30 +++++++ examples/cluster/postgresql/config_iam.yaml | 30 +++++++ examples/namespaced/mysql/config_iam.yaml | 15 ++++ .../namespaced/postgresql/config_iam.yaml | 17 ++++ 5 files changed, 182 insertions(+) create mode 100644 docs/aws-iam-auth.md create mode 100644 examples/cluster/mysql/config_iam.yaml create mode 100644 examples/cluster/postgresql/config_iam.yaml create mode 100644 examples/namespaced/mysql/config_iam.yaml create mode 100644 examples/namespaced/postgresql/config_iam.yaml diff --git a/docs/aws-iam-auth.md b/docs/aws-iam-auth.md new file mode 100644 index 00000000..67e79ea2 --- /dev/null +++ b/docs/aws-iam-auth.md @@ -0,0 +1,90 @@ +# AWS IAM database authentication + +provider-sql can authenticate to Amazon Aurora / RDS (MySQL and PostgreSQL) +using [IAM database authentication](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html) +instead of a static password. The provider generates a short-lived IAM token at +connection time and uses it as the database password. Combined with EKS Pod +Identity or IRSA, this removes static database credentials entirely. + +Select it with the `AWSIAMAuth` credentials source on a `ProviderConfig`: + +```yaml +spec: + credentials: + source: AWSIAMAuth + connectionSecretRef: + name: aurora-conn # endpoint, port, username - no password + # region: us-east-1 # optional (see "Region resolution") +``` + +See the runnable examples: `examples/{cluster,namespaced}/{mysql,postgresql}/config_iam.yaml`. + +## Prerequisites + +1. **Enable IAM authentication** on the Aurora cluster / RDS instance. + +2. **Create an IAM-enabled database user for the provider to connect as.** This + is a one-time bootstrap that needs a normal (password) connection, performed + out of band - the provider does not do it. Use a **dedicated provisioning + role, not the master user** (granting `rds_iam` to the Postgres master + permanently disables its password auth, and the MySQL plugin is fixed at + `CREATE USER` time): + + - PostgreSQL: `CREATE USER crossplane_admin; GRANT rds_iam TO crossplane_admin;` + plus the privileges it needs to manage your databases/roles/grants. + - MySQL: `CREATE USER 'crossplane_admin'@'%' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';` + plus the required `GRANT`s. + + (Creating these users is also possible declaratively through provider-sql's + own `Role`/`Grant` resources from a password-based `ProviderConfig`.) + +3. **Allow the controller's IAM identity to connect** by attaching an + `rds-db:connect` policy for that database user, and give the provider pod that + identity via [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) + or [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). + Credentials are discovered from the environment by the AWS SDK. + +4. **Provide a connection secret** containing only `endpoint`, `port` and + `username` (no `password`). + +## TLS and the Amazon RDS certificate authority + +IAM authentication requires TLS - RDS rejects unencrypted connections. To +*verify* the server certificate, the client needs the Amazon RDS CA, which is +**not** in the default system trust store. provider-sql does **not** ship the RDS +CA bundle; making it available to the provider pod is an operator concern. Two +common approaches: + +- **[trust-manager](https://cert-manager.io/docs/trust/trust-manager/)** - add the + [Amazon RDS CA bundle](https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem) + to the provider pod's trust store. Then set `tls: "true"` (MySQL) or + `sslMode: verify-full` (PostgreSQL) and the certificate verifies normally. +- **Mounted secret** - mount the RDS CA and reference it through the provider's + existing custom TLS mechanism (`tls: custom` + `tlsConfig` for MySQL; + `sslMode: verify-full` with the CA for PostgreSQL). + +If you do neither, IAM auth still works but the connection is **encrypted +without certificate verification**: + +| Engine | Verified (RDS CA available) | Default fallback | +|--------|-----------------------------|------------------| +| MySQL | `tls: "true"` or `tls: custom` | `skip-verify` (encrypted, unverified) | +| PostgreSQL | `sslMode: verify-full` | `sslMode: require` (encrypted, unverified) | + +The provider always enforces at least an encrypted connection for IAM auth. + +## Region resolution + +The region used to sign the token is resolved in order: + +1. `spec.credentials.region` on the `ProviderConfig` +2. a `region` key in the connection secret +3. the controller's AWS environment (`AWS_REGION` / instance metadata) + +If none is set the token cannot be signed correctly, so configure one of them. + +## Notes + +- The provider opens a fresh connection each reconcile and generates a new token + each time, so the 15-minute token lifetime is never an issue. +- Setting `AWSIAMAuth` forces TLS on even if a weaker `tls`/`sslMode` was set. diff --git a/examples/cluster/mysql/config_iam.yaml b/examples/cluster/mysql/config_iam.yaml new file mode 100644 index 00000000..c867d74a --- /dev/null +++ b/examples/cluster/mysql/config_iam.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: mysql.sql.crossplane.io/v1alpha1 +kind: ProviderConfig +metadata: + name: aurora-iam +spec: + credentials: + source: AWSIAMAuth + connectionSecretRef: + namespace: crossplane-system + name: aurora-conn # endpoint, port, username - no password + # region: us-east-1 # optional; otherwise a "region" secret key, then the controller's AWS env + # tls "true" verifies the server certificate against the pod trust store. The + # Amazon RDS CA must be made available to the provider pod (see + # docs/aws-iam-auth.md), for example with trust-manager. Use tls: custom to + # point at a mounted RDS CA secret instead. If TLS is left unset, IAM auth + # falls back to an encrypted but unverified connection. + tls: "true" +--- +# The connection secret holds only endpoint, port and username - no password. +# The provider generates a short-lived IAM token and uses it as the password. +# apiVersion: v1 +# kind: Secret +# metadata: +# name: aurora-conn +# namespace: crossplane-system +# stringData: +# endpoint: my-cluster.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com +# port: "3306" +# username: crossplane_admin diff --git a/examples/cluster/postgresql/config_iam.yaml b/examples/cluster/postgresql/config_iam.yaml new file mode 100644 index 00000000..ad9b364c --- /dev/null +++ b/examples/cluster/postgresql/config_iam.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: ProviderConfig +metadata: + name: aurora-iam +spec: + # defaultDatabase: postgres + credentials: + source: AWSIAMAuth + connectionSecretRef: + namespace: crossplane-system + name: aurora-conn # endpoint, port, username - no password + # region: us-east-1 # optional; otherwise a "region" secret key, then the controller's AWS env + # IAM auth forces sslMode=require (the connection is encrypted but the server + # certificate is not verified). For full verification set sslMode: verify-full + # and make the Amazon RDS CA available to the provider pod (see + # docs/aws-iam-auth.md), for example with trust-manager. + sslMode: require +--- +# The connection secret holds only endpoint, port and username - no password. +# The provider generates a short-lived IAM token and uses it as the password. +# apiVersion: v1 +# kind: Secret +# metadata: +# name: aurora-conn +# namespace: crossplane-system +# stringData: +# endpoint: my-cluster.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com +# port: "5432" +# username: crossplane_admin diff --git a/examples/namespaced/mysql/config_iam.yaml b/examples/namespaced/mysql/config_iam.yaml new file mode 100644 index 00000000..df29d296 --- /dev/null +++ b/examples/namespaced/mysql/config_iam.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: mysql.sql.m.crossplane.io/v1alpha1 +kind: ProviderConfig +metadata: + name: aurora-iam + namespace: default +spec: + credentials: + source: AWSIAMAuth + connectionSecretRef: + name: aurora-conn # endpoint, port, username - no password + # region: us-east-1 # optional; otherwise a "region" secret key, then the controller's AWS env + # See docs/aws-iam-auth.md for the RDS CA trust setup. tls: "true" verifies + # against the pod trust store; tls: custom points at a mounted RDS CA secret. + tls: "true" diff --git a/examples/namespaced/postgresql/config_iam.yaml b/examples/namespaced/postgresql/config_iam.yaml new file mode 100644 index 00000000..487e2278 --- /dev/null +++ b/examples/namespaced/postgresql/config_iam.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: ProviderConfig +metadata: + name: aurora-iam + namespace: default +spec: + # defaultDatabase: postgres + credentials: + source: AWSIAMAuth + connectionSecretRef: + name: aurora-conn # endpoint, port, username - no password + # region: us-east-1 # optional; otherwise a "region" secret key, then the controller's AWS env + # IAM auth forces sslMode=require. For full server-cert verification set + # sslMode: verify-full and supply the Amazon RDS CA to the pod + # (see docs/aws-iam-auth.md). + sslMode: require