From e8934a0e8e4adcd9551121f36620fb8b497610ee Mon Sep 17 00:00:00 2001 From: Florent Viel Date: Thu, 4 Apr 2024 15:16:47 +0200 Subject: [PATCH] add new function to rotate RDB credentials --- README.md | 1 + .../secret-manager-rotate-secret/.gitignore | 3 + .../secret-manager-rotate-secret/README.md | 69 +++++++++++ functions/secret-manager-rotate-secret/go.mod | 7 ++ functions/secret-manager-rotate-secret/go.sum | 6 + .../secret-manager-rotate-secret/handler.go | 114 ++++++++++++++++++ .../secret-manager-rotate-secret/package.json | 15 +++ .../random/string.go | 97 +++++++++++++++ .../serverless.yml | 32 +++++ 9 files changed, 344 insertions(+) create mode 100644 functions/secret-manager-rotate-secret/.gitignore create mode 100644 functions/secret-manager-rotate-secret/README.md create mode 100644 functions/secret-manager-rotate-secret/go.mod create mode 100644 functions/secret-manager-rotate-secret/go.sum create mode 100644 functions/secret-manager-rotate-secret/handler.go create mode 100644 functions/secret-manager-rotate-secret/package.json create mode 100644 functions/secret-manager-rotate-secret/random/string.go create mode 100644 functions/secret-manager-rotate-secret/serverless.yml diff --git a/README.md b/README.md index fca8f67..53686f3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Table of Contents: | **[Typescript with Node runtime](functions/typescript-with-node/README.md)**
A Typescript function using Node runtime. | node18 | [Serverless Framework] | | **[Serverless Gateway Python Example](functions/serverless-gateway-python/README.md)**
A Python serverless API using Serverless Gateway. | python310 | [Python API Framework] | | **[Go and Transactional Email](functions/go-mail/README.md)**
A Go function that send emails using Scaleway SDK. | go121 | [Serverless Framework] | +| **[Rotate RDB Credentials](functions/secret-manager-rotate-secret/README.md)**
A Go function that rotates RDB credentials stored in Secret Manager. | go120 | [Serverless Framework] | ### 📦 Containers diff --git a/functions/secret-manager-rotate-secret/.gitignore b/functions/secret-manager-rotate-secret/.gitignore new file mode 100644 index 0000000..9ab08a1 --- /dev/null +++ b/functions/secret-manager-rotate-secret/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +package-lock.json +.serverless/ diff --git a/functions/secret-manager-rotate-secret/README.md b/functions/secret-manager-rotate-secret/README.md new file mode 100644 index 0000000..47fc5b9 --- /dev/null +++ b/functions/secret-manager-rotate-secret/README.md @@ -0,0 +1,69 @@ +# Rotate RDB Credentials + +This function will rotate the credentials of and RDB database, stored in a secret in the Secret Manager. + +## Requirements + +This example assumes you are familiar with how serverless functions work. If needed, you can check [Scaleway's official documentation](https://www.scaleway.com/en/docs/serverless/functions/quickstart/) + +This example uses the Scaleway Serverless Framework Plugin. Please set up your environment with the requirements stated in the [Scaleway Serverless Framework Plugin](https://github.com/scaleway/serverless-scaleway-functions) before trying out the example. + +An RDB database is required for this to work. The credentials of this database MUST be stored in a secret with the following layout: +```json +{ + "engine": "postgres|mysql", + "username": "db_username", + "password": "db_password", + "host": "db_ip_or_hostname", + "dbname": "db_name", + "port": "db_port" +} +``` + +For the function to work, it needs an API key with the following permissions: +- `SecretManagerFullAccess` +- `RelationalDatabasesFullAccess` + +## Context + +The function will generate a new password for your RDB credentials using the Scaleway API, then it will access the secret where it is stored. After that it will update the RDB credentials with the `username` configured in the secret and the new generated password. Finally it will create a new version of the secret with the new credentials. + +## Setup + +You will need to adjust to your needs the `env` and `secret` settings in the `serverless.yml` file. + +Once your environment is set up, you can run: + +```console +npm install + +serverless deploy +``` + +## Running + +You can use `curl` to trigger your function. It requires the following input, you can store it in a file called `req.json`. +```json +{ + "rdb_instance_id": "your RDB instance ID", + "secret_id": "the secret ID where credentials are stored" +} +``` + +```console +curl -d @req.json +``` + +**Update with the expected output** +The result should be similar to: + +```console +HTTP/2 200 +content-length: 21 +content-type: text/plain +date: Tue, 17 Jan 2023 14:02:46 GMT +server: envoy +x-envoy-upstream-service-time: 222 + +database credentials updated% +``` diff --git a/functions/secret-manager-rotate-secret/go.mod b/functions/secret-manager-rotate-secret/go.mod new file mode 100644 index 0000000..475306d --- /dev/null +++ b/functions/secret-manager-rotate-secret/go.mod @@ -0,0 +1,7 @@ +module secret-manager-rotate-secret + +go 1.20 + +require github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25 + +require gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/functions/secret-manager-rotate-secret/go.sum b/functions/secret-manager-rotate-secret/go.sum new file mode 100644 index 0000000..fd7593f --- /dev/null +++ b/functions/secret-manager-rotate-secret/go.sum @@ -0,0 +1,6 @@ +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25 h1:/8rfZAdFfafRXOgz+ZpMZZWZ5pYggCY9t7e/BvjaBHM= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/functions/secret-manager-rotate-secret/handler.go b/functions/secret-manager-rotate-secret/handler.go new file mode 100644 index 0000000..2f282ff --- /dev/null +++ b/functions/secret-manager-rotate-secret/handler.go @@ -0,0 +1,114 @@ +package secretmanagerrotatesecret + +import ( + "encoding/json" + "fmt" + "net/http" + "secret-manager-rotate-secret/random" + + rdb "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" + secret "github.com/scaleway/scaleway-sdk-go/api/secret/v1beta1" + "github.com/scaleway/scaleway-sdk-go/scw" +) + +type rotateRequest struct { + SecretID string `json:"secret_id"` + RDBInstanceID string `json:"rdb_instance_id"` +} + +type databaseCredentials struct { + Engine string `json:"engine"` + Username string `json:"username"` + Password string `json:"password"` + Hostname string `json:"host"` + DBName string `json:"dbname"` + Port string `json:"port"` +} + +func Handle(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var req rotateRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + client, err := scw.NewClient(scw.WithEnv()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + rdbApi := rdb.NewAPI(client) + secretApi := secret.NewAPI(client) + + // access current secret version to get revision and payload + currentVersion, err := secretApi.AccessSecretVersion(&secret.AccessSecretVersionRequest{ + SecretID: req.SecretID, + Revision: "latest_enabled", + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // generate new password + newPassword, err := random.CreateString(random.StringParams{ + Length: 16, + Upper: true, + MinUpper: 1, + Lower: true, + MinLower: 1, + Numeric: true, + MinNumeric: 1, + Special: true, + MinSpecial: 1, + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // deserialize secret payload to access values + var payload databaseCredentials + err = json.Unmarshal(currentVersion.Data, &payload) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // update RDB user with new password + _, err = rdbApi.UpdateUser(&rdb.UpdateUserRequest{ + InstanceID: req.RDBInstanceID, + Password: scw.StringPtr(string(newPassword)), + Name: payload.Username, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + payload.Password = string(newPassword) + + newData, err := json.Marshal(payload) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // create new version of the secret with same content and password updated + _, err = secretApi.CreateSecretVersion(&secret.CreateSecretVersionRequest{ + SecretID: req.SecretID, + Data: newData, + DisablePrevious: scw.BoolPtr(true), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Fprint(w, "database credentials updated") +} diff --git a/functions/secret-manager-rotate-secret/package.json b/functions/secret-manager-rotate-secret/package.json new file mode 100644 index 0000000..ccecccb --- /dev/null +++ b/functions/secret-manager-rotate-secret/package.json @@ -0,0 +1,15 @@ +{ + "name": "secret-manager-rotate-secret", + "version": "1.0.0", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": {}, + "devDependencies": { + "serverless-scaleway-functions": "^0.4.9" + }, + "description": "" +} diff --git a/functions/secret-manager-rotate-secret/random/string.go b/functions/secret-manager-rotate-secret/random/string.go new file mode 100644 index 0000000..7b8f256 --- /dev/null +++ b/functions/secret-manager-rotate-secret/random/string.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package random + +import ( + "crypto/rand" + "math/big" + "sort" +) + +type StringParams struct { + Length int64 + Upper bool + MinUpper int64 + Lower bool + MinLower int64 + Numeric bool + MinNumeric int64 + Special bool + MinSpecial int64 + OverrideSpecial string +} + +func CreateString(input StringParams) ([]byte, error) { + const numChars = "0123456789" + const lowerChars = "abcdefghijklmnopqrstuvwxyz" + const upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + var specialChars = "!@#$%&*()-_=+[]{}<>:?" + var result []byte + + if input.OverrideSpecial != "" { + specialChars = input.OverrideSpecial + } + + var chars = "" + if input.Upper { + chars += upperChars + } + if input.Lower { + chars += lowerChars + } + if input.Numeric { + chars += numChars + } + if input.Special { + chars += specialChars + } + + minMapping := map[string]int64{ + numChars: input.MinNumeric, + lowerChars: input.MinLower, + upperChars: input.MinUpper, + specialChars: input.MinSpecial, + } + + result = make([]byte, 0, input.Length) + + for k, v := range minMapping { + s, err := generateRandomBytes(&k, v) + if err != nil { + return nil, err + } + result = append(result, s...) + } + + s, err := generateRandomBytes(&chars, input.Length-int64(len(result))) + if err != nil { + return nil, err + } + + result = append(result, s...) + + order := make([]byte, len(result)) + if _, err := rand.Read(order); err != nil { + return nil, err + } + + sort.Slice(result, func(i, j int) bool { + return order[i] < order[j] + }) + + return result, nil +} + +func generateRandomBytes(charSet *string, length int64) ([]byte, error) { + bytes := make([]byte, length) + setLen := big.NewInt(int64(len(*charSet))) + for i := range bytes { + idx, err := rand.Int(rand.Reader, setLen) + if err != nil { + return nil, err + } + bytes[i] = (*charSet)[idx.Int64()] + } + return bytes, nil +} diff --git a/functions/secret-manager-rotate-secret/serverless.yml b/functions/secret-manager-rotate-secret/serverless.yml new file mode 100644 index 0000000..5ae9506 --- /dev/null +++ b/functions/secret-manager-rotate-secret/serverless.yml @@ -0,0 +1,32 @@ +service: secret-manager-rotate-secret +configValidationMode: off +provider: + name: scaleway + runtime: go120 + +plugins: + - serverless-scaleway-functions + +package: + patterns: + - "!node_modules/**" + - "!.gitignore" + - "!.git/**" + +functions: + rotate-secret: + handler: "Handle" + env: + SCW_DEFAULT_ORGANIZATION_ID : "your scalway organization ID" + SCW_DEFAULT_PROJECT_ID : "your scalway project ID" + SCW_DEFAULT_REGION : "fr-par" + secret: + SCW_ACCESS_KEY: "your scaleway access key" + SCW_SECRET_KEY: "your scaleway secret key" + events: + - schedule: + rate: "5 4 1 * *" + # Data passed as input in the request + input: + rdb_instance_id: "your RDB instance ID" + secret_id: "the secret ID where credentials are stored"