
点击上方 云原生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」




