Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encrypt secrets individually - rebased #73

Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ wanting to use `SealedSecret`s with this cluster. The certificate is
printed to the controller log at startup, and available via an HTTP
GET to `/v1/cert.pem` on the controller.

During encryption, the original `Secret` is JSON-encoded and
During encryption, each value in the original `Secret` is
symmetrically encrypted using AES-GCM (AES-256) with a randomly-generated
single-use 32 byte session key. The session key is then asymmetrically
encrypted with the controller's public key using RSA-OAEP (using SHA256), and the
Expand Down
4 changes: 2 additions & 2 deletions cmd/kubeseal/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ func TestSeal(t *testing.T) {
if smeta.GetNamespace() != "myns" {
t.Errorf("Unexpected namespace: %v", smeta.GetNamespace())
}
if len(result.Spec.Data) < 100 {
t.Errorf("Encrypted data is implausibly short: %v", result.Spec.Data)
if len(result.Spec.EncryptedData["foo"]) < 100 {
t.Errorf("Encrypted data is implausibly short: %v", result.Spec.EncryptedData)
}
// NB: See sealedsecret_test.go for e2e crypto test
}
71 changes: 60 additions & 11 deletions pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ func labelFor(o metav1.Object) ([]byte, bool) {
return []byte(label), false
}

// NewSealedSecret creates a new SealedSecret object wrapping the
// provided secret.
func NewSealedSecret(codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey, secret *v1.Secret) (*SealedSecret, error) {
// NewSealedSecretV1 creates a new SealedSecret object wrapping the
// provided secret. This encrypts all the secrets into a single encrypted
// blob and stores it in the `Data` attribute. Keeping this for backward
// compatibility.
func NewSealedSecretV1(codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey, secret *v1.Secret) (*SealedSecret, error) {
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON)
if !ok {
return nil, fmt.Errorf("binary can't serialize JSON")
Expand Down Expand Up @@ -65,6 +67,42 @@ func NewSealedSecret(codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKe
return s, nil
}

// NewSealedSecret creates a new SealedSecret object wrapping the
// provided secret. This encrypts only the values of each secrets
// individually, so secrets can be updated one by one.
func NewSealedSecret(codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey, secret *v1.Secret) (*SealedSecret, error) {
if secret.GetNamespace() == "" {
return nil, fmt.Errorf("Secret must declare a namespace")
}

s := &SealedSecret{
ObjectMeta: metav1.ObjectMeta{
Name: secret.GetName(),
Namespace: secret.GetNamespace(),
},
Spec: SealedSecretSpec{
EncryptedData: map[string][]byte{},
},
}

// RSA-OAEP will fail to decrypt unless the same label is used
// during decryption.
label, clusterWide := labelFor(secret)

for key, value := range secret.Data {
ciphertext, err := crypto.HybridEncrypt(rand.Reader, pubKey, value, label)
if err != nil {
return nil, err
}
s.Spec.EncryptedData[key] = ciphertext
}

if clusterWide {
s.Annotations = map[string]string{SealedSecretClusterWideAnnotation: "true"}
}
return s, nil
}

// Unseal decypts and returns the embedded v1.Secret.
func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, privKey *rsa.PrivateKey) (*v1.Secret, error) {
boolTrue := true
Expand All @@ -76,15 +114,26 @@ func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, privKey *rs
// namespace/name.
label, _ := labelFor(smeta)

plaintext, err := crypto.HybridDecrypt(rand.Reader, privKey, s.Spec.Data, label)
if err != nil {
return nil, err
}

var secret v1.Secret
dec := codecs.UniversalDecoder(secret.GroupVersionKind().GroupVersion())
if err = runtime.DecodeInto(dec, plaintext, &secret); err != nil {
return nil, err
if len(s.Spec.EncryptedData) > 0 {
secret.Data = map[string][]byte{}
for key, value := range s.Spec.EncryptedData {
plaintext, err := crypto.HybridDecrypt(rand.Reader, privKey, value, label)
if err != nil {
return nil, err
}
secret.Data[key] = plaintext
}
} else { // Support decrypting old secrets for backward compatibility
plaintext, err := crypto.HybridDecrypt(rand.Reader, privKey, s.Spec.Data, label)
if err != nil {
return nil, err
}

dec := codecs.UniversalDecoder(secret.GroupVersionKind().GroupVersion())
if err = runtime.DecodeInto(dec, plaintext, &secret); err != nil {
return nil, err
}
}

// Ensure these are set to what we expect
Expand Down
46 changes: 45 additions & 1 deletion pkg/apis/sealed-secrets/v1alpha1/sealedsecret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ func TestSerialize(t *testing.T) {
Namespace: "myns",
},
Spec: SealedSecretSpec{
Data: []byte("xxx"),
EncryptedData: map[string][]byte{
"foo": []byte("secret1"),
"bar": []byte("secret2"),
},
},
}

Expand Down Expand Up @@ -237,3 +240,44 @@ func TestSealRoundTripWithMisMatchClusterWide(t *testing.T) {
t.Fatalf("Unseal did not return expected error: %v", err)
}
}

func TestUnsealingV1Format(t *testing.T) {
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)

SchemeBuilder.AddToScheme(scheme)
v1.SchemeBuilder.AddToScheme(scheme)

rand := testRand()
key, err := rsa.GenerateKey(rand, 2048)
if err != nil {
t.Fatalf("Failed to generate test key: %v", err)
}

secret := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "myname",
Namespace: "myns",
Annotations: map[string]string{
SealedSecretClusterWideAnnotation: "true",
},
},
Data: map[string][]byte{
"foo": []byte("bar"),
},
}

ssecret, err := NewSealedSecretV1(codecs, &key.PublicKey, &secret)
if err != nil {
t.Fatalf("NewSealedSecret returned error: %v", err)
}

secret2, err := ssecret.Unseal(codecs, key)
if err != nil {
t.Fatalf("Unseal returned error: %v", err)
}

if !reflect.DeepEqual(secret.Data, secret2.Data) {
t.Errorf("Unsealed secret != original secret: %v != %v", secret, secret2)
}
}
4 changes: 3 additions & 1 deletion pkg/apis/sealed-secrets/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const (

// SealedSecretSpec is the specification of a SealedSecret
type SealedSecretSpec struct {
Data []byte `json:"data"`
// Data is deprecated and will be removed eventually. Use per-value EncryptedData instead.
Data []byte `json:"data"`
EncryptedData map[string][]byte `json:"encryptedData"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
12 changes: 12 additions & 0 deletions pkg/apis/sealed-secrets/v1alpha1/zz_generated.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ func (in *SealedSecretSpec) DeepCopyInto(out *SealedSecretSpec) {
*out = make([]byte, len(*in))
copy(*out, *in)
}
if in.EncryptedData != nil {
in, out := &in.EncryptedData, &out.EncryptedData
*out = make(map[string][]byte, len(*in))
for key, val := range *in {
if val == nil {
(*out)[key] = nil
} else {
(*out)[key] = make([]byte, len(val))
copy((*out)[key], val)
}
}
}
return
}

Expand Down
18 changes: 0 additions & 18 deletions sealedsecret-crd.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,6 @@ local crd = {
plural: self.singular + "s",
listKind: self.kind + "List",
},
validation: {
openAPIV3Schema: {
"$schema": "http://json-schema.org/draft-04/schema#",
type: "object",
description: "A sealed (encrypted) Secret",
properties: {
spec: {
type: "object",
properties: {
data: {
type: "string",
pattern: "^[A-Za-z0-9+/=]*$", // base64
},
},
},
},
},
},
},
};

Expand Down