Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Table of Contents:
| **[Typescript with Node runtime](functions/typescript-with-node/README.md)** <br/> A Typescript function using Node runtime. | node18 | [Serverless Framework] |
| **[Serverless Gateway Python Example](functions/serverless-gateway-python/README.md)** <br/> A Python serverless API using Serverless Gateway. | python310 | [Python API Framework] |
| **[Go and Transactional Email](functions/go-mail/README.md)** <br/> A Go function that send emails using Scaleway SDK. | go121 | [Serverless Framework] |
| **[Rotate RDB Credentials](functions/secret-manager-rotate-secret/README.md)** <br/> A Go function that rotates RDB credentials stored in Secret Manager. | go120 | [Serverless Framework] |

### 📦 Containers

Expand Down
3 changes: 3 additions & 0 deletions functions/secret-manager-rotate-secret/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
package-lock.json
.serverless/
69 changes: 69 additions & 0 deletions functions/secret-manager-rotate-secret/README.md
Original file line number Diff line number Diff line change
@@ -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 <function URL> -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%
```
7 changes: 7 additions & 0 deletions functions/secret-manager-rotate-secret/go.mod
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions functions/secret-manager-rotate-secret/go.sum
Original file line number Diff line number Diff line change
@@ -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=
114 changes: 114 additions & 0 deletions functions/secret-manager-rotate-secret/handler.go
Original file line number Diff line number Diff line change
@@ -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")
}
15 changes: 15 additions & 0 deletions functions/secret-manager-rotate-secret/package.json
Original file line number Diff line number Diff line change
@@ -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": ""
}
97 changes: 97 additions & 0 deletions functions/secret-manager-rotate-secret/random/string.go
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions functions/secret-manager-rotate-secret/serverless.yml
Original file line number Diff line number Diff line change
@@ -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"