diff --git a/cmd/kes/config.go b/cmd/kes/config.go index 2b6150ad..20b3e86d 100644 --- a/cmd/kes/config.go +++ b/cmd/kes/config.go @@ -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" @@ -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 @@ -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: ${}. -// -// 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 } diff --git a/cmd/kes/server.go b/cmd/kes/server.go index b28f9898..f3d2ee2b 100644 --- a/cmd/kes/server.go +++ b/cmd/kes/server.go @@ -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" @@ -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) @@ -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, diff --git a/internal/auth/identity.go b/internal/auth/identity.go new file mode 100644 index 00000000..a90a97c6 --- /dev/null +++ b/internal/auth/identity.go @@ -0,0 +1,158 @@ +// Copyright 2022 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package auth + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "net/http" + "time" + + "github.com/minio/kes" +) + +// Identify computes the identity of the given HTTP request. +// +// If the request was not sent over TLS or no client +// certificate has been provided, Identify returns +// IdentityUnknown. +func Identify(req *http.Request) kes.Identity { + if req.TLS == nil { + return kes.IdentityUnknown + } + + var cert *x509.Certificate + for _, c := range req.TLS.PeerCertificates { + if c.IsCA { + continue // Ignore CA certificates + } + + if cert != nil { + // There is more than one client certificate + // that is not a CA certificate. Hence, we + // cannot compute an non-ambiguous identity. + // Therefore, we return IdentityUnknown. + return kes.IdentityUnknown + } + cert = c + } + if cert == nil { + return kes.IdentityUnknown + } + + h := sha256.Sum256(cert.RawSubjectPublicKeyInfo) + return kes.Identity(hex.EncodeToString(h[:])) +} + +// ErrNotAssigned is an error indicating that an identity is +// not assigned resp. does not exist. +var ErrNotAssigned = kes.NewError(http.StatusNotFound, "identity not assigned") + +// An IdentitySet is a set of identities that are assigned to policies. +type IdentitySet interface { + // Admin returns the identity of the admin. + // + // The admin is never assigned to any policy + // and can perform any operation. + Admin(ctx context.Context) (kes.Identity, error) + + // Assign assigns the identity to the given policy. + // + // It returns an error when the identity is equal + // to the admin identity. + Assign(ctx context.Context, policy string, identity kes.Identity) error + + // Get returns the IdentityInfo of an assigned identity. + // + // It returns ErrNotAssigned when there is no IdentityInfo + // associated to the given identity. + Get(ctx context.Context, identity kes.Identity) (IdentityInfo, error) + + // Delete deletes the given identity from the list of + // assigned identites. + // + // It returns ErrNotAssigned when the identity is not + // assigned. + Delete(ctx context.Context, identity kes.Identity) error + + // List returns an iterator over all assigned identities. + List(ctx context.Context) (IdentityIterator, error) +} + +// An IdentityIterator iterates over a list of identites. +// for iterator.Next() { +// _ = iterator.Identity() // Get the next identity +// } +// if err := iterator.Close(); err != nil { +// } +// +// Once done iterating, an IdentityIterator should be closed. +// +// In general, an IdentityIterator does not provide any +// ordering guarantees. Concurrent changes to the underlying +// source may not be reflected by the iterator. +type IdentityIterator interface { + // Next moves the iterator to the subsequent identity, if any. + // This identity is available until Next is called again. + // + // It returns true if and only if there is another identity. + // Once an error occurs or once there are no more identities, + // Next returns false. + Next() bool + + // Identity returns the current identity. Identity can be + // called multiple times and returns the same value until + // Next is called again. + Identity() kes.Identity + + // Close closes the iterator and releases resources. It + // returns any error encountered while iterating, if any. + // Otherwise, it returns any error that occurred while + // closing, if any. + Close() error +} + +// IdentityInfo describes an assigned identity. +type IdentityInfo struct { + // Policy is the policy the identity is assigned to. + Policy string + + // CreatedAt is the point in time when the identity + // has been assigned. + CreatedAt time.Time + + // CreatedBy is the identity that assigned this + // identity to its policy. + CreatedBy kes.Identity +} + +// ROIdentitySet wraps i and returns a readonly IdentitySet. +func ROIdentitySet(i IdentitySet) IdentitySet { return roIdentitySet{set: i} } + +type roIdentitySet struct{ set IdentitySet } + +var _ IdentitySet = roIdentitySet{} // compiler check + +func (r roIdentitySet) Admin(ctx context.Context) (kes.Identity, error) { + return r.set.Admin(ctx) +} + +func (r roIdentitySet) Assign(context.Context, string, kes.Identity) error { + return kes.NewError(http.StatusNotImplemented, "readonly identity: assigning an identity is not supported") +} + +func (r roIdentitySet) Get(ctx context.Context, identity kes.Identity) (IdentityInfo, error) { + return r.set.Get(ctx, identity) +} + +func (r roIdentitySet) Delete(context.Context, kes.Identity) error { + return kes.NewError(http.StatusNotImplemented, "readonly identity: deleting an identity is not supported") +} + +func (r roIdentitySet) List(ctx context.Context) (IdentityIterator, error) { + return r.set.List(ctx) +} diff --git a/internal/auth/policy.go b/internal/auth/policy.go index c87736ed..8c59b84e 100644 --- a/internal/auth/policy.go +++ b/internal/auth/policy.go @@ -5,248 +5,129 @@ package auth import ( - "crypto" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "errors" + "context" "net/http" - "sync" + "path" + "time" "github.com/minio/kes" ) -// IdentityFunc maps a X.509 certificate to an -// Identity. This mapping should be deterministic -// and unique in the sense that: -// 1. The same certificate always gets mapped to same identity. -// 2. There is only one (valid / non-expired) certificate that -// gets mapped to a particular (known) identity. -// -// If no certificate is provided or an identity -// cannot be computed - e.g. because the certificate -// does not contain enough information - the IdentityFunc -// should return IdentityUnknown. -type IdentityFunc func(*x509.Certificate) kes.Identity - -// HashPublicKey returns an IdentityFunc that -// computes an identity as the cryptographic -// hash of the certificate's public key. -// -// If the hash function is not available -// it uses crypto.SHA256. -func HashPublicKey(hash crypto.Hash) IdentityFunc { - if !hash.Available() { - hash = crypto.SHA256 - } - return func(cert *x509.Certificate) kes.Identity { - if cert == nil { - return kes.IdentityUnknown - } - h := hash.New() - h.Write(cert.RawSubjectPublicKeyInfo) - return kes.Identity(hex.EncodeToString(h.Sum(nil))) - } -} - -type Roles struct { - Root kes.Identity - Identify IdentityFunc - - lock sync.RWMutex - roles map[string]*kes.Policy // all available roles - effectiveRoles map[kes.Identity]string // identities for which a mapping to a policy name exists -} - -func (r *Roles) Set(name string, policy *kes.Policy) { - r.lock.Lock() - defer r.lock.Unlock() - - if r.roles == nil { - r.roles = map[string]*kes.Policy{} - } - r.roles[name] = policy -} - -func (r *Roles) Get(name string) (*kes.Policy, bool) { - r.lock.RLock() - defer r.lock.RUnlock() +// A PolicySet is a set of policies. +type PolicySet interface { + // Set creates or replaces the policy at the given name. + Set(ctx context.Context, name string, policy *Policy) error - if r.roles == nil { - return nil, false - } - policy, ok := r.roles[name] - return policy, ok -} + // Get returns the policy with the given name. + // + // It returns ErrPolicyNotFound if no policy with + // the given name exists. + Get(ctx context.Context, name string) (*Policy, error) -func (r *Roles) Delete(name string) { - r.lock.Lock() - defer r.lock.Unlock() + // Delete deletes the policy with the given name. + // + // It returns ErrPolicyNotFound if no policy with + // the given name exists. + Delete(ctx context.Context, name string) error - delete(r.roles, name) - if r.effectiveRoles != nil { // Remove all assigned identities - for id, policy := range r.effectiveRoles { - if name == policy { - delete(r.effectiveRoles, id) - } - } - } + // List returns an iterator over all policies. + List(ctx context.Context) (PolicyIterator, error) } -func (r *Roles) Policies() (names []string) { - r.lock.RLock() - defer r.lock.RUnlock() - - names = make([]string, 0, len(r.roles)) - for name := range r.roles { - names = append(names, name) - } - return +// A PolicyIterator iterates over a list of policies. +// for iterator.Next() { +// _ = iterator.Name() // Get the next policy +// } +// if err := iterator.Close(); err != nil { +// } +// +// Once done iterating, a PolicyIterator should be closed. +// +// In general, a PolicyIterator does not provide any +// ordering guranatees. Concurrent changes to the +// underlying source may not be reflected by the iterator. +type PolicyIterator interface { + // Next moves the iterator to the subsequent policy, if any. + // This policy is available until Next is called again. + // + // It returns true if and only if there is another policy. + // Once an error occurs or once there are no more policies, + // Next returns false. + Next() bool + + // Name returns the name of the current policy. Name can be + // called multiple times and returns the same value until + // Next is called again. + Name() string + + // Close closes the iterator and releases resources. It + // returns any error encountered while iterating, if any. + // Otherwise, it returns any error that occurred while + // closing, if any. + Close() error } -func (r *Roles) Assign(name string, id kes.Identity) error { - if id == r.Root { - return errors.New("key: identity is root") - } - - r.lock.Lock() - defer r.lock.Unlock() - - if r.roles == nil { - r.roles = map[string]*kes.Policy{} - } - _, ok := r.roles[name] - if !ok { - return kes.ErrPolicyNotFound - } - if r.effectiveRoles == nil { - r.effectiveRoles = map[kes.Identity]string{} - } - r.effectiveRoles[id] = name - return nil +// A Policy defines whether an HTTP request is allowed or +// should be rejected. +// +// It contains a set of allow and deny rules that are +// matched against the URL path. +type Policy struct { + // Allow is a list of glob patterns that are matched + // against the URL path of incoming requests. + Allow []string + + // Deny is a list of glob patterns that are matched + // against the URL path of incoming requests. + Deny []string + + // CreatedAt is the point in time when the policy + // has been created. + CreatedAt time.Time + + // CreatedBy is the identity that created the policy. + CreatedBy kes.Identity } -func (r *Roles) IsAssigned(id kes.Identity) bool { - if id == r.Root { - return true - } - - r.lock.RLock() - defer r.lock.RUnlock() - - if r.effectiveRoles != nil { - if name, ok := r.effectiveRoles[id]; ok { - _, ok = r.roles[name] - return ok +// Verify reports whether the given HTTP request is allowed. +// It returns no error if: +// (1) No deny pattern matches the URL path *AND* +// (2) At least one allow pattern matches the URL path. +// +// Otherwise, Verify returns ErrNotAllowed. +func (p *Policy) Verify(r *http.Request) error { + for _, pattern := range p.Deny { + if ok, err := path.Match(pattern, r.URL.Path); ok && err == nil { + return kes.ErrNotAllowed } } - return false -} - -func (r *Roles) Identities() map[kes.Identity]string { - r.lock.RLock() - defer r.lock.RUnlock() - - identities := make(map[kes.Identity]string, len(r.effectiveRoles)) - for id, policy := range r.effectiveRoles { - identities[id] = policy - } - return identities -} - -func (r *Roles) Forget(id kes.Identity) { - r.lock.Lock() - delete(r.effectiveRoles, id) - r.lock.Unlock() -} - -func (r *Roles) Verify(req *http.Request) error { - if req.TLS == nil { - // This can only happen if the server accepts non-TLS - // connections - which violates our fundamental security - // assumption. - return kes.NewError(http.StatusBadRequest, "insecure connection: TLS required") - } - - // A client may send none, one or multiple peer certificates - // as part of the TLS handshake. However, expect exactly - // one client certificate to map it to a policy. - // - // In particular, clients may send multiple certificates - for - // example their client certificate as well as intermediate or - // root CA certificates. - // Therefore, we filter all CA certificates and only - // process the remaining leaf certificate(s). - peerCertificates := make([]*x509.Certificate, 0, len(req.TLS.PeerCertificates)) - for _, cert := range req.TLS.PeerCertificates { - if cert.IsCA { - continue + for _, pattern := range p.Allow { + if ok, err := path.Match(pattern, r.URL.Path); ok && err == nil { + return nil } - peerCertificates = append(peerCertificates, cert) } + return kes.ErrNotAllowed +} - if len(peerCertificates) == 0 { - return kes.NewError(http.StatusBadRequest, "no client certificate is present") - } - if len(peerCertificates) > 1 { - return kes.NewError(http.StatusBadRequest, "too many client certificates are present") - } - req.TLS.PeerCertificates = peerCertificates +// ROPolicySet wraps p and returns a readonly PolicySet. +func ROPolicySet(p PolicySet) PolicySet { return roPolicySet{set: p} } - identity := Identify(req, r.Identify) - if identity.IsUnknown() { - return kes.ErrNotAllowed - } - if identity == r.Root { - return nil - } +type roPolicySet struct{ set PolicySet } - var policy *kes.Policy - r.lock.RLock() - if r.roles != nil && r.effectiveRoles != nil { - if name, ok := r.effectiveRoles[identity]; ok { - policy = r.roles[name] - } - } - r.lock.RUnlock() +var _ PolicySet = roPolicySet{} // compiler check - if policy == nil { - return kes.ErrNotAllowed - } - return policy.Verify(req) +func (r roPolicySet) Set(context.Context, string, *Policy) error { + return kes.NewError(http.StatusNotImplemented, "readonly policy-set: setting a policy is not supported") } -// Identify computes the identity of the X.509 -// certificate presented by the peer who sent -// the request. -// -// It returns IdentityUnknown if no TLS connection -// state is present, more than one certificate -// is present or when f returns IdentityUnknown. -func Identify(req *http.Request, f IdentityFunc) kes.Identity { - if req.TLS == nil { - return kes.IdentityUnknown - } - if len(req.TLS.PeerCertificates) > 1 { - return kes.IdentityUnknown - } +func (r roPolicySet) Get(ctx context.Context, name string) (*Policy, error) { + return r.set.Get(ctx, name) +} - var cert *x509.Certificate - if len(req.TLS.PeerCertificates) > 0 { - cert = req.TLS.PeerCertificates[0] - } - if f == nil { - return defaultIdentify(cert) - } - return f(cert) +func (r roPolicySet) Delete(context.Context, string) error { + return kes.NewError(http.StatusNotImplemented, "readonly policy-set: deleting a policy is not supported") } -// defaultIdentify computes the SHA-256 of the -// public key in cert and returns it as hex. -func defaultIdentify(cert *x509.Certificate) kes.Identity { - if cert == nil { - return kes.IdentityUnknown - } - h := sha256.Sum256(cert.RawSubjectPublicKeyInfo) - return kes.Identity(hex.EncodeToString(h[:])) +func (r roPolicySet) List(ctx context.Context) (PolicyIterator, error) { + return r.set.List(ctx) } diff --git a/internal/auth/proxy.go b/internal/auth/proxy.go index d677b283..83a5fcbf 100644 --- a/internal/auth/proxy.go +++ b/internal/auth/proxy.go @@ -15,13 +15,6 @@ import ( ) type TLSProxy struct { - // Identify computes the identity from a X.509 certificate - // sent by the client or proxy. - // - // If it is nil a default IdentityFunc computing the - // SHA-256 of the certificate's public key will be used. - Identify IdentityFunc - // CertHeader is the HTTP header key used to extract the // client certificate forwarded by a TLS proxy. The TLS // proxy has to include the certificate of the actual @@ -128,7 +121,7 @@ func (p *TLSProxy) Verify(req *http.Request) error { } req.TLS.PeerCertificates = peerCertificates - identity := Identify(req, p.Identify) + identity := Identify(req) if identity.IsUnknown() { return kes.ErrNotAllowed } diff --git a/internal/http/handler.go b/internal/http/handler.go index 270e9681..239ce1fc 100644 --- a/internal/http/handler.go +++ b/internal/http/handler.go @@ -19,6 +19,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/prometheus/common/expfmt" "github.com/secure-io/sio-go/sioutil" ) @@ -83,9 +84,14 @@ func limitRequestBody(n int64, f http.HandlerFunc) http.HandlerFunc { // // If the request is not authorized it will return an error to the // client and does not call f. -func enforcePolicies(roles *auth.Roles, f http.HandlerFunc) http.HandlerFunc { +func enforcePolicies(config *ServerConfig, f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if err := roles.Verify(r); err != nil { + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + if err = enclave.VerifyRequest(r); err != nil { Error(w, err) return } @@ -96,14 +102,14 @@ func enforcePolicies(roles *auth.Roles, f http.HandlerFunc) http.HandlerFunc { // audit returns a handler function that wraps f and logs the // HTTP request and response before sending the response status code // back to the client. -func audit(logger *log.Logger, roles *auth.Roles, f http.HandlerFunc) http.HandlerFunc { +func audit(logger *log.Logger, f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w = &AuditResponseWriter{ ResponseWriter: w, Logger: logger, URL: *r.URL, - Identity: auth.Identify(r, roles.Identify), + Identity: auth.Identify(r), Time: time.Now(), } f(w, r) @@ -123,7 +129,7 @@ func handleVersion(version string) http.HandlerFunc { // handleStatus returns a handler function that returns status // information, like server version and server up-time, as JSON // object to the client. -func handleStatus(version string, store key.Store, log *xlog.Target) http.HandlerFunc { +func handleStatus(config *ServerConfig) http.HandlerFunc { type Status struct { Version string `json:"version"` UpTime time.Duration `json:"uptime"` @@ -135,16 +141,21 @@ func handleStatus(version string, store key.Store, log *xlog.Target) http.Handle } startTime := time.Now() return func(w http.ResponseWriter, r *http.Request) { - kmsState, err := store.Status(r.Context()) + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + kmsState, err := enclave.Status(r.Context()) if err != nil { kmsState = key.StoreState{ State: key.StoreUnreachable, } - log.Log().Printf("http: failed to connect to key store: %v", err) + config.ErrorLog.Log().Printf("http: failed to connect to key store: %v", err) } status := Status{ - Version: version, + Version: config.Version, UpTime: time.Since(startTime).Round(time.Second), } status.KMS.State = kmsState.State.String() @@ -162,10 +173,16 @@ func handleStatus(version string, store key.Store, log *xlog.Target) http.Handle // It infers the name of the new Secret from the request URL - in // particular from the URL's path base. // See: https://golang.org/pkg/path/#Base -func handleCreateKey(store key.Store) http.HandlerFunc { +func handleCreateKey(config *ServerConfig) http.HandlerFunc { ErrInvalidKeyName := kes.NewError(http.StatusBadRequest, "invalid key name") return func(w http.ResponseWriter, r *http.Request) { + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + name := pathBase(r.URL.Path) if name == "" { Error(w, ErrInvalidKeyName) @@ -178,7 +195,7 @@ func handleCreateKey(store key.Store) http.HandlerFunc { return } - if err := store.Create(r.Context(), name, key.New(bytes)); err != nil { + if err := enclave.CreateKey(r.Context(), name, key.New(bytes)); err != nil { Error(w, err) } w.WriteHeader(http.StatusOK) @@ -192,15 +209,20 @@ func handleCreateKey(store key.Store) http.HandlerFunc { // It infers the name of the new Secret from the request URL - in // particular from the URL's path base. // See: https://golang.org/pkg/path/#Base -func handleImportKey(store key.Store) http.HandlerFunc { +func handleImportKey(config *ServerConfig) http.HandlerFunc { var ( ErrInvalidKeyName = kes.NewError(http.StatusBadRequest, "invalid key name") ErrInvalidJSON = kes.NewError(http.StatusBadRequest, "invalid json") ErrInvalidKey = kes.NewError(http.StatusBadRequest, "invalid key") ) + type Request struct { + Bytes []byte `json:"bytes"` + } return func(w http.ResponseWriter, r *http.Request) { - type request struct { - Bytes []byte `json:"bytes"` + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return } name := pathBase(r.URL.Path) @@ -209,7 +231,7 @@ func handleImportKey(store key.Store) http.HandlerFunc { return } - var req request + var req Request if err := json.NewDecoder(r.Body).Decode(&req); err != nil { Error(w, ErrInvalidJSON) return @@ -220,7 +242,7 @@ func handleImportKey(store key.Store) http.HandlerFunc { return } - if err := store.Create(r.Context(), name, key.New(req.Bytes)); err != nil { + if err := enclave.CreateKey(r.Context(), name, key.New(req.Bytes)); err != nil { Error(w, err) return } @@ -228,16 +250,22 @@ func handleImportKey(store key.Store) http.HandlerFunc { } } -func handleDeleteKey(store key.Store) http.HandlerFunc { +func handleDeleteKey(config *ServerConfig) http.HandlerFunc { ErrInvalidKeyName := kes.NewError(http.StatusBadRequest, "invalid key name") return func(w http.ResponseWriter, r *http.Request) { + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + name := pathBase(r.URL.Path) if name == "" { Error(w, ErrInvalidKeyName) return } - if err := store.Delete(r.Context(), name); err != nil { + if err := enclave.DeleteKey(r.Context(), name); err != nil { Error(w, err) return } @@ -257,7 +285,7 @@ func handleDeleteKey(store key.Store) http.HandlerFunc { // returned http.HandlerFunc will authenticate but not encrypt // the context value. The client has to provide the same // context value again for decryption. -func handleGenerateKey(store key.Store) http.HandlerFunc { +func handleGenerateKey(config *ServerConfig) http.HandlerFunc { var ( ErrInvalidJSON = kes.NewError(http.StatusBadRequest, "invalid json") ErrInvalidKeyName = kes.NewError(http.StatusBadRequest, "invalid key name") @@ -270,6 +298,12 @@ func handleGenerateKey(store key.Store) http.HandlerFunc { Ciphertext []byte `json:"ciphertext"` } return func(w http.ResponseWriter, r *http.Request) { + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + var req Request if err := json.NewDecoder(r.Body).Decode(&req); err != nil { Error(w, ErrInvalidJSON) @@ -281,7 +315,7 @@ func handleGenerateKey(store key.Store) http.HandlerFunc { Error(w, ErrInvalidKeyName) return } - secret, err := store.Get(r.Context(), name) + secret, err := enclave.GetKey(r.Context(), name) if err != nil { Error(w, err) return @@ -316,7 +350,7 @@ func handleGenerateKey(store key.Store) http.HandlerFunc { // returned http.HandlerFunc will authenticate but not encrypt // the context value. The client has to provide the same // context value again for decryption. -func handleEncryptKey(store key.Store) http.HandlerFunc { +func handleEncryptKey(config *ServerConfig) http.HandlerFunc { var ( ErrInvalidJSON = kes.NewError(http.StatusBadRequest, "invalid json") ErrInvalidKeyName = kes.NewError(http.StatusBadRequest, "invalid key name") @@ -335,12 +369,17 @@ func handleEncryptKey(store key.Store) http.HandlerFunc { return } + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } name := pathBase(r.URL.Path) if name == "" { Error(w, ErrInvalidKeyName) return } - secret, err := store.Get(r.Context(), name) + secret, err := enclave.GetKey(r.Context(), name) if err != nil { Error(w, err) return @@ -363,7 +402,7 @@ func handleEncryptKey(store key.Store) http.HandlerFunc { // If the client has provided a context value during // encryption / key generation then the client has to provide // the same context value again. -func handleDecryptKey(store key.Store) http.HandlerFunc { +func handleDecryptKey(config *ServerConfig) http.HandlerFunc { var ( ErrInvalidJSON = kes.NewError(http.StatusBadRequest, "invalid json") ErrInvalidKeyName = kes.NewError(http.StatusBadRequest, "invalid key name") @@ -382,12 +421,17 @@ func handleDecryptKey(store key.Store) http.HandlerFunc { return } + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } name := pathBase(r.URL.Path) if name == "" { Error(w, ErrInvalidKeyName) return } - secret, err := store.Get(r.Context(), name) + secret, err := enclave.GetKey(r.Context(), name) if err != nil { Error(w, err) return @@ -413,18 +457,22 @@ func handleDecryptKey(store key.Store) http.HandlerFunc { // The client is expected to check for an error trailer // and only consider the listing complete if it receives // no such trailer. -func handleListKeys(store key.Store) http.HandlerFunc { +func handleListKeys(config *ServerConfig) http.HandlerFunc { type Response struct { Name string } return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Trailer", "Status,Error") - - iterator, err := store.List(r.Context()) + enclave, err := getEnclave(config.Vault, r) if err != nil { Error(w, err) return } + iterator, err := enclave.ListKeys(r.Context()) + if err != nil { + Error(w, err) + return + } + w.Header().Set("Trailer", "Status,Error") var ( pattern = pathBase(r.URL.Path) @@ -474,7 +522,7 @@ func handleListKeys(store key.Store) http.HandlerFunc { } } -func handleWritePolicy(roles *auth.Roles) http.HandlerFunc { +func handleWritePolicy(config *ServerConfig) http.HandlerFunc { var ( ErrInvalidPolicyName = kes.NewError(http.StatusBadRequest, "invalid policy name") ErrInvalidJSON = kes.NewError(http.StatusBadRequest, "invalid json") @@ -485,19 +533,31 @@ func handleWritePolicy(roles *auth.Roles) http.HandlerFunc { Error(w, ErrInvalidPolicyName) return } - - var policy kes.Policy + var policy auth.Policy if err := json.NewDecoder(r.Body).Decode(&policy); err != nil { Error(w, ErrInvalidJSON) return } - roles.Set(name, &policy) + + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + if err = enclave.SetPolicy(r.Context(), name, &policy); err != nil { + Error(w, err) + return + } w.WriteHeader(http.StatusOK) } } -func handleReadPolicy(roles *auth.Roles) http.HandlerFunc { +func handleReadPolicy(config *ServerConfig) http.HandlerFunc { ErrInvalidPolicyName := kes.NewError(http.StatusBadRequest, "invalid policy name") + type Response struct { + Allow []string `json:"allow"` + Deny []string `json:"deny"` + } return func(w http.ResponseWriter, r *http.Request) { name := pathBase(r.URL.Path) if name == "" { @@ -505,92 +565,142 @@ func handleReadPolicy(roles *auth.Roles) http.HandlerFunc { return } - policy, ok := roles.Get(name) - if !ok { - Error(w, kes.ErrPolicyNotFound) + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + policy, err := enclave.GetPolicy(r.Context(), name) + if err != nil { + Error(w, err) return } - json.NewEncoder(w).Encode(policy) + json.NewEncoder(w).Encode(&Response{ + Allow: policy.Allow, + Deny: policy.Deny, + }) } } -func handleListPolicies(roles *auth.Roles) http.HandlerFunc { +func handleListPolicies(config *ServerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - policies := []string{} + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + iterator, err := enclave.ListPolicies(r.Context()) + if err != nil { + Error(w, err) + return + } + + var policies []string pattern := pathBase(r.URL.Path) - for _, policy := range roles.Policies() { - if ok, err := path.Match(pattern, policy); ok && err == nil { - policies = append(policies, policy) + for iterator.Next() { + if ok, err := path.Match(pattern, iterator.Name()); ok && err == nil { + policies = append(policies, iterator.Name()) } } json.NewEncoder(w).Encode(policies) } } -func handleDeletePolicy(roles *auth.Roles) http.HandlerFunc { +func handleDeletePolicy(config *ServerConfig) http.HandlerFunc { ErrInvalidPolicyName := kes.NewError(http.StatusBadRequest, "invalid policy name") return func(w http.ResponseWriter, r *http.Request) { + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + name := pathBase(r.URL.Path) if name == "" { Error(w, ErrInvalidPolicyName) return } - roles.Delete(name) + if err = enclave.DeleteKey(r.Context(), name); err != nil { + Error(w, err) + return + } w.WriteHeader(http.StatusOK) } } -func handleAssignIdentity(roles *auth.Roles) http.HandlerFunc { +func handleAssignIdentity(config *ServerConfig) http.HandlerFunc { var ( ErrIdentityUnknown = kes.NewError(http.StatusBadRequest, "identity is unknown") - ErrIdentityRoot = kes.NewError(http.StatusBadRequest, "identity is root") ErrSelfAssign = kes.NewError(http.StatusForbidden, "identity cannot assign policy to itself") ) return func(w http.ResponseWriter, r *http.Request) { + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + identity := kes.Identity(pathBase(r.URL.Path)) if identity.IsUnknown() { Error(w, ErrIdentityUnknown) return } - if identity == roles.Root { - Error(w, ErrIdentityRoot) - return - } - if identity == auth.Identify(r, roles.Identify) { + if self := auth.Identify(r); self == identity { Error(w, ErrSelfAssign) return } policy := pathBase(strings.TrimSuffix(r.URL.Path, identity.String())) - if err := roles.Assign(policy, identity); err != nil { - Error(w, kes.ErrPolicyNotFound) + if err = enclave.AssignIdentity(r.Context(), policy, identity); err != nil { + Error(w, err) return } w.WriteHeader(http.StatusOK) } } -func handleListIdentities(roles *auth.Roles) http.HandlerFunc { +func handleListIdentities(config *ServerConfig) http.HandlerFunc { + type Response struct { + Identity kes.Identity `json:"identity"` + Policy string `json:"policy"` + } + return func(w http.ResponseWriter, r *http.Request) { - type Response struct { - Identity kes.Identity `json:"identity"` - Policy string `json:"policy"` + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) + return + } + iterator, err := enclave.ListIdentities(r.Context()) + if err != nil { + Error(w, err) + return } - w.Header().Set("Trailer", "Status,Error") + w.Header().Set("Trailer", "Status,Error") var ( pattern = pathBase(r.URL.Path) encoder = json.NewEncoder(w) hasWritten bool ) w.Header().Set("Content-Type", "application/x-ndjson") - for identity, policy := range roles.Identities() { - if ok, err := path.Match(pattern, identity.String()); ok && err == nil { + for iterator.Next() { + if ok, err := path.Match(pattern, iterator.Identity().String()); ok && err == nil { + info, err := enclave.GetIdentity(r.Context(), iterator.Identity()) + if err != nil { + if !hasWritten { + Error(w, err) + } else { + ErrorTrailer(w, err) + } + return + } + hasWritten = true err = encoder.Encode(Response{ - Identity: identity, - Policy: policy, + Identity: iterator.Identity(), + Policy: info.Policy, }) // Once we encounter ErrHandlerTimeout the client connection @@ -619,22 +729,17 @@ func handleListIdentities(roles *auth.Roles) http.HandlerFunc { } } -func handleForgetIdentity(roles *auth.Roles) http.HandlerFunc { - var ( - ErrIdentityUnknown = kes.NewError(http.StatusBadRequest, "identity is unknown") - ErrIdentityRoot = kes.NewError(http.StatusBadRequest, "identity is root") - ) +func handleForgetIdentity(config *ServerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - identity := kes.Identity(pathBase(r.URL.Path)) - if identity.IsUnknown() { - Error(w, ErrIdentityUnknown) + enclave, err := getEnclave(config.Vault, r) + if err != nil { + Error(w, err) return } - if identity == roles.Root { - Error(w, ErrIdentityRoot) + if err = enclave.DeleteIdentity(r.Context(), kes.Identity(pathBase(r.URL.Path))); err != nil { + Error(w, err) return } - roles.Forget(identity) w.WriteHeader(http.StatusOK) } } @@ -706,3 +811,7 @@ func handleMetrics(metrics *metric.Metrics) http.HandlerFunc { } func pathBase(p string) string { return path.Base(p) } + +func getEnclave(vault sys.Vault, r *http.Request) (*sys.Enclave, error) { + return vault.GetEnclave(r.Context(), r.URL.Query().Get("enclave")) +} diff --git a/internal/http/proxy.go b/internal/http/proxy.go index f760b388..b67cbc4c 100644 --- a/internal/http/proxy.go +++ b/internal/http/proxy.go @@ -41,7 +41,7 @@ func tlsProxy(proxy *auth.TLSProxy, f http.HandlerFunc) http.HandlerFunc { // Update the audit log identity such that // the audit log shows the actual client and // not the TLS proxy. - aw.Identity = auth.Identify(r, proxy.Identify) + aw.Identity = auth.Identify(r) } f(w, r) } diff --git a/internal/http/server.go b/internal/http/server.go index 421d5ef6..ca172f11 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -9,9 +9,9 @@ import ( "time" "github.com/minio/kes/internal/auth" - "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" ) // A ServerConfig structure is used to configure a @@ -23,12 +23,7 @@ type ServerConfig struct { // Store is the key store holding the cryptographic // keys. - Store key.Store - - // Roles is the authorization system that - // contains identities and the associated - // policies. - Roles *auth.Roles + Vault sys.Vault // Proxy is an optional TLS proxy that sits // in-front of this server and forwards client @@ -57,42 +52,33 @@ type ServerConfig struct { // uses the given ServerConfig to implement the KES // HTTP API. func NewServerMux(config *ServerConfig) *http.ServeMux { - var ( - version = config.Version - store = config.Store - roles = config.Roles - proxy = config.Proxy - auditLog = config.AuditLog - errorLog = config.ErrorLog - metrics = config.Metrics - ) - if version == "" { - version = "v0.0.0-dev" + if config.Version == "" { + config.Version = "v0.0.0-dev" } const MaxBody = 1 << 20 mux := http.NewServeMux() - mux.Handle("/v1/key/create/", timeout(15*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodPost, validatePath("/v1/key/create/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleCreateKey(store))))))))))) - mux.Handle("/v1/key/import/", timeout(15*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodPost, validatePath("/v1/key/import/*", limitRequestBody(MaxBody, tlsProxy(proxy, enforcePolicies(roles, handleImportKey(store))))))))))) - mux.Handle("/v1/key/delete/", timeout(15*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodDelete, validatePath("/v1/key/delete/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleDeleteKey(store))))))))))) - mux.Handle("/v1/key/generate/", timeout(15*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodPost, validatePath("/v1/key/generate/*", limitRequestBody(MaxBody, tlsProxy(proxy, enforcePolicies(roles, handleGenerateKey(store))))))))))) - mux.Handle("/v1/key/encrypt/", timeout(15*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodPost, validatePath("/v1/key/encrypt/*", limitRequestBody(MaxBody/2, tlsProxy(proxy, enforcePolicies(roles, handleEncryptKey(store))))))))))) - mux.Handle("/v1/key/decrypt/", timeout(15*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodPost, validatePath("/v1/key/decrypt/*", limitRequestBody(MaxBody, tlsProxy(proxy, enforcePolicies(roles, handleDecryptKey(store))))))))))) - mux.Handle("/v1/key/list/", timeout(15*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/v1/key/list/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleListKeys(store))))))))))) + mux.Handle("/v1/key/create/", timeout(15*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodPost, validatePath("/v1/key/create/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleCreateKey(config))))))))))) + mux.Handle("/v1/key/import/", timeout(15*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodPost, validatePath("/v1/key/import/*", limitRequestBody(MaxBody, tlsProxy(config.Proxy, enforcePolicies(config, handleImportKey(config))))))))))) + mux.Handle("/v1/key/delete/", timeout(15*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodDelete, validatePath("/v1/key/delete/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleDeleteKey(config))))))))))) + mux.Handle("/v1/key/generate/", timeout(15*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodPost, validatePath("/v1/key/generate/*", limitRequestBody(MaxBody, tlsProxy(config.Proxy, enforcePolicies(config, handleGenerateKey(config))))))))))) + mux.Handle("/v1/key/encrypt/", timeout(15*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodPost, validatePath("/v1/key/encrypt/*", limitRequestBody(MaxBody/2, tlsProxy(config.Proxy, enforcePolicies(config, handleEncryptKey(config))))))))))) + mux.Handle("/v1/key/decrypt/", timeout(15*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodPost, validatePath("/v1/key/decrypt/*", limitRequestBody(MaxBody, tlsProxy(config.Proxy, enforcePolicies(config, handleDecryptKey(config))))))))))) + mux.Handle("/v1/key/list/", timeout(15*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/v1/key/list/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleListKeys(config))))))))))) - mux.Handle("/v1/policy/write/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodPost, validatePath("/v1/policy/write/*", limitRequestBody(MaxBody, tlsProxy(proxy, enforcePolicies(roles, handleWritePolicy(roles))))))))))) - mux.Handle("/v1/policy/read/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/v1/policy/read/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleReadPolicy(roles))))))))))) - mux.Handle("/v1/policy/list/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/v1/policy/list/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleListPolicies(roles))))))))))) - mux.Handle("/v1/policy/delete/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodDelete, validatePath("/v1/policy/delete/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleDeletePolicy(roles))))))))))) + mux.Handle("/v1/policy/write/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodPost, validatePath("/v1/policy/write/*", limitRequestBody(MaxBody, tlsProxy(config.Proxy, enforcePolicies(config, handleWritePolicy(config))))))))))) + mux.Handle("/v1/policy/read/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/v1/policy/read/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleReadPolicy(config))))))))))) + mux.Handle("/v1/policy/list/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/v1/policy/list/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleListPolicies(config))))))))))) + mux.Handle("/v1/policy/delete/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodDelete, validatePath("/v1/policy/delete/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleDeletePolicy(config))))))))))) - mux.Handle("/v1/identity/assign/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodPost, validatePath("/v1/identity/assign/*/*", limitRequestBody(MaxBody, tlsProxy(proxy, enforcePolicies(roles, handleAssignIdentity(roles))))))))))) - mux.Handle("/v1/identity/list/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/v1/identity/list/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleListIdentities(roles))))))))))) - mux.Handle("/v1/identity/forget/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodDelete, validatePath("/v1/identity/forget/*", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleForgetIdentity(roles))))))))))) + mux.Handle("/v1/identity/assign/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodPost, validatePath("/v1/identity/assign/*/*", limitRequestBody(MaxBody, tlsProxy(config.Proxy, enforcePolicies(config, handleAssignIdentity(config))))))))))) + mux.Handle("/v1/identity/list/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/v1/identity/list/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleListIdentities(config))))))))))) + mux.Handle("/v1/identity/forget/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodDelete, validatePath("/v1/identity/forget/*", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleForgetIdentity(config))))))))))) - mux.Handle("/v1/log/audit/trace", metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/v1/log/audit/trace", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleTraceAuditLog(auditLog)))))))))) - mux.Handle("/v1/log/error/trace", metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/v1/log/error/trace", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleTraceErrorLog(errorLog)))))))))) + mux.Handle("/v1/log/audit/trace", config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/v1/log/audit/trace", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleTraceAuditLog(config.AuditLog)))))))))) + mux.Handle("/v1/log/error/trace", config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/v1/log/error/trace", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleTraceErrorLog(config.ErrorLog)))))))))) - mux.Handle("/v1/status", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/v1/status", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleStatus(version, store, errorLog))))))))))) + mux.Handle("/v1/status", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/v1/status", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleStatus(config))))))))))) // Scrapping /v1/metrics should not change the metrics itself. // Further, scrapping /v1/metrics should, by default, not produce @@ -101,9 +87,9 @@ func NewServerMux(config *ServerConfig) *http.ServeMux { // the audit log will contain a lot of events simply pointing to the // monitoring system. Logging an audit event may be something that // can be enabled optionally. - mux.Handle("/v1/metrics", timeout(10*time.Second, requireMethod(http.MethodGet, validatePath("/v1/metrics", limitRequestBody(0, tlsProxy(proxy, enforcePolicies(roles, handleMetrics(metrics)))))))) + mux.Handle("/v1/metrics", timeout(10*time.Second, requireMethod(http.MethodGet, validatePath("/v1/metrics", limitRequestBody(0, tlsProxy(config.Proxy, enforcePolicies(config, handleMetrics(config.Metrics)))))))) - mux.Handle("/version", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, requireMethod(http.MethodGet, validatePath("/version", limitRequestBody(0, tlsProxy(proxy, handleVersion(version)))))))))) // /version is accessible to any identity - mux.Handle("/", timeout(10*time.Second, metrics.Count(metrics.Latency(audit(auditLog.Log(), roles, tlsProxy(proxy, http.NotFound)))))) + mux.Handle("/version", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), requireMethod(http.MethodGet, validatePath("/version", limitRequestBody(0, tlsProxy(config.Proxy, handleVersion(config.Version)))))))))) // /version is accessible to any identity + mux.Handle("/", timeout(10*time.Second, config.Metrics.Count(config.Metrics.Latency(audit(config.AuditLog.Log(), tlsProxy(config.Proxy, http.NotFound)))))) return mux } diff --git a/internal/sys/enclave.go b/internal/sys/enclave.go new file mode 100644 index 00000000..061c503e --- /dev/null +++ b/internal/sys/enclave.go @@ -0,0 +1,171 @@ +// Copyright 2022 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package sys + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "net/http" + + "github.com/minio/kes" + "github.com/minio/kes/internal/auth" + "github.com/minio/kes/internal/key" +) + +// NewEnclave returns a new Enclave with the +// given key store, policy set and identity set. +func NewEnclave(keys key.Store, policies auth.PolicySet, identities auth.IdentitySet) *Enclave { + return &Enclave{ + keys: keys, + policies: policies, + identities: identities, + } +} + +// An Enclave is shielded environment with a Vault that +// stores keys, policies and identities. +type Enclave struct { + keys key.Store + + policies auth.PolicySet + + identities auth.IdentitySet +} + +func (e *Enclave) Status(ctx context.Context) (key.StoreState, error) { return e.keys.Status(ctx) } + +// CreateKey stores the given key if and only if no entry with +// the given name exists. +// +// It returns kes.ErrKeyExists if such an entry exists. +func (e *Enclave) CreateKey(ctx context.Context, name string, key key.Key) error { + return e.keys.Create(ctx, name, key) +} + +// DeleteKey deletes the key associated with the given name. +func (e *Enclave) DeleteKey(ctx context.Context, name string) error { + return e.keys.Delete(ctx, name) +} + +// GetKey returns the key associated with the given name. +// +// It returns kes.ErrKeyNotFound if no such entry exists. +func (e *Enclave) GetKey(ctx context.Context, name string) (key.Key, error) { + return e.keys.Get(ctx, name) +} + +// ListKeys returns a new iterator over all keys within the +// Enclave. +// +// The iterator makes no guarantees about whether concurrent changes +// to the enclave - i.e. creation or deletion of keys - are reflected. +// It does not provide any ordering guarantees. +func (e *Enclave) ListKeys(ctx context.Context) (key.Iterator, error) { + return e.keys.List(ctx) +} + +// SetPolicy creates or overwrites the policy with the given name. +func (e *Enclave) SetPolicy(ctx context.Context, name string, policy *auth.Policy) error { + return e.policies.Set(ctx, name, policy) +} + +// DeletePolicy deletes the policy associated with the given name. +func (e *Enclave) DeletePolicy(ctx context.Context, name string) error { + return e.policies.Delete(ctx, name) +} + +// GetPolicy returns the policy associated with the given name. +// +// It returns kes.ErrPolicyNotFound when no such entry exists. +func (e *Enclave) GetPolicy(ctx context.Context, name string) (*auth.Policy, error) { + return e.policies.Get(ctx, name) +} + +// ListPolicies returns a new iterator over all policies within +// the Enclave. +// +// The iterator makes no guarantees about whether concurrent changes +// to the enclave - i.e. creation or deletion of policies - are +// reflected. It does not provide any ordering guarantees. +func (e *Enclave) ListPolicies(ctx context.Context) (auth.PolicyIterator, error) { + return e.policies.List(ctx) +} + +// AssignIdentity assigns the given identity a policy referenced by +// the policy name. +func (e *Enclave) AssignIdentity(ctx context.Context, policy string, identities kes.Identity) error { + return e.identities.Assign(ctx, policy, identities) +} + +// DeleteIdentity deletes the given identity. +func (e *Enclave) DeleteIdentity(ctx context.Context, identities kes.Identity) error { + return e.identities.Delete(ctx, identities) +} + +// GetIdentity returns metadata about the given identity. +func (e *Enclave) GetIdentity(ctx context.Context, identity kes.Identity) (auth.IdentityInfo, error) { + return e.identities.Get(ctx, identity) +} + +// ListIdentities returns an iterator over all identites within +// the Enclave. +// +// The iterator makes no guarantees about whether concurrent changes +// to the enclave - i.e. assignment or deletion of identities - are +// reflected. It does not provide any ordering guarantees. +func (e *Enclave) ListIdentities(ctx context.Context) (auth.IdentityIterator, error) { + return e.identities.List(ctx) +} + +// VerifyRequest verifies the given request is allowed +// based on the policies and identities within the Enclave. +func (e *Enclave) VerifyRequest(r *http.Request) error { + if r.TLS == nil { + return kes.NewError(http.StatusBadRequest, "insecure connection: TLS required") + } + + var peerCertificates []*x509.Certificate + switch { + case len(r.TLS.PeerCertificates) <= 1: + peerCertificates = r.TLS.PeerCertificates + case len(r.TLS.PeerCertificates) > 1: + for _, cert := range r.TLS.PeerCertificates { + if cert.IsCA { + continue + } + peerCertificates = append(peerCertificates, cert) + } + } + if len(peerCertificates) == 0 { + return kes.NewError(http.StatusBadRequest, "no client certificate is present") + } + if len(peerCertificates) > 1 { + return kes.NewError(http.StatusBadRequest, "too many client certificates are present") + } + + var ( + h = sha256.Sum256(peerCertificates[0].RawSubjectPublicKeyInfo) + identity = kes.Identity(hex.EncodeToString(h[:])) + ) + admin, err := e.identities.Admin(r.Context()) + if err != nil { + return err + } + if identity == admin { + return nil + } + + info, err := e.GetIdentity(r.Context(), identity) + if err != nil { + return err + } + policy, err := e.GetPolicy(r.Context(), info.Policy) + if err != nil { + return err + } + return policy.Verify(r) +} diff --git a/internal/sys/stateless-vault.go b/internal/sys/stateless-vault.go new file mode 100644 index 00000000..66f47527 --- /dev/null +++ b/internal/sys/stateless-vault.go @@ -0,0 +1,59 @@ +// Copyright 2022 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package sys + +import ( + "context" + "net/http" + + "github.com/minio/kes" + "github.com/minio/kes/internal/auth" + "github.com/minio/kes/internal/key" +) + +// NewStatelessVault returns a new Vault with a single Enclave +// that uses the given key store, policy set and identity set. +// +// The Vault is not able to create or delete enclaves. +func NewStatelessVault(operator kes.Identity, keys key.Store, policies auth.PolicySet, identites auth.IdentitySet) Vault { + return &statelessVault{ + enclave: &Enclave{ + keys: keys, + policies: policies, + identities: identites, + }, + operator: operator, + } +} + +type statelessVault struct { + enclave *Enclave + operator kes.Identity +} + +var _ Vault = (*statelessVault)(nil) // compiler check + +func (v *statelessVault) Seal(ctx context.Context) error { return nil } + +func (v *statelessVault) Unseal(context.Context) error { return nil } + +func (v *statelessVault) Operator(_ context.Context) (kes.Identity, error) { + return v.operator, nil +} + +func (v *statelessVault) CreateEnclave(_ context.Context, _ string) (*Enclave, error) { + return nil, kes.NewError(http.StatusNotImplemented, "creating encalves is not supported") +} + +func (v *statelessVault) GetEnclave(_ context.Context, name string) (*Enclave, error) { + if name == "" { + return v.enclave, nil + } + return nil, ErrEnclaveNotFound +} + +func (v *statelessVault) DeleteEnclave(_ context.Context, _ string) error { + return kes.NewError(http.StatusNotImplemented, "deleting encalves is not supported") +} diff --git a/internal/sys/vault.go b/internal/sys/vault.go new file mode 100644 index 00000000..66692139 --- /dev/null +++ b/internal/sys/vault.go @@ -0,0 +1,50 @@ +// Copyright 2022 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package sys + +import ( + "context" + "net/http" + + "github.com/minio/kes" +) + +var ( + ErrEnclaveExists = kes.NewError(http.StatusBadRequest, "enclave already exists") + + ErrEnclaveNotFound = kes.NewError(http.StatusNotFound, "enclave does not exist") +) + +type Vault interface { + // Seal seals the Vault. Once sealed, any subsequent operation + // returns ErrSealed. + // + // It returns ErrSealed if the Vault is already sealed. + Seal(ctx context.Context) error + + // Unseal unseals the Vault. + // + // It returns no error If the Vault is already unsealed. + Unseal(ctx context.Context) error + + // Operator returns the identity of the Vault operator. + Operator(context.Context) (kes.Identity, error) + + // CreateEnclave creates and returns a new Enclave if and only if + // no Enclave with the given name exists. + // + // It returns ErrEnclaveExists if an Enclave with the given name + // already exists. + CreateEnclave(ctx context.Context, name string) (*Enclave, error) + + // GetEnclave returns the Enclave associated with the given name. + // + // It returns ErrEnclaveNotFound if no Enclave with the given + // name exists. + GetEnclave(ctx context.Context, name string) (*Enclave, error) + + // DeleteEnclave deletes the Enclave with the given name. + DeleteEnclave(ctx context.Context, name string) error +} diff --git a/kestest/policy.go b/kestest/policy.go index 069b451d..9d5c89da 100644 --- a/kestest/policy.go +++ b/kestest/policy.go @@ -5,11 +5,16 @@ package kestest import ( + "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" + "encoding/json" "fmt" + "net/http" + "sync" + "time" "github.com/minio/kes" "github.com/minio/kes/internal/auth" @@ -18,16 +23,36 @@ import ( // PolicySet holds a set of KES policies and // the identity-policy associations. type PolicySet struct { - roles *auth.Roles + admin kes.Identity + policies map[string]*auth.Policy + identities map[kes.Identity]auth.IdentityInfo } // Admin returns the admin Identity that can // perform any KES API operation. -func (p *PolicySet) Admin() kes.Identity { return p.roles.Root } +func (p *PolicySet) Admin() kes.Identity { return p.admin } // Add adds the given KES policy to the PolicySet. // Any existing policy with the same name is replaced. -func (p *PolicySet) Add(name string, policy *kes.Policy) { p.roles.Set(name, policy) } +func (p *PolicySet) Add(name string, policy *kes.Policy) { + type Policy struct { + Allow []string `json:"allow"` + Deny []string `json:"deny"` + } + b, err := json.Marshal(policy) + if err != nil { + panic(err) + } + + var jsonPolicy Policy + if err = json.Unmarshal(b, &jsonPolicy); err != nil { + panic(err) + } + p.policies[name] = &auth.Policy{ + Allow: jsonPolicy.Allow, + Deny: jsonPolicy.Deny, + } +} // Allow adds a new KES policy that allows the given API // patterns to the PolicySet. @@ -49,13 +74,34 @@ func (p *PolicySet) Allow(name string, patterns ...string) { // identities, if any. func (p *PolicySet) Assign(name string, ids ...kes.Identity) error { for _, id := range ids { - if err := p.roles.Assign(name, id); err != nil { - return fmt.Errorf("kestest: failed to assign policy %q to %q: %v", name, id, err) + if id.IsUnknown() { + return fmt.Errorf("kestest: failed to assign policy %q to %q: identity is empty", name, id) + } + if id == p.Admin() { + return fmt.Errorf("kestest: failed to assign policy %q to %q: equal to admin identity", name, id) + } + p.identities[id] = auth.IdentityInfo{ + Policy: name, + CreatedAt: time.Now().UTC(), + CreatedBy: p.admin, } } return nil } +func (p *PolicySet) policySet() auth.PolicySet { + return &policySet{ + policies: p.policies, + } +} + +func (p *PolicySet) identitySet() auth.IdentitySet { + return &identitySet{ + admin: p.admin, + roles: p.identities, + } +} + // Identify returns the Identity of the TLS certificate. // // It computes the Identity as fingerprint of the @@ -72,3 +118,137 @@ func Identify(cert *tls.Certificate) kes.Identity { id := sha256.Sum256(cert.Leaf.RawSubjectPublicKeyInfo) return kes.Identity(hex.EncodeToString(id[:])) } + +type policySet struct { + lock sync.RWMutex + policies map[string]*auth.Policy +} + +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 +} + +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 } + +type identitySet struct { + admin kes.Identity + + lock sync.RWMutex + roles map[kes.Identity]auth.IdentityInfo +} + +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() + + i.roles[identity] = auth.IdentityInfo{ + Policy: policy, + CreatedAt: time.Now().UTC(), + } + 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 +} + +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 } diff --git a/kestest/server.go b/kestest/server.go index e15fd3da..1d08f8ac 100644 --- a/kestest/server.go +++ b/kestest/server.go @@ -28,6 +28,7 @@ import ( "github.com/minio/kes/internal/log" "github.com/minio/kes/internal/mem" "github.com/minio/kes/internal/metric" + "github.com/minio/kes/internal/sys" ) // NewServer starts and returns a new Server. @@ -105,22 +106,22 @@ func (s *Server) start() { adminCert = s.IssueClientCertificate("kestest: admin") ) s.policies = &PolicySet{ - roles: &auth.Roles{ - Root: Identify(&adminCert), - }, + admin: Identify(&adminCert), + policies: make(map[string]*auth.Policy), + identities: make(map[kes.Identity]auth.IdentityInfo), } errorLog.Add(metrics.ErrorEventCounter()) auditLog.Add(metrics.AuditEventCounter()) + store := key.NewCache(&mem.Store{}, &key.CacheConfig{ + Expiry: 30 * time.Second, + ExpiryUnused: 5 * time.Second, + }) serverCert := issueCertificate("kestest: server", s.caCertificate, s.caPrivateKey, x509.ExtKeyUsageServerAuth) s.server = httptest.NewUnstartedServer(xhttp.NewServerMux(&xhttp.ServerConfig{ - Version: "v0.0.0-dev", - Store: key.NewCache(&mem.Store{}, &key.CacheConfig{ - Expiry: 30 * time.Second, - ExpiryUnused: 5 * time.Second, - }), - Roles: s.policies.roles, + Version: "v0.0.0-dev", + Vault: sys.NewStatelessVault(Identify(&adminCert), store, s.policies.policySet(), s.policies.identitySet()), Proxy: nil, AuditLog: auditLog, ErrorLog: errorLog,