暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

实战Kubernetes动态准入控制webhook

云原生CTO 2021-07-26
1306

点击上方 云原生CTO,选择 设为星标

               优质文章,每日送达


「【只做懂你de云原生干货知识共享】」

实战Kubernetes动态准入控制

为 Kubernetes 动态准入控制创建 webhook 的演练。

「https://github.com/FairwindsOps/polaris」

关于polaris这个项目来自Fairwinds它主要核心让您的k8s集群顺利运行,它运行各种检查以确保使用最佳实践配置帮助您避免k8s集群将来出现问题。

在花时间熟悉 Polaris 的过程中,我开始注意到它的一些局限性。为了克服这些限制之一,我决定创建一个 webhook 并在一篇文章中分享这个过程。本文中说明的代码可在此处下载。

「https://github.com/larkintuckerllc/hello-dynamic-admission-control」

什么是 Kubernetes 准入控制器?

首先,什么是 Kubernetes 准入控制器?简而言之,Kubernetes 准入控制器是管理和强制执行集群使用方式的插件。它们可以被认为是拦截(经过身份验证的)API 请求并可能更改请求对象或完全拒绝请求的守门人。— Kubernetes — Kubernetes 准入控制器指南

「https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/」

什么是动态准入控制呢?

在 Kubernetes 附带的 30 多个准入控制器中,有两个因其几乎无限的灵活性而扮演着特殊的角色——「ValidatingAdmissionWebhooks」「MutatingAdmissionWebhooks」…… 它们自己没有实现任何策略决策逻辑。相反,相应的操作是从集群内运行的服务的 REST 端点(Webhook)获取的。这种方法将准入控制器逻辑与 Kubernetes API 服务器分离,从而允许用户实现自定义逻辑,以便在 Kubernetes 集群中创建、更新或删除资源时执行。

k8s规范化细粒度的东西,你应该知道

在使用 Polaris 的过程中,我被 Kubernetes 的粗粒度授权机制RBAC所震撼。例如,对 pod 资源具有创建访问权限的用户可以根据需要创建 pod。同时,我们知道我们需要限制用户创建会带来安全风险的 pod。为了说明如何利用准入控制器 webhook 来建立自定义安全策略,让我们考虑一个解决 Kubernetes 缺点之一的示例:

它的许多默认值都经过优化以易于使用和减少摩擦,有时以牺牲安全性为代价。这些设置之一是默认情况下允许容器以 root 身份运行(并且,无需进一步配置且 Dockerfile 中没有 USER 指令,也将这样做)。尽管容器在一定程度上与底层主机隔离,但以 root 身份运行容器确实会增加部署的风险——作为许多安全最佳实践之一,应该避免。例如,最近暴露的 runC 漏洞(CVE-2019-5736)只有在容器以 root 身份运行时才能被利用。

虽然 Polaris 提供了一个带有一组强大安全检查的 webhook,甚至允许创建自定义检查,但它本质上仅限于验证 pod 和容器。限制其他资源呢?

这让我想到我需要学习如何创建自己的动态准入控制 webhook。

先决条件

如果你想跟着,你需要:

「一个 Kubernetes 集群」

「kubectl 安装在本地;为集群配置」

「本地安装的 Docker」

「本地安装的 Node.js」

「本地安装的openssl;通常由操作系统安装预先安装」

请注意:虽然我尝试使用 MicroK8s 和 Minikube 本地集群,但我最终使用 Amazon Elastic Kubernetes Service (EKS) 集群写了这篇文章,因为我无法可靠地让 Telepresence(在下一节中介绍)与它们中的任何一个一起工作.

Telepresence

在本地开发和调试服务,官方推荐的Telepresence「https://kubernetes.io/docs/tasks/debug-application-cluster/local-debugging/」

因为我们要快速迭代,所以我们安装并使用Telepresence在本地开发。

Kubernetes 的应用程序通常由多个独立的服务组成,每个服务都在自己的容器中运行。在远程 Kubernetes 集群上开发和调试这些服务可能很麻烦,需要您在正在运行的容器上获取shell 并在远程 shell 内运行您的工具。

Telepresence 是一种简化本地开发和调试服务过程的工具,同时将服务代理到远程 Kubernetes 集群。使用远程呈现允许您为本地服务使用自定义工具,例如调试器和 IDE,并为服务提供对 ConfigMap、秘密和在远程集群上运行的服务的完全访问权限。

— Kubernetes —在本地开发和调试服务 安装 Telepresence 后,我们首先在本地创建Express“Hello World”应用程序。我们创建一个 pod 和服务来交付我们的应用程序:

$ telepresence --new-deployment hdac --expose 3000

事实上,我们观察到了新的 pod 和service:

$ kubectl get all
NAME       READY   STATUS    RESTARTS   AGE
pod/hdac   1/1     Running   0          29s
NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/hdac         ClusterIP   10.100.38.46   <none>        3000/TCP   29s
service/kubernetes   ClusterIP   10.100.0.1     <none>        443/TCP    39m

在Telepresence命令提供的 shell 中,我们在本地运行我们的应用程序,从而将其交付到集群中。

$ node app.js

在另一个终端中,我们可以运行一个临时 pod 来访问集群内的服务;在 DNS 名称hdac.default.svc上可用:

$ kubectl run tmp --image=busybox --restart=Never -it --rm -- wget -O - -q -T 3 http://hdac.default.svc:3000
Hello World!pod "tmp" deleted

请注意:这是很多命令行选项

TLS 证书

我们接下来需要通过 HTTPS 为我们的应用程序提供服务。

由于 Webhook 必须通过 HTTPS 提供服务,因此我们需要为服务器提供适当的证书。这些证书可以是自签名的(而是:由自签名 CA 签名),但是我们需要 Kubernetes 在与 webhook 服务器通信时指示相应的 CA 证书。另外,证书的通用名(CN)必须与Kubernetes API服务器使用的服务器名相匹配,对于内部服务是「webhook-server.webhook-demo.svc」在我们的例子中。

有点棘手的是,创建自签名 TLS 证书与从自签名 CA 创建 TLS 证书不同;后者是我们需要做的。它描述了以下步骤背后的概念。在应用程序 ( app ) 文件夹中,我们首先创建 CA 密钥和证书:

$ openssl req \ 
  -new \ 
  -x509 \ 
  -nodes \ -days 
  365 \ 
  -subj '/CN=my-ca' \ 
  -keyout ca.key \ 
  -out ca.crt

我们接下来创建服务器密钥:

$ openssl genrsa \ 
  -out server.key 2048

我们创建一个证书签名请求:

$ openssl req \ 
  -new \   -key 
  server.key \ 
-subj '/CN=hdac.default.svc' \ 
  -out server.csr

我们创建服务器证书:

$ openssl x509 \ 
  -req \ 
  -in server.csr \   -CA 
  ca.crt \ 
-CAkey ca.key \ 
  -CAcreateserial \ -days 
  365 \ 
  -out server.crt

更新的应用程序代码,服务 HTTPS,此时是:

const express = require('express');
const fs = require('fs');
const https = require('https');

const app = express();
const port = 3000;

const options = { 
  ca: fs.readFileSync('ca.crt'), 
  cert: fs.readFileSync('server.crt'), 
  key: fs.readFileSync('server.key'), 
}; 

app.get('/', (req, res) => {
  res.send('Hello World!');
});

const server = https.createServer(options, app);

server.listen(port, () => {
  console.log(`Server running on port ${port}/`);
});

通过这些更改,我们将应用程序交付到集群中:

$ Telepresence --new-deployment hdac --expose 3000 
$ node app.js

和以前一样,我们可以运行一个临时 pod 来访问集群中 DNS 名称hdac.default.svc上可用的服务:

$ kubectl run tmp --image=busybox --restart=Never -it --rm -- wget -O - -q -T 3 https://hdac.default.svc:3000
wget: note: TLS certificate validation not implemented
Hello World!pod "tmp" deleted

请注意:如果此时想要进行 TLS 证书验证,则可以创建一个单独的 pod 来运行带有cacert选项的curl命令(传入我们之前创建的ca.crt文件)。

动态准入控制 Webhook 应用程序

在这里,我们将我们的应用程序调整为动态管理控制 webhook;即,接受请求并始终允许操作的简单方法。

Webhooks 被发送一个 POST 请求,内容类型为:application/json,在admission.k8s.io API 组中的一个AdmissionReview API 对象序列化为JSON 作为主体。Webhooks 以 200 HTTP 状态代码、Content-Type: application/json 和一个包含 AdmissionReview 对象的主体(与它们发送的版本相同)响应,响应节填充并序列化为 JSON。

根据上述文档,请求JSON包含以下结构:

{
  "apiVersion""admission.k8s.io/v1",
  "kind""AdmissionReview",
  "request": {
    "uid""<unique identifier>",
    ...
  }
  ...
}

响应JSON最低限度具有以下结构;allow属性的值指示是否允许该操作。

{
  "apiVersion""admission.k8s.io/v1",
  "kind""AdmissionReview",
  "response": {
    "uid""<value from request.uid>",
    "allowed"true
  }
}

这是我遗漏的最后一个细节,让我头疼了好几个小时。Kubernetes 不允许在 webhook 配置中指定端口;它始终假定 HTTPS 端口为 443。

考虑到所有这些,我们更新应用程序代码如下:

const bodyParser = require('body-parser');
const express = require('express');
const fs = require('fs');
const https = require('https');

const app = express();
app.use(bodyParser.json());
const port = 443;

const options = { 
  ca: fs.readFileSync('ca.crt'), 
  cert: fs.readFileSync('server.crt'), 
  key: fs.readFileSync('server.key'), 
}; 

app.post('/', (req, res) => {
  if (
    req.body.request === undefined ||
    req.body.request.uid === undefined
  ) {
    res.status(400).send();
    return;
  }
  console.log(req.body); // DEBUGGING
  const { request: { uid } } = req.body;
  res.send({
    apiVersion: 'admission.k8s.io/v1',
    kind: 'AdmissionReview',
    response: {
      uid,
      allowed: true,
    },
  });
});

const server = https.createServer(options, app);

server.listen(port, () => {
  console.log(`Server running on port ${port}/`);
});

观察要点:

「这里我们使用body-parser库(这个必须安装)来提取JSON post数据」

「我们将应用程序更改为在端口443上运行 我们将方法从get改为post」

「在post方法中,我们对请求 JSON 进行基本验证并返回响应以允许操作」

「我们将更新的应用程序(由于使用特权端口而必须使用sudo)交付到集群中:」

$ Telepresence --new-deployment hdac --expose 443 
$ sudo node app.js

我们可以运行一个临时 pod 来验证应用程序的行为:

$ kubectl run tmp \
  --image=curlimages/curl \
  --restart=Never \
  -it \
  --rm \
  -- \
  curl \
  --insecure \
  -H 'Content-Type: application/json' \
  --request POST \
  --data '{"request": { "uid": "sample" } }' \
  https://hdac.default.svc
{"apiVersion":"admission.k8s.io/v1","kind":"AdmissionReview","response":{"uid":"sample","allowed":true}}pod "tmp" deleted

请注意:现在我们正在认真对待命令行选项 (SMILE) 的数量。

观察要点:

「我们改用curl;我发现它更容易用于复杂的请求」

「为避免 TLS 证书错误,我们使用insecure选项」

验证 Webhook 配置

要启用 webhook 应用程序,我们需要创建一个 ValidatingWebhookConfiguration 资源。但首先,我们需要创建ca.crt文件的单行 base64 编码版本。

$ cat ca.crt | base64 --wrap=0
[OBMITTED]

请注意:这里有一点混淆:ca.crt文件已经包含了一个base64编码的数据块;这里我们对整个文件进行编码(包括已经编码的数据)。

我们为ValidatingWebhookConfiguration创建一个配置文件:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "pod-policy.example.com"
webhooks:
- name: "pod-policy.example.com"
  rules:
  - apiGroups:   [""]
    apiVersions: ["v1"]
    operations:  ["CREATE"]
    resources:   ["pods"]
    scope:       "Namespaced"
  clientConfig:
    service:
      namespace: "default"
      name: "hdac"
    caBundle: "[OBMITTED]"
  admissionReviewVersions: ["v1"]
  sideEffects: None
  timeoutSeconds: 5

观察要点:

「名称,此处为pod-policy.example.com,需要全局唯一并表示为 DNS 名称」

「此配置仅适用于 pod 创建」

「所述caBundle属性是从上述编码数据」

「为简单起见,我们的 webhook 应用程序仅支持v1;在admissionReviewVersions属性中设置」

我们创建 ValidatingWebhookConfiguration:

$ kubectl apply -f Admission.yaml
 validatingwebhookconfiguration.admissionregistration.k8s.io/pod-policy.example.com已创建

重要的是要注意我们让应用程序从上一节开始运行。然后我们使用配置文件创建一个示例 pod:

apiVersion: v1
kind: Pod
metadata:
  labels:
  name: hello-pod
spec:
  containers:
  - name: ubuntu
    image: ubuntu:18.04
    command: ['tail''-f''/dev/null']

并应用它:

$ kubectl apply -f pod.yaml
 pod/hello-pod 创建

回顾我们应用程序的控制台,我们看到了请求 JSON 的完整结构(或它的几个深度),因为代码中有一个调试console.log。

{ kind: 'AdmissionReview',
  apiVersion: 'admission.k8s.io/v1',
  request: 
   { uid: 'eae5fb07-baa5-4ddc-876f-1b3f399a5833',
     kind: { group: '', version: 'v1', kind: 'Pod' },
     resource: { group: '', version: 'v1', resource: 'pods' },
     requestKind: { group: '', version: 'v1', kind: 'Pod' },
     requestResource: { group: '', version: 'v1', resource: 'pods' },
     name: 'hello-pod',
     namespace: 'default',
     operation: 'CREATE',
     userInfo: 
      { username: 'kubernetes-admin',
        uid: 'heptio-authenticator-aws:143287522423:AIDASCXE2CR33MJQ4BOAQ',
        groups: [Array],
        extra: [Object] },
     object: 
      { kind: 'Pod',
        apiVersion: 'v1',
        metadata: [Object],
        spec: [Object],
        status: [Object] },
     oldObject: null,
     dryRun: false,
     options: { kind: 'CreateOptions', apiVersion: 'meta.k8s.io/v1' } } }

虽然它超出了本文的范围,但“简单地”验证操作等同于验证提供的请求 JSON。

请注意:在其他项目中,我发现Ajv:另一个 JSON 模式验证器 库是一个特别强大的 JSON 验证解决方案。

容器化

现在我们的应用程序可以运行了,我们可以继续容器化它。首先,我们对应用程序代码做一些小的改动:

const bodyParser = require('body-parser');
const express = require('express');
const fs = require('fs');
const https = require('https');

const app = express();
app.use(bodyParser.json());
const port = 8443;

const options = { 
  ca: fs.readFileSync('ca.crt'), 
  cert: fs.readFileSync('server.crt'), 
  key: fs.readFileSync('server.key'), 
}; 

app.get('/hc', (req, res) => {
  res.send('ok');
});

app.post('/', (req, res) => {
  if (
    req.body.request === undefined ||
    req.body.request.uid === undefined
  ) {
    res.status(400).send();
    return;
  }
  console.log(req.body); // DEBUGGING
  const { request: { uid } } = req.body;
  res.send({
    apiVersion: 'admission.k8s.io/v1',
    kind: 'AdmissionReview',
    response: {
      uid,
      allowed: true,
    },
  });
});

const server = https.createServer(options, app);

server.listen(port, () => {
  console.log(`Server running on port ${port}/`);
});

观察要点:

「我们将应用程序更改为在非特权端口上运行;8443」

「我们添加了健康检查、hc、端点」

「我们还需要将我们之前生成的关键文件(容器将以非特权用户身份运行)的文件权限更改为世界可读,例如,」

$ chmod 644 *.key

我们创建一个Dockerfile:

FROM node:12.18.2
WORKDIR /usr/src/app
COPY app/package*.json ./
RUN npm install
COPY app .
EXPOSE 8443
USER 1000:1000
CMD [ "npm""start" ]

然后我们需要构建镜像并将其推送到存储库;就我而言,我使用了Amazon Elastic Container Repository (ECR)。另一个明显的选择是Docker Hub。

接下来,我们需要为我们的应用程序创建服务和 pod 定义:

apiVersion: v1
kind: Service
metadata:
  name: hdac
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: 8443
  selector:
    run: hdac
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: hdac
  name: hdac
spec:
  containers:
  - image: [OBMITTED]/hdac:0.1.0
    name: hdac
    ports:
    - containerPort: 8443
    imagePullPolicy: Always
    livenessProbe:
      httpGet:
        port: 8443
        path: /hc
        scheme: HTTPS
    readinessProbe:
      httpGet:
        port: 8443
        path: /hc
        scheme: HTTPS
    resources:
      limits:
        cpu: 100m
        memory: 128Mi
      requests:
        cpu: 100m
        memory: 128Mi
    securityContext:
      runAsNonRoot: true
      readOnlyRootFilesystem: true

观察要点:

「这里我们简单地部署了一个 pod,而不是首选部署。我这样做是为了让这篇文章保持简单」

最后,我们将更新后的应用程序部署到集群中:

$ kubectl apply -f controller.yaml
 service/hdac created 
pod/hdac created

请注意:为了应用此配置,我们必须先删除 ValidatingWebhookConfiguration,然后重新应用它;有点像鸡和蛋的问题。

结论

虽然这是一个有点冗长的过程,但实际代码相当简单,可以作为为 Kubernetes 动态准入控制创建自己的 webhook 的起点。我希望您发现这篇文章对您有所帮助和信息量!

参考:

「https://itnext.io/how-to-deploy-a-cross-cloud-kubernetes-cluster-with-built-in-disaster-recovery-bbce27fcc9d7」


文章转载自云原生CTO,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论