Skip to content

Commit

Permalink
An experimental support for authn/authz w/ RBAC utilizing kube-rbac-p…
Browse files Browse the repository at this point in the history
…roxy

Pre-requisites:

- Build the helm binary from this commit and place it in `$GOPATH/src/k8s.io/helm/bin/helm`
- Build the tiller docker image from this commit and push to mumoshu/tiller:canary
- Build a kube-rbac-proxy image from the latest master of https://github.com/brancz/kube-rbac-proxy and tag it mumoshu/kube-rbac-proxy:v0.2.0

Usage:

```
# 1. Update the tiller deployment
$ k edit deploy tiller-deploy

# 1-1. Modify the tiller container to listen on an another port while enabling the experimental auth feature

  - args:
    - --experimental-rbac-proxy
    - --listen
    - :44137
    command:
    - /tiller
    image: mumoshu/tiller:canary

# 1-2. Add kube-rbac-proxy is a sidecar container

  - args:
    - --alsologtostderr
    - --v=10
    - --insecure-listen-address=:44134
    - --upstream=http://127.0.0.1:44137/
    - --upstream-force-h2c
    - --auth-header-fields-enabled
    - --auth-header-user-field-name=x-forwarded-user
    - --auth-header-groups-field-name=x-forwarded-groups
    image: mumoshu/kube-rbac-proxy:v0.2.0
    imagePullPolicy: IfNotPresent
    name: kube-rbac-proxy
    ports:
    - containerPort: 44134
      name: grpc
      protocol: TCP

# 2. Create a serviceaccount for testing purpose

$ k create serviceaccount permissive-sa
$ k create clusterrolebinding permissive-binding   --clusterrole=cluster-admin --serviceaccount=permissive-sa
$ secret=<permissive-sa's secret name>
$ token=$(k get secret $secret -o json | jq -r '.data.token' | base64 -D)
$ k config set-credentials permissive-sa --token $token

# 3. Create a invalid serviceaccount for testing purpose

$ kubectl config set-credentials invalid-sa --token somerandomstring

# 4. Test it!

# 4-1. A k8s user who is authenticated and authorized to to access tiller can list helm releases

$ kubensx use #=> user: permissive-sa
$ $GOPATH/src/k8s.io/helm/bin/helm list --debug --experimental-rbac-auth-proxy

# 4-2. A k8s user who is not authenticated can not access tiller
#
# Note that the error message is unintuitive at the moment!

$ kubensx use #=> user: invalid-sa
$ $GOPATH/src/k8s.io/helm/bin/helm list --debug --experimental-rbac-auth-proxy

[debug] Created tunnel using local port: '56945'

[debug] SERVER: "127.0.0.1:56945"

Error: transport: received the unexpected content-type "text/plain; charset=utf-8"
```

Exercise:

- Create a kube-rbax-procy resource-attributes file to restrict the serviceaccount to only access specific tiller APIs. Note that a gRPC call results in an HTTP/2 request to the path /<package.Type>/<method> so you can use nonResourceURLs to write policies for gRPC calls.

Future work:

- Tiller impersonates as the authenticated user
  • Loading branch information
mumoshu committed Jan 15, 2018
1 parent b4c0886 commit 58b13ba
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 9 deletions.
22 changes: 22 additions & 0 deletions cmd/helm/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ var (
tlsCertDefault = "$HELM_HOME/cert.pem"
tlsKeyDefault = "$HELM_HOME/key.pem"

// assumes two thins:
// 1. kube-rbac-proxy is installed as a sidecar of tiller and
// we can reach to tiller only via kube-rbac-proxy
// 2. helm passes the bearer token specified in your kubeconfig to kube-rbac-proxy for delegating authn/authz
rbacProxyEnable bool

tillerTunnel *kube.Tunnel
settings helm_env.EnvSettings
)
Expand Down Expand Up @@ -289,6 +295,21 @@ func newClient() helm.Interface {
}
options = append(options, helm.WithTLS(tlscfg))
}

if rbacProxyEnable {
config, err := configForContext(settings.KubeContext)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if config.TLSClientConfig.KeyFile != "" {
fmt.Fprintln(os.Stderr, "You have specified a key file in your kubeconfig but TLS between helm and kube-rbac-proxy is not implemented yet")
os.Exit(1)
}
if config.BearerToken != "" {
options = append(options, helm.WithBearerToken(config.BearerToken))
}
}
return helm.NewClient(options...)
}

Expand All @@ -302,5 +323,6 @@ func addFlagsTLS(cmd *cobra.Command) *cobra.Command {
cmd.Flags().StringVar(&tlsKeyFile, "tls-key", tlsKeyDefault, "path to TLS key file")
cmd.Flags().BoolVar(&tlsVerify, "tls-verify", false, "enable TLS for request and verify remote")
cmd.Flags().BoolVar(&tlsEnable, "tls", false, "enable TLS for request")
cmd.Flags().BoolVar(&rbacProxyEnable, "experimental-rbac-proxy", false, "enable authn/authz to Tiller with K8S RBAC using kube-rbac-proxy")
return cmd
}
7 changes: 6 additions & 1 deletion cmd/tiller/tiller.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ var (
enableTracing = flag.Bool("trace", false, "enable rpc tracing")
store = flag.String("storage", storageConfigMap, "storage driver to use. One of 'configmap', 'memory', or 'secret'")
remoteReleaseModules = flag.Bool("experimental-release", false, "enable experimental release modules")
rbacProxy = flag.Bool("experimental-rbac-proxy", false, "enable experimental RBAC auth proxy to authn/authz the helm client")
tlsEnable = flag.Bool("tls", tlsEnableEnvVarDefault(), "enable TLS")
tlsVerify = flag.Bool("tls-verify", tlsVerifyEnvVarDefault(), "enable TLS and verify remote certificate")
keyFile = flag.String("tls-key", tlsDefaultsFromEnv("tls-key"), "path to TLS private key file")
Expand Down Expand Up @@ -163,7 +164,11 @@ func start() {
}))
}

rootServer = tiller.NewServer(opts...)
f := &tiller.ServerOptsFactory{
AuthProxyEnabled: *rbacProxy,
}

rootServer = tiller.NewServer(f, opts...)

lstn, err := net.Listen("tcp", *grpcAddr)
if err != nil {
Expand Down
28 changes: 28 additions & 0 deletions pkg/helm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"time"

"golang.org/x/net/context"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/keepalive"
Expand Down Expand Up @@ -296,6 +297,13 @@ func (h *Client) connect(ctx context.Context) (conn *grpc.ClientConn, err error)
Time: time.Duration(30) * time.Second,
}),
}
if h.opts.useBearerToken {
bearerTokenAccess := NewTokenAccess(&oauth2.Token{
TokenType: "Bearer",
AccessToken: h.opts.bearerToken,
})
opts = append(opts, grpc.WithPerRPCCredentials(bearerTokenAccess))
}
switch {
case h.opts.useTLS:
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(h.opts.tlsConfig)))
Expand All @@ -308,6 +316,26 @@ func (h *Client) connect(ctx context.Context) (conn *grpc.ClientConn, err error)
return conn, nil
}

// oauthAccess supplies PerRPCCredentials from a given token.
type tokenAccess struct {
token oauth2.Token
}

// NewOauthAccess constructs the PerRPCCredentials using a given token.
func NewTokenAccess(token *oauth2.Token) credentials.PerRPCCredentials {
return tokenAccess{token: *token}
}

func (oa tokenAccess) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": oa.token.Type() + " " + oa.token.AccessToken,
}, nil
}

func (oa tokenAccess) RequireTransportSecurity() bool {
return false
}

// Executes tiller.ListReleases RPC.
func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) {
c, err := h.connect(ctx)
Expand Down
12 changes: 12 additions & 0 deletions pkg/helm/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type options struct {
host string
// if set dry-run helm client calls
dryRun bool
// bearer token used to authenticate against kube-rbac-proxy in front of tiller
bearerToken string
// if set enable bearer token auth on helm client calls
useBearerToken bool
// if set enable TLS on helm client calls
useTLS bool
// if set, re-use an existing name
Expand Down Expand Up @@ -95,6 +99,14 @@ func WithTLS(cfg *tls.Config) Option {
}
}

// WithBearerToken specifics the bearer token used for authenticating helm client against kube-rbac-proxy in front of tiller
func WithBearerToken(bearerToken string) Option {
return func(opts *options) {
opts.useBearerToken = true
opts.bearerToken = bearerToken
}
}

// BeforeCall returns an option that allows intercepting a helm client rpc
// before being sent OTA to tiller. The intercepting function should return
// an error to indicate that the call should not proceed or nil otherwise.
Expand Down
59 changes: 51 additions & 8 deletions pkg/tiller/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,26 @@ import (
// grpc library default is 4MB
var maxMsgSize = 1024 * 1024 * 20

// ServerOptsFactory creates a set of `grpc.ServerOption` to add validation, authn and authz to Tiller
type ServerOptsFactory struct {
AuthProxyEnabled bool
}

// DefaultServerOpts returns the set of default grpc ServerOption's that Tiller requires.
func DefaultServerOpts() []grpc.ServerOption {
func (f ServerOptsFactory) DefaultServerOpts() []grpc.ServerOption {
return []grpc.ServerOption{
grpc.MaxMsgSize(maxMsgSize),
grpc.UnaryInterceptor(newUnaryInterceptor()),
grpc.StreamInterceptor(newStreamInterceptor()),
grpc.UnaryInterceptor(f.newUnaryInterceptor()),
grpc.StreamInterceptor(f.newStreamInterceptor()),
}
}

// NewServer creates a new grpc server.
func NewServer(opts ...grpc.ServerOption) *grpc.Server {
return grpc.NewServer(append(DefaultServerOpts(), opts...)...)
func NewServer(f *ServerOptsFactory, opts ...grpc.ServerOption) *grpc.Server {
return grpc.NewServer(append(f.DefaultServerOpts(), opts...)...)
}

func newUnaryInterceptor() grpc.UnaryServerInterceptor {
func (f *ServerOptsFactory) newUnaryInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if err := checkClientVersion(ctx); err != nil {
// whitelist GetVersion() from the version check
Expand All @@ -56,16 +61,23 @@ func newUnaryInterceptor() grpc.UnaryServerInterceptor {
return nil, err
}
}
if err := f.optionallyCheckAuthenticatedUser(ctx); err != nil {
return nil, err
}
return goprom.UnaryServerInterceptor(ctx, req, info, handler)
}
}

func newStreamInterceptor() grpc.StreamServerInterceptor {
func (f *ServerOptsFactory) newStreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if err := checkClientVersion(ss.Context()); err != nil {
ctx := ss.Context()
if err := checkClientVersion(ctx); err != nil {
log.Println(err)
return err
}
if err := f.optionallyCheckAuthenticatedUser(ctx); err != nil {
return err
}
return goprom.StreamServerInterceptor(srv, ss, info, handler)
}
}
Expand Down Expand Up @@ -93,3 +105,34 @@ func checkClientVersion(ctx context.Context) error {
}
return nil
}

func authenticatedUserFromContext(ctx context.Context) (string, []string) {
user := ""
groups := []string{}
if md, ok := metadata.FromIncomingContext(ctx); ok {
log.Printf("Request Metadata: %v", md)
if v, ok := md["x-forwarded-user"]; ok && len(v) > 0 {
user = v[0]
}
if v, ok := md["x-forwarded-groups"]; ok && len(v) > 0 {
groups = strings.Split(v[0], "|")
}
}
return user, groups
}

func checkAuthenticatedUser(ctx context.Context) error {
u, g := authenticatedUserFromContext(ctx)
if u == "" {
return fmt.Errorf("unauthorized access to tiller")
}
log.Printf("Authenticated as: user=%s, groups=%s", u, strings.Join(g, ","))
return nil
}

func (f *ServerOptsFactory) optionallyCheckAuthenticatedUser(ctx context.Context) error {
if f.AuthProxyEnabled {
return checkAuthenticatedUser(ctx)
}
return nil
}

0 comments on commit 58b13ba

Please sign in to comment.