mirror of https://github.com/containerd/nri.git
stub: implement plugin authentication.
Add a new option for acquiring a public/private key pair from an external source for authenticating with the runtime. Using this option implicitly enables plugin authentication prior to registration. Also add a new environment variable to read keys from files. Together with bind-mounted secrets, this should provide enough plumbing to enable transparent authentication of containerized plugins. Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
This commit is contained in:
parent
e5918f0ff0
commit
65af47358b
124
pkg/stub/stub.go
124
pkg/stub/stub.go
|
|
@ -20,6 +20,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
stdnet "net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -28,6 +29,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/containerd/nri/pkg/api"
|
||||
"github.com/containerd/nri/pkg/auth"
|
||||
"github.com/containerd/nri/pkg/auth/ecdh"
|
||||
nrilog "github.com/containerd/nri/pkg/log"
|
||||
"github.com/containerd/nri/pkg/net"
|
||||
"github.com/containerd/nri/pkg/net/multiplex"
|
||||
|
|
@ -178,6 +181,11 @@ type Stub interface {
|
|||
// This is the default timeout if the plugin has not been started or
|
||||
// the timeout received in the Configure request otherwise.
|
||||
RequestTimeout() time.Duration
|
||||
|
||||
// GetRole returns the role the plugin was authenticated to, if any.
|
||||
GetRole() string
|
||||
// GetTags returns any tags associated with the plugin's role, if any.
|
||||
GetTags() map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -185,6 +193,8 @@ const (
|
|||
DefaultRegistrationTimeout = api.DefaultPluginRegistrationTimeout
|
||||
// DefaultRequestTimeout is the default plugin request processing timeout.
|
||||
DefaultRequestTimeout = api.DefaultPluginRequestTimeout
|
||||
// DefaultAuthentication is the name of the default authentication algorithm.
|
||||
DefaultAuthentication = ecdh.Name
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -197,6 +207,9 @@ var (
|
|||
// ErrNoService indicates that the stub has no runtime service/connection,
|
||||
// for instance by UpdateContainers on a stub which has not been started.
|
||||
ErrNoService = errors.New("stub: no service/connection")
|
||||
|
||||
// ErrAuth indicates failure to authentication with the runtime.
|
||||
ErrAuth = errors.New("stub: failed to authenticate")
|
||||
)
|
||||
|
||||
// EventMask holds a mask of events for plugin subscription.
|
||||
|
|
@ -268,6 +281,16 @@ func WithTTRPCOptions(clientOpts []ttrpc.ClientOpts, serverOpts []ttrpc.ServerOp
|
|||
}
|
||||
}
|
||||
|
||||
// WithAuthentication sets authentication keys for the plugin. The stub will
|
||||
// use these keys to authenticate itself with the runtime before registration.
|
||||
func WithAuthentication(algorithm string, fetchKeys auth.KeyFetcher) Option {
|
||||
return func(s *stub) error {
|
||||
s.authKeys = fetchKeys
|
||||
s.authAlgo = algorithm
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// stub implements Stub.
|
||||
type stub struct {
|
||||
sync.Mutex
|
||||
|
|
@ -286,6 +309,10 @@ type stub struct {
|
|||
rpcl stdnet.Listener
|
||||
rpcs *ttrpc.Server
|
||||
rpcc *ttrpc.Client
|
||||
authKeys auth.KeyFetcher
|
||||
authAlgo string
|
||||
role string
|
||||
tags map[string]string
|
||||
runtime api.RuntimeService
|
||||
started bool
|
||||
doneC chan struct{}
|
||||
|
|
@ -337,11 +364,25 @@ func New(p interface{}, opts ...Option) (Stub, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if stub.authKeys == nil {
|
||||
if dir := os.Getenv(api.PluginAuthKeyDirEnvVar); dir != "" {
|
||||
stub.authKeys = auth.NewFileKeyFetcher(
|
||||
filepath.Join(dir, "private"),
|
||||
filepath.Join(dir, "public"),
|
||||
)
|
||||
if algo := os.Getenv(api.PluginAuthAlgoEnvVar); algo != "" {
|
||||
stub.authAlgo = algo
|
||||
} else {
|
||||
stub.authAlgo = DefaultAuthentication
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := stub.setupHandlers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := stub.ensureIdentity(); err != nil {
|
||||
if err := stub.ensureNameAndIndex(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -431,6 +472,11 @@ func (stub *stub) Start(ctx context.Context) (retErr error) {
|
|||
|
||||
stub.runtime = api.NewRuntimeClient(rpcc)
|
||||
|
||||
if err = stub.authenticate(ctx); err != nil {
|
||||
stub.close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = stub.register(ctx); err != nil {
|
||||
stub.close()
|
||||
return err
|
||||
|
|
@ -530,6 +576,14 @@ func (stub *stub) RequestTimeout() time.Duration {
|
|||
return stub.requestTimeout
|
||||
}
|
||||
|
||||
func (stub *stub) GetRole() string {
|
||||
return stub.role
|
||||
}
|
||||
|
||||
func (stub *stub) GetTags() map[string]string {
|
||||
return maps.Clone(stub.tags)
|
||||
}
|
||||
|
||||
// Connect the plugin to NRI.
|
||||
func (stub *stub) connect() error {
|
||||
if stub.conn != nil {
|
||||
|
|
@ -564,6 +618,70 @@ func (stub *stub) connect() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Authenticate the plugin with NRI, if we have been set up with keys.
|
||||
func (stub *stub) authenticate(ctx context.Context) error {
|
||||
if stub.authKeys == nil {
|
||||
log.Infof(ctx, "No authentication keys set...")
|
||||
return nil
|
||||
}
|
||||
|
||||
defer stub.authKeys.ClearKeys()
|
||||
private, err := stub.authKeys.PrivateKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to fetch private key: %w", ErrAuth, err)
|
||||
}
|
||||
public, err := stub.authKeys.PublicKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to fetch public key: %w", ErrAuth, err)
|
||||
}
|
||||
|
||||
impl, err := auth.Get(stub.authAlgo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to set up authentication: %w", ErrAuth, err)
|
||||
}
|
||||
|
||||
algo, err := impl.NewWithKeys(private, public)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to set up authentication: %w", ErrAuth, err)
|
||||
}
|
||||
|
||||
client := auth.NewAuthenticationClient(stub.rpcc)
|
||||
ctx, cancel := context.WithTimeout(ctx, stub.requestTimeout)
|
||||
defer cancel()
|
||||
|
||||
rpl, err := client.RequestChallenge(ctx,
|
||||
&auth.RequestChallengeRequest{
|
||||
Algorithm: stub.authAlgo,
|
||||
PublicKey: public,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrAuth, err)
|
||||
}
|
||||
|
||||
response, err := algo.Response(rpl.Challenge, auth.PublicKey(rpl.PublicKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrAuth, err)
|
||||
}
|
||||
|
||||
result, err := client.RespondChallenge(ctx,
|
||||
&auth.RespondChallengeRequest{
|
||||
Response: response,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrAuth, err)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "Authenticated with role %s (tags: %v)...",
|
||||
result.Role, result.Tags)
|
||||
|
||||
stub.role = result.Role
|
||||
stub.tags = result.Tags
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register the plugin with NRI.
|
||||
func (stub *stub) register(ctx context.Context) error {
|
||||
log.Infof(ctx, "Registering plugin %s...", stub.Name())
|
||||
|
|
@ -829,8 +947,8 @@ func (stub *stub) ValidateContainerAdjustment(ctx context.Context, req *api.Vali
|
|||
return &api.ValidateContainerAdjustmentResponse{}, nil
|
||||
}
|
||||
|
||||
// ensureIdentity sets plugin index and name from the binary if those are unset.
|
||||
func (stub *stub) ensureIdentity() error {
|
||||
// ensureNameAndIndex sets plugin index and name from the binary if those are unset.
|
||||
func (stub *stub) ensureNameAndIndex() error {
|
||||
if stub.idx != "" && stub.name != "" {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue