Utilisateurs, groupes et sudo#
Modèle de sécurité Unix#
Identifiants utilisateur et groupe#
Le modèle de sécurité Unix repose sur trois concepts fondamentaux : les UIDs (User IDs), les GIDs (Group IDs), et les permissions sur les fichiers. Toute ressource du système — fichier, processus, socket — appartient à un utilisateur et à un groupe. Les droits d’accès sont définis séparément pour le propriétaire, le groupe propriétaire, et les autres.
Chaque processus possède plusieurs identifiants :
UID réel (real UID) : l’utilisateur qui a lancé le processus.
UID effectif (effective UID, EUID) : l’identité avec laquelle les vérifications d’accès sont réalisées. Peut différer du UID réel grâce au bit setuid.
UID sauvegardé (saved UID) : permet à un processus de baisser temporairement ses privilèges et de les récupérer.
UID effectif et setuid
Le bit setuid sur un exécutable (chmod u+s) fait que le processus s’exécute avec l’EUID du propriétaire du fichier plutôt que celui de l’utilisateur qui le lance. Exemple : /usr/bin/passwd appartient à root et a le bit setuid — c’est ce qui permet à un utilisateur normal de changer son propre mot de passe (le processus passwd a l’EUID 0 le temps d’écrire dans /etc/shadow).
Catégories d’utilisateurs#
Les UIDs sont conventionnellement répartis en trois catégories :
Plage d’UIDs |
Catégorie |
Exemples |
|---|---|---|
0 |
Superutilisateur |
root |
1 – 999 |
Utilisateurs système |
daemon, www-data, postgres, sshd |
1000 – 65534 |
Utilisateurs humains |
alice, bob, lôc |
65534 |
Nobody |
nfsnobody (utilisateur sans privilèges) |
Root — UID 0, pas le nom
Ce qui donne les privilèges de superutilisateur, c’est l”UID 0, pas le nom « root ». Un compte nommé « toor » avec UID 0 aurait exactement les mêmes privilèges. Le noyau ne compare que les valeurs numériques des UIDs — il ignore complètement les noms.
Les utilisateurs système sont créés lors de l’installation d’un démon pour que celui-ci s’exécute sans les privilèges de root, tout en ayant accès à ses propres fichiers. Par exemple, www-data (Apache/Nginx) possède /var/www/ mais ne peut pas écrire dans /etc/.
Principe du moindre privilège#
Un principe fondamental de la sécurité Unix : chaque processus ne doit avoir que les privilèges strictement nécessaires à sa tâche. En pratique, cela se traduit par :
Les daemons tournent sous leur propre utilisateur système (pas root).
Les administrateurs travaillent avec un compte personnel et n’utilisent root que ponctuellement via
sudo.Les services avec accès réseau tournent dans des environnements confinés (chroot, namespaces, cgroups).
/etc/passwd#
Structure du fichier#
/etc/passwd est un fichier texte lisible par tous qui contient les informations de base sur chaque compte. Chaque ligne représente un compte et comprend sept champs séparés par des deux-points :
nom:mot_de_passe:UID:GID:GECOS:répertoire_personnel:shell
Champ |
Description |
|---|---|
|
Nom de connexion (login) |
|
|
|
Identifiant numérique de l’utilisateur |
|
Identifiant numérique du groupe principal |
|
Informations libres (nom complet, téléphone…) |
|
Home directory |
|
Shell par défaut ( |
Exemple :
root:x:0:0:root:/root:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
alice:x:1001:1001:Alice Dupont,,,:/home/alice:/bin/bash
/usr/sbin/nologin et /bin/false
Un shell /usr/sbin/nologin ou /bin/false empêche toute connexion interactive. C’est la pratique standard pour les comptes de service : l’utilisateur www-data peut posséder des fichiers et lancer des processus, mais personne ne peut se connecter en tant que www-data avec un shell interactif. La commande nologin affiche un message d’avertissement avant de refuser la connexion.
Parse réel de /etc/passwd#
# Parse réel de /etc/passwd avec pandas
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
def parse_passwd(path="/etc/passwd"):
"""Parse /etc/passwd et retourne un DataFrame pandas."""
colonnes = ["login", "password", "uid", "gid", "gecos", "home", "shell"]
lignes = []
with open(path, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(":")
if len(parts) == 7:
lignes.append({
"login": parts[0],
"password": parts[1],
"uid": int(parts[2]),
"gid": int(parts[3]),
"gecos": parts[4],
"home": parts[5],
"shell": parts[6],
})
return pd.DataFrame(lignes, columns=colonnes)
df_passwd = parse_passwd()
print(f"Nombre total de comptes : {len(df_passwd)}")
print(f"\n=== Catégories d'UIDs ===")
print(f" root (UID 0) : {len(df_passwd[df_passwd.uid == 0])}")
print(f" Système (1–999) : {len(df_passwd[(df_passwd.uid >= 1) & (df_passwd.uid <= 999)])}")
print(f" Utilisateurs (≥1000) : {len(df_passwd[df_passwd.uid >= 1000])}")
print(f"\n=== Shells utilisés ===")
print(df_passwd.groupby("shell")["login"].count().sort_values(ascending=False).to_string())
print(f"\n=== Comptes avec shell interactif ===")
shells_interactifs = ["/bin/bash", "/bin/sh", "/bin/zsh", "/bin/fish", "/usr/bin/bash",
"/usr/bin/zsh", "/usr/bin/fish"]
interactifs = df_passwd[df_passwd.shell.isin(shells_interactifs)]
print(interactifs[["login", "uid", "gid", "home", "shell"]].to_string(index=False))
Nombre total de comptes : 45
=== Catégories d'UIDs ===
root (UID 0) : 1
Système (1–999) : 42
Utilisateurs (≥1000) : 2
=== Shells utilisés ===
shell
/usr/sbin/nologin 35
/bin/false 6
/bin/bash 3
/bin/sync 1
=== Comptes avec shell interactif ===
login uid gid home shell
root 0 0 /root /bin/bash
loc 1000 1000 /home/loc /bin/bash
postgres 111 115 /var/lib/postgresql /bin/bash
# Distribution des UIDs — histogramme
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# Histogramme complet
palette = sns.color_palette("muted", 3)
def uid_category(uid):
if uid == 0:
return "root (0)"
elif uid < 1000:
return "Système (1–999)"
else:
return "Utilisateur (≥1000)"
df_passwd["categorie"] = df_passwd["uid"].apply(uid_category)
counts = df_passwd["categorie"].value_counts()
cat_order = ["root (0)", "Système (1–999)", "Utilisateur (≥1000)"]
counts = counts.reindex([c for c in cat_order if c in counts.index])
axes[0].bar(counts.index, counts.values, color=palette[:len(counts)], edgecolor="none")
axes[0].set_title("Répartition des comptes par catégorie d'UID")
axes[0].set_ylabel("Nombre de comptes")
axes[0].spines[["top", "right"]].set_visible(False)
for i, (label, val) in enumerate(counts.items()):
axes[0].text(i, val + 0.1, str(val), ha="center", va="bottom", fontsize=10)
# Distribution des UIDs système (hors root)
systeme = df_passwd[(df_passwd.uid >= 1) & (df_passwd.uid <= 999)]
if not systeme.empty:
axes[1].hist(systeme["uid"], bins=20, color=palette[1], edgecolor="white", linewidth=0.5)
axes[1].set_title("Distribution des UIDs système (1–999)")
axes[1].set_xlabel("UID")
axes[1].set_ylabel("Nombre de comptes")
axes[1].spines[["top", "right"]].set_visible(False)
else:
axes[1].text(0.5, 0.5, "Aucun compte système", ha="center", va="center",
transform=axes[1].transAxes)
axes[1].axis("off")
plt.suptitle("/etc/passwd — analyse des comptes système", fontsize=12, fontweight="bold")
plt.show()
NSS — Name Service Switch
Les fonctions de résolution de noms (getpwuid(), getgrnam(), etc.) ne lisent pas directement /etc/passwd. Elles passent par le NSS (Name Service Switch), configuré dans /etc/nsswitch.conf. Cela permet de résoudre les identités depuis des sources multiples : fichiers locaux, LDAP, NIS, base de données. La commande getent passwd alice interroge NSS et fonctionne quelle que soit la source configurée.
/etc/shadow#
Pourquoi shadow ?#
Historiquement, les hashs des mots de passe étaient stockés directement dans le second champ de /etc/passwd. Comme ce fichier doit être lisible par tous (pour mapper les UIDs aux noms), les hashs étaient accessibles à tout le monde — ce qui permettait des attaques par dictionnaire hors-ligne.
La solution est le shadow password : les hashs sont déplacés dans /etc/shadow, accessible uniquement par root (-rw-r----- root shadow).
Structure de /etc/shadow#
Chaque ligne contient neuf champs séparés par des deux-points :
login:hash:dernier_changement:min:max:warn:inactif:expiration:réservé
Champ |
Description |
|---|---|
|
Nom de connexion |
|
Hash du mot de passe (format |
|
Jours depuis epoch Unix du dernier changement |
|
Nombre minimum de jours avant changement autorisé |
|
Nombre maximum de jours avant expiration obligatoire |
|
Jours d’avertissement avant expiration |
|
Jours d’inactivité avant désactivation du compte |
|
Date d’expiration du compte (jours depuis epoch) |
Format du hash#
Le champ hash suit le format modular crypt :
$id$sel$hash
|
Algorithme |
Rounds par défaut |
Recommandation |
|---|---|---|---|
|
MD5 |
— |
Obsolète |
|
SHA-256 |
5000 |
Acceptable |
|
SHA-512 |
5000 |
Standard courant |
|
yescrypt |
variable |
Recommandé (Debian 11+) |
|
bcrypt |
variable |
Recommandé |
Exemple :
alice:$6$rounds=65536$sElMfGQreq$KhNiXR7oJiRB...:19800:0:90:14:::
Choisir un algorithme de hash fort
SHA-512 avec un grand nombre de rounds reste acceptable. Pour les nouvelles installations, préférez yescrypt (Debian 11+) ou bcrypt : ces algorithmes sont résistants aux accélérateurs matériels (GPU, ASIC) car ils sont conçus pour être intensifs en mémoire. Configurez l’algorithme dans /etc/pam.d/common-password (Debian) ou /etc/security/pwquality.conf (RHEL).
Politique de mots de passe avec chage#
chage (change age) gère les politiques d’expiration des mots de passe :
# Voir la politique d'un utilisateur
chage -l alice
# Last password change : Jan 15, 2025
# Password expires : Apr 15, 2025
# Account expires : never
# Maximum number of days between password change : 90
# Forcer le changement de mot de passe à la prochaine connexion
chage -d 0 alice
# Définir une expiration dans 90 jours
chage -M 90 alice
# Définir une période d'avertissement de 14 jours
chage -W 14 alice
# Définir la date d'expiration du compte
chage -E 2025-12-31 alice
# Simulation d'une politique de rotation de mots de passe
# Visualisation du cycle de vie d'un mot de passe
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import numpy as np
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, ax = plt.subplots(figsize=(12, 3.5))
palette = sns.color_palette("muted", 5)
# Paramètres (jours)
min_days = 7 # délai minimum avant changement
max_days = 90 # durée de validité
warn_days = 14 # période d'avertissement
inactive = 30 # compte désactivé après N jours sans connexion post-expiration
phases = [
(0, min_days, palette[2], f"Délai min\n({min_days}j)"),
(min_days, max_days - warn_days, palette[0], f"Valide\n({max_days - warn_days - min_days}j)"),
(max_days - warn_days, max_days, palette[4], f"Avertissement\n({warn_days}j)"),
(max_days, max_days + inactive, palette[3], f"Expiré / Inactif\n({inactive}j)"),
]
for start, end, color, label in phases:
ax.barh(0, end - start, left=start, height=0.4,
color=color, edgecolor="white", linewidth=2)
ax.text((start + end) / 2, 0, label,
ha="center", va="center", fontsize=8.5, color="white", fontweight="bold")
# Annotations
ax.axvline(max_days, color="red", linestyle="--", linewidth=1.5, alpha=0.7)
ax.text(max_days + 1, 0.28, "Expiration", color="red", fontsize=8.5, va="center")
ax.set_xlim(0, max_days + inactive + 10)
ax.set_yticks([])
ax.set_xlabel("Jours depuis le dernier changement de mot de passe")
ax.set_title(f"Cycle de vie d'un mot de passe — politique chage (max={max_days}j, warn={warn_days}j, inactif={inactive}j)")
ax.spines[["top", "left", "right"]].set_visible(False)
legend_patches = [
mpatches.Patch(color=palette[2], label=f"Délai minimum ({min_days}j)"),
mpatches.Patch(color=palette[0], label="Période valide"),
mpatches.Patch(color=palette[4], label=f"Avertissement ({warn_days}j)"),
mpatches.Patch(color=palette[3], label=f"Expiré/inactif ({inactive}j)"),
]
ax.legend(handles=legend_patches, loc="upper right", fontsize=8.5)
plt.show()
/etc/group et /etc/gshadow#
Structure de /etc/group#
Chaque ligne de /etc/group décrit un groupe avec quatre champs :
nom_groupe:mot_de_passe:GID:membres
Champ |
Description |
|---|---|
|
Nom du groupe |
|
|
|
Identifiant numérique du groupe |
|
Liste des membres supplémentaires (séparés par des virgules) |
Groupe primaire vs groupes supplémentaires
Chaque utilisateur a un groupe primaire défini dans /etc/passwd (champ GID). Ce groupe est propriétaire des nouveaux fichiers créés par l’utilisateur. Les groupes supplémentaires listés dans /etc/group donnent des droits additionnels sans changer la propriété des fichiers. Par exemple, appartenir au groupe sudo ou wheel permet d’utiliser sudo.
Parse réel de /etc/group#
# Parse réel de /etc/group
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
def parse_group(path="/etc/group"):
"""Parse /etc/group et retourne un DataFrame pandas."""
lignes = []
with open(path, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(":")
if len(parts) == 4:
membres = [m for m in parts[3].split(",") if m]
lignes.append({
"groupe": parts[0],
"gid": int(parts[2]),
"membres": membres,
"nb_membres": len(membres),
})
return pd.DataFrame(lignes)
df_group = parse_group()
print(f"Nombre total de groupes : {len(df_group)}")
print(f"\n=== Catégories de GIDs ===")
print(f" root (GID 0) : {len(df_group[df_group.gid == 0])}")
print(f" Système (1–999) : {len(df_group[(df_group.gid >= 1) & (df_group.gid <= 999)])}")
print(f" Utilisateurs (≥1000) : {len(df_group[df_group.gid >= 1000])}")
print(f"\n=== Groupes avec des membres explicites ===")
avec_membres = df_group[df_group.nb_membres > 0].sort_values("nb_membres", ascending=False)
if not avec_membres.empty:
for _, row in avec_membres.iterrows():
print(f" {row['groupe']:20s} (GID {row['gid']:5d}) : {', '.join(row['membres'])}")
else:
print(" (aucun groupe avec membres supplémentaires)")
Nombre total de groupes : 75
=== Catégories de GIDs ===
root (GID 0) : 1
Système (1–999) : 72
Utilisateurs (≥1000) : 2
=== Groupes avec des membres explicites ===
video (GID 44) : loc, ollama
scanner (GID 102) : saned, loc
floppy (GID 25) : loc
sudo (GID 27) : loc
audio (GID 29) : loc
dip (GID 30) : loc
cdrom (GID 24) : loc
plugdev (GID 46) : loc
users (GID 100) : loc
render (GID 992) : ollama
netdev (GID 101) : loc
ssl-cert (GID 105) : postgres
bluetooth (GID 106) : loc
lpadmin (GID 108) : loc
ollama (GID 986) : loc
# Visualisation : distribution des GIDs et groupes avec membres
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
palette = sns.color_palette("muted", 3)
# Distribution par catégorie
def gid_category(gid):
if gid == 0:
return "root (0)"
elif gid < 1000:
return "Système (1–999)"
else:
return "Utilisateur (≥1000)"
df_group["categorie"] = df_group["gid"].apply(gid_category)
counts = df_group["categorie"].value_counts()
cat_order = ["root (0)", "Système (1–999)", "Utilisateur (≥1000)"]
counts = counts.reindex([c for c in cat_order if c in counts.index])
axes[0].bar(counts.index, counts.values, color=palette[:len(counts)], edgecolor="none")
axes[0].set_title("Répartition des groupes par catégorie de GID")
axes[0].set_ylabel("Nombre de groupes")
axes[0].spines[["top", "right"]].set_visible(False)
for i, (_, val) in enumerate(counts.items()):
axes[0].text(i, val + 0.1, str(val), ha="center", va="bottom", fontsize=10)
# Groupes avec le plus de membres
top_membres = df_group[df_group.nb_membres > 0].sort_values("nb_membres", ascending=True).tail(10)
if not top_membres.empty:
axes[1].barh(top_membres["groupe"], top_membres["nb_membres"],
color=palette[1], edgecolor="none")
axes[1].set_title("Groupes avec le plus de membres")
axes[1].set_xlabel("Nombre de membres")
axes[1].spines[["top", "right"]].set_visible(False)
else:
axes[1].text(0.5, 0.5, "Aucun groupe avec\nmembres supplémentaires",
ha="center", va="center", transform=axes[1].transAxes, fontsize=11)
axes[1].axis("off")
plt.suptitle("/etc/group — analyse des groupes système", fontsize=12, fontweight="bold")
plt.show()
Supprimer un groupe utilisé comme groupe primaire
groupdel échoue si le groupe est le groupe primaire d’un utilisateur existant. Il faut d’abord changer le groupe primaire de l’utilisateur (usermod -g autregroupe user) ou supprimer l’utilisateur. De même, supprimer un groupe supplémentaire ne met pas automatiquement à jour les fichiers appartenant à ce groupe — ils conservent l’ancien GID numérique.
Commandes de gestion des utilisateurs et groupes#
Créer et modifier des utilisateurs#
# Créer un utilisateur avec home directory et shell Bash
useradd -m -s /bin/bash -c "Alice Dupont" alice
# Créer avec un groupe principal et des groupes supplémentaires
useradd -m -s /bin/bash -g users -G sudo,docker alice
# Créer un compte système (pas de home, shell nologin)
useradd -r -s /usr/sbin/nologin -d /var/lib/myapp myapp
# Définir/changer le mot de passe
passwd alice
# Modifier un compte existant
usermod -s /bin/zsh alice # changer le shell
usermod -aG docker alice # ajouter au groupe docker
usermod -L alice # verrouiller le compte
usermod -U alice # déverrouiller le compte
usermod -e 2025-12-31 alice # définir une date d'expiration
usermod -d /home/newalice -m alice # déplacer le home directory
# Supprimer un utilisateur
userdel alice # garde le home directory
userdel -r alice # supprime aussi le home et la boîte mail
useradd vs adduser
Sur les systèmes Debian/Ubuntu, adduser est un script Perl de haut niveau qui pose des questions interactives et crée le home directory automatiquement. useradd est l’outil POSIX bas niveau disponible sur toutes les distributions. En scripts, utilisez toujours useradd avec des options explicites pour garantir la portabilité.
Gestion des groupes#
# Créer un groupe
groupadd devteam
groupadd -g 2000 devteam # avec un GID spécifique
# Modifier un groupe
groupmod -n developers devteam # renommer
groupmod -g 2001 developers # changer le GID
# Supprimer un groupe
groupdel developers
# Gérer les membres d'un groupe
gpasswd -a alice developers # ajouter
gpasswd -d alice developers # retirer
gpasswd -M alice,bob developers # définir la liste complète
# Voir les groupes d'un utilisateur
id alice
# uid=1001(alice) gid=1001(alice) groups=1001(alice),27(sudo),998(docker)
groups alice
# alice : alice sudo docker
Commandes d’information#
# Informations sur l'utilisateur courant
id
whoami
# Historique des connexions
last
lastb # tentatives de connexion échouées
# Utilisateurs actuellement connectés
who
w
PAM — Pluggable Authentication Modules#
Architecture PAM#
PAM (Pluggable Authentication Modules) est une couche d’abstraction qui découple les applications de la mécanique d’authentification. Sans PAM, chaque application (login, sshd, sudo, su) devrait implémenter elle-même la vérification des mots de passe, la gestion des sessions, etc. Avec PAM, les applications délèguent ces tâches à des modules configurables.
La configuration PAM est dans /etc/pam.d/. Chaque service a son fichier :
/etc/pam.d/
├── common-auth → inclus par la plupart des services (Debian)
├── common-session → configuration de session commune
├── common-password → politique de mot de passe commune
├── sshd → spécifique à OpenSSH
├── sudo → spécifique à sudo
├── login → console locale
└── su → changement d'utilisateur
Structure d’une règle PAM#
Chaque ligne d’un fichier PAM suit le format :
type contrôle module [arguments]
Types de modules :
auth— authentification (vérifier l’identité)account— contrôle d’accès (compte expiré ? heure autorisée ?)password— gestion des mots de passesession— actions en début/fin de session (monter home, limites…)
Valeurs de contrôle :
required— doit réussir, mais continue l’évaluation (résultat différé)requisite— doit réussir, stoppe immédiatement en cas d’échecsufficient— si réussit, stoppe l’évaluation de ce type (pas d’échec précédent)optional— résultat ignoré sauf si c’est le seul module de ce type
Modules PAM courants#
# /etc/pam.d/common-auth (Debian/Ubuntu typique)
auth required pam_env.so
auth required pam_faillock.so preauth
auth sufficient pam_unix.so nullok
auth required pam_faillock.so authfail
auth required pam_deny.so
# /etc/pam.d/common-session
session required pam_unix.so
session optional pam_systemd.so
session required pam_limits.so
session optional pam_umask.so
Module |
Rôle |
|---|---|
|
Authentification Unix classique (passwd/shadow) |
|
Verrouillage après N échecs successifs |
|
Application des limites de |
|
Chargement des variables d’environnement |
|
Intégration avec systemd (cgroup, journal) |
|
Authentification LDAP |
|
Authentification à deux facteurs TOTP |
/etc/security/limits.conf#
Ce fichier définit les limites de ressources des processus, appliquées par pam_limits.so à la connexion :
# /etc/security/limits.conf
# Format : domaine type item valeur
#
# Limites pour tous les utilisateurs
* soft nofile 1024
* hard nofile 65536
* soft nproc 1024
* hard nproc 4096
# Limites pour un groupe
@developers soft nproc 4096
@developers hard nproc 8192
# Limites pour un utilisateur
postgres soft nofile 65536
postgres hard nofile 65536
postgres - memlock unlimited
limits.conf et les services systemd
Les limites de limits.conf s’appliquent aux sessions PAM — c’est-à-dire aux connexions interactives et aux processus lancés depuis un shell. Les services gérés par systemd ignorent limits.conf ; leurs limites se configurent via les directives LimitNOFILE=, LimitNPROC= etc. dans la section [Service] du fichier .service.
Valeurs soft vs hard
Chaque ressource a deux niveaux de limite : soft (limite active, que le processus peut augmenter jusqu’à la limite hard) et hard (plafond absolu, seul root peut l’augmenter). Un utilisateur peut consulter ses limites avec ulimit -a et les modifier à la hausse jusqu’à la limite hard avec ulimit -n 65536.
sudo et sudoers#
Pourquoi sudo#
sudo (substitute user do) permet à un utilisateur ordinaire d’exécuter des commandes avec les privilèges d’un autre utilisateur (typiquement root), sous contrôle et avec journalisation. C’est le mécanisme standard pour administrer un système sans se connecter directement en root.
Avantages de sudo sur su - :
Granularité : on peut autoriser uniquement certaines commandes.
Traçabilité : chaque commande sudo est journalisée (utilisateur, commande, résultat).
Pas de partage du mot de passe root : chaque administrateur utilise son propre mot de passe.
Timeout configurable : le mot de passe n’est pas redemandé pendant N minutes.
/etc/sudoers et visudo#
Toujours utiliser visudo
Ne jamais éditer /etc/sudoers directement avec un éditeur texte. visudo valide la syntaxe avant de sauvegarder ; un fichier sudoers corrompu peut bloquer complètement l’accès root. En cas de doute : sudo visudo -c pour vérifier la syntaxe sans modification.
# Format d'une règle sudoers
# utilisateur hôte=(exécuter_en_tant_que) commandes
# L'utilisateur alice peut tout faire en root sur tous les hôtes
alice ALL=(ALL:ALL) ALL
# Les membres du groupe sudo peuvent tout faire
%sudo ALL=(ALL:ALL) ALL
# Alice peut redémarrer nginx sans mot de passe
alice ALL=(root) NOPASSWD: /bin/systemctl restart nginx
# Bob peut uniquement consulter les logs
bob ALL=(root) /bin/journalctl, /bin/cat /var/log/*
# Inclure un fichier de règles supplémentaires
@includedir /etc/sudoers.d
Bonnes pratiques sudoers#
# Créer un fichier dans /etc/sudoers.d/ pour chaque rôle
# /etc/sudoers.d/webadmin
%webadmin ALL=(root) NOPASSWD: /bin/systemctl start nginx
%webadmin ALL=(root) NOPASSWD: /bin/systemctl stop nginx
%webadmin ALL=(root) NOPASSWD: /bin/systemctl reload nginx
%webadmin ALL=(root) NOPASSWD: /bin/systemctl status nginx
# Vérifier les droits sudo d'un utilisateur
sudo -l -U alice
# User alice may run the following commands on hostname:
# (root) NOPASSWD: /bin/systemctl restart nginx
NOPASSWD — utiliser avec précaution
NOPASSWD est pratique pour les scripts d’automatisation, mais dangereux si la commande autorisée peut être détournée pour obtenir un shell root (par exemple vim, less, find -exec, etc.). Préférez toujours spécifier le chemin absolu complet et les arguments exacts de la commande autorisée.
sudo -l et audit#
# Lister les commandes autorisées pour l'utilisateur courant
sudo -l
# Les logs sudo sont dans syslog/journal
journalctl | grep sudo
# ou sous /var/log/auth.log (Debian/Ubuntu)
grep sudo /var/log/auth.log | tail -20
# Exemple de log sudo :
# Mar 15 14:32:01 serveur sudo: alice : TTY=pts/0 ; PWD=/home/alice ;
# USER=root ; COMMAND=/bin/systemctl restart nginx
Polkit#
Rôle de Polkit#
Polkit (PolicyKit) est un framework d’autorisation qui gère l’élévation de privilèges pour les applications graphiques et les services D-Bus. Là où sudo est centré sur la ligne de commande, Polkit est conçu pour répondre à des requêtes venant de services système comme udisks2 (montage de disques), NetworkManager (configuration réseau), ou PackageKit (installation de paquets).
Le modèle est le suivant : une application ordinaire demande à un service système (via D-Bus) d’effectuer une action privilégiée. Le service consulte Polkit, qui vérifie si l’utilisateur est autorisé. Si nécessaire, Polkit demande une authentification à l’utilisateur (via une fenêtre graphique dans un environnement desktop).
Règles Polkit#
Les politiques sont définies dans des fichiers .pkla (ancien format) ou .rules (JavaScript, format moderne) :
# /etc/polkit-1/rules.d/49-allow-disk-mount.rules
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.udisks2.filesystem-mount" &&
subject.isInGroup("plugdev")) {
return polkit.Result.YES;
}
});
Polkit et la sécurité
Polkit a été le sujet de vulnérabilités importantes (CVE-2021-4034 « PwnKit »). Maintenez votre système à jour et vérifiez régulièrement avec pkaction --verbose les actions polkit disponibles. Sur les serveurs sans interface graphique, il est possible de désactiver polkit s’il n’est pas nécessaire.
Résumé#
La gestion des utilisateurs et des privilèges sous Linux s’articule autour de quelques concepts stables hérités d’Unix, complétés par des mécanismes modernes :
Couche |
Outil |
Rôle |
|---|---|---|
Identité |
|
Définition des comptes et authentification |
Groupes |
|
Organisation en équipes, contrôle d’accès partagé |
Authentification |
PAM |
Architecture modulaire, politique de mots de passe |
Délégation |
sudo + |
Élévation de privilèges contrôlée et journalisée |
Services |
Polkit |
Autorisation fine pour les applications et services |
Points clés à retenir
Tout repose sur les UIDs/GIDs : les noms sont des alias humains. Le noyau ne connaît que les nombres.
/etc/shadow : les hashs de mots de passe ne sont jamais dans
/etc/passwdsur un système correctement configuré.PAM : couche d’abstraction indispensable.
pam_limits.soapplique les limites de ressources ;pam_faillock.soprotège contre le brute-force.sudo : toujours auditer
/etc/sudoerset/etc/sudoers.d/. ÉviterNOPASSWD: ALL. Utilisersudo -lpour vérifier les droits.chage: outil essentiel pour imposer une politique de rotation des mots de passe.
Commandes de référence
id <user> # UIDs, GIDs d'un utilisateur
groups <user> # groupes d'appartenance
useradd -m -s /bin/bash <u> # créer un utilisateur
usermod -aG <groupe> <user> # ajouter à un groupe
chage -l <user> # politique de mot de passe
sudo -l # droits sudo de l'utilisateur courant
visudo # éditer sudoers en sécurité
getent passwd <user> # interroger NSS (LDAP inclus)
passwd -l <user> # verrouiller un compte