用Hashicorp Vault搭建自己的CA(四下)——Cert Manager与ACME

阅读本篇文章需要先完成上一篇:ACME

上一篇文章我们给Caddy和“其它“服务器配置了ACME协议,其实还有一种服务也具备ACME客户端功能,就是Kubernetes。这篇文章我们将使用cert-manager(下称cm)通过ACME协议从我们自己的CA获取证书。完成动手练习需要会简单使用kubectl工具。

ℹ️ 本篇文章涉及一些镜像仓库,访问这些仓库可能需要不可描述的方法,请自行解决。

C Nginx Ingress

为了搭建实验环境,我们在Linux虚拟机上启动一个minikube,它将使用Docker作为容器(默认就这个)。使用云服务商提供的正经Kubernetes也行,不过那不是得花钱么。

1
minikube start

输出应该是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
😄  minikube v1.33.1 on Debian 12.6
✨  Automatically selected the docker driver
📌  Using Docker driver with root privileges
👍  Starting "minikube" primary control-plane node in "minikube" cluster
🚜  Pulling base image v0.0.44 ...
🔥  Creating docker container (CPUs=2, Memory=2200MB) ...
🐳  Preparing Kubernetes v1.30.0 on Docker 26.1.1 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

ℹ️ macOS用户可以用OrbStack来代替minikube,又好用又轻👍。

可以看看Docker容器的IP地址:

1
minikube ip

minikube天生就有nginx ingress:

1
minikube addons enable ingress

输出大概是这样的:

1
2
3
4
5
6
7
💡  ingress is an addon maintained by Kubernetes. For any concerns contact minikube on GitHub.
You can view the list of minikube maintainers at: https://github.com/kubernetes/minikube/blob/master/OWNERS
    ▪ Using image registry.k8s.io/ingress-nginx/controller:v1.10.1
    ▪ Using image registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1
    ▪ Using image registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1
🔎  Verifying ingress addon...
🌟  The 'ingress' addon is enabled

等几分钟可以看看状态:

1
kubectl get pods -n ingress-nginx

输出大概是这样的:

1
2
3
4
NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-lh4n7        0/1     Completed   0          60s
ingress-nginx-admission-patch-lctx4         0/1     Completed   0          60s
ingress-nginx-controller-768f948f8f-zcb6j   1/1     Running     0          60s

ℹ️ 假如不是用minikube自带的nginx ingress,而是用其他方法安装的nginx ingress,默认可能是“LoadBalancer”,这样的话需要改一下配置:

1
2
kubectl get service --all-namespaces
kubectl patch svc <服务名字> -n <命名空间> -p '{"spec": {"type": "NodePort"}}'

D DNS:

cert-manager和ACME简易示意图

这里我们需要“劫持”DNS解析,我们要让Vault机器以及cm能够将ingress的FQDN解析为Docker的IP地址。cm会在发起http-01挑战之前先自查,所以它自己也得能解析我们的ingress绑定的FQDN,上面示意图并没有包括这一步。

以下方法二选一:

  1. 改hosts文件 在minikube的主机上给minikube的docker IP地址加一条:
1
sudo bash -c "echo \"$(minikube ip) hellonerd.miyunda.com\" >> /etc/hosts"

同样在Vault的机器上也加一条同样的信息:

1
sudo bash -c "echo \"192.168.49.2 hellonerd.miyunda.com\" >> /etc/hosts"

以上域名是我们实验用的FQDN,证书也是给它申请的。

ℹ️ 使用云服务商提供的Kubernetes可以忽略这步——直接去公网DNS加一条解析即可。

  1. 在家里的DNS服务器上添加解析:

这个每个人家里都不见得一样,就自己研究吧,我的是OPNsense上运行的Dnsmasq:

DNS示例

⚠️ 大多数DNS服务器默认不许把“看起来像公网的FQDN”解析为私有IP地址。我们得关闭rebinding检查。关闭了就有安全隐患,不过反正我这DNS服务器也只是做实验用的,无所谓。家里的正经设备不要这么搞,实在不行直接用foobar.local什么的算了。

E 部署一个app:

我选择使用Kuard用作演示,一个轻巧的服务。

minikube机器上运行:

1
2
3
mkdir ~/hellonerd-certmanager-acme && cd ~/hellonerd-certmanager-acme
nano deploy.yaml
kubectl apply -f deploy.yaml

以上文件内容是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kuard
spec:
  selector:
    matchLabels:
      app: kuard
  replicas: 1
  template:
    metadata:
      labels:
        app: kuard
    spec:
      containers:
      - image: gcr.io/kuar-demo/kuard-amd64:blue
        imagePullPolicy: Always
        name: kuard
        ports:
        - containerPort: 8080

还得给Kuard准备一个服务:

1
2
nano service.yaml
kubectl apply -f service.yaml

文件内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: Service
metadata:
  name: kuard
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: kuard

还有ingress,得让控制器知道如何绑定域名并向后端(Kuard)转发流量:

1
2
nano ingress.yaml
kubectl apply -f ingress.yaml

第12行我们约定了敏感信息将存在hellonerd-example-tls,不过这不会生效,因为minikube不知道哪里有证书签发者(issuer),见第5行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kuard
  annotations: {}
    # cert-manager.io/issuer: "vault-dev"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - hellonerd.miyunda.com
    secretName: hellonerd-example-tls
  rules:
  - host: hellonerd.miyunda.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kuard
            port:
              number: 80

等亿分钟之后查看状态:

1
kubectl get ingress

状态应该是这样的:

1
2
NAME    CLASS   HOSTS                   ADDRESS        PORTS     AGE
kuard   nginx   hellonerd.miyunda.com   192.168.49.2   80, 443   2m4s

ℹ️ 使用云服务商的同学会看到公网地址。

接下来在minikube主机上开启IPv4转发:

1
2
sudo sysctl net.ipv4.ip_forward # sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -P FORWARD ACCEPT

Vault主机添加静态路由:

1
sudo route add -host 192.168.49.2 gw 192.168.3.7 # minikube机器接入家庭网络的网卡的IP地址

ℹ️ 以上一条命令是为了Vault主机能访问minikube里面的Docker容器地址,要是它们两个装在同一个机器可以忽略。使用云服务商提供的Kubernets当然也可以忽略。

在minikube主机和Vault主机都试试访问:

1
curl -kivL hellonerd.miyunda.com

可以看到此时ingress的控制器不知道去哪能搞一个证书,于是就吃铁丝拉笊篱——现编了一个:

1
2
3
4
5
6
7
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
* Server certificate:
*  subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
*  start date: Aug 27 11:29:07 2024 GMT
*  expire date: Aug 27 11:29:07 2025 GMT
*  issuer: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate

而说好的敏感信息也无处寻觅:

1
2
kubectl describe secret hellonerd-example-tls
Error from server (NotFound): secrets "hellonerd-example-tls" not found

F cert-manager:

cert-manager是Kubernetes的数字证书集散中心,它从CA(或者Venafi什么的)提供的API获取数字证书,并按我们的指示安全地存好,ingress可以从存储中获取证书。

安装

安装方法有几种,我用helm完成:

在Debian上安装helm:

1
2
3
4
5
6
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt update
sudo apt install helm
helm version

安装cm:

1
helm repo add jetstack https://charts.jetstack.io --force-update
1
2
3
4
5
6
7
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.15.3 \
  --set crds.enabled=true \
  --set prometheus.enabled=false

注册ACME账号:

准备一个文件:

1
2
nano issuer.yaml
kubectl create -f issuer.yaml

内容是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: vault-dev # 随意命名
spec:
  acme:
    # Vault的ACME服务地址
    server: https://vault-dev.miyunda.com/v1/ca-int-x1/acme/directory
    # 邮件地址,因为我们没有发邮件的服务,所以无所谓
    email: 404@miyunda.com
    # 在ACME服务端注册后会有账号,账号的密钥存这里
    privateKeySecretRef:
      name: vault-dev
    # 使用http-01作为挑战方式
    solvers:
      - selector: {}
        http01:
          ingress:
            ingressClassName: nginx

看看它的状态:

1
kubectl get issuer

输出必须如下:

1
2
NAME        READY   AGE
vault-dev   True    2s

不行的话就要看看详细信息:

1
kubectl describe issuer vault-dev

这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Name:         vault-dev
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Issuer
Metadata:
  Creation Timestamp:  2024-08-22T08:05:47Z
  Generation:          1
  Resource Version:    5097
  UID:                 2075851d-ee06-4591-8562-dde715749a85
Spec:
  Acme:
    Email:  [email protected]
    Private Key Secret Ref:
      Name:  vault-dev
    Server:  https://vault-dev.miyunda.com/v1/ca-int-x1/acme/directory
    Solvers:
      http01:
        Ingress:
          Ingress Class Name:  nginx
      Selector:
Status:
  Acme:
    Last Private Key Hash:  Vo4NFyJrVngYt+Is02yZ3qi3H4kW8rbjEh9OWnRl0T0=
    Last Registered Email:  [email protected]
    Uri:                    https://vault-dev.miyunda.com/v1/ca-int-x1/acme/account/0a4a3c8a-fc9a-53ba-738f-74b4e1432820
  Conditions:
    Last Transition Time:  2024-08-22T08:05:48Z
    Message:               The ACME account was registered with the ACME server
    Observed Generation:   1
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
Events:                    <none>

给ingress申请及安装数字证书

先删了刚才创建的ingress:

1
kubectl delete ingress kuard

然后编辑下那个文件ingress.yaml,去掉第6行的注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kuard
  annotations:
    cert-manager.io/issuer: "vault-dev"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - hellonerd.miyunda.com
    secretName: hellonerd-example-tls
  rules:
  - host: hellonerd.miyunda.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kuard
            port:
              number: 80

再次创建:

1
kubectl apply -f ingress.yaml

第12行我们指定了敏感信息保存位置,这时它还只有证书hellonerd.miyunda.com的私钥(cm发CSR到Vault,Vault不存这个证书的私钥):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
kubectl describe secret hellonerd-example-tls-vvnk8
Name:         hellonerd-example-tls-vvnk8
Namespace:    default
Labels:       cert-manager.io/next-private-key=true
              controller.cert-manager.io/fao=true
Annotations:  <none>

Type:  Opaque

Data
====
tls.key:  1704 bytes

等几分钟再去看就看到它多了个证书本书:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
kubectl describe secret hellonerd-example-tls
Name:         hellonerd-example-tls
Namespace:    default

...

Data
====
tls.crt:  4709 bytes
tls.key:  1675 bytes

再次去访问网站:

1
curl -kivL hellonerd.miyunda.com

我们可以看到,网站已经绑定了我们CA签发的数字证书:

1
2
3
4
5
6
7
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Aug 27 12:25:26 2024 GMT
*  expire date: Sep 28 12:25:56 2024 GMT
*  issuer: OU=Marketing; CN=vault-dev.miyunda.com Intermediate Authority

这也符合下面第9、10行这里的描述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
kubectl describe ingress kuard

Name:             kuard
Labels:           <none>
Namespace:        default
Address:          192.168.49.2
Ingress Class:    nginx
Default backend:  <default>
TLS:
  hellonerd-example-tls terminates hellonerd.miyunda.com
Rules:
  Host                   Path  Backends
  ----                   ----  --------
  hellonerd.miyunda.com
                         /   kuard:80 (10.244.0.6:8080)
Annotations:             cert-manager.io/issuer: vault-dev
Events:
  Type    Reason             Age                From                       Message
  ----    ------             ----               ----                       -------
  Normal  CreateCertificate  50m                cert-manager-ingress-shim  Successfully created Certificate "hellonerd-example-tls"
  Normal  Sync               49m (x2 over 50m)  nginx-ingress-controller   Scheduled for sync

Kuard

排错:

列出数字证书:

1
kubectl get certificate

输出如下:

1
2
NAME                    READY   SECRET                  AGE
hellonerd-example-tls   True    hellonerd-example-tls   9m55s

看看它的信息:

1
kubectl describe certificate hellonerd-example-tls

输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
kubectl describe certificate hellonerd-example-tls
Name:         hellonerd-example-tls
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Certificate
Metadata:
  Creation Timestamp:  2024-08-22T08:05:23Z
  Generation:          1
  Owner References:
    API Version:           networking.k8s.io/v1
    Block Owner Deletion:  true
    Controller:            true
    Kind:                  Ingress
    Name:                  kuard
    UID:                   d2f61502-cbfc-430f-9694-b63c9ee9d3c5
  Resource Version:        5179
  UID:                     d3284be3-b26e-429a-8933-d83d5cff5365
Spec:
  Dns Names:
    hellonerd.miyunda.com
  Issuer Ref:
    Group:      cert-manager.io
    Kind:       Issuer
    Name:       vault-dev
  Secret Name:  hellonerd-example-tls
  Usages:
    digital signature
    key encipherment
Status:
  Conditions:
    Last Transition Time:  2024-08-22T08:06:06Z
    Message:               Certificate is up to date and has not expired
    Observed Generation:   1
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2024-09-23T08:06:06Z
  Not Before:              2024-08-22T08:05:36Z
  Renewal Time:            2024-09-12T16:05:56Z
  Revision:                1
Events:
  Type    Reason     Age   From                                       Message
  ----    ------     ----  ----                                       -------
  Normal  Issuing    13m   cert-manager-certificates-trigger          Issuing certificate as Secret does not exist
  Normal  Generated  13m   cert-manager-certificates-key-manager      Stored new private key in temporary Secret resource "hellonerd-example-tls-bh5gv"
  Normal  Requested  13m   cert-manager-certificates-request-manager  Created new CertificateRequest resource "hellonerd-example-tls-1"
  Normal  Issuing    12m   cert-manager-certificates-issuing          The certificate has been successfully issued

可以看看请求证书的状态:

1
kubectl get certificaterequests # --all-namespaces
1
2
NAME                      APPROVED   DENIED   READY   ISSUER      REQUESTOR                                         AGE
hellonerd-example-tls-1   True                True    vault-dev   system:serviceaccount:cert-manager:cert-manager   11m
1
kubectl describe certificaterequest hellonerd-example-tls-1

也可以看看Order:

1
kubectl get orders.acme.cert-manager.io
1
2
NAME                                 STATE   AGE
hellonerd-example-tls-1-3907671000   valid   11m
1
kubectl describe order hellonerd-example-tls-1-3907671000

好了,看过了等于会了。感谢观看。❤️


这篇文章我们演示了如何使用cm的ACME能力获取我们自己CA的数字证书。下一篇我不用ACME了,因为cm也能直接与Vault交互,而不是使用ACME协议。

0%