Chapitre 1515 minDébutant
Sécurité des scripts : stratégie d'exécution, secrets et signature
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer la stratégie d'exécution de PowerShell et ses limites ;
- signer un script et vérifier sa signature ;
- ranger un secret sans l'écrire dans le script ;
- journaliser PowerShell et protéger les scripts Linux et leurs règles
sudo.
4 exercices corrigés
Sécuriser un script, c'est empêcher qu'il devienne une porte d'entrée : qu'un attaquant le modifie pour exécuter ses propres commandes, y lise un mot de passe, ou détourne les droits avec lesquels il tourne. Ce chapitre explique la stratégie d'exécution de PowerShell (et ce qu'elle ne protège pas), la signature des scripts, le rangement des secrets, la journalisation de PowerShell, les droits des scripts Linux et l'analyse automatique du code.
La stratégie d'exécution de PowerShell
La stratégie d'exécution (execution policy) dit quels scripts PowerShell accepte de lancer sous Windows.
| Valeur | Ce qu'elle autorise | Usage |
|---|---|---|
Restricted | Aucun script, seulement les commandes tapées | Défaut de Windows PowerShell 5.1 sur les postes Windows 10 et 11 |
AllSigned | Seulement les scripts signés par un éditeur approuvé | Serveurs sensibles, environnement maîtrisé |
RemoteSigned | Les scripts locaux ; ceux venus d'Internet doivent être signés | Défaut de Windows Server, bon choix général |
Unrestricted | Tout, avec un avertissement pour les scripts venus d'Internet | Défaut, non modifiable, de PowerShell sous Linux et macOS |
Bypass | Tout, sans aucun avertissement | Outils qui lancent PowerShell en interne |
Undefined | Aucune valeur à cette portée |
PowerShell 7 installé sous Windows démarre en RemoteSigned : son fichier powershell.config.json, dans le dossier d'installation, fixe cette valeur.
Elle se règle à plusieurs portées, de la plus forte à la plus faible : MachinePolicy et UserPolicy (fixées par GPO), Process (la session en cours), CurrentUser, LocalMachine. La première portée définie l'emporte.
Get-ExecutionPolicy -List
Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process Undefined
CurrentUser RemoteSigned
LocalMachine Undefined
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned # pour ton compte, sans droits d'administrateur
En entreprise, la stratégie se fixe par GPO : Configuration ordinateur > Stratégies > Modèles d'administration > Composants Windows > Windows PowerShell > Activer l'exécution des scripts. Une valeur fixée par GPO ne peut pas être changée par Set-ExecutionPolicy.
« Venu d'Internet » se reconnaît à la marque du Web : quand un navigateur ou un client de messagerie enregistre un fichier, Windows lui ajoute un flux de données Zone.Identifier qui indique sa provenance. Après avoir lu et vérifié un script téléchargé, Unblock-File .\script.ps1 retire cette marque.
Signer un script
Une signature Authenticode garantit qu'un script vient bien de son auteur et qu'il n'a pas été modifié depuis : le moindre caractère changé invalide la signature. Avec la stratégie AllSigned, un script modifié par un attaquant ne s'exécute plus.
Il faut un certificat de signature de code, délivré par l'autorité de certification de l'entreprise (modèle de certificat « Signature de code » d'AD CS) ; le fonctionnement des certificats et de la signature numérique est expliqué dans le cours Cryptographie.
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
Set-AuthenticodeSignature -FilePath .\New-ComptesArrivees.ps1 -Certificate $cert `
-TimestampServer 'http://timestamp.digicert.com'
Get-AuthenticodeSignature -FilePath .\New-ComptesArrivees.ps1 | Select-Object Status, StatusMessage
La signature s'ajoute en bas du fichier, dans un bloc de commentaires # SIG # Begin signature block. L'horodatage prouve que le script a été signé pendant la validité du certificat : la signature reste valable après l'expiration de celui-ci. Get-AuthenticodeSignature renvoie un Status :
| Status | Sens |
|---|---|
Valid | Signature correcte, certificat approuvé |
HashMismatch | Le script a été modifié après sa signature |
NotSigned | Pas de signature |
NotTrusted ou UnknownError | Le certificat ou son autorité ne sont pas approuvés sur cette machine |
Pour que les postes et serveurs fassent confiance aux scripts signés, le certificat de l'autorité racine doit être dans le magasin des autorités racines de confiance, et celui du signataire dans les éditeurs approuvés : une GPO les déploie.
Ranger les secrets
Un mot de passe ou un jeton écrit dans un script est lu par tous ceux qui lisent le script : les collègues, la sauvegarde, le dépôt Git, l'attaquant qui a pris pied sur le serveur. Les solutions, de la meilleure à la moins bonne :
- Pas de secret du tout. Une tâche planifiée qui tourne sous un gMSA s'authentifie sur le réseau sans que personne ne connaisse de mot de passe ; un script lancé par un administrateur utilise ses propres droits (Kerberos).
- Un coffre de secrets. Le module Microsoft.PowerShell.SecretManagement donne des commandes uniques (
Get-Secret,Set-Secret) au-dessus de différents coffres : le coffre local SecretStore, ou un coffre d'entreprise (Azure Key Vault, HashiCorp Vault, KeePass, par des extensions). - Un fichier chiffré pour un seul compte.
Export-Clixmlenregistre un objetPSCredentialdont le mot de passe est chiffré par la DPAPI de Windows : seul le même compte, sur la même machine, peut le relire. - Une variable d'environnement ou un fichier protégé, lisible par le seul compte qui lance le script : la solution courante sous Linux.
# Le coffre SecretStore, une fois pour toutes (PowerShell 7.4+ inclut Install-PSResource)
Install-PSResource -Name Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore
Register-SecretVault -Name Coffre -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Set-Secret -Name ZabbixLecture -Secret (Read-Host -AsSecureString 'Jeton Zabbix')
# Dans le script
$jeton = Get-Secret -Name ZabbixLecture -AsPlainText
# Un identifiant chiffré pour le compte qui lance la tâche : à créer EN TANT QUE ce compte, sur ce serveur
Get-Credential | Export-Clixml -Path C:\Scripts\Secrets\copie-compta.xml
$cred = Import-Clixml -Path C:\Scripts\Secrets\copie-compta.xml
Le coffre SecretStore demande par défaut un mot de passe pour s'ouvrir, ce qui ne convient pas à une tâche sans surveillance ; Set-SecretStoreConfiguration permet de régler ce comportement, au prix d'une protection qui repose alors sur le compte Windows. Dans Windows PowerShell 5.1, les modules s'installent avec Install-Module au lieu d'Install-PSResource.
Sous Linux, un secret se range dans un fichier qui n'appartient qu'au compte du script, et se lit au lancement :
sudo install -o rapports -g rapports -m 0600 /dev/null /etc/sorbier/zabbix.token # fichier vide, droits 600
sudoedit /etc/sorbier/zabbix.token # y coller le jeton
ZABBIX_TOKEN=$(< /etc/sorbier/zabbix.token)
Pour un script lancé par systemd, la directive LoadCredential=zabbix-token:/etc/sorbier/zabbix.token dans la section [Service] met le secret à disposition du seul service, dans le fichier $CREDENTIALS_DIRECTORY/zabbix-token, sans qu'il soit lisible par les autres processus.
Enfin, un secret qui a été commité dans Git, même supprimé ensuite, reste dans l'historique : on le considère comme compromis et on le change. Le cours Git et GitHub explique comment l'éviter avec .gitignore et que faire si c'est arrivé.
Journaliser ce que fait PowerShell
PowerShell est un outil d'administration puissant, présent sur toutes les machines Windows : c'est aussi l'un des outils préférés des attaquants, qui l'utilisent pour agir sans déposer de programme (on parle de living off the land). La défense consiste à tout journaliser pour pouvoir détecter et enquêter :
- la journalisation des blocs de script (Script Block Logging), activée par GPO (Modèles d'administration > Composants Windows > Windows PowerShell > Activer la journalisation de blocs de scripts PowerShell), enregistre le code réellement exécuté dans l'événement 4104 du journal
Microsoft-Windows-PowerShell/Operational, même s'il avait été masqué ou encodé ; - la transcription garde un fichier texte de chaque session, dans un dossier protégé ;
- les événements sont envoyés au serveur de journaux et analysés par le SIEM, qui alerte sur les signes classiques : un
powershell.exelancé avec-EncodedCommandou-WindowStyle Hiddenpar un traitement de texte, un téléchargement suivi d'une exécution, une tâche planifiée créée hors des heures habituelles (événement 4698 du journal de sécurité).
La mise en place de ces journaux et de leur transfert est décrite dans le cours Windows Server.
Les droits des scripts Linux et sudo
Sur un serveur Linux, la sécurité d'un script tient à trois règles :
- un script lancé par root n'est modifiable que par root, de même que le dossier qui le contient :
install -o root -g root -m 0750(chapitre 13) ; - un compte de service n'a que les droits utiles. Si le compte
rapportsdoit lancer un seul script avec les droits de root, une règlesudol'autorise pour ce script et rien d'autre, écrite avecsudo visudo -f /etc/sudoers.d/rapports:
rapports ALL=(root) NOPASSWD: /usr/local/sbin/sauvegarde-config.sh
- une valeur venue de l'extérieur n'est jamais exécutée. Pas d'
evalen Bash ni d'Invoke-Expressionen PowerShell sur une donnée lue dans un fichier, une API ou un argument : un nom de fichier commex; rm -rf /deviendrait une commande. Les guillemets autour des variables,--avant les noms de fichiers et les filtres avec variables de l'annuaire jouent le même rôle que les requêtes préparées contre l'injection SQL.
Analyser le code automatiquement
Deux outils relisent tes scripts et signalent les erreurs classiques avant qu'elles ne se produisent :
Install-PSResource -Name PSScriptAnalyzer
Invoke-ScriptAnalyzer -Path C:\Scripts -Recurse # alias inutilisés, variables jamais lues, mots de passe en clair…
sudo apt install shellcheck
shellcheck /usr/local/sbin/sauvegarde-config.sh # guillemets oubliés, tests mal écrits, pièges de set -e…
PSScriptAnalyzer signale par exemple un paramètre nommé Password de type [string] (règle PSAvoidUsingPlainTextForPassword) ou un Invoke-Expression. ShellCheck signale chaque variable non protégée (SC2086), chaque cd non vérifié (SC2164). Les deux s'intègrent à l'éditeur et à un pipeline d'intégration continue, qui refuse un script défectueux avant son déploiement (cours Intégration continue).
Exercices
Exercice 1 sur 4 Lire les stratégies d'exécution Réussi À revoir
Sur un poste du domaine, Get-ExecutionPolicy -List affiche :
Scope ExecutionPolicy
----- ---------------
MachinePolicy AllSigned
UserPolicy Undefined
Process Undefined
CurrentUser Unrestricted
LocalMachine RemoteSigned
- Quelle stratégie s'applique ?
- Un utilisateur tape
Set-ExecutionPolicy -Scope CurrentUser Bypass. Qu'est-ce que ça change ? - Le script
Inventaire.ps1, écrit par un technicien et non signé, peut-il s'exécuter avec.\Inventaire.ps1? Avecpowershell.exe -ExecutionPolicy Bypass -File .\Inventaire.ps1? - Qu'en conclus-tu sur la protection apportée ?
Voir la réponseMasquer la réponse de l'exercice 1
- AllSigned :
MachinePolicy, fixée par GPO, est la portée la plus forte et elle est définie. - Rien en pratique : la valeur de
CurrentUserest bien enregistrée, mais PowerShell affiche une erreur qui prévient qu'elle est remplacée par une stratégie définie à une portée plus forte. La stratégie effective resteAllSigned. - Avec
.\Inventaire.ps1: non, le script n'est pas signé. Avec-ExecutionPolicy Bypass: non plus dans ce cas, car l'option règle la portéeProcess, plus faible queMachinePolicy. Mais l'utilisateur peut toujours ouvrir le fichier et coller son contenu dans la console : les commandes s'exécutent une par une, sans être un « script ». - La stratégie d'exécution, même par GPO, évite les exécutions accidentelles et oblige à signer les scripts officiels. Elle n'empêche pas un utilisateur décidé d'exécuter du code. Le vrai contrôle passe par App Control for Business ou AppLocker (et le mode de langage restreint), la journalisation des blocs de script et le moindre privilège.
Tu avais trouvé ?
Exercice 2 sur 4 La signature cassée Réussi À revoir
Le script signé Disable-ComptesDeparts.ps1 tournait tous les matins sur un serveur en AllSigned. Ce matin, il refuse de démarrer. Get-AuthenticodeSignature renvoie HashMismatch. Que s'est-il passé, et que fais-tu ?
Voir la réponseMasquer la réponse de l'exercice 2
HashMismatch signifie que le contenu du fichier ne correspond plus à celui qui a été signé : quelqu'un a modifié le script après sa signature. Ce peut être une correction légitime d'un collègue qui a oublié de le re-signer, ou une modification malveillante. Dans l'ordre :
- ne pas re-signer le fichier tel quel, ni passer la stratégie en
RemoteSigned« pour dépanner » ; - comparer le fichier avec la version de référence, dans le dépôt Git (
git diff) ou la dernière sauvegarde, pour voir ce qui a changé ; - chercher qui l'a modifié et quand : date de modification du fichier, journaux d'accès au partage, journaux de sécurité ;
- si la modification n'est pas expliquée, la traiter comme un incident de sécurité (un attaquant a pu écrire dans le dossier des scripts, donc il a des droits élevés) ; sinon, valider la correction, la commiter et re-signer le script.
C'est exactement le rôle de la signature : sans elle, la version modifiée aurait tourné sans que personne ne le voie.
Tu avais trouvé ?
Exercice 3 sur 4 Ranger les secrets Réussi À revoir
Pour chaque script, propose la façon de gérer le secret dont il a besoin, et justifie.
- La tâche planifiée nocturne de
srv-fichiersqui copie des fichiers sur un partage desrv-gestion. - Le script interactif de Chloé qui interroge l'API de Zabbix depuis son poste d'administration.
- Le timer systemd de
srv-journauxqui envoie chaque matin un rapport par l'API d'un outil de messagerie, avec un jeton. - Un script Bash que Moussa lance à la main, qui demande un mot de passe d'administration d'un équipement.
Voir la réponseMasquer la réponse de l'exercice 3
- Un gMSA comme compte de la tâche, avec le droit d'écrire sur ce seul partage : aucun secret à stocker, le mot de passe est géré par l'annuaire et change seul.
- Un coffre SecretManagement (SecretStore sur son poste, ou le coffre de l'entreprise) :
Get-Secret -Name ZabbixLecture -AsPlainTextdans le script. Le coffre est déverrouillé par Chloé quand elle travaille. - Un fichier
0600appartenant au compte du service, transmis parLoadCredential=dans l'unité systemd : seul ce service le lit, et il n'apparaît ni dans le script ni dans les arguments de commande. - Une saisie masquée :
read -rsp "Mot de passe : " mdp; echo. Le mot de passe ne s'affiche pas, ne va pas dans l'historique et disparaît à la fin du script ; il ne doit pas être passé en argument à une autre commande, mais par l'entrée standard ou une variable d'environnement.
Tu avais trouvé ?
Exercice 4 sur 4 Une règle sudo dangereuse Réussi À revoir
Pour que le compte rapports lance la sauvegarde, un technicien a écrit dans /etc/sudoers.d/rapports la ligne rapports ALL=(root) NOPASSWD: /bin/bash /home/rapports/sauvegarde.sh. Explique pourquoi cette règle donne en pratique tous les droits de root au compte rapports, et corrige.
Voir la réponseMasquer la réponse de l'exercice 4
Le script est dans /home/rapports, le dossier du compte rapports : ce compte peut le modifier à volonté, y ajouter n'importe quelle commande, puis le lancer en root sans mot de passe. La règle revient à « rapports peut exécuter n'importe quoi en root ». De plus, autoriser /bin/bash avec un argument est fragile : tout ce que fait le script se fait en root.
Correction : déplacer le script dans /usr/local/sbin, propriété de root et non modifiable par les autres (install -o root -g root -m 0750 sauvegarde.sh /usr/local/sbin/sauvegarde-config.sh, après l'avoir relu), le rendre exécutable par son shebang, et n'autoriser que lui :
rapports ALL=(root) NOPASSWD: /usr/local/sbin/sauvegarde-config.sh
Toujours éditer avec sudo visudo -f /etc/sudoers.d/rapports, qui vérifie la syntaxe : une erreur dans un fichier sudoers peut bloquer sudo pour tout le monde.
Tu avais trouvé ?