你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
关键工作负荷的安全注意事项
关键工作负荷本质上需要保护 - 如果应用程序或其基础结构遭到入侵,可用性将面临风险。 此体系结构的关注点是最大程度地提高可靠性,以便应用程序在所有情况下都保持高性能和可用的状态。 应用安全控件主要是为了缓解影响可用性和可靠性的威胁。
注意
业务要求可能会要求采取更多安全措施。 强烈建议根据架构良好的框架中的关键指南:安全性中提供的指南扩展实现中的控件。
标识和访问管理
在应用程序级别,此体系结构使用基于 API 密钥的简单身份验证方案执行一些受限操作,例如创建目录项或删除注释。 高级方案(例如用户身份验证和用户角色)超出了基线体系结构的范围。
如果应用程序需要用户身份验证和帐户管理,请按照 Microsoft 架构良好的框架中概述的原则进行操作。 某些策略包括:使用托管标识提供者、避免自定义标识管理、尽可能使用无密码身份验证,等等。
最低访问权限
配置访问策略,以便用户和应用程序获得履行其职责或实现其功能所需的最低访问级别。 开发人员通常不需要访问生产基础结构,但部署管道需要完全访问权限。 Kubernetes 群集不会将容器映像推送到注册表中,但 GitHub 工作流会。 前端 API 通常不会从消息中转站获取消息,后端辅助角色不一定向中转站发送新消息。 这些决策取决于工作负荷。在决定应分配的访问级别时,要考虑到每个组件的功能都应得到反映。
Azure Mission-Critical 参考实现中的示例:
- 与事件中心配合使用的每个应用程序组件都使用具有 侦听 (
BackgroundProcessor
) 或发送 (CatalogService
) 权限的连接字符串。 该访问级别可确保每个 Pod 只具有实现其功能所需的最低访问权限。 - AKS 代理池的服务主体仅具有 Key Vault 中机密的获取和列出权限,没有更多权限。
- AKS Kubelet 标识仅具有访问全局容器注册表的 AcrPull 权限。
托管标识
若要尽可能提高关键工作负荷的安全性,请避免使用基于服务的机密,例如连接字符串或 API 密钥。 请首选使用托管标识(如果 Azure 服务支持此功能)。
参考实现使用 AKS 代理池的服务分配型托管标识(“Kubelet 标识”)来访问全局 Azure 容器注册表和印花的 Azure Key Vault。 适当的内置角色用于限制访问。 例如,以下 Terraform 代码仅将 AcrPull
角色分配给 Kubelet 标识:
resource "azurerm_role_assignment" "acrpull_role" {
scope = data.azurerm_container_registry.global.id
role_definition_name = "AcrPull"
principal_id = azurerm_kubernetes_cluster.stamp.kubelet_identity.0.object_id
}
机密
访问 Azure 资源时,应尽可能使用“Microsoft Entra 身份验证”而不是密钥。 许多 Azure 服务支持完全禁用密钥身份验证的选项(例如,Azure Cosmos DB、Azure 存储),AKS 支持“Microsoft Entra 工作负荷 ID”。
对于无法使用 Microsoft Entra 身份验证的方案,每个部署戳记都有专用的 Azure 密钥保管库实例来存储密钥。 这些密钥是在部署期间自动创建的,并使用 Terraform 存储在 Key Vault 中。 除了 e2e 环境中的开发人员,没有操作员可以与机密交互。 此外,根据 Key Vault 访问策略的配置,不允许任何用户帐户访问机密。
注意
此工作负荷不使用自定义证书,但适用相同的原则。
在 AKS 群集上,适用于机密存储的 Azure Key Vault 提供程序旨在让应用程序使用机密。 CSI 驱动程序从 Azure Key Vault 加载密钥,并将其以文件形式装载到各个 pod 中。
#
# /src/config/csi-secrets-driver/chart/csi-secrets-driver-config/templates/csi-secrets-driver.yaml
#
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-kv
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
userAssignedIdentityID: {{ .Values.azure.managedIdentityClientId | quote }}
keyvaultName: {{ .Values.azure.keyVaultName | quote }}
tenantId: {{ .Values.azure.tenantId | quote }}
objects: |
array:
{{- range .Values.kvSecrets }}
- |
objectName: {{ . | quote }}
objectAlias: {{ . | lower | replace "-" "_" | quote }}
objectType: secret
{{- end }}
参考实现结合使用 Helm 和 Azure Pipelines 来部署包含 Azure Key Vault 中所有密钥名称的 CSI 驱动程序。 如果 Key Vault 中已装载的机密发生更改,驱动程序还负责对其进行刷新。
在使用者端,两个 .NET 应用程序都使用内置功能从文件中读取配置 (AddKeyPerFile
):
//
// /src/app/AlwaysOn.BackgroundProcessor/Program.cs
// + using Microsoft.Extensions.Configuration;
//
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// Load values from k8s CSI Key Vault driver mount point.
config.AddKeyPerFile(directoryPath: "/mnt/secrets-store/", optional: true, reloadOnChange: true);
// More configuration if needed...
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
组合使用 CSI 驱动程序的自动重新加载和 reloadOnChange: true
可确保 Key Vault 中的密钥发生更改时,新值会装载到群集上。 这并不能保证在应用程序中进行机密轮换。 该实现使用单一实例 Azure Cosmos DB 客户端实例,后者要求 Pod 重启以应用所做的更改。
自定义域和 TLS
基于 Web 的工作负荷应使用 HTTPS 在所有交互级别防止中间人攻击。 例如,从客户端到 API 以及从 API 到 API 的通信。 证书轮换应自动进行,因为证书过期仍然是发生中断或体验降级的常见原因。
参考实现完全支持具有自定义域名(例如 contoso.com
)的 HTTPS,并将相应的配置应用于 int
和 prod
环境。 对于 e2e
环境,也可以添加自定义域,但由于在 Front Door 中将自定义域与 SSL 证书一起使用时 e2e
的生存时间很短且部署时间会增加,因此决定不在参考实现中使用自定义域名。
若要启用部署完全自动化,应通过 Azure DNS 区域管理自定义域。 基础结构部署管道在 Azure DNS 区域中动态创建 CNAME 记录,并自动将这些记录映射到 Azure Front Door 实例。
已启用 Front Door 管理的 SSL 证书,无需手动续订 SSL 证书。 TLS 1.2 配置为最低版本。
#
# /src/infra/workload/globalresources/frontdoor.tf
#
resource "azurerm_frontdoor_custom_https_configuration" "custom_domain_https" {
count = var.custom_fqdn != "" ? 1 : 0
frontend_endpoint_id = "${azurerm_frontdoor.main.id}/frontendEndpoints/${local.frontdoor_custom_frontend_name}"
custom_https_provisioning_enabled = true
custom_https_configuration {
certificate_source = "FrontDoor"
}
}
未使用自定义域预配的环境可以通过默认 Front Door 终结点进行访问,例如 env123.azurefd.net
。
注意
在群集入口控制器上,任何一种情况下都不使用自定义域。 改为使用 Azure 提供的 DNS 名称(例如 [prefix]-cluster.[region].cloudapp.azure.com
)并启用 Let's Encrypt,以便为这些终结点颁发免费的 SSL 证书。
参考实现使用 Jetstack 的 cert-manager
为入口规则自动预配 SSL/TLS 证书(来自 Let's Encrypt)。 更多配置设置(例如 ClusterIssuer
,用于从 Let's Encrypt 请求证书)通过存储在 src/config/cert-manager/chart 中的独立 cert-manager-config
helm chart 进行部署。
此实现使用的是 ClusterIssuer
而不是 Issuer
(正如此文和此文所述),以避免每个命名空间都有颁发者。
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
配置
所有应用程序运行时配置都存储在 Azure Key Vault 中,包括机密和非敏感设置。 配置存储(例如 Azure 应用配置)可用于存储设置,但是,使用单个存储可以减少关键应用程序的潜在故障点数。 使用 Key Vault 进行运行时配置可简化整体实现。
Key Vault 应由部署管道填充。 在实现中,所需的值直接来自 Terraform(例如数据库连接字符串),或以 Terraform 变量的形式从部署管道传递。
单个环境(e2e
、int
、prod
)的基础结构和部署配置存储在源代码存储库的变量文件中。 此方法有两个优点:
- 环境中的所有更改在应用到环境之前都会受到跟踪并会经过部署管道。
- 各个 e2e 环境可以采用不同的配置,因为部署基于分支中的代码。
但管道的敏感值的存储是一个例外。 这些值作为机密存储在 Azure DevOps 变量组中。
容器安全性
所有容器化工作负荷都需要保护容器映像。
参考实现中使用的工作负荷 Docker 容器基于运行时映像(而不是基于 SDK),这样是为了最大程度地减少占用情况和潜在的攻击面。 没有安装其他工具(例如 ping
、wget
或 curl
)。
应用程序以非特权用户 workload
的名义运行,在映像生成过程中创建:
RUN groupadd -r workload && useradd --no-log-init -r -g workload workload
USER workload
参考实现使用 Helm 来打包将各个组件部署在一起所需的 YAML 清单,其中包括其 Kubernetes 部署、服务、自动缩放 (HPA) 配置和安全性上下文。 所有 Helm chart 都包含遵循 Kubernetes 最佳做法的基础安全措施。
这些安全措施包括:
readOnlyFilesystem
:每个容器中的根文件系统/
都设置为只读,以防止容器将内容写入主机文件系统。 此限制可防止攻击者在容器中下载更多工具和持久保存代码。 需要读写访问权限的目录作为卷进行装载。privileged
:所有容器都设置为以非特权方式运行。 将容器以特权方式运行会为容器提供所有功能,还会解除设备控制组控制器强制实施的所有限制。allowPrivilegeEscalation
:防止在某个容器的内部获取的权限超过其父进程。
这些安全措施还尽可能针对第三方容器和 Helm chart(即 cert-manager
)进行配置,并通过 Azure Policy 进行审核。
#
# Example:
# /src/app/charts/backgroundprocessor/values.yaml
#
containerSecurityContext:
privileged: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
每个环境(prod、int、每个 e2e)都有一个 Azure 容器注册表专用实例,全局复制到部署印花的每个区域。
注意
当前参考实现不使用 Docker 映像的漏洞扫描。 建议使用适用于容器注册表的 Microsoft Defender,可以将它与 GitHub Actions 配合使用。
流量入口
Azure Front Door 是此体系结构中的全局负载均衡器。 所有 Web 请求都通过 Front Door 路由,后者会选择适当的后端。 关键应用程序应利用好 Front Door 的其他功能,例如 Web 应用程序防火墙 (WAF)。
Web 应用程序防火墙
一项重要的 Front Door 功能是 Web 应用程序防火墙 (WAF),因为 Front Door 能够检查正在通过的流量。 在预防模式下,将阻止所有可疑请求。 在实现中,配置了两个规则集:Microsoft_DefaultRuleSet
和 Microsoft_BotManagerRuleSet
。
提示
使用 WAF 部署 Front Door 时,建议从检测模式开始,使用自然的最终用户流量密切监视其行为,并微调检测规则。 误报被消除或很少见以后,切换到预防模式。 这是必要的,因为每个应用程序都不相同,某些有效负载可能会被视为恶意负载,但对于该特定工作负荷来说却是完全合法的。
路由
只有经过 Azure Front Door 的那些请求才会被路由到 API 容器(CatalogService
和 HealthService
)。 此行为是使用 Nginx 入口配置强制执行的,该配置会检查 X-Azure-FDID
标头 - 不仅检查该标头是否存在,而且检查它是否是适合特定环境的全局 Front Door 实例的标头。
#
# /src/app/charts/catalogservice/templates/ingress.yaml
#
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
# ...
annotations:
# To restrict traffic coming only through our Front Door instance, we use a header check on the X-Azure-FDID
# The value gets injected by the pipeline. Hence, this ID should be treated as a sensitive value
nginx.ingress.kubernetes.io/modsecurity-snippet: |
SecRuleEngine On
SecRule &REQUEST_HEADERS:X-Azure-FDID \"@eq 0\" \"log,deny,id:106,status:403,msg:\'Front Door ID not present\'\"
SecRule REQUEST_HEADERS:X-Azure-FDID \"@rx ^(?!{{ .Values.azure.frontdoorid }}).*$\" \"log,deny,id:107,status:403,msg:\'Wrong Front Door ID\'\"
# ...
部署管道可确保正确填充此标头,但还需要绕过此限制以进行冒烟测试,因为它们直接探测每个群集,而不通过 Front Door 进行探测。 参考实现使用了冒烟测试是在部署过程中触发的这一事实,因此标头值是已知的,可以添加到冒烟测试 HTTP 请求:
#
# /.ado/pipelines/scripts/Run-SmokeTests.ps1
#
$header = @{
"X-Azure-FDID" = "$frontdoorHeaderId"
"TEST-DATA" = "true" # Header to indicate that posted comments and rating are just for test and can be deleted again by the app
}
安全部署
按照卓越运营所需的基线良好架构原则,所有部署都应完全自动化,除了触发运行或批准入口外,不需要手动步骤。
必须防止可能禁用安全措施的恶意尝试或意外配置错误。 参考实现对基础结构和应用程序部署使用相同的管道,这会强制自动回滚任何潜在的配置偏移,维护基础结构的完整性,并与应用程序代码保持一致。 在下一部署中放弃任何更改。
用于部署的敏感值是在管道运行期间由 Terraform 生成的,或者是作为 Azure DevOps 机密提供的。 这些值受基于角色的访问限制保护。
注意
GitHub 工作流为机密值提供独立存储的类似概念。 机密是加密环境变量,可由 GitHub Actions 使用。
请务必注意管道生成的任何项目,因为这些项目可能包含有关应用程序内部工作的机密值或信息。 参考实现的 Azure DevOps 部署生成两个包含 Terraform 输出的文件:一个用于印花,一个用于全局基础结构。 这些文件不包含会导致直接入侵基础结构的密码。 但是,你可以将它们视为半敏感内容,因为它们会揭示有关基础结构的信息 - 群集 ID、IP 地址、存储帐户名称、Key Vault 名称、Azure Cosmos DB 数据库名称、Front Door 标头 ID 等。
对于利用 Terraform 的工作负荷,需要投入额外的精力来保护状态文件,因为它包含完整的部署上下文,其中包括机密。 状态文件通常存储在某个存储帐户中,该存储帐户应该具有独立于工作负荷的生命周期,你只应从部署管道对其进行访问。 对此文件进行的任何其他访问都应进行记录,并应将警报发送到相应的安全组。
依赖项更新
应用程序使用的库、框架和工具会随时间推移而更新,因此必须定期跟踪这些更新,因为它们通常包含安全修补程序,可能导致攻击者未经授权就访问系统。
参考实现将 GitHub 的 Dependabot 用于 NuGet、Docker、npm、Terraform 和 GitHub Actions 的依赖项更新。 dependabot.yml
配置文件是使用 PowerShell 脚本自动生成的,因为考虑到应用程序的各个部件的复杂性(例如,每个 Terraform 模块都需要单独的条目)。
#
# /.github/dependabot.yml
#
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/src/app/AlwaysOn.HealthService"
schedule:
interval: "monthly"
target-branch: "component-updates"
- package-ecosystem: "docker"
directory: "/src/app/AlwaysOn.HealthService"
schedule:
interval: "monthly"
target-branch: "component-updates"
# ... the rest of the file...
- 更新每月触发一次,因为需要在拥有最新的库和将开销保持在可维护范围内进行折衷。 此外会持续监视关键工具 (Terraform),并手动执行重要更新。
- 拉取请求面向
component-updates
分支,而不是面向main
。 - npm 库配置为仅检查转到已编译应用程序的依赖项,而不检查支持工具(例如
@vue-cli
)。
Dependabot 会为每个更新创建单独的拉取请求 (PR),这会对运营团队造成困扰。 参考实现首先在 component-updates
分支中收集一批更新,然后在 e2e
环境中运行测试,如果成功,则会将另一个 PR 创建到 main
分支中。
防御性编码
API 调用可能会因各种原因(例如代码错误、部署故障、基础结构故障等)而失败。 这种情况下,调用方(客户端应用程序)不应收到广泛的调试信息,因为这可能会为攻击者提供有关应用程序的有用数据点。
参考实现通过仅在失败响应中返回相关 ID 而不共享失败原因(例如异常消息或堆栈跟踪)来演示此原则。 使用此 ID(并借助 Server-Location
标头),操作员就能够使用 Application Insights 来调查事件
//
// Example ASP.NET Core middleware which adds the Correlation ID to every API response.
//
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.Use(async (context, next) =>
{
context.Response.OnStarting(o =>
{
if (o is HttpContext ctx)
{
context.Response.Headers.Add("Server-Name", Environment.MachineName);
context.Response.Headers.Add("Server-Location", sysConfig.AzureRegion);
context.Response.Headers.Add("Correlation-ID", Activity.Current?.RootId);
context.Response.Headers.Add("Requested-Api-Version", ctx.GetRequestedApiVersion()?.ToString());
}
return Task.CompletedTask;
}, context);
await next();
});
// ...
}
下一步
部署参考实现,以便全面了解资源及其配置。