Il y a des éléments dans Active Directory qu’on oublie totalement parce qu’ils n’apparaissent jamais dans les opérations courantes. Le compte krbtgt en fait partie. Invisible dans le quotidien, il est pourtant au cœur de toute l’authentification Kerberos de votre domaine — et sa compromission est l’un des scénarios les plus catastrophiques qu’un attaquant puisse déclencher.

Dans cet article, on va explorer ce qu’est vraiment ce compte, pourquoi il faut changer son mot de passe régulièrement, et comment automatiser cette opération proprement avec Ansible.


Kerberos 101 : ce que fait le KDC

Avant de parler du krbtgt, petit rappel sur le fonctionnement de Kerberos dans un domaine Active Directory.

Quand un utilisateur se connecte à une ressource du domaine (un serveur de fichiers, une appli web, un service quelconque), il ne transmet jamais son mot de passe directement. À la place, le processus Kerberos se déroule en trois temps :

  1. AS-REQ / AS-REP : le client contacte le KDC (Key Distribution Center, c’est-à-dire le contrôleur de domaine) pour demander un TGT (Ticket-Granting Ticket). Si les credentials sont valides, le KDC retourne le TGT, chiffré et signé.

  2. TGS-REQ / TGS-REP : avec son TGT en poche, le client demande au KDC un ticket de service (ST) pour la ressource cible.

  3. AP-REQ : le client présente son ticket de service directement à la ressource, qui peut le valider sans repasser par le KDC.

Ce mécanisme est élégant : la ressource n’a pas à contacter le DC à chaque connexion, et le mot de passe de l’utilisateur ne circule jamais en clair sur le réseau.


Le compte krbtgt : la clef de voûte

Le compte krbtgt est un compte de service spécial, désactivé, créé automatiquement lors de la promotion d’un contrôleur de domaine. Son rôle est unique et fondamental : son mot de passe est utilisé par le KDC pour chiffrer et signer tous les TGT émis dans le domaine.

Autrement dit, chaque ticket Kerberos de votre domaine porte l’empreinte de la clé dérivée du mot de passe krbtgt. Quand un contrôleur de domaine reçoit un TGT à valider, il ne consulte pas une base de données — il vérifie simplement que le ticket a bien été signé avec sa clé krbtgt.

┌─────────────────────────────────────────────────────────┐
│                   Contrôleur de domaine                  │
│                                                         │
│  [ Compte krbtgt ]                                      │
│  Hash du mot de passe ──► Clé de chiffrement des TGT   │
│                                                         │
│  Tout TGT valide = signé avec cette clé                │
└─────────────────────────────────────────────────────────┘

Ce compte ne se connecte jamais, ne s’authentifie jamais, n’envoie aucun trafic réseau. Il existe uniquement comme matériel cryptographique.


L’attaque Golden Ticket : quand krbtgt est compromis

Si un attaquant parvient à extraire le hash NTLM du mot de passe krbtgt, il peut forger de toutes pièces des TGT valides — sans passer par le KDC, pour n’importe quel utilisateur, avec n’importe quels groupes, pour n’importe quelle durée de validité.

C’est ce qu’on appelle le Golden Ticket.

# Exemple avec Mimikatz (à des fins pédagogiques)
# Extraction du hash krbtgt sur un DC compromis
lsadump::lsa /patch

# Forge d'un Golden Ticket valable 10 ans pour "Administrateur"
kerberos::golden /user:Administrateur /domain:mondomaine.local 
  /sid:S-1-5-21-... /krbtgt:<HASH_NTLM> /endin:87600

Avec ce ticket :

  • L’attaquant est indiscernable d’un administrateur légitime aux yeux de tous les serveurs membres du domaine
  • Le ticket reste valide même si le vrai compte administrateur est désactivé ou renommé
  • Le ticket reste valide tant que le mot de passe krbtgt n’a pas été changé
  • L’attaque persiste même après réinstallation de la machine compromise (tant que le domaine tourne)

C’est l’une des rares attaques dans Active Directory qui donne une persistance absolue. D’où l’importance cruciale de changer régulièrement le mot de passe de ce compte.

Note importante : Microsoft maintient en mémoire les deux derniers mots de passe du compte krbtgt (l’actuel et le précédent). Les tickets signés avec l’un ou l’autre restent valides. C’est pourquoi il faut changer le mot de passe deux fois pour invalider complètement les anciens tickets.


Pourquoi ne le fait-on pas plus souvent ?

La réponse honnête : par peur. Et cette peur est en partie justifiée.

Changer le mot de passe krbtgt déclenche une réplication vers tous les contrôleurs de domaine du domaine. Pendant le laps de temps où certains DCs ont le nouveau mot de passe et d’autres pas encore, des authentifications peuvent échouer.

Les cas problématiques classiques :

  • Environnements multi-sites avec des liaisons WAN lentes
  • DCs hors ligne ou en maintenance au moment du changement
  • Tickets Kerberos en cours d’utilisation avec une longue durée de validité

En pratique, dans un environnement bien tenu (DCs synchronisés, latence de réplication raisonnable), le changement se passe sans incident visible. Microsoft recommande d’attendre au moins 10 heures entre les deux rotations — c’est le temps maximal de validité d’un ticket Kerberos par défaut (10h de UserLogonLifetime).


Fréquence recommandée

Il n’y a pas de consensus absolu, mais les recommandations convergeables sont les suivantes :

Contexte Fréquence suggérée
Environnement standard Tous les 180 jours
Post-incident (compromission suspectée) Immédiatement, deux fois à 10h d’intervalle
Réduction d’un ancien employé avec accès DC Immédiatement
Après une migration de DC Recommandé
Après une restauration de DC depuis backup Impératif

Microsoft a d’ailleurs publié un script PowerShell officiel (New-KrbtgtKeys.ps1) qui est devenu la référence pour cette opération. On va s’en servir comme base côté Windows, et l’orchestrer via Ansible.


Architecture de la solution Ansible

L’idée est simple :

Nœud Ansible (Linux)
        │
        │ WinRM (HTTPS)
        ▼
Contrôleur de domaine principal (PDC Emulator)
        │
        │ Réplication AD
        ▼
Autres contrôleurs de domaine

Le playbook va :

  1. Cibler le PDC Emulator (le bon endroit pour les changements de mots de passe AD)
  2. Télécharger ou déposer le script officiel Microsoft
  3. Effectuer une première rotation
  4. Attendre 10 heures (ou un délai configurable)
  5. Effectuer une seconde rotation
  6. Vérifier la réplication sur tous les DCs

Prérequis

Côté contrôleur Ansible :

# Module Windows requis
pip install pywinrm
ansible-galaxy collection install ansible.windows

Côté Windows Server (les DCs) :

# Activation de WinRM (si pas déjà fait via GPO)
Enable-PSRemoting -Force
winrm set winrm/config/service/auth '@{Basic="true"}'
winrm set winrm/config/service '@{AllowUnencrypted="false"}'

Le playbook Ansible

Structure des fichiers

krbtgt-rotation/
├── inventory/
│   └── production.yml
├── roles/
│   └── krbtgt_rotation/
│       ├── tasks/
│       │   └── main.yml
│       ├── files/
│       │   └── New-KrbtgtKeys.ps1
│       └── defaults/
│           └── main.yml
└── site.yml

Inventory

# inventory/production.yml
all:
  children:
    domain_controllers:
      hosts:
        dc01.mondomaine.local:
          ansible_host: 192.168.1.10
        dc02.mondomaine.local:
          ansible_host: 192.168.1.11
      vars:
        ansible_user: "MONDOMAINE\\ansible_svc"
        ansible_password: "{{ vault_ansible_password }}"
        ansible_connection: winrm
        ansible_winrm_transport: kerberos
        ansible_winrm_server_cert_validation: validate
        ansible_port: 5986

Bonne pratique : utiliser ansible-vault pour chiffrer les credentials. Ne jamais mettre de mot de passe en clair dans un inventory.

# Chiffrement du vault
ansible-vault create group_vars/all/vault.yml
# Contenu :
# vault_ansible_password: "VotreMotDePasseServiceAnsible"

Defaults du rôle

# roles/krbtgt_rotation/defaults/main.yml

# Délai entre les deux rotations (en secondes)
# 36000 = 10 heures (recommandation Microsoft)
# Pour les tests, vous pouvez descendre à 60 secondes
krbtgt_rotation_delay: 36000

# Mode : 'simulate' (dry-run) ou 'reset' (rotation réelle)
krbtgt_operation_mode: "simulate"

# Chemin temporaire sur le DC pour le script
krbtgt_script_dest: "C:\\Windows\\Temp\\New-KrbtgtKeys.ps1"

# Domaine cible (sera détecté automatiquement si vide)
krbtgt_domain: ""

Tasks principale

# roles/krbtgt_rotation/tasks/main.yml
---

# ─── Pré-vérifications ───────────────────────────────────────────────────────

- name: Vérifier que le module ActiveDirectory est disponible
  ansible.windows.win_powershell:
    script: |
      if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
          throw "Module ActiveDirectory non disponible. Installez RSAT."
      }
      Write-Output "OK"      
  register: ad_module_check
  changed_when: false

- name: Identifier le PDC Emulator du domaine
  ansible.windows.win_powershell:
    script: |
      $pdc = (Get-ADDomain).PDCEmulator
      Write-Output $pdc      
  register: pdc_emulator
  run_once: true
  changed_when: false

- name: Afficher le PDC Emulator identifié
  ansible.builtin.debug:
    msg: "PDC Emulator : {{ pdc_emulator.output[0] }}"
  run_once: true

# ─── Déploiement du script ───────────────────────────────────────────────────

- name: Copier le script New-KrbtgtKeys.ps1 sur le DC
  ansible.windows.win_copy:
    src: New-KrbtgtKeys.ps1
    dest: "{{ krbtgt_script_dest }}"
  when: inventory_hostname == pdc_emulator.output[0].split('.')[0]

# ─── Vérification de l'état actuel ──────────────────────────────────────────

- name: Vérifier la date du dernier changement de mot de passe krbtgt
  ansible.windows.win_powershell:
    script: |
      $krbtgt = Get-ADUser krbtgt -Properties PasswordLastSet, msDS-KeyVersionNumber
      $result = @{
          PasswordLastSet   = $krbtgt.PasswordLastSet.ToString("yyyy-MM-dd HH:mm:ss")
          KeyVersionNumber  = $krbtgt.'msDS-KeyVersionNumber'
          DaysSinceChange   = ((Get-Date) - $krbtgt.PasswordLastSet).Days
      }
      Write-Output ($result | ConvertTo-Json)      
  register: krbtgt_current_state
  run_once: true
  changed_when: false
  delegate_to: "{{ pdc_emulator.output[0] }}"

- name: Afficher l'état actuel du compte krbtgt
  ansible.builtin.debug:
    msg: "{{ krbtgt_current_state.output[0] | from_json }}"
  run_once: true

# ─── Première rotation ───────────────────────────────────────────────────────

- name: "Première rotation krbtgt (mode: {{ krbtgt_operation_mode }})"
  ansible.windows.win_powershell:
    script: |
      $params = @{
          OperationMode = "{{ krbtgt_operation_mode }}"
          DomainFQDN    = (Get-ADDomain).DNSRoot
      }
      & "{{ krbtgt_script_dest }}" @params      
  register: rotation_first
  run_once: true
  delegate_to: "{{ pdc_emulator.output[0] }}"
  when: krbtgt_operation_mode == "reset"

- name: Log de la première rotation
  ansible.builtin.debug:
    msg: "{{ rotation_first.output }}"
  run_once: true
  when: krbtgt_operation_mode == "reset"

# ─── Vérification de la réplication avant d'attendre ────────────────────────

- name: Forcer la réplication AD vers tous les DCs
  ansible.windows.win_powershell:
    script: |
      repadmin /syncall /AdeP
      Start-Sleep -Seconds 30
      repadmin /showrepl      
  register: repl_check_1
  run_once: true
  delegate_to: "{{ pdc_emulator.output[0] }}"
  when: krbtgt_operation_mode == "reset"

- name: Vérifier que la réplication ne présente pas d'erreurs
  ansible.windows.win_powershell:
    script: |
      $errors = repadmin /showrepl 2>&1 | Select-String "fail|error" -CaseSensitive:$false
      if ($errors) {
          Write-Warning "Erreurs de réplication détectées :"
          $errors | ForEach-Object { Write-Warning $_.Line }
          # On log mais on ne bloque pas - l'admin doit investiguer
      } else {
          Write-Output "Réplication OK"
      }      
  register: repl_errors
  run_once: true
  delegate_to: "{{ pdc_emulator.output[0] }}"
  changed_when: false

# ─── Attente inter-rotation ──────────────────────────────────────────────────

- name: "Attente de {{ krbtgt_rotation_delay }} secondes entre les deux rotations"
  ansible.builtin.pause:
    seconds: "{{ krbtgt_rotation_delay }}"
  run_once: true
  when: krbtgt_operation_mode == "reset"

# ─── Seconde rotation ────────────────────────────────────────────────────────

- name: "Seconde rotation krbtgt (invalidation définitive des anciens tickets)"
  ansible.windows.win_powershell:
    script: |
      $params = @{
          OperationMode = "{{ krbtgt_operation_mode }}"
          DomainFQDN    = (Get-ADDomain).DNSRoot
      }
      & "{{ krbtgt_script_dest }}" @params      
  register: rotation_second
  run_once: true
  delegate_to: "{{ pdc_emulator.output[0] }}"
  when: krbtgt_operation_mode == "reset"

- name: Log de la seconde rotation
  ansible.builtin.debug:
    msg: "{{ rotation_second.output }}"
  run_once: true
  when: krbtgt_operation_mode == "reset"

# ─── Vérification post-rotation ──────────────────────────────────────────────

- name: Vérifier le nouveau numéro de version Kerberos (msDS-KeyVersionNumber)
  ansible.windows.win_powershell:
    script: |
      $krbtgt = Get-ADUser krbtgt -Properties PasswordLastSet, msDS-KeyVersionNumber
      $result = @{
          PasswordLastSet  = $krbtgt.PasswordLastSet.ToString("yyyy-MM-dd HH:mm:ss")
          KeyVersionNumber = $krbtgt.'msDS-KeyVersionNumber'
      }
      Write-Output ($result | ConvertTo-Json)      
  register: krbtgt_new_state
  run_once: true
  changed_when: false
  delegate_to: "{{ pdc_emulator.output[0] }}"

- name: Résumé post-rotation
  ansible.builtin.debug:
    msg:
      - "=== Rotation krbtgt terminée ==="
      - "Nouvel état : {{ krbtgt_new_state.output[0] | from_json }}"
      - "Pensez à documenter cette rotation dans votre CMDB."
  run_once: true

# ─── Nettoyage ───────────────────────────────────────────────────────────────

- name: Supprimer le script du DC après utilisation
  ansible.windows.win_file:
    path: "{{ krbtgt_script_dest }}"
    state: absent
  when: inventory_hostname == pdc_emulator.output[0].split('.')[0]

Playbook principal

# site.yml
---
- name: Rotation du mot de passe krbtgt Active Directory
  hosts: domain_controllers
  gather_facts: false

  vars_files:
    - group_vars/all/vault.yml

  pre_tasks:
    - name: Afficher le mode d'exécution
      ansible.builtin.debug:
        msg: >
          Mode : {{ krbtgt_operation_mode | upper }}
          {% if krbtgt_operation_mode == 'simulate' %}
          (DRY-RUN - aucune modification ne sera effectuée)
          {% else %}
          ⚠️  ROTATION RÉELLE - les mots de passe seront changés
          {% endif %}          
      run_once: true

  roles:
    - role: krbtgt_rotation

Exécution

Mode simulation (recommandé pour tester)

# Vérification sans aucune modification
ansible-playbook site.yml \
  --ask-vault-pass \
  -e "krbtgt_operation_mode=simulate"

Le script affichera l’état actuel du compte, les DCs qui seraient impactés, et simulera le processus sans rien changer.

Mode rotation réelle

# Rotation avec délai complet de 10h entre les deux passes
ansible-playbook site.yml \
  --ask-vault-pass \
  -e "krbtgt_operation_mode=reset" \
  -e "krbtgt_rotation_delay=36000"

Mode rotation accélérée (test en labo)

# Délai de 60 secondes - UNIQUEMENT en environnement de test
ansible-playbook site.yml \
  --ask-vault-pass \
  -e "krbtgt_operation_mode=reset" \
  -e "krbtgt_rotation_delay=60"

Ce qu’il faut surveiller après la rotation

Une fois la rotation effectuée, les indicateurs à vérifier :

Réplication AD

# Sur chaque DC, vérifier que le numéro de version est identique
Get-ADReplicationUpToDatenessVectorTable -Target "dc01.mondomaine.local"

# Vérification du kvno krbtgt sur tous les DCs
repadmin /showobjmeta * "CN=krbtgt,CN=Users,DC=mondomaine,DC=local" | 
  Select-String "pwdLastSet|msDS-KeyVersionNumber"

Tickets Kerberos en cours

Après la première rotation, les tickets existants restent valides (Windows conserve l’ancien mot de passe). Après la seconde rotation, tous les anciens tickets signés avec le krbtgt précédent deviennent invalides.

Les utilisateurs auront simplement à se ré-authentifier de façon transparente lors de leur prochain accès à une ressource — dans la grande majorité des cas, ils ne le verront pas.

Événements Windows à surveiller

Dans l’Observateur d’événements des DCs :

  • Event ID 4723 : tentative de changement de mot de passe
  • Event ID 4724 : réinitialisation de mot de passe par un admin
  • Event ID 4769 : demande de ticket Kerberos (pour détecter des TGS inhabituels après rotation)

Intégration dans un pipeline de sécurité

Pour aller plus loin, cette rotation peut être intégrée dans un pipeline plus large :

Planificateur (cron/AWX/Semaphore)
        │
        ├─► Rotation krbtgt (ce playbook)
        ├─► Notification Slack/Teams/Mail
        └─► Écriture dans CMDB/journal de sécurité

Exemple de notification Slack en fin de playbook :

- name: Notification de fin de rotation
  community.general.slack:
    token: "{{ vault_slack_token }}"
    channel: "#soc-alerts"
    msg: |
      ✅ *Rotation krbtgt terminée*
      Domaine : {{ ansible_domain }}
      Date : {{ ansible_date_time.iso8601 }}
      KeyVersionNumber : {{ krbtgt_new_state.output[0] | from_json | json_query('KeyVersionNumber') }}      
  run_once: true
  delegate_to: localhost

Environnements multi-domaines

Si comme moi vous avez plusieurs domaines AD (infrastructure + utilisateurs), la rotation doit être effectuée indépendamment dans chaque domaine. Le compte krbtgt est local à chaque domaine — il n’y a pas de krbtgt “global” qui couvre plusieurs domaines, même liés par une relation d’approbation.

Dans ce cas, dupliquer l’inventory :

# inventory/production.yml
all:
  children:
    domain_infra:
      hosts:
        dc-infra-01.infradomaine.local:
      vars:
        ansible_user: "INFRA\\ansible_svc"
        # ...

    domain_users:
      hosts:
        dc-users-01.userdomaine.local:
      vars:
        ansible_user: "USERS\\ansible_svc"
        # ...

Et exécuter deux fois le playbook en ciblant chaque groupe.


En résumé

Le compte krbtgt est à Active Directory ce que la clef maîtresse est à un immeuble : si elle tombe entre de mauvaises mains, tous les verrous sont inutiles. Sa rotation régulière est l’une des mesures les plus simples et les plus efficaces pour limiter la durée de vie d’une compromission.

Avec Ansible, l’opération devient reproductible, traçable et surtout non oubliable — programmée dans un cron ou une pipeline AWX, elle se fait sans intervention humaine, avec logs et notification.

Les points clés à retenir :

  • Toujours faire deux rotations espacées d’au moins 10 heures
  • Toujours cibler le PDC Emulator pour le changement
  • Vérifier la réplication AD entre les deux passes
  • En environnement multi-domaines, traiter chaque domaine séparément
  • Tester d’abord en mode simulate avant de passer en reset

Sources et références :