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

chore(secrets): Abstract secret implementation #13953

Merged
merged 4 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions cmd/telegraf/cmd_secretstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import (
"sort"
"strings"

"github.com/awnumar/memguard"
"github.com/urfave/cli/v2"
"golang.org/x/term"

"github.com/influxdata/telegraf/config"
)

func processFilterOnlySecretStoreFlags(ctx *cli.Context) Filters {
Expand Down Expand Up @@ -119,7 +118,7 @@ To also reveal the actual secret, i.e. the value, you can pass the
}
}
_, _ = fmt.Printf(" %-30s %s\n", k, string(v))
config.ReleaseSecret(v)
memguard.WipeBytes(v)
}
}

Expand Down
127 changes: 79 additions & 48 deletions config/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"strings"
"sync/atomic"

"github.com/awnumar/memguard"

"github.com/influxdata/telegraf"
)

Expand All @@ -26,17 +24,67 @@ var secretStorePattern = regexp.MustCompile(`^\w+$`)
// in a secret-store.
var secretPattern = regexp.MustCompile(`@\{(\w+:\w+)\}`)

// secretCount is the number of secrets use in Telegraf
var secretCount atomic.Int64

// selectedImpl is the configured implementation for secrets
var selectedImpl secretImpl = &protectedSecretImpl{}

// secretImpl represents an abstraction for different implementations of secrets
type secretImpl interface {
Container(secret []byte) secretContainer
EmptyBuffer() SecretBuffer
Wipe(secret []byte)
}

// secretContainer represents an abstraction of the container holding the
// actual secret value
type secretContainer interface {
Destroy()
Equals(ref []byte) (bool, error)
Buffer() (SecretBuffer, error)
AsBuffer(secret []byte) SecretBuffer
Replace(secret []byte)
}

// SecretBuffer allows to access the content of the secret
type SecretBuffer interface {
// Size returns the length of the buffer content
Size() int
// Grow will grow the capacity of the underlying buffer to the given size
Grow(capacity int)
// Bytes returns the content of the buffer as bytes.
// NOTE: The returned bytes shall NOT be accessed after destroying the
// buffer using 'Destroy()' as the underlying the memory area might be
// wiped and invalid.
Bytes() []byte
// TemporaryString returns the content of the buffer as a string.
// NOTE: The returned String shall NOT be accessed after destroying the
// buffer using 'Destroy()' as the underlying the memory area might be
// wiped and invalid.
TemporaryString() string
// String returns a copy of the underlying buffer's content as string.
// It is safe to use the returned value after destroying the buffer.
String() string
// Destroy will wipe the buffer's content and destroy the underlying
// buffer. Do not access the buffer after destroying it.
Destroy()
}

// Secret safely stores sensitive data such as a password or token
type Secret struct {
enclave *memguard.Enclave
// container is the implementation for holding the secret. It can be
// protected or not depending on the concrete implementation.
container secretContainer

// resolvers are the functions for resolving a given secret-id (key)
resolvers map[string]telegraf.ResolveFunc

// unlinked contains all references in the secret that are not yet
// linked to the corresponding secret store.
unlinked []string

// Denotes if the secret is completely empty
// notempty denotes if the secret is completely empty
notempty bool
}

Expand Down Expand Up @@ -71,10 +119,10 @@ func (s *Secret) init(secret []byte) {

// Find all parts that need to be resolved and return them
s.unlinked = secretPattern.FindAllString(string(secret), -1)

// Setup the enclave
s.enclave = memguard.NewEnclave(secret)
s.resolvers = nil

// Setup the container implementation
s.container = selectedImpl.Container(secret)
}

// Destroy the secret content
Expand All @@ -83,16 +131,10 @@ func (s *Secret) Destroy() {
s.unlinked = nil
s.notempty = false

if s.enclave == nil {
return
}

// Wipe the secret from memory
lockbuf, err := s.enclave.Open()
if err == nil {
lockbuf.Destroy()
if s.container != nil {
s.container.Destroy()
s.container = nil
}
s.enclave = nil

// Keep track of the number of secrets...
secretCount.Add(-1)
Expand All @@ -105,51 +147,41 @@ func (s *Secret) Empty() bool {

// EqualTo performs a constant-time comparison of the secret to the given reference
func (s *Secret) EqualTo(ref []byte) (bool, error) {
if s.enclave == nil {
if s.container == nil {
return false, nil
}

if len(s.unlinked) > 0 {
return false, fmt.Errorf("unlinked parts in secret: %v", strings.Join(s.unlinked, ";"))
}

// Get a locked-buffer of the secret to perform the comparison
lockbuf, err := s.enclave.Open()
if err != nil {
return false, fmt.Errorf("opening enclave failed: %w", err)
}
defer lockbuf.Destroy()

return lockbuf.EqualTo(ref), nil
return s.container.Equals(ref)
}

// Get return the string representation of the secret
func (s *Secret) Get() ([]byte, error) {
if s.enclave == nil {
return nil, nil
func (s *Secret) Get() (SecretBuffer, error) {
if s.container == nil {
return selectedImpl.EmptyBuffer(), nil
}

if len(s.unlinked) > 0 {
return nil, fmt.Errorf("unlinked parts in secret: %v", strings.Join(s.unlinked, ";"))
}

// Decrypt the secret so we can return it
lockbuf, err := s.enclave.Open()
buffer, err := s.container.Buffer()
if err != nil {
return nil, fmt.Errorf("opening enclave failed: %w", err)
return nil, err
}
defer lockbuf.Destroy()
secret := lockbuf.Bytes()

// We've got a static secret so simply return the buffer
if len(s.resolvers) == 0 {
// Make a copy as we cannot access lockbuf after Destroy, i.e.
// after this function finishes.
newsecret := append([]byte{}, secret...)
return newsecret, protect(newsecret)
return buffer, nil
}
defer buffer.Destroy()

replaceErrs := make([]string, 0)
newsecret := secretPattern.ReplaceAllFunc(secret, func(match []byte) []byte {
newsecret := secretPattern.ReplaceAllFunc(buffer.Bytes(), func(match []byte) []byte {
resolver, found := s.resolvers[string(match)]
if !found {
replaceErrs = append(replaceErrs, fmt.Sprintf("no resolver for %q", match))
Expand All @@ -164,11 +196,11 @@ func (s *Secret) Get() ([]byte, error) {
return replacement
})
if len(replaceErrs) > 0 {
memguard.WipeBytes(newsecret)
selectedImpl.Wipe(newsecret)
return nil, fmt.Errorf("replacing secrets failed: %s", strings.Join(replaceErrs, ";"))
}

return newsecret, protect(newsecret)
return s.container.AsBuffer(newsecret), nil
}

// Set overwrites the secret's value with a new one. Please note, the secret
Expand All @@ -182,7 +214,7 @@ func (s *Secret) Set(value []byte) error {
}

// Set the new secret
s.enclave = memguard.NewEnclave(secret)
s.container.Replace(secret)
s.resolvers = res
s.notempty = len(value) > 0

Expand All @@ -198,27 +230,26 @@ func (s *Secret) GetUnlinked() []string {
// secret-store resolvers.
func (s *Secret) Link(resolvers map[string]telegraf.ResolveFunc) error {
// Decrypt the secret so we can return it
if s.enclave == nil {
if s.container == nil {
return nil
}
lockbuf, err := s.enclave.Open()
buffer, err := s.container.Buffer()
if err != nil {
return fmt.Errorf("opening enclave failed: %w", err)
return err
}
defer lockbuf.Destroy()
secret := lockbuf.Bytes()
defer buffer.Destroy()

// Iterate through the parts and try to resolve them. For static parts
// we directly replace them, while for dynamic ones we store the resolver.
newsecret, res, replaceErrs := resolve(secret, resolvers)
newsecret, res, replaceErrs := resolve(buffer.Bytes(), resolvers)
if len(replaceErrs) > 0 {
return fmt.Errorf("linking secrets failed: %s", strings.Join(replaceErrs, ";"))
}
s.resolvers = res

// Store the secret if it has changed
if string(secret) != string(newsecret) {
s.enclave = memguard.NewEnclave(newsecret)
if buffer.TemporaryString() != string(newsecret) {
s.container.Replace(newsecret)
}

// All linked now
Expand Down
131 changes: 131 additions & 0 deletions config/secret_protected.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package config

import (
"fmt"

"github.com/awnumar/memguard"
)

type protectedSecretImpl struct{}

func (*protectedSecretImpl) Container(secret []byte) secretContainer {
return &protectedSecretContainer{
enclave: memguard.NewEnclave(secret),
}
}

func (*protectedSecretImpl) EmptyBuffer() SecretBuffer {
return &lockedBuffer{}
}

func (*protectedSecretImpl) Wipe(secret []byte) {
memguard.WipeBytes(secret)
}

type lockedBuffer struct {
buf *memguard.LockedBuffer
}

func (lb *lockedBuffer) Size() int {
if lb.buf == nil {
return 0
}
return lb.buf.Size()
}

func (lb *lockedBuffer) Grow(capacity int) {
size := lb.Size()
if capacity <= size {
return
}

buf := memguard.NewBuffer(capacity)
if lb.buf != nil {
buf.Copy(lb.buf.Bytes())
}
lb.buf.Destroy()
lb.buf = buf
}

func (lb *lockedBuffer) Bytes() []byte {
if lb.buf == nil {
return nil
}
return lb.buf.Bytes()
}

func (lb *lockedBuffer) TemporaryString() string {
if lb.buf == nil {
return ""
}
return lb.buf.String()
}

func (lb *lockedBuffer) String() string {
if lb.buf == nil {
return ""
}
return string(lb.buf.Bytes())
}

func (lb *lockedBuffer) Destroy() {
if lb.buf == nil {
return
}
lb.buf.Destroy()
lb.buf = nil
}

type protectedSecretContainer struct {
enclave *memguard.Enclave
}

func (c *protectedSecretContainer) Destroy() {
if c.enclave == nil {
return
}

// Wipe the secret from memory
lockbuf, err := c.enclave.Open()
if err == nil {
lockbuf.Destroy()
}
c.enclave = nil
}

func (c *protectedSecretContainer) Equals(ref []byte) (bool, error) {
if c.enclave == nil {
return false, nil
}

// Get a locked-buffer of the secret to perform the comparison
lockbuf, err := c.enclave.Open()
if err != nil {
return false, fmt.Errorf("opening enclave failed: %w", err)
}
defer lockbuf.Destroy()

return lockbuf.EqualTo(ref), nil
}

func (c *protectedSecretContainer) Buffer() (SecretBuffer, error) {
if c.enclave == nil {
return &lockedBuffer{}, nil
}

// Get a locked-buffer of the secret to perform the comparison
lockbuf, err := c.enclave.Open()
if err != nil {
return nil, fmt.Errorf("opening enclave failed: %w", err)
}

return &lockedBuffer{lockbuf}, nil
}

func (c *protectedSecretContainer) AsBuffer(secret []byte) SecretBuffer {
return &lockedBuffer{memguard.NewBufferFromBytes(secret)}
}

func (c *protectedSecretContainer) Replace(secret []byte) {
c.enclave = memguard.NewEnclave(secret)
}
Loading