Seccomp in Kubernetes: 7 cose che devi sapere fin dall'inizio

Nota. trad.: Presentiamo alla vostra attenzione la traduzione di un articolo di un ingegnere senior di sicurezza applicativa presso la società britannica ASOS.com. Con esso inizia una serie di pubblicazioni dedicate al miglioramento della sicurezza in Kubernetes attraverso l'uso di seccomp. Se ai lettori piace l'introduzione, seguiremo l'autore e continueremo con i suoi materiali futuri su questo argomento.

Seccomp in Kubernetes: 7 cose che devi sapere fin dall'inizio

Questo articolo è il primo di una serie di post su come creare profili seccomp nello spirito di SecDevOps, senza ricorrere alla magia e alla stregoneria. Nella Parte XNUMX tratterò le nozioni di base e i dettagli interni dell'implementazione di seccomp in Kubernetes.

L'ecosistema Kubernetes offre un'ampia varietà di modi per proteggere e isolare i container. L'articolo riguarda la modalità di elaborazione sicura, nota anche come seccomp. La sua essenza è filtrare le chiamate di sistema disponibili per l'esecuzione tramite contenitori.

Perché è importante? Un contenitore è solo un processo in esecuzione su una macchina specifica. E utilizza il kernel proprio come le altre applicazioni. Se i contenitori potessero eseguire chiamate di sistema, molto presto il malware ne trarrebbe vantaggio per aggirare l’isolamento del contenitore e influenzare altre applicazioni: intercettare informazioni, modificare le impostazioni di sistema, ecc.

I profili seccomp definiscono quali chiamate di sistema devono essere consentite o disabilitate. Il runtime del contenitore li attiva all'avvio in modo che il kernel possa monitorarne l'esecuzione. L'utilizzo di tali profili consente di limitare il vettore di attacco e ridurre i danni se un programma all'interno del contenitore (ovvero le vostre dipendenze o le loro dipendenze) inizia a fare qualcosa che non gli è consentito fare.

Arrivare alle basi

Il profilo seccomp di base comprende tre elementi: defaultAction, architectures (o archMap) E syscalls:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(medium-basic-seccomp.json)

defaultAction determina il destino predefinito di qualsiasi chiamata di sistema non specificata nella sezione syscalls. Per semplificarci le cose concentriamoci sui due valori principali che verranno utilizzati:

  • SCMP_ACT_ERRNO — blocca l'esecuzione di una chiamata di sistema,
  • SCMP_ACT_ALLOW - consente.

Nella sezione architectures vengono elencate le architetture di destinazione. Questo è importante perché il filtro stesso, applicato a livello del kernel, dipende dagli identificatori delle chiamate di sistema e non dai loro nomi specificati nel profilo. Il runtime del contenitore li abbinerà agli identificatori prima dell'uso. L'idea è che le chiamate di sistema possono avere ID completamente diversi a seconda dell'architettura del sistema. Ad esempio, chiamata di sistema recvfrom (utilizzato per ricevere informazioni dal socket) ha ID = 64 sui sistemi x64 e ID = 517 su x86. Qui puoi trovare un elenco di tutte le chiamate di sistema per le architetture x86-x64.

Nella sezione syscalls elenca tutte le chiamate di sistema e specifica cosa farne. Ad esempio, puoi creare una whitelist impostando defaultAction su SCMP_ACT_ERRNOe chiama nella sezione syscalls assegnare SCMP_ACT_ALLOW. Pertanto, consenti solo le chiamate specificate nella sezione syscallse vietare tutti gli altri. Per la lista nera dovresti cambiare i valori defaultAction e azioni contrarie.

Ora dovremmo spendere qualche parola sulle sfumature che non sono così evidenti. Tieni presente che i consigli seguenti presuppongono che tu stia distribuendo una linea di applicazioni aziendali su Kubernetes e desideri che vengano eseguite con il minor numero di privilegi possibile.

1. EnablePrivilegeEscalation=false

В securityContext il contenitore ha un parametro AllowPrivilegeEscalation. Se è installato in false, i contenitori inizieranno con (on) morso no_new_priv. Il significato di questo parametro è evidente dal nome: impedisce al contenitore di avviare nuovi processi con più privilegi di quanti ne abbia lui stesso.

Un effetto collaterale dell'impostazione di questa opzione true (impostazione predefinita) è che il runtime del contenitore applica il profilo seccomp all'inizio del processo di avvio. Pertanto, tutte le chiamate di sistema necessarie per eseguire processi runtime interni (ad esempio, impostazione di ID utente/gruppo, eliminazione di determinate funzionalità) devono essere abilitate nel profilo.

A un contenitore che fa cose banali echo hi, saranno necessarie le seguenti autorizzazioni:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "capget",
                "capset",
                "chdir",
                "close",
                "execve",
                "exit_group",
                "fstat",
                "fstatfs",
                "futex",
                "getdents64",
                "getppid",
                "lstat",
                "mprotect",
                "nanosleep",
                "newfstatat",
                "openat",
                "prctl",
                "read",
                "rt_sigaction",
                "statfs",
                "setgid",
                "setgroups",
                "setuid",
                "stat",
                "uname",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-pod-seccomp.json)

...invece di questi:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "close",
                "execve",
                "exit_group",
                "futex",
                "mprotect",
                "nanosleep",
                "stat",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-container-seccomp.json)

Ma ancora una volta, perché questo è un problema? Personalmente, eviterei di inserire nella whitelist le seguenti chiamate di sistema (a meno che non ce ne sia una reale necessità): capset, set_tid_address, setgid, setgroups и setuid. Tuttavia, la vera sfida è che consentendo processi su cui non si ha assolutamente alcun controllo, si collegano i profili all'implementazione del runtime del contenitore. In altre parole, un giorno potresti scoprire che dopo aver aggiornato l'ambiente di runtime del contenitore (da parte tua o, più probabilmente, da parte del fornitore di servizi cloud), i contenitori smettono improvvisamente di funzionare.

Suggerimento # 1: esegui i contenitori con AllowPrivilegeEscaltion=false. Ciò ridurrà la dimensione dei profili seccomp e li renderà meno sensibili alle modifiche nell'ambiente di runtime del contenitore.

2. Impostazione dei profili seccomp a livello di contenitore

Il profilo seccomp può essere impostato a livello di pod:

annotations:
  seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"

...o a livello di contenitore:

annotations:
  container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"

Tieni presente che la sintassi precedente cambierà quando Kubernetes seccomp diventerà GA (questo evento è previsto nella prossima release di Kubernetes - 1.18 - trad. ca.).

Pochi sanno che Kubernetes lo ha sempre avuto insettoche ha causato l'applicazione dei profili seccomp contenitore in pausa. L'ambiente runtime compensa parzialmente questa mancanza, ma questo contenitore non scompare dai pod, poiché viene utilizzato per configurare la loro infrastruttura.

Il problema è che questo contenitore inizia sempre con AllowPrivilegeEscalation=true, che porta ai problemi espressi al paragrafo 1, e questo non può essere cambiato.

Utilizzando i profili seccomp a livello di contenitore, si evita questo errore e si può creare un profilo su misura per un contenitore specifico. Questo dovrà essere fatto finché gli sviluppatori non risolveranno il bug e la nuova versione (forse 1.18?) sarà disponibile per tutti.

Suggerimento # 2: imposta i profili seccomp a livello di contenitore.

In senso pratico, questa regola di solito serve come risposta universale alla domanda: “Perché il mio profilo seccomp funziona docker runma non funziona dopo la distribuzione in un cluster Kubernetes?

3. Utilizzare runtime/default solo come ultima risorsa

Kubernetes dispone di due opzioni per i profili integrati: runtime/default и docker/default. Entrambi sono implementati dal runtime del contenitore, non da Kubernetes. Pertanto possono differire a seconda dell'ambiente runtime utilizzato e della sua versione.

In altre parole, come risultato della modifica del runtime, il contenitore potrebbe avere accesso a un diverso insieme di chiamate di sistema, che potrebbe utilizzare o meno. La maggior parte dei runtime utilizza Implementazione della finestra mobile. Se desideri utilizzare questo profilo, assicurati che sia adatto a te.

Profilo docker/default è stato deprecato a partire da Kubernetes 1.11, quindi evita di utilizzarlo.

Secondo me, profilo runtime/default perfettamente adatto allo scopo per cui è stato creato: proteggere gli utenti dai rischi legati all'esecuzione di un comando docker run sulle loro auto. Tuttavia, quando si tratta di applicazioni aziendali in esecuzione su cluster Kubernetes, oserei sostenere che tale profilo è troppo aperto e gli sviluppatori dovrebbero concentrarsi sulla creazione di profili per le loro applicazioni (o tipi di applicazioni).

Suggerimento # 3: Crea profili seccomp per applicazioni specifiche. Se ciò non è possibile, crea profili per i tipi di applicazione, ad esempio crea un profilo avanzato che includa tutte le API Web dell'applicazione Golang. Utilizzare runtime/default solo come ultima risorsa.

Nei post futuri, tratterò di come creare profili seccomp ispirati a SecDevOps, automatizzarli e testarli nelle pipeline. In altre parole, non avrai scuse per non passare ai profili specifici dell'applicazione.

4. Non confinato NON è un'opzione.

Di primo controllo di sicurezza di Kubernetes si è scoperto che per impostazione predefinita seccomp disabilitato. Ciò significa che se non lo imposti PodSecurityPolicy, che lo abiliterà nel cluster, funzioneranno tutti i pod per i quali il profilo seccomp non è definito seccomp=unconfined.

Operare in questa modalità significa perdere un intero strato di isolamento che protegge il cluster. Questo approccio non è consigliato dagli esperti di sicurezza.

Suggerimento # 4: nessun contenitore nel cluster deve essere in esecuzione seccomp=unconfined, soprattutto negli ambienti di produzione.

5. "Modalità di controllo"

Questo punto non è esclusivo di Kubernetes, ma rientra comunque nella categoria "cose ​​da sapere prima di iniziare".

In effetti, la creazione di profili seccomp è sempre stata impegnativa e si basa in gran parte su tentativi ed errori. Il fatto è che gli utenti semplicemente non hanno l'opportunità di testarli in ambienti di produzione senza rischiare di "lasciare cadere" l'applicazione.

Dopo il rilascio del kernel Linux 4.14, è diventato possibile eseguire parti di un profilo in modalità audit, registrando le informazioni su tutte le chiamate di sistema nel syslog, ma senza bloccarle. È possibile attivare questa modalità utilizzando il parametro SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp non influenzerà il thread che effettua la chiamata di sistema se non corrisponde ad alcuna regola nel filtro, ma verranno registrate le informazioni sulla chiamata di sistema.

Ecco una tipica strategia per utilizzare questa funzionalità:

  1. Consenti le chiamate di sistema necessarie.
  2. Blocca le chiamate dal sistema che sai non saranno utili.
  3. Registra le informazioni su tutte le altre chiamate nel registro.

Un esempio semplificato è simile al seguente:

{
    "defaultAction": "SCMP_ACT_LOG",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "names": [
                "add_key",
                "keyctl",
                "ptrace"
            ],
            "action": "SCMP_ACT_ERRNO"
        }
    ]
}

(medio-misto-seccomp.json)

Ricorda però che devi bloccare tutte le chiamate che sai che non verranno utilizzate e che potrebbero potenzialmente danneggiare il cluster. Una buona base per compilare un elenco è quella ufficiale Documentazione Docker. Spiega in dettaglio quali chiamate di sistema sono bloccate nel profilo predefinito e perché.

Tuttavia, c’è un problema. Sebbene SCMT_ACT_LOG supportato dal kernel Linux dalla fine del 2017, è entrato nell’ecosistema Kubernetes solo in tempi relativamente recenti. Pertanto, per utilizzare questo metodo avrai bisogno di un kernel Linux 4.14 e di una versione runC non inferiore v1.0.0-rc9.

Suggerimento # 5: È possibile creare un profilo in modalità di controllo per i test in produzione combinando liste nere e bianche e tutte le eccezioni possono essere registrate.

6. Utilizza le whitelist

L'inserimento nella whitelist richiede uno sforzo aggiuntivo perché è necessario identificare ogni chiamata di cui l'applicazione potrebbe aver bisogno, ma questo approccio migliora notevolmente la sicurezza:

Si consiglia vivamente di utilizzare l'approccio whitelist poiché è più semplice e affidabile. La lista nera dovrà essere aggiornata ogni volta che viene aggiunta una chiamata di sistema potenzialmente pericolosa (o un flag/opzione pericolosa se è sulla lista nera). Inoltre, spesso è possibile modificare la rappresentazione di un parametro senza cambiarne l'essenza e quindi aggirare le restrizioni della lista nera.

Per le applicazioni Go ho sviluppato uno strumento speciale che accompagna l'applicazione e raccoglie tutte le chiamate effettuate durante l'esecuzione. Ad esempio, per la seguente applicazione:

package main

import "fmt"

func main() {
	fmt.Println("test")
}

... lanciamoci gosystract come segue:

go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf ""%s",n" .Name}}{{- end}}' application-path

... e otteniamo il seguente risultato:

"sched_yield",
"futex",
"write",
"mmap",
"exit_group",
"madvise",
"rt_sigprocmask",
"getpid",
"gettid",
"tgkill",
"rt_sigaction",
"read",
"getpgrp",
"arch_prctl",

Per ora, questo è solo un esempio: seguiranno maggiori dettagli sugli strumenti.

Suggerimento # 6: consenti solo le chiamate di cui hai veramente bisogno e blocca tutte le altre.

7. Gettare le basi giuste (o prepararsi a comportamenti inaspettati)

Il kernel applicherà il profilo indipendentemente da ciò che scrivi al suo interno. Anche se non è esattamente quello che volevi. Ad esempio, se blocchi l'accesso alle chiamate come exit o exit_group, il contenitore non sarà in grado di spegnersi correttamente e nemmeno un semplice comando come echo hi appenderloo per un periodo indeterminato. Di conseguenza, otterrai un utilizzo elevato della CPU nel cluster:

Seccomp in Kubernetes: 7 cose che devi sapere fin dall'inizio

In questi casi, un'utilità può venire in soccorso strace - mostrerà quale potrebbe essere il problema:

Seccomp in Kubernetes: 7 cose che devi sapere fin dall'inizio
sudo strace -c -p 9331

Assicurati che i profili contengano tutte le chiamate di sistema necessarie all'applicazione in fase di runtime.

Suggerimento # 7: presta attenzione ai dettagli e assicurati che tutte le chiamate di sistema necessarie siano inserite nella whitelist.

Con questo si conclude la prima parte di una serie di articoli sull'utilizzo di seccomp in Kubernetes nello spirito di SecDevOps. Nelle parti seguenti parleremo del perché questo è importante e di come automatizzare il processo.

PS da traduttore

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento