Dezentrale VPNs

Neben dem klassischen Setup, in dem sich VPN Clients bei einem zentralen VPN Endpoint einwählen, existieren seit einiger Zeit ebenfalls verteilte VPN Systeme, welche eine sichere Verbindung zwischen den einzelnen Teilnehmern eines Netzwerks umsetzen. Eine bekannte Umsetzung eines Peer2Peer VPNs ist Tinc. Hierbei wird häufig ein sogenanntes Mesh-Routing verwendet. Das heißt, eine Verbindung zwischen zwei Knoten ist innerhalb eines Meshes über mehrere Routen möglich. Dies hat den großen Vorteil, dass das VPN selbst dann bestehen bleibt, wenn eine der Verbindungen ausfallen würde.

Ein typisches Mesh Netzwerk kann z.B. so aussehen:

VPN Mesh
VPN Mesh

Würde die Verbindung zwischen Node 1 und Node 3 unterbrochen werden, kann der Traffic immer noch über Node 2 oder Node 4 weitergeleitet werden.
Das Setup eines Tinc Meshes erfordert allerdings recht viel Aufwand, weil die Knoten gegenseitig ihre Zertifikate kennen müssen. Ein typisches Setup von Tinc liest sicht wie folgt:

  1. Install tinc VPN on each node.
  2. Create the VPN’s working directory on each node.
  3. Create tinc. conf and host files on every node.
  4. Create VPN control scripts for all tinc instances.
  5. Create systemd unit files.
  6. Exchange host files amongst all nodes.
  7. Enable tinc systemd unit(s).

Es können hierzu zwar auch Automatisierungslösungen wie Ansible verwendet werden, allerdings ist die Einrichtung eines Tinc basierten Meshes auch dann nicht trivial und erfordert beim Hinzufügen von neuen Knoten ein Update der anderen Nodes.

Nebula VPN

Ende letzten Jahres hat die Firma Slack ihre interne Lösung für ein verteiltes VPN, welches auch als Overlay für ihre Backend Services verwendet wird, unter dem Namen Nebula VPN als Open Source Software der Allgemeinheit zur Verfügung gestellt. Die Sofware selbst findet sich auf Github und wird dort auch weiter entwickelt.

Aber was ist Nebula nun?

Nebula is a scalable overlay networking tool with a focus on performance, simplicity and security. It lets you seamlessly connect computers anywhere in the world. Nebula is portable, and runs on Linux, OSX, and Windows.

Demnach sind die Hauptziele von Nebula Performanz, Einfachheit und Sicherheit. Es wird ausdrücklich betont, dass diese Software innerhalb von Slack auch für die Anbindung der Laptops von mobilen Mitarbeitern verwendet wird.

Arstechnica hat recht zeitnah zur Veröffentlichung ein Tutorial online gestellt, welches die groben Punkte eines Setups durchgeht.
Darauf aufbauend hier also die deutsche Version, in der ein paar einzelne Schritte noch mal detailierter beleuchtet werden:

Zunächst ein paar Details:

Setup der Basis

Das Setup gliedert sich in vier Schritte:

  1. Aufsetzen der PKI Infrastruktur
  2. einen Plan erstellen
  3. Aufsetzen von (mindestens) einem Lighthouse Knoten
  4. Setup der restlichen Knoten

Alle notwendigen Binaries können bei den Releases vom Projekt gefunden werden. Nach Entpacken des Archives erhalten wir zwei Binaries:

Aufsetzen der PKI Infrastruktur

Als allererstes gilt es ein Root Zertifikat zu erstellen. Mit dem privaten Schlüssel lassen sich dann alle weitere Zertifikate provisionieren.

./nebula-cert ca -name „ACME Nebula Mesh Network“

Wir verschieben die beiden erstellten Dateien ca.crt und ca.key in ein separates Verzeichnis certs. Dieses Root Zertifikat ist ein Jahr gültig. Bestehen gute Gründe, den Gültigkeitszeitraum zu verändern, lässt sich dies mit dem Parameter -duration machen. (z.B. ./nebula-cert ca -duration "10000h" ….

Es ist empfehlenswert den Root CA Schlüssel nur verschlüsselt abzulegen (z.B. per git-crypt).

einen Plan erstellen

Bevor wir fortfahren, sollten wir uns ein paar Gedanken über die Netzwerkkonfiguration machen. Jeder Client erhält eine (fixe) IP Adresse. In einem normalen Subnetz sind dies 254 freie Adressen. Da die IP Adresse eines VPN Nodes bei der Erstellung seines Zertifikats vergeben wird, empfiehlt es sich demnach - insbesondere bei größeren Netzen - eine Liste mit der Adressverteilung anzulegen. Diese Tabelle kann ebenfalls die Node Groups enthalten, welche eine detailiertere Zugriffskontrolle ermöglichen (dazu später mehr).

Folgende Annahmen gelten für unser Beispiel:

Wir definieren nun folgende Gruppen:

Wir können nun folgende Tabelle erstellen:

Host IP Groups
lighthouse1 192.168.100.10
service.prod 192.168.100.20
service.dev 192.168.100.30
anna 192.168.100.101 dev, support-dev, support-prod
anton 192.168.100.102 dev, support-dev

Betrachten wir das Setup in einer Netzwerk Übersicht, sieht das dann so aus:

Netzwerk Übersicht
Größere Ansicht

Aufsetzen eines Lighthouse Knoten

Das Setup vom Lighthouse Knoten unterscheidet sich wenig vom Setup eines anderen Clients. Der einzige Unterschied ist eine andere Konfiguration.

Die Konfiguration selbst wird in einer Datei namens config.yml angelegt. Bevor wir uns näher mit der Konfiguration befassen, müssen wir erst ein Zertifikat für den neuen Knoten erstellen. Für unseren lighthouse Knoten erfolgt dies mit:

./nebula-cert sign \
    -name "lighthouse1" \
    -ca-crt "certs/ca.crt" \
    -ca-key "certs/ca.key" \
    -ip "192.168.100.10/24"

Kommen wir nun zur Konfiguration. Ein Template hierfür stellt das Projekt Repository zur Verfügung.

Die Konfiguration gliedert sich in die einzelne Blöcke (pki, static_host_map, lighthouse, listen, tun, und firewall).
Der erste Bereich gilt dem PKI Setup. Neben dem Root-CA Zertifikat, dem Node Zertifikat und -Schlüssel, lassen sich hier auch Blacklist erstellen, falls einem Node Zertifikat nicht mehr vertraut werden soll.

pki:
  # The CAs that are accepted by this node. Must contain one or more certificates created by 'nebula-cert ca'
  ca: /etc/nebula/ca.crt
  cert: /etc/nebula/host.crt
  key: /etc/nebula/host.key
  #blacklist is a list of certificate fingerprints that we will refuse to talk to
  #blacklist:
  #  - c99d4e650533b92061b09918e838a5a0a6aaee21eed1d12fd937682865936c72

Im nächsten Block wird eine statische Host Map erstellt. Dies ist das initiale Mapping, welches jeder Client zum Auffinden der Peers verwendet.
Hier sollte dann das Mapping der einzelnen lighthouse Knoten eingetragen werden.
Jeder Lighthous Knoten sollte eine statische, public IP Adresse besitzen, welche hier auf seine interne IP gemappt wird.

# The syntax is:
#   "{nebula ip}": ["{routable ip/dns name}:{routable port}"]
# Example, if your lighthouse has the nebula IP of 192.168.100.1 and has the real ip address of 100.64.22.11 and runs on port 4242:
static_host_map:
  "192.168.100.10": ["100.64.22.11:4242"]

Der nächste Block ist die Konfiguration, die für einen lighthouse Node notwendig ist.

Entweder ist ein Node ein lighthouse, oder der Node erhält eine Liste aller lighthous IPs.

lighthouse:
  am_lighthouse: true
  interval: 60
  # hosts is a list of lighthouse hosts this node should report to and query from
  # IMPORTANT: THIS SHOULD BE EMPTY ON LIGHTHOUSE NODES
  hosts:

Weiterhin lassen sich die (UDP) Ports definieren, über den das Mesh aufgebaut werden soll. Dies ist ein kritisches Setting für lighthouse Nodes - da diese immer erreichbar sein sollten. Alle anderen Knoten können dies auch dynamisch zuordnen lassen.

# Port Nebula will be listening on. The default here is 4242. For a lighthouse node, the port should be defined,
# however using port 0 will dynamically assign a port and is recommended for roaming nodes.
listen:
  host: 0.0.0.0
  port: 4242

# Punchy continues to punch inbound/outbound at a regular interval to avoid expiration of firewall nat mappings
punchy: true

Es folgt ein Block, in dem sich das Netzwerk Device konfigurieren lässt.

# Configure the private interface. Note: addr is baked into the nebula certificate
tun:
  dev: lighthouse1
  drop_local_broadcast: false
  drop_multicast: false
  tx_queue: 500
  mtu: 1300
  routes:
  unsafe_routes:

Ebenso das Logging:

logging:
  # panic, fatal, error, warning, info, or debug. Default is info
  level: info
  # json or text formats currently available. Default is text
  format: text

Am Ende gibt es noch einen Block firewall.
Über diese Konfiguration lässt sich die Absicherung der einzelnen Knoten im internen Netzwerk des VPNs (192.168.100.0/24) steuern.
Jeder Client erstellt für das VPN Interface eine eigene Firewall. Über diese lässt sich steuern, welche Ports geöffnet sind und welche Client Gruppen auf diese Ports Zugriff haben.
Der lighthouse Node soll nur per ping cmd erreichbar sein (icmp). Und weiterhin allen ausgehenden Verkehr zulassen.

Bei den anderen Knoten werden wir eine detailiertere Konfiguration verwenden.

firewall:
  conntrack:
    tcp_timeout: 120h
    udp_timeout: 3m
    default_timeout: 10m
    max_connections: 100000

  outbound:
    # Allow all outbound traffic from this node
    - port: any
      proto: any
      host: any

  inbound:
    # Allow icmp between any nebula hosts
    - port: any
      proto: icmp
      host: any

Am Ende können wir unseren lighthouse Node starten. Da wir ein Netzwerk-Device erstellen müssen, benötigt dies erweiterte Access-Rights.

$ sudo nebula -config /etc/nebula/lighthouse/config.yml
INFO[0000] Firewall rule added                           firewallRule="map[caName: caSha: direction:outgoing endPort:0 groups:[] host:any ip:<nil> proto:0 startPort:0]"
INFO[0000] Firewall rule added                           firewallRule="map[caName: caSha: direction:incoming endPort:0 groups:[] host:any ip:<nil> proto:1 startPort:0]"
INFO[0000] Firewall started                              firewallHash=65f8f5d1040b999e7d2d649c3632594c0e7d57d8abd186855307c23fdfe10c03
INFO[0000] Main HostMap created                          network=192.168.100.10/24 preferredRanges="[]"
INFO[0000] UDP hole punching enabled
INFO[0000] Nebula interface is active                    build=1.1.0 interface=lighthouse.cons network=192.168.100.10/24

Setup der restlichen Knoten

Die Konfiguration der Clients geschieht in ähnlicher Art und Weise wie beim lighthouse (wir erinnern uns: es ist ein peer2peer VPN, ein lighthouse ist auch nur ein Client).

Wichtig ist, dass in der config.yml weiterhin unser lighthouse als static Host eingetragen wird:

# The syntax is:
#   "{nebula ip}": ["{routable ip/dns name}:{routable port}"]
# Example, if your lighthouse has the nebula IP of 192.168.100.1 and has the real ip address of 100.64.22.11 and runs on port 4242:
static_host_map:
  "192.168.100.10": ["100.64.22.11:4242"]

Was sich allerdings unterscheidet ist der Block lighthousein der config.yml:

lighthouse:
  # am_lighthouse is used to enable lighthouse functionality for a node. This should ONLY be true on nodes
  # you have configured to be lighthouses in your network
  am_lighthouse: false
  # serve_dns optionally starts a dns listener that responds to various queries and can even be
  # delegated to for resolution
  #serve_dns: false
  #dns:
    # The DNS host defines the IP to bind the dns listener to. This also allows binding to the nebula node IP.
    #host: 0.0.0.0
    #port: 53
  # interval is the number of seconds between updates from this node to a lighthouse.
  # during updates, a node sends information about its current IP addresses to each node.
  interval: 60
  # hosts is a list of lighthouse hosts this node should report to and query from
  # IMPORTANT: THIS SHOULD BE EMPTY ON LIGHTHOUSE NODES
  hosts:
    - "192.168.100.10"

Der aktuelle Client ist kein lighthouse, die IP 192.168.100.10 ist die (VPN) interne IP unseres lighthouse Knoten, an welchen unser Client andere peer2peer nodes bekannt machen kann.

Da die Knoten mit dem Services (service.prod und service.dev) zusätzliche Firewall Regeln beinhalten, ergänzen wir in deren config.yml Datei den Block firewall:

firewall:
  conntrack:
    tcp_timeout: 120h
    udp_timeout: 3m
    default_timeout: 10m
    max_connections: 100000

  outbound:
    # Allow all outbound traffic from this node
    - port: any
      proto: any
      host: any

  inbound:
    # Allow icmp between any nebula hosts
    - port: any
      proto: icmp
      host: any

    # Allow tcp/443 from any host within the VPN
    - port: 443
      proto: tcp
      host: any

Der nächste Block für SSH unterscheidet sich nun für service.dev und service.prod jeweils:

service.prod:

# Allows tcp/22 from any host with group support-prod
    - port: 22
      proto: tcp
      groups:
        - support-prod

service.dev:

# Allows tcp/22 from any host with group support-dev
    - port: 22
      proto: tcp
      groups:
        - support-dev

Theoretisch lassen sich hier auch mehrere Gruppen kombinieren.

Der Rest der Konfiguration ist identlisch.

Damit nun auch unsere obigen Regeln greifen, müssen wir beim Erstellen der Client Zertifikate natürlich auch die entsprechenden Gruppen verwenden. Zusätzlich empfiehlt es sich auch, die Gültigkeit der Zertifikate einzuschränken - z.B. für die mobilen Clients auf zwei Wochen (336 Stunden), bzw. 6 Monate (4032 Stunden) für die Services.

Für unseren Produktion Service sieht der Befehl nun so aus:

$ ./nebula-cert sign \
    -name "service.prod" \
    -ca-crt "certs/ca.crt" \
    -ca-key "certs/ca.key" \
    -ip "192.168.100.20/24" \
    -duration "4032h"

Die weiteren Befehle setzen sich nun wie folgt zusammen:

$ ./nebula-cert sign -name "service.dev" \
	-ip "192.168.100.30/24" \
	-ca-crt "certs/ca.crt" -ca-key "certs/ca.key" \
 	-duration "4032h"

# Die Clients

$ ./nebula-cert sign -name "anna" -groups "dev,support-dev,support-prod" \
	-ip "192.168.100.101/24" \
	-ca-crt "certs/ca.crt" -ca-key "certs/ca.key" \
	-duration "336h"
$ ./nebula-cert sign -name "anton" -groups "dev,support-dev" \
	-ip "192.168.100.102/24" \
	-ca-crt "certs/ca.crt" -ca-key "certs/ca.key"\
	 -duration "336h"

Wir haben nun für jeden konfigurierten Client ein Paar aus crt und key Dateien. Beide Dateien können nun mit dem Binary der jeweiligen Plattform verpackt werden und zur Provisionierung verteilt werden.

Gestartet werden die anderen Nodes mit dem gleichen Befehl, wie auch die lighthouse node:

$ sudo nebula -config /etc/nebula/service.prod/config.yml
INFO[0000] Firewall rule added                           firewallRule="map[caName: caSha: direction:outgoing endPort:0 groups:[] host:any ip:<nil> proto:0 startPort:0]"
INFO[0000] Firewall rule added                           firewallRule="map[caName: caSha: direction:incoming endPort:0 groups:[] host:any ip:<nil> proto:1 startPort:0]"
INFO[0000] Firewall rule added                           firewallRule="map[caName: caSha: direction:incoming endPort:443 groups:[] host:any ip:<nil> proto:6 startPort:443]"
INFO[0000] Firewall rule added                           firewallRule="map[caName: caSha: direction:incoming endPort:22 groups:[support-prod] host: ip:<nil> proto:6 startPort:22]"
INFO[0000] Firewall started                              firewallHash=a098fb9d76aa8b5004f10ebde29bbecf9755d57680e28e07653a508ede541fe3
INFO[0000] Main HostMap created                          network=192.168.100.20/24 preferredRanges="[]"
INFO[0000] UDP hole punching enabled
INFO[0000] Nebula interface is active                    build=1.1.0 interface=utun4 network=192.168.100.20/24
INFO[0000] Handshake message sent                        handshake="map[stage:1 style:ix_psk0]" initiatorIndex=119518810 remoteIndex=0 udpAddr="100.64.22.11:4242" vpnIp=192.168.100.10
…

Wir sehen, dass neben einer anderen IP auch die anderen Firewall Regeln angezeigt werden. Weiterhin unterschiedlich ist der Handshake Request zu dem konfigurierten lighthouse node.

Fazit

In den letzten Wochen haben die Berichte über eingeschränkte Möglichkeiten bei der Remote-Arbeit aufgrund von Limitierungen der Infrastruktur sehr zugenommen.
Die bisher angeschafften, zentralisierten Lösungen sind nicht für eine breite Verwendung dimensioniert.
Zentrale Einwahlknoten stellen immer einen potenziellen Flaschenhals dar.
Slack hat mit Nebula einen VPN Ansatz als OSS veröffentlich, der im Produktivsetup von Slack selbst bewiesen hat, dass er in den Punkten Durchsatz, Stabilität und Koordination mit anderen kommerziellen Produkten mithalten kann.

ArsTechnica erwähnen in ihrem Artikel einige kritische Punkte, denen ich mich allerding ebenfalls anschließen kann:

Positiv bleibt hervorzuheben, dass:

Mittlerweile findet Nebula auch auf anderer Ebene Verwendung. Die Jitsi Knoten von Freifunk München sind mittlerweile über ein internes Nebula VPN verbunden, hier wird es sicher in absehbarer Zeit weitere Erkenntnisse aus den Tests geben.

tweet FreifunkMUC/status/1268103249330089984

https://twitter.com/FreifunkMUC/status/1268103249330089984

Als letzter Punkt sei erwähnt, dass eine moderne Systemarchitektur lieber ohne eine Absicherung über ein VPN auskommen sollte.
Ein Zero-Trust Ansatz ist hier sicherlich erstrebenswert und ermöglich auch im Nachhinein die Öffnung des eigenen Serviceangebots für weitere Parteien, ohne dass weitere zusätzliche Anpassungen notwendig sind.