---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Utilisateurs, groupes et sudo

```{code-cell} python
:tags: [hide-input]

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.

```{admonition} UID effectif et setuid
:class: note
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) |

```{admonition} Root — UID 0, pas le nom
:class: important
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
```

```{admonition} /usr/sbin/nologin et /bin/false
:class: note
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`

```{code-cell} python
# 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))
```

```{code-cell} python
:tags: [hide-input]

# 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()
```

```{admonition} NSS — Name Service Switch
:class: note
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:::
```

```{admonition} Choisir un algorithme de hash fort
:class: important
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 :

```bash
# 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
```

```{code-cell} python
:tags: [hide-input]

# 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_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) |

```{admonition} Groupe primaire vs groupes supplémentaires
:class: note
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`

```{code-cell} python
# 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)")
```

```{code-cell} python
:tags: [hide-input]

# 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()
```

```{admonition} Supprimer un groupe utilisé comme groupe primaire
:class: warning
`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

```bash
# 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
```

```{admonition} useradd vs adduser
:class: tip
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

```bash
# 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

```bash
# 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

```bash
# /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 :

```bash
# /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
```

```{admonition} limits.conf et les services systemd
:class: warning
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`.
```

```{admonition} Valeurs soft vs hard
:class: note
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`

```{admonition} Toujours utiliser visudo
:class: warning
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.
```

```bash
# 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

```bash
# 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
```

```{admonition} NOPASSWD — utiliser avec précaution
:class: warning
`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

```bash
# 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) :

```bash
# /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;
    }
});
```

```{admonition} Polkit et la sécurité
:class: note
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 |

```{admonition} Points clés à retenir
:class: important
- **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.
```

```{admonition} Commandes de référence
:class: tip
```bash
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
```

```
