Skip to content

Commit

Permalink
Merge pull request #1038 from rancher/ephemeral-keys
Browse files Browse the repository at this point in the history
Add ephemeral key type, config, and loading machinery
  • Loading branch information
dbason authored Feb 13, 2023
2 parents 7fb7bea + 684d0b7 commit ffa767d
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 15 deletions.
13 changes: 13 additions & 0 deletions pkg/agent/v2/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/rancher/opni/pkg/patch"
"github.com/rancher/opni/pkg/versions"
"github.com/samber/lo"
"github.com/spf13/afero"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -259,6 +260,18 @@ func New(ctx context.Context, conf *v1beta1.AgentConfig, opts ...AgentOption) (*
return nil, fmt.Errorf("error building trust strategy: %w", err)
}

// Load ephemeral keyrings from disk, if any search dirs are configured
ekeys, err := machinery.LoadEphemeralKeys(afero.Afero{
Fs: afero.NewOsFs(),
}, conf.Spec.Keyring.EphemeralKeyDirs...)
if err != nil {
lg.With(
zap.Error(err),
).Warn("error loading ephemeral keys")
} else if len(ekeys) > 0 {
kr = kr.Merge(keyring.New(lo.ToAnySlice(ekeys)...))
}

gatewayClient, err := clients.NewGatewayClient(ctx,
conf.Spec.GatewayAddress, ip, kr, trust)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/config/v1beta1/agent_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type AgentConfigSpec struct {
Bootstrap *BootstrapSpec `json:"bootstrap,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
Plugins PluginsSpec `json:"plugins,omitempty"`
Keyring KeyringSpec `json:"keyring,omitempty"`
}

type BootstrapSpec struct {
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/v1beta1/gateway_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type GatewayConfigSpec struct {
Plugins PluginsSpec `json:"plugins,omitempty"`
Alerting AlertingSpec `json:"alerting,omitempty"`
Profiling ProfilingSpec `json:"profiling,omitempty"`
Keyring KeyringSpec `json:"keyring,omitempty"`
}

type AlertingSpec struct {
Expand All @@ -54,6 +55,13 @@ type ProfilingSpec struct {
Path string `json:"path,omitempty"`
}

type KeyringSpec struct {
// Directories to search for files containing ephemeral keys.
// All files in these directories will be loaded into the keyring on
// startup. Keys loaded in this way will not be persisted across restarts.
EphemeralKeyDirs []string `json:"ephemeralKeyDirs,omitempty"`
}

func (s MetricsSpec) GetPath() string {
if s.Path == "" {
return "/metrics"
Expand Down
41 changes: 41 additions & 0 deletions pkg/keyring/ephemeral.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package keyring

import (
"encoding/json"
"io"

"github.com/go-playground/validator/v10"
)

type UsageType string

const (
Encryption UsageType = "encryption"
Signing UsageType = "signing"
)

var validate = validator.New()

type EphemeralKey struct {
Usage UsageType `json:"usage" validate:"oneof=encryption signing"`
Secret []byte `json:"secret" validate:"len=32"`
Labels map[string]string `json:"labels" validate:"dive,keys,printascii,endkeys,printascii"`
}

func (e *EphemeralKey) Validate() error {
if err := validate.Struct(e); err != nil {
return err
}
return nil
}

func LoadEphemeralKey(r io.Reader) (*EphemeralKey, error) {
var key EphemeralKey
if err := json.NewDecoder(r).Decode(&key); err != nil {
return nil, err
}
if err := key.Validate(); err != nil {
return nil, err
}
return &key, nil
}
27 changes: 14 additions & 13 deletions pkg/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ var (
var allowedKeyTypes = map[reflect.Type]struct{}{}

type completeKeyring struct {
SharedKeys []*SharedKeys `json:"sharedKeys,omitempty"`
PKPKey []*PKPKey `json:"pkpKey,omitempty"`
CACertsKey []*CACertsKey `json:"caCertsKey,omitempty"`
SharedKeys []*SharedKeys `json:"sharedKeys,omitempty"`
PKPKey []*PKPKey `json:"pkpKey,omitempty"`
CACertsKey []*CACertsKey `json:"caCertsKey,omitempty"`
EphemeralKey []*EphemeralKey `json:"-"`
}

func init() {
Expand All @@ -28,21 +29,21 @@ func init() {
}
}

type UseKeyFn interface{} // func(*KeyringType)
type UseKeyFn any // func(*KeyringType)

type Keyring interface {
Try(...UseKeyFn) bool
ForEach(func(key interface{}))
ForEach(func(key any))
Marshal() ([]byte, error)
Merge(Keyring) Keyring
}

type keyring struct {
Keys map[reflect.Type][]interface{}
Keys map[reflect.Type][]any
}

func New(keys ...interface{}) Keyring {
m := map[reflect.Type][]interface{}{}
func New(keys ...any) Keyring {
m := map[reflect.Type][]any{}
for _, key := range keys {
t := reflect.TypeOf(key)
if _, ok := allowedKeyTypes[t]; !ok {
Expand All @@ -56,11 +57,11 @@ func New(keys ...interface{}) Keyring {
}

func (kr *keyring) Merge(other Keyring) Keyring {
keys := []interface{}{}
kr.ForEach(func(key interface{}) {
keys := []any{}
kr.ForEach(func(key any) {
keys = append(keys, key)
})
other.ForEach(func(key interface{}) {
other.ForEach(func(key any) {
keys = append(keys, key)
})
return New(keys...)
Expand Down Expand Up @@ -88,7 +89,7 @@ func (kr *keyring) Try(fns ...UseKeyFn) bool {
return found
}

func (kr *keyring) ForEach(fn func(key interface{})) {
func (kr *keyring) ForEach(fn func(key any)) {
for _, l := range kr.Keys {
for _, k := range l {
fn(k)
Expand Down Expand Up @@ -121,7 +122,7 @@ func Unmarshal(data []byte) (Keyring, error) {
}
completePtr := reflect.ValueOf(&complete)
fields := reflect.TypeOf(complete).NumField()
values := []interface{}{}
values := []any{}
for i := 0; i < fields; i++ {
field := completePtr.Elem().Field(i)
if !field.IsNil() && field.Kind() == reflect.Slice {
Expand Down
53 changes: 51 additions & 2 deletions pkg/keyring/keyring_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package keyring_test

import (
"crypto/rand"
"crypto/x509"
"encoding/base64"
"fmt"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -29,7 +33,7 @@ var _ = Describe("Keyring", Label("unit"), func() {

By("ensuring ForEach is never called")
counter.Store(0)
kr.ForEach(func(key interface{}) {
kr.ForEach(func(key any) {
counter.Inc()
})
Expect(counter.Load()).To(Equal(int32(0)))
Expand Down Expand Up @@ -129,7 +133,7 @@ var _ = Describe("Keyring", Label("unit"), func() {

By("ensuring ForEach is called for each key")
counter := atomic.NewInt32(0)
kr.ForEach(func(key interface{}) {
kr.ForEach(func(key any) {
counter.Inc()
})
Expect(counter.Load()).To(Equal(int32(3)))
Expand Down Expand Up @@ -195,4 +199,49 @@ var _ = Describe("Keyring", Label("unit"), func() {
keyring.NewSharedKeys([]byte("not_64_bytes"))
}).To(PanicWith("shared secret must be 64 bytes"))
})
It("should discard ephemeral keys when marshaling", func() {
var key [32]byte
_, err := rand.Read(key[:])
Expect(err).NotTo(HaveOccurred())

ek := &keyring.EphemeralKey{
Usage: keyring.Encryption,
Secret: key[:],
Labels: map[string]string{"test": "test"},
}
sk := keyring.NewSharedKeys(make([]byte, 64))
kr1 := keyring.New(ek, sk)
kr2 := keyring.New(sk)

kr1Data, err := kr1.Marshal()
Expect(err).NotTo(HaveOccurred())
kr2Data, err := kr2.Marshal()
Expect(err).NotTo(HaveOccurred())
Expect(kr1Data).To(Equal(kr2Data))
})
It("should load ephemeral keys from json data", func() {
var key [32]byte
_, err := rand.Read(key[:])
Expect(err).NotTo(HaveOccurred())

b64key := base64.StdEncoding.EncodeToString(key[:])

jsonData := fmt.Sprintf(`{
"usage": "encryption",
"secret": %q,
"labels": {
"test": "test"
}
}`, b64key)

expected := &keyring.EphemeralKey{
Usage: keyring.Encryption,
Secret: key[:],
Labels: map[string]string{"test": "test"},
}

actual, err := keyring.LoadEphemeralKey(strings.NewReader(jsonData))
Expect(err).NotTo(HaveOccurred())
Expect(actual).To(Equal(expected))
})
})
60 changes: 60 additions & 0 deletions pkg/machinery/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package machinery

import (
"path/filepath"

"github.com/spf13/afero"
"github.com/ttacon/chalk"
"go.uber.org/zap"

"github.com/rancher/opni/pkg/keyring"
"github.com/rancher/opni/pkg/logger"
)

var keyringLog = logger.New().Named("keyring")

func LoadEphemeralKeys(fsys afero.Afero, dirs ...string) ([]*keyring.EphemeralKey, error) {
var keys []*keyring.EphemeralKey

for _, dir := range dirs {
infos, err := fsys.ReadDir(dir)
if err != nil {
return nil, err
}
for _, info := range infos {
if info.IsDir() {
continue
}
perm := info.Mode().Perm()
path := filepath.Join(dir, info.Name())
lg := keyringLog.With("path", path)
if perm&0040 > 0 {
lg.Warn(chalk.Yellow.Color("Ephemeral key is group-readable. This is insecure."))
}
if perm&0004 > 0 {
lg.Warn(chalk.Yellow.Color("Ephemeral key is world-readable. This is insecure."))
}

f, err := fsys.Open(path)
if err != nil {
return nil, err
}

ekey, err := keyring.LoadEphemeralKey(f)
if err != nil {
lg.With(
zap.Error(err),
).Error("failed to load ephemeral key, skipping")
continue
}
lg.With(
"usage", ekey.Usage,
"labels", ekey.Labels,
).Debug("loaded ephemeral key")

keys = append(keys, ekey)
}
}

return keys, nil
}

0 comments on commit ffa767d

Please sign in to comment.