Skip to content
/linux-syscalls

Container security · syscall filtering

seccomp & filtrage de syscalls dans les conteneurs

seccomp-BPF est le mécanisme noyau qui permet à un processus de restreindre l'ensemble des appels système qu'il (et ses enfants) peut effectuer. C'est la colonne vertébrale de tout runtime de conteneur moderne et une atténuation peu coûteuse et durable contre des classes entières de mouvements post-exploitation.

Qu'est-ce que seccomp

Un filtre seccomp est un petit programme BPF classique attaché à un processus. À chaque appel système, le noyau exécute le filtre avec le numéro d'appel, l'architecture et les arguments en entrée ; le filtre renvoie une action — autoriser, renvoyer une erreur, tuer le thread, ou notifier un superviseur userspace. Une fois installé (via prctl(PR_SET_SECCOMP) ou seccomp()), le filtre s'applique pour la durée de vie du processus et de tous ses descendants ; il ne peut être que durci, jamais relâché.

Actions de filtrage

  • SECCOMP_RET_ALLOW

    Permet l'appel système normalement. Comportement par défaut pour les appels non explicitement listés.

  • SECCOMP_RET_ERRNO

    Retourne -1 avec errno positionné à la valeur indiquée, sans entrer dans l'appel. Utile pour refuser élégamment une fonctionnalité optionnelle (par ex. mlock pour un conteneur qui ne devrait pas verrouiller de pages).

  • SECCOMP_RET_TRAP

    Délivre SIGSYS au thread appelant. Le gestionnaire peut inspecter siginfo_t->si_syscall pour journaliser et décider.

  • SECCOMP_RET_KILL_PROCESS

    Tue immédiatement tout le groupe de threads. Réglage le plus fort ; préféré en production où un appel refusé indique toujours une compromission.

  • SECCOMP_RET_LOG / NOTIFY

    RET_LOG enregistre l'appel dans l'audit sans bloquer. RET_USER_NOTIF (Linux 5.0+) bloque l'appel et le transmet à un superviseur userspace qui peut l'inspecter et l'émuler — utilisé par les bacs à sable type gVisor.

Profil par défaut Docker / Podman

Docker, Podman et les principaux runtimes Kubernetes livrent un profil seccomp par défaut qui autorise environ 300 appels et bloque le reste. Les blocages par défaut sont essentiellement des choses dont une charge n'a jamais légitimement besoin : manipulation de modules noyau, ptrace avancé, bpf() brut, création d'espaces de noms, administration bas niveau, et variantes ABI obsolètes ou compat.

Bloqué par le profil par défaut (4)

Intégration Kubernetes

Kubernetes expose seccomp via le champ securityContext.seccompProfile sur les Pods et conteneurs (GA en 1.19). RuntimeDefault délègue au profil par défaut du kubelet (le même que Docker / containerd). Localhost pointe vers un profil JSON personnalisé sur le nœud. Unconfined désactive complètement seccomp ; ne jamais utiliser en production.

# Pod-level seccomp via securityContext (Kubernetes 1.19+)
apiVersion: v1
kind: Pod
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault   # use kubelet's default (Docker-like) profile
  containers:
    - name: app
      image: my-app:latest
      securityContext:
        seccompProfile:
          type: Localhost
          localhostProfile: my-profiles/app.json

Écrire un filtre personnalisé

Refuser par défaut avec une petite liste d'autorisations est le motif le plus solide. Le helper libseccomp le rend lisible ; un BPF équivalent peut être généré pour intégration directe :

#include <seccomp.h>
int harden(void) {
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
    /* allow only what we need */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read),       0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write),      0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat),     0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close),      0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
    /* W^X: deny PROT_EXEC anonymous mmap */
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(mmap),
        1, SCMP_A2(SCMP_CMP_MASKED_EQ, PROT_EXEC, PROT_EXEC));
    return seccomp_load(ctx);
}

Toujours tester contre la vraie charge : un appel manquant produit un EPERM difficile à diagnostiquer quelque part en profondeur. Des outils comme docker-slim et falco peuvent extraire l'ensemble utilisé à l'exécution.

Syscalls à filtrer avec soin