文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

在 Argo CD 中使用 Sops 增强 GitOps 安全性

2024-12-01 17:14

关注

GitOps 的核心理念就是一切皆代码,意味着用户名、密码、证书、token 等敏感信息也要存储到 Git 仓库中,这显然是非常不安全的,不过我们可以通过 Vault、Keycloak、SOPS 等 Secret 管理工具来解决,最简单的方式是使用 SOPS,因为它可以使用 PGP 密钥来加密内容,如果你使用 kustomize 则还可以在集群内使用相同的 PGP 密钥解密 Secret。ArgoCD 虽然没有内置的 Secret 管理,但是却可以与任何 Secret 管理工具集成。

sops​ 是一款开源的加密文件的编辑器,支持 YAML、JSON、ENV、INI 和 BINARY 格式,同时可以用 AWS KMS、GCP KMS、Azure Key Vault、age 和 PGP 进行加密,官方推荐使用 age​ 来进行加解密,所以我们这里使用 age。age[1] 是一个简单、现代且安全的加密工具(和 Go 库)。

SOPS 与 AGE

首先需要安装 age 工具,可以直接从 Release 页面[2] 下载对应的安装包:

$ wget https://github.91chi.fun/https://github.com//FiloSottile/age/releases/download/v1.0.0/age-v1.0.0-linux-amd64.tar.gz
$ tar -xvf age-v1.0.0-linux-amd64.tar.gz
$ mv age/age /usr/local/bin
$ mv age/age-keygen /usr/local/bin
$ age --version
v1.0.0

然后安装 sops,同样直接从 Release 页面[3]下载对应的安装包:

$ wget https://github.91chi.fun/https://github.com//mozilla/sops/releases/download/v3.7.3/sops-v3.7.3.linux.amd64
$ mv sops-v3.7.3.linux.amd64 sops && chmod +x sops
$ mv sops /usr/local/bin

通过下述命令来查看安装是否成功:

$ sops --version
sops 3.7.3 (latest)

我们先创建一个简单的 Secret 来测试下使用 sops 进行加密:

$ kubectl create secret generic app-secret \
--from-literal=token=SOPS-AGE-TOKEN-TEST \
--dry-run=client \
-o yaml > secret.yaml

生成的 secret 资源清单文件如下所示:

apiVersion: v1
data:
token: U09QUy1BR0UtVE9LRU4tVEVTVA==
kind: Secret
metadata:
name: app-secret

接下来我们使用 age-keygen 命令生成加密的公钥和私钥,可以用如下命令将私钥保存到一个 key.txt 文件中:

$ age-keygen -o key.txt
Public key: age1wvdahagxfgqc53awmmgz52njdk2zm6vkw760tc368gstsypgvusqy7zvtt

然后我们可以使用上面的私钥来加密生成的 secret.yaml 文件:

$ age -o secret.enc.yaml -r age1wvdahagxfgqc53awmmgz52njdk2zm6vkw760tc368gstsypgvusqy7zvtt secret.yaml

加密后生成的 secret.enc.yaml 文件内容如下所示,显示乱码:

age-encryption.org/v1
-> X25519 x8bynJlv6Sz03ks71Jvn92RZQ6IlTj9B8zgU3lJsOFQ
sqrP+zq9nw93mafbBjuc5F6GWIjjzdYtQV6DtV9KiTw
---
6W1cpc//EBqXkF983yVBUBExiYEx/7Y0wEvHjPlmWNg
��NY0Y���^�/A��i��.�N���=�ԦPb�ļ���҈v?-<t�t�
Ӓ/$�Zs�۸�gKz�U���Kf�aϛ�� +
��Y��j��g��IDP>��>g��2m9R�a��qfC�����߻q�n���@�O�'g�P6

同样我们还可以对该加密文件进行解密:

$ age --decrypt -i key.txt secret.enc.yaml
apiVersion: v1
data:
token: U09QUy1BR0UtVE9LRU4tVEVTVA==
kind: Secret
metadata:
creationTimestamp: null
name: app-secret

同样对于 sops 来说也是支持和 age 进行集成的,我们可以使用下面的 sops 命令来对 secret.yaml 文件进行加密:

$ sops --encrypt --age age1wvdahagxfgqc53awmmgz52njdk2zm6vkw760tc368gstsypgvusqy7zvtt secret.yaml > secret.enc.yaml

加密后的文件内容如下所示:

apiVersion: ENC[AES256_GCM,data:e7E=,iv:Pfwj3/74CygAHtWlt9tsnexrH74nfa0teNZzknzfGwA=,tag:U2yJjnalFOuGe8rQK+c7Ng==,type:str]
data:
token: ENC[AES256_GCM,data:8kwq4GqETBJjHbrtS5S3AqJIPcq3Nmf8Gg1muQ==,iv:l7O1UnjzcXOkc48EVvbqGPVv0RQxxNX3aIzCU5B/7/o=,tag:XuNw/N7XDLU17BOQkjn5Rg==,type:str]
kind: ENC[AES256_GCM,data:U4hGrF9C,iv:CloG5/RgWHXN/lNGKHGNxeZJXj8kfjw8OmFAxQblUgY=,tag:gq0wKDUa50odvRNcak+Vig==,type:str]
metadata:
creationTimestamp: null
name: ENC[AES256_GCM,data:PEhXQdE3/vj+bA==,iv:dkWCj5cAqc4IeB2lXdxC7otmCmFn3vGe5s2Ij3uh8ag=,tag:bbUaA1dqXnrLaTnCPVnxpQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1wvdahagxfgqc53awmmgz52njdk2zm6vkw760tc368gstsypgvusqy7zvtt
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwY3JKdmxVM2lFb0NpTjVj
cGdyc3d1QXBvN3RVdzAwMVNVMXZrS3pLYm1zCnZqTGZ5TzVBL2VSay80RkFFTlBC
R1NtZmxoYVlTd2RyWUl1c24wME83K00KLS0tIENlczRGb1QzRCtCeWxpMU9PTXN5
QnpyckI1bmZSSFliMUgyUDlkd0tWalUKK4vKJwcGLsZn5wT9WHh5tvNOEGScOlAb
Fx118rutRK4nVpfIhAvhfS9TDqvhaQ2wFVv3N/a/BhkYpwTrE/cjmQ==
-----END AGE ENCRYPTED FILE----- lastmodified: "2022-08-18T08:04:35Z"
mac: ENC[AES256_GCM,data:/ujRqRKFR/5uqRBGAZzVIsdVR95In18zUrKuHFuJnHrrfRAt4WXzSUTBovIqOaGPQxXvY4jqkWnd7kqlO629CjK3SA6selEb8N6ytN5kGquGUqSYlOAjsnk575VtpMKXIr8jeaGkzJRmU6aEnbPa18kekw0FCX1aP6yubD8Ce2Y=,iv:/bRn1tk7iXplz4OGxqkUGD4UQRRtb5jUnICQyFnT4fg=,tag:kt9CzFye1OXsq+MKXTZeXA==,type:str]
pgp:
- created_at: "2022-08-18T08:04:35Z"
enc: |-
-----BEGIN PGP MESSAGE-----
wcFMA0Eva10jiAHJAQ/+LgUsrJKoo95yCIxbMT1OPjnJhAK/LkIwY9EdHbJewphI
CKwpDwvsrbdpjcmBkCt4sL4S30bPR3qdAjLxJCnGTJPZQzxjOEIzvJNAG5nC3zk/
UVPAWj7nV26CCPMc+/j/GHGwMphoLviMr9et0adtaWILSP0yhMuH8LVzGa04WVEz
AihT849sF/+WrUy4f7axI4Z2IH2mEepSqNZDQR9mmiu+nA9e+QZqsfazLJXRPsNd
2hQn7qSGPZ10bzy9ccA5nO5r1oU2J+GEEMYujur/RL8y5oi3BCSvWc0udfuU0dka
Nn77OA73zS8aziA9pj3D46wgeGYFfX7h2XKytSI15GGTAT7RmM6D2cB9xWzeQncy
4TN0LDvcw/7SRjxY55iDyYHPLTNlMfajKwXoKfeQX5nd0rnZRCovYDoj2OrqZDff
1N25EEWN6MSztZML0eE/k/p7RDBG9bJ6lntXNAXQJRjzhUYeHMnXLc9NCN5P3WdW
Ny155SsGK6n9Ok1SdAolqlOFRKiO8AA+2jPVS7aDUrWktqPCa8hzf/Bm1ttBoYjw
D5Xc5x3IcyZDIISqz/9cQYfiPusZohpGnfwoea5qhvXEY/wM5IwfLdTm8u78djho
HMLFdFUzuprkHZlZlP3HfPbZi5wGpmiqAuYX+i40teOEaQNGhE7HKCJZkAVS0J3S
UQHmBMxL1SL/JGAdSsuddB0liIIriENIxr14W04zeJ+pClxvnzxNYigOYM3Jk8wF
w7zmhD3IvEpSLG0f4a/c486LpNryBBz6qzBZRYqnJ87PQQ==
=K5dC
-----END PGP MESSAGE-----
fp: CCC4D0692165A88405EF1F579CC5737D5CCB9760
unencrypted_suffix: _unencrypted
version: 3.7.3

可以看到主要字段都被加密了。但是其他字段比如 kind 也被加密了,我们可以通过创建一个 .sops.yaml 文件来指定需要被加密的字段,如下所示:

# .sops.yaml
creation_rules:
- encrypted_regex: "^(username|password|)$"
age: "CCC4D0692165A88405EF1F579CC5737D5CCB9760"

这样的话则只会对 username 和 password 两个字段进行加密。

ArgoCD 集成 SOPS

现在我们可以使用 sops​ 来对私密的文件进行加解密了,前面示例中我们在 ArgoCD 中使用的 Helm Chart 方式来同步应用,比如我们会在 values 文件中提供一些比较私密的信息,直接明文提供存储到 Git 仓库上显然是非常不安全的,这个时候我们就可以使用 sops​ 来对这些 values 文件进行加密,当然在同步应用的时候自然就需要 ArgoCD 能够支持对手 SOPS 进行解密了,这里我们还需要使用到 helm-secrets[4] 这个 Helm 插件。

接下来我们需要让 Argo CD 来支持 SOPS,一般来说主要有两种方法:

使用 helm 和 sops 创建自定义的 ArgoCD Docker 镜像,并使用自定义 Docker 镜像,但是 Argo CD 的每个新版本都需要更新该镜像。

在 Argo CD 存储库服务器部署中添加一个初始化容器,以获取带有sops 的 helm 插件,如此处所述,并在 Pod 中使用它。即使更新了 Argo CD 版本,也不需要更新插件,除非插件版本和 Argo CD 版本存在兼容性问题。

为了简单我们这里使用第一种自定义镜像的方式,如下所示的 Dockerfile,它将 sops 和 helm-secrets 集成到 Argo CD 镜像中:

ARG ARGOCD_VERSION="v2.4.9"
FROM argoproj/argocd:$ARGOCD_VERSION
ARG SOPS_VERSION="3.7.3"
ARG VALS_VERSION="0.18.0"
ARG HELM_SECRETS_VERSION="3.15.0"
ARG KUBECTL_VERSION="1.24.3"
# In case wrapper scripts are used, HELM_SECRETS_HELM_PATH needs to be the path of the real helm binary
ENV HELM_SECRETS_HELM_PATH=/usr/local/bin/helm \
HELM_PLUGINS="/home/argocd/.local/share/helm/plugins/" \
HELM_SECRETS_VALUES_ALLOW_SYMLINKS=false \
HELM_SECRETS_VALUES_ALLOW_ABSOLUTE_PATH=false \
HELM_SECRETS_VALUES_ALLOW_PATH_TRAVERSAL=false
USER root
RUN apt-get update && \
apt-get install -y \
curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN curl -fsSL https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl \
-o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl
# sops backend installation
RUN curl -fsSL https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux \
-o /usr/local/bin/sops && chmod +x /usr/local/bin/sops
# vals backend installation
RUN curl -fsSL https://github.com/variantdev/vals/releases/download/v${VALS_VERSION}/vals_${VALS_VERSION}_linux_amd64.tar.gz \
| tar xzf - -C /usr/local/bin/ vals \
&& chmod +x /usr/local/bin/vals
USER 999
RUN helm plugin install --version ${HELM_SECRETS_VERSION} https://github.com/jkroepke/helm-secrets

使用上面的 Dockerfile 重新构建镜像(cnych/argocd:v2.4.9)后,重新替换 ​​argocd-repo-server​​ 应用的镜像,其他组件不需要。

由于默认情况下 ArgoCD 只支持 http:// 和 https:// 作为远程 value 协议,所以我们需要将 helm-secrets 协议也添加到 argocd-cm 这个 ConfigMap 中去。

apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd
name: argocd-cm
data:
helm.valuesFileSchemes: >-
secrets+gpg-import, secrets+gpg-import-kubernetes,
secrets+age-import, secrets+age-import-kubernetes,
secrets,
https

接下来我们还需要配置 Argo CD 存储库服务器,使它可以访问私钥来解密加密的文件。这里使用前面 age-keygen 命令生成的私钥文件 key.txt 创建一个 Kubernetes Secret 对象:

$ kubectl create secret generic helm-secrets-private-keys --from-file=key.txt -n argocd

现在我们需要将该 Secret 以 Volume 的形式挂载到 argocd-repo-server 中去:

volumes:
- name: helm-secrets-private-keys
secret:
secretName: helm-secrets-private-keys
# ......
volumeMounts:
- mountPath: /helm-secrets-private-keys/
name: helm-secrets-private-keys
......

然后更新 argocd-repo-server 组件,更新完成后我们就可以创建如下所示的 Argo CD 应用来对加密文件进行解密了:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app
spec:
source:
helm:
valueFiles:
# Method 1: Mount the gpg key from a kubernetes secret as volume
# secrets+gpg-import:///.asc?
# secrets+age-import:///.txt?
# Example Method 1: (Assumptions: key-volume-mount=/helm-secrets-private-keys, key-name=app, secret.yaml is in the root folder)
- secrets+age-import:///helm-secrets-private-keys/key.txt?secrets.yaml

现在我们再次使用前面的 devops-demo 应用示例进行测试。

devops-demo deploy

我们使用 sops 将要部署的 my-values.yaml 文件进行加密:

$ sops --encrypt --age age1wvdahagxfgqc53awmmgz52njdk2zm6vkw760tc368gstsypgvusqy7zvtt my-values.yaml > my-values.enc.yaml

加密后的文件内容如下所示:

image:
repository: ENC[AES256_GCM,data:ZDnA7yTAe2B+TbcQYhcs4yufLgXJWHzX7IUnYdOXtsqzfEo=,iv:4yn+RkQoTHNVW8Y5yDzHsY2hhpMo8yw6j/uj9g6AvMA=,tag:IPwFo2AfLT7yBwoKrvCLCg==,type:str]
tag: ENC[AES256_GCM,data:koDRtD5NfWn03JJLAZnYYWLgwsJr/kSKtw8WHJoeSLD8Zco4M0Doqw==,iv:DbxefZ03J7dGRviRq2DQHhRkcBiBY5FgSh1lJwjwzEg=,tag:zc6ZL5ObSymSVH+caxUzpA==,type:str]
pullPolicy: ENC[AES256_GCM,data:dJ+xl6llTN2NcEKL,iv:XhX3RGirpJI0Wc1Q/9ld2xWQYqE+6ZLL6laIXEI1unQ=,tag:dDwEUa7nTq9TOkYI2cE0Pg==,type:str]
ingress:
enabled: ENC[AES256_GCM,data:eZB9GA==,iv:p12fWs14ATWke0IiMz0SpAb2rW+ViYcEpGRbOoNt9Uk=,tag:w371uI/KRESNP30eD9rrTQ==,type:bool]
ingressClassName: ENC[AES256_GCM,data:WviAhbo=,iv:Vqx0R8RVWkGipZkR2HZfyOYyZdkc+1fhFEV7AdpI4t0=,tag:fv2hf94svXOQeqfjqXN4gg==,type:str]
path: ENC[AES256_GCM,data:jg==,iv:cRm/OXlGEbNEHhAAm/JpPx5sP9GRmW1fyEAi+SZhfjY=,tag:QAJmQSQ5qWfjnzrm+MWLbQ==,type:str]
hosts:
- ENC[AES256_GCM,data:tb32cnmE1d2qnzzsmG2NzMVOPxkW,iv:RH57dgs0gIS28mB83YX+SQNFNjwoTfPa28YvZsCAJW4=,tag:J7SJXkZKPyydx8NvvCh22w==,type:str]
resources:
limits:
cpu: ENC[AES256_GCM,data:uys2,iv:UfAl2lP2wLzc0GkLcBs33vl4dQqLiXWmoyyucqovuVM=,tag:yXRpMIS11s0iqVZQpJ/Bdw==,type:str]
memory: ENC[AES256_GCM,data:fBHSfog=,iv:lf6fTZfOPlhQVspm2BAl56ps8Q5W6Qz4tMT7A8Au9tA=,tag:XZqHEWEb2qBjWms/qTsAOQ==,type:str]
requests:
cpu: ENC[AES256_GCM,data:MDYW,iv:/j6A3oVQ4HILXFLVAr8Rjcq2CDdHrtPa70uySxQQeBI=,tag:EyWwWl0hFkTWzHFBXndFeA==,type:str]
memory: ENC[AES256_GCM,data:qiwPiRI=,iv:m/oFxJrcdysf26ry7LEcL6IQRRqi5B8Zsjc/YJOkO7c=,tag:3brvdx+dFUN0VyJ6KO8biQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1wvdahagxfgqc53awmmgz52njdk2zm6vkw760tc368gstsypgvusqy7zvtt
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyeHNNTWJhWHZHZERJNnlh
L1FpMkdibERFM2ZtU2FFZ1VhMnYxVG90dUNBCmpTVEk3ODg4aWlhOEY3cDdMSWFW
ZmdoaGtQT3NDU0E0bEZPQlJqNXNuamsKLS0tIHhEcm5memczQTNaVGZzUGNGQmsw
cW1QSDd4dDdwZnI2ZzloM2tGRFJxTW8KMPU93lWiNMMaCfOUANmsv+kfi4R7NAzP
nV2H2EyCTQGsNTeKCS/HkmiSD4/4RLui4Z6TbPf8ALpeGHDH8rVSoA==
-----END AGE ENCRYPTED FILE----- lastmodified: "2022-08-18T08:18:05Z"
mac: ENC[AES256_GCM,data:Z+KJTZRP6L2QEcSG6S43fvqWsROAwEVnQcVkpN/yU1Kk8x0PUXXZkdyJiykQ+7HRBNWJp1wKF1TAlqnrZyUSXx7zl5fZGbalgK8kRKzzTzdSsB+Cp4Km5uYNqWUh+RFtzRVOYwOU7fOsAxiHLFMjzaqLAE6+WsCY9xjfj67NymA=,iv:Kyckp64XCkmpbeSEiampXp47Qr9ZIJRZUWsLDhHIw/4=,tag:/eH5d5e9anLRoiCxdWPS/w==,type:str]
pgp:
- created_at: "2022-08-18T08:18:05Z"
enc: |-
-----BEGIN PGP MESSAGE-----
wcFMA0Eva10jiAHJAQ/9HZJck5xCbIB43fYrmnrMokwQB5HPMMCpl8gw/U4Cz/RD
zs6nlIXhO1U29rQT3s2G9IjfCS0ehfwA6lKGXAuK10jY9HJ7dVthWnKlNsCq35d/
5ZKzKIT2mvK1h6+qYai86FwGyG436nAw198oNvC4d9E46PfBcx7PXP1lRFoOJI7V
St81HwFTWOd88tkPyIfv2XW1bcvWo7Qz8YunNqGriD3SREwgkSlcyIL4neumWAru
YGzTmwEXFjwcTIzel57fI42Qd61wq1p7CKw8njs1pOGucC3uX1b99f1BaeLdQl3C
lJvYrP0SYKJ/JA2kPRkeJHDd39ywI8A/iNOW4nRFxbMoAHdEiwAUg2DOCfMwDgVu
WQiQqTF+7AycdqjpXYjYZ7SI3al6jhcDA2KxvNsPNjT8F5yl3c9MIwMdo/NRoc6G
XNGXqbR+8kChFQiVKCUopbCqHtFaVVV6Ldhk3fB76ht3vgJx9XFR8+KYFLHAezIO
VdzzWqVPv72lO3CkyqHfoL8FwxjNI9KAQkU1T3ETv5YJw7mUWWvdMVee9SVf8Qa1
m3JJGqcRd9kyH/u8tMKsrgfG1/KVeyx1gStlO3ioHlCyjsNBAUZ2QIsFa7gxUmQL
HqgCIqGC/SjFv1+5sHF807sYBBWfARQZRTum/Pg3FHpRiVhNPcvEUPIZjQhT79fS
UQHw1EvK5Wj4Ea3/3jNt9bim+pJrxCoUAKByU8lyjL7vOsogiM7sgp50t54oI/3V
G0hvOZNvWV/V0YLqXoTVEru/rqLUKzHunl9psutAXlUOkA==
=4l27
-----END PGP MESSAGE-----
fp: CCC4D0692165A88405EF1F579CC5737D5CCB9760
unencrypted_suffix: _unencrypted
version: 3.7.3

现在我们需要将该文件重新提交到 Git 仓库中去,接着我们要重新创建 Application 应用,对应的资源清单文件如下所示:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: devops-demo
namespace: argocd
spec:
destination:
namespace: default
server: "https://kubernetes.default.svc"
project: demo
source:
path: helm # Helm 存储库创建应用程序时,chart 必须指定 path
repoURL: "http://git.k8s.local/course/devops-demo-deploy.git"
targetRevision: HEAD
helm:
parameters:
- name: replicaCount
value: "2"
valueFiles:
- secrets+age-import:///helm-secrets-private-keys/key.txt?my-values.enc.yaml

其中核心是 valuesFiles 配置的 secrets+age-import:///helm-secrets-private-keys/key.txt?my-values.enc.yaml,表示导入 /helm-secrets-private-keys/key.txt 文件中的私钥来对 my-values.enc.yaml 文件进行解密。

重新创建上面的对象后,同步应用后正常可以同步成功。

参考资料

[1]age: https://github.com/FiloSottile/age/。

[2]Age Release 页面: https://github.com/FiloSottile/age/releases。

[3]SOPS Release 页面: https://github.com/mozilla/sops/releases/download/v3.7.3/sops-v3.7.3.linux.amd64。

[4]helm-secrets: https://github.com/jkroepke/helm-secrets。

来源:k8s技术圈内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯