VPN souverain : monter son propre serveur avec Headscale en Suisse

VPN souverain : monter son propre serveur avec Headscale en Suisse

·15 min de lecture·Mis à jour le 5 mars 2026

Pourquoi auto-héberger son VPN

La première partie couvrait les solutions rapides pour contourner la censure : DNS alternatif, VPN commercial, Tor. Elles fonctionnent, mais dépendent toutes d'un tiers — et les IP des VPN commerciaux sont connues et listées, donc bloquables une par une.

La solution souveraine : ton propre serveur VPN, dans un pays dont tu choisis la juridiction. Le bloquer suppose de connaître ton IP spécifique, ce qui est nettement plus compliqué que de bloquer les plages d'adresses de Proton ou NordVPN.

Pourquoi Headscale et pas WireGuard brut

WireGuard est le protocole VPN le plus performant qui existe, mais le configurer à la main sur chaque appareil (clés, peers) devient pénible au-delà de deux machines. Tailscale résout ça avec une couche de coordination (clés automatiques, NAT traversal, découverte des peers) — au prix de métadonnées qui transitent par leur cloud.

Headscale est une implémentation open-source et auto-hébergée de ce serveur de coordination. Les appareils utilisent le client Tailscale officiel ; seul le serveur change.

WireGuard brutTailscaleHeadscale
ProtocoleWireGuardWireGuardWireGuard
Gestion des clésManuelleAutomatiqueAutomatique
NAT traversalNonOui (STUN/DERP)Oui (STUN/DERP)
Serveur de coordinationAucunTailscale cloudAuto-hébergé
CoûtGratuitGratuit (3 users) / payantGratuit
SouverainetéTotaleLimitéeTotale

Pourquoi la Suisse

Hors UE, hors 14 Eyes, législation sur la protection des données (nLPD) parmi les plus strictes au monde, et ~10 ms de latence depuis la France ou l'Allemagne. Pour l'hébergement, Infomaniak : datacenters à Genève, 100 % énergie renouvelable, droit suisse exclusivement. Un VPS Cloud à ~5 €/mois suffit.

Prérequis

  • Un VPS sous Debian 12 ou 13 (1 vCPU, 1 Go RAM suffisent)
  • Un nom de domaine (on utilisera hs.example.io pour Headscale)
  • Un accès SSH au VPS
  • Les ports 80, 443 (TCP) et 3478 (UDP) ouverts dans le pare-feu du VPS
  • nginx (installé à l'étape 4bis pour le routage SNI sur le port 443)

Étape 1 : installer Headscale

# Ajouter le dépôt Headscale
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.headscale.net/stable/debian/pubkey.asc | sudo gpg --dearmor -o /etc/apt/keyrings/headscale.gpg
echo "deb [signed-by=/etc/apt/keyrings/headscale.gpg] https://pkgs.headscale.net/stable/debian bookworm main" | sudo tee /etc/apt/sources.list.d/headscale.list

# Installer
sudo apt update
sudo apt install -y headscale

headscale version
# headscale version v0.28.0

Étape 2 : DNS

Un enregistrement A pointant vers l'IP du VPS :

hs.example.ioA203.0.113.x

Headscale l'utilise pour son API de coordination, son certificat Let's Encrypt et son serveur DERP relay intégré.

Étape 3 : configurer Headscale

Le fichier principal est /etc/headscale/config.yaml :

# URL publique du serveur — ce que les clients utilisent pour se connecter
# Les clients se connectent via nginx sur le port 443
server_url: https://hs.example.io:443
# Headscale écoute en local — nginx route le trafic vers lui via SNI
listen_addr: 127.0.0.1:4443
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false

# Clé privée Noise (générée automatiquement au premier lancement)
noise:
  private_key_path: /var/lib/headscale/noise_private.key

# Plages d'adresses du réseau Tailscale
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
  allocation: sequential

# Serveur DERP intégré — relais pour les connexions
# qui ne peuvent pas établir de lien direct
derp:
  server:
    enabled: true
    region_id: 999
    region_code: 'myrelay'
    region_name: 'My DERP CH'
    verify_clients: true
    stun_listen_addr: '0.0.0.0:3478'
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true
    # IMPORTANT : mets l'IP publique de ton VPS ici
    ipv4: 203.0.113.x
  urls:
    # On garde les DERP publics de Tailscale en fallback
    - https://controlplane.tailscale.com/derpmap/default
  paths: []
  auto_update_enabled: true
  update_frequency: 3h

disable_check_updates: true
ephemeral_node_inactivity_timeout: 30m

# Base de données locale SQLite
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite
    write_ahead_log: true

# Certificat TLS automatique via Let's Encrypt
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: contact@example.io
tls_letsencrypt_hostname: hs.example.io
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ':http'

log:
  level: info
  format: text

# Politique d'accès (ACL)
policy:
  mode: file
  path: /etc/headscale/acl.json

# DNS interne du réseau Tailscale
dns:
  magic_dns: true
  base_domain: tail.example.io
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 1.0.0.1

unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: '0770'

# Pas d'envoi de télémétrie à Tailscale
logtail:
  enabled: false

randomize_client_port: false

Points importants :

  • listen_addr: 127.0.0.1:4443 : Headscale écoute en local uniquement. C'est nginx qui expose le 443 et route via SNI, ce qui permet de partager le port avec Xray.
  • tls_letsencrypt_challenge_type: HTTP-01 : le challenge utilise le port 80, Headscale écoute sur :http pour ça.
  • derp.server.ipv4 : l'IP publique réelle du VPS. Sans elle, les clients ne trouvent pas le relais DERP.
  • logtail.enabled: false : aucune télémétrie envoyée aux serveurs Tailscale.

Étape 4 : configurer les ACL

Dans /etc/headscale/acl.json :

{
  "tagOwners": {
    "tag:exit": ["mon-user@"]
  },
  "autoApprovers": {
    "exitNode": ["tag:exit"]
  },
  "acls": [
    {
      "action": "accept",
      "src": ["*"],
      "dst": ["*:*"]
    }
  ]
}

autoApprovers.exitNode permet aux nœuds tagués tag:exit de s'annoncer automatiquement comme exit nodes, au lieu d'exiger une approbation manuelle.

Étape 4bis : nginx SNI routing sur le port 443

Pour faire cohabiter Headscale et Xray sur le même port 443, on utilise le module stream de nginx pour router le TCP selon le SNI (le nom de domaine envoyé en clair dans le handshake TLS).

sudo apt install -y nginx libnginx-mod-stream
sudo tee /etc/nginx/modules-enabled/90-stream-sni.conf << 'EOF'
stream {
    map $ssl_preread_server_name $backend {
        hs.example.io   headscale;
        default         xray;
    }

    upstream headscale {
        server 127.0.0.1:4443;
    }

    upstream xray {
        server 127.0.0.1:8443;
    }

    server {
        listen 443;
        listen [::]:443;
        proxy_pass $backend;
        ssl_preread on;
        proxy_protocol off;
    }
}
EOF

sudo nginx -t && sudo systemctl restart nginx

Important : nginx ne fait aucun déchiffrement TLS. C'est un proxy TCP pur — Headscale gère son TLS (Let's Encrypt), Xray le sien (Reality). nginx ne lit que le SNI en clair dans le ClientHello.

Étape 5 : activer l'IP forwarding

Pour que le VPS route le trafic des autres appareils (exit node) :

cat << EOF | sudo tee /etc/sysctl.d/99-tailscale.conf
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF

sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

Étape 6 : créer un utilisateur et démarrer

sudo systemctl enable --now headscale
sudo headscale users create mon-user
sudo systemctl status headscale

Au premier démarrage, Headscale génère ses clés et obtient le certificat Let's Encrypt. Vérifie que https://hs.example.io répond.

Étape 7 : connecter le VPS comme exit node

# Installer le client Tailscale
curl -fsSL https://tailscale.com/install.sh | sh

# Trouver l'ID numérique de ton utilisateur
sudo headscale users list
# → ID: 1, Name: mon-user

# Générer une clé d'authentification avec le tag exit
# Le flag --user prend l'ID numérique (pas le nom)
sudo headscale preauthkeys create \
  --user 1 \
  --reusable \
  --expiration 24h \
  --tags tag:exit

# Connecter le VPS à son propre Headscale
sudo tailscale up \
  --login-server https://hs.example.io:443 \
  --authkey MA_CLE_PREAUTHKEY \
  --hostname vps-ch \
  --advertise-exit-node

sudo tailscale status

Le VPS est désormais à la fois serveur de coordination Headscale, nœud du réseau, et exit node.

Étape 8 : connecter ses appareils

Linux

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://hs.example.io:443

# Tailscale affiche une URL d'enregistrement — enregistrer le nœud côté serveur :
# --user prend l'ID numérique (pas le nom)
sudo headscale nodes register --key nodekey:abc123... --user 1

Windows

Télécharge Tailscale depuis tailscale.com/download/windows, puis dans un terminal administrateur :

tailscale logout   # se déconnecter du cloud Tailscale si nécessaire
tailscale up --login-server https://hs.example.io:443

Copie l'URL affichée et enregistre le nœud côté serveur.

Android

Installe Tailscale depuis le Play Store, menu ⋮ → Use an alternate serverhttps://hs.example.io:443. L'app ouvre une URL d'enregistrement, à valider côté serveur avec headscale nodes register.

Vérification

sudo headscale nodes list

Tous les appareils doivent apparaître avec leur IP Tailscale (100.64.0.x) et le statut online.

Étape 9 : activer le routage via la Suisse

# Linux
sudo tailscale set --exit-node=vps-ch --exit-node-allow-lan-access

# Windows (PowerShell) — ou via l'interface : menu → Exit Node → vps-ch
tailscale set --exit-node=vps-ch --exit-node-allow-lan-access

Sur Android : menu ⋮ → Use exit nodevps-ch.

--exit-node-allow-lan-access conserve l'accès au réseau local (imprimante, NAS) pendant que le trafic internet passe par le VPS. On utilise tailscale set plutôt que tailscale up pour modifier un paramètre sans réinitialiser les autres (comme --login-server).

Vérification depuis chaque appareil : curl -s https://ipinfo.io doit renvoyer l'IP du VPS suisse.

Usage quotidien : tout le trafic est chiffré par WireGuard, transite par le VPS suisse et sort avec l'IP suisse. Le FAI local ne voit qu'une connexion WireGuard vers une IP unique.

Option anti-DPI : VLESS+Reality (Xray)

WireGuard a un défaut : son trafic est identifiable par Deep Packet Inspection (handshake, taille des paquets, format UDP). En Russie, en Chine, en Iran, WireGuard brut est bloqué — Headscale/Tailscale n'y fonctionnera pas.

La solution : un proxy VLESS+Reality (Xray) sur le même VPS. Contrairement aux VPN obfusqués qui masquent le trafic, VLESS+Reality le fait ressembler à une connexion HTTPS légitime vers un vrai site (par exemple www.microsoft.com). Le DPI ne peut pas le bloquer sans bloquer le site imité.

Installer et configurer Xray sur le VPS

sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install

# Clés Reality
xray x25519
# → Private key: eE6MDfDF1JliKiDijcPojrOJB4-GsA_ux7InREW7hEg
# → Public key:  _kP9S_vKqSksfj9MXNn0pULtphbzRVuNq5DYYafYpz8

xray uuid
# → 8672f031-7aaa-4a63-8835-6cd7f58ea703

openssl rand -hex 8   # short ID
# → e318e30924f77899

Dans /usr/local/etc/xray/config.json :

{
  "log": {
    "loglevel": "warning",
    "access": "/var/log/xray/access.log",
    "error": "/var/log/xray/error.log"
  },
  "inbounds": [
    {
      "listen": "127.0.0.1",
      "port": 8443,
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "8672f031-7aaa-4a63-8835-6cd7f58ea703",
            "flow": "xtls-rprx-vision"
          }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "dest": "www.microsoft.com:443",
          "serverNames": ["www.microsoft.com", "microsoft.com"],
          "privateKey": "eE6MDfDF1JliKiDijcPojrOJB4-GsA_ux7InREW7hEg",
          "shortIds": ["e318e30924f77899"]
        }
      },
      "sniffing": {
        "enabled": true,
        "destOverride": ["http", "tls", "quic"]
      }
    }
  ],
  "outbounds": [
    { "protocol": "freedom", "tag": "direct" },
    { "protocol": "blackhole", "tag": "block" }
  ]
}

Points clés :

  • listen: 127.0.0.1, port: 8443 : Xray écoute en local, nginx route le 443 vers lui.
  • dest: www.microsoft.com:443 : le site imité. Un client non-VLESS qui se connecte est redirigé vers le vrai microsoft.com — le serveur est indistinguable d'un proxy légitime.
  • flow: xtls-rprx-vision : le mode XTLS Vision, le plus performant.
sudo mkdir -p /var/log/xray
sudo systemctl enable --now xray

Grâce au routage SNI de l'étape 4bis, le port 443 dispatche :

Internetnginx (:443)
  ├── SNI: hs.example.ioHeadscale (:4443) → clients Tailscale
  └── SNI: * (default)Xray (:8443)      → clients VLESS+Reality

Pour un observateur, tout le trafic sur le 443 ressemble à du HTTPS normal.

Configurer les clients

Le plus simple est un lien VLESS, importable dans v2rayN (Windows), v2rayNG (Android) et Streisand (iOS) — par import presse-papier ou scan de QR code :

vless://UUID@IP_VPS:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.microsoft.com&fp=chrome&pbk=CLE_PUBLIQUE&sid=SHORT_ID&type=tcp#Mon-VPS-Reality

Sur Linux, on installe Xray en client avec /usr/local/etc/xray/config.json :

{
  "log": { "loglevel": "warning" },
  "inbounds": [
    {
      "listen": "127.0.0.1",
      "port": 10818,
      "protocol": "socks",
      "settings": { "udp": true },
      "tag": "socks-in"
    },
    {
      "listen": "127.0.0.1",
      "port": 10819,
      "protocol": "http",
      "tag": "http-in"
    }
  ],
  "outbounds": [
    {
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "203.0.113.42",
            "port": 443,
            "users": [
              {
                "id": "8672f031-7aaa-4a63-8835-6cd7f58ea703",
                "flow": "xtls-rprx-vision",
                "encryption": "none"
              }
            ]
          }
        ]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "serverName": "www.microsoft.com",
          "publicKey": "_kP9S_vKqSksfj9MXNn0pULtphbzRVuNq5DYYafYpz8",
          "shortId": "e318e30924f77899",
          "fingerprint": "chrome"
        }
      },
      "tag": "vless-out"
    },
    { "protocol": "freedom", "tag": "direct" }
  ]
}

Le client expose un proxy SOCKS5 local (127.0.0.1:10818) et HTTP (127.0.0.1:10819) :

curl -x socks5h://127.0.0.1:10818 https://ifconfig.me
# → doit afficher l'IP de ton VPS

Conflit avec Tailscale : ne lance pas v2rayN et Tailscale (exit node) en même temps — les deux veulent router tout le trafic et créent une boucle. Tailscale au quotidien, v2rayN uniquement dans les pays censurés, sans démarrage automatique.

Quand utiliser quoi

SituationSolution
Usage quotidien (France, Allemagne, etc.)Tailscale via Headscale — exit node VPS, réseau mesh
Voyage en Russie, Chine, IranVLESS+Reality via v2rayN/v2rayNG — anti-DPI
Les deux sont disponiblesTailscale par défaut, VLESS en backup

Tailscale est plus performant (WireGuard natif, connexions directes entre peers). VLESS+Reality est l'arme anti-censure, à activer uniquement quand Tailscale est bloqué.

Vérifier que tout est étanche

Un VPN mal configuré peut fuiter ton IP réelle. Ne fais jamais confiance à un seul service — croise au moins trois sources indépendantes :

echo "=== ipinfo.io ==="
curl -s https://ipinfo.io/json | jq '{ip, city, country, org}'

echo "=== ipleak.net ==="   # teste IPv6 en priorité
curl -s https://ipleak.net/json/ | jq '{ip, country_name, isp_name}'

echo "=== mullvad ==="
curl -s https://am.i.mullvad.net/json | jq '{ip, country, organization}'

Les trois doivent retourner la même IP — celle du VPS suisse. Diagnostic selon ce qui apparaît :

  • Ton FAI local en IPv4 → l'exit node n'est pas actif (tailscale set --exit-node=vps-ch).
  • Une adresse IPv6 de ton FAI (typiquement sur ipleak.net) → fuite IPv6, la plus courante et la plus discrète. Le trafic IPv6 contourne le tunnel.
  • Le résolveur DNS de ton FAI → forcer dns.override_local_dns: true dans la config Headscale (MagicDNS le fait normalement).

Correction d'une fuite IPv6 :

# Identifier l'interface réseau principale
ip route show default
# → default via 192.168.x.x dev eth0 ...

cat << EOF | sudo tee /etc/sysctl.d/99-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.eth0.disable_ipv6 = 1
EOF
sudo sysctl -p /etc/sysctl.d/99-disable-ipv6.conf

Côté VLESS+Reality, vérifier que Xray tourne et que le tunnel sort bien par le VPS :

sudo systemctl status xray
sudo tail -f /var/log/xray/access.log       # les connexions réussies apparaissent ici

curl -x socks5h://127.0.0.1:10818 https://ifconfig.me   # depuis un client

Pour confirmer que le trafic ressemble à du HTTPS normal, une capture montre du TLS 1.3 classique — pas de handshake WireGuard, pas de signature UDP :

sudo tcpdump -i eth0 -c 20 host 203.0.113.42 and port 443 -w /tmp/capture.pcap
tcpdump -r /tmp/capture.pcap -v | head -30

Sécurisation

Pare-feu restreint au strict nécessaire :

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # Let's Encrypt challenge
sudo ufw allow 443/tcp   # nginx → Headscale + Xray (SNI routing)
sudo ufw allow 3478/udp  # STUN (NAT traversal)
sudo ufw allow 41641/udp # WireGuard direct connections
sudo ufw enable

SSH par clés uniquement, dans /etc/ssh/sshd_config :

PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin no

Et les mises à jour automatiques : sudo apt install -y unattended-upgrades && sudo dpkg-reconfigure -plow unattended-upgrades.

VPN commercial vs auto-hébergé : le verdict

ComposantCoût mensuel
VPS Infomaniak (1 vCPU, 1 Go)~5€
Nom de domaine~1€/mois (amorti)
Headscale / Tailscale / LEGratuit
Total~6€/mois

Soit le prix d'un abonnement VPN commercial, mais avec le contrôle du serveur, des logs, de la juridiction et des règles d'accès.

CritèreVPN commercialHeadscale + Xray auto-hébergé
Mise en place2 minutes2-3 heures
MaintenanceAucuneMises à jour à faire
Nombre de serveursDizaines de pays1 (ton VPS)
ConfianceTu fais confiance au providerTu fais confiance à toi-même
Résistance au blocageFaible (IP connues)Forte (IP unique + VLESS+Reality anti-DPI)
SouverainetéAucuneTotale
Coût5-10€/mois~6€/mois
Multi-appareilsLimité selon le planIllimité
Anti-censure (DPI)Variable (Stealth, etc.)VLESS+Reality — éprouvé Russie/Chine

L'auto-hébergement n'est pas pour tout le monde. Pour juste débloquer YouTube en déplacement, un VPN commercial suffit. Mais pour une infrastructure réseau que personne ne peut te couper et qui résiste au DPI le plus agressif, Headscale + Xray sur un VPS suisse est la réponse.


Cet article fait partie d'une série en deux parties. La première partie couvre les solutions immédiates (DNS, VPN commercial, Tor, VLESS+Reality). Cette deuxième partie couvre l'approche souveraine avec Headscale et Xray.

Les techniques présentées ici visent à préserver l'accès à l'information, un droit fondamental reconnu par l'Article 19 de la Déclaration universelle des droits de l'homme. Utilisez-les de manière responsable et en connaissance des lois locales.

PartagerLinkedInXBluesky

Articles similaires