diff --git a/README.md b/README.md index 668c1d6..8975456 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This repository contains Docker Compose setups for a variety of applications, en - GlitchTip (`monitoring/codebase/glitchtip`) - **Networking** - Traefik (`networking/proxies/traefik`) + - NetBird (`networking/vpn/netbird`) - **Security** - HashiCorp Vault (`security/vaults/hashicorp`) - **Operating Systems** diff --git a/networking/vpn/netbird/configuration/management.json b/networking/vpn/netbird/configuration/management.json new file mode 100644 index 0000000..849d3e1 --- /dev/null +++ b/networking/vpn/netbird/configuration/management.json @@ -0,0 +1,63 @@ +{ + "_comment": "Trimmed down from upstream's infrastructure_files/management.json.tmpl to what a single, self-hosted, generic-OIDC setup needs. This file is mounted read-only; on every container start (see docker-compose.yml) it's rendered into a container-local copy with the placeholders below substituted from .env, so this tracked copy is never modified. Edit the rest of this file directly for anything else, the same way Traefik's configuration/*.yml files are hand-edited. See https://docs.netbird.io/selfhosted/identity-providers for advanced IdP integrations.", + "Stuns": [ + { + "Proto": "udp", + "URI": "stun:${NETBIRD_DOMAIN}:3478", + "Username": "", + "Password": null + } + ], + "TURNConfig": { + "Turns": [ + { + "Proto": "udp", + "URI": "turn:${NETBIRD_DOMAIN}:3478", + "Username": "netbird", + "Password": "${NETBIRD_TURN_PASSWORD}" + } + ], + "CredentialsTTL": "12h", + "Secret": "secret", + "TimeBasedCredentials": false + }, + "Relay": { + "Addresses": ["rel://${NETBIRD_DOMAIN}:${NETBIRD_RELAY_PORT}"], + "CredentialsTTL": "24h", + "Secret": "${NETBIRD_RELAY_AUTH_SECRET}" + }, + "Signal": { + "Proto": "https", + "URI": "${NETBIRD_DOMAIN}:${NETBIRD_SIGNAL_PORT}", + "Username": "", + "Password": null + }, + "ReverseProxy": { + "TrustedHTTPProxies": [], + "TrustedHTTPProxiesCount": 0, + "TrustedPeers": ["0.0.0.0/0"] + }, + "DisableDefaultPolicy": false, + "Datadir": "", + "DataStoreEncryptionKey": "${NETBIRD_DATASTORE_ENCRYPTION_KEY}", + "StoreConfig": { + "Engine": "postgres" + }, + "HttpConfig": { + "Address": "0.0.0.0:443", + "AuthIssuer": "${NETBIRD_AUTH_AUTHORITY}", + "AuthAudience": "${NETBIRD_AUTH_AUDIENCE}", + "AuthKeysLocation": "", + "AuthUserIDClaim": "", + "CertFile": "", + "CertKey": "", + "IdpSignKeyRefreshEnabled": false, + "OIDCConfigEndpoint": "${NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT}" + }, + "IdpManagerConfig": { + "ManagerType": "none" + }, + "DeviceAuthorizationFlow": { + "Provider": "none" + } +} diff --git a/networking/vpn/netbird/configuration/turnserver.conf b/networking/vpn/netbird/configuration/turnserver.conf new file mode 100644 index 0000000..cf388d7 --- /dev/null +++ b/networking/vpn/netbird/configuration/turnserver.conf @@ -0,0 +1,33 @@ +# Coturn TURN server configuration for NetBird's relay/NAT-traversal needs. +# This file is mounted read-only; on every container start (see +# `docker-compose.yml`) it's rendered into a container-local copy with the +# TURN password below substituted from NETBIRD_TURN_PASSWORD in `.env`, so +# this tracked copy is never modified. +# Trimmed down from upstream's full `infrastructure_files/turnserver.conf.tmpl` +# (itself the stock `coturn` reference config) to only the directives this +# setup actually uses. Edit the rest of this file directly for anything else, +# the same way Traefik's `configuration/*.yml` files are hand-edited. +# +# TODO: Enable TLS (`cert`/`pkey` below) once real certificates are available, +# the same way Traefik's ACME block is left commented until then. + +listening-port=3478 +# TODO: Uncomment once certificates are in place. +# tls-listening-port=5349 + +min-port=49152 +max-port=65535 + +fingerprint +lt-cred-mech +user=netbird:${NETBIRD_TURN_PASSWORD} + +realm=netbird.selfhosted + +# cert=/etc/coturn/certs/cert.pem +# pkey=/etc/coturn/private/privkey.pem + +log-file=stdout +no-cli +no-software-attribute +pidfile="/var/tmp/turnserver.pid" diff --git a/networking/vpn/netbird/docker-compose.yml b/networking/vpn/netbird/docker-compose.yml new file mode 100644 index 0000000..307e8a9 --- /dev/null +++ b/networking/vpn/netbird/docker-compose.yml @@ -0,0 +1,157 @@ +# TODO: Use Secrets to store the passwords to all containers. +# Requires a real OIDC identity provider (Auth0, Zitadel, Keycloak, ...) and a +# public domain for Let's Encrypt to actually complete a login end to end. +# Only container startup/pull is verified locally here, the auth flow itself +# is verified once this is deployed with a real domain on the VPS. +# +# `postgresql` lives in a separate compose project (`databases/postgresql`), so +# `depends_on` cannot reference it. Wait for the host:port to be reachable +# before starting `management`, the same pattern used by `redis_commander`/`flower`. +# +# After starting Postgres, add `netbird` to `POSTGRESQL_ADDITIONAL_DATABASES` +# in `databases/postgresql`'s `.env` and create a matching role/password. +# +# `coturn` (the TURN relay) needs `network_mode: host` to relay real peer +# traffic, it is the one service in this repo not on the shared `internal` +# network. +networks: + internal: + name: "internal" + # External means the container will be accessible from other containers in the same network. + # If you want to isolate the container create a new network, with different name for each container and without being external. + external: true + +volumes: + netbird_signal: + name: "netbird_signal" + netbird_management: + name: "netbird_management" + +services: + dashboard: + image: "netbirdio/dashboard:latest" + container_name: "netbird-dashboard" + restart: "unless-stopped" + networks: + - "internal" + ports: + - "${NETBIRD_DASHBOARD_HTTP_PORT?Variable not set}:80" + - "${NETBIRD_DASHBOARD_HTTPS_PORT?Variable not set}:443" + environment: + - "NETBIRD_MGMT_API_ENDPOINT=https://${NETBIRD_DOMAIN?Variable not set}:${NETBIRD_MANAGEMENT_PORT?Variable not set}" + - "NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${NETBIRD_DOMAIN?Variable not set}:${NETBIRD_MANAGEMENT_PORT?Variable not set}" + - "AUTH_AUDIENCE=${NETBIRD_AUTH_AUDIENCE?Variable not set}" + - "AUTH_CLIENT_ID=${NETBIRD_AUTH_CLIENT_ID?Variable not set}" + - "AUTH_AUTHORITY=${NETBIRD_AUTH_AUTHORITY?Variable not set}" + - "USE_AUTH0=false" + - "AUTH_SUPPORTED_SCOPES=${NETBIRD_AUTH_SUPPORTED_SCOPES?Variable not set}" + - "AUTH_REDIRECT_URI=/peers" + - "AUTH_SILENT_REDIRECT_URI=/add-peers" + - "NETBIRD_TOKEN_SOURCE=accessToken" + labels: + - "traefik.enable=false" + + signal: + image: "netbirdio/signal:0.76.1" + container_name: "netbird-signal" + restart: "unless-stopped" + depends_on: + - "dashboard" + networks: + - "internal" + ports: + - "${NETBIRD_SIGNAL_PORT?Variable not set}:80" + volumes: + - "netbird_signal:/var/lib/netbird" + labels: + - "traefik.enable=false" + + relay: + image: "netbirdio/relay:0.76.1" + container_name: "netbird-relay" + restart: "unless-stopped" + networks: + - "internal" + ports: + - "${NETBIRD_RELAY_PORT?Variable not set}:${NETBIRD_RELAY_PORT?Variable not set}" + environment: + - "NB_LOG_LEVEL=info" + - "NB_LISTEN_ADDRESS=:${NETBIRD_RELAY_PORT?Variable not set}" + - "NB_EXPOSED_ADDRESS=rel://${NETBIRD_DOMAIN?Variable not set}:${NETBIRD_RELAY_PORT?Variable not set}" + - "NB_AUTH_SECRET=${NETBIRD_RELAY_AUTH_SECRET?Variable not set}" + labels: + - "traefik.enable=false" + + management: + image: "netbirdio/management:0.76.1" + container_name: "netbird-management" + restart: "unless-stopped" + depends_on: + - "dashboard" + networks: + - "internal" + ports: + - "${NETBIRD_MANAGEMENT_PORT?Variable not set}:443" + volumes: + - "netbird_management:/var/lib/netbird" + - "./configuration/management.json:/etc/netbird/management.json:ro" + environment: + - "NETBIRD_STORE_ENGINE_POSTGRES_DSN=postgres://${POSTGRESQL_USER?Variable not set}:${POSTGRESQL_PASSWORD?Variable not set}@postgresql:${POSTGRESQL_PORT?Variable not set}/netbird?sslmode=disable" + - "NETBIRD_DOMAIN=${NETBIRD_DOMAIN?Variable not set}" + - "NETBIRD_RELAY_PORT=${NETBIRD_RELAY_PORT?Variable not set}" + - "NETBIRD_RELAY_AUTH_SECRET=${NETBIRD_RELAY_AUTH_SECRET?Variable not set}" + - "NETBIRD_SIGNAL_PORT=${NETBIRD_SIGNAL_PORT?Variable not set}" + - "NETBIRD_TURN_PASSWORD=${NETBIRD_TURN_PASSWORD?Variable not set}" + - "NETBIRD_DATASTORE_ENCRYPTION_KEY=${NETBIRD_DATASTORE_ENCRYPTION_KEY?Variable not set}" + - "NETBIRD_AUTH_AUTHORITY=${NETBIRD_AUTH_AUTHORITY?Variable not set}" + - "NETBIRD_AUTH_AUDIENCE=${NETBIRD_AUTH_AUDIENCE?Variable not set}" + - "NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT=${NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT?Variable not set}" + # Renders a container-local `/tmp/management.json` from the read-only + # mounted `management.json` on every start (so the tracked copy is never + # modified), and passes `--config` pointing at it, so `.env` is the only + # file to fill in (its `Relay.Secret` reuses the exact same + # `NETBIRD_RELAY_AUTH_SECRET` given to the `relay` service above, so they + # can never drift out of sync by hand-editing one and forgetting the + # other). The image's own ENTRYPOINT is `/go/bin/netbird-mgmt management`, + # so this has to replace `entrypoint:` (not `command:`, which would just + # become extra args to that entrypoint). The base image is plain `ubuntu` + # without `nc`, so use bash's `/dev/tcp` instead of the `nc -z` pattern + # used elsewhere in this repo. + entrypoint: ["bash", "-c"] + command: + # All `$` below are escaped as `$$` so Compose leaves them for the + # container's shell to evaluate at runtime, instead of interpolating + # them from `.env` while parsing this file. + - | + sed -e "s|\$${NETBIRD_DOMAIN}|$$NETBIRD_DOMAIN|g" -e "s|\$${NETBIRD_RELAY_PORT}|$$NETBIRD_RELAY_PORT|g" -e "s|\$${NETBIRD_RELAY_AUTH_SECRET}|$$NETBIRD_RELAY_AUTH_SECRET|g" -e "s|\$${NETBIRD_SIGNAL_PORT}|$$NETBIRD_SIGNAL_PORT|g" -e "s|\$${NETBIRD_TURN_PASSWORD}|$$NETBIRD_TURN_PASSWORD|g" -e "s|\$${NETBIRD_DATASTORE_ENCRYPTION_KEY}|$$NETBIRD_DATASTORE_ENCRYPTION_KEY|g" -e "s|\$${NETBIRD_AUTH_AUTHORITY}|$$NETBIRD_AUTH_AUTHORITY|g" -e "s|\$${NETBIRD_AUTH_AUDIENCE}|$$NETBIRD_AUTH_AUDIENCE|g" -e "s|\$${NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT}|$$NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT|g" /etc/netbird/management.json > /tmp/management.json + until (exec 3<>/dev/tcp/postgresql/${POSTGRESQL_PORT?Variable not set}) 2>/dev/null; do + echo "Waiting for PostgreSQL..."; + sleep 1; + done + exec /go/bin/netbird-mgmt management --config /tmp/management.json --port 443 --log-file console --log-level info --disable-anonymous-metrics=true --dns-domain netbird.selfhosted + labels: + - "traefik.enable=false" + + coturn: + image: "coturn/coturn:4.17.0" + container_name: "netbird-coturn" + restart: "unless-stopped" + network_mode: "host" + volumes: + - "./configuration/turnserver.conf:/etc/coturn/turnserver.conf:ro" + environment: + - "NETBIRD_TURN_PASSWORD=${NETBIRD_TURN_PASSWORD?Variable not set}" + # Renders a container-local `/tmp/turnserver.conf` from the read-only + # mounted `turnserver.conf` on every start (so the tracked copy is never + # modified), so its TURN password always matches `NETBIRD_TURN_PASSWORD` + # in `.env`. The image's own `docker-entrypoint.sh` just prepends + # `turnserver` to arguments starting with `-` and evals each one (harmless + # to still call it afterwards), so it's safe to run this ahead of it + # instead of replacing it outright. + entrypoint: ["sh", "-c"] + command: + - > + sed "s|\$${NETBIRD_TURN_PASSWORD}|$$NETBIRD_TURN_PASSWORD|g" /etc/coturn/turnserver.conf > /tmp/turnserver.conf; + exec docker-entrypoint.sh -c /tmp/turnserver.conf + labels: + - "traefik.enable=false" diff --git a/networking/vpn/netbird/template.env b/networking/vpn/netbird/template.env new file mode 100644 index 0000000..975eed3 --- /dev/null +++ b/networking/vpn/netbird/template.env @@ -0,0 +1,32 @@ +# For all strings containing special characters, use single quotes. +# In case of older docker compose versions (<3.9), do not use single or double quotes at all. + +# Container's docker environment configuration. +COMPOSE_PROJECT_NAME='lowercase_string_without_special_characters' + +# Shared PostgreSQL connection details (must match `databases/postgresql`'s `.env`). +POSTGRESQL_PORT=1234 +POSTGRESQL_USER='string_with_special_characters' +POSTGRESQL_PASSWORD='string_with_special_characters' + +# NetBird's docker environment configuration. +# Requires a real domain (for Let's Encrypt/DNS) and a real OIDC identity +# provider (Auth0, Zitadel, Keycloak, ...) to actually log in, see +# https://docs.netbird.io/selfhosted/identity-providers. +NETBIRD_DOMAIN='netbird.example.com' +NETBIRD_DASHBOARD_HTTP_PORT=1234 +NETBIRD_DASHBOARD_HTTPS_PORT=5678 +NETBIRD_SIGNAL_PORT=1234 +NETBIRD_RELAY_PORT=1234 +NETBIRD_RELAY_AUTH_SECRET='string_without_special_characters' +NETBIRD_MANAGEMENT_PORT=1234 +NETBIRD_AUTH_AUTHORITY='string_with_special_characters' +NETBIRD_AUTH_AUDIENCE='string_with_special_characters' +NETBIRD_AUTH_CLIENT_ID='string_with_special_characters' +NETBIRD_AUTH_SUPPORTED_SCOPES='openid profile email offline_access api' +NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT='string_with_special_characters' +# TURN password, rendered into `turnserver.conf` and `management.json` on +# every container start, so both always agree without hand-editing either. +NETBIRD_TURN_PASSWORD='string_without_special_characters' +# Generate with: openssl rand -base64 32 +NETBIRD_DATASTORE_ENCRYPTION_KEY='string_without_special_characters'