Skip to content

Commit

Permalink
sys: add minimal multi-tenancy support via Enclaves (#183)
Browse files Browse the repository at this point in the history
This commit adds the most minimal multi-tenancy
implementation.

Now, KES contains an internal Vault. The Vault
has one or multiple Enclaves. (For now just one).

Each Enclave has its own key store, policies set
and identity set. A client selects the enclave
using the `?enclave=<name>` query parameter.

For example:
```
https://127.0.0.1:7373/v1/key/create/my-key?enclave=tenant-1
```

However, a stateless KES server will only support a
single Enclave. Therefore, this commit only adds the
most basic Vault implementation.

Future Vault implementations (e.g. a standalone FS
KES server) will use a more sophisticated Vault
implementation that supports seal/unsealing etc.

This commit mainly focuses on changing the current
code base to use a `sys.Vault` and don't change any
client-facing behavior.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
  • Loading branch information
aead committed Feb 3, 2022
1 parent 816d343 commit 1bc46ac
Show file tree
Hide file tree
Showing 13 changed files with 1,138 additions and 410 deletions.
224 changes: 190 additions & 34 deletions cmd/kes/config.go
Expand Up @@ -9,11 +9,14 @@ import (
"errors"
"fmt"
stdlog "log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/minio/kes"
"github.com/minio/kes/internal/auth"
"github.com/minio/kes/internal/aws"
"github.com/minio/kes/internal/azure"
"github.com/minio/kes/internal/fortanix"
Expand All @@ -25,7 +28,6 @@ import (
"github.com/minio/kes/internal/mem"
"github.com/minio/kes/internal/vault"
"github.com/minio/kes/internal/yml"
"gopkg.in/yaml.v2"
)

// connect tries to establish a connection to the KMS specified in the ServerConfig
Expand Down Expand Up @@ -240,46 +242,200 @@ func description(config *yml.ServerConfig) (kind, endpoint string, err error) {
return kind, endpoint, nil
}

// expandEnv replaces s with a value from the environment if
// s refers to an environment variable. If the referenced
// environment variable does not exist s gets replaced with
// the empty string.
//
// s refers to an environment variable if it has the following
// form: ${<name>}.
//
// If s does not refer to an environment variable then s is
// returned unmodified.
func expandEnv(s string) string {
if t := strings.TrimSpace(s); strings.HasPrefix(t, "${") && strings.HasSuffix(t, "}") {
return os.ExpandEnv(t)
// policySetFromConfig returns an in-memory PolicySet
// from the given ServerConfig.
func policySetFromConfig(config *yml.ServerConfig) (auth.PolicySet, error) {
policies := &policySet{
policies: make(map[string]*auth.Policy),
}
return s
for name, policy := range config.Policies {
if _, ok := policies.policies[name]; ok {
return nil, fmt.Errorf("policy %q already exists", name)
}

policies.policies[name] = &auth.Policy{
Allow: policy.Allow,
Deny: policy.Deny,
CreatedAt: time.Now().UTC(),
CreatedBy: config.Admin.Identity.Value(),
}
}
return policies, nil
}

// duration is an alias for time.Duration that
// implements YAML unmarshaling by first replacing
// any reference to an environment variable ${...}
// with the referenced value.
type duration time.Duration
type policySet struct {
lock sync.RWMutex
policies map[string]*auth.Policy
}

var (
_ yaml.Marshaler = duration(0)
_ yaml.Unmarshaler = (*duration)(nil)
)
var _ auth.PolicySet = (*policySet)(nil) // compiler check

func (p *policySet) Set(_ context.Context, name string, policy *auth.Policy) error {
p.lock.Lock()
defer p.lock.Unlock()

p.policies[name] = policy
return nil
}

func (p *policySet) Get(_ context.Context, name string) (*auth.Policy, error) {
p.lock.RLock()
defer p.lock.RUnlock()

policy, ok := p.policies[name]
if !ok {
return nil, kes.ErrPolicyNotFound
}
return policy, nil
}

func (p *policySet) Delete(_ context.Context, name string) error {
p.lock.Lock()
defer p.lock.Unlock()

delete(p.policies, name)
return nil
}

func (p *policySet) List(_ context.Context) (auth.PolicyIterator, error) {
p.lock.RLock()
defer p.lock.RUnlock()

names := make([]string, 0, len(p.policies))
for name := range p.policies {
names = append(names, name)
}
return &policyIterator{
values: names,
}, nil
}

type policyIterator struct {
values []string
current string
}

var _ auth.PolicyIterator = (*policyIterator)(nil) // compiler check

func (i *policyIterator) Next() bool {
next := len(i.values) > 0
if next {
i.current = i.values[0]
}
return next
}

func (i *policyIterator) Name() string { return i.current }

func (i *policyIterator) Close() error { return nil }

// identitySetFromConfig returns an in-memory IdentitySet
// from the given ServerConfig.
func identitySetFromConfig(config *yml.ServerConfig) (auth.IdentitySet, error) {
identities := &identitySet{
admin: config.Admin.Identity.Value(),
roles: map[kes.Identity]auth.IdentityInfo{},
}

for name, policy := range config.Policies {
for _, id := range policy.Identities {
if id.Value().IsUnknown() {
continue
}

if id.Value() == config.Admin.Identity.Value() {
return nil, fmt.Errorf("identity %q is already an admin identity", id.Value())
}
if _, ok := identities.roles[id.Value()]; ok {
return nil, fmt.Errorf("identity %q is already assigned", id.Value())
}
for _, proxyID := range config.TLS.Proxy.Identities {
if id.Value() == proxyID.Value() {
return nil, fmt.Errorf("identity %q is already a TLS proxy identity", id.Value())
}
}
identities.roles[id.Value()] = auth.IdentityInfo{
Policy: name,
CreatedAt: time.Now().UTC(),
CreatedBy: config.Admin.Identity.Value(),
}
}
}
return identities, nil
}

func (d duration) MarshalYAML() (interface{}, error) { return time.Duration(d).String(), nil }
type identitySet struct {
admin kes.Identity

func (d *duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
lock sync.RWMutex
roles map[kes.Identity]auth.IdentityInfo
}

var _ auth.IdentitySet = (*identitySet)(nil) // compiler check

func (i *identitySet) Admin(ctx context.Context) (kes.Identity, error) { return i.admin, nil }

func (i *identitySet) Assign(_ context.Context, policy string, identity kes.Identity) error {
if i.admin == identity {
return kes.NewError(http.StatusBadRequest, "identity is root")
}
i.lock.Lock()
defer i.lock.Unlock()

v, err := time.ParseDuration(expandEnv(s))
if err != nil {
return err
i.roles[identity] = auth.IdentityInfo{
Policy: policy,
CreatedAt: time.Now().UTC(),
}
*d = duration(v)
return nil
}

func (i *identitySet) Get(_ context.Context, identity kes.Identity) (auth.IdentityInfo, error) {
i.lock.RLock()
defer i.lock.RUnlock()

policy, ok := i.roles[identity]
if !ok {
return auth.IdentityInfo{}, kes.ErrNotAllowed
}
return policy, nil
}

func (i *identitySet) Delete(_ context.Context, identity kes.Identity) error {
i.lock.Lock()
defer i.lock.Unlock()

delete(i.roles, identity)
return nil
}

func (i *identitySet) List(_ context.Context) (auth.IdentityIterator, error) {
i.lock.RLock()
defer i.lock.RUnlock()

values := make([]kes.Identity, 0, len(i.roles))
for identity := range i.roles {
values = append(values, identity)
}
return &identityIterator{
values: values,
}, nil
}

type identityIterator struct {
values []kes.Identity
current kes.Identity
}

var _ auth.IdentityIterator = (*identityIterator)(nil) // compiler check

func (i *identityIterator) Next() bool {
next := len(i.values) > 0
if next {
i.current = i.values[0]
}
return next
}

func (i *identityIterator) Identity() kes.Identity { return i.current }

func (i *identityIterator) Close() error { return nil }
36 changes: 10 additions & 26 deletions cmd/kes/server.go
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/minio/kes/internal/key"
xlog "github.com/minio/kes/internal/log"
"github.com/minio/kes/internal/metric"
"github.com/minio/kes/internal/sys"
"github.com/minio/kes/internal/yml"
"github.com/secure-io/sio-go/sioutil"
"golang.org/x/crypto/ssh/terminal"
Expand Down Expand Up @@ -176,32 +177,16 @@ func server(args []string) {
}
}

roles := &auth.Roles{
Root: config.Admin.Identity.Value(),
policySet, err := policySetFromConfig(config)
if err != nil {
stdlog.Fatalf("Error: %v", err)
return
}
for name, policy := range config.Policies {
p, err := kes.NewPolicy(policy.Allow...)
if err != nil {
stdlog.Fatalf("Error: policy %q contains invalid allow pattern: %v", name, err)
}
if err = p.Deny(policy.Deny...); err != nil {
stdlog.Fatalf("Error: policy %q contains invalid deny pattern: %v", name, err)
}
roles.Set(name, p)

for _, identity := range policy.Identities {
if proxy != nil && proxy.Is(identity.Value()) {
stdlog.Fatalf("Error: cannot assign policy %q to TLS proxy %q", name, identity.Value())
}
if roles.IsAssigned(identity.Value()) {
stdlog.Fatalf("Error: cannot assign policy %q to identity %q: this identity already has a policy", name, identity.Value())
}
if !identity.Value().IsUnknown() {
roles.Assign(name, identity.Value())
}
}
identitySet, err := identitySetFromConfig(config)
if err != nil {
stdlog.Fatalf("Error: %v", err)
return
}

store, err := connect(config, quietFlag, errorLog.Log())
if err != nil {
stdlog.Fatalf("Error: %v", err)
Expand Down Expand Up @@ -238,8 +223,7 @@ func server(args []string) {
Addr: config.Address.Value(),
Handler: xhttp.NewServerMux(&xhttp.ServerConfig{
Version: version,
Store: cache,
Roles: roles,
Vault: sys.NewStatelessVault(config.Admin.Identity.Value(), cache, policySet, identitySet),
Proxy: proxy,
AuditLog: auditLog,
ErrorLog: errorLog,
Expand Down

0 comments on commit 1bc46ac

Please sign in to comment.