Skip to content

Commit cbdc291

Browse files
committed
Use memguard locked buffer for Secret operations
1 parent 4fd5b15 commit cbdc291

8 files changed

Lines changed: 222 additions & 133 deletions

File tree

cmd/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/application-stacks/runtime-component-operator/common"
3737
"github.com/application-stacks/runtime-component-operator/internal/controller"
3838
"github.com/application-stacks/runtime-component-operator/utils"
39+
"github.com/awnumar/memguard"
3940
certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
4041
imagev1 "github.com/openshift/api/image/v1"
4142
routev1 "github.com/openshift/api/route/v1"
@@ -68,6 +69,9 @@ func init() {
6869
}
6970

7071
func main() {
72+
memguard.CatchInterrupt()
73+
defer memguard.Purge()
74+
7175
var metricsAddr string
7276
var enableLeaderElection bool
7377
var probeAddr string

common/secret.go

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,92 @@ package common
22

33
import (
44
"context"
5-
"sync"
5+
"fmt"
66

7+
"github.com/awnumar/memguard"
78
corev1 "k8s.io/api/core/v1"
89
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/types"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
912
)
1013

11-
type SecretResource struct {
12-
secret *corev1.Secret
13-
once sync.Once
14+
type LockedBufferSecret struct {
15+
metav1.TypeMeta `json:",inline"`
16+
metav1.ObjectMeta `json:"metadata,omitempty"`
17+
LockedData SecretMap `json:"lockedData,omitempty"`
1418
}
1519

16-
type WaitableSecretResource struct {
17-
*SecretResource
18-
wg sync.WaitGroup
19-
}
20+
type SecretMap map[string]*memguard.LockedBuffer
2021

21-
// Creates a Secret that clears it's data when recCtx is canceled
22-
func NewSecret(recCtx context.Context, name, namespace string) *corev1.Secret {
23-
secretResource := NewSecretResource(name, namespace)
24-
go func() {
25-
<-recCtx.Done()
26-
secretResource.Clear(nil)
27-
}()
28-
return secretResource.GetSecret()
22+
func (sm SecretMap) Destroy() {
23+
for _, buf := range sm {
24+
buf.Destroy()
25+
}
2926
}
3027

31-
func NewWaitableSecret(recCtx context.Context, name, namespace string) (*corev1.Secret, *sync.WaitGroup) {
32-
waitableSecretResource := NewWaitableSecretResource(name, namespace)
33-
go func() {
34-
<-recCtx.Done()
35-
waitableSecretResource.Clear(waitableSecretResource.GetWaitGroup())
36-
}()
37-
return waitableSecretResource.GetSecret(), waitableSecretResource.GetWaitGroup()
28+
func (sm SecretMap) Get(key string) ([]byte, bool) {
29+
if buf, found := sm[key]; found {
30+
return buf.Bytes(), true
31+
}
32+
return []byte{}, false
3833
}
3934

40-
func NewSecretResource(name, namespace string) *SecretResource {
41-
resource := &SecretResource{
42-
secret: &corev1.Secret{
43-
ObjectMeta: metav1.ObjectMeta{
44-
Name: name,
45-
Namespace: namespace,
46-
},
47-
},
35+
func (lockedBufferSecret LockedBufferSecret) Destroy() {
36+
if lockedBufferSecret.LockedData != nil {
37+
lockedBufferSecret.LockedData.Destroy()
4838
}
49-
return resource
5039
}
5140

52-
func NewWaitableSecretResource(name, namespace string) *WaitableSecretResource {
53-
resource := &WaitableSecretResource{
54-
SecretResource: NewSecretResource(name, namespace),
55-
wg: sync.WaitGroup{},
41+
// Gets a Secret from the k8s client loaded as a LockedBufferSecret
42+
func GetSecret(client client.Client, name string, ns string) (*LockedBufferSecret, error) {
43+
if client == nil {
44+
return nil, fmt.Errorf("the reconciler client could not be found")
45+
}
46+
secret := &corev1.Secret{}
47+
secret.Name = name
48+
secret.Namespace = ns
49+
err := client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: ns}, secret)
50+
if err != nil {
51+
return nil, err
5652
}
57-
return resource
58-
}
5953

60-
func (wsr *WaitableSecretResource) GetWaitGroup() *sync.WaitGroup {
61-
return &wsr.wg
54+
lockedBufferSecret := &LockedBufferSecret{}
55+
lockedBufferSecret.TypeMeta = secret.TypeMeta
56+
lockedBufferSecret.ObjectMeta = secret.ObjectMeta
57+
for secretKey, secretValue := range secret.Data {
58+
lockedBufferSecret.LockedData[secretKey] = memguard.NewBufferFromBytes(secretValue)
59+
}
60+
return lockedBufferSecret, nil
6261
}
6362

64-
func (r *SecretResource) GetSecret() *corev1.Secret {
65-
return r.secret
63+
// Copies a Locked Buffer Secret into a core Secret with a corresponding cleanup func
64+
func CopySecret(in *LockedBufferSecret, out *corev1.Secret) func() {
65+
out.TypeMeta = in.TypeMeta
66+
out.ObjectMeta = in.ObjectMeta
67+
for key, buf := range in.LockedData {
68+
out.Data[key] = buf.Bytes()
69+
}
70+
return func() {
71+
for key := range out.Data {
72+
delete(out.Data, key)
73+
}
74+
out.Data = nil
75+
}
6676
}
6777

68-
func (r *SecretResource) Clear(wg *sync.WaitGroup) {
69-
if r.secret == nil {
70-
return
78+
// Returns the client.Get status of Secret name in namespace ns after clearing sensitive byte array content
79+
func CheckSecret(client client.Client, name string, ns string) error {
80+
if client == nil {
81+
return fmt.Errorf("the reconciler client could not be found")
7182
}
7283

73-
r.once.Do(func() {
74-
if wg != nil {
75-
wg.Wait()
76-
}
77-
for secretKey, secretValue := range r.secret.Data {
78-
clear(secretValue)
79-
delete(r.secret.Data, secretKey)
80-
}
81-
})
84+
secret := &corev1.Secret{}
85+
secret.Name = name
86+
secret.Namespace = ns
87+
err := client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: ns}, secret)
88+
for key, value := range secret.Data {
89+
clear(value)
90+
delete(secret.Data, key)
91+
}
92+
return err
8293
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/application-stacks/runtime-component-operator
33
go 1.26
44

55
require (
6+
github.com/awnumar/memguard v0.23.0
67
github.com/cert-manager/cert-manager v1.19.4
78
github.com/go-logr/logr v1.4.3
89
github.com/openshift/api v0.0.0-20260304172252-b0658d22beea
@@ -27,6 +28,7 @@ require (
2728
)
2829

2930
require (
31+
github.com/awnumar/memcall v0.4.0 // indirect
3032
github.com/beorn7/perks v1.0.1 // indirect
3133
github.com/blang/semver/v4 v4.0.0 // indirect
3234
github.com/blendle/zapdriver v1.3.1 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
22
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
3+
github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
4+
github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w=
5+
github.com/awnumar/memguard v0.23.0 h1:sJ3a1/SWlcuKIQ7MV+R9p0Pvo9CWsMbGZvcZQtmc68A=
6+
github.com/awnumar/memguard v0.23.0/go.mod h1:olVofBrsPdITtJ2HgxQKrEYEMyIBAIciVG4wNnZhW9M=
37
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
48
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
59
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=

utils/hash.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,66 @@ import (
44
"bytes"
55
"encoding/hex"
66
"io"
7+
"runtime"
78
"sort"
89

10+
"github.com/application-stacks/runtime-component-operator/common"
911
"lukechampine.com/blake3"
1012
)
1113

14+
func HashSecretMapData(data common.SecretMap) string {
15+
return hash(data, serializeSecretMapData)
16+
}
17+
1218
func HashData(data map[string][]byte) string {
19+
return hash(data, serializeSecretData)
20+
}
21+
22+
func hash(data any, serializer func(any) []byte) string {
1323
hasher := blake3.New(32, nil)
14-
io.Copy(hasher, bytes.NewReader(serializeSecretData(data)))
24+
secretBytes := serializer(data)
25+
io.Copy(hasher, bytes.NewReader(secretBytes))
26+
// clear secret bytes
27+
for i := range secretBytes {
28+
secretBytes[i] = 0
29+
}
1530
hash := hasher.Sum(nil)
31+
// force gc
32+
hasher = nil
33+
runtime.GC()
1634
return hex.EncodeToString(hash)
1735
}
1836

19-
func serializeSecretData(data map[string][]byte) []byte {
37+
func serializeSecretMapData(data any) []byte {
38+
if _, ok := data.(common.SecretMap); !ok {
39+
return []byte{}
40+
}
41+
dataObj := data.(common.SecretMap)
42+
// sort data keys
43+
dataKeys := []string{}
44+
for k := range dataObj {
45+
dataKeys = append(dataKeys, k)
46+
}
47+
sort.Strings(dataKeys)
48+
// load dataBuffer delimited by a null character for every key-value pair <key>\0<value>\0
49+
dataBuffer := []byte{}
50+
for _, k := range dataKeys {
51+
dataBuffer = append(dataBuffer, []byte(k)...)
52+
dataBuffer = append(dataBuffer, '\000')
53+
dataBuffer = append(dataBuffer, dataObj[k].Bytes()...)
54+
dataBuffer = append(dataBuffer, '\000')
55+
}
56+
return dataBuffer
57+
}
58+
59+
func serializeSecretData(data any) []byte {
60+
if _, ok := data.(map[string][]byte); !ok {
61+
return []byte{}
62+
}
63+
dataObj := data.(map[string][]byte)
2064
// sort data keys
2165
dataKeys := []string{}
22-
for k := range data {
66+
for k := range dataObj {
2367
dataKeys = append(dataKeys, k)
2468
}
2569
sort.Strings(dataKeys)
@@ -28,7 +72,7 @@ func serializeSecretData(data map[string][]byte) []byte {
2872
for _, k := range dataKeys {
2973
dataBuffer = append(dataBuffer, []byte(k)...)
3074
dataBuffer = append(dataBuffer, '\000')
31-
dataBuffer = append(dataBuffer, data[k]...)
75+
dataBuffer = append(dataBuffer, dataObj[k]...)
3276
dataBuffer = append(dataBuffer, '\000')
3377
}
3478
return dataBuffer

0 commit comments

Comments
 (0)