Le retour du root : SSH au lieu de su/sudo
En lieu et place de sudo, je préfère utiliser SSH. Je détaille dans cet article mon utilisation d'SSH et ma configuration pour réduire sa surface d'attaque. Accessoirement, je me plains encore.
Il y a quelques mois, j'écrivais un article qui aborde de surface l'ensemble des informations à savoir sur les outils communément utilisés pour l'élévation de privilèges, notamment les bien connus su
et sudo
.
Nous abordions également des concepts tels que les bits setuid et setgid, et j'y partageais des mesures de renforcement. J'indiquais notamment l'intérêt pour une machine locale d'utiliser un compte administratif non-root à partir duquel l'on pourrait considérer l'usage de sudo
, idéalement au sein d'une session sécurisée minimale.
Je voudrais désormais toutefois écrire un article plus personnel, cette deuxième partie qui ne sera pas aussi longue (en fait si, je mens), pour aborder :
- La séparation de privilèges sur une machine locale (bureau)
- Ce que je fais personnellement pour des machines distantes (serveurs)
- La configuration et la réduction de la surface d'attaque de SSH
- Et un petit aparté sur SSHFP en bonus !
Je préfère mettre cette deuxième partie à part pour la simple et bonne raison que je n'ai pas reçu une éducation formelle en sécurité informatique et que je ne me permettrais donc pas de donner des recommandations formelles. La première partie allait largement dans le sens de recommandations déjà existantes notamment chez l'ANSSI.
Sur une machine locale : de plus gros problèmes...
Je ne porte pas la sécurité de Linux particulièrement dans mon cœur :
Nombreuses sont les distributions Linux de bureau à désactiver l'accès à root pour pousser à l'usage de sudo
. Je pense malheureusement que c'est davantage une comédie sécuritaire qu'autre chose.
L'intérêt majeur de sudo
est d'être un outil simple pour restreindre la distribution de l'accès au compte root, mais il n'est pas conçu pour être une barrière de sécurité particulièrement robuste prévenant une escalade de privilège à partir d'un compte classique. Comme tout binaire ayant le bit setuid+root, il doit être considéré comme une importante surface d'attaque.
-rwsr-xr-x 1 root root 182600 Feb 27 2021 /usr/bin/sudo
Voici ce que donne sudo
fraîchement installé sur Debian : n'importe quel processus qui n'est pas a minima contenu par NO_NEW_PRIVS (désactive suid/sgid, cf. première partie) peut exploiter une vulnérabilité dans sudo
peu importe si ce dernier est configuré pour être restreint à un éventuel groupe dédié (sudo/wheel). Le fait de pouvoir l'exécuter est déjà important.
Je passe sur toutes les méthodes évidentes d'exfiltration déjà abordées au cours de précédents articles.
Le modèle de sécurité sur bureau est presque inexistant : à l'exception de quelques daemons du système, toutes les applications que vous utiliserez fonctionneront sous un même compte utilisateur ; et ce, sans modèle de sandboxing efficace qui préviendrait grâce à l'isolation :
- D'une part aux applications d'exfiltrer des données sensibles : faire confiance à la seule nature du code source ouvert d'un programme est une garantie très faible.
- D'autre part à ces applications de faire la même chose en étant tout simplement exploitées car contenant du code vulnérable.
Certaines distributions telles que Fedora (pris en exemple) configurent des MACs comme SELinux pour restreindre les accès, mais la magie s'estompe quand on se rend compte que tout tourne dans une politique unconfined
, ce qui ne change pas grand chose au final.
C'est ce que je m'évertue à trop souvent répéter, notamment ici :
Ce n'est donc pas un simple binaire qui doit représenter la séparation de privilèges entre root et votre compte personnel qui est au final "presque root" (mais on fait semblant que ce n'est pas le cas). D'où l'importance d'un modèle de sécurité pour les applications : c'est ce qui compte... et c'est ce qui n'existe pas pour cette classe de systèmes d'exploitation.
Au fond, la situation est tellement catastrophique qu'utiliser sudo
ou pas, ça ne change pas grand chose. Mais si vous insistez tant à utiliser une distribution classique pour des tâches sensibles, considérez les suggestions de la première partie : utilisez un compte dédié à l'usage de sudo
dans une session minimale. C'est déjà moins pire.
Et par pitié, ne commençons pas à parler de polkit : une composante traditionnellement utilisée sur les distributions de bureau pour faire communiquer des processus non-privilégiés à des processus privilégiés. Ce machin souffre régulièrement de sa nature de code vulnérable qui en plus dépend (hors fork tiers) de SpiderMonkey, un moteur complet pour JavaScript simplement pour des fichiers de configuration. Pourquoi accepte-t-on des surfaces d'attaques aussi énormes ? Quelque chose cloche.
Sur des machines distantes : mon héros SSH et comment je l'utilise
Quand je configure des serveurs, ma préoccupation majeure est la minimisation de la surface d'attaque. On y trouve donc les setuid+root : su
/pam que je verrouille, et puis le cas de sudo
arrive... Son usage est très répandu notamment pour la distribution décentralisée de l'accès aux privilèges root, permettant de limiter autant que possible la transmission du mot de passe root et éventuellement de restreindre les tâches privilégiées.
Mon avis ici : il est évident que chercher à restreindre les tâches avec sudo
est particulièrement difficile. Je prends souvent l'exemple facile de vim
pour expliquer que, même si l'on n'autorise que vim
en sa qualité d'éditeur, on peut simplement y ouvrir un shell root. Cet exemple est maigre car de toute façon on pourrait éditer presque n'importe quoi, mais c'est simplement pour illustrer qu'autoriser une exécution en tant que root peut avoir bien plus de conséquences qu'on puisse ne le penser au premier abord.
Ce n'est donc pas sur sudo
que vous devez vous reposer pour "limiter" les actions privilégiées d'un utilisateur. Considérez au fond que donner l'accès à sudo
, c'est ni plus ni moins donner accès à root. Vous devriez vous intéresser à des solutions robustes comme SELinux pour cela.
Personnellement, j'ai fait le choix de tout simplement utiliser SSH pour me connecter à root.
Mais on m'a toujours dit qu'il fallait ne pas autoriser SSH pour se connecter directement au compte root ?
En guise de contexte, ceci est souvent conseillé :
Alors, oui et non. Déjà ce n'est pas forcément une mauvaise chose et c'est une idée perçue malheureusement populaire : il n'y a pas un risque réellement plus grand à autoriser la connexion à distance sur root directement, alors que vous l'exposez in fine à la surface d'attaque qu'est sudo
. C'est même le contraire, et c'est assez cocasse quand on y pense.
Ce qui en revanche est une mauvaise pratique est de manipuler le mot de passe root. Délestez-vous de ce fardeau et utilisez des clés (ed25519), permettant une authentification robuste, auditable et décentralisée :
Ce paramètre interdit SSH d'accepter l'authentification par mot de passe au compte root. Le mot de passe est un moyen de distribution centralisé et généralement bien plus faible qu'une clé cryptographique. Il faudra néanmoins porter une attention particulière à la sécurité du stockage de cette dernière, donc à la sécurité du client évidemment.
Je ne suis pas confortable à l'idée d'entrer directement dans le compte root à distance...
C'est compréhensible. Un des arguments en faveur de sudo
est de limiter les erreurs humaines en explicitant les commandes entrées qui ont besoin de privilèges (même si on va pas se mentir, beaucoup préfixent sudo
machinalement à chaque commande). Ce que vous pouvez éventuellement faire est de n'autoriser que des connexions locales à root :
Ce n'est pas une mesure de sécurité directe, attention : le compte qui possède une clé est évidemment un compte "presque root", au final, qui mérite toute votre attention. Cependant cela peut vous aider à mieux gérer les commandes qui nécessitent ou non d'être montées en privilèges. Cela revient au même que d'avoir accès à sudo
dans l'idée où ce compte est indirectement privilégié (mais au moins on se débarrasse d'une surface d'attaque).
Je n'ai pas d'avis tranché : autoriser root via localhost seulement peut être perçu in fine comme une forme de sécurité par l'obscurité (les bots et autres scripts kiddies essayant souvent la connexion sur root) qui me semble vraiment peu utile voire introduit une nouvelle possibilité de se faire compromettre la clé permettant d'accéder à root.
Je communique cette possibilité simplement pour suivre "dans l'absolu" la "bonne pratique" de ne pas exposer root directement à l'extérieur... mais je vous conseille de remettre en question ce dogme datant de l'èretelnet
.
Pour générer des clés côté client :
ssh-keygen -t ed25519 -o -a 100
Si on vous demande une phrase de passe pour chiffrer la clé localement, demandez-vous si votre modèle de menace implique une possibilité d'exfiltration de cette clé. C'est notamment le cas si votre client est votre OS de bureau, où tout fonctionne sans restriction forte. Ne tombez pas non plus dans une fausse sensation de sécurité cela dit, car comme nous l'avons vu dans des précédents articles il est relativement trivial de recueillir les entrées de votre clavier sur cette classe de systèmes (sans compter les attaques sur le presse-papier ou le manque d'isolation du serveur graphique...).
Le paramètre -a 100
de la commande ci-dessus indique que nous souhaitons 100 tours de KDF (fonction de dérivation) pour le chiffrement de la clé. Combiné à une phrase de passe forte, les attaques par bruteforce de la clé sont loin d'être un problème.
La clé publique (le contenu de id_ed25519.pub
) devra ensuite être provisionnée dans ~/.ssh/authorized_keys
côté serveur. Assurez-vous que les permissions sur tous ces fichiers soient adéquates. Si vous voulez pouvoir vous connecter à root localement, alors il faudra refaire cette étape depuis le compte adapté.
Au lieu de le faire manuellement, on peut utiliser :
Vous pouvez désinstaller sudo
et restreindre su
si vous êtes satisfait de cette manière de faire, et même verrouiller le mot de passe du compte root puisque l'on souhaite utiliser des clés de toute façon :
Il est même possible d'avoir plusieurs comptes root aux noms différents :
Ce qui montre par ailleurs que reposer sa sécurité sur le simple nom d'un compte à deviner est assez risible. Il y a plein de possibilités à explorer !
Un des avantages de sudo
est sa simplicité à auditer les commandes qui ont été exécutées dans une session. Comme je l'avais mentionné dans la première partie, je conseille quelque chose comme auditd
, bien plus puissant, et qui permet de bien journaliser les actions effectuées.
Quoiqu'il en soit, vous êtes toujours invité à penser dans le cadre du modèle de sécurité d'un serveur :
- Séparer les utilisateurs des différents services
- Utiliser des sandbox (NO_NEW_PRIVS), des machines virtuelles...
- Considérer l'usage de MACs pour réellement limiter un utilisateur
Réduction de la surface d'attaque de SSH
Cela ne mérite pas d'en faire un article supplémentaire, donc je mettrais cette partie ici. La configuration du daemon SSH se passe dans /etc/ssh/sshd_config
.
Il s'agit tout d'abord de configurer SSH en limitant notamment les primitives cryptographiques peu sûres et peu communes.
HostKey /etc/ssh/ssh_host_ed25519_key
HostKeyAlgorithms ssh-ed25519
PubkeyAcceptedKeyTypes ssh-ed25519
KexAlgorithms curve25519-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com
Ces primitives devraient être supportées par n'importe quel client moderne. Aux amateurs, vous pouvez voir que je suis un fan de Bernstein. :)
Pour être certain que les clés de l'hôte utilisées pour l'authentification du serveur auprès du client (HostKey) n'ont pas été réutilisées par un script d'installation mal conçu, vous pouvez les regénérer :
cd /etc/ssh
rm ssh_host_ed25519_key*
ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" > /dev/null
Ensuite, au choix, autorisez la connexion à root à distance ou localement. Mais dans tous les cas, interdisez l'utilisation de mots de passe :
# Autoriser root sans mot de passe
PermitRootLogin prohibit-password
# Autoriser root sans mot de passe (localement)
PermitRootLogin no
Match Address 127.0.0.1
PermitRootLogin prohibit-password
En guise de bonne pratique, mettez les blocs Match
en fin de fichier.
Bannissez globalement l'utilisation de mots de passe (car ça craint) :
PasswordAuthentication no
ChallengeResponseAuthentication no # OpenSSH <8.7 (Debian)
KbdInteractiveAuthentication no # OpenSSH 8.7+
PubkeyAuthentication yes
Par défaut, la plupart des versions d'OpenSSH lisent aussi le fichier .ssh/authorized_keys2
. C'est rarement ce que vous souhaitez et cela peut être utilisé dans certaines attaques, donc on préférera indiquer ceci pour être plus tranquille :
AuthorizedKeysFile .ssh/authorized_keys
N'autorisez que les utilisateurs qui peuvent se connecter à distance (il est aussi possible d'utiliser AllowGroups
avec un groupe dédié) :
AllowUsers admin1 admin2
# Autre exemple avec usage de pattern (équivalent au Match)
AllowUsers user1 root@ADRESSE_IP
# Autorisation d'un groupe particulier
AllowGroups ssh-users
En réalité, ces paramètres ont une priorité sur PermitRootLogin no
qui devient de ce fait redondant. Il faudra cependant préciser AllowUsers root
(au niveau général ou dans le bloc Match Address 127.0.0.1
) si vous souhaitez pouvoir vous connecter à root. Cela reste une bonne pratique de limiter les comptes pouvant être accédés via SSH.
Configurez LoginGraceTime
pour limiter les tentatives de connexion indésirables : temps à partir duquel le serveur arrête la connexion si la tentative est infructueuse. 30 secondes ou moins est une bonne idée :
LoginGraceTime 30s
Dans la même idée MaxAuthTries
limite le nombre de tentatives par connexion. 4 ou moins est une bonne idée :
MaxAuthTries 4
Ensuite on peut aussi limiter directement le nombre de nouvelles connexions concurrentes (par origine) avec PerSourceMaxStartups
en montant éventuellement MaxStartups
(global) :
PerSourceMaxStartups 1
MaxStartups 4096
# Si le service ssh se plaint de "error: ppoll: Invalid argument"
# il faudra ajouter un overwrite LimitNOFILE=8192 pour ce service
Attention, ça ne fonctionne qu'avec OpenSSH 8.5 et ultérieur !
Le changement de port SSH n'est pas une mesure de sécurité, et relève de la sécurité par l'obscurité sur laquelle nous ne souhaitons pas nous baser. Cependant vous pouvez toujours le changer si vous souhaitez que les bots pullulent moins dans vos logs, à condition de savoir que ça ne sert pas à grand chose (promis ?) :
Port 22
Désactiver l'authentification par mot de passe en faveur des clés est une mesure à elle seule qui anéantit déjà 99% des tentatives de bruteforce. Vous pouvez également considérer de limiter l'accès à des adresses connues, ce qui est encore plus efficace que le simple changement de port.
Vous pouvez tester ces modifications en redémarrant le service SSH : votre session actuelle ne sera pas déconnectée, vous pouvez (et devez) ainsi tester si tout va bien. Je conseille aussi d'utiliser une version d'OpenSSH évidemment à jour. Cela vaut aussi pour Debian qui dans sa version stable n'apporte qu'un sous-ensemble de correctifs : utilisez les backports si possible (mais évitez Sid qui n'a pas une équipe de sécurité dédiée).
Petite astuce : imaginez que vous souhaitez écrire des scripts d'automatisation nécessitant un accès à root, mais vous voulez restreindre cette connexion à une commande (et pas plus !). Il est possible dans /root/.ssh/authorized_keys
de préfixer la clé utilisée pour cette connexion avec une commande donnée :
command="/usr/bin/apt update" ssh-ed25519 ...
Soit dit en passant, le préfixe from="ADRESSE_IP"
permet d'indiquer à SSH de n'autoriser la connexion via cette clé que via une adresse bien précise.
On peut enfin configurer ceci dans sshd_config
sans oublier la note à propos de AllowUsers
si cette directive est utilisée :
PermitRootLogin forced-commands-only
La commande sera enclenchée et rien d'autre !
Nous avons donc parcouru plusieurs façons de restreindre l'accès à root. Ces morceaux de configuration ne sont pas à recopier directement, j'insiste : ils vous permettront de déterminer le choix idéal. Encore une fois, se connecter à distance à root n'a rien de farfelu bien que l'idée du contraire soit souvent répandue.
Note sur le SSH Agent Forwarding
Je ne suis personnellement pas un fan de cette méthode et je ne la conseille pas vraiment, mais je l'aborderai quand même à une fin explicative.
Il existe une autre façon de pouvoir se connecter à root localement sur un serveur distant, sans pour autant fournir une clé à un compte régulier du serveur qui sera "presque root", ce qui n'apportait (à mon avis) pas un réel gain de sécurité par rapport à la connexion directe à root de l'extérieur.
L'idée est d'utiliser le SSH Agent Forwarding, c'est-à-dire la capacité d'OpenSSH à transmettre votre "agent" (ou "identité") : votre clé privée ne quittera pas votre client, mais l'agent permettra depuis le serveur de vous connecter à d'autres comptes voire d'autres serveurs avec la même clé.
Je n'en ai pas parlé jusqu'ici car selon moi l'Agent Forwarding (que je vais abréger AF) nécessite un minimum de discipline dans son utilisation. En quelque sorte l'AF est un service qui agit comme un trousseau de clés SSH, et plus grossièrement comme un gestionnaire de mots de passe. Cela vous permet, une fois le service lancé, de ne pas devoir à rentrer la phrase de passe pour la déchiffrer plusieurs fois car elle sera chargée en mémoire.
On se souvient de la catastrophe chez Matrix... on y arrive.
Votre principal outil côté client sera ssh-agent
. La mise en place du service est à la discrétion de votre plateforme. Arch Linux comme à son habitude dispose d'une excellente documentation à ce sujet. Certaines commandes peuvent légèrement varier sur macOS et Windows.
Néanmoins je déconseille fortement l'utilisation de ssh-agent
sur Windows. Au lieu de charger en mémoire (RAM), le service stocke les clés... sur le disque, dans le registre. Même après suppression, des traces peuvent subsister dû à la nature non-volatile du support.
Pour ajouter une clé privée au service :
# Ajout de durée infinie
ssh-add ~/.ssh/id_ed25519
# Ajout pour une durée déterminée (ici 1h)
ssh-add -t 1h ~/.ssh/id_ed25519
# Confirmation à chaque utilisation de l'agent
ssh-add -c ~/.ssh/id_ed25519
La clé publique doit bien évidemment être ajoutée dans le fichier ~/.ssh/authorized_keys
de chaque serveur et compte auxquels vous souhaitez pouvoir vous connecter.
Enfin, pour vous connecter au serveur avec l'AF activé, on précise -A
:
ssh -A user@server.domain.tld
Et normalement, on doit pouvoir se connecter à root ainsi :
ssh root@localhost
Sans oublier que /root/.ssh/authorized_keys
doit au préalable contenir la clé publique de votre client. Il n'y a pas de clé à avoir ailleurs (pas sur un compte du serveur), et pourtant vous pouvez vous connecter simplement à root grâce à l'Agent Forwarding.
Maintenant, les points négatifs de cette méthode :
- Le service chargeant les clés en mémoire, il faudrait les décharger de la mémoire quand vous n'en avez plus besoin. Par exemple en arrêtant le service, ou en supprimant les clés avec
ssh-add -D
. - Vous devez vraiment faire confiance au serveur auquel vous vous connectez : bien qu'il ne pourra évidemment pas récupérer la clé privée, il pourra utiliser l'agent pour se connecter à des serveurs qui reconnaissent votre clé. D'où l'intérêt de la partie suivante...
Encore une fois, je ne suis pas un fan de l'Agent Forwarding, intéressant sur le papier pour notre usage mais avec de gros défauts. Même si l'authenticité du serveur est garantie en face, si ce dernier est compromis en profondeur, vous allez amèrement le regretter (même si vous avez déjà de gros problèmes). Le post-mortem de Matrix est assez évocateur de ces soucis-là.
SSHFP : une petite cure de tōfu ?
SSHFP est un standard permis par DNSSEC. Il consiste à épingler la clé publique de l'hôte dans une zone DNS signée (d'où l'intérêt de DNSSEC), permettant au client de s'assurer de l'authenticité du serveur en face. Pour ce faire, il faut d'abord un domaine pour le serveur, avec DNSSEC actif.
Ensuite, il faut ajouter un enregistrement SSHFP
dans la zone DNS. Pas de panique, OpenSSH a déjà un utilitaire pour les générer pour vous :
ssh-keygen -r server.domain.tld
L'utilitaire devrait automatiquement utiliser les clés publiques présentes dans /etc/ssh
. Vous devriez voir une liste d'enregistrements. Ceux qui nous intéressent possèdent une quatrième partie avec un "4", indiquant l'usage de ed25519 comme primitive :
server.domain.tld IN SSHFP 4 1 ...
server.domain.tld IN SSHFP 4 2 ...
La cinquième partie indique l'usage de SHA-1 (1) ou de SHA-256 (2) pour l'algorithme de l'empreinte de la clé publique. Vous pouvez juste ajouter la seconde dans votre zone DNS, tant que votre client est compatible.
Côté client justement, on peut vérifier la présence de SSHFP :
ssh-keyscan -D server.domain.tld
dig SSHFP server.domain.tld +short
Bien, si votre résolveur DNS est paré et que les enregistrements SSHFP sont propagés, il faut maintenant configurer le client pour utiliser SSHFP :
VerifyHostKeyDNS=yes
indique à OpenSSH d'utiliser SSHFP pour vérifier l'authenticité, et UserKnownHostsFile=/dev/null
permet d'être sûr que SSH n'utilise pas l'empreinte qui pourrait déjà exister dans known_hosts
. C'est le moment d'expliquer qu'en temps normal, vous reposeriez sur une sécurité suivant le modèle TOFU (trust on first use) : vous faites confiance au serveur la première fois, et son identité est ensuite garantie dans known_hosts
. SSHFP résout ce problème de confiance initiale à condition évidemment que vous fassiez confiance à l'infrastructure autour de DNSSEC...
Attention, ça ne marche pas bien sur la version native d'OpenSSH de Windows à l'heure actuelle, qui ne supporte toujours pas getrrsetbyname.
VerifyHostKeyDNS
peut être configuré dans /etc/ssh/ssh_client
.
Une piste alternative pour une garantie d'authenticité (pas forcément substituable à SSHFP) serait de recourir à l'utilisation de certificats SSH avec une autorité de certification (CA). Je n'ai personnellement pas exploré cette piste à l'heure actuelle donc je ne pourrais la détailler moi-même, mais je vous encourage à faire vos propres recherches à ce sujet. Cette solution, plus lourde à mettre en place, me paraît plus adaptée dans des environnements où le scaling est exigé.
Récapitulatif et conclusion
J'espère que cet article aura pu vous donner une bonne idée de comment j'utilise SSH en remplacement direct de sudo
ou su
sur mes machines. Faisons donc un petit récapitulatif car je pense qu'il y a beaucoup d'informations dispersées.
su
: binaire setuid+root, disponible par défaut sur l'écrasante majorité des distributions UNIX-like, distribution centralisée des mots de passe. Doit être utilisé avec parcimonie en raison de la manipulation de mots de passe et avec -
pour prévenir (entre autres) des attaques via $PATH
.
Ce que je fais : restreindresu
au groupe wheel, et verrouiller les mots de passe des comptes que j'accède par clé avecpasswd -l
.
sudo
: binaire setuid+root, avec journalisation par défaut, configurable mais aussi avec une plus grande surface d'attaque. N'est en réalité pas suffisamment robuste pour limiter les actions root et revient à donner les privilèges en se donnant bonne conscience.
Ce que je fais : je n'installe pas sudo
sauf si j'en ai réellement besoin.
ssh
: surface d'attaque minimale/éprouvée, authentification décentralisée avec clés cryptographiques. Peut aussi bénéficier de la journalisation.
Ce que je fais : j'utilise SSH, configuré pour pouvoir uniquement me connecter avec des clés à des comptes privilégiés ou réguliers.
Idéalement, il faut malgré tout limiter au maximum les opérations privilégiées quand cela est possible.
Cela s'applique surtout au modèle de sécurité serveur. Nous avons déjà mentionné en longueur le cas des machines de bureau, qui ont d'autres plus gros problèmes que sudo
, lequel a bien évidemment toujours des cas d'utilisations valides comme pour les rôles SELinux.
D'autres auteurs font part de la même problématique :