Skip to content

Commit 150b3ac

Browse files
committed
Initial git import
1 parent 21801f8 commit 150b3ac

File tree

11 files changed

+1143
-0
lines changed

11 files changed

+1143
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@
1212

1313
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
1414
.glide/
15+
16+
/controller
17+
/ksonnet-seal

Makefile

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
GO = go
2+
GO_FLAGS =
3+
GOFMT = gofmt
4+
5+
# TODO: Simplify this once ./... ignores ./vendor
6+
GO_PACKAGES = ./cmd/... ./api/...
7+
GO_FILES := $(shell find $(shell $(GO) list -f '{{.Dir}}' $(GO_PACKAGES)) -name \*.go)
8+
9+
all: controller ksonnet-seal
10+
11+
controller: $(GO_FILES)
12+
$(GO) build -o $@ $(GO_FLAGS) ./cmd/controller/...
13+
14+
ksonnet-seal: $(GO_FILES)
15+
$(GO) build -o $@ $(GO_FLAGS) ./cmd/ksonnet-seal/...
16+
17+
test:
18+
$(GO) test $(GO_FLAGS) $(GO_PACKAGES)
19+
20+
vet:
21+
$(GO) vet $(GO_FLAGS) $(GO_PACKAGES)
22+
23+
fmt:
24+
$(GOFMT) -s -w $(GO_FILES)
25+
26+
clean:
27+
$(RM) ./controller ./ksonnet-seal
28+
29+
.PHONY: all test clean vet fmt

apis/v1alpha1/register.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package v1alpha1
2+
3+
import (
4+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
"k8s.io/apimachinery/pkg/runtime"
6+
"k8s.io/apimachinery/pkg/runtime/schema"
7+
"k8s.io/client-go/pkg/api"
8+
)
9+
10+
// GroupName is the group name used in this package
11+
const GroupName = "ksonnet.io"
12+
13+
var (
14+
// SchemeGroupVersion is the group version used to register these objects
15+
SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
16+
17+
// SchemeBuilder adds this group to scheme
18+
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
19+
)
20+
21+
func init() {
22+
SchemeBuilder.AddToScheme(api.Scheme)
23+
}
24+
25+
func addKnownTypes(scheme *runtime.Scheme) error {
26+
scheme.AddKnownTypes(SchemeGroupVersion,
27+
&SealedSecret{},
28+
&SealedSecretList{},
29+
)
30+
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
31+
return nil
32+
}

apis/v1alpha1/sealedsecret.go

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package v1alpha1
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rsa"
7+
"crypto/sha256"
8+
"encoding/binary"
9+
"encoding/json"
10+
"errors"
11+
"fmt"
12+
"io"
13+
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/runtime"
16+
"k8s.io/apimachinery/pkg/runtime/schema"
17+
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
18+
"k8s.io/client-go/pkg/api/v1"
19+
)
20+
21+
const (
22+
// SealedSecretName is the name used in SealedSecret TPR
23+
SealedSecretName = "sealed-secret." + GroupName
24+
// SealedSecretPlural is the collection plural used with SealedSecret API
25+
SealedSecretPlural = "sealedsecrets"
26+
27+
sessionKeyBytes = 32
28+
)
29+
30+
// ErrTooShort indicates the provided data is too short to be valid
31+
var ErrTooShort = errors.New("SealedSecret data is too short")
32+
33+
// SealedSecretSpec is the specification of a SealedSecret
34+
type SealedSecretSpec struct {
35+
Data []byte `json:"data"`
36+
}
37+
38+
// SealedSecret is the K8s representation of a "sealed Secret" - a
39+
// regular k8s Secret that has been sealed (encrypted) using the
40+
// controller's key.
41+
type SealedSecret struct {
42+
metav1.TypeMeta `json:",inline"`
43+
// Note, can't use implicit object here:
44+
// https://github.com/kubernetes/client-go/issues/8
45+
Metadata metav1.ObjectMeta `json:"metadata"`
46+
47+
Spec SealedSecretSpec `json:"spec"`
48+
}
49+
50+
// SealedSecretList represents a list of SealedSecrets
51+
type SealedSecretList struct {
52+
metav1.TypeMeta `json:",inline"`
53+
Metadata metav1.ListMeta `json:"metadata"`
54+
55+
Items []SealedSecret `json:"items"`
56+
}
57+
58+
// GetObjectKind is required for Object interface
59+
func (s *SealedSecret) GetObjectKind() schema.ObjectKind {
60+
return &s.TypeMeta
61+
}
62+
63+
// GetObjectMeta is required for ObjectMetaAccessor interface
64+
func (s *SealedSecret) GetObjectMeta() metav1.Object {
65+
return &s.Metadata
66+
}
67+
68+
// GetObjectKind is required for Object interface
69+
func (sl *SealedSecretList) GetObjectKind() schema.ObjectKind {
70+
return &sl.TypeMeta
71+
}
72+
73+
// GetListMeta is required for ListMetaAccessor interface
74+
func (sl *SealedSecretList) GetListMeta() metav1.List {
75+
return &sl.Metadata
76+
}
77+
78+
func labelFor(o metav1.Object) []byte {
79+
label := fmt.Sprintf("%s/%s", o.GetNamespace(), o.GetName())
80+
return []byte(label)
81+
}
82+
83+
func hybridEncrypt(rnd io.Reader, pubKey *rsa.PublicKey, plaintext, label []byte) ([]byte, error) {
84+
// Generate a random symmetric key
85+
sessionKey := make([]byte, sessionKeyBytes)
86+
if _, err := io.ReadFull(rnd, sessionKey); err != nil {
87+
return nil, err
88+
}
89+
90+
block, err := aes.NewCipher(sessionKey)
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
aed, err := cipher.NewGCM(block)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
// Encrypt symmetric key
101+
rsaCiphertext, err := rsa.EncryptOAEP(sha256.New(), rnd, pubKey, sessionKey, label)
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
// First 2 bytes are RSA ciphertext length, so we can separate
107+
// all the pieces later.
108+
ciphertext := make([]byte, 2)
109+
binary.BigEndian.PutUint16(ciphertext, uint16(len(rsaCiphertext)))
110+
ciphertext = append(ciphertext, rsaCiphertext...)
111+
112+
// SessionKey is random (and single-use), so zero nonce is ok
113+
zeroNonce := make([]byte, aed.NonceSize())
114+
115+
// Append symmetrically encrypted Secret
116+
ciphertext = aed.Seal(ciphertext, zeroNonce, plaintext, nil)
117+
118+
return ciphertext, nil
119+
}
120+
121+
func hybridDecrypt(rnd io.Reader, privKey *rsa.PrivateKey, ciphertext, label []byte) ([]byte, error) {
122+
if len(ciphertext) < 2 {
123+
return nil, ErrTooShort
124+
}
125+
rsaLen := int(binary.BigEndian.Uint16(ciphertext))
126+
if len(ciphertext) < rsaLen+2 {
127+
return nil, ErrTooShort
128+
}
129+
130+
rsaCiphertext := ciphertext[2 : rsaLen+2]
131+
aesCiphertext := ciphertext[rsaLen+2:]
132+
133+
sessionKey, err := rsa.DecryptOAEP(sha256.New(), rnd, privKey, rsaCiphertext, label)
134+
if err != nil {
135+
return nil, err
136+
}
137+
138+
block, err := aes.NewCipher(sessionKey)
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
aed, err := cipher.NewGCM(block)
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
// Key is random (and single-use), so zero nonce is ok
149+
zeroNonce := make([]byte, aed.NonceSize())
150+
151+
plaintext, err := aed.Open(nil, zeroNonce, aesCiphertext, nil)
152+
if err != nil {
153+
return nil, err
154+
}
155+
156+
return plaintext, nil
157+
}
158+
159+
// NewSealedSecret creates a new SealedSecret object wrapping the
160+
// provided secret.
161+
func NewSealedSecret(codecs runtimeserializer.CodecFactory, rnd io.Reader, pubKey *rsa.PublicKey, secret *v1.Secret) (*SealedSecret, error) {
162+
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON)
163+
if !ok {
164+
return nil, fmt.Errorf("binary can't serialize JSON")
165+
}
166+
167+
if secret.GetNamespace() == "" {
168+
return nil, fmt.Errorf("Secret must declare a namespace")
169+
}
170+
171+
codec := codecs.EncoderForVersion(info.Serializer, v1.SchemeGroupVersion)
172+
plaintext, err := runtime.Encode(codec, secret)
173+
if err != nil {
174+
return nil, err
175+
}
176+
177+
// RSA-OAEP will fail to decrypt unless the same label is used
178+
// during decryption.
179+
label := labelFor(secret)
180+
181+
ciphertext, err := hybridEncrypt(rnd, pubKey, plaintext, label)
182+
if err != nil {
183+
return nil, err
184+
}
185+
186+
return &SealedSecret{
187+
Metadata: metav1.ObjectMeta{
188+
Name: secret.GetName(),
189+
Namespace: secret.GetNamespace(),
190+
},
191+
Spec: SealedSecretSpec{
192+
Data: ciphertext,
193+
},
194+
}, nil
195+
}
196+
197+
// Unseal decypts and returns the embedded v1.Secret.
198+
func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, rnd io.Reader, privKey *rsa.PrivateKey) (*v1.Secret, error) {
199+
boolTrue := true
200+
smeta := s.GetObjectMeta()
201+
202+
// This will fail to decrypt unless the same label was used
203+
// during encryption. This check ensures that we can't be
204+
// tricked into decrypting a sealed secret into an unexpected
205+
// namespace/name.
206+
label := labelFor(smeta)
207+
208+
plaintext, err := hybridDecrypt(rnd, privKey, s.Spec.Data, label)
209+
if err != nil {
210+
return nil, err
211+
}
212+
213+
var secret v1.Secret
214+
dec := codecs.UniversalDecoder(secret.GroupVersionKind().GroupVersion())
215+
if err = runtime.DecodeInto(dec, plaintext, &secret); err != nil {
216+
return nil, err
217+
}
218+
219+
// Ensure these are set to what we expect
220+
secret.SetNamespace(smeta.GetNamespace())
221+
secret.SetName(smeta.GetName())
222+
223+
// Refer back to owning SealedSecret
224+
ownerRefs := []metav1.OwnerReference{
225+
metav1.OwnerReference{
226+
APIVersion: s.GetObjectKind().GroupVersionKind().GroupVersion().String(),
227+
Kind: s.GetObjectKind().GroupVersionKind().Kind,
228+
Name: smeta.GetName(),
229+
UID: smeta.GetUID(),
230+
Controller: &boolTrue,
231+
},
232+
}
233+
secret.SetOwnerReferences(ownerRefs)
234+
235+
return &secret, nil
236+
}
237+
238+
// The code below is used only to work around a known problem with third-party
239+
// resources and ugorji. If/when these issues are resolved, the code below
240+
// should no longer be required.
241+
242+
// SealedSecretListCopy is a workaround for a ugorji issue
243+
type SealedSecretListCopy SealedSecretList
244+
245+
// SealedSecretCopy is a workaround for a ugorji issue
246+
type SealedSecretCopy SealedSecret
247+
248+
// UnmarshalJSON ensures SealedSecret objects can be decoded from JSON successfully
249+
func (s *SealedSecret) UnmarshalJSON(data []byte) error {
250+
tmp := SealedSecretCopy{}
251+
err := json.Unmarshal(data, &tmp)
252+
if err != nil {
253+
return err
254+
}
255+
tmp2 := SealedSecret(tmp)
256+
*s = tmp2
257+
return nil
258+
}
259+
260+
// UnmarshalJSON ensures SealedSecretList objects can be decoded from JSON successfully
261+
func (sl *SealedSecretList) UnmarshalJSON(data []byte) error {
262+
tmp := SealedSecretListCopy{}
263+
err := json.Unmarshal(data, &tmp)
264+
if err != nil {
265+
return err
266+
}
267+
tmp2 := SealedSecretList(tmp)
268+
*sl = tmp2
269+
return nil
270+
}

0 commit comments

Comments
 (0)