From 8bf749f52a9d79c3367ee4f63a40de15e0785353 Mon Sep 17 00:00:00 2001 From: De Bruyn Annandale Date: Mon, 9 Jun 2025 13:14:06 +0200 Subject: [PATCH] feat: add support for AWS RDS IAM authentication and update dependencies --- db/sql/SqlDb.go | 7 +++++++ go.mod | 14 +++++++++++++ go.sum | 28 +++++++++++++++++++++++++ util/aws_iam.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ util/config.go | 26 ++++++++++++++++++----- 5 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 util/aws_iam.go diff --git a/db/sql/SqlDb.go b/db/sql/SqlDb.go index 7fe9493c5..d868d7740 100644 --- a/db/sql/SqlDb.go +++ b/db/sql/SqlDb.go @@ -9,6 +9,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/Masterminds/squirrel" "github.com/go-gorp/gorp/v3" @@ -364,6 +365,12 @@ func (d *SqlDb) Connect(_ string) { panic(err) } + if cfg.Dialect == util.DbDriverPostgres && cfg.PostgresIAM { + maxLifetime := 14 * time.Minute + log.Debugf("Setting connection max lifetime to %s for IAM authentication", maxLifetime) + sqlDb.SetConnMaxLifetime(maxLifetime) + } + var dialect gorp.Dialect switch cfg.Dialect { diff --git a/go.mod b/go.mod index 911cf3886..b2daa855c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.24.0 require ( github.com/Masterminds/squirrel v1.5.4 + github.com/aws/aws-sdk-go-v2/config v1.27.7 + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.3 github.com/coreos/go-oidc/v3 v3.14.1 github.com/creack/pty v1.1.24 github.com/go-git/go-git/v5 v5.16.0 @@ -38,6 +40,18 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/aws/aws-sdk-go-v2 v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.4 // indirect + github.com/aws/smithy-go v1.20.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect diff --git a/go.sum b/go.sum index 455d03ba3..956ce4fab 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,34 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go-v2 v1.25.3 h1:xYiLpZTQs1mzvz5PaI6uR0Wh57ippuEthxS4iK5v0n0= +github.com/aws/aws-sdk-go-v2 v1.25.3/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I= +github.com/aws/aws-sdk-go-v2/config v1.27.7 h1:JSfb5nOQF01iOgxFI5OIKWwDiEXWTyTgg1Mm1mHi0A4= +github.com/aws/aws-sdk-go-v2/config v1.27.7/go.mod h1:PH0/cNpoMO+B04qET699o5W92Ca79fVtbUnvMIZro4I= +github.com/aws/aws-sdk-go-v2/credentials v1.17.7 h1:WJd+ubWKoBeRh7A5iNMnxEOs982SyVKOJD+K8HIezu4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.7/go.mod h1:UQi7LMR0Vhvs+44w5ec8Q+VS+cd10cjwgHwiVkE0YGU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3 h1:p+y7FvkK2dxS+FEwRIDHDe//ZX+jDhP8HHE50ppj4iI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3/go.mod h1:/fYB+FZbDlwlAiynK9KDXlzZl3ANI9JkD0Uhz5FjNT4= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.3 h1:mfxA6HX/mla8BrjVHdVD0G49+0Z+xKel//NCPBk0qbo= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.3/go.mod h1:PjvlBlYNNXPrMAGarXrnV+UYv1T9XyTT2Ono41NQjq8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3 h1:ifbIbHZyGl1alsAhPIYsHOg5MuApgqOvVeI8wIugXfs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3/go.mod h1:oQZXg3c6SNeY6OZrDY+xHcF4VGIEoNotX2B4PrDeoJI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3 h1:Qvodo9gHG9F3E8SfYOspPeBt0bjSbsevK8WhRAUHcoY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3/go.mod h1:vCKrdLXtybdf/uQd/YfVR2r5pcbNuEYKzMQpcxmeSJw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5 h1:K/NXvIftOlX+oGgWGIa3jDyYLDNsdVhsjHmsBH2GLAQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5/go.mod h1:cl9HGLV66EnCmMNzq4sYOti+/xo8w34CsgzVtm2GgsY= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.2 h1:XOPfar83RIRPEzfihnp+U6udOveKZJvPQ76SKWrLRHc= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.2/go.mod h1:Vv9Xyk1KMHXrR3vNQe8W5LMFdTjSeWk0gBZBzvf3Qa0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2 h1:pi0Skl6mNl2w8qWZXcdOyg197Zsf4G97U7Sso9JXGZE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2/go.mod h1:JYzLoEVeLXk+L4tn1+rrkfhkxl6mLDEVaDSvGq9og90= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.4 h1:Ppup1nVNAOWbBOrcoOxaxPeEnSFB2RnnQdguhXpmeQk= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.4/go.mod h1:+K1rNPVyGxkRuv9NNiaZ4YhBFuyw2MMA9SlIJ1Zlpz8= +github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= +github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= diff --git a/util/aws_iam.go b/util/aws_iam.go new file mode 100644 index 000000000..2e7501bc3 --- /dev/null +++ b/util/aws_iam.go @@ -0,0 +1,56 @@ +package util + +import ( + "context" + "strings" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/rds/auth" + log "github.com/sirupsen/logrus" +) + +func GetRDSAuthToken(hostname string, username string, region string) (string, error) { + hostname, port := ExtractHostPort(hostname, "5432") + log.Debugf("Generating RDS auth token for %s:%s, user %s in region %s", hostname, port, username, region) + + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + if err != nil { + log.Errorf("Failed to load AWS config: %v", err) + return "", err + } + + credProvider := cfg.Credentials + + creds, err := credProvider.Retrieve(context.TODO()) + if err != nil { + log.Errorf("Failed to retrieve AWS credentials: %v", err) + return "", err + } + + log.Debugf("AWS credentials retrieved successfully. Access key ID: %s, Provider: %s", + creds.AccessKeyID[:4]+"...", creds.Source) + + log.Debugf("Building auth token for endpoint %s:%s in region %s", hostname, port, region) + authToken, err := auth.BuildAuthToken( + context.TODO(), + hostname+":"+port, + region, + username, + cfg.Credentials, + ) + if err != nil { + log.Errorf("Failed to generate RDS auth token: %v", err) + return "", err + } + + log.Debugf("Successfully generated RDS auth token (length: %d)", len(authToken)) + return authToken, nil +} + +func ExtractHostPort(hostPort string, defaultPort string) (host string, port string) { + parts := strings.Split(hostPort, ":") + if len(parts) == 1 { + return parts[0], defaultPort + } + return parts[0], parts[1] +} diff --git a/util/config.go b/util/config.go index 3fd55f1a4..b59c207e5 100644 --- a/util/config.go +++ b/util/config.go @@ -50,11 +50,13 @@ const ( type DbConfig struct { Dialect string `json:"-"` - Hostname string `json:"host,omitempty" env:"SEMAPHORE_DB_HOST" default:"0.0.0.0"` - Username string `json:"user,omitempty" env:"SEMAPHORE_DB_USER"` - Password string `json:"pass,omitempty" env:"SEMAPHORE_DB_PASS"` - DbName string `json:"name,omitempty" env:"SEMAPHORE_DB" default:"semaphore"` - Options map[string]string `json:"options,omitempty" env:"SEMAPHORE_DB_OPTIONS"` + Hostname string `json:"host,omitempty" env:"SEMAPHORE_DB_HOST" default:"0.0.0.0"` + Username string `json:"user,omitempty" env:"SEMAPHORE_DB_USER"` + Password string `json:"pass,omitempty" env:"SEMAPHORE_DB_PASS"` + DbName string `json:"name,omitempty" env:"SEMAPHORE_DB" default:"semaphore"` + Options map[string]string `json:"options,omitempty" env:"SEMAPHORE_DB_OPTIONS"` + PostgresIAM bool `json:"postgres_iam,omitempty" env:"SEMAPHORE_DB_POSTGRES_IAM"` + PostgresIAMRegion string `json:"postgres_iam_region,omitempty" env:"SEMAPHORE_DB_POSTGRES_IAM_REGION"` } type LdapMappings struct { @@ -998,6 +1000,20 @@ func (d *DbConfig) GetConnectionString(includeDbName bool) (connectionString str } connectionString += mapToQueryString(options) case DbDriverPostgres: + if d.PostgresIAM { + + if d.PostgresIAMRegion == "" { + return "", fmt.Errorf("Postgres IAM region is not set") + } + + authToken, tokenErr := GetRDSAuthToken(d.Hostname, dbUser, d.PostgresIAMRegion) + if tokenErr != nil { + return "", tokenErr + } + + dbPass = authToken + } + if includeDbName { connectionString = fmt.Sprintf( "postgres://%s:%s@%s/%s",