mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-10 08:20:59 +00:00
A secret generator using sops.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.mozilla.org/sops/decrypt"
|
||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||
"sigs.k8s.io/kustomize/pkg/resmap"
|
||||
"sigs.k8s.io/kustomize/pkg/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Loads secrets from a sops-encoded file.
|
||||
// See https://github.com/mozilla/sops
|
||||
// Based on https://github.com/Agilicus/kustomize-sops
|
||||
// and the sibling example SecretsFromDatabase.
|
||||
type plugin struct {
|
||||
rf *resmap.Factory
|
||||
ldr ifc.Loader
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||
File string `json:"file,omitempty" file:"name,omitempty"`
|
||||
// List of keys to extract from secret map.
|
||||
Keys []string `json:"keys,omitempty" yaml:"keys,omitempty"`
|
||||
}
|
||||
|
||||
//noinspection GoUnusedGlobalVariable
|
||||
//nolint: golint
|
||||
var KustomizePlugin plugin
|
||||
|
||||
func (p *plugin) Config(
|
||||
ldr ifc.Loader, rf *resmap.Factory, c []byte) error {
|
||||
p.rf = rf
|
||||
p.ldr = ldr
|
||||
return yaml.Unmarshal(c, p)
|
||||
}
|
||||
|
||||
func (p *plugin) Generate() (resmap.ResMap, error) {
|
||||
secrets, err := p.loadSecretsViaSops()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.makeK8sSecret(secrets)
|
||||
}
|
||||
|
||||
func (p *plugin) loadSecretsViaSops() (secrets map[string]string, err error) {
|
||||
bytes, err := p.ldr.Load(p.File)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "trouble reading file %s", p.File)
|
||||
}
|
||||
var yamlMap []byte
|
||||
if isFakeEncryptedData(bytes) {
|
||||
yamlMap = []byte(fakeDecryptedData)
|
||||
} else {
|
||||
yamlMap, err = decrypt.Data(bytes, "yaml")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "decrypting content from %s", p.File)
|
||||
}
|
||||
}
|
||||
secrets = make(map[string]string)
|
||||
err = yaml.Unmarshal(yamlMap, &secrets)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(
|
||||
err, "unmarshal failure from '%s'", string(yamlMap))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *plugin) makeK8sSecret(
|
||||
secrets map[string]string) (resmap.ResMap, error) {
|
||||
args := types.SecretArgs{}
|
||||
args.Name = p.Name
|
||||
args.Namespace = p.Namespace
|
||||
for _, k := range p.Keys {
|
||||
if v, ok := secrets[k]; ok {
|
||||
args.LiteralSources = append(
|
||||
args.LiteralSources, k+"="+v)
|
||||
}
|
||||
}
|
||||
return p.rf.FromSecretArgs(p.ldr, nil, args)
|
||||
}
|
||||
|
||||
// See test for justification of this hackery.
|
||||
// The test is meant to just cover plugin behavior,
|
||||
// and assume that sops works. There's currently
|
||||
// no way to inject a "mock" sops into this plugin.
|
||||
const fakeDecryptedData = `
|
||||
VEGETABLE: carrot
|
||||
ROCKET: saturn-v
|
||||
FRUIT: apple
|
||||
CAR: dymaxion
|
||||
`
|
||||
|
||||
func isFakeEncryptedData(bytes []byte) bool {
|
||||
return strings.Contains(string(bytes), "__ELIDED_FOR_KUSTOMIZE_TEST__")
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/pkg/kusttest"
|
||||
"sigs.k8s.io/kustomize/plugin"
|
||||
)
|
||||
|
||||
func TestSopsEncodedSecretsPlugin(t *testing.T) {
|
||||
tc := plugin.NewEnvForTest(t).Set()
|
||||
defer tc.Reset()
|
||||
|
||||
tc.BuildGoPlugin(
|
||||
"someteam.example.com", "v1", "SopsEncodedSecrets")
|
||||
|
||||
th := kusttest_test.NewKustTestPluginHarness(t, "/app")
|
||||
|
||||
/*
|
||||
|
||||
# Writing a portable test for sops is problematic,
|
||||
# because sops decoding assumes access to a local
|
||||
# private key in some form, and these test need
|
||||
# to run anywhere, and they don't use a real file
|
||||
# system. Need to revisit this;
|
||||
# maybe we can stick the private key in an ENV var?
|
||||
# And use GPG instead of gcp_kms?
|
||||
|
||||
# To try this plugin by itself with real data
|
||||
# in Google cloud kms, do the following:
|
||||
|
||||
gcloud kms keyrings create sops --location global
|
||||
gcloud kms keys create sops-key --location global \
|
||||
--keyring sops --purpose encryption
|
||||
gcloud kms keys list --location global --keyring sops
|
||||
|
||||
project=$(\
|
||||
gcloud kms keys list --location global --keyring sops |\
|
||||
grep GOOGLE | cut -d" " -f1)
|
||||
echo $project
|
||||
|
||||
go get -u go.mozilla.org/sops/cmd/sops
|
||||
|
||||
cat <<'EOF' >/tmp/sec_clear.yaml
|
||||
VEGETABLE: carrot
|
||||
ROCKET: saturn-v
|
||||
FRUIT: apple
|
||||
CAR: dymaxion
|
||||
EOF
|
||||
|
||||
# Put the output of the following command into
|
||||
# the encodedFileContent constant below:
|
||||
sops --encrypt --gcp-kms $project /tmp/sec_clear.yaml
|
||||
|
||||
*/
|
||||
const encodedFileContent = `
|
||||
VEGETABLE: ENC[AES256_GCM,data:9mKo4gCm,iv:nkhvWPDbMkDeLXAhTxQOsCaz3ACAx4VS9CLR3tGe5zI=,tag:KIY4z/eE3DFnKHbHHB0ytQ==,type:str]
|
||||
ROCKET: ENC[AES256_GCM,data:6C7vnZYkh+Q=,iv:66/EAqulH7OtMMvSyMZSL5ZbktEm4Yj5S7g/Zb+XgUk=,tag:yEaxZs57fKn7Uebk+ouDDw==,type:str]
|
||||
FRUIT: ENC[AES256_GCM,data:2a/KQxA=,iv:7GmWqc6uA6h539DQVpGq8m0WZLAUi9jzZ6iQAnDEY0s=,tag:ItvY4ziCEW3yNLo/YKMxnw==,type:str]
|
||||
CAR: ENC[AES256_GCM,data:SZFq30w5NZE=,iv:paZ+ghcYoIVIvuGvKP6K6+K7hIgS/l3KgoBxjzjIBHs=,tag:iNL2kvYMppDRXuybmsUFRw==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms:
|
||||
- resource_id: projects/__ELIDED_FOR_KUSTOMIZE_TEST__/locations/global/keyRings/sops/cryptoKeys/sops-key
|
||||
created_at: '2019-06-19T22:32:52Z'
|
||||
enc: __ELIDED_FOR_KUSTOMIZE_TEST__=
|
||||
azure_kv: []
|
||||
lastmodified: '2019-06-19T22:32:52Z'
|
||||
mac: ENC[AES256_GCM,data:__ELIDED_FOR_KUSTOMIZE_TEST__:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.3.1
|
||||
`
|
||||
|
||||
th.WriteF("/app/mySecrets.yaml", encodedFileContent)
|
||||
|
||||
m := th.LoadAndRunGenerator(`
|
||||
apiVersion: someteam.example.com/v1
|
||||
kind: SopsEncodedSecrets
|
||||
metadata:
|
||||
name: mySecretGenerator
|
||||
name: forbiddenValues
|
||||
namespace: production
|
||||
file: mySecrets.yaml
|
||||
keys:
|
||||
- ROCKET
|
||||
- CAR
|
||||
`)
|
||||
th.AssertActualEqualsExpected(m, `
|
||||
apiVersion: v1
|
||||
data:
|
||||
CAR: ZHltYXhpb24=
|
||||
ROCKET: c2F0dXJuLXY=
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: forbiddenValues
|
||||
namespace: production
|
||||
type: Opaque
|
||||
`)
|
||||
}
|
||||
Reference in New Issue
Block a user