Installation de Netbird

👋 Présentation
Netbird est une solution open source (BSD 3-Clause License) permettant de sécuriser des accès à distance. Basé sur le VPN Wireguard, elle permet également d'affiner les accès aux ressources locales depuis les clients distants de manière plus fine que des règles firewall. Elle intègre également des solutions IAM telles que Zitadel (installé par défaut dans la stack Docker fournie par Netbird) ou encore Keycloak afin d'authentifier les utilisateurs.
Notion importante : Netbird est plus proche du principe du bastion que du VPN classique. Si vous comptez remplacer votre VPN OpenVPN avec des road-warriors, je ne pense pas que Netbird soit le plus approprié.
Dans cette série d'articles, nous allons voir comment l'installer et comment l'utiliser. Concernant l'installation, j'ai décidé de ne pas utiliser la version par défaut (avec Zitadel et Caddy) mais de l'intégrer avec Keycloak et Traefik. Le tout se basera sur Docker.
Contexte
Mon objectif est de déployer un PoC de SaaS sécurisé afin de pouvoir rendre accessible plusieurs instances de l'application Paperless-NGX non exposées sur Internet à différentes entités externes.
Là où je pousse un peu, c'est que cela doit être le plus transparent pour l'utilisateur final. Une fois authentifié sur Netbird par le biais de l'IDP, l'utilisateur l'est également sur l'application visée.

Dans le détail :

L'ensemble de l'infrastructure tourne dans des VM sur Proxmox VE.
🚀 Installation de Netbird par défaut
Si vous voulez avoir un premier aperçu de la solution sans prise de tête sur l'installation, rendez-vous alors sur Installation rapide.
🚀 Installation avec Traefik et Keycloak
Netbird, Traefik et Keycloak tournent sur Docker dans une VM commune.
Netbird sera accessible sur netbird.dev.quercylibre.fr et Keycloak sur keycloak.dev.quercylibre.fr.
Traefik
traefik/
|-- compose.yml
|-- config/
| |-- acme.json
| |-- crowdsec/
| | `-- ban.html
| |-- dyn_traefik/
| | `-- crowdsec.yml
| |-- secrets/
| | |-- ovh_application_key.secret
| | |-- ovh_application_secret.secret
| | |-- ovh_consumer_key.secret
| | `-- ovh_endpoint.secret
| `-- traefik.yml
`-- logs/
|-- access.log
|-- traefik.log
networks:
traefik:
external: true
secrets:
ovh_endpoint:
file: "./config/secrets/ovh_endpoint.secret"
ovh_application_key:
file: "./config/secrets/ovh_application_key.secret"
ovh_application_secret:
file: "./config/secrets/ovh_application_secret.secret"
ovh_consumer_key:
file: "./config/secrets/ovh_consumer_key.secret"
services:
traefik:
image: "traefik:v3"
container_name: traefik
networks:
- traefik
secrets:
- "ovh_endpoint"
- "ovh_application_key"
- "ovh_application_secret"
- "ovh_consumer_key"
environment:
- "TZ=Europe/Paris"
- "OVH_ENDPOINT_FILE=/run/secrets/ovh_endpoint"
- "OVH_APPLICATION_KEY_FILE=/run/secrets/ovh_application_key"
- "OVH_APPLICATION_SECRET_FILE=/run/secrets/ovh_application_secret"
- "OVH_CONSUMER_KEY_FILE=/run/secrets/ovh_consumer_key"
volumes:
# Mapping sur le socket interne de Docker
- '/var/run/docker.sock:/var/run/docker.sock:ro'
# Mapping du fichier de configuration statique
- './config/traefik.yml:/traefik.yml'
# Mapping du dossier contenant la configuration dynamique
- './config/dyn_traefik/:/dyn_traefik/'
# Mapping du fichier de stockage des certificats
- './config/acme.json:/acme.json'
- "./logs:/var/log"
# Mapping du fichier ban de crowdsec
- "./config/crowdsec/ban.html:/ban.html"
ports:
- "80:80"
- "443:443"
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.dev.quercylibre.fr`)"
- "traefik.http.routers.traefik-dashboard.service=api@internal"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik-dashboard.middlewares=crowdsec@file,traefik-dashboard-ipallowlist"
- "traefik.http.middlewares.traefik-dashboard-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.0/24"
- "traefik.http.services.traefik-dashboard-service.loadbalancer.server.port=8080"
api:
dashboard: true
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /dyn_traefik/
watch: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
caServer: "https://acme-v02.api.letsencrypt.org/directory"
email: "<EMAIL>"
storage: "/acme.json"
keyType: EC384
dnsChallenge:
provider: ovh
delayBeforeCheck: 10
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
log:
filePath: "/var/log/traefik.log"
format: json
level: INFO
maxSize: 5
maxBackups: 50
maxAge: 10
compress: true
accessLog:
filePath: "/var/log/access.log"
format: json
fields:
defaultMode: keep
names:
StartUTC: drop
experimental:
plugins:
bouncer:
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.4.1"
Vous pouvez vous passer de Crowdsec dans le cadre du PoC. Si vous souhaitez l'installer, vous pouvez vous baser sur cet article : 🛡️ Sécurisation avec Crowdsec
Définition d'une tâche logrotate sur le serveur afin d'assurer une rotation des logs de Traefik (access.log et traefik.log) :
compress
/srv/docker/traefik/logs/*.log {
size 20M
daily
rotate 14
missingok
notifempty postrotate
docker kill --signal="USR1" traefik # préciser le nom du conteneur visé, ici "traefik"
endscript
}
Instanciez le conteneur.
Keycloak
---
networks:
keycloak_network:
traefik:
external: true
services:
postgres:
image: postgres:16.2
container_name: keycloak-pgsql
volumes:
- ./postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
networks:
- keycloak_network
healthcheck:
test: [ "CMD", "pg_isready", "-q", "-d", "${POSTGRES_DB}", "-U", "${POSTGRES_USER}" ]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
restart: unless-stopped
keycloak:
image: quay.io/keycloak/keycloak:26.1
container_name: keycloak
command: start
environment:
KC_HOSTNAME: keycloak.dev.quercylibre.fr
KC_HOSTNAME_STRICT_BACKCHANNEL: false
KC_HTTP_ENABLED: true
KC_HOSTNAME_STRICT_HTTPS: false
KC_PROXY_HEADERS: 'xforwarded'
PROXY_ADDRESS_FORWARDING: 'true'
KC_HEALTH_ENABLED: true
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/${POSTGRES_DB}
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
KC_DB_SCHEMA: public
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.rule=Host(`keycloak.dev.quercylibre.fr`)"
- "traefik.http.routers.keycloak.service=keycloak"
- "traefik.http.routers.keycloak.entrypoints=websecure"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
- "traefik.http.routers.keycloak.tls=true"
- "traefik.http.routers.keycloak.tls.certresolver=letsencrypt"
- "traefik.http.services.keycloak.loadbalancer.passhostheader=true"
- "traefik.http.routers.keycloak.middlewares=compresstraefik"
- "traefik.http.middlewares.compresstraefik.compress=true"
- "traefik.docker.network=traefik"
restart: unless-stopped
healthcheck:
test:
- "CMD-SHELL"
- |
exec 3<>/dev/tcp/localhost/9000 &&
echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 &&
cat <&3 | tee /tmp/healthcheck.log | grep -q '200 OK'
interval: 10s
timeout: 5s
retries: 3
start_period: 90s
depends_on:
postgres:
condition: service_healthy
networks:
- keycloak_network
- traefik
POSTGRES_DB=<KEYCLOAK_DB_NAME>
POSTGRES_USER=<KEYCLOAK_DB_USER>
POSTGRES_PASSWORD=<KEYCLOAK_DB_USER_PASSWORD>
KEYCLOAK_ADMIN=<KEYCLOAK_ADMIN_USER>
KEYCLOAK_ADMIN_PASSWORD=<KEYCLOAK_ADMIN_USER_PASSWORD>
Lors de votre première connexion à Keycloak, il vous sera demandé de changer le mot de passe de l'administrateur de keycloak.
IMPORTANT : avant d'attaquer Netbird, il vous faut configurer ce dernier dans Keycloak. Heureusement, Netbird propose une documentation IDP Keycloak
Instanciez le conteneur.
Netbird
C'est la partie la plus intense 🙃. Malgré la documentation officielle (et son script de configuration semi-automatique) et celles trouvées sur Internet, aucune ne permettait de configurer de manière opérationnelle Netbird avec Traefik.
En effet dans le cadre de l'installation par défaut, il est demandé d'ouvrir différents ports : - les ports TCP 80, 443, 33073, 10000 et 33080 à destination des briques logicielles de Netbird ; - les ports UDP : 3478, 49152-65535 à destination du serveur Coturn.
Dans ce cas, je souhaite que tous les ports destinés aux briques logicielles de Netbird soient remplacés par le port TCP/443 (j'ai commencé à plancher sur Coturn mais pour le moment je vais le laisser tel quel).
Il faudra donc ouvrir sur le net le port TCP/443 et les ports UDP 3478, 49152-65535. Dans mon cas j'utilise OPNSense en frontend de l'ensemble de l'infra.
Je vous livre donc ma configuration from scratch que vous devrez adapter à votre environnement. Prenez une bonne tasse de café ☕ voire plusieurs !
netbird
|-- compose.yml
|-- management.json
|-- netbird-mgmt/
|-- netbird-signal/
`-- turnserver.conf
---
networks:
traefik:
external: true
services:
# UI dashboard
dashboard:
image: netbirdio/dashboard:latest
restart: unless-stopped
networks:
- traefik
environment:
# Endpoints
- NETBIRD_MGMT_API_ENDPOINT=https://netbird.dev.quercylibre.fr
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://netbird.dev.quercylibre.fr
# OIDC
- AUTH_AUDIENCE=netbird-client
- AUTH_CLIENT_ID=netbird-client
- AUTH_CLIENT_SECRET=<CLIENT_SECRET_GENERE_AVEC_KEYCLOAK>
- AUTH_AUTHORITY=https://keycloak.dev.quercylibre.fr/realms/Netbird
- USE_AUTH0=false
- AUTH_SUPPORTED_SCOPES=openid profile email offline_access api
- AUTH_REDIRECT_URI=
- AUTH_SILENT_REDIRECT_URI=
- NETBIRD_TOKEN_SOURCE=accessToken
labels:
- traefik.enable=true
- traefik.http.routers.netbird-dashboard.entrypoints=websecure,web
- traefik.http.routers.netbird-dashboard.rule=Host(`netbird.dev.quercylibre.fr`)
- traefik.http.routers.netbird-dashboard.tls=true
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-dashboard.service=netbird-dashboard@docker
- traefik.http.services.netbird-dashboard.loadbalancer.server.port=80
- traefik.docker.network=traefik
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
# Signal
signal:
image: netbirdio/signal:latest
restart: unless-stopped
networks:
- traefik
volumes:
- ./netbird-signal:/var/lib/netbird
labels:
- traefik.enable=true
- traefik.http.routers.netbird-signal.entrypoints=websecure
- traefik.http.routers.netbird-signal.rule=Host(`netbird.dev.quercylibre.fr`) && PathPrefix(`/signalexchange.SignalExchange/`)
- traefik.http.routers.netbird-signal.tls=true
- traefik.http.services.netbird-signal.loadbalancer.server.port=10000
- traefik.http.services.netbird-signal.loadbalancer.server.scheme=h2c
- traefik.docker.network=traefik
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
# Relay
relay:
image: netbirdio/relay:latest
restart: unless-stopped
networks:
- traefik
environment:
- NB_LOG_LEVEL=info
- NB_LISTEN_ADDRESS=:33080
- NB_EXPOSED_ADDRESS=rels://netbird.dev.quercylibre.fr:443/relay
# /!\ Pour générer le secret ci-dessous : openssl rand -base64 32 | sed 's/=//g' /!\
- NB_AUTH_SECRET=<SECURE_SECRET>
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
labels:
- traefik.enable=true
- traefik.http.routers.netbird-relay.entrypoints=websecure
- traefik.http.routers.netbird-relay.rule=Host(`netbird.dev.quercylibre.fr`) && PathPrefix(`/relay`)
- traefik.http.routers.netbird-relay.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-relay.tls=true
- traefik.http.routers.netbird-relay.service=netbird-relay
- traefik.http.services.netbird-relay.loadbalancer.server.port=33080
# Management
management:
image: netbirdio/management:latest
restart: unless-stopped
networks:
- traefik
depends_on:
- dashboard
volumes:
- ./netbird-mgmt:/var/lib/netbird
- ./management.json:/etc/netbird/management.json
command: [
"--port", "443",
"--log-file", "console",
"--log-level", "info",
"--disable-anonymous-metrics=false",
"--single-account-mode-domain=netbird.dev.quercylibre.fr",
"--dns-domain=netbird.selfhosted"
]
labels:
- traefik.enable=true
- traefik.http.routers.netbird-api.entrypoints=websecure,web
- traefik.http.routers.netbird-api.rule=Host(`netbird.dev.quercylibre.fr`) && PathPrefix(`/api`)
- traefik.http.routers.netbird-api.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-api.tls=true
- traefik.http.routers.netbird-api.service=netbird-api
- traefik.http.services.netbird-api.loadbalancer.server.port=443
- traefik.http.routers.netbird-management.entrypoints=websecure,web
- traefik.http.routers.netbird-management.rule=Host(`netbird.dev.quercylibre.fr`) && PathPrefix(`/management.ManagementService/`)
- traefik.http.routers.netbird-management.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-management.tls=true
- traefik.http.routers.netbird-management.service=netbird-management
- traefik.http.services.netbird-management.loadbalancer.server.port=443
- traefik.http.services.netbird-management.loadbalancer.server.scheme=h2c
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
environment:
- NETBIRD_STORE_ENGINE_POSTGRES_DSN=
- NETBIRD_STORE_ENGINE_MYSQL_DSN=
# Coturn
coturn:
image: coturn/coturn:latest
restart: unless-stopped
#domainname: netbird.dev.quercylibre.fr
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
network_mode: host
command:
- -c /etc/turnserver.conf
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:netbird.dev.quercylibre.fr:3478",
"Username": "",
"Password": ""
}
],
"TURNConfig": {
"TimeBasedCredentials": false,
"CredentialsTTL": "12h0m0s",
"Secret": "secret",
"Turns": [
{
"Proto": "udp",
"URI": "turn:netbird.dev.quercylibre.fr:3478",
"Username": "self",
// /!\ Secret généré avec openssl rand -base64 32 | sed 's/=//g' /!\
"Password": "<SECRET>"
}
]
},
"Relay": {
"Addresses": [
"rels://netbird.dev.quercylibre.fr:443/relay"
],
"CredentialsTTL": "24h0m0s",
// /!\ Secret généré avec openssl rand -base64 32 | sed 's/=//g' /!\
"Secret": "<SECRET>"
},
"Signal": {
"Proto": "https",
"URI": "netbird.dev.quercylibre.fr:443",
"Username": "",
"Password": ""
},
"Datadir": "/var/lib/netbird/",
// /!\ Secret généré avec openssl rand -base64 32 | sed 's/=//g' /!\
"DataStoreEncryptionKey": "<SECRET>",
"HttpConfig": {
"LetsEncryptDomain": "",
"CertFile": "",
"CertKey": "",
"AuthAudience": "netbird-client",
"AuthIssuer": "https://keycloak.dev.quercylibre.fr/realms/Netbird",
"AuthUserIDClaim": "",
"AuthKeysLocation": "https://keycloak.dev.quercylibre.fr/realms/Netbird/protocol/openid-connect/certs",
"OIDCConfigEndpoint": "https://keycloak.dev.quercylibre.fr/realms/Netbird/.well-known/openid-configuration",
"IdpSignKeyRefreshEnabled": false,
"ExtraAuthAudience": ""
},
"IdpManagerConfig": {
"ManagerType": "keycloak",
"ClientConfig": {
"Issuer": "null",
"TokenEndpoint": "https://keycloak.dev.quercylibre.fr/realms/Netbird/protocol/openid-connect/token",
"ClientID": "netbird-backend",
// Secret généré avec Keycloak lors de la configuration du client netbird-backend
"ClientSecret": "SECRET_netbird-backend",
"GrantType": "client_credentials"
},
"ExtraConfig": {
"AdminEndpoint": "https://keycloak.dev.quercylibre.fr/admin/realms/Netbird"
},
"Auth0ClientCredentials": null,
"AzureClientCredentials": null,
"KeycloakClientCredentials": null,
"ZitadelClientCredentials": null
},
"DeviceAuthorizationFlow": {
"Provider": "hosted",
"ProviderConfig": {
"ClientID": "netbird-client",
// Secret généré avec Keycloak lors de la configuration du client netbird-client
"ClientSecret": "SECRET_netbird-client",
"Domain": "keycloak.dev.quercylibre.fr",
"Audience": "netbird-client",
"TokenEndpoint": "https://keycloak.dev.quercylibre.fr/realms/Netbird/protocol/openid-connect/token",
"DeviceAuthEndpoint": "https://keycloak.dev.quercylibre.fr/realms/Netbird/protocol/openid-connect/auth/device",
"AuthorizationEndpoint": "",
"Scope": "openid",
"UseIDToken": false,
"RedirectURLs": null
}
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"ClientID": "netbird-client",
// Secret généré avec Keycloak lors de la configuration du client netbird-client
"ClientSecret": "SECRET_netbird-client",
"Domain": "",
"Audience": "netbird-client",
"TokenEndpoint": "https://keycloak.dev.quercylibre.fr/realms/Netbird/protocol/openid-connect/token",
"DeviceAuthEndpoint": "",
"AuthorizationEndpoint": "https://keycloak.dev.quercylibre.fr/realms/Netbird/protocol/openid-connect/auth",
"Scope": "openid profile email offline_access api",
"UseIDToken": false,
// Le paramètre ci-dessous est très important car cette url sera utilisé par le client
"RedirectURLs": [
"http://localhost:53000"
]
}
},
"StoreConfig": {
"Engine": "sqlite"
},
"ReverseProxy": {
"TrustedHTTPProxies": [],
"TrustedHTTPProxiesCount": 0,
"TrustedPeers": [
"0.0.0.0/0"
]
}
}
listening-port=3478
tls-listening-port=5349
external-ip=82.66.224.154
min-port=49152
max-port=65535
fingerprint
lt-cred-mech
# /!\ Secret généré avec openssl rand -base64 32 | sed 's/=//g' /!\
user=self:SECRET
realm=wiretrustee.com
cert=/etc/coturn/certs/cert.pem
pkey=/etc/coturn/private/privkey.pem
log-file=stdout
no-software-attribute
pidfile="/var/tmp/turnserver.pid"
no-cli
Il ne vous reste plus qu'à lancer tout ce petit monde et à debugguer en cas de souci.
Première prise en main
Une fois la stack Netbird lancée, il ne vous reste plus qu'à vous connecter sur le dashboard de Netbird avec l'utilisateur créé lors de la configuration de Netbird dans Keycloak.
Par défaut le premier utilisateur connecté devient l'administrateur de l'application.
À partir de cette étape, nous allons pouvoir déployer des clients et définir les accès.
Si vous ne souhaitez pas aller plus loin, vous pouvez tout de même déployer des clients (pensez à créer des users dans Keycloak pour les PC clients) et installer Netbird sur un serveur GNU/Linux par exemple. Ce dernier sera alors accessible depuis son ndd ou adresse IP virtuelle fournis par défaut par Netbird. Belginux en fait d'ailleurs une démonstration ici Netbird avec Docker