Cedar supports defining a schema with which policies can be validated from. Schemas also serve as a way to document what attributes each entity.
This project uses the cedar-policy/cedar-go library, which does not yet support schema validation of policies. The referenced schemas are primarily created to document the entity shapes and actions that the project uses today, and help policy authors validate their policies.
For the authorization schema, see cedarschema/k8s-authorization.cedarschema.
This project supports the following Principal entities:
k8s::Group
. Groups are identified by the group name in policy.entity Group = { "name": __cedar::String };
k8s::User
. Users are identified by the user's UID as reported by the authenticator. The group list comes in from the Kubernetes authenticator (webhook, serviceaccount, OIDC, etc), so we dynamically build the list of group Entities for a request. Kubernetes authenticators can also includes extra key/value information on a user, and that is encoded in the 'extra' attribute.entity User in [Group] = { "extra"?: Set < ExtraAttribute >, "name": __cedar::String }; type Extra = { "key": __cedar::String, "values"?: Set < __cedar::String > };
k8s::ServiceAccount
. When a user's name in a SubjectAccessReview starts withsystem:serviceaccount:
, the authorizer sets the principal type tok8s::ServiceAccount
with the following attributes.entity ServiceAccount in [Group] = { "extra"?: Set < ExtraAttribute >, "name": __cedar::String, "namespace": __cedar::String };
k8s::Node
. When a user's name in a SubjectAccessReview starts withsystem:node:
, the authorizer sets the principal type tok8s::Node
with the following attributes. Most node authorization happens in the in-tree NodeAuthorizer and happens before a Cedar decision, but there are some rules the NodeAuthorizer delegates to RBAC and the NodeRestriciton Admission plugin. Cedar can allow or forbid any of those reqeusts.entity Node in [Group] = { "extra"?: Set < ExtraAttribute >, "name": __cedar::String };
Authorization actions are pretty simple, just the verb
name from the Kubernetes request. The following policy would allow any user in the group viewers
to get
/list
/watch
on any request, unless the request's resource has the field resource
and the value of resource
is secrets
.
permit (
principal in k8s::Group::"viewers",
action in [k8s::Action::"get", k8s::Action::"list", k8s::Action::"watch"],
resource is k8s::Resource
) unless {
resource.resource == "secrets" &&
resource.apiGroup == "" // "" is the core API group in Kubernetes
};
We do have an action group for all read-only actions. It encompasses get
/list
/watch
, and is called readOnly
, and only applies to k8s::Resource
resources.
permit (
principal in k8s::Group::"viewers",
action in k8s::Action::"readOnly", // allows any get/list/watch
resource is k8s::Resource
) unless {
resource.resource == "secrets" &&
resource.apiGroup == "" // "" is the core API group in Kubernetes
};
"resource"
,"k8s::Resource"
, and"resource.resource
, why the redundancy?!This is an unfortunate naming collision. Cedar policies always have a
resource
as part of the policy. We call Kubernetes typed objectsk8s::Resource
as opposed to thek8s::NonResourceURL
type, because that's what Kubernetes calls them. And finally, Kubernetes authorization checks refer to they type of object as aresource
, along with the object'sapiGroup
,namespace
,name
, etc.
We define two primary resource types for this authorizer:
NonResourceURL
: This is for non-resource requests made to the Kubernetes API server. Examples include/healthz
,/livez
,/metrics
, and subpaths (Hint: runkubectl get --raw /
to see others). A request's path is also used as the identifier in the entity list when evaluated for authorization. Paths can match a*
on the suffix.Examples:entity NonResourceURL = { "path": __cedar::String };
// allow multiple URLs permit ( principal in k8s::Group::"system:authenticated", action == k8s::Action::"get", resource is k8s::NonResourceURL ) when { ["/version", "/healthz"].contains(resource.path) || resource.path like "/healthz/*" }; // explicitly list one path permit ( principal in k8s::Group::"version-getter", action == k8s::Action::"get", resource == k8s::NonResourceURL::"/version" );
Resource
: This is for resource requests made to the Kubernetes API server. Entity IDs on resources are the constructed URL path being made for the request.Examples:entity Resource = { "apiGroup": __cedar::String, "fieldSelector"?: Set < FieldRequirement >, "labelSelector"?: Set < LabelRequirement >, "name"?: __cedar::String, "namespace"?: __cedar::String, "resource": __cedar::String, "subresource"?: __cedar::String };
// "viewers" group members can get/list/watch any Namespaced other than secrets permit ( principal in k8s::Group::"viewers", action in [ k8s::Action::"get", k8s::Action::"list", k8s::Action::"watch"], resource is k8s::Resource ) unless { resource.resource == "secrets" && // "" is the core API group in Kubernetes resource.apiGroup == "" // any/all namespaces }; // Allow developers to manage deployments in any namespace other than kube-system or kube-public permit ( principal in k8s::Group::"developers", action in [ k8s::Action::"get", k8s::Action::"list", k8s::Action::"watch", k8s::Action::"create", k8s::Action::"update", k8s::Action::"delete"], resource is k8s::Resource ) when { resource.resource == "deployments" && resource.apiGroup == "apps" && // require a namespace name so cluster-scoped collection requests are not permitted resource has namespace } unless { // permit does not apply under these conditions resource has namespace && ["kube-system", "kube-public" ].contains(resource.namespace) };
Resource
has a fieldSelector
and labelSelector
types. These were added in Kubernetes 1.31 behind the AuthorizeWithSelectors
feature gate so authorizers can enforce that a watch or list request has a field or label selector:
type FieldRequirement = {
"field": __cedar::String,
"operator": __cedar::String,
"value": __cedar::String
};
type LabelRequirement = {
"key": __cedar::String,
"operator": __cedar::String,
"values": Set < __cedar::String >
};
Selectors can be used to enforce attribute-based access policies, such as enforcing that a user can only get/list/watch resources where the label owner
equals the user's name
permit (
principal is k8s::User,
action in [k8s::Action::"list", k8s::Action::"watch"],
resource is k8s::Resource
) when {
// "" is the core API group in Kubernetes
resource.apiGroup == "" &&
resource.resource == "configmaps" &&
resource has labelSelector &&
resource.labelSelector.containsAny([
{"key": "owner","operator": "=", "values": [principal.name]},
{"key": "owner","operator": "==", "values": [principal.name]},
{"key": "owner","operator": "in", "values": [principal.name]}])
};
For the user test-user
, the first request would fail, but the second will succeed:
$ KUBECONFIG=./mount/test-user-kubeconfig.yaml kubectl get secrets
Error from server (Forbidden): secrets is forbidden: User "test-user" cannot list resource "secrets" in API group "" in the namespace "default"
$ KUBECONFIG=./mount/test-user-kubeconfig.yaml kubectl get secrets -l owner=test-user
NAME TYPE DATA AGE
example-secret Opaque 1 2d20h
# as admin
$ kubectl get secrets --show-labels
NAME TYPE DATA AGE LABELS
example-secret Opaque 1 2d20h owner=test-user
other-example-secret Opaque 1 2d20h owner=prod-user
To make an impersonated request as another user, Kubernetes sends multiple authorization requests to an authorizer: one for each attribute being impersonated: The user's name, the UID (if set), the groups (if set), and the userInfo extra key/value map. To support this, we define a few types:
Group
. This structure is the same from the principal type. This only functions if the user can also impersonate the requested username.:permit ( principal in k8s::Group::"actors", action == k8s::Action::"impersonate", resource == k8s::Group::"superheros" );
User
. This structure is the same from the principal type:permit ( principal is k8s::User, action == k8s::Action::"impersonate", resource is k8s::User ) when { principal.name == "markhamill" && resource.name == "lukeskywaker" };
PrincipalUID
: To allow impersonating a Principal's UID, the policy's resource type must bePrincipalUID
. This only functions if the user can also impersonate the requested username.Examples:entity PrincipalUID;
permit ( principal in k8s::Group::"actors", action == k8s::Action::"impersonate", resource == k8s::PrincipalUID::"26A82C8D-CC8B-49BB-B2CF-070B9CF1A4F8" );
Extra
: To allow impersonating a principal's key/values extra info, the policy's resource type must beExtra
. This only functions if the user can also impersonate the requested username.Examples:entity Extra = { "key": __cedar::String, "values"?: Set < __cedar::String > };
permit ( principal in k8s::Group::"actors", action == k8s::Action::"impersonate", resource is k8s::Extra ) when { resource.key == "order" && resource has values && ["jedi"].containsAll(resource.values) };
ServiceAccount
This structure is the same from the principal type:permit ( principal is k8s::ServiceAccount, action == k8s::Action::"impersonate", resource is k8s::ServiceAccount ) when { principal.name == "kube-controller-manager" && principal.namespace == "kube-system" && resource.name == "service-account-controller" && resource.namespace == "kube-system" };
Node
This structure is the same from the principal type:// On Kubernetes versions 1.29+ with the `ServiceAccountTokenPodNodeInfo` flag enabled, // Kubernetes injects a node name into the Service Account token, which gets propagated // into the user's info extra map. We transform the map into a set of key/value // records with key of string and value as a set of strings. // // This allows a service account to impersonate only the node included in the SA token's // node claim, which practicly translates to "only impersonate the node a pod is running on" permit ( principal is k8s::ServiceAccount, action == k8s::Action::"impersonate", resource is k8s::Node ) when { principal.name == "default" && principal.namespace == "default" && principal has extra && principal.extra.contains({ "key": "authentication.kubernetes.io/node-name", "values": [resource.name]}) };
To see a generated schema with all admission entities and actions, you can view k8s-full.cedarschema.
This package also contains a webhook that can evaluate Kubernetes requests in the Admission validation stage, evaluating the full request object.
Unlike authorization which is deny by default, Cedar Admission policies are allow by default, so only forbid
policies have any effect on admission.
The admission webhook automatically injects a the following Cedar policy for every request, which applies to all admission requests.
permit (
principal,
action in [
k8s::admission::Action::"create",
k8s::admission::Action::"update",
k8s::admission::Action::"delete",
k8s::admission::Action::"connect"],
resource
);
Principals in the admission webhook are identical to the entities used in Authorization, including ServiceAccounts and Group membership.
Validating Admission requests are only made for mutating requests, so only create, update, delete, and connect are admissible.
Admission actions exist in a different Cedar namespace than Authorization, and are prefixed with k8s::admission::Action
.
// largely redundant policy, as authorization already only allows non-mutating verbs on resources
forbid (
principal in k8s::Group::"system:viewers",
action in [
k8s::admission::Action::"create",
k8s::admission::Action::"update",
k8s::admission::Action::"delete"],
resource
);
Most admission actions currently apply to any Kubernetes type that have a metav1.ObjectMeta
.
Resources for Admission policies are derived from the Kubernetes API Group and version. The resource entity structure matches that of the Kubernetes API structure, with some special cases.
// Forbid pods with hostNetwork in namespaces other than kube-system
forbid (
principal,
action in [
k8s::admission::Action::"create", k8s::admission::Action::"update"],
resource is core::v1::Pod
) when {
resource has spec &&
resource.spec has hostNetwork &&
resource.spec.hostNetwork == true
} unless {
resource has metadata &&
resource.metadata has namespace &&
resource.metadata.namespace == "kube-system"
};
Until cedar-go supports entity maps, we've manually added KeyValue
and KeyValueStringSlice
types into the meta::v1
namespace to support key/value labels.
Any Kubernetes types that consist of map[string]string{}
or map[string][]string{}
are converted to a Set of KeyValue or KeyValueStringSlice.
namespace meta::v1 {
type KeyValue = {
"key": __cedar::String,
"value"?: __cedar::String
};
type KeyValueStringSlice = {
"key": __cedar::String,
"value"?: Set < __cedar::String >
};
// ...
entity ObjectMeta = {
"annotations"?: Set < meta::v1::KeyValue >,
"labels"?: Set < meta::v1::KeyValue >,
// ...
"name"?: __cedar::String,
"namespace"?: __cedar::String,
// ...
};
// ...
}
The Kubernetes CONNECT
admission action only applies to a small set of structures that don't appear in the Kubernetes OpenAPI Schema, so we inject them manually:
namespace core::v1 {
// other types and entities
entity NodeProxyOptions = {
"apiVersion": __cedar::String,
"kind": __cedar::String,
"path": __cedar::String
};
entity PodAttachOptions = {
"apiVersion": __cedar::String,
"command": Set < __cedar::String >,
"container": __cedar::String,
"kind": __cedar::String,
"stderr": __cedar::Bool,
"stdin": __cedar::Bool,
"stdout": __cedar::Bool,
"tty": __cedar::Bool
};
entity PodExecOptions = {
"apiVersion": __cedar::String,
"command": Set < __cedar::String >,
"container": __cedar::String,
"kind": __cedar::String,
"stderr": __cedar::Bool,
"stdin": __cedar::Bool,
"stdout": __cedar::Bool,
"tty": __cedar::Bool
};
entity PodPortForwardOptions = {
"apiVersion": __cedar::String,
"kind": __cedar::String,
"ports"?: Set < __cedar::String >
};
entity PodProxyOptions = {
"apiVersion": __cedar::String,
"kind": __cedar::String,
"path": __cedar::String
};
entity ServiceProxyOptions = {
"apiVersion": __cedar::String,
"kind": __cedar::String,
"path": __cedar::String
};
}
Policy can be used to forbid proxying to those types:
// deny policy on exec unless command is `whoami`
forbid (
principal,
action == k8s::admission::Action::"connect",
resource is core::v1::PodExecOptions
) unless {
resource.command = ["whoami"]
};