• Kubernetes集群中配置Ingress支持HTTPS访问(一):cfssl


    一.系统环境

    本文主要基于Kubernetes1.22.2和Linux操作系统Ubuntu 18.04。

    服务器版本 docker软件版本 Kubernetes(k8s)集群版本 kube-bench版本 CPU架构
    Ubuntu 18.04.5 LTS Docker version 20.10.14 v1.22.2 0.6.7 x86_64

    Kubernetes集群架构:k8scludes1作为master节点,k8scludes2,k8scludes3作为worker节点。

    服务器 操作系统版本 CPU架构 进程 功能描述
    k8scludes1/192.168.110.128 Ubuntu 18.04.5 LTS x86_64 docker,kube-apiserver,etcd,kube-scheduler,kube-controller-manager,kubelet,kube-proxy,coredns,calico k8s master节点
    k8scludes2/192.168.110.129 Ubuntu 18.04.5 LTS x86_64 docker,kubelet,kube-proxy,calico k8s worker节点
    k8scludes3/192.168.110.130 Ubuntu 18.04.5 LTS x86_64 docker,kubelet,kube-proxy,calico k8s worker节点

    二.前言

    HTTPS是安全的HTTP协议,它通过SSL/TLS协议为客户端与服务器之间的通信提供加密。在Kubernetes集群中,Ingress资源管理集群外的访问,通过配置Ingress,我们可以为服务提供安全的HTTPS访问。

    在Kubernetes集群中配置Ingress以支持HTTPS访问的前提是已经有一套可以正常运行的Kubernetes集群,关于Kubernetes(k8s)集群的安装部署,可以查看博客《Ubuntu 安装部署Kubernetes(k8s)集群》https://www.cnblogs.com/renshengdezheli/p/17632858.html。

    三.对称加密和非对称加密简介

    数据加密的类型有:

    • 对称加密:使用相同的密钥进行加密和解密。优点是算法公开、计算量小、加密速度快、加密效率高,但密钥分发是一个挑战,如果密钥在协商过程中被泄露,那么密文就可能被破解。常见的对称加密算法有des,aes,des256,des512等
    • 非对称加密:使用一对密钥,一个用于加密(公钥(可公开)),另一个用于解密(私钥(私有的))。优点是安全性高,即使密文被拦截、公钥被获取,由于无法获取到私钥,攻击者也无法破译密文。常见的非对称加密算法有RSA、DSA、ECC等。但是非对称加密的计算过程复杂,加解密效率相对较低,举个例子:A给B传输文件,A先使用B的公钥加密,然后把加密文件发送给B,B使用自己的私钥进行解密。
    • 哈希函数:输入不定长的值,总能得到一个定长的值。

    另外非对称加密除了可以用来做数据加密(公钥加密,私钥解密),还可以用来做数字签名(私钥加密,公钥解密),数字签名用来验证身份,举个例子:A要给B发送一个数据,A需要向B证明这数据就是A发送的,不是别人发送的。A对需要传输的文件生成一个哈希值,文件内容不发生变化,哈希值就不会发生变化,A使用私钥加密哈希值,然后把数据和加密后的哈希值发送给B, B使用A的公钥解密哈希值,B将收到的数据生成一个哈希值,如果文件在传输的过程中没有被修改,则两个哈希值应该是一样的 ,这样就可以验证数据是不是A发送的,以及数据有没有被修改过。

    混合加密即对称加密和非对称加密的混合使用,有时会把文件先进行对称加密再非对称加密,再传输,这样对称加密的密钥传输过程就是安全的,但是存在中间人攻击,B把公钥发给A,途中被中间人C截获了,然后把C的公钥发给A,A自以为使用了“正确的B的公钥”,其实收到的是C伪装的公钥,A加密之后把加密文件发给B,C截获了加密文件,并使用C的私钥解密得到明文,最后使用正确的B公钥加密文件,把文件发给B,这就是中间人攻击

    解决中间人攻击的方法是使用CA,认证中心(CA)是一个权威的、受信任的第三方机构,其核心职能是发放和管理数字证书,用于证明和确认交易参与者的身份,保证电子商务交易过程中身份可认证性。举个例子:B发送csr(证书请求文件)给CA证书中心,CA审核通过之后,给B颁发证书,证书上有CA的盖章(数字签名),说明这个证书是CA认证过的没问题,B把证书发给A,A使用CA的公钥验证证书的数值签名,验证是不是CA颁发的证 书,浏览器里也存储着证书信息,确定这是CA给B颁发的证书之后,使用证书加密密钥,传输给B。

    注意:证书的本质就是公钥。

    总的来说,对称加密和非对称加密各有优缺点,具体使用哪种方式取决于实际需求和场景。

    四.什么是HTTPS

    HTTPS,全称Hypertext Transfer Protocol Secure,是超文本传输安全协议。它是一种通过计算机网络进行安全通信的传输协议,以安全为目标的 HTTP 通道。在HTTP的基础上,HTTPS通过传输加密和身份认证保证了传输过程的安全性。

    HTTPS的诞生是为了解决HTTP通信使用明文的问题,验证通信方的身份,以及证明报文的完整性。在HTTP与TCP之间,HTTPS加入了SSL(Secure Sockets Layer)/TLS(Transport Layer Security)协议来为数据传输提供加密和身份验证。如果进行更具体的解释,可以将HTTPS理解为身披SSL/TLS协议这层外壳的HTTP,即https=http+tls/ssl,ssl是tls的前身。

    HTTPS解决数据传输安全问题的方案就是使用加密算法,具体来说是混合加密算法,也就是对称加密和非对称加密的混合使用。

    五.Ingress简介

    Ingress是Kubernetes中的一个API对象,用于管理对集群内服务的外部访问。Ingress资源允许您定义如何路由外部HTTP(S)流量到集群中的不同服务。

    kubernetes服务的发布方式有:NodePort,LoadBalancer,Ingress,关于使用NodePort或者LoadBalancer发布Kubernetes服务,详情请查看博客《Kubernetes(k8s)服务service:service的发现和service的发布》,Kubernetes(k8s)使用ingress发布服务,我们在博客《Kubernetes(k8s)使用ingress发布服务》中也做了详细介绍。但是之前使用ingress发布服务,使用的是http的方式,是以明文的方式访问的,这样不安全。

    本文配置Ingress支持HTTPS访问,提高安全性,避免流量被劫持,提高搜索站点的权重。

    六.配置ingress对外发布服务

    6.1 安装NGINX ingress controller控制器

    要使用Ingress,需要先安装一个Ingress控制器来处理Ingress对象。本次使用Nginx Ingress Controller控制器,Nginx Ingress Controller控制器本质上是一个nginx的反向代理(根据访问地址的不同,转发到不同的服务器)。Nginx Ingress Controller的官网为:https://kubernetes.github.io/ingress-nginx/deploy/。

    下载ingress-nginx的部署yaml文件。

    root@k8scludes1:~/TLS-ingress# wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.1/deploy/static/provider/cloud/deploy.yaml
    Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.109.133|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 19299 (19K) [text/plain]
    Saving to: ‘deploy.yaml’
    
    deploy.yaml                                          100%[====================================================================================================================>]  18.85K  35.5KB/s    in 0.5s    
    
    2022-04-20 15:09:22 (35.5 KB/s) - ‘deploy.yaml’ saved [19299/19299]
    

    查看ingress-nginx所需的镜像。

    root@k8scludes1:~/TLS-ingress# grep image deploy.yaml 
              image: k8s.gcr.io/ingress-nginx/controller:v1.1.1@sha256:0bc88eb15f9e7f84e8e56c14fa5735aaa488b840983f87bd79b1054190e660de
              imagePullPolicy: IfNotPresent
              image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660
              imagePullPolicy: IfNotPresent
              image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660
              imagePullPolicy: IfNotPresent
    

    因为k8s.gcr.io/ingress-nginx/controller:v1.1.1镜像下载不了,我们搜索可用的镜像。

    root@k8scludes1:~# docker search controller:v1.1.1 --no-trunc
    NAME                              DESCRIPTION                                  STARS     OFFICIAL   AUTOMATED
    loging/ingress-nginx-controller   k8s.gcr.io/ingress-nginx/controller:v1.1.1   1                    
    jhonsun777/controller             k8s.gcr.io/ingress-nginx/controller:v1.1.1   0           
    
    root@k8scludes1:~/TLS-ingress# docker search kube-webhook-certgen:v1.1.1 --no-trunc
    NAME                                              DESCRIPTION                                                       STARS     OFFICIAL   AUTOMATED
    lianyuxue1020/kube-webhook-certgen                new pull lianyuxue1020/kube-webhook-certgen:v1.1.1                1                    
    xyz349925756/ingress-nginx-kube-webhook-certgen   k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1              0                    
    freemankevin/kube-webhook-certgen                 correspond:k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1   0                    
    longjianghu/ingress-nginx-kube-webhook-certgen    k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1原版镜像          0                    
    caihy/ingress-nginx-kube-webhook-certgen          FROM k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1         0                    
    

    在worker节点下载controller镜像和kube-webhook-certgen镜像。

    root@k8scludes2:~# docker pull  willdockerhub/ingress-nginx-controller:v1.0.0
    root@k8scludes2:~# docker pull  docker.io/liangjw/kube-webhook-certgen:v1.1.1
    
    root@k8scludes3:~# docker pull  willdockerhub/ingress-nginx-controller:v1.0.0
    root@k8scludes3:~# docker pull  docker.io/liangjw/kube-webhook-certgen:v1.1.1
    

    把deploy.yaml文件里的镜像修改为我们下载好的镜像,注意ingress-nginx-controller和kube-webhook-certgen镜像的版本不要差太多,不然部署失败。

    root@k8scludes1:~/TLS-ingress# grep image deploy.yaml
              image: willdockerhub/ingress-nginx-controller:v1.0.0
              imagePullPolicy: IfNotPresent
              image: docker.io/liangjw/kube-webhook-certgen:v1.1.1
              imagePullPolicy: IfNotPresent
              image: docker.io/liangjw/kube-webhook-certgen:v1.1.1
              imagePullPolicy: IfNotPresent
    

    应用ingress-nginx-controller。

    root@k8scludes1:~/TLS-ingress# kubectl apply -f deploy.yaml
    namespace/ingress-nginx created
    serviceaccount/ingress-nginx created
    configmap/ingress-nginx-controller created
    clusterrole.rbac.authorization.k8s.io/ingress-nginx created
    clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
    role.rbac.authorization.k8s.io/ingress-nginx created
    rolebinding.rbac.authorization.k8s.io/ingress-nginx created
    service/ingress-nginx-controller-admission created
    service/ingress-nginx-controller created
    deployment.apps/ingress-nginx-controller created
    ingressclass.networking.k8s.io/nginx created
    validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
    serviceaccount/ingress-nginx-admission created
    clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
    clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
    role.rbac.authorization.k8s.io/ingress-nginx-admission created
    rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
    job.batch/ingress-nginx-admission-create created
    job.batch/ingress-nginx-admission-patch created
    

    显示如下,则ingress-nginx-controller控制器部署成功。

    root@k8scludes1:~/TLS-ingress# kubectl get pod -n ingress-nginx -o wide 
    NAME                                        READY   STATUS      RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
    ingress-nginx-admission-create--1-t5hqt     0/1     Completed   0          23s   10.244.218.147   k8scludes2   <none>           <none>
    ingress-nginx-admission-patch--1-wzb6x      0/1     Completed   1          23s   10.244.218.146   k8scludes2   <none>           <none>
    ingress-nginx-controller-6b64bc6f47-dgjdq   1/1     Running     0          23s   10.244.218.148   k8scludes2   <none>           <none>
    

    注意deploy.yaml文件里- --watch-ingress-without-class=true参数加不加都没有影响。

    root@k8scludes1:~/TLS-ingress# grep -A12 arg deploy.yaml
              args:
                - /nginx-ingress-controller
                - --election-id=ingress-controller-leader
                - --controller-class=k8s.io/ingress-nginx
                - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
                - --validating-webhook=:8443
                - --validating-webhook-certificate=/usr/local/certificates/cert
                - --validating-webhook-key=/usr/local/certificates/key
                  #- --watch-ingress-without-class=true
    

    6.2 创建pod

    NGINX ingress controller控制器搭建成功之后创建pod。

    在worker节点提前下载好nginx镜像。

    root@k8scludes2:~# docker pull hub.c.163.com/library/nginx:latest
    root@k8scludes3:~# docker pull hub.c.163.com/library/nginx:latest
    

    pod配置文件如下,功能为使用Nginx镜像创建pod。

    root@k8scludes1:~/TLS-ingress# vim pod.yaml 
    
    #使用nginx镜像创建pod
    root@k8scludes1:~/TLS-ingress# cat pod.yaml 
    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        test: podtest
      name: podtest
    spec:
      #当需要关闭容器时,立即杀死容器而不等待默认的30秒优雅停机时长。
      terminationGracePeriodSeconds: 0
      containers:
      - name: nginx
        image: hub.c.163.com/library/nginx:latest
        #imagePullPolicy: IfNotPresent:表示如果本地已经存在该镜像,则不重新下载;否则从远程 Docker Hub 下载该镜像
        imagePullPolicy: IfNotPresent
    

    生成三个pod用于ingress访问。

    root@k8scludes1:~/TLS-ingress# sed 's/podtest/nginx1/' pod.yaml | kubectl apply -f -
    pod/nginx1 created
    
    root@k8scludes1:~/TLS-ingress# sed 's/podtest/nginx2/' pod.yaml | kubectl apply -f -
    pod/nginx2 created
    
    root@k8scludes1:~/TLS-ingress# sed 's/podtest/nginx3/' pod.yaml | kubectl apply -f -
    pod/nginx3 created
    

    查看pod。

    root@k8scludes1:~/TLS-ingress# kubectl get pod -o wide
    NAME     READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
    nginx1   1/1     Running   0          31s   10.244.218.149   k8scludes2   <none>           <none>
    nginx2   1/1     Running   0          21s   10.244.218.150   k8scludes2   <none>           <none>
    nginx3   1/1     Running   0          12s   10.244.1.81      k8scludes3   <none>           <none>
    

    修改nginx的index.html文件,用于辨别每个pod。

    root@k8scludes1:~/TLS-ingress# kubectl exec -it nginx1 -- sh -c "echo 111 >/usr/share/nginx/html/index.html"
    
    root@k8scludes1:~/TLS-ingress# kubectl exec -it nginx2 -- sh -c "echo 222 >/usr/share/nginx/html/index.html"
    
    root@k8scludes1:~/TLS-ingress# kubectl exec -it nginx3 -- sh -c "mkdir /usr/share/nginx/html/ingress; echo 333 >/usr/share/nginx/html/ingress/index.html"
    

    6.3 为pod创建svc服务

    给每个pod创建一个svc服务。

    root@k8scludes1:~/TLS-ingress# kubectl expose --name=nginx1svc pod nginx1 --port=80
    service/nginx1svc exposed
    
    root@k8scludes1:~/TLS-ingress# kubectl expose --name=nginx2svc pod nginx2 --port=80
    service/nginx2svc exposed
    
    root@k8scludes1:~/TLS-ingress# kubectl expose --name=nginx3svc pod nginx3 --port=80
    service/nginx3svc exposed
    

    查看svc。

    root@k8scludes1:~/TLS-ingress# kubectl get svc -o wide
    NAME        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE   SELECTOR
    nginx1svc   ClusterIP   10.98.119.83    <none>        80/TCP    24s   test=nginx1
    nginx2svc   ClusterIP   10.106.4.10     <none>        80/TCP    17s   test=nginx2
    nginx3svc   ClusterIP   10.101.154.93   <none>        80/TCP    9s    test=nginx3
    

    访问svc。

    root@k8scludes1:~/TLS-ingress# curl 10.98.119.83
    111
    
    root@k8scludes1:~/TLS-ingress# curl 10.106.4.10
    222
    
    root@k8scludes1:~/TLS-ingress# curl 10.101.154.93/ingress/index.html
    333
    

    6.4 使用ingress发布服务

    创建ingress规则。

    root@k8scludes1:~/TLS-ingress# vim ingress-rule.yaml
    
    #注意在annotations中需要指定你的ingress是何种,此处使用的nginx-ingress所以是Nginx,否则无法通过配置的域名www.nginx13.com访问
    #访问www.nginx13.com就相当于访问nginx1svc服务
    root@k8scludes1:~/TLS-ingress# cat ingress-rule.yaml 
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: my-ingress
      annotations:
        kubernetes.io/ingress.class: "nginx"
    spec:
      rules:
      - host: www.nginx13.com
        http:
          paths:
          #访问网址目录
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx1svc
                port:
                  number: 80
          - path: /ingress
            pathType: Prefix
            backend:
              service:
                name: nginx3svc
                port:
                  number: 80
    
      - host: www.nginx2.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx2svc
                port:
                  number: 80
    

    应用ingress规则。

    root@k8scludes1:~/TLS-ingress# kubectl apply -f ingress-rule.yaml 
    ingress.networking.k8s.io/my-ingress created
    

    查看ingress。

    root@k8scludes1:~/TLS-ingress# kubectl get ingress -o wide
    NAME         CLASS    HOSTS                            ADDRESS   PORTS   AGE
    my-ingress   <none>   www.nginx13.com,www.nginx2.com             80      10s
    
    root@k8scludes1:~/TLS-ingress# kubectl get ing -o wide
    NAME         CLASS    HOSTS                            ADDRESS   PORTS   AGE
    my-ingress   <none>   www.nginx13.com,www.nginx2.com             80      34s
    

    可以发现svc的80端口被映射为32253端口。

    root@k8scludes1:~/TLS-ingress# kubectl get svc -n ingress-nginx -o wide
    NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE   SELECTOR
    ingress-nginx-controller             NodePort    10.96.184.210   <none>        80:32253/TCP,443:30876/TCP   22m   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    ingress-nginx-controller-admission   ClusterIP   10.102.52.109   <none>        443/TCP                      22m   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    

    当在其他命名空间创建相同的ingress规则时,会提醒重复。

    root@k8scludes1:~/TLS-ingress# kubectl apply -f ingress-rule.yaml -n default
    Error from server (BadRequest): error when creating "ingress-rule.yaml": admission webhook "validate.nginx.ingress.kubernetes.io" denied the request: host "www.nginx13.com" and path "/" is already defined in ingress tls-ingress/my-ingress
    

    6.5 访问服务

    6.5.1 使用Linux客户端来访问服务

    ingress规则创建之后,我们使用Linux客户端来访问服务。

    我们使用etcd2机器作为客户端,因为ingress-nginx-controller控制器在k8scludes2上,k8scludes2的IP地址为192.168.110.129。

    root@k8scludes1:~/TLS-ingress# kubectl get pod -o wide -n ingress-nginx
    NAME                                        READY   STATUS      RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
    ingress-nginx-admission-create--1-t5hqt     0/1     Completed   0          23m   10.244.218.147   k8scludes2   <none>           <none>
    ingress-nginx-admission-patch--1-wzb6x      0/1     Completed   1          23m   10.244.218.146   k8scludes2   <none>           <none>
    ingress-nginx-controller-6b64bc6f47-dgjdq   1/1     Running     0          23m   10.244.218.148   k8scludes2   <none>           <none>
    

    我们配置客户端etcd2机器的 /etc/hosts文件,ingress-nginx-controller控制器IP和域名的映射。

    [root@etcd2 ~]# vim /etc/hosts
    
    ingress-nginx-controller控制器IP和域名的映射
    [root@etcd2 ~]# cat /etc/hosts
    127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
    ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
    192.168.110.133 etcd1
    192.168.110.131 etcd2
    192.168.110.132 etcd3
    192.168.110.129 www.nginx13.com
    192.168.110.129 www.nginx2.com
    

    直接访问域名的80端口被拒绝了。

    [root@etcd2 ~]# curl www.nginx13.com
    curl: (7) Failed connect to www.nginx13.com:80; 拒绝连接
    
    [root@etcd2 ~]# curl www.nginx2.com
    curl: (7) Failed connect to www.nginx2.com:80; 拒绝连接
    

    ingress-nginx-controller服务把80端口被映射为32253端口,所以外界需要访问32253端口。

    root@k8scludes1:~/TLS-ingress# kubectl get svc -n ingress-nginx -o wide
    NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE   SELECTOR
    ingress-nginx-controller             NodePort    10.96.184.210   <none>        80:32253/TCP,443:30876/TCP   22m   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    ingress-nginx-controller-admission   ClusterIP   10.102.52.109   <none>        443/TCP                      22m   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    

    现在通过域名就可以访问nginx的不同服务了。

    [root@etcd2 ~]# curl www.nginx13.com:32253
    111
    
    [root@etcd2 ~]# curl www.nginx2.com:32253/
    222
    
    [root@etcd2 ~]# curl www.nginx13.com:32253/ingress/index.html
    333
    

    6.5.2 使用Windows客户端访问服务

    我们也可以使用Windows机器作为客户端访问服务。修改Windows机器的C:\Windows\System32\drivers\etc\HOSTS文件的IP域名映射。

    192.168.110.129 www.nginx13.com
    192.168.110.129 www.nginx2.com
    

    然后使用浏览器通过域名访问nginx服务。

    image-20230728163747507

    image-20230728163803302

    image-20230728163819194

    自此ingress对外发布服务成功,但是现在使用的是http://www.nginx13.com访问的,http是明文传输,不安全。

    七.配置Ingress支持HTTPS访问

    7.1 使用ingress-nginx-controller自带的证书

    此时pod,svc没有做任何https配置,客户端使用https访问ingress-nginx-controller,浏览器会发出警告,这是因为ingress-nginx-controller也有证书,但是客户端一核实,不是CA颁发的证书,浏览器就报警了。

    https端口为443,对应的端口为30876。

    root@k8scludes1:~/TLS-ingress# kubectl get svc -n ingress-nginx -o wide
    NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE   SELECTOR
    ingress-nginx-controller             NodePort    10.96.184.210   <none>        80:32253/TCP,443:30876/TCP   22m   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    ingress-nginx-controller-admission   ClusterIP   10.102.52.109   <none>        443/TCP                      22m   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    

    浏览器访问https://www.nginx2.com:31473/,查看证书,点击证书无效。

    image-20230728164551030

    可以看到,证书为ingress自带的证书。

    image-20230728164634801

    继续访问。

    image-20230728164721194

    使用https可以访问服务,但是证书是ingress自带的证书。

    image-20230728164756572

    ingress-nginx-controller给不同的域名提供域名解析服务,证书是各个域名共享的,即所有的站点共享这个证书。

    7.2 使用cfssl工具生成证书

    7.2.1 安装cfssl

    现在我们想申请自己专属的证书,而不是ingress-nginx-controller共享的证书,可以使用cfssl工具自己生成一个证书。

    cfssl下载地址:https://github.com/cloudflare/cfssl/releases

    下载这三个文件:cfssl_1.6.1_linux_amd64 , cfssl-certinfo_1.6.1_linux_amd64 , cfssljson_1.6.1_linux_amd64 。

    image-20230728164902561

    把下载好的cfssl文件放到 /usr/local/bin/目录下。

    root@k8scludes1:~/TLS-ingress# cd /usr/local/bin/
    
    root@k8scludes1:/usr/local/bin# ls
    
    root@k8scludes1:/usr/local/bin# pwd
    /usr/local/bin
    
    root@k8scludes1:/usr/local/bin# rz -E
    rz waiting to receive.
    
    root@k8scludes1:/usr/local/bin# ls
    cfssl_1.6.1_linux_amd64  cfssl-certinfo_1.6.1_linux_amd64  cfssljson_1.6.1_linux_amd64
    

    文件重命名。

    root@k8scludes1:/usr/local/bin# mv cfssl_1.6.1_linux_amd64 cfssl
    
    root@k8scludes1:/usr/local/bin# mv cfssl-certinfo_1.6.1_linux_amd64 cfssl-certinfo
    
    root@k8scludes1:/usr/local/bin# mv cfssljson_1.6.1_linux_amd64 cfssljson
    
    root@k8scludes1:/usr/local/bin# ll -h
    total 40M
    drwxr-xr-x  2 root root 4.0K Apr 21 19:45 ./
    drwxr-xr-x 10 root root 4.0K Nov 27  2020 ../
    -rw-r--r--  1 root root  16M Apr 21 17:39 cfssl
    -rw-r--r--  1 root root  13M Apr 21 17:50 cfssl-certinfo
    -rw-r--r--  1 root root  11M Apr 21 17:42 cfssljson
    

    赋予可执行权限。

    root@k8scludes1:/usr/local/bin# chmod +x ./*
    

    7.2.2 生成CA

    创建tls目录存放证书文件。

    root@k8scludes1:/usr/local/bin# cd
    
    root@k8scludes1:~/TLS-ingress# mkdir tls
    
    root@k8scludes1:~/TLS-ingress# cd tls/
    
    root@k8scludes1:~/TLS-ingress/tls# pwd
    /root/TLS-ingress/tls
    

    使用cfssl工具生成证书的原理是:自己模拟一套完整的环境,包括CA也是自己搭建,需要CA(ca公钥,ca私钥),我们自己的私钥和ca给我们颁发的证书,申请ca证书还需要证书请求文件csr。

    生成CA配置文件。

    • ca-config.json:可以定义多个 profiles,分别指定不同的过期时间、使用场景等参数;后续在签名证书时使用某个profile;www,client都是profile。
    • signing:表示该证书可用于签名其它证书;生成的ca.pem证书中 CA=TRUE;
    • server auth:表示client可以用该 CA 对server提供的证书进行验证;
    • client auth:表示server可以用该CA对client提供的证书进行验证 ;
    root@k8scludes1:~/TLS-ingress/tls# cfssl print-defaults config > ca-config.json
    
    root@k8scludes1:~/TLS-ingress/tls# cat ca-config.json 
    {
        "signing": {
            "default": {
                "expiry": "168h"
            },
            "profiles": {
                "www": {
                    "expiry": "8760h",
                    "usages": [
                        "signing",
                        "key encipherment",
                        "server auth"
                    ]
                },
                "client": {
                    "expiry": "8760h",
                    "usages": [
                        "signing",
                        "key encipherment",
                        "client auth"
                    ]
                }
            }
        }
    }
    

    修改CA配置文件,这里只设置一个profiles:www。

    root@k8scludes1:~/TLS-ingress/tls# vim ca-config.json 
    
    root@k8scludes1:~/TLS-ingress/tls# cat ca-config.json 
    {
        "signing": {
            "default": {
                "expiry": "1680h"
            },
            "profiles": {
                "www": {
                    "expiry": "8760h",
                    "usages": [
                        "signing",
                        "key encipherment",
                        "server auth"
                    ]
                }
            }
        }
    }
    

    现在生成ca的证书请求文件csr。

    • CN: Common Name,浏览器使用该字段验证网站是否合法,一般写的是域名;
    • C: Country, 国家;
    • L: Locality,地区,城市;
    • O: Organization Name,组织名称,公司名称;
    • OU: Organization Unit Name,组织单位名称,公司部门;
    • ST: State,州,省。
    root@k8scludes1:~/TLS-ingress/tls# cfssl print-defaults csr > ca-csr.json
    
    root@k8scludes1:~/TLS-ingress/tls# cat ca-csr.json 
    {
        "CN": "example.net",
        "hosts": [
            "example.net",
            "www.example.net"
        ],
        "key": {
            "algo": "ecdsa",
            "size": 256
        },
        "names": [
            {
                "C": "US",
                "ST": "CA",
                "L": "San Francisco"
            }
        ]
    }
    

    ca的证书请求文件ca-csr.json,修改域名为nginxx.com,城市信息换成广州,algo表示加密算法,size表示算法长度。

    root@k8scludes1:~/TLS-ingress/tls# vim ca-csr.json 
    
    root@k8scludes1:~/TLS-ingress/tls# cat ca-csr.json 
    {
        "CN": "nginxx.com",
        "key": {
            "algo": "ecdsa",
            "size": 256
        },
        "names": [
            {
                "C": "CN",
                "ST": "guangdon",
                "L": "guangzhou"
            }
        ]
    }
    

    下面生成CA权威机构。

    root@k8scludes1:~/TLS-ingress/tls# cfssl gencert -initca ca-csr.json | cfssljson -bare ca
    2022/04/22 15:42:54 [INFO] generating a new CA key and certificate from CSR
    2022/04/22 15:42:54 [INFO] generate received request
    2022/04/22 15:42:54 [INFO] received CSR
    2022/04/22 15:42:54 [INFO] generating key: ecdsa-256
    2022/04/22 15:42:54 [INFO] encoded CSR
    2022/04/22 15:42:54 [INFO] signed certificate with serial number 361660789191812789469217914317150175823294603356
    

    生成了一个自签名的证书,CA的证书(ca.pem),CA的私钥(ca-key.pem)。

    root@k8scludes1:~/TLS-ingress/tls# ls
    ca-config.json  ca.csr  ca-csr.json  ca-key.pem  ca.pem
    

    7.2.3 生成用户证书

    生成用户test的证书请求文件。

    root@k8scludes1:~/TLS-ingress/tls# cfssl print-defaults csr >test-csr.json
    
    root@k8scludes1:~/TLS-ingress/tls# cat test-csr.json 
    {
        "CN": "example.net",
        "hosts": [
            "example.net",
            "www.example.net"
        ],
        "key": {
            "algo": "ecdsa",
            "size": 256
        },
        "names": [
            {
                "C": "US",
                "ST": "CA",
                "L": "San Francisco"
            }
        ]
    }
    

    修改test-csr.json,"www.nginxx.com"表示申请的证书只给www.nginxx.com使用,hosts参数为空的话,便是所有的站点都可以使用该证书。

    root@k8scludes1:~/TLS-ingress/tls# vim test-csr.json 
    
    root@k8scludes1:~/TLS-ingress/tls# cat test-csr.json 
    {
        "CN": "nginxx.com",
        "key": {
            "algo": "ecdsa",
            "size": 256
        },
        "hosts":[
              "www.nginxx.com"
        ], 
        "names": [
            {
                "C": "CN",
                "ST": "guangdon",
                "L": "guangzhou"
            }
        ]
    }
    
    root@k8scludes1:~/TLS-ingress/tls# ls
    ca-config.json  ca.csr  ca-csr.json  ca-key.pem  ca.pem  test-csr.json
    

    把test用户的证书请求文件发给CA,让CA审批,ca.pem是ca公钥,ca-key.pem是ca私钥,ca-config.json是ca配置文件,profile指定为www,最后的用户名test是给用户颁发证书名字的前缀。

    root@k8scludes1:~/TLS-ingress/tls# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=www test-csr.json | cfssljson -bare test
    2022/04/22 16:05:02 [INFO] generate received request
    2022/04/22 16:05:02 [INFO] received CSR
    2022/04/22 16:05:02 [INFO] generating key: ecdsa-256
    2022/04/22 16:05:02 [INFO] encoded CSR
    2022/04/22 16:05:02 [INFO] signed certificate with serial number 468422675225780030350363273085193709570812230921
    
    root@k8scludes1:~/TLS-ingress/tls# ls
    ca-config.json  ca.csr  ca-csr.json  ca-key.pem  ca.pem  test.csr  test-csr.json  test-key.pem  test.pem
    

    test-key.pem是test用户的私钥,test.pem是ca给test颁发的证书(也就是公钥)。

    root@k8scludes1:~/TLS-ingress/tls# ls test*
    test.csr  test-csr.json  test-key.pem  test.pem
    

    7.2.4 使用自定义的证书

    接下来要替换掉ingress-nginx-controller自带的证书,使用我们生成的证书。

    查看secret。

    root@k8scludes1:~/TLS-ingress/tls# kubectl get secrets 
    NAME                  TYPE                                  DATA   AGE
    default-token-mxb4r   kubernetes.io/service-account-token   3      3d23h
    

    创建一个tls类型的secret,里面包含test用户的私钥和证书,查看tls类型的secret的语法。

    root@k8scludes1:~/TLS-ingress/tls# kubectl create secret tls --help
    Create a TLS secret from the given public/private key pair.
    
     The public/private key pair must exist beforehand. The public key certificate must be .PEM encoded and match the given
    private key.
    
    Examples:
      # Create a new TLS secret named tls-secret with the given key pair
      kubectl create secret tls tls-secret --cert=path/to/tls.cert --key=path/to/tls.key
    
    Options:
          --allow-missing-template-keys=true: If true, ignore any errors in templates when a field or map key is missing in
    the template. Only applies to golang and jsonpath output formats.
          --append-hash=false: Append a hash of the secret to its name.
          --cert='': Path to PEM encoded public key certificate.
          --dry-run='none': Must be "none", "server", or "client". If client strategy, only print the object that would be
    sent, without sending it. If server strategy, submit server-side request without persisting the resource.
          --field-manager='kubectl-create': Name of the manager used to track field ownership.
          --key='': Path to private key associated with given certificate.
      -o, --output='': Output format. One of:
    json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-as-json|jsonpath-file.
          --save-config=false: If true, the configuration of current object will be saved in its annotation. Otherwise, the
    annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.
          --show-managed-fields=false: If true, keep the managedFields when printing objects in JSON or YAML format.
          --template='': Template string or path to template file to use when -o=go-template, -o=go-template-file. The
    template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].
          --validate=true: If true, use a schema to validate the input before sending it
    
    Usage:
      kubectl create secret tls NAME --cert=path/to/cert/file --key=path/to/key/file [--dry-run=server|client|none]
    [options]
    
    Use "kubectl options" for a list of global command-line options (applies to all commands).
    

    创建一个tls类型的secret,里面包含test用户的私钥和证书。

    root@k8scludes1:~/TLS-ingress/tls# ls
    ca-config.json  ca.csr  ca-csr.json  ca-key.pem  ca.pem  test.csr  test-csr.json  test-key.pem  test.pem
    
    root@k8scludes1:~/TLS-ingress/tls# kubectl create secret tls test-tls-secret --cert=test.pem --key=test-key.pem
    secret/test-tls-secret created
    

    secrets创建成功。

    root@k8scludes1:~/TLS-ingress/tls# kubectl get secrets 
    NAME                  TYPE                                  DATA   AGE
    default-token-mxb4r   kubernetes.io/service-account-token   3      3d23h
    test-tls-secret       kubernetes.io/tls                     2      8s
    

    删除现有的ingress规则。

    root@k8scludes1:~/TLS-ingress/tls# pwd
    /root/TLS-ingress/tls
    
    root@k8scludes1:~/TLS-ingress/tls# kubectl get ingress
    NAME         CLASS    HOSTS                            ADDRESS           PORTS   AGE
    my-ingress   <none>   www.nginx13.com,www.nginx2.com   192.168.110.129   80      46h
    
    root@k8scludes1:~/TLS-ingress/tls# cd ../
    
    root@k8scludes1:~/TLS-ingress# ls
    bak  certgen15.tar  controller1.tar  deploy.yaml  deploy.yaml.1  deploy.yml  ingress-rule.yaml  pod.yaml  tls  vim
    
    root@k8scludes1:~/TLS-ingress# kubectl delete -f ingress-rule.yaml 
    ingress.networking.k8s.io "my-ingress" deleted
    root@k8scludes1:~/TLS-ingress# kubectl get ingress
    No resources found in tls-ingress namespace.
    

    修改ingress规则,ingress-rule.yaml里指定了tls的域名信息和secret,secret里包含了证书。

    root@k8scludes1:~/TLS-ingress# vim ingress-rule.yaml 
    
    root@k8scludes1:~/TLS-ingress# cat ingress-rule.yaml 
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: my-ingress
      annotations:
        kubernetes.io/ingress.class: "nginx"
    spec:
      tls: 
      - hosts: 
        - www.nginxx.com
        secretName: test-tls-secret
      rules:
      - host: www.nginx13.com
        http:
          paths:
          #访问网址目录
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx1svc
                port:
                  number: 80
          - path: /ingress
            pathType: Prefix
            backend:
              service:
                name: nginx3svc
                port:
                  number: 80
    
      - host: www.nginx2.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx2svc
                port:
                  number: 80
    

    应用ingress规则。

    root@k8scludes1:~/TLS-ingress# kubectl apply -f ingress-rule.yaml 
    ingress.networking.k8s.io/my-ingress created
    

    查看ingress规则,现在已经有443端口了,http对应80端口,https对应443端口。

    root@k8scludes1:~/TLS-ingress# kubectl get ingress -o wide
    NAME         CLASS    HOSTS                            ADDRESS   PORTS     AGE
    my-ingress   <none>   www.nginx13.com,www.nginx2.com             80, 443   16s
    

    443端口对应的是31473端口,因为secret里包含了证书,会自动覆盖ingress-nginx-control里面的证书。

    root@k8scludes1:~/TLS-ingress# kubectl get svc -o wide -n ingress-nginx
    NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE   SELECTOR
    ingress-nginx-controller             NodePort    10.98.61.146    <none>        80:31853/TCP,443:31473/TCP   46h   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    ingress-nginx-controller-admission   ClusterIP   10.102.212.60   <none>        443/TCP                      46h   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    

    下面使用Linux客户端进行访问,客户端的/etc/hosts如下:

    [root@etcd2 ~]# cat /etc/hosts
    127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
    ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
    192.168.110.133 etcd1
    192.168.110.131 etcd2
    192.168.110.132 etcd3
    192.168.110.129 www.nginx13.com
    192.168.110.129 www.nginx2.com
    

    使用https访问,可以发现证书为ingress自带的证书:CN=Kubernetes Ingress Controller Fake Certificate。

    [root@etcd2 ~]# curl -kv https://www.nginx13.com:31473/ingress/index.html
    * About to connect() to www.nginx13.com port 31473 (#0)
    *   Trying 192.168.110.129...
    * Connected to www.nginx13.com (192.168.110.129) port 31473 (#0)
    * Initializing NSS with certpath: sql:/etc/pki/nssdb
    * skipping SSL peer certificate verification
    * SSL connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    * Server certificate:
    * 	subject: CN=Kubernetes Ingress Controller Fake Certificate,O=Acme Co
    * 	start date: 422 04:09:27 2022 GMT
    * 	expire date: 422 04:09:27 2023 GMT
    * 	common name: Kubernetes Ingress Controller Fake Certificate
    * 	issuer: CN=Kubernetes Ingress Controller Fake Certificate,O=Acme Co
    > GET /ingress/index.html HTTP/1.1
    > User-Agent: curl/7.29.0
    > Host: www.nginx13.com:31473
    > Accept: */*
    > 
    < HTTP/1.1 200 OK
    < Date: Fri, 22 Apr 2022 08:51:08 GMT
    < Content-Type: text/html
    < Content-Length: 4
    < Connection: keep-alive
    < Last-Modified: Fri, 22 Apr 2022 07:17:09 GMT
    < ETag: "62625675-4"
    < Accept-Ranges: bytes
    < Strict-Transport-Security: max-age=15724800; includeSubDomains
    < 
    333
    * Connection #0 to host www.nginx13.com left intact
    

    Windows客户端使用https访问服务,浏览器输入https://www.nginx13.com:31473/ingress/index.html,我们查看证书。

    image-20230728165144101

    发现证书是ingress自带的证书。

    image-20230728165213137

    继续访问。

    image-20230728165248610

    可以发现,现在可以https访问ingress了,但是证书不是我们自己生成的那个证书,而是ingress自带的证书,是因为我们自己生成的证书只给域名www.nginxx.com使用,其他域名访问只能使用ingress自带的证书。

    image-20230728165322147

    现在修改ingress规则,使其使用我们自己生成的证书。

    删除ingress规则。

    root@k8scludes1:~/TLS-ingress# ls
    bak  certgen15.tar  controller1.tar  deploy.yaml  deploy.yaml.1  deploy.yml  ingress-rule.yaml  pod.yaml  tls  vim
    
    root@k8scludes1:~/TLS-ingress# kubectl delete -f ingress-rule.yaml 
    ingress.networking.k8s.io "my-ingress" deleted
    
    root@k8scludes1:~/TLS-ingress# kubectl get ingress
    No resources found in tls-ingress namespace.
    

    修改ingress规则,host域名只有www.nginxx.com。

    root@k8scludes1:~/TLS-ingress# vim ingress-rule.yaml 
    
    root@k8scludes1:~/TLS-ingress# cat ingress-rule.yaml 
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: my-ingress
      annotations:
        kubernetes.io/ingress.class: "nginx"
    spec:
      tls: 
      - hosts: 
        - www.nginxx.com
        secretName: test-tls-secret
      rules:
      - host: www.nginxx.com
        http:
          paths:
          #访问网址目录
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx1svc
                port:
                  number: 80
          - path: /ingress
            pathType: Prefix
            backend:
              service:
                name: nginx3svc
                port:
                  number: 80
          - path: /n2
            pathType: Prefix
            backend:
              service:
                name: nginx2svc
                port:
                  number: 80
    

    应用ingress规则。

    root@k8scludes1:~/TLS-ingress# kubectl apply -f ingress-rule.yaml 
    ingress.networking.k8s.io/my-ingress created
    
    root@k8scludes1:~/TLS-ingress# kubectl get ingress -o wide
    NAME         CLASS    HOSTS            ADDRESS           PORTS     AGE
    my-ingress   <none>   www.nginxx.com   192.168.110.129   80, 443   2m6s
    
    root@k8scludes1:~/TLS-ingress# kubectl get svc -o wide -n ingress-nginx
    NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE   SELECTOR
    ingress-nginx-controller             NodePort    10.98.61.146    <none>        80:31853/TCP,443:31473/TCP   47h   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    ingress-nginx-controller-admission   ClusterIP   10.102.212.60   <none>        443/TCP                      47h   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
    

    首先修改Linux客户端的/etc/hosts,添加IP域名映射:192.168.110.129 www.nginxx.com。

    [root@etcd2 ~]# vim /etc/hosts
    
    [root@etcd2 ~]# cat /etc/hosts
    127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
    ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
    192.168.110.133 etcd1
    192.168.110.131 etcd2
    192.168.110.132 etcd3
    192.168.110.129 www.nginx13.com
    192.168.110.129 www.nginx2.com
    192.168.110.129 www.nginxx.com
    

    使用Linux客户端访问服务,使用https访问,发现证书变为我们自定义的nginxx.com。

    [root@etcd2 ~]# curl -kv https://www.nginxx.com:31473/ingress/index.html
    * About to connect() to www.nginxx.com port 31473 (#0)
    *   Trying 192.168.110.129...
    * Connected to www.nginxx.com (192.168.110.129) port 31473 (#0)
    * Initializing NSS with certpath: sql:/etc/pki/nssdb
    * skipping SSL peer certificate verification
    * SSL connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
    * Server certificate:
    * 	subject: CN=nginxx.com,L=guangzhou,ST=guangdon,C=CN
    * 	start date: 422 08:00:00 2022 GMT
    * 	expire date: 422 08:00:00 2023 GMT
    * 	common name: nginxx.com
    * 	issuer: CN=nginxx.com,L=guangzhou,ST=guangdon,C=CN
    > GET /ingress/index.html HTTP/1.1
    > User-Agent: curl/7.29.0
    > Host: www.nginxx.com:31473
    > Accept: */*
    > 
    < HTTP/1.1 200 OK
    < Date: Fri, 22 Apr 2022 09:26:31 GMT
    < Content-Type: text/html
    < Content-Length: 4
    < Connection: keep-alive
    < Last-Modified: Fri, 22 Apr 2022 07:17:09 GMT
    < ETag: "62625675-4"
    < Accept-Ranges: bytes
    < Strict-Transport-Security: max-age=15724800; includeSubDomains
    < 
    333
    * Connection #0 to host www.nginxx.com left intact
    

    下面使用Windows客户端访问服务,查看证书。

    image-20230728174014215

    证书为我们自定义的证书。

    image-20230728174049992

    继续访问。

    image-20230728174122626

    image-20230728174148127

    现在既可以使用https访问服务,又使用了我们自定义的证书了。

    本文使用cfssl工具生成了证书,实现了https访问,但是那个证书是我们自定义的,不是权威机构颁发的证书,在浏览器里https访问还是有警告,要想申请权威证书,请查看博客《配置Ingress支持HTTPS访问(二):使用cert-manager申请证书》,使用cert-manager向Let’s Encrypt机构申请的证书,是权威证书,在浏览器里https访问是不会有警告的。

    八.总结

    通过本文,您应该了解了如何在Kubernetes集群中为Ingress配置HTTPS。通过使用TLS证书和适当的Ingress配置,您可以确保服务与客户端之间的通信是安全和加密的。

  • 相关阅读:
    pikach靶场暴力破解
    Spark调度底层执行原理详解(第35天)
    批量自动html文档排版工具
    hive从入门到放弃(三)——DML数据操作
    轮到国产游戏统治Steam榜单
    华为机试 - 冠亚军排名
    HTML+CSS+JS网页设计期末课程大作业—— 艺术官网17页(包含登陆注册)
    C语言模拟类的宏
    代码随想录刷题|动态规划理论基础 LeetCode 509. 斐波那契数 70. 爬楼梯 746. 使用最小花费爬楼梯
    excel高级绘图技巧100讲(二十二)-如何对不规则数据进行分列
  • 原文地址:https://www.cnblogs.com/renshengdezheli/p/18206809