title: Linux: Shell basics
description: Bases shell, iptables, etc
published: true
date: 2023-12-15T15:05:34.459Z
tags: linux, bash, iptables, vim
editor: markdown
dateCreated: 2026-09-24T20:33:13.700Z
Introduction
Une des grandes forces d'UNIX et de GNU/Linux tient dans sa philosophie de "tout est fichier", et plus spécifiquement, tout est fichier et flux de texte. Et UNIX, tout comme GNU/Linux, disposent de nombreux outils (/awk, sed, uniq, sort, etc/) permettant de manipuler ces flux de textes et ces fichiers.
Le /shell/ que nous allons étudier un peu plus en détail s'appelle Bash, pour /Bourne Again SHell/. Il descend du... /Bourne Shell/ et a été développé par Brian Fox pour le projet GNU. Il est apparu dans une première version en 1989, et n'a cessé d'évoluer jusqu'à nos jours (la version 5 date de 2019). C'est le shell par défaut sur la plupart des distributions GNU/Linux, sur macOS jusqu'à macOS Catalina, sur Solaris 11.
Par contre d'autres UNIX utilisent d'autres shells par défaut (même si bash a été porté pour ces OS), par exemple:
- ksh (Korne Shell) sur OpenBSD et AIX
- tcsh sur FreeBSD
- ash sur NetBSD et Alpine Linux
Il existe aussi d'autres shells:
- Z-Shell, zsh, très puissant en mode interactif (mais moins adapté au scripts)
- fish, très joli et avec de nombreuses bonnes idées.
Ils sont en général compatibles avec le shell originel, Bourne shell, généralement abrégé /sh/ (1977), descendant du Thompson (comme Ken Thompson) shell, mais diffèrent au niveau de leurs fonctions internes et de leurs possibilités.
ACLs
Droits classiques
Tout d'abord, nous allons (re)voir les droits classiques sous UNIX et GNU/Linux.
#on va d'abord se créer un répertoire
#de bidouille
$ mkdir -p test/{droits,acls,mask}
$ for i in droits acls mask; do for x in a b c; do touch test/$i/$x; done; done
Si on exécute:
$ ls -al
total 12
drwxr-sr-x 3 lidstah lidstah 4096 Sep 7 18:22 .
drwxr-sr-x 42 lidstah lidstah 4096 Sep 7 18:20 ..
-rw-r--r-- 1 lidstah lidstah 0 Sep 7 18:20 bla
drwxr-sr-x 2 lidstah lidstah 4096 Sep 7 18:22 lol
-rw-r--r-- 1 lidstah lidstah 0 Sep 7 18:20 plop
-rw-r--r-- 1 lidstah lidstah 0 Sep 7 18:20 pwet
On remarque un premier champ composé de commutateurs. Par exemple:
drwxr-sr-x 2 lidstah lidstah 4096 Sep 7 18:22 lol
M'indique que le fichier est un répertoire (/d/), qui est lisible, modifiable et exécutable (rwx) par mon utilisateur, lisible et exécutable (le s signifie /setgid/ et implique ici le x d'exécutable) par le groupe "lidstah" et enfin lisible et exécutable par tout le monde.
NOTE: un répertoire est TOUJOURS exécutable
NOTE bis: on verra setuid, setgid et sticky bit un peu plus loin dans ce chapitre
Ce qui nous amène aux groupes de permissions et types de permissions!
Groupes de permissions
Les permissions sont organisées en trois groupes:
- Propriétaire (owner): le propriétaire du fichier
- Groupe (group): le groupe auquel le fichier est assigné
- Les Autres (all users): les permissions à appliquer à tous les autres utilisateurs et groupes du système.
Petit récapitulatif pour la notation /chmod/:
| char | signification |
|---|---|
| u | utilisateur |
| g | groupe |
| o | les Autres |
| a | Tous |
Types de permissions
Là aussi, on a trois types de permissions (on appelle une /triade/ ces trois types de permissions)
- lecture (read): le groupe de permissions peut-il lire le fichier?
- écriture (write): le groupe de permissions peut-il modifier le fichier?
- exécution (execute): le groupe de permissions peut-il exécuter le fichier?
Quels utilitaires nous permettent de modifier les permissions d'un fichier?
On a deux grands copains pour faire ça:
- chown, pour modifier l'utilisateur et le groupe propriétaires du fichier, exemple:
#Changer l'owner et le groupe d'un fichier:
$ chown user:group fichier
#Changer de façon récursive l'utilisateur et le groupe
#d'un répertoire et de son contenu:
$ chown -R user:group repertoire
- chmod, pour modifier les permissions accordées aux groupes de permissions, exemples:
#Changer, pour l'utilisateur courant, les droits du
#fichier en read, write, execute
$ chmod u+rwx fichier
#Changer, pour l'utilisateur courant, les droits du
#fichier en rwx, et enlever tous les droits au
#groupe et aux Autres, deux méthodes:
$ chmod u+rwx,g-rwx,o-rwx fichier
#Ou plus simplement:
$ chmod 700 fichier
#On peut aussi directement affecter les droits avec a
$ chmod a+rw fichier
En effet, chmod permet d'utiliser une notation "humaine" et une notation "octale" pour la définition des droits.
Un petit tableau sur les valeurs octales et leur signification:
| Valeur | Droits |
|---|---|
| 4 | Read |
| 2 | Write |
| 1 | Execute |
| 0 | None |
Et donc, pour les droits rwx: 4+2+1 = 7, rw: 4+2 = 6, read-only (read): 4, read et execute: 4+1 = 5... etc. On va pouvoir rapidement mettre ça en pratique
Je vous incite fortement à lire les /manpages/ correspondantes
Et maintenant, quelques exercices (simples). On va tout d'abord créer un répertoire "test" et quelques fichiers à l'intérieur de ce répertoire:
$ mkdir test && for i in a b c; do touch test/$i; done
#Ce qui va nous donner quelque chose du genre:
$ ls -al test
total 8
drwxr-sr-x 2 lidstah lidstah 4096 Sep 7 18:43 .
drwxr-sr-x 42 lidstah lidstah 4096 Sep 7 18:43 ..
-rw-r--r-- 1 lidstah lidstah 0 Sep 7 18:43 a
-rw-r--r-- 1 lidstah lidstah 0 Sep 7 18:43 b
-rw-r--r-- 1 lidstah lidstah 0 Sep 7 18:43 c
Exercices
Quelles commandes me permettraient:
- De modifier les droits de tous les fichiers du répertoire pour qu'ils soient lisibles, modifiables et exécutables par tout le monde? NOTE: NE JAMAIS, JAMAIS, faire ça
- C'est pour ça qu'on va maintenant les modifier pour qu'ils soient lisibles, modifiables uniquement par mon utilisateur
- Il faudrait maintenant que b soit aussi lisible pour tout le monde et modifiable par l'utilisateur et son groupe, comment faire?
- Que se passe-t-il si je chmod 400 le fichier c? puis-je le supprimer? remodifier ses droits?
Droits Spéciaux
suid
SUID signifie /Set User ID/ bit. Il permet de lancer un programme avec les droits de son propriétaire.
Vous l'utilisez très souvent sans vous en rendre compte. Si on regarde:
$ ls -al /usr/bin/sudo
-rwsr-xr-x 1 root root 136976 May 25 09:21 /usr/bin/sudo
Ici, on voit que l'exécutable sudo (/Summon User DO/) appartient à root et au groupe root, et que le droit d'exécution de l'utilisateur a été remplacé par /setuid/.
De même, passwd:
$ ls -al /bin/passwd
-rwsr-xr-x 1 root root 63736 juil. 27 2018 /bin/passwd
Ici, quel est l'intérêt que passwd soit /setuid/? Tout simplement car passwd modifie le contenu du fichier:
/etc/shadow
lequel appartient à root:root. Hors, un utilisateur peut vouloir changer son mot de passe. Il faut donc que lorsqu'il invoque le programme passwd, ce programme, lancé par cet utilisateur, puisse modifier le fichier shadow appartenant à root:root.
Si on lance la commande passwd en tant qu'utilisateur et qu'on regarde la liste des processus, on va voir que passwd est lancé en tant que root:
#dans un premier shell, on tape la commande passwd
$ passwd
Changement du mot de passe pour lidstah.
Current password:
#dans un second shell
#(tmux, autre console virtuelle (ctrl-alt-Fx)
$ ps aux|grep passwd
root 15038 0.0 0.0 9356 2880 pts/2 S+ 21:30 0:00 passwd
lidstah 15047 0.0 0.0 6092 892 pts/4 S+ 21:30 0:00 grep passwd
Dès lors, on voit que le bit SUID permet de lancer des programmes et commandes avec les droits du propriétaire du fichier, plutôt que ceux de l'utilisateur qui lance le programme. Dans le cas de passwd, on ne veut pas qu'un utilisateur puisse modifier le mot de passe d'un autre utilisateur. Cette logique est intégrée dans le logiciel passwd, pas dans le mécanisme de SUID ou dans la logique du kernel.
Il est donc très important, lorsqu'on crée un programme ou un script exécutable avec le bit SUID, de considérer dès le début l'aspect sécurité et de ne jamais le négliger: "Security First".
On peut activer le bit SUID sur un exécutable de la sorte:
$ chmod u+s fichier
sgid
SGID, je vous le donne dans le mille, signifie "Set Group ID". Il est très similaire à SUID, sauf qu'il permet d'exécuter un processus avec les droits du groupe plutôt que les droits de l'utilisateur propriétaire du fichier.
On peut l'activer via la commande:
$ chown user:group fichier
$ chmod u-s,g+s fichier
Une autre utilité est de pouvoir créer un répertoire appartenant à un groupe, le SGID, et ainsi tous les fichiers créés dans ce répertoire appartiendront au groupe SGID:
$ mkdir sgidtest
$ chown user:groupe sgidtest
$ chmod g+s sgidtest
$ touch sgidtest/plop
$ ls -al sgidtest
sticky bit
Le /sticky bit/ est un droit d'accès qui peut être principalement assigné aux répertoires. Son apparition remonte à 4.3BSD en 1986, ce qui ne me rajeunit pas .
Typiquement, il permet de limiter les droits de suppression et renommage du répertoire et de son contenu uniquement à root et à l'utilisateur ayant créé le répertoire. Par exemple, si le groupe dispose des autorisations de lecture et écriture, un utilisateur membre de ce groupe pourra modifier le fichier et le lire, mais ne pourra pas le renommer, le déplacer ou le supprimer. Seul le propriétaire du fichier et root disposeront de ces droits. On le rencontre souvent dans /tmp:
$ ls -al /tmp
total 48
drwxrwxrwt 10 root root 4096 Sep 7 18:52 .
drwxr-xr-x 22 root root 4096 Apr 17 17:05 ..
drwxrwxrwt 2 root root 4096 Sep 7 17:57 .ICE-unix
-r--r--r-- 1 root lidstah 11 Sep 7 17:58 .X0-lock
drwxrwxrwt 2 root root 4096 Sep 7 17:58 .X11-unix
drwx------ 2 lidstah lidstah 4096 Sep 7 18:01 Temp-2f0c98b9-3af5-4e0c-a19f-c6819188f606
drwx------ 2 lidstah lidstah 4096 Sep 7 18:01 Temp-80daa3df-3a93-424e-8d2f-3468c4147f89
drwx------ 2 lidstah lidstah 4096 Sep 7 17:58 babel-vn4wSf
drwx------ 2 lidstah lidstah 4096 Sep 7 17:58 emacs1000
drwx------ 2 lidstah lidstah 4096 Sep 7 17:58 ns.lidstah.:0
-rw------- 1 lidstah lidstah 100 Sep 7 17:58 serverauth.LRiGaenhh6
srw------- 1 lidstah lidstah 0 Sep 7 18:01 ssh_mux_192.168.1.227_10022_git
srw------- 1 lidstah lidstah 0 Sep 7 17:58 ssh_mux_89.234.176.135_22_lidstah
srw------- 1 lidstah lidstah 0 Sep 7 18:25 ssh_mux_docker-prod.lidstah.info_22_lidstah
srw------- 1 lidstah lidstah 0 Sep 7 17:58 ssh_mux_providence.lidstah.info_22_lidstah
drwx------ 2 lidstah lidstah 4096 Sep 7 17:58 tmux-1000
Ici, par exemple, on voit bien que les répertoires .ICE-unix et .X11-unix ont le sticky bit activé (le "t" à la place du "x" du groupe de permissions "les Autres"). L'activation du sticky bit "t" implique que les droits d'exécution sont aussi implicitement activés. Plus rare, on peut rencontrer le sticky bit "T", qui n'active pas les droits d'exécution - et est donc inutile sur un répertoire. Le noyau Linux ignore le sticky bit sur les fichiers - ce qui n'est pas le cas de tous les UNIX.
Comment manipuler le sticky bit
C'est très simple, avec notre ami /chmod/ bien sûr!
#On peut le set en utilisant le commutateur +t
$ chmod +t test/
#Ou en utilisant sa dénomination octale, 1000:
$ chmod 1755 test/
#De même on peut le supprimer:
$ chmod -t test/
$ chmod 0755 test/
ACL - Installation
Tout d'abord, il faut vérifier si notre kernel supporte les /Access Control Lists/, par exemple ici sur une Alpine Linux 3.12:
$ grep ACL /boot/config-*
CONFIG_EXT2_FS_POSIX_ACL=y
CONFIG_EXT3_FS_POSIX_ACL=y
CONFIG_EXT4_FS_POSIX_ACL=y
CONFIG_REISERFS_FS_POSIX_ACL=y
CONFIG_JFS_POSIX_ACL=y
CONFIG_XFS_POSIX_ACL=y
CONFIG_BTRFS_FS_POSIX_ACL=y
CONFIG_F2FS_FS_POSIX_ACL=y
CONFIG_FS_POSIX_ACL=y
CONFIG_TMPFS_POSIX_ACL=y
CONFIG_JFFS2_FS_POSIX_ACL=y
# CONFIG_NFS_V3_ACL is not set
# CONFIG_NFSD_V3_ACL is not set
CONFIG_CEPH_FS_POSIX_ACL=y
CONFIG_9P_FS_POSIX_ACL=y
La ligne qui nous intéresse principalement est:
CONFIG_FS_POSIX_ACL=y
On peut avoir besoin de permissions plus fines que ce qu'on peut obtenir via les permissions de base du système de fichiers ou de SUID/SGID. Imaginons que vous vouliez partager un fichier dans un répertoire de votre $HOME à un utilisateur n'appartenant pas à votre groupe. Il faut trouver un moyen de l'autoriser à accéder à votre fichier, pour pouvoir le lire ou le modifier par exemple.
Pour ça, on peut utiliser /acl/.
Il s'installe très simplement sous Debian:
$ sudo apt install acl
Et voilà!
ACL - Les commandes
getfacl
La commande getfacl permet d'obtenir des informations sur les ACLs attribuées à un fichier ou répertoire, et si elles s'appliquent - ou pas (notamment si on /chmod/ un fichier après l'avoir /setfacl/
)
setfacl
/setfacl/ est la commande nous permettant d'ajouter, supprimer et modifier une ACL sur un fichier, par exemple:
$ touch test/acltest
$ setfacl -m u:bernard:rw- test/acltest
ici, l'utilisateur "bernard" (ahem) disposera désormais des droits en lecture et écriture sur le fichier test/acltest.
La syntaxe de /setfacl/ est toujours de la forme:
prefix:utilisateurOugroupe:droits
Les préfixes disponibles sont:
| char | signification |
|---|---|
| u | droits utilisateur |
| g | droits groupe |
| o | droits Autres |
NOTE: pour le préfixe "o" on ne précise évidemment pas d'utilisateur ou de groupe, ce qui est logique puisqu'on parle de droits assignés aux Autres.
Et bien sûr les droits habituels: r, w et x.
On peut aussi vouloir appliquer récursivement une ACL sur tout un répertoire:
$ setfacl -Rm u:bernard:rw- test/
Et on peut bien sûr préciser une liste d'utilisateurs et groupes:
$ setfacl -m u:bernard:rw-,u:raymonde:rwx,g:pmu:r--,o:--- test/acltest
Maintenant, comment vérifier qu'un fichier ou répertoire est visé par une ou plusieurs ACL?
Tout d'abord, on peut lister le(s) fichier(s) en question:
$ ls -al test/acltest
-rw-rw-r--+ 1 lidstah lidstah 0 sept. 8 18:46 test/acltest
Ici le "+" en fin de triades nous indique que des acls s'appliquent sur ce fichier.
Et donc, avec getfacl (ici c'est l'utilisateur www-data qui dispose de droits sur ce fichier):
$ getfacl test/acltest
# file: test/acltest
# owner: lidstah
# group: lidstah
user::rw-
user:www-data:rw-
group::r--
mask::rw-
other::r--
Pour supprimer une ACL, on utilisera:
$ setfacl -b fichier
On peut aussi supprimer une partie de l'ACL avec l'option -x:
$ setfacl -x u:bernard,g:pmu test/acltest
Droits par défaut et héritage
On peut définir des droits par défaut sur le contenu d'un répertoire avec le... pré-préfixe d:
$ setfacl -m d:u:www-data:rwx test
#ou, plus simple si on a plusieurs users/groupes:
$ setfacl -dm u:www-data:rw-,g:pmu:r--,o:--- test/
Ainsi, tous les fichiers créés dans le répertoire test pourront être lus, écris et modifiés par www-data. On peut légitimement se poser la question de l'intérêt de ce genre de droits, mais bon
Gestion du masque
Sous GNU/Linux et UNIX, les nouveaux fichiers sont créés avec un ensemble de permissions par défaut. Enfin, en vrai, les permissions d'un nouveau fichier sont restreintes par un masque qui est appelé /umask/
On utilisera donc l'utilitaire /umask(2)/ - la manpage est rigolote - pour vérifier sa valeur, ou modifier ce masque.
# afficher le masque actuel
$ umask -S
u=rwx,g=rx,o=rx
# et en octal
$ umask
0022
Maintenant, on peut modifier ce masque (notamment dans des scripts shell ), par exemple, pour que les fichiers que nous (ou notre script) vont créer soit uniquement lisibles par tout le monde:
$ umask u=r,g=r,o=r
# ou plus court:
$ umask a=r
Si on veut rigoler un peu, on peut faire en sorte que par défaut, tous les fichiers créés n'aient aucune permissions:
$ umask a=
Ici, le "=" prend la place de toutes les permissions.
On peut aussi décider de créer nos fichiers pour qu'ils soient, par défaut, exécutables par notre groupe, sans changer les autres droits:
$ umask g+x
Bien évidemment, on peut aussi utiliser la représentation octale pour modifier le /umask/.
Maintenant, sous UNIX et GNU/Linux, les permissions "de base" pour un fichier sont 666 et pour un répertoire 777. C'est le masque /umask/ qui permet de limiter ces droits à la création.
Par exemple:
$ touch masque #no joke
$ ls -al masque
-rw-r--r-- 1 lidstah lidstah 0 sept. 9 19:27 masque
Ici, on voit que les permissions, une fois le fichier créé et le masque appliqué, sont 644, soit, 666 - 022.
De même, si je crée un répertoire, ses permissions par défaut de 777 deviendront 755 une fois le masque 022 appliqué (777 - 022).
On peut donc se faire un petit tableau récapitulatif des valeurs de masque et des droits par défaut qu'il confère sur les fichiers et les répertoires:
| valeur bit umask | permissions fichier | permissions répertoire |
|---|---|---|
| 0 | rw | rwx |
| 1 | rw | rw |
| 2 | r | rx |
| 3 | r | r |
| 4 | w | wx |
| 5 | w | w |
| 6 | x | x |
| 7 | nada | que dalle |
Expressions Régulières ("Regex")
Une expression régulière, a.k.a. /regex/, "Regular Expression", qu'on appelle aussi expressions rationnelles, c'est une séquence de caractères qui définie un modèle de recherche (typiquement, dans du texte).
Ce concept a été créé dans les années 50, notamment par Stephen Cole Kleene qui en a formalisé la description. Il a été popularisé par UNIX et notamment ses utilitaires de manipulation de texte (/grep/, /awk/, /sed/, /ed/, etc). Ken Thompson (un des créateurs d'UNIX avec Dennis Ritchie) avait implémenté dans les années 60 un éditeur (QED) implémentant les /regexes/ de Kleene sur CTS, et en a profité pour inventer la compilation JIT (/Just In Time/) dans la foulée.
On catégorise les expressions régulières en deux grands groupes:
- Les regexes POSIX
- Les regexes Perl
On les utilise énormément dans les traitements de texte (sous le capot), éditeurs de texte (c'est vous qui la tapez :p), dans l'analyse lexicale (compilateurs), et donc dans à peu près tous les langages de programmation modernes, dans les utilitaires de manipulation de texte (précités: grep, awk, etc), dans les fichiers de configuration de serveurs Web (nginx, apache2, etc), de reverses proxies (HAProxy, nginx, etc), de cache web (Varnish par exemple, nuster, etc) afin de lier des actions ou ACLs à des /patterns/.
On peut les utiliser pour rechercher rapidement toutes les occurences d'un /pattern/ dans du code, du texte, une base de données (par exemple PostGreSQL qui utilise les /regexes/ de Spencer (tcl)), etc...
Par exemple, on peut utiliser des regexes pour mettre en cache (ou non) du contenu en fonction de l'hôte, de l'user-agent, de l'URL visitée, ou pour envoyer les bots de bing, Baidu et Yandex vers une null-backend leur envoyant une 429 "Too Many Requests" (même si j'aimerai bien qu'on invente le code de retour "430 f*** you"), et tous les autres bots vers une backend de /rate-limit/.
On peut les utiliser pour /parser/ des logs, en extraire les données pertinentes et les envoyer vers un agrégateur de logs (rsyslog, logstash) et effectuer de plus amples traitements (à coups de regexes là encore) afin d'en extraire des métriques métier, business, etc, En vrai, on les utilise tout le temps
/Note: à propos de Grep et Awk, je ne saurais que trop vous recommander les vidéos de la chaîne Youtube Computerphile avec Brian Kernighan, qui a écrit AWK avec Alfred Aho et Peter Weinberger - A,W,K - et qui a aussi écrit, avec Dennis Ritchie, C Programming Language, la référence - en un mot comme en cent, ce type est un des pères fondateurs des sciences de l'informatique./
Métacaractères et regexes POSIX
Tableau récapitulatif
Tout d'abord, un petit tableau qui va nous permettre d'énumérer les métacaractères qui sont un des fondements du "langage" des expressions régulières:
| Symbole | Définition |
|---|---|
| ^ | Début de ligne |
| $ | fin de ligne |
| < | "curseur" en début de mot |
| > | "curseur" en fin de mot |
| . | n'importe quel caractère |
| ... | tout caractère énuméré entre les crochets |
| [^...] | tout sauf les caractères énumérés |
| ? | une occurence ou aucune du caractère ou de la regex précédente |
| * | 0 ou x occurence(s) du caractère ou de la regex précédente |
| pipe | séparateur OR |
| \b | mot |
| \B | pas un mot |
| \w | tout caractère alphanumérique (et underscore) |
| \W | tout caractère non-alphanumérique |
| \d | tout chiffre |
| \D | n'est pas un chiffre |
| \s | whitespace (espace) |
| \S | n'est pas un espace |
| A-Z | Toutes les majuscules de A à Z (combinable) |
| 0-9A-Z | Toutes les majuscules et tous les chiffres |
| /Note: le caractère pipe " | " ne passe pas dans les tableaux org-mode |
/Note bis: notez la différence de signification du métacaractère "*" avec le caractère joker du shell!/
Exemples simples (inutiles, ou pas
)
- Sélectionner toutes les lignes d'un fichier commençant par une majuscule:
$ egrep '^[A-Z]' fichier
- Sélectionner toutes les lignes d'un fichier commençant par une minuscule:
$ egrep '^[a-z]' fichier
- Sélectionner toutes les lignes d'un fichier contenant un chiffre:
$ egrep '[0-9]' fichier
- Sélectionner toutes les lignes d'un fichier commençant par une minuscule et contenant un chiffre:
$ egrep '^[a-z].*[0-9]' fichier
- Remplacer toutes les lignes commençant par A ou D par pwet en affichant sur /stdout/ le fichier "test":
$ sed 's/^[AD].*$/pwet/g' test
- Dans un éditeur de texte convenable (Vim, Emacs, et sûrement des horreurs comme Sublime Text ou Atom, ou des usines à gaz comme Visual Studio ou Eclipse), rechercher tous les mots commençant par "U":
/\bU\w+
# la regex étant \bU\w+
Ici, le commutateur \b indique de sélectionner tous les mots commençant par U, en /matchant/ tous les caractères alphanumériques (+ underscore) (\w) jusqu'au prochain caractère n'étant pas /matché/ par \w (+)
Globs bash
Bash ne supporte pas les regexes POSIX, contrairement à beaucoup de langages de programmation et d'utilitaires du système (grep, sed, awk, vim, etc) ou logiciels (nginx, apache, haproxy, etc). À la place il utilise un mécanisme qu'on appelle /globbing/ afin de lui permettre de proposer des fonctionnalités équivalentes.
Tout d'abord, tout comme pour les regexes, un petit tableau récapitulatif:
| caractère | signification | exemple |
|---|---|---|
| ? | /matche/ n'importe quel caractère tout seul | ls ??.c |
| * | /matche/ zéro ou plusieurs caractère(s) | ls b*.sh |
| A-Z | /matche/ le caractère s'il est dans l'intervalle | ls a-z??.sh |
| ^ | début de ligne | grep '^[a-d]' fichier |
| ! | idem que ^ mais dans le range | grep '!a-d' fichier |
| $ | fin de ligne | |
{pat1,pat2,...} |
/matche/ les noms de fichiers correspondant aux patterns | rm {*.doc,???.xlsx} |
| |
applique une ou plusieurs conditions au glob pattern | exemple plus bas |
- Matcher plusieurs patterns:
$ ls a*+(.c|.sh|.ksh)
- exemple dans un menu bash via /case/
#!/bin/bash
echo "Quelle opération voulez vous faire?"
read reponse
case $reponse in
1 | R ) echo "Reboot";;
2 | P ) echo "Poweroff";;
*) echo "nope nope nope";;
esac
Vim (Vi iMproved)
Historique
Au début il y avait /ex/ - qui était lui même une extension de l'éditeur de texte /ed/. Bill Joy, en 1976, créa une extension à /ex/ qu'il nomma /vi/, pour /visual/. En effet, l'édition, dans /ed/ et /ex/, se faisait ligne par ligne. /vi/ permettait d'utiliser /ex/ en mode plein-écran, et donc d'avoir suivant le terminal utilisé de 25 à 40 lignes de texte sur 80 colonnes utilisables, ce qui était une nette progression par rapport à l'édition ligne par ligne.
Il est à noter que Bill Joy est un des principaux contributeur des premières version de BSD UNIX (/Berkeley Software Distribution/), et aussi un des membres fondateurs de Sun Microsystems. Il a aussi créé un /shell/, le C-shell (csh, et son successeur tcsh), dont la syntaxe se rapproche plus du langage C que la syntaxe de /Bash/.
À la fin des années 80, le programmeur Bram Moolenaar (RIP 2023, à 62 ans ), heureux acquéreur d'un Amiga (une machine et un OS géniaux à l'époque, en avance sur leur temps d'une bonne décennie), était insatisfait des éditeurs de texte disponibles sur cette machine. En effet, il était habitué à travailler sur des stations UNIX où l'éditeur consacré était /vi/. Il a donc développé en 1988, sur la base de l'éditeur /STvi/, /Stevie/ originaire de l'Atari ST, un clone de vi, qu'il a nommé VIM, pour /Vi IMitation/, dont l'objectif était au départ de recréer les fonctionnalités de /vi./ Au fur et à mesure de son évolution, de nombreuses fonctionnalités ont été ajoutées (tabs, splits, plugins, fonctions d'éditions avancées, VimScript, correction orthographique, folding, etc) et son nom a évolué en /Vi IMproved/.
Il est possible de le transformer en IDE complet via ses nombreux plugins (plugins Git, plugins de CI/CD, etc), même si ses fonctionnalités de base sont en général suffisantes pour la plupart des utilisations.
Pour la petite histoire, ça fait maintenant 31 ans que j'utilise Vim - je l'ai utilisé pour la première fois en 1993 sur Amiga, grâce aux disquettes de logiciels libres /Fred Fish/ (RIP ). Même si j'utilise désormais principalement Emacs sur mes postes de travail (avec le plugin Evil permettant de retrouver les modes, raccourcis et fonctionnalités de VIM dans Emacs), je m'en sers quasiment tous les jours dans mon métier: /vi/ est quasiment toujours disponible dans les utilitaires de base d'un UNIX ou UNIX-like, et Vim est en général toujours disponible dans les paquets de ces distributions.
Installation
Très simple, suivant votre distribution, ici Debian-like:
$ sudo apt install vim
Configuration
La configuration de Vim s'effectue dans deux fichiers:
# pour tout le système
/etc/vim/vimrc
# pour l'utilisateur
~/.vimrc
On va donc lancer Vim en lui disant de modifier directement:
$ vim .vimrc
Vous devriez obtenir quelque chose de ce style:

C'est assez dépouillé, non? On va y revenir plus tard, mais vous démarrez en mode /Normal/. C'est le mode qui permet de manipuler des blocs (mots, paragraphes, caractères, lignes, etc) de texte. Mais pas d'insérer du texte. Pour se faire, on va passer en mode d'insertion en appuyant sur i, ce qui va nous donner ça:

Remarquez la dernière ligne qui indique désormais -- INSERTION -- afin de vous signifier que vous êtes en mode d'insertion.
Par défaut, Vim n'active ni la numérotation des lignes, ni la coloration syntaxique. On va remédier à ce problème, en tapant:
set nu
syntax on
Maintenant se posent deux questions:
- Comment sortir du mode d'insertion?
- Comment sauvegarder notre fichier et quitter Vim?
Quel que soit le mode (hormis le mode Normal) on peut toujours revenir au mode normal en appuyant sur Esc (et en général, pour faire bonne mesure, j'appuie toujours deux fois de suite sur Esc). Un autre moyen est la combinaison de touches Ctrl-c.
Normalement, vous devriez tous être revenus au mode normal.
Si vous tapez ":" vous allez passer en mode ligne de commande, et vous allez pouvoir taper une commande vim pour sauvegarder (w) et quitter (q). Vous devriez avoir quelque chose comme ça:

Il suffit d'appuyer sur Entrée pour valider et donc sauvegarder et quitter Vim.
On aurait pû aussi tout simplement, en revenant en mode normal, taper directement ZZ pour sauvegarder et quitter sans avoir à taper :wq. De même, on aurait aussi pu juste taper :x au lieu de :wq, x étant ici un synonyme de wq en mode ligne de commande.
De même, on pourrait vouloir quitter sans sauvegarder, dans ce cas, toujours en mode normal, on taperait:
:q!
Voilà, vous venez d'effectuer vos tous premiers pas sur Vim, c'est pas si compliqué, si? Maintenant, on va voir comment l'utiliser au quotidien. Alors, bien évidemment, il y a beaucoup, beaucoup, beaucoup d'options de configurations dans Vim, mais l'objectif de ce cours est de vous permettre de l'utiliser correctement à un niveau débutant. Au bout de 27 ans, il m'arrive encore assez souvent de découvrir de nouveaux "trucs" possibles avec Vim. C'est un éditeur dont la courbe d'apprentissage est très élevée, mais dont la maîtrise vaut vraiment le coup.
Utilisation (de base)
Un éditeur de texte modal
Ainsi qu'on vient juste de le voir, Vim est un éditeur /modal/. Nous avons vu très rapidement le mode Normal, le mode d'Insertion, le mode ligne de commande, et comment revenir au mode Normal (Esc ou Ctrl-c).
Mais quels sont tous les modes de Vim?
| Mode | Touches | Fonctionnalités |
|---|---|---|
| Normal | Esc,C-c | manipulations de texte (suppression, copier/coller, déplacement rapide) |
| Insertion | i,I,A,O,r,R,s,S | Insertion,remplacement, etc de texte |
| ligne de commande | : | commandes Vim (sauvegarder, quitter, exécuter une commande shell, etc) |
| Visuel | v, S-v, C-v | Sélection avancée (caractère, ligne, bloc) pour manipulation de texte |
| Sélection | Visuel -> sélectionner -> Ctrl-G -> Insertion | |
| Ex | Q | Q -> ex-mode -> ensemble de commandes Vim -> :vi pour repasser en normal |
Buffers, tabs, splits
Buffers
Tout d'abord, il faut savoir que tout fichier ouvert dans Vim est copié dans un /buffer/ (tampon), où les modifications seront effectuée, ce /buffer/ étant écrit sur le fichier lorsqu'on exécute la commande :w. Bien évidemment, Vim permet de travailler sur plusieurs buffers en même temps.
Plutôt qu'une longue explication, touffue, on va plutôt regarder directement comment ça fonctionne.
On va d'abord lancer Vim:
$ vim
On a aucun fichier ouvert, on va en ouvrir un (en read-only). Pour ça, en mode Normal, on va taper :e chemin/nom_du_fichier, par exemple:
:e /etc/fstab
Puis on va en ouvrir un second:
:e /etc/bash.bashrc
Zut, notre premier fichier a disparu! Rassurez-vous, il est toujours là! si on tape la commande :buffers on va obtenir quelque chose de ce style:

Là, on voit que j'ai deux buffers ouverts:
Le numéro 1, fstab, et le second (qui est celui sélectionné, d'où le "a"ctive) qui est mon fichier bash.bashrc. On appuie sur entrée pour sortir du mode buffer. Maintenant, si je veux travailler sur mon fstab, c'est à dire mon buffer numéro 1, je vais simplement taper:
:b1
Et ainsi de suite...
Tabs
Vim nous permet aussi d'utiliser des onglets, ce qui peut être pratique si on édite quelques fichiers et qu'on veut naviguer rapidement entre eux.
Ouvrons un nouvel onglet:
:tabnew
Maintenant, ce nouvel onglet est vide. Je vais donc lui assigner un de mes buffers, par exemple notre buffer numéro 2, bash.bashrc, qui n'est pas visible actuellement. Je tape donc :b2, et je devrais obtenir quelque chose de ce style:

Pour bouger d'un onglet à l'autre, en mode Normal, je vais taper gt pour passer à l'onglet suivant, et gT pour passer à l'onglet précédent.
Splits
On a vu les buffers, les onglets, c'est bien sympa, mais parfois on peut travailler sur des fichiers de plusieurs centaines voire milliers de lignes, et il peut être utile d'avoir des divisions verticales ou horizontales et dans chacun de ses splits une vue différente du fichier qu'on édite, ou plus simplement des fichiers différents dans chaque /split/. Par exemple, ça donne ça avec un fichier de config HAProxy:

Reprenons notre fichier bash.bashrc, dans notre second onglet. On va créer un split vertical. On se place en mode Normal, puis on tape: Ctrl-w v (c'est à dire: Ctrl-w puis v). Et hop. Pour passer d'un split à l'autre, on peut utiliser le raccourci Ctrl-w w (ou Ctrl-w h pour aller à gauche, Ctrl-w l pour aller à droite, Ctrl-w j pour aller en bas, et Ctrl-w k pour aller en haut).
Si on veut créer un split horizontal, on utilisera Ctrl-w n. Pour fermer un split, on fait simplement :q.
Les splits sont aussi pratiques pour avoir, par exemple, la /manpage/ correspondante au fichier de configuration qu'on est en train de modifier, ou de la doc, etc.
Manipulations de base, déplacement, copier/coller (yank/paste), sélections visuelles
Notion de verbe, complément, objet
Les commandes de Vim répondent à une syntaxe précise. par exemple di" signifie "Delete Into double-quotes", et provoque la suppression des caractères situés entre deux double-quotes. De la même manière, ciw signifie Change In Word, et provoquera la suppression du mot sur lequel le curseur se trouve et le passage en mode d'insertion. De la même façon, d$ signifie "supprime tout du curseur à la fin de la ligne".
Dès lors, une commande comme:
ggdG
Signifiera: déplace toi au début du fichier et supprime le contenu jusqu'à la fin du fichier. (bourrin)
ci) ou ci} effectueront la même chose entre deux parenthèses, etc.
de même, yi" signifie "Yank in quotes" et copiera le contenu situé entre deux double-quotes.
Déplacements
Alors, lorsqu'on est en mode normal, on se déplace non pas avec les touches du curseur, mais avec les touches h, j, k et l. Pourquoi? Alors déjà car c'est mieux d'un point de vue ergonomie d'avoir toujours les mains sur le /home row/ de son clavier, et ensuite... un peu à cause de ça:

Ça c'est le clavier d'un vieux terminal des années 70. Notez les touches H,J,K et L de ce clavier
Alors bien sûr, se déplacer caractère par caractère et ligne par ligne dans un fichier de plusieurs centaines de lignes peut vite devenir... lourdingue . Pour ça, on a plusieurs possibilités (toujours en mode normal, hein):
| Combo | Effet |
|---|---|
| H | déplace à la première ligne visible |
| L | déplace à la dernière ligne visible |
| M | déplace au milieu de l'aire visible |
| gg | déplace au début du fichier |
| G | déplace à la fin du fichier |
| 160G | déplace à la ligne 160 |
| fz | déplace à la première occurence de z dans la ligne depuis le curseur |
| Fz | déplace à la premiere occurence de z dans la ligne AVANT le curseur |
| t,T | idem que f et F mais déplace au caractère précédent le caractère recherché |
| 0 | déplace au début de ligne |
| ^ | déplace au premier caractère de la ligne |
| $ | déplace à la fin de la ligne |
| A | déplace après le dernier caractère de la ligne et passe en mode d'insertion |
| I | déplace au début de la ligne et passe en mode d'insertion |
| / | lance la recherche sur un pattern |
| n,N | navigue entre les résultats de la dernière recherche |
| mX | crée une marque X au curseur |
| 'X | revient sur la marque X (marques possibles: a-zA-Z |
| w | déplace de mot en mot |
| b | déplace de mot en mot en arrière |
| E | déplace de mot en mot en positionnant à la fin de chaque mot |
| Ctrl-d, Ctrl-u | déplace d'une demi page en avant ou en arrière |
| déplace de paragraphe en paragraphe | |
| % | déplace du début à la fin d'un bloc de code et inversement |
Exercez vous sur le buffer bash.bashrc que nous avons ouvert pour vous faire la main. Je ne vous demande pas d'apprendre tout ça par coeur hein, mais avec le temps vous verrez que ça devient comme une seconde nature
Notez aussi que la majorité des commandes de Vim permettent, en les préfixant d'un nombre, de les exécuter plusieurs fois. Par exemple 4w vous déplacera de 4 mots.
Suppression, remplacer du texte, undo, redo, copier, coller
Pour supprimer une ligne directement, on peut taper dd. La ligne est alors placée dans le presse-papiers. on peut alors la coller après le curseur avec p et avant le curseur avec P.
De même, on peut copier (/yank/) une ligne directement en tapant yy.
On peut, de même, supprimer ou copier du texte en suffixant l'opérateur d ou y d'un opérateur de mouvement.
Par exemple:
# supprimer 2 mots
d2w
# yanker 2 mots
y2w
# supprimer jusqu'au caractère X
dtX
# copier jusqu'au caractère X
ytX
On peut remplacer un caractère en se positionnant dessus, puis en tapant r et le caractère voulu.
On peut passer en "mode" de remplacement en tapant R puis le texte remplaçant l'ancien texte. Comme pour le mode d'insertion, il suffit de taper Esc ou Ctrl-c pour sortir de ce mode.
On peut aussi supprimer le caractère sous le curseur avec la touche x.
Exemple: imaginons que j'ai fait une typo, j'ai tapé outli au lieu d'outil:
# je me place sur le l
outli
# je tape xp
outil
Enfin, Vim propose un undo infini. En mode normal, on utilise u pour undo, et Ctrl-r pour redo.
On peut citer des plugins sympathiques comme Gundo pour obtenir un undo plus puissant (arbre des éditions, etc).
Dernière note, pour répéter la dernière commande - par exemple de mouvement, on peut utiliser la touche ;.
Sélection visuelles
Bien sûr, on peut aussi utiliser les modes visuel, visuel-ligne et visuel-bloc pour respectivement sélectionner depuis le curseur, sélectionner des lignes, ou sélectionner des blocs et leur appliquer les mêmes manipulations qu'en mode normal.
| Combo | Mode |
|---|---|
| v | visuel |
| Shift-v | visuel ligne |
| Ctrl-v | visuel bloc |
On effectue notre sélection de la même façon qu'on se déplace (hjkl, 160G (pour sélectionner depuis le curseur jusqu'à la ligne 160, etc), et ainsi de suite). Puis on sélectionne l'action à effectuer.
Là je vais utiliser des exemples visuels que je vous recommande chaudement d'effectuer vous aussi sur votre poste de travail, mais imaginons ce bloc de configuration haproxy:
server worker1 172.17.0.1:8080 check
server worker2 172.17.0.2:8080 check
server worker3 172.17.0.3:8080 check
server worker4 172.17.0.4:8080 check
Disons que je veux ajouter rapidement les options maxconn 10 et maxqueue 20 afin de limiter le nombre de connexions concurrentes sur chaque serveur (disons que c'est une backend de garage pour bots de crawl). Je vais d'abord me placer à la fin de la première ligne, puis activer le mode visuel-bloc, "descendre" jusqu'à la dernière ligne, puis taper (toujours en mode visuel bloc):
#A pour insérer après le dernier caractère de la ligne
Amaxconn 10 maxqueue 20 Esc
Ce qui va me donner:
server worker1 172.17.0.1:8080 check maxconn 10 maxqueue 20
server worker2 172.17.0.2:8080 check maxconn 10 maxqueue 20
server worker3 172.17.0.3:8080 check maxconn 10 maxqueue 20
server worker4 172.17.0.4:8080 check maxconn 10 maxqueue 20
Comme vous voyez, c'est quand même pratique, non?
Macros
Bien évidemment, on peut utiliser des macros avec Vim.
Pour activer l'enregistrement de macro, on va utiliser la touche q en mode normal, suivie de la lettre à laquelle on va assigner cette macro. On effectue ensuite les manipulations qu'on va vouloir répéter, puis on va appuyer à nouveau sur q pour enregistrer la macro et revenir en mode normal.
Pour utiliser cette macro, on utilisera (en mode normal), la touche @ suivie de la lettre à laquelle nous avons assigné cette macro. Évidemment, si on précède @ d'un nombre, la macro sera répétée autant de fois que voulu.
Recherche et substitution
Pour effectuer une recherche depuis le curseur vers la fin du fichier (ça boucle donc une fois le dernier terme trouvé en fin de fichier, on repart du début), on utilise la touche / puis on tape son motif de recherche. Les regexes sont prises en compte bien évidemment. Pour effectuer une recherche inverse, on utilise la touche ?.
Pour effectuer une substitution, on utilisera la commande du mode ligne de commande s (donc, ):
| Commande | Action |
|---|---|
| substitution dans la ligne actuelle | |
| :%s | substitution dans tout le fichier |
| :20,42s | substitution de la ligne 20 à la ligne 42 |
on suffixe cette commande du motif de recherche et du motif de remplacement, en règle générale séparés par le caractère / et on termine par le CommandFlag (ici, gc) Exemple:
# je veux remplacer dans tout le fichier
# tous les mots commençant par Z
# par le terme "bidule"
:%s/\bZ\w+/bidule/gc
Ici, le CommandFlag gc signifie: (g)lobal (c)onfirm. Global indique d'effectuer la manipulation sur tout le buffer (si on l'omet, seule la première occurence sera modifiée), et c demande confirmation à chaque itération.
| CommandFlag | Action |
|---|---|
| rien | première occurence du motif de recherche |
| g | toutes les occurences |
| c | Demande confirmation à chaque substitution |
| i | insensible à la casse |
| I | force à respecter la casse |
De même, si notre motif de recherche utilise le caractère "/", par exemple une liste d'urls, on peut soit l'échapper avec "", soit utiliser un autre motif de séparateur (par exemple "#" ou ";"). À noter que tout ce qui est dit dans ce point s'applique aussi aux substitutions avec le Stream EDitor (sed) qu'on peut être amené à souvent utiliser dans des scripts
Pour aller plus loin.
- Vimtutor (intégré à Vim):
$ vimtutor
- A Byte of Vim (pdf):
https://vim.swaroopch.com/byte-of-vim.pdf
En Anglais et pour Vim 7 (on est au 8 et des poussières) mais toujours pertinent, un ebook qui vous permettra d'apprendre, et d'approfondir vos connaissances, sur Vim.
Scripting Bash
Hello World!
Ouvrons un fichier que nous appellerons helloworld.sh (original hein?):
#!/bin/bash
echo "Hello, world!"
On sauvegarde, on quitte, et on:
# rend le script exécutable
$ chmod u+x helloworld.sh
# on l'exécute
$ ./helloworld.sh
Notez bien le "./": comme "helloworld.sh" n'est pas dans le PATH, on ne peut l'exécuter qu'en précisant son chemin. On reviendra sur la variable PATH plus loin.
En règle générale, si vous voulez exécuter vos scripts sans avoir à préciser leur chemin, il faut les placer dans le PATH. Deux solutions:
- soit vous ajoutez un répertoire au PATH, par exemple $HOME/bin
- soit vous placez votre script dans un des répertoires prévus pour ça:
# Linux
/usr/local/bin
#BSD
/usr/local/bin
ou
/opt/bin/
Notez aussi qu'en général, les programmes que vous compilez vous mêmes seront en général placés dans ces répertoires sur un:
$ sudo make install
Si le Makefile est propre, bien sûr.
Mais évitez de les placer dans les répertoires système comme /bin et cie.
Et voilà pour hello world. Trop dur, hein?
Exécution d'un script
Jobs
Pour lancer une commande en arrière-plan, on utilisera le caractère "&" à la fin de la ligne de commande. Exemple, avec vim:
$ vim &
[1] 2604
La commande jobs nous permet de lister les jobs en cours:
$ jobs
[1]+ Stoppé vim
Pour ramener vim au premier plan:
$ fg %1
Si on veut remettre Vim en arrière plan, on tapera Ctrl-z dans Vim
Alors c'est un exemple succint, mais on peut imaginer une commande dont l'exécution est particulièrement longue. Dans le cas de Vim, programme interactif, jobs nous indique qu'il est Stoppé, cependant une autre commande non-interactive poursuivra son exécution en arrière plan.
À l'inverse, on peut forcer une commande Stoppée en arrière plan (avec un Ctrl-z, typiquement) à continuer son exécution en utilisant la commande bg, de la même façon que fg.
Enfin, imaginons un script lançant plusieurs commandes "longues" (et potentiellement monothread). Plutôt que de les lancer une par une, on pourrait les lancer toutes ensemble en arrière plan, et utiliser la commande wait pour que le script attende la fin de l'exécution de ses commandes:
#!/bin/bash
commande_super_longue1 &
commande_super_longue2 &
commande_super_longue3 &
wait
suite du script...
De même, on peut utiliser l'instruction:
suspend
Pour suspendre le shell (même effet que Ctrl-z).
Par exemple, ouvrons un shell enfant à notre shell actuel:
$ bash
$ suspend
[1]+ Stoppé bash
$ jobs
[1]+ Stoppé bash
shebang
Le /shebang/, c'est la combinaison des caractères # et !. Tout script shell commence en général par cette ligne. Elle permet d'indiquer quel interpréteur on veut utiliser pour la suite du script. Exemples:
#!/bin/bash
#!/bin/ksh
#!/usr/bin/python
#!/usr/local/bin/zsh_compilé_a_la_maing
On peut aussi - et c'est même conseillé pour des raisons de portabilités, notamment si votre script est censé pouvoir tourner sur plusieurs distributions GNU/Linux ou d'autres UNIX comme FreeBSD, ou AIX, utiliser cette ligne:
#!/usr/bin/env bash
Qui va interroger les variables d'environnement du système pour déterminer où se trouve l'interpréteur bash, qui peut varier en fonction de l'OS utilisé, exemple:
# Debian GNU/Linux
$ which bash
/usr/bin/bash
#OpenBSD
$ which bash
/usr/local/bin/bash
Tiens, d'ailleurs, on voit que bash n'est pas localisé dans /bin, hors, ça marche dans nos scripts. Comment ça se fait? Vérifions donc ce répertoire:
$ ls -al /bin
lrwxrwxrwx 1 root root 7 août 3 2019 /bin -> usr/bin
Oh, mais... /bin est un lien symbolique en fait sur Debian! Voilà la réponse à notre question!
source
La commande /source/ permet de charger un fichier de fonctions à l'intérieur du script shell courant, ou d'une invite de commande. On peut s'en servir, par exemple, pour provisionner des variables qui seront utilisées dans notre script en effectuant quelques vérifications sur l'OS utilisé, la disponibilité ou non de telle ou telle commande, etc.
Par exemple, créons un premier script (on va l'appeler srctest.sh) qui va provisionner une variable, et vérifier si nous sommes root, ou pas:
#!/bin/bash
PWET=$(date)
is_root() {
[ $(id -u) -eq 0 ] && return $TRUE || return $FALSE
}
Puis créons un script monscript.sh tout con qui va sourcer notre premier fichier:
#!/bin/bash
source srctest.sh
echo "La valeur de PWET est $PWET"
is_root && echo "bouh, t'es en root" || echo "ok, t'es pas root"
/source/ nous a donc permis de provisionner une variable et d'appeler une fonction depuis un autre script dans notre script, ce qui peut être pratique: imaginons un script nettement plus complexe que le nôtre, il peut être intéressant, pour le rendre plus lisible et plus facile à configurer, de stocker certaines variables et fonctions dans un fichier que l'on sourcera lors de l'exécution de notre méga-script. Et il sera plus simple de modifier certaines variables ou fonctions depuis un fichier plus petit et dédié à ça, plutôt que de retrouver une variable au milieu de 15000 lignes (quoiqu'avec un éditeur comme Vim... :p)
Oh, mais qu'est ce qu'on a fait dans nos deux scripts? on a créé une variable (qui stocke le résultat d'une commande) et on a effectué un test. Ce qui m'amène au prochain chapître, les variables!
Variables
Je ne vais pas vous faire l'insulte de vous expliquer ce qu'est une variable, on va surtout regarder comment elles sont implémentées en Bash
Réécrivons donc notre hello-world de tout à l'heure:
#!/bin/bash
HELLO="Hello World!"
echo $HELLO
Ici, nous venons de créer une variable qu'on pourrait considérer comme étant globale (fear). On peut par contre créer des variables /locales/ à nos fonctions, par exemple, si on modifie (encore) notre Hello World précédent:
#!/bin/bash
HELLO="Hello"
function hello {
HELLO="World!"
echo $HELLO
}
echo $HELLO
hello
echo $HELLO
Que se passe-t-il lorsque j'exécute mon script helloworld?
On peut aussi assigner le résultat d'une ou plusieurs fonctions à nos variables (encore heureux), et ce avec deux syntaxes différentes (youpi):
#!/bin/bash
DATE=`date`
DATE2=$(date)
echo $DATE
echo $DATE2
Alors, vous allez me dire: à quoi ça sert d'avoir deux méthodes pour faire la même chose?
En fait, la syntaxe $() est plus récente que la syntaxe utilisant les backticks. Et on peut faire des choses plus rigolotes comme:
#!/bin/bash
PWET=$(echo $(ls)" "$(date))
echo $PWET
ce qu'on ne pourrait pas faire avec les backticks, qui seraient ambigüs du point de vue de l'interpréteur.
Variables spéciales
Bash possède plusieurs variables spéciales.
| Variable | Signification |
|---|---|
| $IFS | Séparateur (par défaut espace) |
| $? | code de retour du script |
| $! | PID d'un process background (wait) |
| $$ | PID du shell courant |
${-} |
options du shell courant |
${_} |
dernier argument de la dernière commande exécutée |
Arguments
Bien sûr, on peut passer des arguments à nos scripts, pour pouvoir les tester ensuite. Mais comment faire en sorte que notre script les reconnaisse? On utilise pour cela les /variables positionnelles/, dont la syntaxe est ${N}, ou N représente la position de l'argument. Si N < 10, on peut omettre les {}.
Essayons donc nos nouveaux amis, dans un script qu'on va nommer args.sh (par exemple):
#!/bin/bash
echo "Premier argument: ${1}"
echo "Second argument: ${2}"
testons notre script!
$ bash arg.sh pwet plop
$ bash arg.sh pwet
Que se passe-t-il lorsqu'on exécute la seconde ligne?
On peut aussi utiliser la variable ${@} pour lister tous les paramètres passés au script - ou à la fonction, on y reviendra plus tard:
#!/bin/bash
echo ${@}
for i in ${@}
do
echo $i
done
On peut aussi utiliser la variable ${*} pour faire... la même chose, à un détail près: son résultat est influencé par la valeur de la variable IFS vue plus haut. Par exemple:
#!/bin/bash
IFS=";"
echo ${@}
echo ${*}
Quelle est la différence?
Enfin, la variable ${#} permet d'obtenir le nombre d'arguments qui ont été passés au script (ou à la fonction). Typiquement, si votre script demande 3 arguments, on pourra tester la valeur de cette variable et sortir du script si seulement 2 arguments on été passés.
Imaginez un script ou une fonction qui récupère l'id d'un client, son mail, et un flag depuis une base de données. Si le flag est vide, le script supprime l'utilisateur. Si ce troisième argument n'est pas passé au script ou à la fonction, il sera toujours vide, et donc... on supprimera toujours l'utilisateur. Oups. (bon en vrai... on évitera ce genre de flag dans une BDD, c'est pour l'exemple).
getopts
/getopts/ est une fonction interne de bash qui est la version "bashisée" de getopt. Contrairement à /getopt/ qui peut varier entre OS, /getopts/ est définie par le standard POSIX.
getopts nous permet de /parser/ les options courtes passées au script. Vous voyez la commande ls -l ? eh bien -l est une option courte. une option "longue" serait ls --list (qui n'existe pas hein, ne cherchez pas. par contre -L est équivalent à --dereference (liens symboliques)).
getopts peut aussi /parser/ des combinaisons, comme dans ls -al (plus court que ls -a -l). Attention, getopts ne peut pas parser les options longues comme --help.
getopts va parcourir le contenu de la variable ${@} pour extraire de cette chaîne de caractère des options et leurs arguments. Imaginons un script:
$ monscript -a option1 -b adresse_ip
La syntaxe de getopts est celle-ci:
getopts optstring optname [ argument ]
Quand getopts va /parser/ ${@} il va comparer son contenu avec le contenu des options définies dans /optstring/. S'il en trouve une, il la place dans une variable optname. S'il ne matche rien, il placera dans optname le caractère "?".
De même, si l'option nécessite un argument, il sera placé dans la variable $OPTARG. S'il ne trouve pas d'argument alors qu'il devrait y en avoir un, il placera le caractère ":" dans la variable OPTARG.
Puis, getopts incrémente son compteur positionnel $OPTIND, et procède au traitement de l'option suivante.
On peut signifier à getopts la fin des options avec le caractère spécial "--" (d'où le fait qu'il ne prenne pas en compte les options longues).
Lorsque getopts a terminé de traiter les options passées au script, il retourne simplement /false/. Typiquement, on le fera tourner dans une boucle, typiquement une boucle /while/ dans une construction du type:
while getopts options optname [argument]
do
...
done
Bon, maintenant, comment ça fonctionne? Imaginons que notre script a besoin de savoir s'il doit par exemple lister (-l) ou copier (-c) le contenu d'un fichier (oui, c'est nul comme exemple, mais c'est simple), ou afficher sa taille (-s, size) sans argument (donc, toujours le même fichier. Par exemple, un fichier de log):
getopts "cls" optname
Maintenant imaginons qu'on aimerait bien passer un argument à copier et lister. Pour se faire, on ajoute le caractère ":" après chacune de nos options ayant besoin d'un argument.
getopts "c:l:s" optname
Imaginons un script - tout pourri - qui va nous permettre de dire bonjour à quelqu'un plusieurs fois (ou une fois par défaut). Oui c'est nul. Mais c'est simple à comprendre .
Notre script aura pour syntaxe:
# pour dire 42 fois coucou à Jean-Eudes
$ coucou -n Jean-Eudes -t 42
On pourrait faire quelque chose du style:
#!/bin/bash
NAME=""
TIMES=1
while getopts "n:t:" options
do
case "${options}" in
n) NAME=${OPTARG};;
t) TIMES=${OPTARG};;
*) exit 1;;
esac
done
for i in $(seq 1 $TIMES)
do
echo "Coucou "$NAME"!"
done
Alors, c'est gentil, mais on ne teste pas plein de conditions possibles dans notre script! Que se passe-t-il si je fais:
$ scriptopt -n Truc -t Pwetpwetlolilol
Comment je pourrais éviter ce problème?
Ce qui nous amène aux tests et conditions en Bash
Tests et conditions
Bien évidemment, Bash nous permet de tester le contenu d'une variable, le retour d'une fonction ou d'une commande, et ainsi de suite. On va distinguer deux grands types de conditions:
- Les tests (commande intégrée à bash /test/ , simples et doubles crochets)
- Les conditions auxquelles vous êtes habitués: if, then, elif (else if), else
Tests et tests à simple crochets
Tout d'abord, contrairement à if..elif..else, /test/ retourne juste un statut de sortie égal à 0 (true) ou 1 (false). Point. On peut l'écrire de la sorte dans un shell:
NUM=un chiffre qui vous plaît, exemple: NUM=755
test $NUM -gt 5 && echo "yep" || echo "nope"
Ici, si le résultat du test est vrai, test va sortir avec une valeur de code de retour égale à zéro. "&&" indique la marche à suivre si la dernière commande est sortie avec un code de retour égal à 0 (i.e. tout s'est bien passé), et "||" indique la marche à suivre si le code de retour de la dernière commande est différent de 0. "-gt" signifie "greater than", plus grand que.
On peut abréger /test/ en mettant notre condition entre crochets:
[ $NUM -gt 5 ] && echo "yep" || echo "nope"
NOTE: les tests à simple crochets sont compatibles avec sh
De même, test nous permettra de tester l'existence de fichiers, répertoires:
$ [ -e /etc/shadow ] && echo "ouf" || echo "oh mer.."
- Tests simples
| Test | Signification |
|---|---|
| -gt | plus grand que |
| -lt | plus petit que |
| -eq | égal |
| -ne | n'est pas égal |
| -ge | supérieur ou égal |
| -le | inférieur ou égal |
- Tests sur les fichiers, variables, expressions, options...
| Test | Signification |
|---|---|
| -a,-e | teste si un fichier existe |
| -b | teste si fichier de bloc (genre /dev/sda, fear) |
| -c | fichier de caractères spéciaux (/dev/urandom par ex) |
| -d | teste si le fichier est un répertoire |
| -f | teste si le fichier existe et est un fichier standard |
| -g | teste si le fichier est setgid |
| -h,-L | teste si le fichier est un lien symbolique |
| -k | teste si on a le sticky-bit sur le fichier |
| -p | ce fichier est il un fichier fifo (pipe file) |
| -r | le fichier est il lisible par l'utilisateur? |
| -s | le fichier existe et n'est pas vide |
| -S | le fichier est-il une socket? |
| -t | file descriptor (velu) |
| -u | teste si le fichier est setuid |
| -w | teste si l'utilisateur peut écrire le fichier |
| -x | teste si l'utilisateur peut exécuter le fichier |
| -O | teste si le fichier appartient à l'user |
| -G | teste si le fichier appartient au groupe de l'utilisateur |
| -N | teste si le fichier a été modifié depuis sa dernière lecture (atime) |
| f1 -nt f2 | teste si f1 est plus récent que f2 |
| f1 -ot f2 | f1 est-il plus vieux que f2? |
| f1 -ef f2 | f1 est il un lien en dur de f2 (hardlink) |
| test -n chaîne | teste si la chaîne n'est pas vide |
| -z | true si la chaîne testée est vide |
| chaine1 = chaine2 | teste si chaine1 est identique à chaine2 |
| chaine1 != chaine2 | true si chaine1 est différente de chaine2 |
| "chaine1" < "chaine2" | velu: teste si chaine1 est sortie lexicographiquement avant chaine2 |
| "chaine1" > "chaine2" | idem mais dans l'autre sens |
| -o option | vrai si l'option shell /option/ est activée |
| -v variable | vrai si la variable shell est /set/ |
| -R variable | velu, vrai si la variable est /set/ ET que c'est une référence (directe ou indirecte |
| ! expression | vrai si expression est nulle |
| expr1 -a expr2 | vrai si l'expression 1 et l'expression 2 sont nulles |
| expr1 -o expr2 | vrai si une des deux expressions n'est pas nulle |
De même, il est possible d'utiliser le ET et le OU logiques dans un test, respectivement avec les arguments -a et -o:
[ $num -lt 1 -o $num -eq 100 ] && echo "num est soit inférieur à 1 soit égal à 100" || echo "num est entre 1 et 100"
[ $num -lt 1 -a $num -eq 100 ] || echo "jamais vrai!"
Tests à doubles crochets
Contrairement aux tests à simple crochets, les tests à double crochets ne sont pas compatibles avec sh mais elles permettent de faire plus de choses , par exemple utiliser le wildcard:
[[ "$nom" == *yolo* ]] && echo "contient bien yolo" || echo "pas de yolo"
Et permettent aussi d'utiliser les expressions régulières
nom="thierry"
[[ "$nom" == [tT]hierry ]] && echo "it's me" || echo "who the fuck are you?"
Conditions
Alors on peut déjà faire beaucoup de choses avec /test/, mais avec les opérateurs if, elif et else, on peut combiner ces conditions afin d'effectuer des tests bien plus complexes:
#!/bin/bash
if [ -f ./thisisatest ]
then
echo "le fichier existe"
elif [ -d ./thisisatest ]
then
echo "le répertoire existe"
else
echo "je cree le fichier"
touch thisisatest
fi
bien sûr, on peut mettre autant de /elif/ que l'on veut.
Un autre de type de condition est le test case, que nous avons déjà utilisé:
#!/bin/bash
echo "votre nom:"
read answer
case $answer in
Gates ) echo "Retourne sous Windows!";;
Torvalds ) echo "NVidia, F*** you!";;
*) echo "mais t'es qui toi?";;
esac
Calcul
Opérateurs arithmétiques
| Opérateur | Signification |
|---|---|
| var++,var-- | post-incrémente/décrémente la variable |
| ++var,--var | pré-incrémente/décrémente la variable |
| !,~ | négation logique, négation bit à bit |
| ** | puissance |
| *,/,% | multiplication, division, modulo |
| +,- | addition, soustraction |
| expr ? expr | opérateur conditionnel |
Attention, il en existe d'autres, je reste dans le conventionnel ici.
Calcul sur des entiers
Quatre possibilités:
- le vieux /expr/
- /let/
- /declare/
- Les expansions arithmétiques Bash
# expr
$ expr 1 + 1
$ expr $var / 3
$ expr $var \* 10
# let
$ let x=3 y=42 z=x*y a=z**2
$ echo $x $y $z $a
# declare
$ declare -i x=3 y=42 z=x*y a=z**2
$ echo $x $y $z $a
# expansion
$ num=3
$ echo $((num+2))
$ moarnum=$((num*4))
$ ((num+=3))
$ echo $((num++))
$ ((x=3,y=42,z=x*y,a=z**2)); echo $x $y $z $a
Calcul sur des flottants
Trois solutions:
- printf (builtin)
- awk
- bc
Concernant printf, on va préciser... hum, la précision justement, par exemple ici 3 chiffres après la virgule, puis le multiplieur et enfin l'exponentielle inverse du multiplieur. yeah, ça vend du rêve:
# ze formule, mult=multiplier
$ printf %.<precision>f "$((10**<mult> * 2/3))e-<mult>
# calculer 2/3 à 3 chiffres après la virgule:
$ printf %.3f "$((10**3 * 2/3))e-3"
# calculer 2/3 à 6 chiffres après la virgule:
$ printf %.6f "$((10**6 * 2/3))e-6"
0.666
AWK - écrit entre autre par Brian Kernighan:
$ awk "BEGIN {print 2/3}"
$ awk "BEGIN {x=2/3; y=42; z=x*y; print z}"
Et enfin bc:
$ sudo apt install bc
$ echo "2/3" | bc
0
# oups!
$ echo "scale=3;2/3"|bc
0.666
$ bc <<< "scale=3;2/3"
0.666
Bon, soyons francs: Bash n'est pas vraiment fait pour faire des maths. Utilisez donc /bc/ ou /awk/ pour tout ce qui est plus compliqué que des opérations sur les entiers
Boucles
Là encore, comme pour les variables, je ne vais pas vous faire l'insulte de vous expliquer ce qu'est une boucle
for
la boucle /for/ nous permet d'itérer sur une /liste/. Par exemple:
#!/bin/bash
for i in $(seq 1 10)
do
echo $i
done
On peut aussi, plutôt que d'utiliser le vieil utilitaire /seq/, utiliser un intervalle:
#!/bin/bash
for i in {1..10}
do
echo $i
done
De même, on peut préciser un pas à l'intervalle, par exemple ici, compter de 10 à 0 de 2 en 2:
#!/bin/bash
for i in {10..0..2}
do
echo $i
done
On peut aussi itérer sur le contenu d'un fichier ou d'une variable:
#!/bin/bash
liste_ips="10.10.0.1 10.10.0.2 10.10.0.3"
for i in $liste_ips
do
echo "je vais ban "$i
done
Et bien sûr sur le résultat d'une commande:
#!/bin/bash
for i in $(tail -100000 /var/log/haproxy.log | grep "wp_admin" | awk '{ print $6 }' |\
sed 's/:.*$//g' | sort -n | uniq)
do
iptables -I INPUT -s $i -j DROP
done
à votre avis, qu'est ce qu'on fait là? Indice: c'est très très bourrin
De même, on peut sortir d'une boucle si une condition est vérifiée avec l'instruction /break/:
#!/bin/bash
for i in ${1..10}
do
if [ $i -eq 8 ]
then
break
fi
echo $i
done
(Bon, là, ça ne sert pas à grand chose, mais imaginons qu'on teste l'espace disque dispo sur le backup, etc)
à l'inverse, on peut utiliser l'instruction /continue/ pour dire à un script d'ignorer l'itération en cours et de continuer à la prochaine:
#!/bin/bash
for i in ${1..10}
do
if [ $i -eq 8 ]
then
continue
fi
echo $i
done
/Question: dans les deux boucles précédentes, que se passe-t-il si je place le echo avant la condition?/
les boucles /for/ sont aussi très intéressantes pour effectuer des opérations sur, par exemple, un ensemble de fichiers, en utilisant les /wildcards/:
#!/bin/bash
REP=$HOME/org
for i in $REP/*.org
do
pandoc $i -t latex -o $i.pdf
done
while et until
Les boucles /while/ ("tant que") et /until/ ("jusqu'à ce que" - la différence est importante) nous permettent d'exécuter un bloc de commandes tant qu'une condition n'est pas vérifiée, ou jusqu'à ce qu'elle soit vérifiée. On peut aussi facilement créer des boucles infinies avec while:
#!/bin/bash
while true
do
echo "I'll never stop"
sleep 5s
echo "Ctrl-c pour quitter"
done
Maintenant, faisons un test un chouilla plus compliqué:
#!/bin/bash
compteur=1
while [ $compteur -lt 10 ]
do
echo $compteur
((compteur++))
done
avant de l'exécuter, à votre avis quel sera le résultat?
/until/ fonctionne de façon très similaire, par exemple:
#!/bin/bash
compteur=1
until [ $compteur -gt 10 ]
do
echo $compteur
((compteur++))
done
Quel sera le résultat?
En vrai, on peut tout à obtenir le même résultat avec un /while/ - Quel serait-il à votre avis?
select
On peut utiliser l'instruction /select/ pour créer un menu simple:
#!/bin/bash
player="Ken Ryu Blanka Zangieff Quit"
PS3='Select your player: '
select i in $player
do
if [[ $i == 'Quit' ]]
then
break
fi
echo Hello $i
done
Fonctions
On l'a déjà vu, on peut créer une fonction de deux façons différentes:
nom_fonction() {
commandes
}
# Ou
function mafonction {
commandes
}
Les deux méthodes sont identiques, c'est juste une question de préférence personnelle
On peut bien sûr (et c'est l'intérêt) passer des /arguments/ à notre fonction:
#!/bin/bash
function write_sthg {
echo Hello $1
}
write_sthg World
On peut aussi dire à la fonction de retourner un code de sortie (afin de pouvoir l'analyser avec des tests sur la valeur de $? par exemple):
#!/bin/bash
function write_sthg {
echo Hello $1
return 42
}
write_sthg World
echo $?
en règle générale, un code de retour égal à zéro signifie que tout s'est bien déroulé. Un code de retour différent peut être généré pour différents types d'erreur. De même, /return/ peut prendre la valeur d'une variable.
On a vu dans le chapître sur les variables que l'on pouvait assigner des variables locales à une fonction, donc je ne vais pas m'étendre là-dessus
Tableaux
Aaah, les tableaux en bash... hum. On peut créer:
- des tableaux indexés
- des tableaux associatifs
# tableau indexé:
declare -a ma_table
# tableau associatif
declare -A ma_table
- On peut créer des tableaux indexés directement:
$ ma_table=(plop pwet)
#ou en initialisant la première cellule du tableau
$ ma_table[0]=plop
- On peut afficher toutes les valeurs de notre tableau de deux façons:
$ echo ${ma_table[@]}
# Ou
$ echo ${ma_table[*]}
Les deux variantes sont équivalentes sauf si on les utilise entre quotes et double quotes:
$ ma_table=(pwet plop)
$ for i in "${ma_table[@]}"; do echo $i; done
$ for i in "${ma_table[*]}"; do echo $i; done
Quelle est la différence entre les deux résultats?
- Pour afficher tous les indexs d'un tableau indexé ou associatif:
#!/bin/bash
# tableau indexé
ma_table=(foo bar baz)
for i in "${!my_array[@]}"
do
echo $i
done
#!/bin/bash
# tableau associatif
declare -A ma_table
ma_table=([foo]=bar [baz]=foobar)
for k in "${!ma_table[@]}"
do
echo $k
done
- Pour récupérer la taille d'un tableau:
$ echo ${#ma_table[@]}
- Pour ajouter un (ou plusieurs) élément(s) à un tableau:
Si le tableau est un tableau indexé, c'est très simple:
$ ma_table=(plop pwet)
$ ma_table+=(blip)
# On peut aussi ajouter plusieurs éléments
$ ma_table+=(blop ping)
Si le tableau est un tableau associatif, il faut bien sûr préciser la clef associée:
$ declare -A ma_table
$ ma_table[pwet]="plop"
$ ma_table+=([blip]=blop [ping]=pong)
- Pour supprimer un élément d'un tableau:
Dans le cas d'un tableau indexé:
$ ma_table=(plop pwet blip)
# on efface le second élément
$ unset ma_table[1]
$ echo ${ma_table[@]}
$ echo ${!ma_table[@]}
Tiens, que remarquez-vous concernant nos indexs?
Dans le cas d'un tableau associatif, on supprime la clef:
$ declare -A ma_table
$ ma_table+=([pwet]=plop [blip]=blop [ping]=pong)
$ unset ma_table[blip]
$ echo ${ma_table[@]}
$ echo ${!ma_table[@]}
Enfin, pour supprimer tout le contenu d'un tableau:
$ unset ma_table
$ echo ${!ma_table[@]}
Et on a fait le tour des tableaux
Netfilter/iptables
Présentation
Définitions
Netfilter est un /framework/ implémentant un pare-feu au sein du kernel Linux, et ce depuis sa version 2.4.

/source: Wikipedia, Jan Engelhardt/
iptables est un utilitaire permettant de manipuler les différentes tables gérées par netfilter de façon plus "humaine" (ça se discute :p)
Netfilter fonctionne sur les couches 2,3,4 du modèle OSI, et les couches 1,2 et 3 du modèle TCP/IP. Il utilise les modules kernel suivants:
| ip_tables | tables IPv4 |
|---|---|
| ip6_tables | tables IPv6 |
| arp_tables | tables ARP |
| ebtables | filtrage (firewall) |
On peut manipuler ces tables avec les utilitaires /iptable/, /ip6tables/, /arptables/, et /ebtables/.
Chaines
- PREROUTING: réception de paquet
- INPUT: rendu sur place
- FORWARD: transmission (à une autre interface réseau par exemple)
- OUTPUT: généré localement avant d'être envoyé
- POSTROUTING: envoi du paquet
la table Filter
La table filter est la table qu'on affichera par défaut sur un:
$ sudo iptables -L
# en la spécifiant
$ sudo iptables -t filter -L -n --line-numbers
C'est aussi sur cette table qu'on agira par défaut - après tout, c'est cette table qui va nous servir à configurer nos règles de /firewalling/.
Par exemple, si on veut agir sur la table NAT, il faudra le préciser dans notre ligne de commande:
# du NAT, the easy way, entre eth0 et eth1
# il faudra aussi configurer la machine en mode routeur dans sysctl.conf :)
$ sudo iptables -t nat -A POSTROUTING -i eth0 -o eth1 -j MASQUERADE
Utilisation
Stratégies de filtrage
Par défaut, iptables utilise la stratégie ACCEPT sur ses chaînes. Cela signifie que tout ce qui n'est pas explicitement interdit (DROP) sera accepté. Typiquement, sur un environnement de production, on ne voudra autoriser en entrée que le trafic sur les ports dont nous avons besoin, par exemple les ports 80, 443 sur un serveur Web, et bien sûr le port qu'on utilise pour SSH (par défaut le 22), car on ne voudrait pas s'enfermer dehors.
On va donc vouloir faire l'inverse: ce qui n'est pas explicitement autorisé est implicitement interdit, en changeant la stratégie de filtrage d'iptables/netfilter.
Dans notre cas, si nous voulons autoriser uniquement le trafic sur les ports 22, 80 et 443, et configurer iptables pour DROP tout le reste, on utiliserait les règles suivantes:
# on autorise le trafic sur ces ports sur notre interface eth0
# (ou ens18, ou enps2f0, eno1, etc suivant le matériel utilisé)
$ sudo iptables -A INPUT -p tcp -i eth0 --dport 22 -j ACCEPT
$ sudo iptables -A INPUT -p tcp -i eth0 --dport 80 -j ACCEPT
$ sudo iptables -A INPUT -p tcp -i eth0 --dport 443 -j ACCEPT
# on passe en policy DROP par défaut
$ sudo iptables -P INPUT DROP
Maintenant, essayez de ping votre VM de test, que se passe-t-il?
Ajout de règles
On peut donc aussi vouloir, dans cette configuration, autoriser les requêtes ICMP (ping), juste pour vérifier que la machine est UP. Pour cela, on va devoir ajouter une règle autorisant l'ICMP. Pour ajouter une règle iptables à une chaîne d'une table (en l'occurence la table filter, ici), on a vu le commutateur -A, /append/, qui ajoute la règle à la suite des autres règles déjà définies. La règle est ici très simple:
$ sudo iptables -A INPUT -p icmp -j ACCEPT
On peut aussi vouloir insérer une règle avant les autres, ou à une position précise, avec l'option -I (/Insert/), de la forme:
# avant tout le monde
$ sudo iptables -I INPUT -p icmp -j ACCEPT
# en position 3
$ sudo iptables -I INPUT 3 -p icmp -j ACCEPT
Bon, maintenant, on peut bien ping notre machine. Peut-elle ping, au hasard, google.fr? 8.8.8.8? Donc, la résolution DNS est pétée
Pourquoi? parcequ'on accepte uniquement les paquets en entrée /à destination/ des ports 22, 80, 443. Mais on accepte pas les paquets réponse DNS en provenance du port 53 de notre résolveur DNS. DNS utilise UDP, donc, on veut autoriser les paquets UDP dont le port source d'émission (i.e. le port du serveur DNS) est le port 53:
$ sudo iptables -A INPUT -p udp --sport 53 -j ACCEPT
Et on devrait pouvoir DDoS tranquillement google.fr en le "pingant" (enfin, DDoS... lol)
Par contre, le petit souci de la policy DROP, c'est que même les communications sur l'interface /loopback/ sont interdites
On va donc les autoriser, ça peut être pratique...
$ sudo iptables -A INPUT -i lo -j ACCEPT
Tiens, essayons d'installer un paquet sur notre distrib', au hasard, curl. Que se passe-t-il?
En fait, tout comme pour la résolution DNS tout à l'heure, on autorise pas le trafic de retour en provenance des ports 80 et 443. Comment va-t-on les autoriser?
Maintenant, imaginons un scénario de type DDoS. Le sous-réseau public d'un de nos concurrents (au hasard, un concurrent du pays imaginaire d'Al-Quolanudar (pour les fans de /What we do in the Shadows/. Sacré Nandor.) dont le sous réseau est 109.110.12.0/17. Afin de sauvegarder notre business, on va tout simplement l'envoyer paître:
$ sudo iptables -A INPUT -s 109.110.12.0/17 -j DROP
Et voilà, problème résolu.
Suppression de règles
Comment supprime-t-on une règle iptables?
Deux solutions:
$ sudo iptables -L -n --line-numbers
# on repère le numéro de la règle à supprimer, par ex 12 ici:
$ sudo iptables -D INPUT 12
Ou sinon, il faut taper exactement la règle en remplaçant -A ou -I par -D:
$ sudo iptables -D INPUT -s 109.110.12.0/17 -j DROP
On peut aussi flusher le filtre et toutes les tables:
ATTENTION si vous êtes en policy DROP, ça va aussi supprimer les règles qui autorisaient SSH, web, etc. Et donc, vous allez vous enfermer dehors!
# on évite de s'enfermer dehors!
$ sudo iptables -P INPUT ACCEPT
# on flushe!
$ sudo iptables -F
# on delete toutes les chaînes utilisateur
$ sudo iptables -X
conntrack
Conntrack, /Connection Tracking/ permet au kernel de conserver la trace de toutes les sessions et connexions (logiques) et donc de tous les paquets qui composent une connexion. NAT utilise conntrack, et iptables peut utiliser ces informations pour agir comme un pare-feu persistant.
Conntrack stocke donc l'état des connexions et permet à iptables d'agir en fonction de ces états, qui sont:
- NEW: le paquet essaie de créer une nouvelle connexion
- ESTABLISHED: le paquet appartient à une connexion déjà existante
- RELATED: c'est le paquet initial d'une nouvelle connexion qui était "attendue" (par exemple une réponse ou un début de download, etc). à utiliser avec prudence car elle peut permettre l'ouverture non-désirée de ports par un potentiel attaquant.
- INVALID: paquet considéré comme invalide, comme l'indique son nom
- UNTRACKED: c'est un état spécial qu'on va utiliser pour, justement, contourner conntrack.
Pour ne pas risquer de couper une connexion déjà existante sur un flush/reload/modification de nos règles iptables, on va se servir de l'état ESTABLISHED de la connexion dans conntrack et créer une règle iptables correspondantes:
$ sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
On peut aussi s'en servir de protection syn-flood:
$ sudo iptables -A INPUT -p tcp ! --syn -m conntrack --ctstate NEW -j DROP
Bonus
Il peut être intéressant d'ajouter des commentaires à nos règles iptables, afin de les reconnaître facilement lorsqu'on les liste. Si je reprends la règle de ban d'un subnet de tout à l'heure:
$ sudo iptables -A INPUT -s 109.110.12.0/17 -j DROP -m comment --comment "/* Al-Quolanudar ban */"
On peut aussi mettre en place du /rate-limiting/ par IP (ici, 5 paquets par seconde - potentiellement violent hein):
$ sudo iptables -A INPUT -i eth0 -p tcp --syn -m limit --limit 5/s -j ACCEPT
# en UDP - pas de SYN en UDP (fire and forget)
$ sudo iptables -A INPUT -i eth0 -p udp -m limit --limit 5/s -j ACCEPT
On peut bien sûr faire du NAT un peu plus propre que mon exemple précédent, par exemple avec un VPN L3 dont le subnet serait 10.10.10.0/24:
$ sudo iptables -A FORWARD -i eth0 -o tun0 -m conntrack -ctstate ESTABLISHED -j ACCEPT
$ sudo iptables -A FORWARD -s 10.10.10.0/24 -o eth0 -j ACCEPT
$ sudo iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -o eth0 -j MASQUERADE
Bien évidemment, on peut aussi, assez souvent, vouloir bannir les sous-réseaux d'un AS complet. Pour cela il faut obtenir la liste de ses sous-réseaux depuis une DB whois reverse (typiquement radb.net):
$ whois -h whois.radb.net -- '-i origin AS24940' | grep route |\
egrep -v "(route6|descr|mnt)" | awk '{ print $2 }' > hetzner_subnets
# hetzner est un hébergeur allemand comme OVH/Scaleway/DigitalOcean/Vultr/etc
$ for i in $(cat hetzner_subnets); do sudo iptables -I INPUT -s $i -j DROP; done
Sauvegarder et restaurer des règles iptables
On a plusieurs moyens, par exemple le paquet /iptables-persistent/.
Une autre solution est le couple /iptables-save/ et /iptables-restore/:
$ sudo iptables-save | sudo tee /etc/iptables.rules
$ sudo iptables-restore < /etc/iptables.rules
Et pour le rappeler au boot, on peut soit créer une unité systemd, soit les réinjecter à l'UP d'une interface réseau:
$ sudo vim /etc/network/interfaces
...
iface ens18 inet static
address 10.0.0.1/24
gateway 10.0.0.254
up iptables-restore < /etc/iptables.rules
Outils réseau
netstat et ss (erf)
Pour regarder les connexions établies, les sockets et streams UNIX en cours d'utilisation, on dispose de deux utilitaires:
- l'utilitaire historique, netstat, qu'il faut désormais installer avec le paquet /net-tools/ sur Debian. Par exemple, pour afficher la liste de toutes les connexions tcp (-t), avec leur pid et nom de processus (-p), actives ou pas (-a), en numérique (IPs, pas de résolution dns, et ports en numérique (:22 au lieu de :ssh par exemple)):
$ sudo netstat -tpan
Connexions Internet actives (serveurs et établies)
Proto Recv-Q Send-Q Adresse locale Adresse distante Etat PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 410/sshd
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1/init
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 9245/apache2
tcp 0 68 192.168.1.198:22 192.168.1.197:42660 ESTABLISHED 5029/sshd: lidstah
tcp6 0 0 :::22 :::* LISTEN 410/sshd
tcp6 0 0 :::111 :::* LISTEN 1/init
- le nouvel utilitaire /ss/, dont on se demande qu'est ce qui est passé par la tête de ses mainteneurs quand ils ont décidé de lui donner ce petit nom si sympathique. pour info, /ss/ signifie /socket statistics/.
$ sudo ss -tanp
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=410,fd=3))
LISTEN 0 128 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=314,fd=4),("systemd",pid=1,fd=78))
LISTEN 0 128 0.0.0.0:80 0.0.0.0:* users:(("apache2",pid=9247,fd=3),("apache2",pid=9246,fd=3),("apache2",pid=9245,fd=3))
ESTAB 0 68 192.168.1.198:22 192.168.1.197:42660 users:(("sshd",pid=5035,fd=3),("sshd",pid=5029,fd=3))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=410,fd=4))
LISTEN 0 128 [::]:111 [::]:* users:(("rpcbind",pid=314,fd=6),("systemd",pid=1,fd=80))
netcat
Netcat, a.k.a /nc/, est une sorte de couteau suisse. On peut faire plein de choses avec:
- Telnet du pauvre
$ nc mail.server.tld 25
- Tester, scanner des ports (le /nmap/ du pauvre, en gros)
$ nc -v -n -z -w 1 192.168.1.198 1-100
- chat "primaire"
# sur le "serveur"
$ nc -l -p 6667
# sur le client
nc ip_serveur 6667
- transfert de fichier en "one shot"
# sur le receveur:
$ nc -l -p 1234 > file
# sur l'émetteur: (-w pour timeout après 3 secondes sans rien envoyer)
$ nc -w 3 ip_receveur 1234 < file
- serveur web "one shot"
- faire de tout processus un serveur, par exemple d'un shell (dangereux mais rigolo)
# sur le serveur:
nc -l -p 1234 -e /bin/bash
# sur le client, si l'ip du serveur est 192.168.1.100
nc 192.168.1.100 1234
ls -al
...
nmap
nmap nous permet de scanner un hôte, un sous-réseau, etc, à la recherche de ports ouverts. Un peu ce qu'on vient de faire avec netcat mais en nettement plus intéressant. En général il est aussi intéressant d'activer le mode verbose³ (-vvv) pour avoir beaucoup d'informations:
$ nmap -A -T4 -vvv 192.168.1.198
Starting Nmap 7.80 ( https://nmap.org ) at 2020-09-16 18:54 CEST
NSE: Loaded 151 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
Initiating Ping Scan at 18:54
Scanning 192.168.1.198 [2 ports]
Completed Ping Scan at 18:54, 0.00s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 18:54
Completed Parallel DNS resolution of 1 host. at 18:54, 0.00s elapsed
DNS resolution of 1 IPs took 0.00s. Mode: Async [#: 2, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 18:54
Scanning 192.168.1.198 [1000 ports]
Discovered open port 111/tcp on 192.168.1.198
Discovered open port 22/tcp on 192.168.1.198
Discovered open port 80/tcp on 192.168.1.198
Completed Connect Scan at 18:54, 0.06s elapsed (1000 total ports)
Initiating Service scan at 18:54
Scanning 3 services on 192.168.1.198
Completed Service scan at 18:54, 6.01s elapsed (3 services on 1 host)
NSE: Script scanning 192.168.1.198.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.18s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.01s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
Nmap scan report for 192.168.1.198
Host is up, received syn-ack (0.0031s latency).
Scanned at 2020-09-16 18:54:05 CEST for 6s
Not shown: 997 closed ports
Reason: 997 conn-refused
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 26:b4:6a:3b:8b:57:84:80:14:e7:8b:a5:03:66:0b:23 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3W9fkhGRcAquYowXIDvYQbo9ZCdaQJfXAcGGafASLCRvCaCXM+S/iEMFTad+5w7pb/LY8UFaF85ypoPyL/+hJLSNP/pX+rUjVynDTyTduFtv7rTlnOv03uhKf09NJsQT39H6i3Mx3uw6+8stJwEeeK+POnvPdHt4Ba5jUqkpi7cLdwbGk4MmxJ23rFVTbJvaO/3UcI6vbHP6kyW3IkT17mGGp60AD7nIUIJvVExaMPhjD7ILQXXxzUdSQ4tORnlPqcP3u2QqNEFNjSr4hf/elnSoluMjfqZwH/Zhb3Xh2MhF9irjDex9Njx/ZU7YH01xASjWcyMS1Z14ECu3SbU4L
| 256 68:9a:9f:dd:1f:f0:91:aa:a4:ed:69:a0:dc:ea:36:23 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIhbFwqgkc8FfAxwG+iG/l41MeHqAHroIJeauX7EqVZbk3li5VzI3QJTvJyYecma4C2SGgZtUpTwSGFz1iRLATg=
| 256 18:bc:ba:66:6a:a2:85:9e:34:9e:84:ea:85:ff:de:a7 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ5CKS7vdszGdPrHf/Qchf1WatFGGLC3pjr+4nAvzKWx
80/tcp open http syn-ack Apache httpd 2.4.38 ((Debian))
| http-methods:
|_ Supported Methods: OPTIONS HEAD GET POST
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Apache2 Debian Default Page: It works
111/tcp open rpcbind syn-ack 2-4 (RPC #100000)
| rpcinfo:
| program version port/proto service
| 100000 2,3,4 111/tcp rpcbind
| 100000 2,3,4 111/udp rpcbind
| 100000 3,4 111/tcp6 rpcbind
|_ 100000 3,4 111/udp6 rpcbind
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:54
Completed NSE at 18:54, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 6.73 seconds
Bien sûr, je peux aussi nmap tout un subnet:
$ nmap -A -T4 -vvv 192.168.1.0/24
# je vous passe le résultat :D
Nmap est un logiciel extrêmement complet - et complexe - je vous recommande donc chaudement la lecture de la /manpage/ ainsi que la doc' sur le site officiel https://nmap.org
tcpdump, tshark, WireShark
Ceux qui étaient en cours réseau l'an dernier doivent se rappeler de Wireshark. tcpdump est un peu son ancêtre, et tshark son implémentation en mode texte. Ces logiciels permettent d'écouter le trafic réseau d'une interface et de le /dumper/ soit dans la sortie standard (stdout), soit dans un fichier, soit dans un clicodrome, etc.
Ils peuvent être utiles pour détecter différents types de problèmes - Cependant évitez de dumper tout le trafic réseau d'une interface réseau WAN d'une grosse plateforme Web sans filtrer un minimum (src, port, etc) car ça risque vite d'être illisible
TP: mise en oeuvre sur Debian
Pour ce TP, votre groupe va avoir besoin:
- D'une VM "Web" (apache2, nginx, etc + PHP) et d'un applicatif (Wordpress ou autre, facultatif mais c'est mieux)
- D'une VM "BDD" (MariaDB, MySQL, PostgreSQL, etc)
- Les deux VMs doivent avoir deux interfaces réseau: une sera considérée comme l'interface réseau connectée à Internet, la seconde comme l'interface réseau d'un réseau privé entre les machines, et donc on ne filtrera rien sur ces interfaces réseau "LAN".
Step 1: Secure that stuff! (6 points)
Préparez un script qui prendra en compte en arguments les ports autorisés sur l'interface primaire (eth0, ens18, etc) de vos deux VMs et provisionnera les règles iptables correspondantes. Répondez aux questions suivantes:
- Quels ports allons-nous autoriser sur l'interface réseau WAN de chaque VM et pourquoi?
- Quelle(s) règle(s) iptables allons nous provisionner sur l'interface LAN de chaque VM? Pourquoi?
- On va vouloir interdire l'accès à notre plateforme aux machines de l'hébergeur Digital Ocean (et donc à tous leurs blocs), comment faire?
- Quelle solution allons-nous utiliser pour rendre ces règles persistantes (i.e. résistantes au reboot
)
Step 2: "Parsage" des logs et exports BDD (4 points)
Préparez un, ou plusieurs scripts, qui vont effectuer les tâches suivantes en parsant les logs Apache et en effectuant des requêtes MySQL:
On va vouloir parser les logs Apache2 de façon à extraire:
- Les différentes adresses IP visitant notre site, ainsi que la fréquence de ces visites (i.e. le nombre de requêtes effectuées par ces IPs)
- Les URLs visitées le plus souvent
- On va vouloir que nos scripts écrivent leurs résultats dans différents fichiers dans notre répertoire utilisateur (ACLs), au format CSV
Côté base de données:
- On va vouloir extraire, au format CSV, de notre base de données: le nombre d'articles et le nombre d'utilisateurs, en plaçant ce résultat dans un fichier csv stocké sur notre serveur Apache (hint: scp) et dans notre répertoire utilisateur
Step 3: Ban all the lamers! 10 points
Sur notre serveur Web, que nous allons considérer comme notre frontal, on va vouloir bloquer automatiquement les utilisateurs "malveillants", c'est à dire:
- Les utilisateurs effectuant un certain nombre de requêtes dans un intervalle de temps donné ("DoS")
- Les utilisateurs visitant un peu trop souvent une URL précise (au hasard, page de login - "Brute Force")
- Les petits rigolos essayant de se connecter en SSH sur notre serveur Web (idem)
On va vouloir les bannir via iptables, pendant un intervalle de temps donné (on prendra 1h dans notre cas).