Mise en place d’un serveur OpenVPN

Description succinte de la mise en place d’un VPN avec OpenVPN, de la gestion des différents certificats aux spécifités d’un setup où le serveur VPN est derrière un NAT et une IP dynamique en passant par la gestion des utilisateurs…

Prérequis

  • packages: openvpn, openssl;
  • Une autorité de certification (un root CA, un certificat serveur et au moins un certificat client);
  • une IP fixe ou au moins un hostname chez dyndns ou autre avec un moyen simple de mettre à jour l’IP;
  • un subnet différent du LAN pour le VPN (mettons: 10.11.12.0/24 pour le VPN et 192.168.1.0/24 pour le LAN);
  • iptables & Co.

Étape 1: les certificats/clés/…

Autorité de certification1

Méthode rapide et pas détaillée pour créer une autorité de certification maison et pas officielle (mais utilisable):

Fichier de configuration openssl:

Tout ce qui concernera l’autorité pourra être stocké (par exemple) dans /home/ssl/. On y stockera la configuration de OpenSSL dans un fichier openssl.conf:

HOME        = .
RANDFILE    = $ENV::HOME/.rnd
oid_section = new_oids

[ new_oids ]
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7

[ ca ]
default_ca = CA_default

[ CA_default ]
dir              = /home/ssl           # {dossier} Stockage de tout ce qui concerne l'autorité
certs            = $dir/certs          # {dossier}
crl_dir          = $dir/crl            # {dossier}
database         = $dir/index.txt      # {fichier} intiliasé à 01
new_certs_dir    = $dir/newcerts       # {dossier}
certificate      = $dir/ca/root.crt    # {fichier} clé publique de l'autorité (mode: 0644)
serial           = $dir/serial         # {fichier} intiliasé à 01
crlnumber        = $dir/crlnumber      # {fichier} intiliasé à 01
crl              = $dir/crl.pem        # {fichier} Liste des certificats révoqués
private_key      = $dir/ca/root.key    # {fichier} clé privée de l'autorité (mode: 0600)
RANDFILE         = $dir/private/.rand  # {fichier} 
name_opt         = ca_default          # section dans le fichier openssl.conf
cert_opt         = ca_default          # idem
default_days     = 365                 # durée de validité des certificats par défaut (en jours)
default_crl_days = 30                  # durée de validité de la CRL en jours
default_md       = sha256              # digest par défaut
preserve         = no 
policy           = policy_anything

[ policy_match ]
countryName            = match
stateOrProvinceName    = match
organizationName       = match
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ policy_anything ]
countryName            = optional
stateOrProvinceName    = optional
localityName           = optional
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ req ]
default_bits       = 2048
default_keyfile    = privkey.pem
distinguished_name = req_distinguished_name
attributes         = req_attributes
x509_extensions    = v3_ca
string_mask        = utf8only

[ req_distinguished_name ]
countryName                    = Country Name (2 letter code)
countryName_default            = FR
countryName_min                = 2
countryName_max                = 2
stateOrProvinceName            = State or Province Name (full name)
stateOrProvinceName_default    = Mon département ou ma région
localityName                   = Locality Name (eg, city)
localityName_default           = Ma ville
0.organizationName             = Organization Name (eg, company)
0.organizationName_default     = Mon entité juridique (moi, association, société, ...)
organizationalUnitName         = Organizational Unit Name (eg, section)
organizationalUnitName_default = Service de l'entité qui gère l'autorité de certification
commonName                     = Common Name (e.g. server FQDN or YOUR name)
commonName_max                 = 64
emailAddress                   = Email Address
emailAddress_max               = 64
emailAddress_default           = adresse email du service qui gère l'autorité de cert.

[ req_attributes ]
challengePassword     = A challenge password
challengePassword_min = 4
challengePassword_max = 20
unstructuredName      = Mon entité juridique

[ ServerCert ]
basicConstraints       = CA:FALSE
nsCertType             = server
keyUsage               = digitalSignature,keyEncipherment,keyAgreement
extendedKeyUsage       = serverAuth
nsComment              = "Server Certificate for mon_entité_juridique"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer

[ ClientCert ]
basicConstraints       = CA:FALSE
nsCertType             = client
keyUsage               = digitalSignature,keyAgreement
extendedKeyUsage       = clientAuth
nsComment              = "Client Certificate for mon_entité_juridique"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

[ v3_ca ]
basicConstraints = CA:TRUE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer

[ crl_ext ]
authorityKeyIdentifier = keyid:always

# on ne se servira pas de tout ça:
[ proxy_cert_ext ]
basicConstraints       = CA:FALSE
nsComment              = "Proxy Certificate for mon_entité_juridique"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
proxyCertInfo          = critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo

[ tsa ]
default_tsa = tsa_config1

[ tsa_config1 ]
dir                    = ./demoCA
serial                 = $dir/tsaserial
crypto_device          = builtin
signer_cert            = $dir/tsacert.pem
certs                  = $dir/cacert.pem
signer_key             = $dir/private/tsakey.pem
default_policy         = tsa_policy1
other_policies         = tsa_policy2, tsa_policy3
digests                = md5, sha1
accuracy               = secs:1, millisecs:500, microsecs:100
clock_precision_digits = 0
ordering               = yes
tsa_name               = yes
ess_cert_id_chain      = no

GéNÉRATION DE La clé privée:

openssl genrsa -des3 -out /home/ssl/ca/root.key 2048

NB:

  • On note bien la passphrase que l’on a renseignée, on en aura besoin à chaque fois que l’on voudra générer un certificat avec cette autorité.
  • La clé privée doit-être accessible uniquement à root!

génération de la clé publique de l’autoritée:

openssl req -new -sha256 -x509 -days validité2 -key /home/ssl/ca/root.key -out /home/ssl/ca/root.crt -config /home/ssl/openssl.cfg

2La durée de validité du certificat s’exprime en jours à partir du moment où l’on génère le certificat.

Il faut répondre à quelques questions:

  • Organisation Name:  nom/prénom ou raison sociale
  • Second Organisation Name: rien
  • Organizational Unit Name: nom du service qui gère l’autorité
  • Common Name (e.g., YOUR name): Autorite de Certification de …
  • Email Address: une adresse email valide et si possible générique (du type: service+ssl@exemple.fr)

Certificat serveur1

On crée une clé privée et une demande de certificat (CSR – Certificate Signing Request):

openssl req -nodes -new -outform PEM -keyform PEM -newkey rsa:2048 -sha256 -days validité2 -out SERVEUR.csr -keyout SERVEUR.key

Il faut surtout bien répondre à la question “Common Name (e.g. YOUR name):” avec le nom complet du serveur VPN (par exemple: vpn.exemple.net). Idéalement, on répond aux autres questions avec les mêmes valeurs que dans le fichier de configuration. Si on fait tout sur la machine qui sert à l’autorité de certification, on peut rajouter une option -config /home/ssl/openssl.conf afin que les réponses soient préremplies.

Maintenant, on crée la clé publique du serveur en signant le CSR:

openssl ca -config /home/ssl/openssl.conf -extensions ServerCert -days validité2 -out SERVEUR.crt -infiles SERVEUR.csr

NB:

  • On a besoin de la passphrase de la clé privée de l’autorité de certification;
  • Le CSR peut se générer sur n’importe quelle machine, avec n’importe quel utilisateur;
  • L’autorité n’a besoin que du CSR et de sa clé privée pour générer le certificat serveur ;
  • Pour une autorité, il vaut mieux une grande durée de validité (genre 10ans) ;
  • OpenSSL demande une passphrase pour la clé privée: elle est facultative. Si on en met quand même une, il faudra la saisir chaque fois qu’on démarre le service qui l’utilise.

Certificat(s) client(s)1:

Les recommandations sont les mêmes que pour le certificat serveur. Les étapes sont similaires.

Clé privée + CSR:

openssl req -nodes -new -outform PEM -keyform PEM -newkey rsa:2048 -sha256 -days validité2 -out CLIENT.csr -keyout CLIENT.key

Ici, pour “Common Name (e.g. YOUR name):”, il faut renseigner le nom de l’utilisateur, ou un nom générique si on veut n’utiliser qu’un seul certificat client pour le VPN et utiliser ensuite des user/pasword.

Clé publique/CRT:

openssl ca -config /home/ssl/openssl.cnf -extensions ClientCert -days validité2 -out CLIENT.crt -infiles CLIENT.csr

On note bien la différence lors de la signature: -extensions …. Cette différence permet de définir l’usage qui sera fait du certificat.

 

1Il est peut-être possible de s’en sortir avec des certificats auto-signés, mais je n’ai pas testé. Un certificat auto-signé se fait comme ça:

openssl req -nodes -x509 -newkey rsa:2048 -sha256 -keyout MonCrtAutoS.key -out MonCrtAutoS.crt -days DureeValiditeeEnJours

Clé secrète openvpn:

C’est spécifique à openvpn. Ça sert à avoir  une clé secrète et partagée entre le client et le serveur pour sécuriser l’établissement de la communication et se protéger de certains type DoS ou d’attaque MITM (Man In The Middle).

La clé se génère comme ça:

openvpn —genkey —secret /etc/openvpn/ssl/secret.key

Clé Diffie-Hellman

Cette clé permet au client et au serveur de négocier un nombre qui sera utilisé ensuite comme clé pour chiffrer les échanges entre le client et le serveur. C’est une protection supplémentaire contre les attaques MITM.

Ça se génère comme ça:

openssl dhparam -outform PEM -out /etc/openvpn/ssl/dh2048.pem -2 2048

Étape 2: la configuration du serveur OpenVPN

On va pouvoir commencer à utiliser tout ce que l’on vient de faire…

D’abord, on s’assure de l’existence des dossiers /etc/openvpn/{ssl,ssl/ca,ssl/crt,ipp,ccd} et du fichier /etc/openvpn/ipp/monvpn.txt (créé avec un bête touch(1))

/etc/openvpn/monvpn.conf

#local X.Y.Z.T, si la machine a plusieurs interfaces réseau on spécifie l'adress d'écoute
port 1194
proto udp
dev tun
ca /etc/openvpn/ssl/ca/root.crt # mode: 0644, root:root
cert /etc/openvpn/ssl/crt/SERVEUR.crt # mode: 0644, root:root
key /etc/openvpn/ssl/crt/SERVEUR.key # mode: 0600, root:root
tls-auth /etc/openvpn/ssl/secret.key 0 # mode: 0600, root:root
dh /etc/openvpn/ssl/dh2048.pem # mode: 0644, root:root
key-method 2
## VPN subnet:
## le serveur s'attribuera l'IP 10.11.12.1
server 10.11.12.0 255.255.255.0
ifconfig-pool-persist /etc/openvpn/ipp/monvpn.txt
client-config-dir /etc/openvpn/ccd
push "route 10.11.12.0 255.255.255.0"
# ces 3 lignes sont facultatives:
push "dhcp-option DOMAIN vpn.exemple.net"
push "dhcp-option DNS ipDesDNSLocaux"
push "dhcp-option NTP ipDuNTPLocal"
keepalive 10 60
script-security 2
tmp-dir /dev/shm # /tmp ou /var/tmp font aussi l'affaire
# si on veut utiliser des logins/password en plus des certificats
auth-user-pass-verify /usr/local/sbin/ucheck.sh via-file
comp-lzo
max-clients 64 # le nombre maximum de clients VPN simultanés que l'on autorise
user nobody
group nogroup
persist-key
persist-tun
## logs et verbiage:
status /var/log/openvpn/status.log
log-append  /var/log/openvpn/server.log
verb 3
mute 10

iptables minimal

iptables -A INPUT -i $WAN -p udp --dport 1194 -j ACCEPT # autorisation des connexions au VPN

si on veut que les machines accèdent à un LAN (mettons 192.168.1.0/24), il faut aussi ça:

iptables -A FORWARD -s 10.11.12.0/24 -d 192.168.1.0/24 -i $VPN -o $LAN -j ACCEPT
iptables -A FORWARD -s 192.168.1.0/24 -d 10.11.12.0/24 -i $LAN -o $VPN -j ACCEPT
  • $VPN: c’est l’interface réseau virtuelle utilisée par OpenVPN (en général: tun0)
  • $LAN: c’est l’interface réseau du LAN (par exemple: eth0)
  • $WAN: c’est l’interface réseau publique (connectée à internet, par exemple eth1)

sysctl (ip_forward)

Il faut autoriser l’IP forwarding:

sysctl -w net.ipv4.ip_forward=1

Et dans on rajoute le net.ipv4.ip_forward=1 dans le fichier /etc/sysctl.conf.

Facultatif: scripts d’authentification des clients

Ces scripts permettent de n’utiliser qu’un seul certificat client pour tous les utilisateurs du VPN. Ce certificat n’a pas besoin de passphrase (un secret de moins à lâcher dans la nature).

Ces deux scripts sont assez simples et mériteraient sûrement d’être améliorés.

La base des utilisateurs est un fichier dont le format est le suivant: une déclaration d’utilisateur par ligne (login:password). Les lignes vides sont ignorées et il n’y a pas de commentaires (pas de lignes commençant par #, //, ;, …).

Le login de l’utilisateur est encodé en base64, on ne stocke que le hash MD5 du mot de passe (lui même encodé en base64).

Par exemple, pour l’utilisateur toto (mot de passe “zéro+zéro=la tête à”), ça donne:

dG90bw==:MGFjZjE3MmFhMmRiYjc4NTI0MjRlZWY2NzlmNGI5MTcK

Pour rajouter un utilisateur, on peut faire ça comme ça:

echo "$(echo -n 'toto' | base64):$(echo -n 'zéro+zéro=la tête à' | md5sum | awk '{ print $1 }' | base64)" >> /etc/openvpn/monvpn.users

Pour le supprimer, par contre, ça va être plus emmerdant 😉

ucheck.sh

C’est juste un wrapper du script python qui fait le vrai boulot:

#!/bin/sh
exec /usr/bin/python /usr/local/sbin/ucheck.py "$@"

Le vrai script (ucheck.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys,os,syslog
from hashlib import md5
from base64 import b64encode,b64decode

UDB="/etc/openvpn/monvpn.users"

def loaddb(f):
    """Chargement en mémoire de la base des utilisateur (UDB)"""
    h = None
    db = {}
    try:
        h = open(f, 'r')
    except IOError as e:
        syslog.syslog(syslog.LOG_WARNING, "[err] %s" % e.__str__())
        return {}
    except OSError as e:
        syslog.syslog(syslog.LOG_WARNING, "[err] %s" % e.__str__())
        return {}
    for l in h.readlines():
        if not l.strip():
            continue
        d = l.strip().split(':')
        db[d[0].strip()] = d[1].strip()
    h.close()
    return db
# loaddb()

def getauth(f):
    """Récupération du login et du password tranmis par OpenVPN au script"""
    h = None
    try:
        h = open(f, 'r')
    except IOError:
        return ()
    except OSError:
        return ()
    t = h.readlines()
    h.close()
    u = b64encode(t[0].strip())
    p = b64encode( md5( t[1].strip() ).hexdigest() )
    return (u, p)
# getauth()

def acheck(u, p):
    """Vérification du login/password soumis par l'utilisateur à OpenVPN"""
    db = loaddb(UDB)
    if not db:
        syslog.syslog(syslog.LOG_WARNING, "[err] no user database in '%s'" % UDB)
        return 1 # auth == failure
    if not db.has_key(u):
        syslog.syslog(syslog.LOG_WARNING, "[err] unknown user '%s'" % b64decode(u))
        return 1 # auth == failure
    if db[u] != p:
        syslog.syslog(syslog.LOG_WARNING, "[err] wrong password for '%s'" % b64decode(u))
        return 1 # auth == failure
    return 0 # auth == ok
# acheck()

def main(argv):
    syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_AUTH)
    a = getauth(argv[0])
    if not a:
        syslog.syslog(syslog.LOG_WARNING, "[err] no auth data from openvpn!")
        return 1 # auth == failure
    return acheck(a[0], a[1])
# main()

if __name__ == "__main__":
    sys.exit( main(sys.argv[1:]) )

Étape 3: la configuration du client OpenVPN

Fichier de configuration du client OpenVPN (extension habituelle: .ovpn):

Les lignes commençant par ; sont des commentaires. Les commentaires ne peuvent pas être sur la même ligne que leur objet.

client
dev tun
proto udp
remote vpn.exemple.net 1194
; requis si le serveur OpenVPN a une IP dynamique et s'il se trouve derrière un NAT
float
resolv-retry infinite
nobind
persist-key
persist-tun
mute-replay-warnings
ca root.crt
cert CLIENT.crt
key CLIENT.key
; vérifie que le certificat du serveur OpenVPN a bien l'attribut serveur (c'est le cas avec la conf openssl)
ns-cert-type server
tls-auth secret.key 1
; vérifie que le certificat du serveur est bien celui de vpn.exemple.net
verify-x509-name vpn.exemple.net name
keepalive 10 60
explicit-exit-notify 2
comp-lzo
verb 2
mute 20
reneg-sec 0
; si on utilise un login/password pour authentifier les clients en plus du certificat
auth-user-pass
auth-nocache

Quoi fournir à l’utilisateur:

  • Un client OpenVPN:
    • inclu dans le package openvpn des distributions Linux ou des *BSD ;
    • TunnelBlick marche bien pour MacOS X ;
    • le client windows officiel est disponible ;
    • le Google Play Store propose un client OpenVPN gratuit qui fonctionne sans rooter le téléphone ou la tablette ;
    • il me semble qu’il faut jailbreaker les iPhones/iPad pour installer un client OpenVPN.
  • Le fichier de conf défini au point précédent ;
  • Le certificat client/utilisateur (CLIENT.crt) et sa clé privée (CLIENT.key) ;
  • La clé secrète du serveur (secret.key)
  • Le certificat de l’autorité de certification qui sert à générer et signer les certificats du serveur et des clients/utilisateurs (root.crt). Et surtout pas le root.key!

Notes spéciale: Cas où le serveur openvpn est derrière un NAT

Règle de {D,S}NAT avec netfilter/iptables:

Un truc dans ce goût là devrait faire l’affaire, en admettant que 192.168.1.254 soit l’IP du serveur OpenVPN:

iptables -t nat -A PREROUTING -i $WAN -o $LAN -p udp --dport 1194 -j DNAT --to-destination 192.168.1.254:1194

Si on a une IP fixe en sortie (X.Y.Z.T), on peut tenter un:

iptables -t nat -A POSTROUTING -i $LAN -o $WAN -p udp --sport 1194 -j SNAT --to-source X.Y.Z.T:1194

Sinon, la règle de MASQUERADING qui sert pour le partage de connexion à Internet suffira.

Modif requise dans la conf client d’OpenVPN:

Il faut être sûr que le fichier contienne une ligne avec le mot clé float (comme dans l’exemple).