Aller au contenu

Installation de Netbird

netbird-logo

👋 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.

schema-entite-paperless

Dans le détail :

schema-infra-detail

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

Arborescence
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
compose.yml
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"
config/traefik.yml
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) :

/etc/logrotate.d/traefik
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

Arborescence
keycloak/
|-- .env
|-- compose.yml
`-- postgres_data/
compose.yml
---
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
.env
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 !

Arborescence
netbird
|-- compose.yml
|-- management.json
|-- netbird-mgmt/
|-- netbird-signal/
`-- turnserver.conf
compose.yml
---
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"
management.json
{
    "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"
        ]
    }
}
turnserver.conf
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