Chapitre 2

Niveau 1 — IAM & Auth | Niveau 2 — Gmail

Audit des identités, Super Admins, 2FA, emails de récupération, comptes inactifs. Audit Gmail : transferts automatiques, délégations, filtres de redirection et App Passwords.

Niveaux 1 & 2 — Les plus critiques
IAM/Auth et Gmail constituent 70% des vecteurs d'attaque réels
N1 — Identités & Auth N2 — Gmail & Messagerie
CRITIQUE — À VÉRIFIER EN PREMIER

Priorités absolues de ce chapitre

  • 2FA enforced sur tous les comptes sans exception
  • Emails de récupération Super Admins = internes au domaine
  • Aucun transfert automatique Gmail actif
  • Aucun App Password (Less Secure App) actif

4. Niveau 1 — Identités, Accès et Authentification

4.1 Inventaire des utilisateurs

La première étape est d'obtenir la liste exhaustive des utilisateurs et leurs attributs de sécurité.

def get_all_users(service):
    """Retourne tous les utilisateurs du domaine avec leurs attributs de sécurité."""
    users = []
    page_token = None
    while True:
        result = service.users().list(
            domain=DOMAIN,
            maxResults=500,
            pageToken=page_token,
            projection="full",
            orderBy="email",
        ).execute()
        users.extend(result.get("users", []))
        page_token = result.get("nextPageToken")
        if not page_token:
            break
    return users
Attributs à extraire et analyser :
Attribut APISignificationAlerte si
isAdminSuper Admin> 4 comptes
isDelegatedAdminAdmin déléguéLister et justifier
suspendedCompte suspenduPrésent sans offboarding
isEnrolledIn2Sv2FA enrolléeFalse = CRITIQUE
isEnforcedIn2Sv2FA obligatoire enforcedFalse = non conforme CIS
lastLoginTimeDernière connexion> 90 jours = inactif
recoveryEmailEmail de récupérationExterne = risque bypass 2FA
recoveryPhoneTéléphone de récupérationExterne = risque SIM swap

4.2 Analyse des comptes Super Admin

Le CIS Benchmark recommande un maximum de 4 Super Admins (idéalement 2-3). Chaque compte Super Admin compromis donne un accès total au tenant.

Points de contrôle :
def audit_super_admins(users):
    super_admins = [u for u in users if u.get("isAdmin")]
    risks = []
    for sa in super_admins:
        recovery = sa.get("recoveryEmail", "")
        if recovery and not recovery.endswith(f"@{DOMAIN}"):
            risks.append({
                "email": sa["primaryEmail"],
                "risk": "Email de récupération externe",
                "recovery": recovery,
                "severity": "CRITIQUE",
            })
    return super_admins, risks

4.3 Analyse de la 2FA

Distinction cruciale : Il existe une différence entre 2FA enrollée (l'utilisateur a configuré un second facteur) et 2FA enforced (la politique admin oblige la 2FA — un utilisateur ne peut pas la désactiver).
def audit_2fa(users):
    findings = {
        "no_2fa": [],           # Aucune 2FA configurée
        "enrolled_not_enforced": [],  # 2FA configurée mais pas obligatoire
        "fully_enforced": [],   # 2FA enforced
    }
    for user in users:
        enrolled = user.get("isEnrolledIn2Sv", False)
        enforced = user.get("isEnforcedIn2Sv", False)
        if not enrolled:
            findings["no_2fa"].append(user["primaryEmail"])
        elif enrolled and not enforced:
            findings["enrolled_not_enforced"].append(user["primaryEmail"])
        else:
            findings["fully_enforced"].append(user["primaryEmail"])
    return findings
Types de 2FA — Résistance au phishing :
Type 2FARésistance phishingRecommandation
FIDO2 / Passkey / YubiKey✅ TotaleObligatoire pour admins
TOTP (Google Authenticator)⚠️ PartielleVulnérable AITM
SMS OTP❌ FaibleVulnérable SIM swap
Backup codes❌ Très faibleÀ usage unique uniquement

4.4 Emails de récupération — Vecteur d'account takeover

L'email de récupération est le talon d'Achille de la 2FA. Si l'email de récupération est compromis, un attaquant peut réinitialiser le mot de passe sans avoir besoin de la 2FA. C'est le vecteur d'attaque le plus sous-estimé en environnement Workspace.

def audit_recovery_info(service, user_emails):
    findings = {
        "external_recovery_emails": [],
        "external_recovery_phones": [],
        "no_recovery": [],
    }
    for email in user_emails:
        user = service.users().get(userKey=email, projection="full").execute()
        recovery_email = user.get("recoveryEmail", "")
        recovery_phone = user.get("recoveryPhone", "")

        if recovery_email and not recovery_email.endswith(f"@{DOMAIN}"):
            findings["external_recovery_emails"].append({
                "user": email,
                "recovery_email": recovery_email,
                "provider": recovery_email.split("@")[-1],
            })
        if not recovery_email and not recovery_phone:
            findings["no_recovery"].append(email)
    return findings
Critères de risque pour les emails de récupération :
TypeNiveauRaison
@gmail.com, @outlook.com🔴 ÉLEVÉGrand public, souvent peu sécurisé
@yahoo.fr, @hotmail.fr🔴 ÉLEVÉBreaches historiques massives (Yahoo 2016)
Domaine d'une autre entreprise🟠 MOYENEmployé précédent ou freelance
@domaine.com interne✅ OKContrôlé par l'organisation

4.5 Comptes inactifs et suspendus

Un compte inactif non supprimé représente un vecteur d'attaque persistant. Les comptes suspendus non traités signalent l'absence d'une procédure d'offboarding.

from datetime import datetime, timezone, timedelta

def audit_inactive_accounts(users, inactive_days=90):
    threshold = datetime.now(timezone.utc) - timedelta(days=inactive_days)
    inactive = []
    for user in users:
        last_login = user.get("lastLoginTime")
        if last_login:
            last_login_dt = datetime.fromisoformat(
                last_login.replace("Z", "+00:00")
            )
            if last_login_dt < threshold and not user.get("suspended"):
                inactive.append({
                    "email": user["primaryEmail"],
                    "last_login": last_login,
                    "days_inactive": (datetime.now(timezone.utc) - last_login_dt).days,
                })
    suspended = [u for u in users if u.get("suspended")]
    return inactive, suspended

4.6 Rôles administratifs

Google Workspace permet de déléguer des rôles granulaires (admin messagerie, admin helpdesk, etc.). Un audit doit lister tous les rôles administratifs et vérifier le respect du principe du moindre privilège.

def audit_admin_roles(service):
    """Liste tous les rôles et leurs assignations."""
    roles = service.roles().list(customer="my_customer").execute()
    role_assignments = service.roleAssignments().list(
        customer="my_customer"
    ).execute()

    findings = {
        "custom_roles": [r for r in roles.get("items", []) if not r.get("isSystemRole")],
        "assignments": role_assignments.get("items", []),
        "super_admin_assignments": [
            a for a in role_assignments.get("items", [])
            if a.get("roleId") == "SUPER_ADMIN_ROLE_ID"
        ],
    }
    return findings

---

5. Niveau 2 — Messagerie Gmail

5.1 Transferts automatiques

Le transfert automatique de messagerie est l'un des risques les plus graves : il crée une exfiltration de données en temps réel, silencieuse et persistante. À auditer sur tous les comptes.

def audit_gmail_forwarding(gmail_service, user_email):
    """Vérifie si un utilisateur a configuré un transfert automatique."""
    try:
        settings = gmail_service.users().settings().getAutoForwarding(
            userId=user_email
        ).execute()
        if settings.get("enabled"):
            return {
                "email": user_email,
                "forwarding_to": settings.get("emailAddress"),
                "disposition": settings.get("disposition"),
                "severity": "CRITIQUE",
            }
    except Exception as e:
        return {"email": user_email, "error": str(e)}
    return None
Contre-mesures :

5.2 Délégation de boîte mail

La délégation permet à un utilisateur de gérer la boîte d'un autre. C'est une fonctionnalité légitime (assistante/dirigeant) mais qui crée une surface d'attaque si mal configurée.

def audit_gmail_delegation(gmail_service, user_email):
    """Liste les délégations actives sur un compte."""
    try:
        delegates = gmail_service.users().settings().delegates().list(
            userId=user_email
        ).execute()
        items = delegates.get("delegates", [])
        if items:
            external = [
                d for d in items
                if not d.get("delegateEmail", "").endswith(f"@{DOMAIN}")
            ]
            return {
                "email": user_email,
                "total_delegates": len(items),
                "external_delegates": external,
                "severity": "CRITIQUE" if external else "INFO",
            }
    except Exception:
        pass
    return None

5.3 Filtres Gmail avec actions de redirection

Les filtres Gmail peuvent rediriger silencieusement certains emails vers des adresses externes. Moins visibles que le transfert automatique, ils représentent un risque d'exfiltration ciblée.

def audit_gmail_filters(gmail_service, user_email):
    """Détecte les filtres avec redirection vers l'extérieur."""
    risky_filters = []
    try:
        filters = gmail_service.users().settings().filters().list(
            userId=user_email
        ).execute()
        for f in filters.get("filter", []):
            action = f.get("action", {})
            # Vérifier si le filtre redirige vers une adresse externe
            if "addLabelIds" in action or "forward" in str(action).lower():
                risky_filters.append({
                    "filter_id": f.get("id"),
                    "criteria": f.get("criteria", {}),
                    "action": action,
                })
    except Exception:
        pass
    return risky_filters

5.4 Tokens de session actifs (App Passwords)

Les App Passwords (anciennement "Less Secure Apps") permettent un accès IMAP/POP3 sans 2FA. Leur présence signale qu'un client mail ancien (Outlook, Thunderbird en mode IMAP basique) accède au compte en contournant la 2FA.

def audit_app_passwords(security_service, user_email):
    """Détecte les App Passwords (Less Secure Apps)."""
    try:
        asps = security_service.asps().list(userKey=user_email).execute()
        items = asps.get("items", [])
        if items:
            return {
                "email": user_email,
                "count": len(items),
                "apps": [a.get("name", "") for a in items],
                "oldest": min(a.get("creationTime", "") for a in items),
            }
    except Exception:
        pass
    return None

---