Partilhar via


Considerações de segurança para cargas de trabalho de missão crítica

As cargas de trabalho de missão crítica devem ser inerentemente protegidas. Se um aplicativo ou sua infraestrutura for comprometido, a disponibilidade estará em risco. O foco dessa arquitetura é maximizar a confiabilidade para que o aplicativo permaneça eficiente e disponível em todas as circunstâncias. Os controles de segurança são aplicados principalmente com o objetivo de mitigar ameaças que afetam a disponibilidade e a confiabilidade.

Nota

Seus requisitos de negócios podem precisar de mais medidas de segurança. É altamente recomendável que você estenda os controles em sua implementação de acordo com as orientações em Considerações de segurança do Azure Well-Architected Framework para cargas de trabalho de missão crítica.

Gestão de identidades e acessos

No nível do aplicativo, essa arquitetura usa um esquema de autenticação simples baseado em chaves de API para algumas operações restritas, como a criação de itens de catálogo ou a exclusão de comentários. Cenários avançados, como autenticação de usuário e funções de usuário, estão além do escopo da arquitetura de linha de base.

Se seu aplicativo exigir autenticação de usuário e gerenciamento de conta, siga as recomendações para gerenciamento de identidade e acesso. Algumas estratégias incluem o uso de provedores de identidade gerenciados, evitar o gerenciamento de identidades personalizadas e usar autenticação sem senha quando possível.

Acesso com privilégios mínimos

Configure políticas de acesso para que os usuários e aplicativos obtenham o nível mínimo de acesso de que precisam para cumprir sua função. Os desenvolvedores normalmente não precisam de acesso à infraestrutura de produção, mas o pipeline de implantação precisa de acesso total. Os clusters do Kubernetes não enviam imagens de contêiner para um registro, mas os fluxos de trabalho do GitHub podem. As APIs de front-end geralmente não recebem mensagens do agente de mensagens e os trabalhadores de back-end não necessariamente enviam novas mensagens para o broker. Essas decisões dependem da carga de trabalho, e o nível de acesso atribuído deve refletir a funcionalidade de cada componente.

Exemplos da implementação de referência de missão crítica do Azure incluem:

  • Cada componente de aplicativo que funciona com Hubs de Eventos do Azure usa uma cadeia de conexão com permissões Ouvir (BackgroundProcessor) ou Enviar (CatalogService). Esse nível de acesso garante que cada pod tenha apenas o acesso mínimo necessário para cumprir sua função.
  • A entidade de serviço para o pool de agentes do Serviço Kubernetes do Azure (AKS) só tem permissões Obter e Listar para Segredos no Cofre de Chaves do Azure.
  • A identidade AKS Kubelet só tem a permissão AcrPull para acessar o registro de contêiner global.

Identidades geridas

Para melhorar a segurança de uma carga de trabalho de missão crítica, evite usar segredos baseados em serviço, como cadeias de conexão ou chaves de API, quando possível. Recomendamos que você use identidades gerenciadas se o serviço do Azure der suporte a esse recurso.

A implementação de referência usa uma identidade gerenciada atribuída ao serviço no pool de agentes AKS ("identidade Kubelet") para acessar o Registro de Contêiner do Azure global e o cofre de chaves de um carimbo. As funções internas apropriadas são usadas para restringir o acesso. Por exemplo, este código Terraform atribui apenas a AcrPull função à identidade 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
}

Segredos

Quando possível, use a autenticação do Microsoft Entra em vez de chaves ao acessar recursos do Azure. Muitos serviços do Azure, como o Azure Cosmos DB e o Armazenamento do Azure, oferecem suporte à opção de desabilitar completamente a autenticação de chave. O AKS suporta o ID de carga de trabalho do Microsoft Entra.

Para cenários em que não é possível usar a autenticação do Microsoft Entra, cada carimbo de implantação tem uma instância dedicada do Cofre da Chave para armazenar chaves. Essas chaves são criadas automaticamente durante a implantação e são armazenadas no Cofre de Chaves com Terraform. Nenhum operador humano, exceto desenvolvedores em ambientes de ponta a ponta, pode interagir com segredos. Além disso, as políticas de acesso ao Cofre da Chave são configuradas para que nenhuma conta de usuário tenha permissão para acessar segredos.

Nota

Essa carga de trabalho não usa certificados personalizados, mas os mesmos princípios se aplicam.

No cluster AKS, o Key Vault Provider for Secrets Store permite que o aplicativo consuma segredos. O driver CSI carrega chaves do Key Vault e as monta como arquivos em pods individuais.

#
# /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 }}

A implementação de referência usa o Helm com o Azure Pipelines para implantar o driver CSI que contém todos os nomes de chave do Cofre da Chave. O driver também é responsável por atualizar os segredos montados se eles mudarem no Cofre da Chave.

No lado do consumidor, ambos os aplicativos .NET usam o recurso interno para ler a configuração de arquivos (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 Kubernetes 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>();
    });

A combinação da recarga automática do driver CSI ajuda reloadOnChange: true a garantir que, quando as chaves forem alteradas no Cofre de Chaves, os novos valores sejam montados no cluster. Esse processo não garante a rotação secreta no aplicativo. A implementação usa uma instância de cliente singleton do Azure Cosmos DB que requer que o pod seja reiniciado para aplicar a alteração.

Domínios personalizados e TLS

As cargas de trabalho baseadas na Web devem usar HTTPS para evitar ataques man-in-the-middle em todos os níveis de interação, como a comunicação do cliente para a API ou da API para a API. Certifique-se de automatizar a rotação de certificados, pois os certificados expirados ainda são uma causa comum de interrupções e experiências degradadas.

A implementação de referência suporta totalmente HTTPS com nomes de domínio personalizados, como contoso.com. Ele também aplica a configuração apropriada para ambos os int prod ambientes. Você também pode adicionar domínios personalizados para e2e ambientes. No entanto, essa implementação de referência não usa nomes de domínio personalizados devido à natureza de curta duração e ao aumento do tempo de e2e implantação quando você usa domínios personalizados com certificados SSL na Porta da Frente do Azure.

Para habilitar a automação completa da implantação, você deve gerenciar o domínio personalizado por meio de uma Zona DNS do Azure. O pipeline de implantação de infraestrutura cria dinamicamente registros CNAME na zona DNS do Azure e mapeia esses registros automaticamente para uma instância do Azure Front Door.

Os certificados SSL gerenciados pela Porta da Frente do Azure estão habilitados, o que remove a necessidade de renovações manuais de certificados SSL. TLS 1.2 é configurado como a versão mínima.

#
# /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"
  }
}

Os ambientes que não são provisionados com domínios personalizados podem ser acessados por meio do ponto de extremidade padrão do Azure Front Door. Por exemplo, você pode contatá-los em um endereço como env123.azurefd.net.

Nota

No controlador de entrada de cluster, os domínios personalizados não são usados em nenhum dos casos. Em vez disso, um nome DNS fornecido pelo Azure, como [prefix]-cluster.[region].cloudapp.azure.com é usado com Let's Encrypt, que pode emitir certificados SSL gratuitos para esses pontos de extremidade.

A implementação de referência usa Jetstack's cert-manager para provisionar automaticamente certificados SSL/TLS do Let's Encrypt para regras de entrada. Mais definições de configuração, como o ClusterIssuer, que solicita certificados do Let's Encrypt, são implantadas por meio de um gráfico de leme separado cert-manager-config que é armazenado em src/config/cert-manager/chart.

Essa implementação usa ClusterIssuer em vez de Issuer evitar ter emissores para cada namespace. Para obter mais informações, consulte a documentação do cert-manager e as notas de versão do cert-manager.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:

Configuração

Toda a configuração do tempo de execução do aplicativo é armazenada no Cofre da Chave, incluindo segredos e configurações não confidenciais. Você pode usar um repositório de configuração, como a Configuração de Aplicativo do Azure, para armazenar as configurações. No entanto, ter um único armazenamento reduz o número de potenciais pontos de falha para aplicativos de missão crítica. Use o Key Vault para configuração de tempo de execução para simplificar a implementação geral.

Os cofres de chaves devem ser preenchidos pelo pipeline de implantação. Na implementação, os valores necessários são originados diretamente do Terraform, como cadeias de conexão de banco de dados, ou passados como variáveis Terraform do pipeline de implantação.

A configuração de infraestrutura e implantação de ambientes individuais, como e2e, inte prod, é armazenada em arquivos variáveis que fazem parte do repositório de código-fonte. Esta abordagem tem duas vantagens:

  • Todas as alterações em um ambiente são controladas e passam por pipelines de implantação antes de serem aplicadas ao ambiente.
  • Ambientes individuais e2e podem ser configurados de forma diferente porque a implantação é baseada em código em uma ramificação.

Uma exceção é o armazenamento de valores sensíveis para pipelines. Esses valores são armazenados como segredos em grupos de variáveis do Azure DevOps.

Segurança do contentor

É necessário proteger imagens de contêiner para todas as cargas de trabalho em contêineres.

Essa implementação de referência usa contêineres do Workload Docker baseados em imagens de tempo de execução, não SDK, para minimizar o espaço ocupado e a superfície de ataque potencial. Não há outras ferramentas, como ping, wgetou curl, instaladas.

O aplicativo é executado sob um usuário workload não privilegiado que foi criado como parte do processo de compilação da imagem:

RUN groupadd -r workload && useradd --no-log-init -r -g workload workload
USER workload

A implementação de referência usa o Helm para empacotar os manifestos do YAML de que ele precisa implantar componentes individuais. Esse processo inclui sua implantação do Kubernetes, serviços, configuração de Autoscaling de Pod Horizontal e contexto de segurança. Todos os gráficos Helm contêm medidas de segurança fundamentais que seguem as práticas recomendadas do Kubernetes.

Estas medidas de segurança são:

  • readOnlyFilesystem: O sistema / de arquivos raiz em cada contêiner é definido como somente leitura para impedir que o contêiner grave no sistema de arquivos host. Essa restrição impede que os invasores baixem mais ferramentas e persistam o código no contêiner. Os diretórios que exigem acesso de leitura e gravação são montados como volumes.
  • privileged: Todos os contêineres são definidos para serem executados como não privilegiados. A execução de um contêiner como privilegiado dá todos os recursos ao contêiner e também elimina todas as limitações impostas pelo controlador do grupo de controle de dispositivo.
  • allowPrivilegeEscalation: Impede que o interior de um contêiner ganhe mais privilégios do que seu processo pai.

Essas medidas de segurança também são configuradas para contêineres que não são da Microsoft e gráficos Helm, como cert-manager quando possível. Você pode usar a Política do Azure para auditar essas medidas de segurança.

#
# Example:
# /src/app/charts/backgroundprocessor/values.yaml
#
containerSecurityContext:
  privileged: false
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

Cada ambiente, incluindo prod, inte cada e2e ambiente, tem uma instância dedicada do Registro de Contêiner que tem replicação global para cada uma das regiões onde os carimbos são implantados.

Nota

Esta implementação de referência não usa a verificação de vulnerabilidades de imagens do Docker. Recomendamos que você use o Microsoft Defender para registros de contêiner, potencialmente com o GitHub Actions.

Entrada de tráfego

O Azure Front Door é o balanceador de carga global nesta arquitetura. Todas as solicitações da Web são roteadas pelo Azure Front Door, que seleciona o back-end apropriado. Os aplicativos de missão crítica devem aproveitar outros recursos do Azure Front Door, como firewalls de aplicativos Web (WAFs).

Firewall de aplicações Web

Um recurso importante do Azure Front Door é o WAF porque ele permite que o Azure Front Door inspecione o tráfego que está passando. No modo de prevenção, todas as solicitações suspeitas são bloqueadas. Na implementação, dois conjuntos de regras são configurados. Esses conjuntos de regras são Microsoft_DefaultRuleSet e Microsoft_BotManagerRuleSet.

Gorjeta

Ao implantar o Azure Front Door com WAF, recomendamos que você comece com o modo de Deteção . Monitore de perto seu comportamento com o tráfego natural de clientes e ajuste as regras de deteção. Depois de eliminar os falsos positivos, ou se os falsos positivos forem raros, mude para o modo de Prevenção . Esse processo é necessário porque cada aplicativo é diferente e algumas cargas úteis podem ser consideradas mal-intencionadas, mesmo que sejam legítimas para essa carga de trabalho específica.

Encaminhamento

Somente as solicitações que chegam pelo Azure Front Door são roteadas para os contêineres de API, como CatalogService e HealthService. Use uma configuração de ingresso Nginx para ajudar a impor esse comportamento. Ele verifica a presença de um X-Azure-FDID cabeçalho e se ele é o correto para a instância global do Azure Front Door de um ambiente específico.

#
# /src/app/charts/catalogservice/templates/ingress.yaml
#
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  # ...
  annotations:
    # To restrict traffic coming only through our Azure Front Door instance, we use a header check on the X-Azure-FDID.
    # The pipeline injects the value. Therefore, it's important to treat this ID 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\'\"
  # ...

Os pipelines de implantação ajudam a garantir que esse cabeçalho seja preenchido corretamente, mas ele também precisa ignorar essa restrição para testes de fumaça, pois eles sondam cada cluster diretamente em vez de por meio da Porta da Frente do Azure. A implementação de referência usa o fato de que os testes de fumaça são executados como parte da implantação. Esse design permite que o valor do cabeçalho seja conhecido e adicionado às solicitações HTTP do teste de fumaça.

#
# /.ado/pipelines/scripts/Run-SmokeTests.ps1
#
$header = @{
  "X-Azure-FDID" = "$frontdoorHeaderId"
  "TEST-DATA"  = "true" # Header to indicate that posted comments and ratings are for tests and can be deleted again by the app.
}

Implantações seguras

Para seguir os princípios básicos bem arquitetados para excelência operacional, automatize totalmente todas as implantações. Eles não devem exigir etapas manuais, exceto para acionar a corrida ou aprovar um portão.

Você deve evitar tentativas maliciosas ou configurações incorretas acidentais que possam desativar as medidas de segurança. A implementação de referência usa o mesmo pipeline para a implantação de infraestrutura e aplicativo, o que força uma reversão automatizada de qualquer desvio de configuração potencial. Essa reversão ajuda a manter a integridade da infraestrutura e o alinhamento com o código do aplicativo. Qualquer alteração é descartada na próxima implantação.

O Terraform gera valores confidenciais para implantação durante a execução do pipeline ou o Azure DevOps os fornece como segredos. Esses valores são protegidos com restrições de acesso baseadas em função.

Nota

Os fluxos de trabalho do GitHub fornecem um conceito semelhante de armazenamentos separados para valores secretos. Os segredos são variáveis ambientais criptografadas que as Ações do GitHub podem usar.

É importante prestar atenção a quaisquer artefatos que o pipeline produza, pois esses artefatos podem potencialmente conter valores secretos ou informações sobre o funcionamento interno do aplicativo. A implantação do Azure DevOps da implementação de referência gera dois arquivos com saídas Terraform. Um arquivo é para carimbos e um arquivo é para infraestrutura global. Esses arquivos não contêm senhas que possam comprometer a infraestrutura. No entanto, você deve considerar esses arquivos confidenciais porque eles revelam informações sobre a infraestrutura, incluindo IDs de cluster, endereços IP, nomes de contas de armazenamento, nomes de Cofre de Chaves, nomes de banco de dados do Azure Cosmos DB e IDs de cabeçalho da Porta da Frente do Azure.

Para cargas de trabalho que usam o Terraform, você precisa fazer um esforço extra para proteger o arquivo de estado, pois ele contém contexto de implantação completo, incluindo segredos. O arquivo de estado normalmente é armazenado em uma conta de armazenamento que deve ter um ciclo de vida separado da carga de trabalho e só deve ser acessível a partir de um pipeline de implantação. Você deve registrar qualquer outro acesso a esse arquivo e enviar alertas para o grupo de segurança apropriado.

Atualizações de dependência

Bibliotecas, estruturas e ferramentas que o aplicativo usa são atualizadas ao longo do tempo. É importante concluir essas atualizações regularmente porque elas geralmente contêm correções para problemas de segurança que podem dar aos invasores acesso não autorizado ao sistema.

A implementação de referência usa o Dependabot do GitHub para atualizações de dependência NuGet, Docker, npm, Terraform e GitHub Actions. O dependabot.yml arquivo de configuração é gerado automaticamente com um script do PowerShell devido à complexidade das várias partes do aplicativo. Por exemplo, cada módulo Terraform precisa de uma entrada separada.

#
# /.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...
  • As atualizações são acionadas mensalmente como um compromisso entre ter as bibliotecas mais atualizadas e manter a sobrecarga sustentável. Além disso, ferramentas importantes como Terraform são monitoradas continuamente e atualizações importantes são executadas manualmente.
  • As solicitações pull (PRs) visam a component-updates ramificação em vez de main.
  • As bibliotecas Npm são configuradas para verificar apenas as dependências que vão para o aplicativo compilado em vez de para ferramentas de suporte como @vue-cli.

O Dependabot cria um PR separado para cada atualização, o que pode sobrecarregar a equipe de operações. A implementação de referência primeiro coleta um lote de atualizações na ramificação e, em component-updates seguida, executa testes no e2e ambiente. Se esses testes forem bem-sucedidos, ele criará outro PR direcionado à main ramificação.

Codificação defensiva

As chamadas de API podem falhar devido a vários motivos, incluindo erros de código, implantações com mau funcionamento e falhas de infraestrutura. Se uma chamada de API falhar, o chamador ou o aplicativo cliente não deverá receber informações de depuração extensas, pois essas informações podem fornecer aos adversários pontos de dados úteis sobre o aplicativo.

A implementação de referência demonstra esse princípio retornando apenas a ID de correlação na resposta com falha. Ele não compartilha o motivo da falha, como mensagem de exceção ou rastreamento de pilha. Usando essa ID e com a ajuda do Server-Location cabeçalho, um operador pode investigar o incidente usando o 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();
    });
    
    // ...
}

Próximo passo

Implante a implementação de referência para obter uma compreensão completa dos recursos e sua configuração.