Introduction
Suite à cet article 1 où je mets en place un relai Nostr accessible publiquement, examinons plus en détails la sécurité de l’application.
Je ne suis pas un expert en cybersécurité et ce qui est décrit ici ne peut être considéré exhaustif dans la quête d’une sécurisation complète d’un système informatique.
Le raisonnement que je propose est d’avoir une vision globale en décrivant l’architecture du logiciel, puis d’utiliser des outils qui facilitent son suivi. Ensuite vient une analyse qui se divise en deux : une partie statique appliquée lors de la conception de l’application, puis une partie dynamique, appliquée lorsque l’application est en cours d’utilisation.
L’écosystème
Commençons par bien définir l’écosystème technique. Plus le périmètre technique sera défini et identifié, plus on réduira les incompréhensions ou omissions et les surfaces d’attaques.
Reprenons le schéma proposé dans l’article référé 1.
Ce qui nous donne la matrice de flux suivante :
Nom | Source | Destination | Détails | Commentaires |
---|---|---|---|---|
Flux entrant | Internet | Reverse Proxy, port 80 | HTTP | Gestion des connexions HTTP |
Flux entrant sécurisé | Internet | Reverse Proxy, port 443 | HTTPS | Gestion des connexions HTTPs, avec certificat |
Flux sortant sécurisé | Reverse Proxy, port 443 | Internet | HTTPS | Gestion des connexions HTTPs, avec certificat |
Redirection vers l’application | Reverse Proxy | Relai Nostr, port 8001 | HTTP | Transition en clair dans l’écosystème local |
Les connexions réseaux sont chiffrées via le reverse proxy, et assurent l’authenticité des interlocuteurs, ainsi que la confidentialité et l’intégrité des échanges.
L’application est administrée via la plateforme Docker engine en mode swarm 2, sur un système Debian. La version de Docker engine est 24.0.7
, celle de containerd
1.6.26
tandis que l’actuelle de la runtime bas niveau runc
est 1.1.10
. Attention à cette faille récemment découverte3, il faut passer à la version >= 1.1.12
!
$docker version
Server: Docker Engine - Community
Engine:
Version: 24.0.7
OS/Arch: linux/amd64
containerd:
Version: 1.6.26
runc:
Version: 1.1.10
docker-init:
Version: 0.19.0
L’application est lancée via un service Docker, et tout cette stack réside sur une distribution GNU/Linux Debian 11
. Point important : pas de CAPS Docker spécifiques à déclarer.
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 11 (bullseye)
Release: 11
Codename: bullseye
Les informations plus bas-niveau de la machine hôte sont les suivantes :
$ uname -a
Linux machine.antoine.local 5.10.X #1 SMP Debian 5.10.X x86_64 GNU/Linux
➡ On dispose d’une vue globale des différentes couches que composent le système. Ces informations restent nécessaires à la compréhension globale même si elles restent en dehors de la partie applicative.
L’application
Deux modules composent le logiciel final : le relai et le reverse proxy.
Le relai Nostr
Il est basé sur Python 3.11. Le gestionnaire de packages pip
installe les dépendances.
On utilise Docker pour packager le logiciel, dont voici le Dockerfile
.
FROM python:3.11.1-slim
WORKDIR /app/
RUN apt-get update
RUN apt-get install build-essential -y
RUN apt-get install pkg-config -y --no-install-recommends
COPY pyproject.toml requirements.txt /app/
RUN pip install -r requirements.txt
COPY pyrelay /app/pyrelay
ENV PYTHONPATH=/app
La liste des dépendances est la suivante 4:
attr==0.3.2
attrs==22.2.0
pydantic==1.10.4
secp256k1==0.14.0
SQLAlchemy==1.4.46
websockets==10.4
alembic==1.9.1
aiosqlite==0.18.0
Le relai Nostr stocke les informations des différents clients dans une base de données locale SQLite
, utilisant le module asyncio
. Les connexions clients - relais sont réalisées via des websockets
.
Le reverse proxy
Il est basé sur l’image Docker nginx-certbot:5.0.0
. On n’a pas besoin de construire l’image nous-même, elle est présente en ligne. Il suffira de la télécharger pour l’analyser. C’est une image construite à partir d’une autre image Docker : nginx:1.25.3
5.
$ docker pull jonasal/nginx-certbot:5.0.0
➡ De même pour l’application, on dispose maintenant d’une vue globale des différents modules qui la composent.
La recherche de vulnérabilités
J’ai précédemment mentionné 6 l’utilitaire Grype
qui se limite à l’analyse de vulnérabilités d’images Docker et filesystems. Pour cet article je propose d’utiliser Trivy
7 qui embarque d’autres analyses telles que les SBOM
, les configurations Docker et Kubernetes.
Proposant des analyses assez complètes allant des packages systèmes jusqu’aux licences logicielles, voici son périmètre d’action 8 :
- OS Packages
- Language-specific Packages
- IaC files
- Kubernetes clusters
Concernant les vulnérabilités, Trivy maintient une base de données des vulnérabilités connues, puis fait un matching sur les composants analysés. Le téléchargement de cette base de données est possible via la commande 9:
$ trivy image --download-db-only
Voici les différentes sources d’informations :
Type de détection | Source |
---|---|
OS Packages | Multiples sources depending of target, see here |
Languages Packages | Multiples sources depending of target, see here |
Kubernetes | Kubernetes official CVE feed |
Attention à la notation de sévérité : Trivy donne la sévérité affichée par les “Vendors” (les distributeurs). Exemple : The severity is taken from the selected data source since the severity from vendors is more accurate. Using CVE-2023-0464 as an example, while it is rated as “HIGH” in NVD, Red Hat has marked its ‘Impact’ as “Low”. As a result, Trivy will display it as “Low”. The severity depends on the compile option, the default configuration, etc. NVD doesn’t know how the vendor distributes the software. Red Hat evaluates the severity more accurately. That’s why Trivy prefers vendor scores over NVD.
Installons Trivy et voyons les informations que l’on peut en tirer.
Sur Debian, on exécute le code suivant.
$ wget https://github.com/aquasecurity/trivy/releases/download/v0.48.3/trivy_0.48.3_Linux-64bit.deb
On vérifie le checksum
$ wget https://github.com/aquasecurity/trivy/releases/download/v0.48.3/trivy_0.48.3_checksums.txt && grep "trivy_0.48.3_Linux-64bit.deb" trivy_0.48.3_checksums.txt
17a1721063c0af9eb51a49f65f4814512b22ed8e00995a73de6379e0a9480449 trivy_0.48.3_Linux-64bit.deb
$ sha256sum trivy_0.48.3_Linux-64bit.deb
17a1721063c0af9eb51a49f65f4814512b22ed8e00995a73de6379e0a9480449 trivy_0.48.3_Linux-64bit.deb
Et on finit par installer.
$ sudo dpkg -i trivy_0.48.3_Linux-64bit.deb
Analyse du relai Nostr
Analyse du Dockerfile
De nombreux conseils sont fournis par l’analyse du Dockerfile.
$ trivy config Dockerfile
Tests: 26 (SUCCESSES: 22, FAILURES: 4, EXCEPTIONS: 0)
Failures: 4 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 3, CRITICAL: 0)
HIGH: Specify at least 1 USER command in Dockerfile with non-root user as argument
════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.
See https://avd.aquasec.com/misconfig/ds002
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
HIGH: The instruction 'RUN <package-manager> update' should always be followed by '<package-manager> install' in the same RUN statement.
════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
The instruction 'RUN <package-manager> update' should always be followed by '<package-manager> install' in the same RUN statement.
See https://avd.aquasec.com/misconfig/ds017
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Dockerfile:5
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
5 [ RUN apt-get update
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
LOW: Add HEALTHCHECK instruction in your Dockerfile
════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers.
See https://avd.aquasec.com/misconfig/ds026
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
HIGH: '--no-install-recommends' flag is missed: 'apt-get install build-essential -y'
════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
'apt-get' install should use '--no-install-recommends' to minimize image size.
See https://avd.aquasec.com/misconfig/ds029
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Dockerfile:6
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
6 [ RUN apt-get install build-essential -y
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
➡ On peut facilement appliquer ces recommandations qui tendent à sécuriser l’exécution des processus, limiter la taille de l’image générée et améliorer la conformité du conteneur instancié.
Analyse statique
Dans la mesure où cette application est développée ici, on a la main sur son cycle de vie et sur la gestion granulaire des dépendances. Comptons déjà toutes les vulnérabilités. Pour cela, mettons en place un template Trivy10. Nous formatons facilement les résultats. Voici son contenu 11.
{{- $critical := 0 }}
{{- $high := 0 }}
{{- $medium := 0 }}
{{- $low := 0 }}
{{- range . }}
{{- range .Vulnerabilities }}
{{- if eq .Severity "CRITICAL" }}{{- $critical = add $critical 1 }}{{- end }}
{{- if eq .Severity "HIGH" }}{{- $high = add $high 1 }}{{- end }}
{{- if eq .Severity "MEDIUM" }}{{- $medium = add $medium 1 }}{{- end }}
{{- if eq .Severity "LOW" }}{{- $low = add $low 1 }}{{- end }}
{{- end }}
{{- end }}
critical, high, medium, low
{{ $critical }}, {{ $high }}, {{ $medium }}, {{ $low }}
Et le résultat est donné par la commande suivante.
$ trivy image pyrelay --format template --template "@count_vulnerabilities.tpl" 2>/dev/null
critical, high, medium, low
1, 63, 115, 442
Le nombre de vulnérabilités est assez important. Mais cette information n’est pas assez précise pour statuer complètement. Voyons lesquelles ont été corrigées et que nous pouvons peut-être appliquer.
On lance une nouvelle recherche, on ne retiendra que le statut 12 fixed
, pour savoir si l’on peut mettre à jour le composant concerné. Filtrons également par criticité et concentrons-nous seulement sur les failles les plus critiques.
$ trivy image pyrelay --severity HIGH --ignore-status unknown,not_affected,under_investigation,will_not_fix,fix_deferred,end_of_life,affected
Total: 18 (HIGH: 18, CRITICAL: 0)
┌──────────────┬────────────────┬──────────┬────────┬───────────────────┬────────────────────────┬──────────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├──────────────┼────────────────┼──────────┼────────┼───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ libc-bin │ CVE-2023-4911 │ HIGH │ fixed │ 2.31-13+deb11u5 │ 2.31-13+deb11u7 │ glibc: buffer overflow in ld.so leading to privilege │
│ │ │ │ │ │ │ escalation │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-4911 │
├──────────────┼────────────────┤ │ ├───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ libgnutls30 │ CVE-2023-0361 │ │ │ 3.7.1-5+deb11u2 │ 3.7.1-5+deb11u3 │ gnutls: timing side-channel in the TLS RSA key exchange code │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-0361 │
├──────────────┼────────────────┤ │ ├───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ libncursesw6 │ CVE-2022-29458 │ │ │ 6.2+20201114-2 │ 6.2+20201114-2+deb11u1 │ segfaulting OOB read │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-29458 │
│ ├────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-29491 │ │ │ │ 6.2+20201114-2+deb11u2 │ ncurses: Local users can trigger security-relevant memory │
│ │ │ │ │ │ │ corruption via malformed data │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-29491 │
├──────────────┼────────────────┤ │ ├───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ libssl1.1 │ CVE-2022-4450 │ │ │ 1.1.1n-0+deb11u3 │ 1.1.1n-0+deb11u4 │ openssl: double free after calling PEM_read_bio_ex │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-4450 │
│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-0215 │ │ │ │ │ openssl: use-after-free following BIO_new_NDEF │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-0215 │
│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-0286 │ │ │ │ │ openssl: X.400 address type confusion in X.509 GeneralName │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-0286 │
│ ├────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-0464 │ │ │ │ 1.1.1n-0+deb11u5 │ openssl: Denial of service by excessive resource usage in │
│ │ │ │ │ │ │ verifying X509 policy... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-0464 │
├──────────────┼────────────────┤ │ ├───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ libtinfo6 │ CVE-2022-29458 │ │ │ 6.2+20201114-2 │ 6.2+20201114-2+deb11u1 │ segfaulting OOB read │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-29458 │
│ ├────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-29491 │ │ │ │ 6.2+20201114-2+deb11u2 │ ncurses: Local users can trigger security-relevant memory │
│ │ │ │ │ │ │ corruption via malformed data │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-29491 │
├──────────────┼────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ ncurses-base │ CVE-2022-29458 │ │ │ │ 6.2+20201114-2+deb11u1 │ segfaulting OOB read │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-29458 │
│ ├────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-29491 │ │ │ │ 6.2+20201114-2+deb11u2 │ ncurses: Local users can trigger security-relevant memory │
│ │ │ │ │ │ │ corruption via malformed data │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-29491 │
├──────────────┼────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ ncurses-bin │ CVE-2022-29458 │ │ │ │ 6.2+20201114-2+deb11u1 │ segfaulting OOB read │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-29458 │
│ ├────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-29491 │ │ │ │ 6.2+20201114-2+deb11u2 │ ncurses: Local users can trigger security-relevant memory │
│ │ │ │ │ │ │ corruption via malformed data │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-29491 │
├──────────────┼────────────────┤ │ ├───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ openssl │ CVE-2022-4450 │ │ │ 1.1.1n-0+deb11u3 │ 1.1.1n-0+deb11u4 │ openssl: double free after calling PEM_read_bio_ex │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-4450 │
│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-0215 │ │ │ │ │ openssl: use-after-free following BIO_new_NDEF │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-0215 │
│ ├────────────────┤ │ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-0286 │ │ │ │ │ openssl: X.400 address type confusion in X.509 GeneralName │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-0286 │
│ ├────────────────┤ │ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-0464 │ │ │ │ 1.1.1n-0+deb11u5 │ openssl: Denial of service by excessive resource usage in │
│ │ │ │ │ │ │ verifying X509 policy... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-0464 │
└──────────────┴────────────────┴──────────┴────────┴───────────────────┴────────────────────────┴──────────────────────────────────────────────────────────────┘
18 des 63 failles HIGH
de notre image pyrelay
peuvent-être corrigées. Penchons-nous sur la plus haute criticité : la faille CRITICAL
. Aucun correctif n’est présent. Mais impacte-elle notre application ? En recherchant plus d’informations sur son rôle, on apprend qu’elle est liée à la gestion de base de données Berkeley13.
$ trivy image pyrelay --severity CRITICAL --ignore-status unknown,not_affected,under_investigation,will_not_fix,fix_deferred,end_of_life
Total: 1 (CRITICAL: 1)
┌──────────┬───────────────┬──────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├──────────┼───────────────┼──────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────┤
│ libdb5.3 │ CVE-2019-8457 │ CRITICAL │ affected │ 5.3.28+dfsg1-0.8 │ │ heap out-of-bound read in function rtreenode() │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-8457 │
└──────────┴───────────────┴──────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────┘
Analyse dynamique
Recherchons plus d’informations sur cette bibliothèque. N’oublions pas que d’un point de vue ensembliste, la totalité de l’environnement n’est pas dédiée à l’application. Par exemple il embarque les binaires apt-get
, ls
, … qui sont inutiles à l’exécution du relai Nostr.
Une simple commande ldd
nous informerait des bibliothèques dynamiques attachées au programme. Mais nous utilisons l’interpréteur Python et elle n’est pas applicable. C’est l’ajout de la variable d’environnement LD_DEBUG
qui va nous montrer les appels aux bibliothèques dynamiques.
Attention cependant, on ne verra que les bibliothèques appelées lorsque le programme tourne. Cela signifie utiliser l’application entièrement pour être sûr que l’intégralité des fonctionnalités applicatives appellent ou n’appellent pas une bibliothèque spécifique.
Voici un rappel des valeurs possibles de la variable d’environnement LD_DEBUG
.
LD_DEBUG=help ls
Valid options for the LD_DEBUG environment variable are:
libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit
To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment
variable.
Relançons le service nostr_pyrelay
et exécutons la commande suivante, en passant minutieusement sur toutes les fonctionnalités :
$ docker exec -it nostr_pyrelay.1.43f44ozc4dhpuctpeylt8x6cw bash
root@nostr_pyrelay:/app$ LD_DEBUG=libs python pyrelay/relay/server.py 2>&1 | grep -iv python | grep "/lib"
9140: trying file=/usr/local/bin/../lib/libc.so.6
9140: trying file=/lib/x86_64-linux-gnu/libc.so.6
9140: trying file=/lib/x86_64-linux-gnu/libpthread.so.0
9140: trying file=/lib/x86_64-linux-gnu/libdl.so.2
9140: trying file=/lib/x86_64-linux-gnu/libutil.so.1
9140: trying file=/lib/x86_64-linux-gnu/libm.so.6
9140: calling init: /lib/x86_64-linux-gnu/libpthread.so.0
9140: calling init: /lib/x86_64-linux-gnu/libc.so.6
9140: calling init: /lib/x86_64-linux-gnu/libm.so.6
9140: calling init: /lib/x86_64-linux-gnu/libutil.so.1
9140: calling init: /lib/x86_64-linux-gnu/libdl.so.2
9140: trying file=/usr/lib/x86_64-linux-gnu/libssl.so.1.1
9140: trying file=/usr/lib/x86_64-linux-gnu/libcrypto.so.1.1
9140: calling init: /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1
9140: calling init: /usr/lib/x86_64-linux-gnu/libssl.so.1.1
9140: trying file=/lib/x86_64-linux-gnu/libz.so.1
9140: calling init: /lib/x86_64-linux-gnu/libz.so.1
9140: trying file=/lib/x86_64-linux-gnu/libbz2.so.1.0
9140: calling init: /lib/x86_64-linux-gnu/libbz2.so.1.0
9140: trying file=/lib/x86_64-linux-gnu/liblzma.so.5
9140: calling init: /lib/x86_64-linux-gnu/liblzma.so.5
9140: trying file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6
9140: trying file=/lib/x86_64-linux-gnu/libgcc_s.so.1
9140: calling init: /lib/x86_64-linux-gnu/libgcc_s.so.1
9140: calling init: /usr/lib/x86_64-linux-gnu/libstdc++.so.6
Si la bibliothèque libdb
n’apparaît pas dans ces lignes, et qu’on est sûrs que l’intégralité des fonctionnalités de l’application a été testée, c’est qu’aucun appel à la bibliothèque libdb
n’est réalisé. Cette faille de sécurité peut être jugée comme mineure sur notre système. On peut réitérer cette analyse avec les autres CVE qui sont affected
.
➡ Cette méthode reste expérimentale, et nécessite de plonger dans les couches les plus basses du système.
Mitigation des vulnérabilités
Voyons maintenant comment corriger une des failles de sécurité, en mettant à jour une dépendance. On ajoute une ligne d’installation dans le Dockerfile :
FROM python:3.11.1-slim
WORKDIR /app/
RUN apt-get update
RUN apt-get install build-essential -y
RUN apt-get install pkg-config -y --no-install-recommends
# on ajoute la mise à jour de la bibliothèque libc-bin
RUN apt-get install libc-bin=2.31-13+deb11u7
COPY pyproject.toml requirements.txt /app/
RUN pip install -r requirements.txt
COPY pyrelay /app/pyrelay
ENV PYTHONPATH=/app
On rebuilde le tout et on relance l’analyse, qui ne montre plus cette faille. L’application a été patchée, on peut dérouler la suite de tests, augmenter son numéro de version, et l’utiliser.
➡ Je décris ici qu’une partie minime de l’analyse de vulnérabilités et de leur correction. L’idée est de montrer comment l’outil Trivy nous informe sur la sécurité de notre application, et comment mettre en place des vérifications. Bien sûr, les points d’attentions varient selon la nature de l’application packagée, et des métriques à rendre visibles. En effet, tracer par exemple l’évolution des vulnérabilités semble une bonne pratique pour toujours rester à l’écoute des potentielles failles de sécurité qui pourraient compromettre le service, et in fine, les utilisateurs.
Analyse du reverse proxy
Nous ne maintenons pas l’image Docker du reverse proxy. Il est donc plus difficile de modifier son cycle de vie, son développement, son ajout de dépendances,… Même si l’image Docker est open source, elle peut être récupérée à notre compte et être intégrée à nos développements.
Mon parti pris sera de privilégier le moins de customisation possible. Plus on utilise les logiciels tels quels, moins ils nécessitent d’attention, hormis leur intégration au sein de notre écosystème. Customiser, adapter un logiciel à nos besoins signifie développer, donc maintenir un service, et devient un point d’attention supplémentaire. Un compromis est à atteindre entre la complexité d’une nouvelle intégration logicielle et celle inhérente à un nouveau développement.
On peut répéter l’analyse statique des vulnérabilités d’une part, et tester dynamiquement l’accès au reverse proxy.
Pour la première option, on notera que le nombre de vulnérabilités augmente significativement avec l’ajout de Python et certbot
.
# vulnerabilities in nginx image
$ trivy image nginx --format template --template "@tp.tpl" 2>/dev/null
critical, high, medium, low
2, 18, 33, 85
# vulnerabilities in nginx-certbot image
$ trivy image jonasal/nginx-certbot:5.0.0 --format template --template "@tp.tpl" 2>/dev/null
critical, high, medium, low
2, 29, 50, 89
Les modules nginx
peuvent embarquer des failles de sécurité, et il est toujours intéressant d’en connaitre la liste :
$ docker run -it jonasal/nginx-certbot:5.0.0 bash
root@e8b3baf03248:/$ nginx -V 2>&1 | tr ' ' '\n' | grep module
--modules-path=/usr/lib/nginx/modules
--with-http_addition_module
--with-http_auth_request_module
--with-http_dav_module
--with-http_flv_module
--with-http_gunzip_module
--with-http_gzip_static_module
--with-http_mp4_module
--with-http_random_index_module
--with-http_realip_module
--with-http_secure_link_module
--with-http_slice_module
--with-http_ssl_module
--with-http_stub_status_module
--with-http_sub_module
--with-http_v2_module
--with-http_v3_module
--with-mail_ssl_module
--with-stream_realip_module
--with-stream_ssl_module
--with-stream_ssl_preread_module
De ce fait on pourra, en plus de Trivy, en consultant cette page14, savoir si l’un des modules installé embarque une faille de sécurité.
Pour la seconde option, tournons-nous vers l’utilitaire Whatweb
15.
Web scanning
J’ai mis en place une image Docker, facilitant l’utilisation de ce logiciel 16. On lance tout ça en visant notre endpoint à l’adresse antoine.local
(cf article référé 1).
$ wget https://gist.githubusercontent.com/antlas0/a63c5f08ef9d6bc4b3252a4b394c9f95/raw/21a6711e56665429e267defc94a190893ae597c1/Dockerfile
$ docker build . -t ww:0.0.1
[...]
$ docker run ww:0.0.1 whatweb antoine.local -a 3
https://antoine.local [426 Upgrade Required] Country[XXXX][XX], HTTPServer[nginx], IP[xx.xx.xx.xx], UncommonHeaders[upgrade], WebSocket, nginx
Le reverse proxy et l’application répondent présents et divulguent certaines informations sur leur identité et fonctionnement. On voit que seules les propriétés applicatives sont accessibles, et on ne sait pas que la plateforme Docker est utilisée en coulisse.
➡ N’utilisant pas de framework webs connus, le nombre de failles de sécurité diminue, mais au dépit de reporter la responsabilité sur le relai Nostr, dont le développement devient crucial.
La composition des modules
Pour connaître précisément tous les composants logiciels embarqués dans chaque module, Trivy vient encore à la rescousse. Une liste dotée d’un format spécifique sera alors générée et pourra servir de référence. On connaitra alors les compositions exactes de chaque module, que l’on nomme SBOM
(Software Bill of Materials).
Trivy supporte la reconnaissance des packages systèmes et de développement 17.
Deux formats de description sont possibles18, le SPDX
et le CDX
. Tournons-nous vers le plus récent qui se veut un peu plus léger.
Tentons de retrouver la composition du relai. Le scan commence par l’image Docker, le système d’exploitation et les packages systèmes.
$ trivy image --format cyclonedx --output result.json pyrelay:0.0.1
$ cat result.json
{
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": "urn:uuid:98a177ce-b5b8-4642-a586-e6ddfce63ca6",
"version": 1,
"metadata": {
"timestamp": "",
"tools": [
{
"vendor": "aquasecurity",
"name": "trivy",
"version": "0.48.3"
}
],
"component": {
"bom-ref": "8d8fe4ce-1e41-4283-9bf0-f3face7ae57d",
"type": "container",
"name": "pyrelay:1.0.0",
"properties": [
{
"name": "aquasecurity:trivy:DiffID",
"value": "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf"
},
{
"name": "aquasecurity:trivy:ImageID",
"value": "sha256:af71e4e9289ba9f42609a6f6fb4f91df30168d42459992a896194363cd7c9871"
},
{
"name": "aquasecurity:trivy:RepoTag",
"value": "pyrelay:1.0.0"
},
{
"name": "aquasecurity:trivy:SchemaVersion",
"value": "2"
}
]
}
},
"components": [
{
"bom-ref": "c8938c83-ea3e-4ab0-8ae6-d8a5ef1ebcb7",
"type": "operating-system",
"name": "debian",
"version": "11.6",
"properties": [
{
"name": "aquasecurity:trivy:Class",
"value": "os-pkgs"
},
{
"name": "aquasecurity:trivy:Type",
"value": "debian"
}
]
},
{
"bom-ref": "pkg:deb/debian/adduser@3.118?arch=all&distro=debian-11.6",
"type": "library",
"supplier": {
"name": "Debian Adduser Developers <adduser@packages.debian.org>"
},
"name": "adduser",
"version": "3.118",
"licenses": [
{
"license": {
"name": "GPL-2.0"
}
}
],
"purl": "pkg:deb/debian/adduser@3.118?arch=all&distro=debian-11.6",
"properties": [
[...]
Puis les packages python sont reconnus, exemple ici avec le package attr
.
[...]
{
"bom-ref": "pkg:pypi/attr@0.3.2?file_path=usr%2Flocal%2Flib%2Fpython3.11%2Fsite-packages%2Fattr-0.3.2.dist-info%2FMETADATA",
"type": "library",
"name": "attr",
"version": "0.3.2",
"licenses": [
{
"license": {
"name": "MIT"
}
}
],
[...]
On peut, grâce à ce fichier, par exemple suivre les évolutions de licences au fur et à mesure du développement et des versions réalisées.
➡ La SBOM
est un outil très intéressant, elle donne une vue précise de tout le logiciel étudié. En comparant plusieurs fois la SBOM
de plusieurs versions de son logiciel, on sera en mesure de déterminer l’évolution des dépendances, de sa complexité et des changements de licence.
Web application firewall
On repart sur une analyse dynamique, dont l’un des objectifs est de se protéger des intrusions. Lors du fonctionnement de l’application, mettre en place un WAF
19 adresse ce problème. En surveillant les connexions HTTP
établies en temps-réel, le firewall sera en mesure de stopper celles jugées malveillantes. Un module 20 dédié au reverse proxy Nginx peut être installé, contenant une base de références d’attaques web, identifiées par l’OWASP 21.
Adaptation de l’architecture
On peut aussi le mettre en place de façon modulaire comme un intermédiaire supplémentaire dans l’écosystème, à travers lequel toutes les connexions vont passer. Le reverse proxy Nginx peut, à la place de rediriger directement les connexions vers le relai, les router vers le WAF
, qui les enverra vers le relai après une analyse de sécurité.
Le service de reverse proxy utilise une image déjà customisée, nginx-certbot:5.0.0
. Pour des raisons de simplicité, le meilleur compromis semble d’ajouter un service Docker de plus pour le WAF
22, plutôt que d’intégrer le module Nginx Modsecurity
dans l’image nginx-certbot
, nécessitant un ajout et une recompilation complète du module Nginx et de l’image Docker. Les inconvénients se trouveront alors dans la complexité de gestion de deux services Docker en amont du relai, et de l’augmentation des temps de réponse HTTP
. Ceci dit, tout reste modulaire : un service pour la gestion SSL/TLS
, et un autre pour l’analyse de flux.
Installation
On récupère l’image
$ docker pull owasp/modsecurity-crs:3.3.5-nginx-202401080101
On dispose le WAF
entre le reverse proxy et le relai, ce qui nécessite la configuration suivante :
- Le reverse proxy pointe maintenant vers le
WAF
, - le
WAF
pointe vers le relai.
Le point d’attention sur cette image est la présence des variables d’environnement et des templates. La combinaison des deux permet d’arriver à ses fins. Dans notre cas, on montera le fichier server default.conf
dans le dossier templates
23, en précisant le resolver Docker avec la variable DNS_SERVER=127.0.0.11
. Pour plus d’informations sur le niveau paranoia
, c’est par ici24.
Lancement
On lance les services Docker. Comme précisé dans l’article référé1, j’utilise un outil homemade pour lancer les services Docker. Voici la configuration de services, on retrouve les paramètres standards Docker.
{
"spec": {
"image": "jonasal/nginx-certbot",
"tag":"5.0.0",
"name": "nginx_reverse_proxy",
"env":["CERTBOT_EMAIL=user@example.com"],
"hostname":"nginx_reverse_proxy",
"mounts": ["nginx_system.conf:/etc/nginx/nginx.conf/:ro", "./user/:/etc/nginx/user_conf.d/:ro", "access.log:/var/log/nginx/access.log:rw", "./letsencrypt/:/etc/letsencrypt/:rw"],
"endpoints":{
"mode":"vip",
"ports":{"443": 443, "80": 80}
}
}
},
{
"spec": {
"image": "owasp/modsecurity-crs",
"tag":"3.3.5-nginx-202401080101",
"name": "nostr_waf",
"hostname":"nostr_waf",
"mounts": ["nginx_user.conf:/etc/nginx/templates/conf.d/default.conf.template"],
"env":["PARANOIA=1", "DNS_SERVER=127.0.0.11"],
"endpoints":{
"mode":"dnsrr"
}
}
},
{
"spec": {
"image": "pyrelay",
"tag":"latest",
"name": "nostr_pyrelay",
"command": "python pyrelay/relay/server.py",
"env":["SERVER_NAME=antoine.local"],
"hostname":"nostr_pyrelay",
"endpoints":{
"mode":"dnsrr"
}
}
}
On vérifie que le service WAF
redirige son flux vers le relai nostr_pyrelay
:
$ docker exec -it nostr_waf.1.f39bwqlrl21a9tsnjv1gtf0aa cat /etc/nginx/nginx.conf
load_module modules/ngx_http_modsecurity_module.so;
worker_processes auto;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
keepalive_timeout 60s;
sendfile on;
resolver 127.0.0.11 valid=5s;
include /etc/nginx/conf.d/*.conf;
}
Le module modsecurity
est bien chargé, le resolver Docker est bien renseigné.
On jette un coup d’oeil au server HTTP
configuré :
$ docker exec -it nostr_waf.1.f39bwqlrl21a9tsnjv1gtf0aa cat /etc/nginx/conf.d/default.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80 default_server;
location / {
proxy_pass http://nostr_pyrelay:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
La redirection est bien faite du WAF
vers le relai !
Maintenant, le reverse proxy doit faire passer les requêtes HTTP
par le WAF
.
$ docker exec -it nginx_reverse_proxy.1.ytfy0s2l7vjbzk9viwjnraly0 cat /etc/nginx/conf.d/nginx.conf
server {
# Listen to port 443 on both IPv4 and IPv6.
listen 443 ssl default_server reuseport;
listen [::]:443 ssl default_server reuseport;
# Domain names this server should respond to.
server_name antoine.local;
[...]
location / {
proxy_pass http://nostr_waf:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Tout est bon, les aiguillages sont établis.
Place au test de connexion, on vérifie que l’on atteint bien le relai à partir d’internet.
$ docker service logs nostr_pyrelay
nostr_pyrelay.1.w5mxphrcoyu4@host | __main__ - INFO - New message from connection conn_uid=452a18dc-9edb-4734-b921-ee463af78c37
nostr_pyrelay.1.w5mxphrcoyu4@host | pyrelay.relay.dispatcher - INFO - Got request=NostrRequest(subscription_id='Profiles', filters=[NostrFilter(ids=None, authors=['', ''], kinds=[], since=1704276504, until=None, limit=None, generic_tags=None)]) conn_uid=452a18dc-9edb-4734-b921-ee463af78c37
nostr_pyrelay.1.w5mxphrcoyu4@host | __main__ - INFO - New message from connection conn_uid=452a18dc-9edb-4734-b921-ee463af78c37
nostr_pyrelay.1.w5mxphrcoyu4@host | pyrelay.relay.dispatcher - INFO - Got request=NostrRequest(subscription_id='RESUME-Following1706762439', filters=[NostrFilter(ids=None, authors=['', ''], kinds=[], since=1706762439, until=None, limit=5000, generic_tags=None)]) conn_uid=452a18dc-9edb-4734-b921-ee463af78c37
Les traces du relai sont parlantes, on observe les requêtes du client Nostur, que l’on a configuré sur ce relai.
Dans le container nostr_waf
, on observe les transferts de connexions en provenance du reverse proxy, dont l’IP est 192.168.122.243
. Le user-agent
du client Nostur est visible.
nostr_waf.1.f39bwqlrl21a@host | [notice] 1#1: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/923/0)
nostr_waf.1.f39bwqlrl21a@host | 192.168.122.243 - - [xx/xxx/2024:xx:xx:xx +0000] "GET / HTTP/1.1" 101 36 "-" "Nostur/259 CFNetwork/1490.0.4 Darwin/23.2.0" "10.10.0.4"
➡ L’architecture est adaptée, fonctionnelle. Maintenant tentons des requêtes malveillantes, et observons le résultat.
Vérification du fonctionnement
Premièrement, le scan du site web via l’utilitaire Whatweb est déjà détecté et stoppé, le firewall web renvoi un code d’erreur 403
.
$ docker run ww:0.0.1 whatweb antoine.local -a 3
https://antoine.local [403 Forbidden] Country[XXXX][XX], HTTPServer[nginx], IP[xx.xx.xx.xx], Title[403 Forbidden], nginx
Testons ensuite l’injection SQL via l’outil SQLMap
25. Pour attester du fonctionnement, on lancera les mêmes commandes à partir du reverse proxy, qui a un accès direct au relai, et à partir d’une machine extérieure au swarm.
Sur le reverse proxy, on récupère l’IP du relai
root@nginx_reverse_proxy:~$ nslookup nostr_pyrelay
Server: 127.0.0.11
Address: 127.0.0.11#53
Non-authoritative answer:
Name: nostr_pyrelay
Address: 192.168.122.239
et on lance l’outil SQLMap
root@nginx_reverse_proxy:/$ git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev
root@nginx_reverse_proxy:/$ cd sqlmap-dev
root@nginx_reverse_proxy:/$ python3 sqlmap.py -u 192.168.122.239:8001
[*] starting
[10:31:48] [INFO] testing connection to the target URL
[10:31:48] [WARNING] the web server responded with an HTTP error code (426) which could interfere with the results of the tests
[10:31:48] [INFO] testing if the target URL content is stable
[10:31:49] [INFO] target URL content is stable
[10:31:49] [CRITICAL] no parameter(s) found for testing in the provided data (e.g. GET parameter 'id' in 'www.site.com/index.php?id=1')
[10:31:49] [WARNING] HTTP error codes detected during run:
426 (Upgrade Required) - 2 times
[*] ending
Pas d’information glanée, mais en tous cas l’analyse s’est déroulée correctement, ce qui n’est pas le cas à partir d’une machine extérieure au swarm. On obtient un 403 Forbidden
:
# on an internet machine
$ python sqlmap.py -u https://antoine.local
[*] starting
[11:26:06] [INFO] testing connection to the target URL
[11:26:06] [WARNING] the web server responded with an HTTP error code (403) which could interfere with the results of the tests
[11:26:06] [INFO] checking if the target is protected by some kind of WAF/IPS
[11:26:06] [INFO] testing if the target URL content is stable
[11:26:07] [INFO] target URL content is stable
[11:26:07] [CRITICAL] no parameter(s) found for testing in the provided data (e.g. GET parameter 'id' in 'www.site.com/index.php?id=1')
[11:26:07] [WARNING] HTTP error codes detected during run:
403 (Forbidden) - 3 times
[*] ending
Les logs du WAF
sont explicites :
{
"transaction": {
"client_ip": "192.168.122.243",
"time_stamp": "",
"server_id": "",
"client_port": 50872,
"host_ip": "192.168.122.241",
"host_port": 80,
"unique_id": "",
"request": {
"method": "GET",
"http_version": 1.1,
"uri": "/",
"headers": {
"Connection": "close",
"Cache-Control": "no-cache",
"X-Real-IP": "10.10.0.4",
"Host": "antoine.local",
"X-Forwarded-For": "10.10.0.4",
"User-Agent": "sqlmap/1.8.1.7#dev (https://sqlmap.org)",
"Accept": "*/*",
"Accept-Encoding": "gzip,deflate"
}
},
"response": {
"body": "<html>\r\n<head><title>403 Forbidden</title></head>\r\n<body>\r\n<center><h1>403 Forbidden</h1></center>\r\n<hr><center>nginx/1.25.3</center>\r\n</body>\r\n</html>\r\n",
"http_code": 403,
"headers": {
"Server": "nginx/1.25.3",
"Date": "",
"Content-Length": "153",
"Content-Type": "text/html",
"Connection": "close"
}
},
[...]
"messages": [
{
"message": "Found User-Agent associated with security scanner",
"details": {
"match": "Matched \"Operator `PmFromFile' with parameter `scanners-user-agents.data' against variable `REQUEST_HEADERS:User-Agent' (Value: `sqlmap/1.8.1.7#dev (https://sqlmap.org)' )",
"reference": "o0,6v133,39t:lowercase",
"ruleId": "913100",
"file": "/etc/modsecurity.d/owasp-crs/rules/REQUEST-913-SCANNER-DETECTION.conf",
"lineNumber": "34",
"data": "Matched Data: sqlmap found within REQUEST_HEADERS:User-Agent: sqlmap/1.8.1.7#dev (https://sqlmap.org)",
"severity": "2",
"ver": "OWASP_CRS/3.3.5",
"rev": "",
"tags": [
"modsecurity",
"application-multi",
"language-multi",
"platform-multi",
"attack-reputation-scanner",
"paranoia-level/1",
"OWASP_CRS",
"capec/1000/118/224/541/310",
"PCI/6.5.10"
],
"maturity": "0",
"accuracy": "0"
}
},
{
"message": "Inbound Anomaly Score Exceeded (Total Score: 5)",
"details": {
"match": "Matched \"Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `5' )",
"reference": "",
"ruleId": "949110",
"file": "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf",
"lineNumber": "81",
"data": "",
"severity": "2",
"ver": "OWASP_CRS/3.3.5",
"rev": "",
"tags": [
"modsecurity",
"application-multi",
"language-multi",
"platform-multi",
"attack-generic"
],
"maturity": "0",
"accuracy": "0"
}
}
]
}
}
Le scanner SQLMap
a été détecté, et les réponses HTTP
ont été modifiées pour renvoyer une page vide et le code d’erreur 403
.
➡ Le WAF
est donc fonctionnel, notre architecture propose une sécurité accrue face aux tentatives d’intrusions via le flux HTTP
.
Conclusion
Nous avons brièvement abordé la sécurité applicative. La sécurité n’est pas qu’une affaire de conception ou de supervision. Les deux aspects doivent cohabiter dans une vision globale.
Un outil comme Trivy amène de la clarté sur la stack logicielle construite au moment de son développement. Même si son analyse technique propose des réponses certaines sur les périmètres d’impacts, c’est bien la vision humaine qui reste centrale pour orienter les choix de conception. Des procédés automatisés au moment de la production logicielle peuvent adresser cette vision durable, en suivant par exemple l’évolution des vulnérabilités.
Cette vision humaine se base sur
- Une connaissance globale du système (intgration des modules, flux réseaux,…),
- Une connaissance précise de chaque module applicatif,
- Un suivi continu des développements et des plateformes utilisées (Docker,…).
Mais une analyse en temps-réel lorsque le produit est utilisé reste inévitable pour superviser les évènements, contrôler les accès, et répondre aux attaques.
https://antlas.art/posts/nostr_experimentations/ ↩︎ ↩︎ ↩︎ ↩︎
https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv ↩︎
https://github.com/antlas0/pyrelay/blob/master/requirements.txt ↩︎
https://github.com/JonasAlfredsson/docker-nginx-certbot/blob/master/src/Dockerfile#L1C1-L1C18 ↩︎
https://aquasecurity.github.io/trivy/v0.48/docs/coverage/ ↩︎
https://aquasecurity.github.io/trivy/v0.48/docs/configuration/db/#vulnerability-database ↩︎
https://aquasecurity.github.io/trivy/v0.18.3/examples/report/ ↩︎
https://gist.github.com/antlas0/1e30b228d3e5f21a60eb5e8b098a723c#file-count_vulnerabilities-tpl ↩︎
https://aquasecurity.github.io/trivy/v0.48/docs/configuration/filtering/#by-status ↩︎
https://gist.github.com/antlas0/a63c5f08ef9d6bc4b3252a4b394c9f95 ↩︎
https://aquasecurity.github.io/trivy/v0.37/docs/vulnerability/detection/language/ ↩︎
https://scribesecurity.com/blog/spdx-vs-cyclonedx-sbom-formats-compared/ ↩︎
https://www.nginx.com/blog/compiling-and-installing-modsecurity-for-open-source-nginx/ ↩︎
https://fr.wikipedia.org/wiki/Open_Web_Application_Security_Project ↩︎
https://github.com/coreruleset/modsecurity-crs-docker/tree/develop?tab=readme-ov-file#nginx-based-images-breaking-change ↩︎
https://coreruleset.org/20211028/working-with-paranoia-levels/ ↩︎