/
locksmith.go
315 lines (258 loc) · 9.33 KB
/
locksmith.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
package locksmith
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"strings"
"google.golang.org/api/iam/v1"
"google.golang.org/api/iterator"
iampb "cloud.google.com/go/iam/apiv1/iampb"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
)
// Directive is used to tell the 'locksmith' what operations to perform.
type Directive struct {
// In a future version, this field will be used to allow
// for specifying whether a GCP service account key or
// API Key is the target. Support may be extended for all
// secrets/keys on GCP that developers need/want to rotate.
RotationType string `json:"rotationType,omitempty"`
// The service account email whose keys will be rotated
ServiceAccountEmail string `json:"serviceAccountEmail"`
// The application service account that needs access to the secret
ApplicationServiceAccount string `json:"applicationServiceAccount"`
// Option to disable the secret version. If true, all previous versions
// of the secret will be disabled.
DisableSecretVersions bool `json:"disableSecretVersions,omitempty"`
// Option to disable the key. If true all previous serviceAccount
// keys will be disabled.
DisableServiceAccountKeys bool `json:"disableServiceAccountKeys,omitempty"`
// The name of the secret. ex: my-prod-secret
// If omitted, a new secret will be created, unless an
// existing secret can be found that is tied to the same service account.
SecretName string `json:"secretName,omitempty"`
}
// CreateServiceAccountKey creates a service account key, and if DisableServiceAccountKeys
// is set to 'true' in the directive, it will disable all other service account keys for that service
// account. It will return one of []byte or error. The []byte (the KeyFile) contains the key material
// of the service account. This should be treated as a secret and should only ever be placed in secret manager.
func CreateServiceAccountKey(ctx context.Context, iamService *iam.Service, serviceAccountEmail string, disableAction bool) (*iam.ServiceAccountKey, error) {
log.Println("Starting the process to create service account key...")
serviceAccount := fmt.Sprintf("projects/-/serviceAccounts/%v", serviceAccountEmail)
// This will disable the service account keys if the directive states to do so.
if disableAction {
disableServiceAccountKeys(iamService, serviceAccount)
}
request := &iam.CreateServiceAccountKeyRequest{}
key, err := iamService.Projects.ServiceAccounts.Keys.Create(serviceAccount, request).Do()
if err != nil {
return nil, err
}
log.Printf("Created service account key: %v", serviceAccount)
return key, err
}
// Disable any existing keys for the service account.
func disableServiceAccountKeys(iamService *iam.Service, serviceAccount string) error {
resp, err := iamService.Projects.ServiceAccounts.Keys.List(serviceAccount).Do()
if err != nil {
return err
}
var disable iam.DisableServiceAccountKeyRequest
for _, v := range resp.Keys {
if !v.Disabled {
_, err = iamService.Projects.ServiceAccounts.Keys.Disable(v.Name, &disable).Do()
if err != nil {
return err
}
log.Printf("Disabled the key: %v", v.Name)
}
}
return err
}
// Create a new secret version and vaults the given value in that version.
// If DisableSecretVersions is set to 'true' in the Directive, all other
// version of the secret will be disabled.
func VaultKey(ctx context.Context, sm *secretmanager.Client, key []byte, secretName string, disableSecretAction bool) error {
log.Println("Starting the key vaulting process...")
if disableSecretAction {
disableSecretVersions(ctx, sm, secretName)
}
req := &secretmanagerpb.AddSecretVersionRequest{
Payload: &secretmanagerpb.SecretPayload{
Data: key,
},
Parent: secretName,
}
version, err := sm.AddSecretVersion(ctx, req)
if err != nil {
return err
}
log.Printf("Created secret version: %v", version.Name)
return err
}
// Disables all secret versions for a given secret.
func disableSecretVersions(ctx context.Context, sm *secretmanager.Client, secret string) error {
log.Println("Checking if there are previous versions to disable...")
var listRequest secretmanagerpb.ListSecretVersionsRequest
listRequest.Parent = secret
it := sm.ListSecretVersions(ctx, &listRequest)
for {
resp, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
if resp.State == 1 {
disableSecretVersion := &secretmanagerpb.DisableSecretVersionRequest{}
disableSecretVersion.Name = resp.Name
version, err := sm.DisableSecretVersion(ctx, disableSecretVersion)
if err != nil {
return err
}
log.Printf("Disabled version: %v", version.Name)
}
}
return nil
}
// Creates a secret in the given projectID and labels it.
func createSecret(ctx context.Context, sm *secretmanager.Client, projectID string, serviceAccountEmail string) (*secretmanagerpb.Secret, error) {
s := fmt.Sprintf("lsm-%v-%v", projectID, rand.Intn(100000))
labels := make(map[string]string)
labels["islsm"] = "true"
labels["serviceaccountemail"] = strings.ReplaceAll(strings.ReplaceAll(serviceAccountEmail, "@", "-"), ".", "-")
createSecretRequest := &secretmanagerpb.CreateSecretRequest{
Parent: fmt.Sprintf("projects/%v", projectID),
SecretId: s,
Secret: &secretmanagerpb.Secret{
Labels: labels,
Replication: &secretmanagerpb.Replication{
Replication: &secretmanagerpb.Replication_Automatic_{
Automatic: &secretmanagerpb.Replication_Automatic{}}},
},
}
resp, err := sm.CreateSecret(ctx, createSecretRequest)
if err != nil {
return nil, err
}
return resp, nil
}
// Checks if a secret exists or not for the given service account by checking secret labels that may have the email
// address of the service account. If a secret has a matching label, we don't need to create a new secret for the new
// service account key.
func checkForSecret(ctx context.Context, sm *secretmanager.Client, projectID string, serviceAccountEmail string) (*secretmanagerpb.Secret, bool, error) {
req := &secretmanagerpb.ListSecretsRequest{
Parent: fmt.Sprintf("projects/%v", projectID),
Filter: fmt.Sprintf("labels.islsm=true AND labels.serviceaccountemail=%v", strings.ReplaceAll(strings.ReplaceAll(serviceAccountEmail, "@", "-"), ".", "-")),
}
it := sm.ListSecrets(context.TODO(), req)
for {
resp, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, false, err
}
if resp.Name != "" {
return resp, true, nil
}
}
return nil, false, nil
}
// TODO: Implement a process that will allow for rotating all service account keys
// by listing all projects, all SAs in those projects, and then queueing them to be rotated
// by existing functions.
// Authoritative grant to access the secret data. Overwrites any existing policy.
func grantSecretAccessor(ctx context.Context, sm *secretmanager.Client, secretName string, applicationServiceAccount string) error {
req := &iampb.SetIamPolicyRequest{
Resource: secretName,
Policy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Members: []string{"serviceAccount:" + applicationServiceAccount},
Role: "roles/secretmanager.secretAccessor",
},
},
},
}
pol, err := sm.SetIamPolicy(ctx, req)
if err != nil {
return err
}
log.Printf("new policy was created for secret: %v", pol)
return nil
}
// Ensures required values are provided in the directive.
func (d Directive) validateDirective() (bool, error) {
if d.ServiceAccountEmail == "" || d.ApplicationServiceAccount == "" {
return false, fmt.Errorf("both ServiceAccountEmail and ApplicationServiceAccount email must be provided.ServiceAccountEmail received was %v and ApplicationServiceAccount provided was %v", d.ServiceAccountEmail, d.ApplicationServiceAccount)
}
return true, nil
}
// Cloud Function entrypoint.
func Handler(w http.ResponseWriter, r *http.Request) {
var d Directive
err := json.NewDecoder(r.Body).Decode(&d)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
sm, err := secretmanager.NewClient(ctx)
if err != nil {
log.Fatal(err)
}
iamService, err := iam.NewService(ctx)
if err != nil {
log.Fatal(err)
}
switch isValid, err := d.validateDirective(); isValid {
case true:
serviceAccountKey, err := CreateServiceAccountKey(ctx, iamService, d.ServiceAccountEmail, d.DisableServiceAccountKeys)
if err != nil {
log.Fatal(err)
}
projectID := os.Getenv("SecureStoreProjectID")
// Decode private key data to []byte
privateKeyData, err := base64.StdEncoding.DecodeString(serviceAccountKey.PrivateKeyData)
if err != nil {
log.Fatal(err)
}
secret, exists, err := checkForSecret(ctx, sm, projectID, d.ServiceAccountEmail)
if err != nil {
log.Fatal(err)
}
switch exists {
case true:
err = VaultKey(ctx, sm, privateKeyData, secret.Name, d.DisableSecretVersions)
if err != nil {
log.Fatal(err)
}
err = grantSecretAccessor(ctx, sm, secret.Name, d.ApplicationServiceAccount)
if err != nil {
log.Fatal(err)
}
default:
secret, err := createSecret(ctx, sm, projectID, d.ServiceAccountEmail)
if err != nil {
log.Fatal(err)
}
err = VaultKey(ctx, sm, privateKeyData, secret.Name, false)
if err != nil {
log.Fatal(err)
}
err = grantSecretAccessor(ctx, sm, secret.Name, d.ApplicationServiceAccount)
if err != nil {
log.Fatal(err)
}
}
default:
log.Fatal(err)
}
}