Transformer un Mac en serveur

macOS n'est pas conçu pour être un serveur, mais il est possible de s'en approcher raisonnablement. Cet article détaille comment désactiver les agents utilisateurs et les daemons système inutiles sur un Mac (Mac Mini ou VM UTM) pour obtenir un environnement allégé, piloté exclusivement via SSH. On y aborde également la question de WindowServer — qu'on ne peut pas supprimer sans renoncer à SIP — et la mise en place d'un accès graphique ponctuel via VNC tunnelisé dans SSH, en utilisant uniquement des outils gratuits et open source.


Changements entre macOS Tahoe 26.3 et 26.4

La version 26.4 introduit plusieurs nouveaux services liés à Apple Intelligence (intelligentroutingd, milod, intelligencetasksd, textunderstandingd, ciphermld), au tracking biométrique et comportemental (biomesyncd, BiomeAgent), aux accessoires (accessoryupdaterd, uarpassetmanagerd, uarpd), et à l'affichage (AmbientDisplayAgent, liquiddetectiond, cameracaptured). D'autres services consumer apparaissent également : appstoreagent, GamePolicyAgent, managedappdistributionagent, devicecheckd, businessservicesd, swtransparencyd, entre autres.

Extension massive de SIP — Là où la version 26.3 ne comptait que ~8 daemons irréductibles (bluetoothd, AirPlayXPCHelper, analyticsd, triald.system, backupd, backupd-helper, mediaremoted, corespeechd_system), la 26.4 en protège désormais plus de 40, incluant la plupart des services Apple Intelligence, iCloud/Find My, Asset Cache, Bluetooth, NFC, accessoires UARP, et plusieurs services d'affichage et de télémétrie. La commande launchctl disable est acceptée silencieusement par le système mais n'a aucun effet sur ces services, et même le bootout suivi d'un kill -9 ne les empêche pas de revenir au reboot.


Pré-requis


Lister les applications pré-installées

La liste des applications pré-installées par macOS s'obtient ainsi :

ls -shla /System/Applications ; ls -shla /Applications

Doit-on supprimer ces applications ?
C'est impossible même avec root car elles sont protégées par SIP et SSV.
Il faudrait passer par le mode Recovery pour briser le sceau cryptographique.
Problème : à chaque MAJ, tout sera restauré !

Quelle approche donc ?
→ Désactiver les daemons/agents et empêcher ces applications de se lancer.

Voir tous les agents utilisateurs actifs :

launchctl list | grep -v "^-" | sort

Voir tous les daemons système actifs :

sudo launchctl list | grep com.apple | sort

Script 1 — disable-consumer-agents.sh

Premier script : disable-consumer-agents.sh pour désactiver les agents utilisateurs superflus.
(conçu en suivant les listes obtenues au-dessus)

#!/bin/bash
# disable-consumer-agents.sh
# Désactive tous les agents utilisateur non nécessaires sur un serveur macOS
# Mis à jour pour macOS Tahoe 26.4

UID_CURRENT=$(id -u)

disable_agent() {
 local service="com.apple.$1"
 echo " Disabling: $service"
 launchctl disable "gui/${UID_CURRENT}/${service}" 2>/dev/null
 launchctl kill SIGTERM "gui/${UID_CURRENT}/${service}" 2>/dev/null
}

echo "=== Siri / IA / ML ==="
for svc in assistantd siriactionsd "siri.context.service" siriknowledged \
 siriinferenced sirittsd assistant_cdmd corespeechd mlhostd \
 generativeexperiencesd intelligenceplatformd intelligencecontextd \
 ModelCatalogAgent duetexpertd knowledge-agent knowledgeconstructiond \
 "spotlightknowledged.updater" privatecloudcomputed \
 intelligentroutingd \
 milod; do
 disable_agent "$svc"
done

echo "=== iCloud consumer ==="
for svc in bird cloudd cloudphotod "icloud.searchpartyuseragent" \
 iCloudNotificationAgent \
 "protectedcloudstorage.protectedcloudkeysyncing" \
 replicatord syncdefaultsd cmfsyncagent; do
 disable_agent "$svc"
done

echo "=== Find My / Localisation ==="
for svc in "findmy.findmylocateagent" \
 CoreLocationAgent \
 routined; do
 disable_agent "$svc"
done

echo "=== Médias ==="
for svc in AMPLibraryAgent AMPArtworkAgent AMPDeviceDiscoveryAgent \
 "podcasts.PodcastContentService" photolibraryd photoanalysisd \
 mediaanalysisd mediaremoteagent itunescloudd replayd \
 "videoconference.camera" \
 amsondevicestoraged; do
 disable_agent "$svc"
done

echo "=== Communications ==="
for svc in imagent "imcore.imtransferagent" \
 "imdpersistence.IMDPersistenceAgent" \
 imautomatichistorydeletionagent CallHistoryPluginHelper \
 CallHistorySyncHelper CommCenter \
 "telephonyutilities.callservicesd" rapportd; do
 disable_agent "$svc"
done

echo "=== Bluetooth / Continuity ==="
for svc in "BTServer.cloudpairing" \
 "cmio.ContinuityCaptureAgent" \
 ThreadCommissionerService; do
 disable_agent "$svc"
done

echo "=== Télémétrie / Analytics ==="
for svc in analyticsagent geoanalyticsd feedbackd dprivacyd \
 proactiveeventtrackerd UsageTrackingAgent parsec-fbf \
 inputanalyticsd "ap.adprivacyd" "ap.promotedcontentd" \
 triald metrickitd \
 diagnostics_agent \
 diagnosticextensionsd \
 spindump_agent \
 BiomeAgent \
 parsecd; do
 disable_agent "$svc"
done

echo "=== Consumer divers ==="
for svc in "Maps.mapssyncd" geod geodMachServiceBridge homed homeeventsd \
 homeenergyd "GameController.gamecontrolleragentd" financed \
 commerce weatherd tipsd remindd recentsd voicebankingd avatarsd \
 amsaccountsd amsengagementd followupd frauddefensed \
 appplaceholdersyncd liveactivitiesd studentd assessmentagent \
 "email.maild" "Safari.PasswordBreachAgent" SafariBookmarksSyncAgent \
 familycircled FamilyControlsAgent ScreenTimeAgent \
 "backgroundassets.user" jetpackassetd AirPlayUIAgent sharingd \
 StatusKitAgent \
 FeatureAccessAgent \
 suggestd \
 sociallayerd \
 ContextStoreAgent \
 calaccessd \
 "contacts.donation-agent" \
 SoftwareUpdateNotificationManager \
 MENotificationService \
 "WorkflowKit.ShortcutsViewService" \
 "MobileAccessoryUpdater.fudHelperAgent" \
 devicecheckd \
 managedappdistributionagent \
 ndoagent \
 swcd \
 SecureBackupDaemon \
 "coreservices.useractivityd"; do
 disable_agent "$svc"
done

echo ""
echo "=== Terminé. Reboot recommandé pour valider. ==="

Script 2 — disable-system-daemons.sh

Deuxième script : disable-system-daemons.sh pour désactiver les daemons systèmes non nécessaires pour un serveur macOS.
Lancer avec sudo.

#!/bin/bash
# disable-system-daemons.sh
# Désactive les daemons système non nécessaires sur un serveur macOS
# Mis à jour pour macOS Tahoe 26.4
# Lancer AVEC sudo

if [ "$EUID" -ne 0 ]; then
 echo "Ce script doit être lancé avec sudo"
 exit 1
fi

disable_daemon() {
 local service="com.apple.$1"
 echo " Disabling: $service"
 launchctl disable "system/${service}" 2>/dev/null
 launchctl kill SIGTERM "system/${service}" 2>/dev/null
}

echo "=== Télémétrie / Analytics système ==="
for svc in analyticsd \
 osanalytics.osanalyticshelper \
 audioanalyticsd \
 ecosystemanalyticsd \
 wifianalyticsd \
 SubmitDiagInfo \
 tailspind \
 rtcreportingd \
 spindump \
 sysdiagnose \
 appleseed.fbahelperd \
 betaenrollmentd \
 triald.system \
 PerfPowerTelemetryClientRegistrationService \
 CrashReporterSupportHelper \
 coresymbolicationd \
 ecosystemd \
 adid \
 dprivacyd \
 "signpost.signpost_reporter" \
 bosreporter \
 boswatcher; do
 disable_daemon "$svc"
done

echo "=== Apple Intelligence / ML système ==="
for svc in modelcatalogd \
 modelmanagerd \
 corespeechd_system \
 aned \
 aneuserd \
 ospredictiond \
 coreduetd \
 contextstored \
 biomed \
 "siri.acousticsignature" \
 handwritingd \
 centaurid; do
 disable_daemon "$svc"
done

echo "=== iCloud / Find My système ==="
for svc in icloud.searchpartyd \
 icloud.findmydeviced \
 findmy.findmybeaconingd \
 findmymacd \
 findmymacmessenger; do
 disable_daemon "$svc"
done

echo "=== App Store / Commerce ==="
for svc in appstored \
 AppStoreDaemon.StorePrivilegedTaskService \
 AppStoreDaemon.StorePrivilegedODRService \
 commerce \
 storeaccountd \
 storeassetd \
 storedownloadd \
 storelegacy \
 storereceiptinstaller \
 storeuid \
 inboxupdaterd \
 appinstalld; do
 disable_daemon "$svc"
done

echo "=== Médias / Entertainment système ==="
for svc in musicd \
 mediaremoted \
 AssetCache.builtin \
 AssetCacheManagerService \
 AssetCacheTetheratorService \
 AssetCacheLocatorService \
 assetsubscriptiond; do
 disable_daemon "$svc"
done

echo "=== Bluetooth ==="
for svc in bluetoothd \
 BlueTool \
 BTServer.le \
 nearbyd \
 rapportd; do
 disable_daemon "$svc"
done

echo "=== NFC / Accessoires inutiles ==="
for svc in nfcd \
 usbsmartcardreaderd \
 avbdeviced \
 usbaudiod \
 usbctelemetryd \
 threadradiod \
 accessoryupdaterd \
 uarpassetmanagerd \
 uarpd \
 uarphidd; do
 disable_daemon "$svc"
done

echo "=== AirPlay / Partage ==="
for svc in AirPlayXPCHelper \
 NetworkSharing \
 wifip2pd \
 wifivelocityd \
 wifianalyticsd \
 srp-mdns-proxy; do
 disable_daemon "$svc"
done

echo "=== Jeux ==="
for svc in GameController.gamecontrollerd \
 gamepolicyd \
 fpsd.arcadeservice; do
 disable_daemon "$svc"
done

echo "=== Time Machine ==="
for svc in backupd \
 backupd-helper; do
 disable_daemon "$svc"
done

echo "=== MDM / Enterprise ==="
for svc in familycontrols \
 mdmclient.daemon \
 ManagedClient \
 ManagedClient.enroll \
 remotemanagementd \
 managedappdistributiond; do
 disable_daemon "$svc"
done

echo "=== Postfix ==="
for svc in postfix.master \
 postfix.newaliases; do
 disable_daemon "$svc"
done

echo "=== Spotlight indexation ==="
for svc in metadata.mds.scan \
 metadata.mds.index \
 metadata.mds.spindump; do
 disable_daemon "$svc"
done

echo "=== Affichage / Consumer divers ==="
for svc in AmbientDisplayAgent \
 wallpaper.export \
 liquiddetectiond \
 cameracaptured \
 countryd \
 fairplaydeviceidentityd; do
 disable_daemon "$svc"
done

echo ""
echo "=== Terminé. Reboot recommandé. ==="

Script 3 — disable-resistant-daemons.sh

Troisième script : disable-resistant-daemons.sh désactive les agents et daemons résistants au simple disable en utilisant bootout.
Lancer avec sudo.

#!/bin/bash
# disable-resistant-daemons.sh
# Bootout + disable des services résistants (agents + daemons)
# Mis à jour pour macOS Tahoe 26.4
# Lancer AVEC sudo

if [ "$EUID" -ne 0 ]; then
 echo "Ce script doit être lancé avec sudo"
 exit 1
fi

UID_TARGET=$(id -u "${SUDO_USER:-$USER}")

bootout_system() {
 local service="com.apple.$1"
 echo " → [system] ${service}"
 launchctl bootout "system/${service}" 2>/dev/null
 local pid
 pid=$(launchctl list "${service}" 2>/dev/null \
 | awk '/"PID"/{gsub(/[^0-9]/,"",$3); print $3}')
 if [ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null; then
 echo " ↳ PID ${pid} encore vivant, kill -9 forcé"
 kill -9 "$pid" 2>/dev/null
 fi
 launchctl disable "system/${service}" 2>/dev/null
}

bootout_agent() {
 local service="com.apple.$1"
 echo " → [agent] ${service}"
 launchctl bootout "gui/${UID_TARGET}/${service}" 2>/dev/null
 launchctl disable "gui/${UID_TARGET}/${service}" 2>/dev/null
}

check_system() {
 local svc="com.apple.$1"
 local pid
 pid=$(launchctl list "${svc}" 2>/dev/null \
 | awk '/"PID"/{gsub(/[^0-9]/,"",$3); print $3}')
 if [ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null; then
 echo " ⚠ Toujours actif : ${svc} (PID ${pid})"
 else
 echo " ✓ Inactif : ${svc}"
 fi
}

# ═══════════════════════════════════════════
# AGENTS UTILISATEUR (gui/UID)
# ═══════════════════════════════════════════

echo ""
echo "╔═══════════════════════════════════════╗"
echo "║ AGENTS UTILISATEUR — bootout ║"
echo "╚═══════════════════════════════════════╝"

echo ""
echo "=== Siri / IA / ML ==="
for svc in assistantd siriactionsd "siri.context.service" siriknowledged \
 siriinferenced sirittsd assistant_cdmd corespeechd \
 generativeexperiencesd ModelCatalogAgent duetexpertd \
 knowledge-agent "spotlightknowledged.updater" \
 intelligentroutingd milod; do
 bootout_agent "$svc"
done

echo ""
echo "=== iCloud consumer ==="
for svc in bird cloudd "icloud.searchpartyuseragent" \
 iCloudNotificationAgent replicatord syncdefaultsd cmfsyncagent; do
 bootout_agent "$svc"
done

echo ""
echo "=== Find My / Localisation ==="
for svc in "findmy.findmylocateagent" CoreLocationAgent routined; do
 bootout_agent "$svc"
done

echo ""
echo "=== Médias ==="
for svc in AMPDeviceDiscoveryAgent photolibraryd photoanalysisd \
 mediaanalysisd mediaremoteagent itunescloudd replayd \
 "videoconference.camera"; do
 bootout_agent "$svc"
done

echo ""
echo "=== Communications ==="
for svc in imagent "imcore.imtransferagent" \
 "imdpersistence.IMDPersistenceAgent" \
 CallHistoryPluginHelper CallHistorySyncHelper CommCenter \
 "telephonyutilities.callservicesd" rapportd; do
 bootout_agent "$svc"
done

echo ""
echo "=== Bluetooth / Continuity ==="
for svc in "BTServer.cloudpairing" "cmio.ContinuityCaptureAgent" \
 ThreadCommissionerService; do
 bootout_agent "$svc"
done

echo ""
echo "=== Télémétrie / Analytics ==="
for svc in analyticsagent UsageTrackingAgent parsec-fbf inputanalyticsd \
 "ap.adprivacyd" "ap.promotedcontentd" triald \
 diagnostics_agent diagnosticextensionsd BiomeAgent parsecd \
 biomesyncd; do
 bootout_agent "$svc"
done

echo ""
echo "=== Consumer divers ==="
for svc in geod geodMachServiceBridge homed homeeventsd homeenergyd \
 "GameController.gamecontrolleragentd" financed weatherd \
 remindd voicebankingd amsaccountsd amsengagementd followupd \
 frauddefensed appplaceholdersyncd liveactivitiesd assessmentagent \
 "email.maild" familycircled FamilyControlsAgent ScreenTimeAgent \
 AirPlayUIAgent sharingd StatusKitAgent FeatureAccessAgent \
 suggestd sociallayerd ContextStoreAgent calaccessd \
 "contacts.donation-agent" SoftwareUpdateNotificationManager \
 "WorkflowKit.ShortcutsViewService" ndoagent SecureBackupDaemon \
 "coreservices.useractivityd" appstoreagent swcd \
 MENotificationService "MobileAccessoryUpdater.fudHelperAgent" \
 MTLAssetUpgraderD; do
 bootout_agent "$svc"
done

# ═══════════════════════════════════════════
# DAEMONS SYSTÈME (system/)
# ═══════════════════════════════════════════

echo ""
echo "╔═══════════════════════════════════════╗"
echo "║ DAEMONS SYSTÈME — bootout ║"
echo "╚═══════════════════════════════════════╝"

echo ""
echo "=== Télémétrie / Analytics ==="
for svc in analyticsd osanalytics.osanalyticshelper rtcreportingd \
 spindump sysdiagnose tailspind SubmitDiagInfo \
 triald.system PerfPowerTelemetryClientRegistrationService \
 CrashReporterSupportHelper coresymbolicationd adid \
 ReportMemoryException; do
 bootout_system "$svc"
done

echo ""
echo "=== Apple Intelligence / ML système ==="
for svc in modelcatalogd modelmanagerd corespeechd_system \
 coreduetd contextstored biomed; do
 bootout_system "$svc"
done

echo ""
echo "=== iCloud / Find My système ==="
for svc in icloud.searchpartyd icloud.findmydeviced \
 findmy.findmybeaconingd findmymacd cloudd; do
 bootout_system "$svc"
done

echo ""
echo "=== App Store / Commerce ==="
for svc in AppStoreDaemon.StorePrivilegedODRService; do
 bootout_system "$svc"
done

echo ""
echo "=== Médias / Asset Cache ==="
for svc in mediaremoted AssetCache.builtin AssetCacheTetheratorService \
 AssetCacheLocatorService assetsubscriptiond; do
 bootout_system "$svc"
done

echo ""
echo "=== Bluetooth ==="
for svc in bluetoothd nearbyd; do
 bootout_system "$svc"
done

echo ""
echo "=== NFC / Accessoires ==="
for svc in nfcd accessoryupdaterd uarpassetmanagerd uarpd; do
 bootout_system "$svc"
done

echo ""
echo "=== AirPlay / Partage ==="
for svc in AirPlayXPCHelper wifip2pd; do
 bootout_system "$svc"
done

echo ""
echo "=== Jeux ==="
for svc in GameController.gamecontrollerd; do
 bootout_system "$svc"
done

echo ""
echo "=== Time Machine ==="
for svc in backupd backupd-helper; do
 bootout_system "$svc"
done

echo ""
echo "=== Affichage / Consumer ==="
for svc in AmbientDisplayAgent wallpaper.export liquiddetectiond \
 cameracaptured countryd fairplaydeviceidentityd; do
 bootout_system "$svc"
done

echo ""
echo "════════════════════════════════════════"
echo " Vérification post-bootout (daemons)"
echo "════════════════════════════════════════"

for svc in \
 analyticsd \
 AirPlayXPCHelper \
 bluetoothd \
 mediaremoted \
 corespeechd_system \
 triald.system \
 backupd \
 modelcatalogd \
 icloud.searchpartyd \
 GameController.gamecontrollerd \
 biomed \
 coreduetd \
 contextstored; do
 check_system "$svc"
done

echo ""
echo "=== Reboot recommandé pour valider la persistance ==="

Exécution des scripts

# 1 — Sans sudo
chmod +x disable-consumer-agents.sh && ./disable-consumer-agents.sh

# 2 — Avec sudo
chmod +x disable-system-daemons.sh && sudo ./disable-system-daemons.sh

# 3 — Avec sudo (optionnel, nettoie à chaud)
chmod +x disable-resistant-daemons.sh && sudo ./disable-resistant-daemons.sh

# Puis reboot
sudo reboot

Résultats obtenus


Bon à savoir

Contrairement à la version 26.3 qui ne comptait qu'une poignée de daemons irréductibles, macOS 26.4 protège désormais plus de 40 daemons système via SIP. Parmi eux : analyticsd, mediaremoted, corespeechd_system, coreduetd, contextstored, biomed, triald.system, modelcatalogd, modelmanagerd, aned, aneuserd, icloud.searchpartyd, icloud.findmydeviced, findmy.findmybeaconingd, findmymacd, cloudd, AssetCache.builtin, AssetCacheTetheratorService, AssetCacheLocatorService, assetsubscriptiond, bluetoothd, nearbyd, nfcd, accessoryupdaterd, uarpassetmanagerd, uarpd, AirPlayXPCHelper, wifip2pd, GameController.gamecontrollerd, backupd, backupd-helper, AmbientDisplayAgent, wallpaper.export, liquiddetectiond, cameracaptured, countryd, fairplaydeviceidentityd, PerfPowerTelemetryClientRegistrationService, CrashReporterSupportHelper, coresymbolicationd, osanalytics.osanalyticshelper, adid, ReportMemoryException, metadata.mds.scan.

Le seul moyen de les désactiver : le mode Recovery avec csrutil disable. Mais ce n'est pas vraiment souhaitable.

Vérifier si un disable a réellement été enregistré :

sudo plutil -p /var/db/com.apple.xpc.launchd/disabled.plist \
 | grep "nom.du.service"

(Si le service n'apparaît pas → SIP a ignoré la commande silencieusement)


Installer Homebrew

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Suivre les indications données à la fin de l'installation :

echo >> ~/.zprofile ; echo 'eval "$(/opt/homebrew/bin/brew shellenv zsh)"' >> ~/.zprofile ; eval "$(/opt/homebrew/bin/brew shellenv zsh)"

Peut-on supprimer WindowServer et booter en mode terminal sur macOS ?

Réponse courte : non. Voici pourquoi.

WindowServer n'est pas seulement un serveur d'affichage. C'est le point central de la session utilisateur sur macOS. Il gère :

Contrairement à X11 qu'on peut simplement choisir de ne pas démarrer, WindowServer est démarré par loginwindow, lui-même démarré par launchd comme condition de session. On ne peut pas le bootout sans effondrer toute la session.

Option 1 — Mode console (>console)

Il existait un mode sans session GUI sur macOS, équivalent au runlevel 3 sous Linux. Au boot, macOS présentait un login en mode texte. WindowServer ne démarrait pas.
Cette option ne fonctionne plus depuis Monterey, et Tahoe ne fait pas exception :

sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser -string ">console"
sudo defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser

(Affiche bien >console… mais au reboot, la GUI démarre quand même. loginwindow ignore silencieusement cette valeur depuis Monterey.)

Annuler :

sudo defaults delete /Library/Preferences/com.apple.loginwindow autoLoginUser

Option 2 — Autologin

La session GUI démarre automatiquement sans mot de passe. WindowServer tourne à vide, tout passe par SSH. C'est le comportement correct pour un serveur headless.

sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser $USER

Problème : si FileVault est activé, l'autologin est bloqué inconditionnellement par Apple.
Vérifier :

sudo fdesetup status

(FileVault is On → autologin impossible, login GUI obligatoire au boot.)

Option 3 — Désactiver SIP depuis Recovery Mode

La seule vraie solution pour empêcher WindowServer de démarrer. Depuis le Terminal en Recovery :

csrutil disable

Puis bootout de WindowServer et loginwindow via notre script de cleanup.
Non recommandé en production : SIP protège aussi l'intégrité des binaires système.

Conclusion

Sur un Mac Mini serveur ou une VM avec FileVault activé et SIP intact, le workflow réaliste est le suivant :
1. Un login GUI au boot (physiquement, ou via un KVM/écran) si nécessaire.
2. Sinon tout via une connexion en SSH.

Tableau des blocages :

macOS n'est architecturalement plus conçu pour être un système headless au sens Unix du terme. C'est un choix délibéré d'Apple.


Configurer une connexion VNC sécurisée via tunnel SSH sur macOS

SSH est déjà fonctionnel et on n'a pas touché au port 22.
(consultez l'article "Activer et configurer le serveur SSH sur macOS Tahoe" pour savoir comment changer le port SSH)

Il existe des alternatives payantes ou propriétaires pour accéder graphiquement à un Mac distant : Screens 5 et Jump Desktop sont des clients VNC macOS bien intégrés, Apple Remote Desktop (ARD) offre des fonctionnalités d'administration avancées, et RustDesk permet de s'auto-héberger avec son propre serveur de relais. Ces solutions ont l'avantage d'être plus simples à configurer, mais elles impliquent soit un coût, soit une dépendance à un serveur tiers. L'approche retenue ici repose uniquement sur des outils gratuits et open source : le serveur VNC natif de macOS, PF pour restreindre l'accès, et TigerVNC comme client.

Étape 1 — Activer le partage d'écran

Dans "Réglages Système" → "Général" → "Partage" → "Partage d'écran", Activer.

Étape 2 — Sécuriser la connexion VNC avec PF

Empêcher quiconque de se connecter en VNC sans passer par un tunnel SSH :

sudo nano /etc/pf.anchors/vnc-restrict

Coller le contenu suivant :

# Bloquer VNC depuis l'extérieur
block return in proto tcp from any to any port 5900

# Autoriser uniquement localhost (pour le tunnel SSH)
pass in quick on lo0 proto tcp from any to any port 5900

Intégrer l'ancre dans la configuration pf :

sudo nano /etc/pf.conf

Ajouter à la fin :

# Restriction VNC
anchor "vnc-restrict"
load anchor "vnc-restrict" from "/etc/pf.anchors/vnc-restrict"

Charger les règles et relancer pf :

sudo pfctl -f /etc/pf.conf
sudo pfctl -e

Depuis l'autre Mac

Installer TigerVNC viewer via Homebrew :

brew install --cask tigervnc-viewer

Ouvrir le tunnel SSH vers le Mac distant :

ssh -L 5901:localhost:5900 utilisateur-mac@adresse-ip-mac-distant

Ouvrir TigerVNC.app et lancer la session VNC via SSH :

open -a "TigerVNC" --args localhost:5901

(Si l'écran devient noir, désactiver puis réactiver le partage d'écran sur le Mac distant : "Réglages Système" → "Général" → "Partage" → "Partage d'écran", désactiver, attendre 3 secondes puis réactiver. C'est à cause d'un bug.)


Durcir le serveur

Les étapes précédentes réduisent la surface d'attaque au niveau des services macOS. Il reste trois mesures complémentaires à appliquer pour renforcer l'accès réseau.

Durcir SSH

Désactiver l'authentification par mot de passe et n'autoriser que les clés est la mesure qui a le plus d'impact. (consultez l'article "Activer et configurer le serveur SSH sur macOS Tahoe").

Installer et configurer sshguard

sshguard surveille les logs système et bannit automatiquement les adresses IP après un certain nombre de tentatives d'authentification SSH échouées. Il s'intègre nativement avec PF sur macOS.

Installer sshguard via Homebrew :

brew install sshguard

sshguard fonctionne en alimentant une table PF nommée sshguard. Il faut déclarer cette table et la règle de blocage associée dans /etc/pf.conf :

sudo nano /etc/pf.conf

Ajouter à la fin :

# sshguard
table <sshguard> persist
block in quick proto tcp from <sshguard> to any port 22

Recharger PF pour prendre en compte les nouvelles règles :

sudo pfctl -f /etc/pf.conf
sudo pfctl -e

(Les messages No ALTQ support in kernel / ALTQ related functions disabled sont normaux sur Apple Silicon : ALTQ est le sous-système de gestion de files d'attente réseau de PF, absent du kernel macOS ARM. Ils n'affectent pas le fonctionnement des règles de blocage. Le message pfctl: Use of -f option, could result in flushing of rules… est un avertissement informatif standard de macOS, sans conséquence.)

Lancer sshguard et l'activer au démarrage :

sudo brew services start sshguard

(Le warning Taking root:admin ownership of some sshguard paths est le comportement normal de Homebrew lorsqu'un service est démarré avec sudo : il prend la main sur certains chemins pour pouvoir tourner en root. L'inconvénient mentionné — suppression manuelle nécessaire avant upgrade ou reinstall — est réel mais sans conséquence au quotidien.)

Vérifier que sshguard tourne et consulter les bannissements en cours :

sudo brew services info sshguard
sudo pfctl -t sshguard -T show

(Le résultat attendu pour brew services info : Running ✔, Loaded ✔, User: root, avec un PID attribué. La commande pfctl -t sshguard -T show n'affiche rien si la table est vide — c'est le comportement normal en l'absence de tentatives d'intrusion. Elle aurait retourné une erreur si la table était absente.)

Désactiver mDNS advertising

Par défaut, macOS annonce ses services (partage d'écran, SSH, etc.) sur le réseau local via Bonjour. Ces annonces signalent la présence du serveur à n'importe quel outil de découverte réseau. On peut les désactiver sans affecter la connectivité :

sudo defaults write /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements -bool YES

Vérifier :

sudo defaults read /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements

(Doit afficher 1. Un reboot est nécessaire pour que la modification soit prise en compte.)

Annuler si nécessaire :

sudo defaults delete /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements

Durcir PF avec une politique de refus par défaut

Par défaut, PF sur macOS adopte une politique implicite pass all : tout le trafic est autorisé sauf ce qui est explicitement bloqué. L'approche inverse — tout bloquer, puis n'autoriser que ce qui est nécessaire — est plus sûre. On la met en place via une ancre dédiée, car macOS écarte silencieusement les règles block et pass placées directement dans le ruleset principal.

Créer le fichier d'ancre :

sudo nano /etc/pf.anchors/base-policy

Coller le contenu suivant :

block all
pass on lo0
pass in proto tcp to any port 22
pass out all

(Remplacer 22 par le port SSH configuré si celui-ci a été modifié. pass on lo0 autorise le loopback — nécessaire pour le tunnel VNC et les communications locales. pass out all autorise le trafic sortant initié par le serveur lui-même, sans quoi il ne pourrait plus rien initier.)

Déclarer l'ancre dans /etc/pf.conf, après le bloc com.apple et avant les ancres VNC et sshguard :

sudo nano /etc/pf.conf

Le fichier doit ressembler à ceci :

#
# Default PF configuration file.
#
# This file contains the main ruleset, which gets automatically loaded
# at startup. PF will not be automatically enabled, however. Instead,
# each component which utilizes PF is responsible for enabling and disabling
# PF via -E and -X as documented in pfctl(8). That will ensure that PF
# is disabled only when the last enable reference is released.
#
# Care must be taken to ensure that the main ruleset does not get flushed,
# as the nested anchors rely on the anchor point defined here. In addition,
# to the anchors loaded by this file, some system services would dynamically 
# insert anchors into the main ruleset. These anchors will be added only when
# the system service is used and would removed on termination of the service.
#
# See pf.conf(5) for syntax.
#

#
# com.apple anchor point
# 
scrub-anchor "com.apple/*"
nat-anchor "com.apple/*"
rdr-anchor "com.apple/*"
dummynet-anchor "com.apple/*"
anchor "com.apple/*"
load anchor "com.apple" from "/etc/pf.anchors/com.apple"

# Politique de base
anchor "base-policy"
load anchor "base-policy" from "/etc/pf.anchors/base-policy"

# Restriction VNC
anchor "vnc-restrict"
load anchor "vnc-restrict" from "/etc/pf.anchors/vnc-restrict"

# sshguard
table <sshguard> persist
block drop in quick proto tcp from <sshguard> to any port 22

(L'ordre est impératif dans PF : options → normalisation → translation → filtrage. Les directives scrub-anchor et nat-anchor de com.apple doivent rester en premier. Les ancres de filtrage (base-policy, vnc-restrict) et les règles (sshguard) viennent après le bloc com.apple.)

Recharger PF :

sudo pfctl -f /etc/pf.conf

(Le rechargement des règles interrompt brièvement les connexions SSH existantes — c'est normal. Il est possible d'être éjecté de la session en cours. Il suffit de se reconnecter : si SSH répond, la règle pass in proto tcp to any port 22 est bien active.)

Vérifier que l'ancre est bien chargée :

sudo pfctl -sr
sudo pfctl -a base-policy -sr

(La première commande doit afficher l'ancre base-policy dans la liste. La seconde affiche le contenu de l'ancre : on doit y voir les quatre règles block all, pass on lo0, pass in proto tcp et pass out all.)

Vérifier que le blocage est effectif — depuis la machine cliente, tenter de joindre un port fermé :

nc -zv <ip-du-serveur> 80

(Doit expirer sans réponse — et non retourner Connection refused. Un refus signifie que le système répond lui-même faute de service sur ce port ; un timeout signifie que PF a silencieusement absorbé le paquet.)



↑ Haut de page