Kontext
Ich betreibe einen Monitoring-Stack mit Docker Compose, bestehend aus u.a. Grafana, Prometheus, Node Exporter, cAdvisor, InfluxDB, einem WireGuard-Container und einem speziellen Sidecar-Container (grafana-netinit), der im Netzwerk-Namespace von Grafana läuft.
Ziel ist, dass der Grafana-Container über den WireGuard-Container auf mein Heimnetz (z.B. 172.16.46.0/24) zugreifen kann, ohne das offizielle Grafana-Image zu verändern.
Alle Secrets (Tokens, Passwörter, Client IDs, E-Mail-Adressen) sind im Compose-File durch Platzhalter wie <SECRET_...> ersetzt.
Warum der Sidecar nötig ist
Das offizielle Grafana-Image ist ein generisches, fertig gebautes Container-Image, in dem ich selbst keine zusätzlichen Pakete wie iproute2 installieren oder den EntryPoint anpassen möchte (bzw. zum Teil auch gar nicht kann, z.B. wenn es als unverändertes Upstream-Image laufen soll).
Im Container laufen die Prozesse zudem typischerweise nicht mit vollen Kernel-Capabilities, sodass Befehle wie ip route add ohne passende CAP_NET_ADMIN-Rechte scheitern würden.
Statt das Grafana-Image zu „verbiegen“ oder Sicherheits-/Upgrade-Probleme zu riskieren, übernimmt der Sidecar (grafana-netinit) die Aufgabe, im gemeinsamen Netzwerk-Namespace die Route zu setzen.
Durch network_mode: "service:grafana" teilen sich beide Container denselben Network Namespace (Interfaces, IP-Adressen, Routing-Tabelle), sodass der Sidecar mit CAP_NET_ADMIN die Route setzen kann, während Grafana selbst unverändert und mit minimalen Rechten läuft.
Skizze / Zeichnung (Textform)
+-----------------------+
| Heimnetz |
| 172.16.46.0/24 |
+-----------+-----------+
^
| (WireGuard-Tunnel)
v
+-----------------------------+-----------------------------+
| Docker Host / monitoring-Netz |
| |
| 172.20.0.0/16 (Bridge "monitoring") |
| |
| +---------------------------+ |
| | wireguard | IP: 172.20.0.50 |
| | - Tunnel ins Heimnetz |---------------------------+
| +---------------------------+ |
| |
| +---------------------------------------------------+ |
| | gemeinsamer Network Namespace | |
| | (Grafana + grafana-netinit via | |
| | network_mode: "service:grafana") | |
| | | |
| | +-------------------+ +-----------------+ | |
| | | Grafana | | grafana-netinit | | |
| | | (App, kein | | (Sidecar, | | |
| | | CAP_NET_ADMIN) | | CAP_NET_ADMIN) | | |
| | +---------+---------+ +---------+-------+ | |
| | | | | |
| | Routing-Tabelle im Namespace | | |
| | ---------------------------- | | |
| | default via 172.20.0.1 dev ethX | | |
| | 172.20.0.0/16 dev ethX | | |
| | 172.21.0.0/16 dev ethY | | |
| | 172.16.46.0/24 via 172.20.0.50 |<---------+ |
| +---------------------------------------------------+ |
+-----------------------------------------------------------+Kurz erklärt:
- Grafana und der Sidecar teilen sich denselben Netzwerk-Namespace (gleichen Satz an Interfaces und Routing-Tabelle).
- Der Sidecar setzt die Route
172.16.46.0/24 via 172.20.0.50 dev <IFACE>in diesem Namespace. - Grafana schickt dadurch Traffic ins Heimnetz automatisch über den WireGuard-Container, obwohl es selbst keine zusätzlichen Rechte oder Tools dafür besitzt.
Relevante Services (vereinfacht)
-
Grafana
- Image:
grafana/grafana-oss:latest - Auth via Generic OAuth gegen
<OIDC_PROVIDER_URL> - Hängt an zwei Netzen:
monitoring(172.20.0.0/16) undproxy(172.21.0.0/16) - Ziel: Zugriff auf Heimnetz
172.16.46.0/24über WireGuard.
- Image:
-
WireGuard
- Image:
lscr.io/linuxserver/wireguard:latest - Statische IP im
monitoring-Netz:172.20.0.50 - Baut Tunnel ins Heimnetz auf, das
172.16.46.0/24routen kann.
- Image:
-
grafana-netinit (Sidecar)
- Image:
busybox:1.36(alternativalpine:3.19mitiproute2) network_mode: "service:grafana"→ teilt sich den Netzwerk-Namespace mit Grafanacap_add: [ NET_ADMIN ]- Aufgabe: Statische Route im Grafana-Netznamespace setzen, damit Traffic ins Heimnetz über WireGuard geht.
- Image:
Problem & Ursache
- Anfangs funktionierte die Route, später nicht mehr.
- Im Grafana-Container fehlte der Eintrag
172.16.46.0/24 via 172.20.0.50 dev <iface>. - Analyse zeigte:
- Docker ordnet die NICs je nach Netzwerk-Reihenfolge zu; das
monitoring-Netz war frühereth0, spätereth1. - Der ursprüngliche Befehl nutzte ein hart codiertes Interface (
dev eth0), das nicht mehr zum 172.20er Netz passte. - Zusätzlich hat Docker Compose/Portainer das
$IFACEimcommand:-Block interpoliert, wodurch der Befehl im Container unvollständig wurde.
- Docker ordnet die NICs je nach Netzwerk-Reihenfolge zu; das
finale Lösung: dynamisches Interface + escaped $
Im grafana-netinit-Service wird das Interface dynamisch aus der Routing-Tabelle ermittelt, und die Shell-Variable wird im Compose-File korrekt escaped:
grafana-netinit:
image: busybox:1.36
network_mode: "service:grafana"
cap_add:
- NET_ADMIN
depends_on:
- grafana
- wireguard
command: >
/bin/sh -lc "
IFACE=$(ip route | awk '/172.20.0.0\\/16/ {print $3; exit}');
ip route replace 172.16.46.0/24 via 172.20.0.50 dev $$IFACE || true;
sleep infinity
"
restart: unless-stoppedWichtige Details:
network_mode: "service:grafana"sorgt dafür, dass der Sidecar im selben Netznamespace wie Grafana läuft und dortip routebearbeiten kann, ohne das Grafana-Image anzupassen.IFACE=$(ip route | awk '/172.20.0.0\\/16/ {print $3; exit}')ermittelt das tatsächliche Interface (z.B.eth1) für das 172.20er Netz dynamisch.dev $$IFACE: Das doppelte Dollarzeichen verhindert, dass Compose/Portainer die Variable schon beim Parsen interpoliert. Im Container kommtdev $IFACEan, das dann von/bin/shkorrekt aufgelöst wird.
Optional könnte ein Watchdog eingesetzt werden, der periodisch prüft, ob die Route noch existiert, und sie bei Bedarf nachsetzt, falls ein Grafana-Restart die Routing-Tabelle überschreibt.
Checkliste / How-To
- WireGuard-Container mit fixer IP im gleichen Netz wie Grafana (
172.20.0.50immonitoring-Netz). - Grafana und WireGuard im selben Docker-Netzwerk (
monitoring). - Sidecar
grafana-netinitmit:network_mode: "service:grafana"cap_add: NET_ADMIN- dynamischer IFACE-Ermittlung und statischem Route-Command wie oben.
- Nach Deployment prüfen:
docker exec -it <grafana-container> ip route # Erwartet: # 172.16.46.0/24 via 172.20.0.50 dev ethX - Sicherstellen, dass WireGuard das Heimnetz (
172.16.46.0/24) korrekt alsAllowedIPs/Route kennt und Antworten zurück insmonitoring-Netz gehen.
Dieses Setup erlaubt Grafana den Zugriff auf Ressourcen im Heimnetz über den WireGuard-Tunnel, ohne das offizielle Grafana-Image anzupassen, und ist robust gegenüber Interface-Änderungen in Docker.