service account是k8s为pod内部的进程访问apiserver创建的一种用户。其实在pod外部也可以通过sa的token和证书访问apiserver,不过在pod外部一般都是采用client 证书的方式。
创建一个namespace,就会自动生成名字为 default 的 service account。
root@master:~# kubectl create ns test
namespace/test created
root@master:~# kubectl get sa -n test
NAME SECRETS AGE
default 1 6s
当然我们也可以再创建额外的sa。
root@master:~# kubectl create sa sa1 -n test
serviceaccount/sa1 created
root@master:~# kubectl get sa -n test
NAME SECRETS AGE
default 1 94s
sa1 1 2s
有了sa后,我们就可以使用sa的token和apiserver交互了,由于所有通信都通过TLS进行,所以也得需要证书(ca.crt,这里的证书指的是server端的ca证书)或者允许不安全的连接(--insecure)。
token和证书如何获取的?每个sa都会自动关联一个secret,token和证书就存在secret中。在pod内部他们被放在如下文件中(所有pod内部的ca.crt证书都一样,都是/etc/kubernetes/pki/ca.crt)
/var/run/secrets/kubernetes.io/serviceaccount/token
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
在外部可以通过secret获取。下面分别实验这两种方式下如何访问apiserver。
root@master:~# kubectl describe sa sa1 -n test
Name: sa1
Namespace: test
Labels: <none>
Annotations: <none>
Image pull secrets: <none>
Mountable secrets: sa1-token-p5wxt
Tokens: sa1-token-p5wxt
Events: <none>
外部访问apiserver
下面验证在外部如何通过sa的token和证书访问apiserver。
首先获取sa的token,cert和apiserver endpoint。
SERVICE_ACCOUNT=sa1
# Get the ServiceAccount's token Secret's name
SECRET=$(kubectl get serviceaccount -n test ${SERVICE_ACCOUNT} -o json | jq -Mr '.secrets[].name | select(contains("token"))')
# Extract the Bearer token from the Secret and decode
TOKEN=$(kubectl get secret -n test ${SECRET} -o json | jq -Mr '.data.token' | base64 -d)
# Extract, decode and write the ca.crt to a temporary location
kubectl get secret -n test ${SECRET} -o json | jq -Mr '.data["ca.crt"]' | base64 -d > /tmp/ca.crt
# Get the API Server location
APISERVER=$(kubectl config view --minify | grep server | cut -f 2- -d ":" | tr -d " ")
使用curl命令,指定token和insecure(表示不对server端证书进行认证),开始和apiserver交互。
root@master:~# curl --header "Authorization: Bearer $TOKEN" --insecure -s $APISERVER/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.122.20:6443"
}
]
}root@master:~#
也可以通过 --cacert /tmp/ca.crt 指定证书。
root@master:~# curl --header "Authorization: Bearer $TOKEN" --cacert /tmp/ca.crt -s $APISERVER/api/
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.122.20:6443"
}
]
}root@master:~#
pod内部访问apiserver
首先创建一个包含curl命令的pod,虽然没有指定sa,但是会自动将test namespace下的default的sa分配给这个pod。
root@master:~# cat <<EOF | kubectl create -f -
> apiVersion: v1
> kind: Pod
> metadata:
> name: test
> namespace: test
>
> spec:
> containers:
> - name: samplepod
> command: ["/bin/sh", "-c", "sleep 99999"]
> image: byrnedo/alpine-curl
> EOF
pod/test created
进入pod内部,获取token,crt。注意的是在pod内部是通过下面的两个环境变量获取apiserver的endpoint的,这里的endpoint是service ip和port,即10.96.0.10:443,而在pod外部使用的endpoint是192.168.122.20:6443.
KUBERNETES_SERVICE_HOST
KUBERNETES_PORT_443_TCP_PORT
root@master:~# kubectl exec -it -n test test sh
获取token和证书
/ # TOKEN=`cat /var/run/secrets/kubernetes.io/serviceaccount/token`
/ # APISERVER="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_PORT_443_TCP_PORT"
不使用证书访问
/ # curl --header "Authorization: Bearer $TOKEN" --insecure -s $APISERVER/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.122.20:6443"
}
]
}/ #
使用证书访问
/ # CAPATH="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
/ # curl --header "Authorization: Bearer $TOKEN" --cacert $CAPATH -s $APISERVER/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.122.20:6443"
}
]
}/ #
sa的默认权利
当curl请求通过apiserver的认证后,会被分配一个user - system:serviceaccount:test:sa1,和一个group - system:serviceaccounts:test:sa1,同时也会被分配另一个group system:authenticated代表这是一个通过认证的请求。
前面的user和group目前是没有关联任何role或者clusterrole的,这意味着他们是没有任何权利去查看或者修改k8s内部资源的。而system:authenticated是系统自动创建的group,并且已经被默认关联到了下面的三个clusterrole,他们是有查看资源的权利,但是很受限
system:public-info-viewer
system:discovery
system:basic-user
通过下面的clusterrolebinding可看到,上面的三个clusterrole确实绑定到group system:authenticated了。
root@master:~# kubectl describe clusterrolebinding system:public-info-viewer
Name: system:public-info-viewer
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
Role:
Kind: ClusterRole
Name: system:public-info-viewer
Subjects:
Kind Name Namespace
---- ---- ---------
Group system:authenticated
Group system:unauthenticated
root@master:~# kubectl describe clusterrolebinding system:discovery
Name: system:discovery
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
Role:
Kind: ClusterRole
Name: system:discovery
Subjects:
Kind Name Namespace
---- ---- ---------
Group system:authenticated
root@master:~# kubectl describe clusterrolebinding system:basic-user
Name: system:basic-user
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
Role:
Kind: ClusterRole
Name: system:basic-user
Subjects:
Kind Name Namespace
---- ---- ---------
Group system:authenticated
通过下面的命令查看这三个clusterrole都有什么权利,可以看到权利是比较低的,只能查看Non-Resource URLs,不能查看pod,namespace,deployment等资源信息。
root@master:~# kubectl describe clusterrole system:public-info-viewer
Name: system:public-info-viewer
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
[/healthz] [] [get]
[/livez] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version] [] [get]
root@master:~# kubectl describe clusterrole system:discovery
Name: system:discovery
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version] [] [get]
root@master:~# kubectl describe clusterrole system:basic-user
Name: system:basic-user
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
尝试获取pod信息,但是被Forbidden,因为没有被授权。
root@master:~# curl --header "Authorization: Bearer $TOKEN" --insecure -s $APISERVER/api/v1/namespaces/test
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "namespaces \"test\" is forbidden: User \"system:serviceaccount:test:sa1\" cannot get resource \"namespaces\" in API group \"\" in the namespace \"test\"",
"reason": "Forbidden",
"details": {
"name": "test",
"kind": "namespaces"
},
"code": 403
提高sa权利
如何提高sa的权利呢?
a. 修改默认的这三个clusterrole,但是这是公共的,不建议修改。
b. 将sa绑定的其他权利比较高的clusterrole,比如cluster-admin。
c. 新创建一个role或者clusterrole,指定好需要的权利,将sa绑定上即可。这是推荐的做法。
下面采用第三种方法进行验证。
在test namespace创建一个role read-pod,这个role的权利只可以获取namespace test下的pod。
root@master:~# cat <<EOF | kubectl create -f -
> apiVersion: rbac.authorization.k8s.io/v1
> kind: Role
> metadata:
> namespace: test
> name: read-pod
> rules:
> - apiGroups: [""]
> resources: ["pods"]
> verbs: ["get", "list"]
> EOF
role.rbac.authorization.k8s.io/read-pod created
root@master:~# kubectl create rolebinding test -n test --role read-pod --serviceaccount test:sa1
rolebinding.rbac.authorization.k8s.io/test created
验证一下,可以获取test namespace下的pod
root@master:~# curl --header "Authorization: Bearer $TOKEN" --cacert /tmp/ca.crt -s $APISERVER/api/v1/namespaces/test/pods/test
{
"kind": "Pod",
"apiVersion": "v1",
"metadata": {
"name": "test",
"namespace": "test",
"selfLink": "/api/v1/namespaces/test/pods/test",
"uid": "12ef72cf-be59-4329-8e67-2f3c805a553f",
"resourceVersion": "13801401",
"creationTimestamp": "2020-08-22T22:50:22Z",
"annotations": {
"cni.projectcalico.org/podIP": "10.24.166.144/32",
"cni.projectcalico.org/podIPs": "10.24.166.144/32",
"k8s.v1.cni.cncf.io/network-status": "[{\n \"name\": \"k8s-pod-network\",\n \"ips\": [\n \"10.24.166.144\"\n ],\n \"default\": true,\n \"dns\": {}\n}]",
"k8s.v1.cni.cncf.io/networks-status": "[{\n \"name\": \"k8s-pod-network\",\n \"ips\": [\n \"10.24.166.144\"\n ],\n \"default\": true,\n \"dns\": {}\n}]"
}
},
...
但是pod的子资源是不能获取的,比如获取pods/logs,因为role里只指定了pod资源。如果想获取子资源,还得单独指定。
root@master:~# curl --header "Authorization: Bearer $TOKEN" --cacert /tmp/ca.crt -s $APISERVER/api/v1/namespaces/test/pods/test/logs
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "pods \"test\" is forbidden: User \"system:serviceaccount:test:sa1\" cannot get resource \"pods/logs\" in API group \"\" in the namespace \"test\"",
"reason": "Forbidden",
"details": {
"name": "test",
"kind": "pods"
},
"code": 403
Non-resource requests 和 resource requests
下面一段话是官网对这两个概念的解释,但是还是不太明白什么意思。
Non-resource requests Requests to endpoints other than /api/v1/... or /apis/<group>/<version>/... are considered "non-resource requests", and use the lower-cased HTTP method of the request as the verb. For example, a GET request to endpoints like /api or /healthz would use get as the verb.
Resource requests To determine the request verb for a resource API endpoint, review the HTTP verb used and whether or not the request acts on an individual resource or a collection of resources:
而且查看clusterrole时,也把这两种请求区分开来,如下
root@master:~# kubectl describe clusterrole system:discovery
Name: system:discovery
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version] [] [get]
所以看了下源码,如果请求的url path满足下面的三个条件之一的话就是Non-Resource request,否则就是 resource request。
a. 请求url path字段小于3,比如
/livez(一个字段),/api(一个字段), /api/v1(两个字段)等
b. 如果请求url path大于等于3了,但是url path不是以 api 或者 apis开始。比如 /livez/poststarthook/crd-informer-synced
c. url path以/apis开始的,但是后面的字段小于3,比如
/apis/{api-group}或者 /apis/{api-group}/{version}
代码路径
./staging/src/k8s.io/apiserver/pkg/endpoints/request/requestinfo.go
如下结构体用于保存解析http请求的内容
// RequestInfo holds information parsed from the http.Request
type RequestInfo struct {
// IsResourceRequest indicates whether or not the request is for an API resource or subresource
IsResourceRequest bool
// Path is the URL path of the request
Path string
// Verb is the kube verb associated with the request for API requests, not the http verb. This includes things like list and watch.
// for non-resource requests, this is the lowercase http verb
Verb string
APIPrefix string
APIGroup string
APIVersion string
Namespace string
// Resource is the name of the resource being requested. This is not the kind. For example: pods
Resource string
// Subresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
Subresource string
// Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
Name string
// Parts are the path parts for the request, always starting with /{resource}/{name}
Parts []string
}
func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, error) {
// start with a non-resource request until proven otherwise
requestInfo := RequestInfo{
IsResourceRequest: false,
Path: req.URL.Path,
Verb: strings.ToLower(req.Method),
}
//如果请求的字段小于3,则认为是 no-resource 请求,比如 /healthz,/readyz
currentParts := splitPath(req.URL.Path)
if len(currentParts) < 3 {
// return a non-resource request
return &requestInfo, nil
}
//不是以 api 或者 apis开始的都认为是 no-resource 请求,
if !r.APIPrefixes.Has(currentParts[0]) {
// return a non-resource request
return &requestInfo, nil
}
requestInfo.APIPrefix = currentParts[0]
currentParts = currentParts[1:]
//走到这里说明url path开始是api或者是apis。
//下面的判断是如果不是api开始的,就是说以apis开始的请求。
if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) {
//apis开始的请求,如果后面的字段小于3,也表示 non-resource 请求,比如 /apis/apiregistration.k8s.io/v1
// one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
if len(currentParts) < 3 {
// return a non-resource request
return &requestInfo, nil
}
requestInfo.APIGroup = currentParts[0]
currentParts = currentParts[1:]
}
requestInfo.IsResourceRequest = true
requestInfo.APIVersion = currentParts[0]
currentParts = currentParts[1:]
// handle input of form /{specialVerb}/*
if specialVerbs.Has(currentParts[0]) {
if len(currentParts) < 2 {
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
}
requestInfo.Verb = currentParts[0]
currentParts = currentParts[1:]
} else {
switch req.Method {
case "POST":
requestInfo.Verb = "create"
case "GET", "HEAD":
requestInfo.Verb = "get"
case "PUT":
requestInfo.Verb = "update"
case "PATCH":
requestInfo.Verb = "patch"
case "DELETE":
requestInfo.Verb = "delete"
default:
requestInfo.Verb = ""
}
}
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind
if currentParts[0] == "namespaces" {
if len(currentParts) > 1 {
requestInfo.Namespace = currentParts[1]
// if there is another step after the namespace name and it is not a known namespace subresource
// move currentParts to include it as a resource in its own right
if len(currentParts) > 2 && !namespaceSubresources.Has(currentParts[2]) {
currentParts = currentParts[2:]
}
}
} else {
requestInfo.Namespace = metav1.NamespaceNone
}
// parsing successful, so we now know the proper value for .Parts
requestInfo.Parts = currentParts
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
switch {
case len(requestInfo.Parts) >= 3 && !specialVerbsNoSubresources.Has(requestInfo.Verb):
requestInfo.Subresource = requestInfo.Parts[2]
fallthrough
case len(requestInfo.Parts) >= 2:
requestInfo.Name = requestInfo.Parts[1]
fallthrough
case len(requestInfo.Parts) >= 1:
requestInfo.Resource = requestInfo.Parts[0]
}
// if there's no name on the request and we thought it was a get before, then the actual verb is a list or a watch
if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
opts := metainternalversion.ListOptions{}
if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, &opts); err != nil {
// An error in parsing request will result in default to "list" and not setting "name" field.
klog.Errorf("Couldn't parse request %#v: %v", req.URL.Query(), err)
// Reset opts to not rely on partial results from parsing.
// However, if watch is set, let's report it.
opts = metainternalversion.ListOptions{}
if values := req.URL.Query()["watch"]; len(values) > 0 {
switch strings.ToLower(values[0]) {
case "false", "0":
default:
opts.Watch = true
}
}
}
if opts.Watch {
requestInfo.Verb = "watch"
} else {
requestInfo.Verb = "list"
}
if opts.FieldSelector != nil {
if name, ok := opts.FieldSelector.RequiresExactMatch("metadata.name"); ok {
if len(path.IsValidPathSegmentName(name)) == 0 {
requestInfo.Name = name
}
}
}
}
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
requestInfo.Verb = "deletecollection"
}
return &requestInfo, nil
}
从上述源码也能看出 http verb如何转换成 request verb
POST -> create
GET/HEAD with resourceName -> get
GET/HEAD without resourceName -> list(如果没有指定资源名字,则列出所有的资源,比如指定了获取pod1 /api/v1/namespaces/test/pods/pod1,则只获取pod1的信息,如果不指定pod名字,就会返回test namespace下的所有pod)
PUT-> update
PATCH->patch
DELETE with resourceName ->delete
DELETE without resourceName ->delete(同get,如果没有指定删除具体的资源,则删除所有的资源)
Non-resource requests只能在clusterrole中配置,而resource requests可以在role或者clusterrole中配置。
Non-resource requests 和resource requests的配置格式也不一样,如下
//resource requests
rules:
- apiGroups: [""]
#
# at the HTTP level, the name of the resource for accessing Node
# objects is "nodes"
resources: ["nodes"]
verbs: ["get", "list", "watch"]
//Non-resource requests
rules:
- nonResourceURLs: ["/healthz", "/healthz/*"] # '*' in a nonResourceURL is a suffix glob match
verbs: ["get", "post"]
参考
service account相关
https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/
授权相关
https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/
https://kubernetes.io/docs/reference/access-authn-authz/rbac/
https://kubernetes.io/docs/reference/access-authn-authz/authorization/
https://kubernetes.io/docs/reference/access-authn-authz/authentication/