Skip to content

Commit

Permalink
Merge pull request #20347 from ericchiang/authz_grpc
Browse files Browse the repository at this point in the history
Auto commit by PR queue bot
  • Loading branch information
k8s-merge-robot committed Feb 27, 2016
2 parents 1f9a7e2 + b50ede6 commit 00d99ac
Show file tree
Hide file tree
Showing 13 changed files with 1,197 additions and 33 deletions.
5 changes: 3 additions & 2 deletions cmd/kube-apiserver/app/options/options.go
Expand Up @@ -47,7 +47,7 @@ type APIServer struct {
AdvertiseAddress net.IP
AllowPrivileged bool
AuthorizationMode string
AuthorizationPolicyFile string
AuthorizationConfig apiserver.AuthorizationConfig
BasicAuthFile string
CloudConfigFile string
CloudProvider string
Expand Down Expand Up @@ -214,7 +214,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
fs.StringVar(&s.KeystoneURL, "experimental-keystone-url", s.KeystoneURL, "If passed, activates the keystone authentication plugin")
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
fs.StringVar(&s.AuthorizationConfig.PolicyFile, "authorization-policy-file", s.AuthorizationConfig.PolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
fs.StringVar(&s.AuthorizationConfig.WebhookConfigFile, "authorization-webhook-config-file", s.AuthorizationConfig.WebhookConfigFile, "File with webhook configuration in kubeconfig format, used with --authorization-mode=Webhook. The API server will query the remote service to determine access on the API server's secure port.")
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
fs.StringVar(&s.AdmissionControlConfigFile, "admission-control-config-file", s.AdmissionControlConfigFile, "File with admission control configuration.")
fs.StringSliceVar(&s.EtcdServerList, "etcd-servers", s.EtcdServerList, "List of etcd servers to watch (http://ip:port), comma separated. Mutually exclusive with -etcd-config")
Expand Down
2 changes: 1 addition & 1 deletion cmd/kube-apiserver/app/server.go
Expand Up @@ -406,7 +406,7 @@ func Run(s *options.APIServer) error {
}

authorizationModeNames := strings.Split(s.AuthorizationMode, ",")
authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(authorizationModeNames, s.AuthorizationPolicyFile)
authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(authorizationModeNames, s.AuthorizationConfig)
if err != nil {
glog.Fatalf("Invalid Authorization Config: %v", err)
}
Expand Down
107 changes: 107 additions & 0 deletions docs/admin/authorization.md
Expand Up @@ -49,10 +49,12 @@ The following implementations are available, and are selected by flag:
- `--authorization-mode=AlwaysDeny`
- `--authorization-mode=AlwaysAllow`
- `--authorization-mode=ABAC`
- `--authorization-mode=Webhook`

`AlwaysDeny` blocks all requests (used in tests).
`AlwaysAllow` allows all requests; use if you don't need authorization.
`ABAC` allows for user-configured authorization policy. ABAC stands for Attribute-Based Access Control.
`Webhook` allows for authorization to be driven by a remote service using REST.

## ABAC Mode

Expand Down Expand Up @@ -167,6 +169,111 @@ For example, if you wanted to grant the default service account in the kube-syst

The apiserver will need to be restarted to pickup the new policy lines.

## Webhook Mode

When specified, mode `Webhook` causes Kubernetes to query an outside REST service when determining user privileges.

### Configuration File Format

Mode `Webhook` requires a file for HTTP configuration, specify by the `--authorization-webhook-config-file=SOME_FILENAME` flag.

The configuration file uses the [kubeconfig](../user-guide/kubeconfig-file.md) file format. Within the file "users" refers to the API Server webhook and "clusters" refers to the remote service.

A configuration example which uses HTTPS client auth:

```yaml
# clusters refers to the remote service.
clusters:
- name: name-of-remote-authz-service
cluster:
certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
server: https://authz.example.com/authorize # URL of remote service to query. Must use 'https'.

# users refers to the API Server's webhook configuration.
users:
- name: name-of-api-server
user:
client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
client-key: /path/to/key.pem # key matching the cert
```

### Request Payloads

When faced with an authorization decision, the API Server POSTs a JSON serialized api.authorization.v1beta1.SubjectAccessReview object describing the action. This object contains fields describing the user attempting to make the request, and either details about the resource being accessed or requests attributes.

Note that webhook API objects are subject to the same [versioning compatibility rules](../api.md) as other Kubernetes API objects. Implementers should be aware of loser compatibility promises for beta objects and check the "apiVersion" field of the request to ensure correct deserialization. Additionally, the API Server must enable the `authorization.k8s.io/v1beta1` API extensions group (`--runtime-config=authorization.k8s.io/v1beta1=true`).

An example request body:

```json
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"spec": {
"resourceAttributes": {
"namespace": "kittensandponies",
"verb": "GET",
"group": "*",
"resource": "pods"
},
"user": "jane",
"group": [
"group1",
"group2"
]
}
}
```

The remote service is expected to fill the SubjectAccessReviewStatus field of the request and respond to either allow or disallow access. The response body's "spec" field is ignored and may be omitted. A permissive response would return:

```json
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": true
}
}
```

To disallow access, the remote service would return:

```json
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": false,
"reason": "user does not have read access to the namespace"
}
}
```

Access to non-resource paths are sent as:

```json
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"spec": {
"nonResourceAttributes": {
"path": "/debug",
"verb": "GET"
},
"user": "jane",
"group": [
"group1",
"group2"
]
}
}
```

Non-resource paths include: `/api`, `/apis`, `/metrics`, `/resetMetrics`, `/logs`, `/debug`, `/healthz`, `/swagger-ui/`, `/swaggerapi/`, `/ui`, and `/version.` Clients require access to `/api`, `/api/*/`, `/apis/`, `/apis/*`, `/apis/*/*`, and `/version` to discover what resources and versions are present on the server. Access to other non-resource paths can be disallowed without restricting access to the REST api.

For further documentation refer to the authorization.v1beta1 API objects and plugin/pkg/auth/authorizer/webhook/webhook.go.

## Plugin Development

Other implementations can be developed fairly easily.
Expand Down
3 changes: 2 additions & 1 deletion docs/admin/kube-apiserver.md
Expand Up @@ -56,8 +56,9 @@ kube-apiserver
--advertise-address=<nil>: The IP address on which to advertise the apiserver to members of the cluster. This address must be reachable by the rest of the cluster. If blank, the --bind-address will be used. If --bind-address is unspecified, the host's default interface will be used.
--allow-privileged[=false]: If true, allow privileged containers.
--apiserver-count=1: The number of apiservers running in the cluster
--authorization-mode="AlwaysAllow": Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: AlwaysAllow,AlwaysDeny,ABAC
--authorization-mode="AlwaysAllow": Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: AlwaysAllow,AlwaysDeny,ABAC,Webhook
--authorization-policy-file="": File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.
--authorization-webhook-config-file="": File with webhook configuration in kubeconfig format, used with --authorization-mode=Webhook. The API server will query the remote service to determine access on the API server's secure port.
--basic-auth-file="": If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.
--bind-address=0.0.0.0: The IP address on which to listen for the --secure-port port. The associated interface(s) must be reachable by the rest of the cluster, and by CLI/web clients. If blank, all interfaces will be used (0.0.0.0).
--cert-dir="/var/run/kubernetes": The directory where the TLS certs are located (by default /var/run/kubernetes). If --tls-cert-file and --tls-private-key-file are provided, this flag will be ignored.
Expand Down
2 changes: 1 addition & 1 deletion hack/test-go.sh
Expand Up @@ -326,7 +326,7 @@ for (( i=0, j=0; ; )); do
# KUBE_TEST_API sets the version of each group to be tested. KUBE_API_VERSIONS
# register the groups/versions as supported by k8s. So KUBE_API_VERSIONS
# needs to be the superset of KUBE_TEST_API.
KUBE_TEST_API="${apiVersion}" KUBE_API_VERSIONS="v1,autoscaling/v1,batch/v1,extensions/v1beta1,componentconfig/v1alpha1,metrics/v1alpha1" ETCD_PREFIX=${etcdPrefix} runTests "$@"
KUBE_TEST_API="${apiVersion}" KUBE_API_VERSIONS="v1,autoscaling/v1,batch/v1,extensions/v1beta1,componentconfig/v1alpha1,metrics/v1alpha1,authorization.k8s.io/v1beta1" ETCD_PREFIX=${etcdPrefix} runTests "$@"
i=${i}+1
j=${j}+1
if [[ i -eq ${apiVersionsCount} ]] && [[ j -eq ${etcdPrefixesCount} ]]; then
Expand Down
1 change: 1 addition & 0 deletions hack/verify-flags/known-flags.txt
Expand Up @@ -20,6 +20,7 @@ apiserver-count
auth-path
authorization-mode
authorization-policy-file
authorization-webhook-config-file
basic-auth-file
bench-pods
bench-quiet
Expand Down
36 changes: 31 additions & 5 deletions pkg/apiserver/authz.go
Expand Up @@ -23,6 +23,7 @@ import (
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/authorizer/abac"
"k8s.io/kubernetes/pkg/auth/authorizer/union"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/webhook"
)

// Attributes implements authorizer.Attributes interface.
Expand Down Expand Up @@ -60,15 +61,28 @@ const (
ModeAlwaysAllow string = "AlwaysAllow"
ModeAlwaysDeny string = "AlwaysDeny"
ModeABAC string = "ABAC"
ModeWebhook string = "Webhook"
)

// Keep this list in sync with constant list above.
var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}
var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC, ModeWebhook}

type AuthorizationConfig struct {
// Options for ModeABAC

// Path to a ABAC policy file.
PolicyFile string

// Options for ModeWebhook

// Kubeconfig file for Webhook authorization plugin.
WebhookConfigFile string
}

// NewAuthorizerFromAuthorizationConfig returns the right sort of union of multiple authorizer.Authorizer objects
// based on the authorizationMode or an error. authorizationMode should be a comma separated values
// of AuthorizationModeChoices.
func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, authorizationPolicyFile string) (authorizer.Authorizer, error) {
func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, config AuthorizationConfig) (authorizer.Authorizer, error) {

if len(authorizationModes) == 0 {
return nil, errors.New("Atleast one authorization mode should be passed")
Expand All @@ -88,23 +102,35 @@ func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, authoriza
case ModeAlwaysDeny:
authorizers = append(authorizers, NewAlwaysDenyAuthorizer())
case ModeABAC:
if authorizationPolicyFile == "" {
if config.PolicyFile == "" {
return nil, errors.New("ABAC's authorization policy file not passed")
}
abacAuthorizer, err := abac.NewFromFile(authorizationPolicyFile)
abacAuthorizer, err := abac.NewFromFile(config.PolicyFile)
if err != nil {
return nil, err
}
authorizers = append(authorizers, abacAuthorizer)
case ModeWebhook:
if config.WebhookConfigFile == "" {
return nil, errors.New("Webhook's configuration file not passed")
}
webhookAuthorizer, err := webhook.New(config.WebhookConfigFile)
if err != nil {
return nil, err
}
authorizers = append(authorizers, webhookAuthorizer)
default:
return nil, fmt.Errorf("Unknown authorization mode %s specified", authorizationMode)
}
authorizerMap[authorizationMode] = true
}

if !authorizerMap[ModeABAC] && authorizationPolicyFile != "" {
if !authorizerMap[ModeABAC] && config.PolicyFile != "" {
return nil, errors.New("Cannot specify --authorization-policy-file without mode ABAC")
}
if !authorizerMap[ModeWebhook] && config.WebhookConfigFile != "" {
return nil, errors.New("Cannot specify --authorization-webhook-config-file without mode Webhook")
}

return union.New(authorizers...), nil
}
90 changes: 67 additions & 23 deletions pkg/apiserver/authz_test.go
Expand Up @@ -41,31 +41,75 @@ func TestNewAlwaysDenyAuthorizer(t *testing.T) {
// NewAuthorizerFromAuthorizationConfig has multiple return possibilities. This test
// validates that errors are returned only when proper.
func TestNewAuthorizerFromAuthorizationConfig(t *testing.T) {
// Unknown modes should return errors
if _, err := NewAuthorizerFromAuthorizationConfig([]string{"DoesNotExist"}, ""); err == nil {
t.Errorf("NewAuthorizerFromAuthorizationConfig using a fake mode should have returned an error")
}

// ModeAlwaysAllow and ModeAlwaysDeny should return without authorizationPolicyFile
// but error if one is given
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny}, ""); err != nil {
t.Errorf("NewAuthorizerFromAuthorizationConfig returned an error: %s", err)
}
examplePolicyFile := "../auth/authorizer/abac/example_policy_file.jsonl"

// ModeABAC requires a policy file
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}, ""); err == nil {
t.Errorf("NewAuthorizerFromAuthorizationConfig using a fake mode should have returned an error")
}
// ModeABAC should not error if a valid policy path is provided
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}, "../auth/authorizer/abac/example_policy_file.jsonl"); err != nil {
t.Errorf("NewAuthorizerFromAuthorizationConfig errored while using a valid policy file: %s", err)
}
// Authorization Policy file cannot be used without ModeABAC
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny}, "../auth/authorizer/abac/example_policy_file.jsonl"); err == nil {
t.Errorf("NewAuthorizerFromAuthorizationConfig should have errored when Authorization Policy File is used without ModeABAC")
tests := []struct {
modes []string
config AuthorizationConfig
wantErr bool
msg string
}{
{
// Unknown modes should return errors
modes: []string{"DoesNotExist"},
wantErr: true,
msg: "using a fake mode should have returned an error",
},
{
// ModeAlwaysAllow and ModeAlwaysDeny should return without authorizationPolicyFile
// but error if one is given
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny},
msg: "returned an error for valid config",
},
{
// ModeABAC requires a policy file
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC},
wantErr: true,
msg: "specifying ABAC with no policy file should return an error",
},
{
// ModeABAC should not error if a valid policy path is provided
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC},
config: AuthorizationConfig{PolicyFile: examplePolicyFile},
msg: "errored while using a valid policy file",
},
{

// Authorization Policy file cannot be used without ModeABAC
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny},
config: AuthorizationConfig{PolicyFile: examplePolicyFile},
wantErr: true,
msg: "should have errored when Authorization Policy File is used without ModeABAC",
},
{
// Atleast one authorizationMode is necessary
modes: []string{},
config: AuthorizationConfig{PolicyFile: examplePolicyFile},
wantErr: true,
msg: "should have errored when no authorization modes are passed",
},
{
// ModeWebhook requires at minimum a target.
modes: []string{ModeWebhook},
wantErr: true,
msg: "should have errored when config was empty with ModeWebhook",
},
{
// Cannot provide webhook flags without ModeWebhook
modes: []string{ModeAlwaysAllow},
config: AuthorizationConfig{WebhookConfigFile: "authz_webhook_config.yml"},
wantErr: true,
msg: "should have errored when Webhook config file is used without ModeWebhook",
},
}
// Atleast one authorizationMode is necessary
if _, err := NewAuthorizerFromAuthorizationConfig([]string{}, "../auth/authorizer/abac/example_policy_file.jsonl"); err == nil {
t.Errorf("NewAuthorizerFromAuthorizationConfig should have errored when no authorization modes are passed")

for _, tt := range tests {
_, err := NewAuthorizerFromAuthorizationConfig(tt.modes, tt.config)
if tt.wantErr && (err == nil) {
t.Errorf("NewAuthorizerFromAuthorizationConfig %s", tt.msg)
} else if !tt.wantErr && (err != nil) {
t.Errorf("NewAuthorizerFromAuthorizationConfig %s: %v", tt.msg, err)
}
}
}
18 changes: 18 additions & 0 deletions plugin/pkg/auth/authorizer/doc.go
@@ -0,0 +1,18 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package authorizer contains implementations for pkg/auth/authorizer interfaces
package authorizer

1 comment on commit 00d99ac

@k8s-teamcity-mesosphere

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TeamCity OSS :: Kubernetes Mesos :: 4 - Smoke Tests Build 17608 outcome was SUCCESS
Summary: Tests passed: 1, ignored: 238 Build time: 00:08:34

Please sign in to comment.