Utilisateurs, groupes et sudo#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns
import pandas as pd

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

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

Nom de connexion (login)

mot_de_passe

x si le hash est dans /etc/shadow (standard moderne)

UID

Identifiant numérique de l’utilisateur

GID

Identifiant numérique du groupe principal

GECOS

Informations libres (nom complet, téléphone…)

répertoire_personnel

Home directory

shell

Shell par défaut (/bin/bash, /usr/sbin/nologin, /bin/false)

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()
_images/516889913f4a5bd8dfc8f6b0dae0ce60093662ddff57117eba33d62aea38601e.png

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

login

Nom de connexion

hash

Hash du mot de passe (format $id$sel$hash)

dernier_changement

Jours depuis epoch Unix du dernier changement

min

Nombre minimum de jours avant changement autorisé

max

Nombre maximum de jours avant expiration obligatoire

warn

Jours d’avertissement avant expiration

inactif

Jours d’inactivité avant désactivation du compte

expiration

Date d’expiration du compte (jours depuis epoch)

Format du hash#

Le champ hash suit le format modular crypt :

$id$sel$hash

$id$

Algorithme

Rounds par défaut

Recommandation

$1$

MD5

Obsolète

$5$

SHA-256

5000

Acceptable

$6$

SHA-512

5000

Standard courant

$y$

yescrypt

variable

Recommandé (Debian 11+)

$2y$

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()
_images/c26df9e4146c2983d8e32afb835184c12df16cf46c3b11cf92c1281304ca6d66.png

/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_groupe

Nom du groupe

mot_de_passe

x si le hash est dans /etc/gshadow (rarement utilisé)

GID

Identifiant numérique du groupe

membres

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()
_images/69f5e0f6d3d23c50046279b4fee6fd6a49962f1063331427aef7e2c80945fb5a.png

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 passe

  • session — 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’échec

  • sufficient — 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

pam_unix.so

Authentification Unix classique (passwd/shadow)

pam_faillock.so

Verrouillage après N échecs successifs

pam_limits.so

Application des limites de /etc/security/limits.conf

pam_env.so

Chargement des variables d’environnement

pam_systemd.so

Intégration avec systemd (cgroup, journal)

pam_ldap.so

Authentification LDAP

pam_google_authenticator.so

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é

/etc/passwd, /etc/shadow

Définition des comptes et authentification

Groupes

/etc/group, /etc/gshadow

Organisation en équipes, contrôle d’accès partagé

Authentification

PAM

Architecture modulaire, politique de mots de passe

Délégation

sudo + /etc/sudoers

É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/passwd sur un système correctement configuré.

  • PAM : couche d’abstraction indispensable. pam_limits.so applique les limites de ressources ; pam_faillock.so protège contre le brute-force.

  • sudo : toujours auditer /etc/sudoers et /etc/sudoers.d/. Éviter NOPASSWD: ALL. Utiliser sudo -l pour 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