标签 数字证书 下的文章

解决登录Harbor Registry时鉴权失败的问题

今天在测试之前搭建好的高可用Harbor时,发现了一个问题:使用docker login harbor时,有时成功,有时失败:

# docker login -u user -p passwd http://hub.my-domain.com:36666
Login Succeeded

# docker login -u user -p passwd http://hub.my-domain.com:36666
Error response from daemon: login attempt to http://hub.my-domain.com:36666/v2/ failed with status: 401 Unauthorized

我们在DNS中将hub.my-domain.com这个域名解析成两个IP,分别是两个Harbor节点的public IP,这可能是问题的诱发原因,但我还不知道问题根源在哪里。以下是问题的查找过程记录。

1、保证每个Harbor node都是可以login ok的

我在client端通过修改/etc/hosts将hub.my-domain.com分别解析成上述说到的两个node IP并测试。测试结果表明:无论单独解析成哪个IP,docker login http://hub.my-domain.com:36666都会100%的成功。

2、查看两个Harbor node上的registry log,弄清问题现象

将/etc/hosts中hub.my-domain.com的硬解析删除,恢复DNS解析。打开两个terminal tab分别监视连个Harbor node上的registry的日志。经过几次测试,发现一个现象:当docker login成功时,都是一个node上的日志出现更新;而当docker login fail时,我们会看到两个Node上的registry日志都有变化,似乎请求发给了两个node

node1:
Jun 15 14:40:01 172.19.0.1 registry[30242]: time="2017-06-15T06:40:01.245822446Z" level=debug msg="authorizing request" go.version=go1.7.3 http.request.host="hub.my-domain.com:36666" http.request.id=62add46e-e176-4eb8-b36a-84a9fbe7ac9c http.request.method=GET http.request.remoteaddr=xx.xx.xx.xx http.request.uri="/v2/" http.request.useragent="docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\(linux\\))" instance.id=43380207-7b61-4d45-b06a-a017c9a075af service=registry version="v2.4.1+unknown"

Jun 15 14:40:01 172.19.0.1 registry[30242]: time="2017-06-15T06:40:01.246002519Z" level=error msg="token signed by untrusted key with ID: \"BASH:RNPJ:PEBU:7THG:2NAR:OSFV:CG6U:ANV4:CCNB:ODZR:4BL6:TMD6\""

node2:

Jun 15 14:40:01 172.18.0.1 registry[28674]: time="2017-06-15T06:40:01.213604228Z" level=debug msg="authorizing request" go.version=go1.7.3 http.request.host="hub.my-domain.com:36666" http.request.id=bb6eeb8f-99f1-47a0-8cae-dae9b402b758 http.request.method=GET http.request.remoteaddr=xx.xx.xx.xx http.request.uri="/v2/" http.request.useragent="docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\(linux\\))" instance.id=2a364e0c-425f-47a9-b144-887d439243ba service=registry version="v2.4.1+unknown"

Jun 15 14:40:01 172.18.0.1 registry[28674]: time="2017-06-15T06:40:01.21374491Z" level=warning msg="error authorizing context: authorization token required" go.version=go1.7.3 http.request.host="hub.my-domain.com:36666" http.request.id=bb6eeb8f-99f1-47a0-8cae-dae9b402b758 http.request.method=GET http.request.remoteaddr=xx.xx.xx.xx http.request.uri="/v2/" http.request.useragent="docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\(linux\\))" instance.id=2a364e0c-425f-47a9-b144-887d439243ba service=registry version="v2.4.1+unknown"

Jun 15 14:40:01 172.18.0.1 registry[28674]: 172.18.0.3 - - [15/Jun/2017:06:40:01 +0000] "GET /v2/ HTTP/1.1" 401 87 "" "docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\(linux\\))"

3、探寻Harbor原理,弄清问题根源

打开harbor在github.com的wiki页,在”Architecture Overview of Harbor“中我找到了docker login的流程:

img{512x368}

从图片上,我一眼就看到了从docker client发出的*”两个请求: a和c流程”,看来docker client的确不止一次向Harbor发起了请求。wiki上对docker login流程给了简明扼要的解释。大致的流程是:

  • docker向registry发起请求,由于registry是基于token auth的,因此registry回复应答,告诉docker client去哪个URL去获取token;
  • docker client根据应答中的URL向token service(ui)发起请求,通过user和passwd获取token;如果user和passwd在db中通过了验证,那么token service将用自己的私钥(harbor/common/config/ui/private_key.pem)生成一个token,返回给docker client端;
  • docker client获得token后再向registry发起login请求,registry用自己的证书(harbor/common/config/registry/root.crt)对token进行校验。通过则返回成功,否则返回失败。

从这个原理,我们可以知道问题就出在docker client多次向Harbor发起请求这个环节:对于每次请求,DNS会将域名可能解析为不同IP,因此不同请求可能落到不同的node上。这样当docker client拿着node1上token service分配的token去到node2的registry上鉴权时,就会出现鉴权失败的情况。

4、统一私钥和证书,问题得以解决

token service的私钥(harbor/common/config/ui/private_key.pem)和registry的证书(harbor/common/config/registry/root.crt)都是在prepare时生成的,两个节点都独立prepare过,因此两个node上的private_key.pem和root.crt是不同的,这就是问题根源。

解决这个问题很简单,就是统一私钥和证书。比如:将node1上的private_key.pem和root.crt复制到node2上,并重新创建node2上的container:

// node2上

将node1上的harbor/common/config/ui/private_key.pem复制到node2上的harbor/common/config/ui/private_key.pem;
将node1上的harbor/common/config/registry/root.crt复制到harbor/common/config/registry/root.crt;

$ docker-compose down -v
$ docker-compose up -d

更换了private_key.pem和root.crt的node2上的Harbor启动后,再进行login测试,就会100%成功了!

# docker login -u admin -p passwd http://hub.my-domain.com:36666
Login Succeeded

微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

在Kubernetes Pod中使用Service Account访问API Server

Kubernetes API Server是整个Kubernetes集群的核心,我们不仅有从集群外部访问API Server的需求,有时,我们还需要从Pod的内部访问API Server。

然而,在生产环境中,Kubernetes API Server都是“设防”的。在《Kubernetes集群的安全配置》一文中,我提到过:Kubernetes通过client cert、static token、basic auth等方法对客户端请求进行身份验证。对于运行于Pod中的Process而言,有些时候这些方法是适合的,但有些时候,像client cert、static token或basic auth这些信息是不便于暴露给Pod中的Process的。并且通过这些方法通过API Server验证后的请求是具有全部授权的,可以任意操作Kubernetes cluster,这显然是不能满足安全要求的。为此,Kubernetes更推荐大家使用service account这种方案的。本文就带大家详细说说如何通过service account从一个Pod中访问API Server的。

零、试验环境

本文的试验环境是Kubernetes 1.3.7 cluster,双节点,master承载负荷。cluster通过kube-up.sh搭建的,具体的搭建方法见《一篇文章带你了解Kubernetes安装》。

一、什么是service account?

什么是service account? 顾名思义,相对于user account(比如:kubectl访问APIServer时用的就是user account),service account就是Pod中的Process用于访问Kubernetes API的account,它为Pod中的Process提供了一种身份标识。相比于user account的全局性权限,service account更适合一些轻量级的task,更聚焦于授权给某些特定Pod中的Process所使用。

service account作为一种resource存在于Kubernetes cluster中,我们可以通过kubectl获取当前cluster中的service acount列表:

# kubectl get serviceaccount --all-namespaces
NAMESPACE                    NAME           SECRETS   AGE
default                      default        1         140d
kube-system                  default        1         140d

我们查看一下kube-system namespace下名为”default”的service account的详细信息:

# kubectl describe serviceaccount/default -n kube-system
Name:        default
Namespace:    kube-system
Labels:        <none>

Image pull secrets:    <none>

Mountable secrets:     default-token-hpni0

Tokens:                default-token-hpni0

我们看到service account并不复杂,只是关联了一个secret资源作为token,该token也叫service-account-token,该token才是真正在API Server验证(authentication)环节起作用的:

# kubectl get secret  -n kube-system
NAME                  TYPE                                  DATA      AGE
default-token-hpni0   kubernetes.io/service-account-token   3         140d

# kubectl get secret default-token-hpni0 -o yaml -n kube-system
apiVersion: v1
data:
  ca.crt: {base64 encoding of ca.crt data}
  namespace: a3ViZS1zeXN0ZW0=
  token: {base64 encoding of bearer token}

kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
    kubernetes.io/service-account.uid: 90ded7ff-9120-11e6-a0a6-00163e1625a9
  creationTimestamp: 2016-10-13T08:39:33Z
  name: default-token-hpni0
  namespace: kube-system
  resourceVersion: "2864"
  selfLink: /api/v1/namespaces/kube-system/secrets/default-token-hpni0
  uid: 90e71909-9120-11e6-a0a6-00163e1625a9
type: kubernetes.io/service-account-token

我们看到这个类型为service-account-token的secret资源包含的数据有三部分:ca.crt、namespace和token。

  • ca.crt
    这个是API Server的CA公钥证书,用于Pod中的Process对API Server的服务端数字证书进行校验时使用的;

  • namespace
    这个就是Secret所在namespace的值的base64编码:# echo -n “kube-system”|base64 => “a3ViZS1zeXN0ZW0=”

  • token

这是一段用API Server私钥签发(sign)的bearer tokens的base64编码,在API Server authenticating环节,它将派上用场。

二、API Server的service account authentication(身份验证)

前面说过,service account为Pod中的Process提供了一种身份标识,在Kubernetes的身份校验(authenticating)环节,以某个service account提供身份的Pod的用户名为:

system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT)

以上面那个kube-system namespace下的“default” service account为例,使用它的Pod的username全称为:

system:serviceaccount:kube-system:default

有了username,那么credentials呢?就是上面提到的service-account-token中的token。在《Kubernetes集群的安全配置》一文中我们谈到过,API Server的authenticating环节支持多种身份校验方式:client cert、bearer token、static password auth等,这些方式中有一种方式通过authenticating(Kubernetes API Server会逐个方式尝试),那么身份校验就会通过。一旦API Server发现client发起的request使用的是service account token的方式,API Server就会自动采用signed bearer token方式进行身份校验。而request就会使用携带的service account token参与验证。该token是API Server在创建service account时用API server启动参数:–service-account-key-file的值签署(sign)生成的。如果–service-account-key-file未传入任何值,那么将默认使用–tls-private-key-file的值,即API Server的私钥(server.key)。

通过authenticating后,API Server将根据Pod username所在的group:system:serviceaccounts和system:serviceaccounts:(NAMESPACE)的权限对其进行authorityadmission control两个环节的处理。在这两个环节中,cluster管理员可以对service account的权限进行细化设置。

三、默认的service account

Kubernetes会为每个cluster中的namespace自动创建一个默认的service account资源,并命名为”default”:

# kubectl get serviceaccount --all-namespaces
NAMESPACE                    NAME           SECRETS   AGE
default                      default        1         140d
kube-system                  default        1         140d

如果Pod中没有显式指定spec.serviceAccount字段值,那么Kubernetes会将该namespace下的”default” service account自动mount到在这个namespace中创建的Pod里。我们以namespace “default”为例,我们查看一下其中的一个Pod的信息:

# kubectl describe pod/index-api-2822468404-4oofr
Name:        index-api-2822468404-4oofr
Namespace:    default
... ...

Containers:
  index-api:
   ... ...
    Volume Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-40z0x (ro)
    Environment Variables:    <none>
... ...
Volumes:
... ...
  default-token-40z0x:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-40z0x

QoS Class:    BestEffort
Tolerations:    <none>
No events.

可以看到,kubernetes将default namespace中的service account “default”的service account token挂载(mount)到了Pod中容器的/var/run/secrets/kubernetes.io/serviceaccount路径下。

深入容器内部,查看mount的serviceaccount路径下的结构:

# docker exec 3d11ee06e0f8 ls  /var/run/secrets/kubernetes.io/serviceaccount
ca.crt
namespace
token

这三个文件与上面提到的service account的token中的数据是一一对应的。

四、default service account doesn’t work

上面提到过,每个Pod都会被自动挂载一个其所在namespace的default service account,该service account用于该Pod中的Process访问API Server时使用。Pod中的Process该怎么用这个service account呢?Kubernetes官方提供了一个client-go项目可以为你演示如何使用service account访问API Server。这里我们就基于client-go项目中的examples/in-cluster/main.go来测试一下是否能成功访问API Server。

先下载client-go源码:

# go get k8s.io/client-go

# ls -F
CHANGELOG.md  dynamic/   Godeps/     INSTALL.md   LICENSE   OWNERS  plugin/    rest/     third_party/  transport/  vendor/
discovery/    examples/  informers/  kubernetes/  listers/  pkg/    README.md  testing/  tools/        util/

我们改造一下examples/in-cluster/main.go,考虑到panic会导致不便于观察Pod日志,我们将panic改为输出到“标准输出”,并且不return,让Pod周期性的输出相关日志,即便fail:

// k8s.io/client-go/examples/in-cluster/main.go
... ...
func main() {
    // creates the in-cluster config
    config, err := rest.InClusterConfig()
    if err != nil {
        fmt.Println(err)
    }
    // creates the clientset
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        fmt.Println(err)
    }
    for {
        pods, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{})
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))
        }
        time.Sleep(10 * time.Second)
    }
}

基于该main.go的go build默认输出,创建一个简单的Dockerfile:

From ubuntu:14.04
MAINTAINER Tony Bai <bigwhite.cn@gmail.com>

COPY main /root/main
RUN chmod +x /root/main
WORKDIR /root
ENTRYPOINT ["/root/main"]

构建一个测试用docker image:

# docker build -t k8s/example1:latest .
... ...

# docker images|grep k8s
k8s/example1                                                  latest              ceb3efdb2f91        14 hours ago        264.4 MB

创建一份deployment manifest:

//main.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-example1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: k8s-example1
    spec:
      containers:
      - name: k8s-example1
        image: k8s/example1:latest
        imagePullPolicy: IfNotPresent

我们来创建该deployment(kubectl create -f main.yaml -n kube-system),观察Pod中的main程序能否成功访问到API Server:

# kubectl logs k8s-example1-1569038391-jfxhx
the server has asked for the client to provide credentials (get pods)
the server has asked for the client to provide credentials (get pods)

API Server log(/var/log/upstart/kube-apiserver.log):

E0302 15:45:40.944496   12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error
E0302 15:45:50.946598   12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error
E0302 15:46:00.948398   12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error

出错了!kube-system namespace下的”default” service account似乎不好用啊!(注意:这是在kubernetes 1.3.7环境)。

五、创建一个新的自用的service account

在kubernetes github issues中,有好多issue是关于”default” service account不好用的问题,给出的解决方法似乎都是创建一个新的service account。

service account的创建非常简单,我们创建一个serviceaccount.yaml:

//serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: k8s-example1

创建该service account:

# kubectl create -f serviceaccount.yaml
serviceaccount "k8s-example1" created

# kubectl get serviceaccount
NAME           SECRETS   AGE
default        1         139d
k8s-example1   1         12s

修改main.yaml,让Pod显示使用这个新的service account:

//main.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-example1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: k8s-example1
    spec:
      serviceAccount: k8s-example1
      containers:
      - name: k8s-example1
        image: k8s/example1:latest
        imagePullPolicy: IfNotPresent

好了,我们重新创建该deployment,查看Pod日志:

# kubectl logs k8s-example1-456041623-rqj87
There are 14 pods in the cluster
There are 14 pods in the cluster
... ...

我们看到main程序使用新的service account成功通过了API Server的身份验证环节,并获得了cluster的相关信息。

六、尾声

在我的另外一个使用kubeadm安装的k8s 1.5.1环境中,我重复做了上面这个简单测试,不同的是这次我直接使用了default service account。在k8s 1.5.1下,pod的执行结果是ok的,也就是说通过default serviceaccount,我们的client-go in-cluster example程序可以顺利通过API Server的身份验证,获取到相关的Pods元信息。

七、参考资料


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats