Condividi tramite


Considerazioni sulla sicurezza per carichi di lavoro cruciali

I carichi di lavoro cruciali devono essere intrinsecamente protetti. Se un'applicazione o l'infrastruttura è compromessa, la disponibilità è a rischio. L'obiettivo di questa architettura è ottimizzare l'affidabilità in modo che l'applicazione rimanga efficiente e disponibile in tutte le circostanze. I controlli di sicurezza vengono applicati principalmente allo scopo di mitigare le minacce che influiscono sulla disponibilità e sull'affidabilità.

Nota

I requisiti aziendali potrebbero richiedere più misure di sicurezza. È consigliabile estendere i controlli nell'implementazione in base alle indicazioni fornite in Azure Well-Architected Framework security considerations for mission-critical workloads (Considerazioni sulla sicurezza di Azure Well-Architected Framework per carichi di lavoro cruciali).

Gestione delle identità e dell'accesso

A livello di applicazione, questa architettura usa uno schema di autenticazione semplice basato sulle chiavi API per alcune operazioni limitate, ad esempio la creazione di elementi del catalogo o l'eliminazione di commenti. Gli scenari avanzati, ad esempio l'autenticazione utente e i ruoli utente, non rientrano nell'ambito dell'architettura di base.

Se l'applicazione richiede l'autenticazione utente e la gestione degli account, seguire le indicazioni per la gestione delle identità e degli accessi. Alcune strategie includono l'uso di provider di identità gestite, l'evitare la gestione delle identità personalizzate e l'uso dell'autenticazione senza password, quando possibile.

Accesso con privilegi minimi

Configurare i criteri di accesso in modo che gli utenti e le applicazioni ottengano il livello minimo di accesso necessario per soddisfare la propria funzione. Gli sviluppatori in genere non devono accedere all'infrastruttura di produzione, ma la pipeline di distribuzione richiede l'accesso completo. I cluster Kubernetes non esegono il push delle immagini del contenitore in un registro, ma i flussi di lavoro di GitHub potrebbero. Le API front-end in genere non ricevono messaggi dal broker di messaggi e i ruoli di lavoro back-end non inviano necessariamente nuovi messaggi al broker. Queste decisioni dipendono dal carico di lavoro e dal livello di accesso assegnato deve riflettere la funzionalità di ogni componente.

Gli esempi dell'implementazione di riferimento mission-critical di Azure includono:

  • Ogni componente dell'applicazione che funziona con Hub eventi di Azure usa un stringa di connessione con autorizzazioni Listen (BackgroundProcessor) o Send (CatalogService). Tale livello di accesso garantisce che ogni pod disponga solo dell'accesso minimo necessario per soddisfare la relativa funzione.
  • L'entità servizio per il pool di agenti servizio Azure Kubernetes (servizio Azure Kubernetes) dispone solo delle autorizzazioni Get ed List per i segreti in Azure Key Vault.
  • L'identità Kubelet del servizio Azure Kubelet ha solo l'autorizzazione AcrPull per accedere al registro contenitori globale.

Identità gestite

Per migliorare la sicurezza di un carico di lavoro cruciale, evitare di usare segreti basati su servizi, ad esempio stringa di connessione o chiavi API, quando possibile. È consigliabile usare le identità gestite se il servizio di Azure supporta tale funzionalità.

L'implementazione di riferimento usa un'identità gestita assegnata dal servizio nel pool di agenti del servizio Azure Kubelet per accedere al Registro Azure Container globale e all'insieme di credenziali delle chiavi di un stamp. I ruoli predefiniti appropriati vengono usati per limitare l'accesso. Ad esempio, questo codice Terraform assegna solo il AcrPull ruolo all'identità 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
}

Segreti

Quando possibile, usare l'autenticazione di Microsoft Entra anziché le chiavi quando si accede alle risorse di Azure. Molti servizi di Azure, ad esempio Azure Cosmos DB e Archiviazione di Azure, supportano l'opzione per disabilitare completamente l'autenticazione della chiave. Il servizio Azure Kubernetes supporta ID dei carichi di lavoro di Microsoft Entra.

Per gli scenari in cui non è possibile usare l'autenticazione Microsoft Entra, ogni stamp di distribuzione ha un'istanza dedicata di Key Vault per archiviare le chiavi. Queste chiavi vengono create automaticamente durante la distribuzione e vengono archiviate in Key Vault con Terraform. Nessun operatore umano, ad eccezione degli sviluppatori in ambienti end-to-end, può interagire con i segreti. Inoltre, i criteri di accesso di Key Vault sono configurati in modo che nessun account utente sia autorizzato ad accedere ai segreti.

Nota

Questo carico di lavoro non usa certificati personalizzati, ma si applicano gli stessi principi.

Nel cluster del servizio Azure Kubernetes il provider dell'insieme di credenziali delle chiavi per l'archivio segreti consente all'applicazione di utilizzare i segreti. Il driver CSI carica le chiavi da Key Vault e le monta come file in singoli 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 }}

L'implementazione di riferimento usa Helm con Azure Pipelines per distribuire il driver CSI che contiene tutti i nomi di chiave di Key Vault. Il driver è anche responsabile dell'aggiornamento dei segreti montati se cambiano in Key Vault.

Sul lato consumer, entrambe le applicazioni .NET usano la funzionalità predefinita per leggere la configurazione dai file (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>();
    });

La combinazione del ricaricamento automatico del driver CSI e reloadOnChange: true garantisce che quando le chiavi cambiano in Key Vault, i nuovi valori vengono montati nel cluster. Questo processo non garantisce la rotazione dei segreti nell'applicazione. L'implementazione usa un'istanza client di Azure Cosmos DB singleton che richiede il riavvio del pod per applicare la modifica.

Domini personalizzati e TLS

I carichi di lavoro basati sul Web devono usare HTTPS per impedire attacchi man-in-the-middle su tutti i livelli di interazione, ad esempio la comunicazione dal client all'API o dall'API all'API. Assicurarsi di automatizzare la rotazione dei certificati perché i certificati scaduti sono ancora una causa comune di interruzioni e di esperienze ridotte.

L'implementazione di riferimento supporta completamente HTTPS con nomi di dominio personalizzati, ad esempio contoso.com. Applica anche la configurazione appropriata sia agli ambienti che prod a int . È anche possibile aggiungere domini personalizzati per e2e gli ambienti. Tuttavia, questa implementazione di riferimento non usa nomi di dominio personalizzati a causa della natura di breve durata e dell'aumento del tempo di e2e distribuzione quando si usano domini personalizzati con certificati SSL in Frontdoor di Azure.

Per abilitare l'automazione completa della distribuzione, è necessario gestire il dominio personalizzato tramite una zona DNS di Azure. La pipeline di distribuzione dell'infrastruttura crea dinamicamente record CNAME nella zona DNS di Azure ed esegue automaticamente il mapping di questi record a un'istanza di Frontdoor di Azure.

I certificati SSL gestiti da Frontdoor di Azure sono abilitati, che rimuove il requisito per i rinnovi manuali dei certificati SSL. TLS 1.2 è configurato come versione minima.

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

Gli ambienti di cui non è stato effettuato il provisioning con domini personalizzati sono accessibili tramite l'endpoint predefinito di Frontdoor di Azure. Ad esempio, è possibile raggiungerli in un indirizzo come env123.azurefd.net.

Nota

Nel controller di ingresso del cluster, i domini personalizzati non vengono usati in entrambi i casi. Viene invece usato un nome DNS fornito da Azure, ad [prefix]-cluster.[region].cloudapp.azure.com esempio con Let's Encrypt, che può rilasciare certificati SSL gratuiti per tali endpoint.

L'implementazione di riferimento usa Jetstack per effettuare automaticamente il provisioning dei cert-manager certificati SSL/TLS dalle regole Let's Encrypt for ingress. Altre impostazioni di configurazione, ad esempio ClusterIssuer, che richiede certificati da Let's Encrypt, vengono distribuite tramite un grafico Helm separato cert-manager-config archiviato in src/config/cert-manager/chart.

Questa implementazione usa ClusterIssuer invece di evitare di Issuer avere autorità emittenti per ogni spazio dei nomi. Per altre informazioni, vedere la documentazione di cert-manager e le note sulla versione di cert-manager.

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

Impostazione

Tutte le configurazioni del runtime dell'applicazione vengono archiviate in Key Vault, inclusi segreti e impostazioni senza distinzione. È possibile usare un archivio di configurazione, ad esempio app Azure Configurazione, per archiviare le impostazioni. Tuttavia, la presenza di un singolo archivio riduce il numero di potenziali punti di errore per le applicazioni cruciali. Usare Key Vault per la configurazione di runtime per semplificare l'implementazione complessiva.

Gli insiemi di credenziali delle chiavi devono essere popolati dalla pipeline di distribuzione. Nell'implementazione, i valori obbligatori vengono originati direttamente da Terraform, ad esempio i stringa di connessione di database o passati come variabili Terraform dalla pipeline di distribuzione.

La configurazione dell'infrastruttura e della distribuzione di singoli ambienti, ad esempio e2e, inte prod, viene archiviata in file di variabili che fanno parte del repository del codice sorgente. Questo approccio offre due vantaggi:

  • Tutte le modifiche in un ambiente vengono rilevate e passano attraverso le pipeline di distribuzione prima di essere applicate all'ambiente.
  • I singoli e2e ambienti possono essere configurati in modo diverso perché la distribuzione si basa sul codice in un ramo.

Un'eccezione è l'archiviazione di valori sensibili per le pipeline. Questi valori vengono archiviati come segreti nei gruppi di variabili di Azure DevOps.

Sicurezza dei contenitori

È necessario proteggere le immagini del contenitore per tutti i carichi di lavoro in contenitori.

Questa implementazione di riferimento usa contenitori Docker del carico di lavoro basati su immagini di runtime, non SDK, per ridurre al minimo il footprint e la potenziale superficie di attacco. Non sono installati altri strumenti, ad esempio ping, wgeto curl.

L'applicazione viene eseguita con un utente workload senza privilegi creato come parte del processo di compilazione dell'immagine:

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

L'implementazione di riferimento usa Helm per creare un pacchetto dei manifesti YAML necessari per distribuire singoli componenti. Questo processo include la distribuzione, i servizi, la configurazione della scalabilità automatica orizzontale dei pod e il contesto di sicurezza di Kubernetes. Tutti i grafici Helm contengono misure di sicurezza fondamentali che seguono le procedure consigliate di Kubernetes.

Queste misure di sicurezza sono:

  • readOnlyFilesystem: il file system / radice in ogni contenitore è impostato su sola lettura per impedire al contenitore di scrivere nel file system host. Questa restrizione impedisce agli utenti malintenzionati di scaricare altri strumenti e rendere persistente il codice nel contenitore. Le directory che richiedono l'accesso in lettura/scrittura vengono montate come volumi.
  • privileged: tutti i contenitori sono impostati per l'esecuzione come senza privilegi. L'esecuzione di un contenitore con privilegi offre tutte le funzionalità al contenitore e solleva anche tutte le limitazioni applicate dal controller del gruppo di controllo del dispositivo.
  • allowPrivilegeEscalation: impedisce all'interno di un contenitore di ottenere più privilegi rispetto al processo padre.

Queste misure di sicurezza sono configurate anche per i contenitori non Microsoft e i grafici Helm, come cert-manager quando possibile. È possibile usare Criteri di Azure per controllare queste misure di sicurezza.

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

Ogni ambiente, incluso prod, inte ogni e2e ambiente, dispone di un'istanza dedicata di Registro Container con replica globale in ognuna delle aree in cui vengono distribuiti gli indicatori.

Nota

Questa implementazione di riferimento non usa l'analisi delle vulnerabilità delle immagini Docker. È consigliabile usare Microsoft Defender per i registri contenitori, potenzialmente con GitHub Actions.

Traffico in ingresso

Frontdoor di Azure è il servizio di bilanciamento del carico globale in questa architettura. Tutte le richieste Web vengono instradate tramite Frontdoor di Azure, che seleziona il back-end appropriato. Le applicazioni cruciali devono sfruttare altre funzionalità di Frontdoor di Azure, ad esempio web application firewall (WAF).

Web application firewall

Una funzionalità importante di Frontdoor di Azure è il WAF perché consente a Frontdoor di Azure di controllare il traffico che passa attraverso. Nella modalità Prevenzione tutte le richieste sospette vengono bloccate. Nell'implementazione vengono configurati due set di regole. Questi set di regole sono Microsoft_DefaultRuleSet e Microsoft_BotManagerRuleSet.

Suggerimento

Quando si distribuisce Frontdoor di Azure con WAF, è consigliabile iniziare con la modalità rilevamento . Monitora attentamente il comportamento con il traffico naturale dei clienti e ottimizza le regole di rilevamento. Dopo aver eliminato i falsi positivi o se i falsi positivi sono rari, passare alla modalità prevenzione . Questo processo è necessario perché ogni applicazione è diversa e alcuni payload possono essere considerati dannosi, anche se sono legittimi per quel carico di lavoro specifico.

Routing

Solo le richieste che arrivano tramite Frontdoor di Azure vengono instradate ai contenitori API, ad esempio CatalogService e HealthService. Usare una configurazione di ingresso Nginx per applicare questo comportamento. Verifica la presenza di un'intestazione X-Azure-FDID e se è quella giusta per l'istanza globale di Frontdoor di Azure di un ambiente specifico.

#
# /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\'\"
  # ...

Le pipeline di distribuzione consentono di assicurarsi che questa intestazione sia popolata correttamente, ma deve anche ignorare questa restrizione per i test di fumo perché esegue il probe di ogni cluster direttamente anziché tramite Frontdoor di Azure. L'implementazione di riferimento usa il fatto che i test smoke vengono eseguiti come parte della distribuzione. Questa progettazione consente di conoscere e aggiungere il valore dell'intestazione alle richieste HTTP di smoke test.

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

Distribuzioni sicure

Per seguire i principi di base ben progettato per l'eccellenza operativa, automatizzare completamente tutte le distribuzioni. Non devono essere necessari passaggi manuali tranne per attivare l'esecuzione o approvare un gate.

È necessario evitare tentativi dannosi o configurazioni accidentali che possono disabilitare le misure di sicurezza. L'implementazione di riferimento usa la stessa pipeline per la distribuzione dell'infrastruttura e dell'applicazione, che forza un rollback automatizzato di eventuali deviazioni di configurazione. Questo rollback consente di mantenere l'integrità dell'infrastruttura e l'allineamento con il codice dell'applicazione. Qualsiasi modifica viene rimossa nella distribuzione successiva.

Terraform genera valori sensibili per la distribuzione durante l'esecuzione della pipeline o Azure DevOps li fornisce come segreti. Questi valori sono protetti con restrizioni di accesso in base al ruolo.

Nota

I flussi di lavoro di GitHub offrono un concetto simile di archivi separati per i valori dei segreti. I segreti vengono crittografati e variabili di ambiente che GitHub Actions può usare.

È importante prestare attenzione a tutti gli artefatti prodotti dalla pipeline perché tali artefatti possono potenzialmente contenere valori segreti o informazioni sui lavori interni dell'applicazione. La distribuzione di Azure DevOps dell'implementazione di riferimento genera due file con output Terraform. Un file è per gli stamp e un file è per l'infrastruttura globale. Questi file non contengono password che potrebbero compromettere l'infrastruttura. È tuttavia consigliabile considerare che questi file siano sensibili perché rivelano informazioni sull'infrastruttura, inclusi ID cluster, indirizzi IP, nomi degli account di archiviazione, nomi di Key Vault, nomi di database di Azure Cosmos DB e ID intestazione di Frontdoor di Azure.

Per i carichi di lavoro che usano Terraform, è necessario impegnarsi in modo aggiuntivo per proteggere il file di stato perché contiene il contesto di distribuzione completo, inclusi i segreti. Il file di stato viene in genere archiviato in un account di archiviazione che deve avere un ciclo di vita separato dal carico di lavoro e deve essere accessibile solo da una pipeline di distribuzione. È consigliabile registrare qualsiasi altro accesso a questo file e inviare avvisi al gruppo di sicurezza appropriato.

Sono state aggiornate le dipendenze di sviluppo

Le librerie, i framework e gli strumenti usati dall'applicazione vengono aggiornati nel tempo. È importante completare regolarmente questi aggiornamenti perché spesso contengono correzioni per i problemi di sicurezza che potrebbero concedere agli utenti malintenzionati l'accesso non autorizzato al sistema.

L'implementazione di riferimento usa gli aggiornamenti delle dipendenze Dependabot di GitHub per NuGet, Docker, npm, Terraform e GitHub Actions. Il dependabot.yml file di configurazione viene generato automaticamente con uno script di PowerShell a causa della complessità delle varie parti dell'applicazione. Ad esempio, ogni modulo Terraform richiede una voce separata.

#
# /.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...
  • Gli aggiornamenti vengono attivati ogni mese come compromesso tra avere le librerie più aggiornate e mantenere il sovraccarico gestibile. Inoltre, gli strumenti chiave come Terraform vengono monitorati continuamente e gli aggiornamenti importanti vengono eseguiti manualmente.
  • Le richieste pull (PRS) hanno come destinazione il component-updates ramo anziché main.
  • Le librerie Npm sono configurate per controllare solo le dipendenze che passano all'applicazione compilata anziché a strumenti di supporto come @vue-cli.

Dependabot crea una richiesta pull separata per ogni aggiornamento, che può sovraccaricare il team operativo. L'implementazione di riferimento raccoglie prima un batch di aggiornamenti nel component-updates ramo e quindi esegue i test nell'ambiente e2e . Se questi test hanno esito positivo, crea un'altra richiesta pull destinata al main ramo.

Codifica difensiva

Le chiamate API possono non riuscire a causa di vari motivi, tra cui errori di codice, distribuzioni non funzionanti e errori dell'infrastruttura. Se una chiamata API ha esito negativo, il chiamante o l'applicazione client non dovrebbe ricevere informazioni di debug complete perché tali informazioni potrebbero fornire punti dati utili agli avversari sull'applicazione.

L'implementazione di riferimento illustra questo principio restituendo solo l'ID di correlazione nella risposta non riuscita. Non condivide il motivo dell'errore, ad esempio il messaggio di eccezione o l'analisi dello stack. Usando questo ID e con l'aiuto dell'intestazione Server-Location , un operatore può analizzare l'evento imprevisto usando 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();
    });
    
    // ...
}

Passaggio successivo

Distribuire l'implementazione di riferimento per ottenere una conoscenza completa delle risorse e della relativa configurazione.