forked from buttahtoast/subst
-
Notifications
You must be signed in to change notification settings - Fork 1
/
sops.go
397 lines (355 loc) · 13.4 KB
/
sops.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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
package sops
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/bedag/subst/internal/decryptors"
"github.com/bedag/subst/internal/decryptors/sops/kustomize-controller/age"
"github.com/bedag/subst/internal/decryptors/sops/kustomize-controller/awskms"
"github.com/bedag/subst/internal/decryptors/sops/kustomize-controller/azkv"
intkeyservice "github.com/bedag/subst/internal/decryptors/sops/kustomize-controller/keyservice"
"github.com/bedag/subst/internal/decryptors/sops/kustomize-controller/pgp"
"go.mozilla.org/sops/v3"
"go.mozilla.org/sops/v3/aes"
"go.mozilla.org/sops/v3/cmd/sops/common"
"go.mozilla.org/sops/v3/cmd/sops/formats"
"go.mozilla.org/sops/v3/keyservice"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
const (
// DecryptionProviderSOPS is the SOPS provider name.
DecryptionProviderSOPS = "sops"
// DecryptionPGPExt is the extension of the file containing an armored PGP
// key.
DecryptionPGPExt = ".asc"
// DecryptionAgeExt is the extension of the file containing an age key
// file.
DecryptionAgeExt = ".agekey"
// DecryptionVaultTokenFileName is the name of the file containing the
// Hashicorp Vault token.
DecryptionVaultTokenFileName = "sops.vault-token"
// DecryptionAWSKmsFile is the name of the file containing the AWS KMS
// credentials.
DecryptionAWSKmsFile = "sops.aws-kms"
// DecryptionAzureAuthFile is the name of the file containing the Azure
// credentials.
DecryptionAzureAuthFile = "sops.azure-kv"
// DecryptionGCPCredsFile is the name of the file containing the GCP
// credentials.
DecryptionGCPCredsFile = "sops.gcp-kms"
// maxEncryptedFileSize is the max allowed file size in bytes of an encrypted
// file.
maxEncryptedFileSize int64 = 5 << 20
// unsupportedFormat is used to signal no sopsFormatToMarkerBytes format was
// detected by detectFormatFromMarkerBytes.
unsupportedFormat = formats.Format(-1)
)
var (
// sopsFormatToString is the counterpart to
// https://github.com/mozilla/sops/blob/v3.7.2/cmd/sops/formats/formats.go#L16
sopsFormatToString = map[formats.Format]string{
formats.Binary: "binary",
formats.Dotenv: "dotenv",
formats.Ini: "INI",
formats.Json: "JSON",
formats.Yaml: "YAML",
}
// sopsFormatToMarkerBytes contains a list of formats and their byte
// order markers, used to detect if a Secret data field is SOPS' encrypted.
sopsFormatToMarkerBytes = map[formats.Format][]byte{
// formats.Binary is a JSON envelop at encrypted rest
formats.Binary: []byte("\"mac\": \"ENC["),
formats.Dotenv: []byte("sops_mac=ENC["),
formats.Ini: []byte("[sops]"),
formats.Json: []byte("\"mac\": \"ENC["),
formats.Yaml: []byte("mac: ENC["),
}
)
// Decryptor performs decryption operations for a v1.Kustomization.
// The only supported decryption provider at present is
// DecryptionProviderSOPS.
type SOPSDecryptor struct {
// maxFileSize is the max size in bytes a file is allowed to have to be
// decrypted. Defaults to maxEncryptedFileSize.
maxFileSize int64
// checkSopsMac instructs the decryptor to perform the SOPS data integrity
// check using the MAC. Not enabled by default, as arbitrary data gets
// injected into most resources, causing the integrity check to fail.
// Mostly kept around for feature completeness and documentation purposes.
checkSopsMac bool
// gnuPGHome is the absolute path of the GnuPG home directory used to
// decrypt PGP data. When empty, the systems' GnuPG keyring is used.
// When set, ImportKeys() imports found PGP keys into this keyring.
gnuPGHome pgp.GnuPGHome
// ageIdentities is the set of age identities available to the decryptor.
ageIdentities age.ParsedIdentities
// vaultToken is the Hashicorp Vault token used to authenticate towards
// any Vault server.
vaultToken string
// awsCredsProvider is the AWS credentials provider object used to authenticate
// towards any AWS KMS.
awsCredsProvider *awskms.CredsProvider
// azureToken is the Azure credential token used to authenticate towards
// any Azure Key Vault.
azureToken *azkv.Token
// gcpCredsJSON is the JSON credential file of the service account used to
// authenticate towards any GCP KMS.
gcpCredsJSON []byte
// keyServices are the SOPS keyservice.KeyServiceClient's available to the
// decryptor.
keyServices []keyservice.KeyServiceClient
localServiceOnce sync.Once
// Interface decryptor config
Config decryptors.DecryptorConfig
}
// NewDecryptor creates a new Decryptor for the given kustomization.
// gnuPGHome can be empty, in which case the systems' keyring is used.
func NewSOPSDecryptor(config decryptors.DecryptorConfig, gnuPGHome string) *SOPSDecryptor {
return &SOPSDecryptor{
maxFileSize: maxEncryptedFileSize,
gnuPGHome: pgp.GnuPGHome(gnuPGHome),
Config: config,
}
}
// NewTempDecryptor creates a new Decryptor, with a temporary GnuPG
// home directory to Decryptor.ImportKeys() into.
func NewSOPSTempDecryptor(config decryptors.DecryptorConfig) (*SOPSDecryptor, func(), error) {
gnuPGHome, err := pgp.NewGnuPGHome()
if err != nil {
return nil, nil, fmt.Errorf("cannot create keyring: %w", err)
}
cleanup := func() { _ = os.RemoveAll(gnuPGHome.String()) }
return NewSOPSDecryptor(config, gnuPGHome.String()), cleanup, nil
}
// Only call this for Temporary Decryptors
func (d *SOPSDecryptor) RemoveKeyRing() error {
return os.RemoveAll(string(d.gnuPGHome))
}
// IsEncrypted returns true if the given data is encrypted by SOPS.
func (d *SOPSDecryptor) IsEncrypted(data []byte) (bool, error) {
if len(data) == 0 {
return false, nil
}
jdata, err := decryptors.UnmarshalJSONorYAML(data)
if err != nil {
return false, err
}
sopsField := jdata["sops"]
if sopsField == nil || sopsField == "" {
return false, nil
}
return true, nil
}
// Read reads the input data, decrypts it, and returns the decrypted data.
func (d *SOPSDecryptor) Decrypt(data []byte) (content map[string]interface{}, err error) {
content, err = decryptors.UnmarshalJSONorYAML(data)
if err != nil {
return nil, err
}
if !d.Config.SkipDecrypt {
jcontent, err := json.Marshal(content)
if err != nil {
return nil, err
}
data, err = d.SopsDecryptWithFormat(jcontent, formats.Json, formats.Json)
if err != nil {
return nil, err
}
content, err = decryptors.UnmarshalJSONorYAML(data)
if err != nil {
return nil, err
}
}
delete(content, "sops")
return content, nil
}
// AddGPGKey adds given GPG key to the decryptor's keyring.
func (d *SOPSDecryptor) AddGPGKey(key []byte) error {
return d.gnuPGHome.Import(key)
}
// AddAgeKey to the decryptor's identities.
func (d *SOPSDecryptor) AddAgeKey(key []byte) error {
return d.ageIdentities.Import(string(key))
}
// SetVaultToken sets the Vault token for the decryptor.
func (d *SOPSDecryptor) SetVaultToken(token []byte) {
vtoken := string(token)
vtoken = strings.Trim(strings.TrimSpace(vtoken), "\n")
d.vaultToken = vtoken
}
// SetAWSCredentials adds AWS credentials for the decryptor.
// Reference: https://github.com/getsops/sops#aws-kms-encryption-context
func (d *SOPSDecryptor) SetAWSCredentials(token []byte) (err error) {
d.awsCredsProvider, err = awskms.LoadCredsProviderFromYaml(token)
return err
}
// SetAzureAuthFile adds AWS credentials for the decryptor.
func (d *SOPSDecryptor) SetAzureCredentials(config []byte) (err error) {
conf := azkv.AADConfig{}
if err = azkv.LoadAADConfigFromBytes(config, &conf); err != nil {
return err
}
if d.azureToken, err = azkv.TokenFromAADConfig(conf); err != nil {
return err
}
return nil
}
// SetGCPCredentials adds GCP credentials for the decryptor.
func (d *SOPSDecryptor) SetGCPCredentials(config []byte) {
d.gcpCredsJSON = bytes.Trim(config, "\n")
}
func (d *SOPSDecryptor) KeysFromSecret(secretName string, namespace string, client *kubernetes.Clientset, ctx context.Context) (err error) {
// Retrieve Secret
keySecret, err := client.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return &decryptors.MissingKubernetesSecret{Secret: secretName, Namespace: namespace}
} else if err != nil {
return err
}
// Exract all keys from secret
for name, value := range keySecret.Data {
switch filepath.Ext(name) {
case DecryptionPGPExt:
if err = d.AddGPGKey(value); err != nil {
return fmt.Errorf("failed to import data from %s decryption Secret '%s': %w", name, secretName, err)
}
case DecryptionAgeExt:
if err = d.AddAgeKey(value); err != nil {
return fmt.Errorf("failed to import data from %s decryption Secret '%s': %w", name, secretName, err)
}
case filepath.Ext(DecryptionVaultTokenFileName):
// Make sure we have the absolute name
if name == DecryptionVaultTokenFileName {
d.SetVaultToken(value)
}
case filepath.Ext(DecryptionAWSKmsFile):
if name == DecryptionAWSKmsFile {
if d.SetAWSCredentials(value); err != nil {
return fmt.Errorf("failed to import data from %s decryption Secret '%s': %w", name, secretName, err)
}
}
case filepath.Ext(DecryptionAzureAuthFile):
if name == DecryptionAzureAuthFile {
if err = d.SetAzureCredentials(value); err != nil {
return fmt.Errorf("failed to import data from %s decryption Secret '%s': %w", name, secretName, err)
}
}
case filepath.Ext(DecryptionGCPCredsFile):
if name == DecryptionGCPCredsFile {
d.SetGCPCredentials(value)
}
}
}
return nil
}
// SopsDecryptWithFormat attempts to load a SOPS encrypted file using the store
// for the input format, gathers the data key for it from the key service,
// and then decrypts the file data with the retrieved data key.
// It returns the decrypted bytes in the provided output format, or an error.
func (d *SOPSDecryptor) SopsDecryptWithFormat(data []byte, inputFormat, outputFormat formats.Format) (_ []byte, err error) {
defer func() {
// It was discovered that malicious input and/or output instructions can
// make SOPS panic. Recover from this panic and return as an error.
if r := recover(); r != nil {
err = fmt.Errorf("failed to emit encrypted %s file as decrypted %s: %v",
sopsFormatToString[inputFormat], sopsFormatToString[outputFormat], r)
}
}()
store := common.StoreForFormat(inputFormat)
tree, err := store.LoadEncryptedFile(data)
if err != nil {
return nil, sopsUserErr(fmt.Sprintf("failed to load encrypted %s data", sopsFormatToString[inputFormat]), err)
}
for _, group := range tree.Metadata.KeyGroups {
// Sort MasterKeys in the group so offline ones are tried first
sort.SliceStable(group, func(i, j int) bool {
return intkeyservice.IsOfflineMethod(group[i]) && !intkeyservice.IsOfflineMethod(group[j])
})
}
metadataKey, err := tree.Metadata.GetDataKeyWithKeyServices(d.keyServiceServer())
if err != nil {
return nil, sopsUserErr("cannot get sops data key", err)
}
cipher := aes.NewCipher()
mac, err := tree.Decrypt(metadataKey, cipher)
if err != nil {
return nil, sopsUserErr("error decrypting sops tree", err)
}
if d.checkSopsMac {
// Compute the hash of the cleartext tree and compare it with
// the one that was stored in the document. If they match,
// integrity was preserved
// Ref: go.mozilla.org/sops/v3/decrypt/decrypt.go
originalMac, err := cipher.Decrypt(
tree.Metadata.MessageAuthenticationCode,
metadataKey,
tree.Metadata.LastModified.Format(time.RFC3339),
)
if err != nil {
return nil, sopsUserErr("failed to verify sops data integrity", err)
}
if originalMac != mac {
// If the file has an empty MAC, display "no MAC"
if originalMac == "" {
originalMac = "no MAC"
}
return nil, fmt.Errorf("failed to verify sops data integrity: expected mac '%s', got '%s'", originalMac, mac)
}
}
outputStore := common.StoreForFormat(outputFormat)
out, err := outputStore.EmitPlainFile(tree.Branches)
if err != nil {
return nil, sopsUserErr(fmt.Sprintf("failed to emit encrypted %s file as decrypted %s",
sopsFormatToString[inputFormat], sopsFormatToString[outputFormat]), err)
}
return out, err
}
// keyServiceServer returns the SOPS (local) key service clients used to serve
// decryption requests. loadKeyServiceServers() is only configured on the first
// call.
func (d *SOPSDecryptor) keyServiceServer() []keyservice.KeyServiceClient {
d.localServiceOnce.Do(func() {
d.loadKeyServiceServers()
})
return d.keyServices
}
// loadKeyServiceServers loads the SOPS (local) key service clients used to
// serve decryption requests for the current set of Decryptor
// credentials.
func (d *SOPSDecryptor) loadKeyServiceServers() {
serverOpts := []intkeyservice.ServerOption{
intkeyservice.WithGnuPGHome(d.gnuPGHome),
intkeyservice.WithVaultToken(d.vaultToken),
intkeyservice.WithAgeIdentities(d.ageIdentities),
intkeyservice.WithGCPCredsJSON(d.gcpCredsJSON),
}
if d.azureToken != nil {
serverOpts = append(serverOpts, intkeyservice.WithAzureToken{Token: d.azureToken})
}
serverOpts = append(serverOpts, intkeyservice.WithAWSKeys{CredsProvider: d.awsCredsProvider})
server := intkeyservice.NewServer(serverOpts...)
d.keyServices = append(make([]keyservice.KeyServiceClient, 0), keyservice.NewCustomLocalClient(server))
}
func sopsUserErr(msg string, err error) error {
if userErr, ok := err.(sops.UserError); ok {
err = fmt.Errorf(userErr.UserError())
}
return fmt.Errorf("%s: %w", msg, err)
}
func detectFormatFromMarkerBytes(b []byte) formats.Format {
for k, v := range sopsFormatToMarkerBytes {
if bytes.Contains(b, v) {
return k
}
}
return unsupportedFormat
}