Skip to content

Commit 21b75ff

Browse files
authored
add new function to rotate RDB credentials (#80)
1 parent 2b46245 commit 21b75ff

File tree

9 files changed

+344
-0
lines changed

9 files changed

+344
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Table of Contents:
5757
| **[Typescript with Node runtime](functions/typescript-with-node/README.md)** <br/> A Typescript function using Node runtime. | node18 | [Serverless Framework] |
5858
| **[Serverless Gateway Python Example](functions/serverless-gateway-python/README.md)** <br/> A Python serverless API using Serverless Gateway. | python310 | [Python API Framework] |
5959
| **[Go and Transactional Email](functions/go-mail/README.md)** <br/> A Go function that send emails using Scaleway SDK. | go121 | [Serverless Framework] |
60+
| **[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] |
6061

6162
### 📦 Containers
6263

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
package-lock.json
3+
.serverless/
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Rotate RDB Credentials
2+
3+
This function will rotate the credentials of and RDB database, stored in a secret in the Secret Manager.
4+
5+
## Requirements
6+
7+
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/)
8+
9+
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.
10+
11+
An RDB database is required for this to work. The credentials of this database MUST be stored in a secret with the following layout:
12+
```json
13+
{
14+
"engine": "postgres|mysql",
15+
"username": "db_username",
16+
"password": "db_password",
17+
"host": "db_ip_or_hostname",
18+
"dbname": "db_name",
19+
"port": "db_port"
20+
}
21+
```
22+
23+
For the function to work, it needs an API key with the following permissions:
24+
- `SecretManagerFullAccess`
25+
- `RelationalDatabasesFullAccess`
26+
27+
## Context
28+
29+
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.
30+
31+
## Setup
32+
33+
You will need to adjust to your needs the `env` and `secret` settings in the `serverless.yml` file.
34+
35+
Once your environment is set up, you can run:
36+
37+
```console
38+
npm install
39+
40+
serverless deploy
41+
```
42+
43+
## Running
44+
45+
You can use `curl` to trigger your function. It requires the following input, you can store it in a file called `req.json`.
46+
```json
47+
{
48+
"rdb_instance_id": "your RDB instance ID",
49+
"secret_id": "the secret ID where credentials are stored"
50+
}
51+
```
52+
53+
```console
54+
curl <function URL> -d @req.json
55+
```
56+
57+
**Update with the expected output**
58+
The result should be similar to:
59+
60+
```console
61+
HTTP/2 200
62+
content-length: 21
63+
content-type: text/plain
64+
date: Tue, 17 Jan 2023 14:02:46 GMT
65+
server: envoy
66+
x-envoy-upstream-service-time: 222
67+
68+
database credentials updated%
69+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module secret-manager-rotate-secret
2+
3+
go 1.20
4+
5+
require github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25
6+
7+
require gopkg.in/yaml.v2 v2.4.0 // indirect
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25 h1:/8rfZAdFfafRXOgz+ZpMZZWZ5pYggCY9t7e/BvjaBHM=
2+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
3+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
4+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
5+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
6+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package secretmanagerrotatesecret
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"secret-manager-rotate-secret/random"
8+
9+
rdb "github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
10+
secret "github.com/scaleway/scaleway-sdk-go/api/secret/v1beta1"
11+
"github.com/scaleway/scaleway-sdk-go/scw"
12+
)
13+
14+
type rotateRequest struct {
15+
SecretID string `json:"secret_id"`
16+
RDBInstanceID string `json:"rdb_instance_id"`
17+
}
18+
19+
type databaseCredentials struct {
20+
Engine string `json:"engine"`
21+
Username string `json:"username"`
22+
Password string `json:"password"`
23+
Hostname string `json:"host"`
24+
DBName string `json:"dbname"`
25+
Port string `json:"port"`
26+
}
27+
28+
func Handle(w http.ResponseWriter, r *http.Request) {
29+
defer r.Body.Close()
30+
31+
var req rotateRequest
32+
err := json.NewDecoder(r.Body).Decode(&req)
33+
if err != nil {
34+
http.Error(w, err.Error(), http.StatusBadRequest)
35+
return
36+
}
37+
38+
client, err := scw.NewClient(scw.WithEnv())
39+
if err != nil {
40+
http.Error(w, err.Error(), http.StatusInternalServerError)
41+
return
42+
}
43+
44+
rdbApi := rdb.NewAPI(client)
45+
secretApi := secret.NewAPI(client)
46+
47+
// access current secret version to get revision and payload
48+
currentVersion, err := secretApi.AccessSecretVersion(&secret.AccessSecretVersionRequest{
49+
SecretID: req.SecretID,
50+
Revision: "latest_enabled",
51+
})
52+
if err != nil {
53+
http.Error(w, err.Error(), http.StatusInternalServerError)
54+
return
55+
}
56+
57+
// generate new password
58+
newPassword, err := random.CreateString(random.StringParams{
59+
Length: 16,
60+
Upper: true,
61+
MinUpper: 1,
62+
Lower: true,
63+
MinLower: 1,
64+
Numeric: true,
65+
MinNumeric: 1,
66+
Special: true,
67+
MinSpecial: 1,
68+
})
69+
70+
if err != nil {
71+
http.Error(w, err.Error(), http.StatusInternalServerError)
72+
return
73+
}
74+
75+
// deserialize secret payload to access values
76+
var payload databaseCredentials
77+
err = json.Unmarshal(currentVersion.Data, &payload)
78+
if err != nil {
79+
http.Error(w, err.Error(), http.StatusInternalServerError)
80+
return
81+
}
82+
83+
// update RDB user with new password
84+
_, err = rdbApi.UpdateUser(&rdb.UpdateUserRequest{
85+
InstanceID: req.RDBInstanceID,
86+
Password: scw.StringPtr(string(newPassword)),
87+
Name: payload.Username,
88+
})
89+
if err != nil {
90+
http.Error(w, err.Error(), http.StatusInternalServerError)
91+
return
92+
}
93+
94+
payload.Password = string(newPassword)
95+
96+
newData, err := json.Marshal(payload)
97+
if err != nil {
98+
http.Error(w, err.Error(), http.StatusInternalServerError)
99+
return
100+
}
101+
102+
// create new version of the secret with same content and password updated
103+
_, err = secretApi.CreateSecretVersion(&secret.CreateSecretVersionRequest{
104+
SecretID: req.SecretID,
105+
Data: newData,
106+
DisablePrevious: scw.BoolPtr(true),
107+
})
108+
if err != nil {
109+
http.Error(w, err.Error(), http.StatusInternalServerError)
110+
return
111+
}
112+
113+
fmt.Fprint(w, "database credentials updated")
114+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "secret-manager-rotate-secret",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"test": "echo \"Error: no test specified\" && exit 1"
6+
},
7+
"keywords": [],
8+
"author": "",
9+
"license": "ISC",
10+
"dependencies": {},
11+
"devDependencies": {
12+
"serverless-scaleway-functions": "^0.4.9"
13+
},
14+
"description": ""
15+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package random
5+
6+
import (
7+
"crypto/rand"
8+
"math/big"
9+
"sort"
10+
)
11+
12+
type StringParams struct {
13+
Length int64
14+
Upper bool
15+
MinUpper int64
16+
Lower bool
17+
MinLower int64
18+
Numeric bool
19+
MinNumeric int64
20+
Special bool
21+
MinSpecial int64
22+
OverrideSpecial string
23+
}
24+
25+
func CreateString(input StringParams) ([]byte, error) {
26+
const numChars = "0123456789"
27+
const lowerChars = "abcdefghijklmnopqrstuvwxyz"
28+
const upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
29+
var specialChars = "!@#$%&*()-_=+[]{}<>:?"
30+
var result []byte
31+
32+
if input.OverrideSpecial != "" {
33+
specialChars = input.OverrideSpecial
34+
}
35+
36+
var chars = ""
37+
if input.Upper {
38+
chars += upperChars
39+
}
40+
if input.Lower {
41+
chars += lowerChars
42+
}
43+
if input.Numeric {
44+
chars += numChars
45+
}
46+
if input.Special {
47+
chars += specialChars
48+
}
49+
50+
minMapping := map[string]int64{
51+
numChars: input.MinNumeric,
52+
lowerChars: input.MinLower,
53+
upperChars: input.MinUpper,
54+
specialChars: input.MinSpecial,
55+
}
56+
57+
result = make([]byte, 0, input.Length)
58+
59+
for k, v := range minMapping {
60+
s, err := generateRandomBytes(&k, v)
61+
if err != nil {
62+
return nil, err
63+
}
64+
result = append(result, s...)
65+
}
66+
67+
s, err := generateRandomBytes(&chars, input.Length-int64(len(result)))
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
result = append(result, s...)
73+
74+
order := make([]byte, len(result))
75+
if _, err := rand.Read(order); err != nil {
76+
return nil, err
77+
}
78+
79+
sort.Slice(result, func(i, j int) bool {
80+
return order[i] < order[j]
81+
})
82+
83+
return result, nil
84+
}
85+
86+
func generateRandomBytes(charSet *string, length int64) ([]byte, error) {
87+
bytes := make([]byte, length)
88+
setLen := big.NewInt(int64(len(*charSet)))
89+
for i := range bytes {
90+
idx, err := rand.Int(rand.Reader, setLen)
91+
if err != nil {
92+
return nil, err
93+
}
94+
bytes[i] = (*charSet)[idx.Int64()]
95+
}
96+
return bytes, nil
97+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
service: secret-manager-rotate-secret
2+
configValidationMode: off
3+
provider:
4+
name: scaleway
5+
runtime: go120
6+
7+
plugins:
8+
- serverless-scaleway-functions
9+
10+
package:
11+
patterns:
12+
- "!node_modules/**"
13+
- "!.gitignore"
14+
- "!.git/**"
15+
16+
functions:
17+
rotate-secret:
18+
handler: "Handle"
19+
env:
20+
SCW_DEFAULT_ORGANIZATION_ID : "your scalway organization ID"
21+
SCW_DEFAULT_PROJECT_ID : "your scalway project ID"
22+
SCW_DEFAULT_REGION : "fr-par"
23+
secret:
24+
SCW_ACCESS_KEY: "your scaleway access key"
25+
SCW_SECRET_KEY: "your scaleway secret key"
26+
events:
27+
- schedule:
28+
rate: "5 4 1 * *"
29+
# Data passed as input in the request
30+
input:
31+
rdb_instance_id: "your RDB instance ID"
32+
secret_id: "the secret ID where credentials are stored"

0 commit comments

Comments
 (0)