Nota di traduzione.: Presentiamo la traduzione dell'articolo di un senior application security engineer della britannica ASOS.com. Con questo dà inizio a una serie di pubblicazioni dedicate all'incremento della sicurezza in Kubernetes tramite l'uso di seccomp. Se l'introduzione piacerà ai lettori, seguiremo l'autore e continueremo con i suoi futuri materiali su questo tema.

Questo articolo è il primo di una serie di pubblicazioni su come creare profili seccomp nello spirito del SecDevOps, senza ricorrere a magie e incantesimi. Nella prima parte parlerò delle basi e dei dettagli interni dell'implementazione di seccomp in Kubernetes.
L'ecosistema Kubernetes offre una varietà sufficiente di modi per garantire la sicurezza e l'isolamento dei container. L'articolo è dedicato alla modalità di calcolo sicuro, nota anche come seccomp. La sua essenza consiste nel filtrare le chiamate di sistema disponibili per l'esecuzione nei container.
Perché è importante? Un container è semplicemente un processo avviato su una certa macchina. E utilizza il kernel insieme ad altre applicazioni. Se i container potessero eseguire qualsiasi chiamata di sistema, molto presto i malware ne approfitterebbero per eludere l'isolamento del container e influenzare altre applicazioni: intercettare informazioni, modificare le impostazioni di sistema, ecc.
I profili seccomp definiscono quali chiamate di sistema devono essere autorizzate o vietate. L'ambiente di esecuzione del container li attiva durante l'avvio, affinché il kernel possa controllarne l'esecuzione. L'uso di tali profili consente di limitare il vettore d'attacco e ridurre i danni nel caso in cui qualche programma all'interno del container (cioè le vostre dipendenze, o le loro dipendenze) inizi a fare ciò che non gli è permesso.
Esploriamo le basi
Un profilo seccomp di base include 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"
}
]
}()
defaultAction definisce il destino predefinito di qualsiasi chiamata di sistema non specificata nella sezione syscalls. Per semplificare il compito, concentriamoci su due valori principali che verranno utilizzati:
-
SCMP_ACT_ERRNO— blocca l'esecuzione della chiamata di sistema, -
SCMP_ACT_ALLOW— permette.
Nella sezione architectures vengono elencate le architetture target. Questo è importante, poiché il filtro applicato a livello di kernel dipende dagli identificatori delle chiamate di sistema e non dai loro nomi scritti nel profilo. Prima dell'applicazione, l'ambiente di esecuzione del container assocerà gli identificatori. Il punto è che le chiamate di sistema possono avere ID completamente diversi a seconda dell'architettura del sistema. Ad esempio, la chiamata di sistema recvfrom (utilizzata per ricevere informazioni da un socket) ha ID = 64 nei sistemi x64 e ID = 517 nei sistemi x86. puoi trovare un elenco di tutte le chiamate di sistema per le architetture x86-x64.
Nella sezione syscalls vengono elencate tutte le chiamate di sistema e viene indicato cosa fare con esse. Ad esempio, è possibile creare una whitelist impostando defaultAction in SCMP_ACT_ERRNO, e assegnare alle chiamate nella sezione syscalls un valore di SCMP_ACT_ALLOW. In questo modo consenti solo le chiamate scritte nella sezione syscalls, e vieti tutte le altre. Per una blacklist, si dovrebbero invertire i valori defaultAction e le azioni.
Ora bisogna dire qualche parola sulle sfumature che non sono così evidenti. Nota che le raccomandazioni qui di seguito sono basate sul presupposto che tu stia deployando una serie di applicazioni business su Kubernetes e ti sia importante che esse funzionino con il minor numero di privilegi.
1. AllowPrivilegeEscalation=false
In securityContext il container ha un parametro AllowPrivilegeEscalation. Se impostato su false, i container verranno avviati con il bit (on) impostato a . Il significato di questo parametro è chiaro dal nome: non permette al container di avviare nuovi processi con privilegi superiori a quelli di cui già dispone.
Un effetto collaterale di questo parametro, se impostato su true (valore predefinito), è che il runtime del container applica il profilo seccomp all'inizio del processo di avvio. Pertanto, tutte le chiamate di sistema necessarie per avviare i processi interni dell'ambiente di esecuzione (ad esempio, impostazione degli identificatori utente/gruppo, scartare alcune capability) devono essere permesse nel profilo.
Al container che esegue un banale echo hi, saranno necessari i seguenti permessi:
{
"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"
}
]
}()
… 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"
}
]
}()
Ma ancora, perché è un problema? Personalmente eviterei di includere nella whitelist le seguenti chiamate di sistema (se non c'è una reale necessità): capset, set_tid_address, setgid, setgroups e setuid. Tuttavia, la vera difficoltà sta nel fatto che, consentendo processi che non controlli affatto, associ i profili all'implementazione dell'ambiente di esecuzione dei container. In altre parole, potresti trovarti ad affrontare il problema che, dopo un aggiornamento dell'ambiente runtime del container (da parte tua o, più probabilmente, da parte del fornitore di servizi cloud), i container smettano improvvisamente di avviarsi.
Consiglio n. 1: Esegui i container con AllowPrivilegeEscaltion=false. Questo ridurrà la dimensione dei profili seccomp e li renderà meno sensibili ai cambiamenti dell'ambiente di esecuzione dei container.
2. Impostazione dei profili seccomp a livello di container
Il profilo seccomp può essere impostato a livello di pod:
annotations:
seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"… o a livello di container:
annotations:
container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"Nota che la sintassi sopra cambierà quando il seccomp di Kubernetes (questo evento è atteso nella prossima versione di Kubernetes — 1.18 — nota del traduttore).
Pochi sanno che in Kubernetes è sempre esistito , a causa del quale i profili seccomp venivano applicati al . L'ambiente di esecuzione compensa parzialmente questa mancanza, tuttavia questo contenitore non scompare dai pod, poiché viene utilizzato per configurare la loro infrastruttura.
Il problema è che questo contenitore si avvia sempre con AllowPrivilegeEscalation=true, portando ai problemi esposti nel punto 1, e non è possibile modificarlo.
Applicando profili seccomp a livello di contenitore, eviti questa trappola e puoi creare un profilo che sarà 'adattato' a un specifico contenitore. Dobbiamo farlo finché gli sviluppatori non risolvono il bug e la nuova versione (forse, 1.18?) non sarà disponibile per tutti gli interessati.
Consiglio n. 2: Imposta profili seccomp a livello di contenitore.
Nel senso pratico, questa regola funge solitamente da risposta universale alla domanda: 'Perché il mio profilo seccomp funziona con docker run, ma non funziona dopo il deployment nel cluster Kubernetes?'.
3. Utilizza runtime/default solo in casi estremi
In Kubernetes ci sono due opzioni di profili incorporati: runtime/default e docker/default. Entrambi sono implementati dall'ambiente di esecuzione del contenitore, e non da Kubernetes. Pertanto, possono variare a seconda dell'ambiente di esecuzione utilizzato e della sua versione.
In altre parole, a causa della modifica del runtime, il contenitore potrebbe accedere a un diverso insieme di syscalls che potrebbe utilizzare o meno. La maggior parte degli ambienti di esecuzione utilizza . Se desideri utilizzare questo profilo, assicurati che sia adatto a te.
Il profilo docker/default è considerato obsoleto a partire da Kubernetes 1.11, quindi evita di utilizzarlo.
A mio parere, il profilo runtime/default è perfettamente adatto agli scopi per cui è stato creato: proteggere gli utenti dai rischi legati all'esecuzione di comandi docker run sulle loro macchine. Tuttavia, per quanto riguarda le applicazioni aziendali che operano in cluster Kubernetes, oserei affermare che tale profilo è troppo aperto e gli sviluppatori dovrebbero concentrarsi sulla creazione di profili per le loro applicazioni (o tipi di applicazioni).
Consiglio n. 3: Crea profili seccomp per applicazioni specifiche. Se ciò non è possibile, occupati di profili per tipi di applicazioni, ad esempio, crea un profilo esteso che includa tutte le API web dell'applicazione in Golang. Utilizza runtime/default solo come ultima risorsa.
Nelle pubblicazioni future parlerò di come creare profili seccomp nello spirito del SecDevOps, automatizzarli e testarli nei pipeline. In altre parole, non avrete più scuse per non passare a profili specifici per le applicazioni.
4. Unconfined — NON è un'opzione
Da è emerso che per impostazione predefinita . Ciò significa che se non impostate PodSecurityPolicy, che lo attivi nel cluster, tutti i pod per i quali non è definito un profilo seccomp opereranno in modalità seccomp=unconfined.
Lavorare in questa modalità significa perdere un intero strato di isolamento che protegge il cluster. Un approccio del genere non è raccomandato dagli esperti di sicurezza.
Consiglio n. 4: Nessun container nel cluster dovrebbe operare in modalità seccomp=unconfined, soprattutto in ambienti di produzione.
5. «Modalità audit»
Questo aspetto non è unico per Kubernetes, ma rientra comunque nella categoria di «cose da sapere prima di iniziare».
È risaputo che la creazione di profili seccomp è sempre stata un compito difficile e si basa in larga misura sul metodo di tentativi ed errori. Infatti, gli utenti non hanno semplicemente la possibilità di testarla in ambienti di produzione senza rischiare di "far crollare" l'applicazione.
Con l'arrivo del kernel Linux 4.14 è diventata possibile l'esecuzione di parti del profilo in modalità audit, registrando nel syslog informazioni su tutte le chiamate di sistema, senza bloccarle. Questa modalità può essere attivata utilizzando il parametro SCMT_ACT_LOG:
SCMP_ACT_LOG: seccomp non influirà sul funzionamento del thread che effettua la chiamata di sistema, a meno che non rientri sotto qualche regola del filtro, tuttavia le informazioni sulla chiamata di sistema saranno registrate nel log.
Ecco una strategia tipica per utilizzare questa funzionalità:
- Consentire le chiamate di sistema necessarie.
- Bloccare le chiamate di sistema di cui si sa che non saranno utili.
- Registrare nel log tutte le altre chiamate.
Un esempio semplificato appare come segue:
{
"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"
}
]
}()
Ma ricorda che è necessario bloccare tutte le chiamate che si sa non verranno utilizzate e che potrebbero potenzialmente danneggiare il cluster. Una buona base per redigere l'elenco è la documentazione ufficiale . Essa spiega in dettaglio quali chiamate di sistema sono bloccate nel profilo predefinito e perché.
Tuttavia, c'è un inghippo. Anche se SCMT_ACT_LOG è supportato dal kernel Linux fin dalla fine del 2017, è entrato nell'ecosistema di Kubernetes solo di recente. Pertanto, per utilizzare questo metodo sarà necessaria una versione del kernel Linux 4.14 e runC di versione non inferiore a .
Consiglio n. 5: È possibile creare un profilo in modalità audit per il test in produzione combinando liste nere e bianche, registrando tutte le eccezioni nel registro.
6. Utilizza liste bianche
La creazione di liste bianche richiede un ulteriore sforzo, poiché è necessario identificare ogni chiamata che potrebbe essere necessaria all'applicazione, tuttavia questo approccio aumenta notevolmente la sicurezza:
Si raccomanda vivamente di utilizzare un approccio basato su liste bianche, poiché è più semplice e affidabile. La lista nera deve essere aggiornata ogni volta che si aggiunge una chiamata di sistema potenzialmente pericolosa (o un flag/opzione pericolosa, se si trovano nella lista nera). Inoltre, spesso è possibile modificare la rappresentazione di un parametro senza cambiarne il contenuto, aggirando così le restrizioni della lista nera.
Per le applicazioni scritte in Go, ho sviluppato uno strumento speciale che affianca 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")
} … eseguiamo gosystract in questo modo:
go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf "%s", .Name}}{{- end}}' application-path… e otterremo il seguente risultato:
"sched_yield",
"futex",
"write",
"mmap",
"exit_group",
"madvise",
"rt_sigprocmask",
"getpid",
"gettid",
"tgkill",
"rt_sigaction",
"read",
"getpgrp",
"arch_prctl",Questo è solo un esempio: ulteriori dettagli sugli strumenti seguiranno.
Consiglio n. 6: Consenti solo le chiamate che ti servono realmente e blocca tutte le altre.
7. Stabilire delle solide basi (o prepararsi a comportamenti imprevedibili)
Il kernel monitorerà il rispetto del profilo indipendentemente da ciò che hai specificato. Anche se potrebbe non essere esattamente ciò che desideravi. Ad esempio, se blocchi l'accesso a chiamate come exit o exit_group, il contenitore non potrà chiudersi correttamente e anche un semplice comando come echo hi verrà bloccato a tempo indefinito. Di conseguenza, avrai un'elevata carico della CPU nel cluster:

In questi casi, può tornarti utile l'utilità strace — ti mostrerà qual è il problema:

sudo strace -c -p 9331
Assicurati che i profili contengano tutte le chiamate di sistema necessarie all'applicazione durante il funzionamento.
Consiglio n. 7: Fai attenzione ai dettagli e verifica che tutte le chiamate di sistema necessarie siano incluse nella whitelist.
Questa è la prima parte del ciclo di articoli sull'uso di seccomp in Kubernetes in stile SecDevOps. Nelle prossime parti parleremo del perché sia importante e di come automatizzare il processo.
P.S. dal traduttore
Leggi anche nel nostro blog:
- «»;
- «»;
- «»;
- «».
Fonte: habr.com
