Skip to content

update netbird server modules - #487367

Closed
shuuri-labs wants to merge 15 commits into
NixOS:masterfrom
shuuri-labs:update-netbird-server-components
Closed

update netbird server modules#487367
shuuri-labs wants to merge 15 commits into
NixOS:masterfrom
shuuri-labs:update-netbird-server-components

Conversation

@shuuri-labs

@shuuri-labs shuuri-labs commented Feb 5, 2026

Copy link
Copy Markdown

Motivation

The existing NetBird server modules have limited configurability - management and signal lack TLS options, there's no relay server module, and the embedded IDP isn't exposed. Essentially, the NixOS module's options lag pretty far behind the latest versions of NetBird's various components

Changes

Bug fix: signal state directory

The signal server's RuntimeDirectory, StateDirectory, and WorkingDirectory all pointed to netbird-mgmt instead of netbird-signal. This caused signal and management to share (and potentially corrupt) each other's files when both ran on the same machine.

New module: services.netbird.server.relay

Full relay server module with:

  • Typed port option (default 33080) for the internal listen port - avoids conflicts with nginx on port 443
  • Secure secret injection via systemd LoadCredential → EnvironmentFile → NB_AUTH_SECRET env var (secret never appears in /proc or the Nix store)
  • Embedded STUN server with configurable UDP ports
  • Nginx reverse proxy with WebSocket upgrade headers
  • Firewall integration (openFirewall)
  • extraOptions escape hatch for advanced CLI flags (direct TLS, etc.)
  • DynamicUser = true and full systemd hardening (ProtectSystem = "strict", etc.)

The orchestrator module (server.nix) gains useRelay and relayAuthSecretFile options to wire relay + management together automatically.

Expanded: services.netbird.server.management

  • Relay integration: relayAddresses and relaySecretFile (via _secret pattern for JSON config injection)
  • Database backends: store.engine (sqlite/postgres/mysql) with store.postgres.dsnFile / store.mysql.dsnFile for secret DSN handling
  • Embedded IDP: idp.embedded.enable toggle - sets ManagerType = "integrated" and provides sensible ProviderConfig defaults derived from domain. Users customize via the freeform settings option (e.g.
    settings.ProviderConfig.Owner.Email)
  • TLS support (Let's Encrypt or manual certs)
  • GeoLite updates disabled by default (privacy for self-hosted)
  • Backwards-compat aliases: singleAccountModeDomain → singleAccountMode.domain, disableSingleAccountMode → removed with migration message
  • ProtectSystem = "strict" and full systemd hardening

Expanded: services.netbird.server.signal

  • TLS support (Let's Encrypt or manual certs) with validation assertions
  • Firewall integration (openFirewall)
  • Metrics port option
  • ProtectSystem = "strict", restartTriggers, directory mode hardening

Expanded: services.netbird.clients (client module)

  • Multi-instance support via services.netbird.clients.
  • DNS, routing, and firewall configuration options
  • Rosenpass (post-quantum cryptography) support
  • SSH server integration
  • Self-hosted server connection options

Commits

  1. nixos/netbird: expand client module options - multi-instance client support, DNS/routing/firewall options, Rosenpass, SSH server, self-hosted connection; expanded netbird.md documentation; register new heading
    IDs in redirects.json
  2. nixos/netbird: fix signal server state directory - 3-line bug fix: netbird-mgmt → netbird-signal in RuntimeDirectory/StateDirectory/WorkingDirectory
  3. maintainers: add shuuri-labs - add maintainer entry to maintainer-list.nix
  4. nixos/netbird: update server maintainer to shuuri-labs - change meta.maintainers in server.nix
  5. nixos/netbird: add TLS and firewall options to signal server - TLS (Let's Encrypt / manual certs), openFirewall, metricsPort, assertions, ProtectSystem = "strict", restartTriggers
  6. nixos/netbird: add relay server module - new relay.nix with secure secret handling (LoadCredential + EnvironmentFile), STUN, nginx, firewall, DynamicUser = true; server.nix gains useRelay/relayAuthSecretFile
    orchestration and unconditional turnDomain default
  7. nixos/netbird: expand management server options - relay config, database backends, embedded IDP, TLS, backwards-compat aliases, ProtectSystem = "strict", Relay.Secret warning fix, defaultText corrections
  8. nixos/tests/netbird: reorganize and add server tests - move client test into directory, add server-management (3 nodes), server-signal, server-relay tests
  9. nixos/netbird: update server documentation - expanded server.md with quickstart, relay vs Coturn, embedded IDP, database, and complete examples

Tests

  • Reorganized nixos/tests/netbird.nix → nixos/tests/netbird/ directory with client.nix, server-management.nix, server-signal.nix, server-relay.nix
  • Management test covers 3 configurations: basic, with relay, with PostgreSQL
  • Signal test verifies service startup, correct ports, and isolated state directory
  • Relay test verifies service startup, port binding, state directory, and firewall rules

Documentation

  • Expanded netbird.md (client) with DNS, routing, security, Rosenpass, SSH, and self-hosted sections
  • Expanded server.md with quickstart guides (Coturn and relay), relay vs Coturn comparison table, embedded IDP setup, PostgreSQL backend example, relay standalone configuration, and a complete self-hosted example

How to test

# Run all NetBird tests
nix build .#checks.x86_64-linux.nixosTests.netbird -L

# Run individual tests
nix build .#checks.x86_64-linux.nixosTests.netbird.server-management -L
nix build .#checks.x86_64-linux.nixosTests.netbird.server-relay -L
nix build .#checks.x86_64-linux.nixosTests.netbird.server-signal -L

Things done

  • Built on platform:
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • Tested, as applicable:
  • Ran nixpkgs-review on this PR. See nixpkgs-review usage.
  • Tested basic functionality of all binary files, usually in ./result/bin/.
  • Nixpkgs Release Notes
    • Package update: when the change is major or breaking.
  • NixOS Release Notes
    • Module addition: when adding a new NixOS module.
    • Module update: when the change is significant.
  • Fits CONTRIBUTING.md, pkgs/README.md, maintainers/README.md and other READMEs.

@nixpkgs-ci nixpkgs-ci Bot added 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux. 10.rebuild-darwin: 0 This PR does not cause any packages to rebuild on Darwin. 12.first-time contribution This PR is the author's first one; please be gentle! 9.needs: reviewer This PR currently has no reviewers requested and needs attention. 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: module (update) This PR changes an existing module in `nixos/` labels Feb 5, 2026
@shuuri-labs
shuuri-labs force-pushed the update-netbird-server-components branch from 9091206 to a73ec87 Compare February 6, 2026 13:10
@nixpkgs-ci
nixpkgs-ci Bot requested a review from GetPsyched February 6, 2026 13:16
@nixpkgs-ci nixpkgs-ci Bot added 8.has: documentation This PR adds or changes documentation and removed 9.needs: reviewer This PR currently has no reviewers requested and needs attention. labels Feb 6, 2026
@shuuri-labs
shuuri-labs force-pushed the update-netbird-server-components branch 2 times, most recently from 3861769 to 4ec6429 Compare February 6, 2026 13:56
@PatrickDaG

Copy link
Copy Markdown
Contributor

I was about to actually take the time and review this insanely huge, single commit PR when I found my name in there.

I don't know what happened but I strongly think you took my PR piped it through your LLM of choice and then posted it. I'm not generally against the usage of LLM however when it's this obvious I would have liked at least a notice.

Anyway what I am really against is setting me as a maintainer without asking, or even notifying me. Please do not do that. As I've stated in the other netbird PR I am not interested in maintaining it anymore.

If you actually want to adopt/modernize the netbird module:

  • add yourself as a maintainer
  • split up this PR into multiple smaller commits
  • minimize the change
    I haven't completely read everything but from skimming it seems to contain a lot of unnecessary option declarations and other things that just blow up the changeset.

@PatrickDaG PatrickDaG left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated above.

@shuuri-labs
shuuri-labs force-pushed the update-netbird-server-components branch from 4ec6429 to c2ca54f Compare February 25, 2026 16:43
@nixpkgs-ci nixpkgs-ci Bot added the 8.has: maintainer-list (update) This PR changes `maintainers/maintainer-list.nix` label Feb 25, 2026
@shuuri-labs
shuuri-labs force-pushed the update-netbird-server-components branch 3 times, most recently from b42d5bc to 22fe74f Compare February 25, 2026 19:16
@shuuri-labs

shuuri-labs commented Feb 26, 2026

Copy link
Copy Markdown
Author

@PatrickDaG apologies, I'm new to nixpkg submissions and forgot to update the maintainer. I've since removed you in lieu of myself. This PR is based on main, where you're the listed maintainer. No PR of yours was run through an LLM, and though I did leverage Claude for assistance, the PR isn't 100% vibe-coded. As far as I can see, that's fine by the contribution guidelines, but if a heads-up on this is helpful to reviewers then sure, Ican include one in future!

I've re-read the contribution guidelines in-depth, received some advice on a few key RFCs on another PR of mine here and re-written this PR in accordance. Notably, I've split this PR into several smaller commits (each detailed in the description) and limited the addition of new typed options. Hopefully this is more parseable for the reviewer :) if there's any additional feedback, I'll do my best to implement it ASAP!

@debtquity

Copy link
Copy Markdown
Contributor

@shuuri-labs thanks for contributing this, I have been itching to re-write the services.netbird module since upstream included a reverse proxy feature [1].

On that note, I looked through changes and noticed you decided to keep nginx. Is there any consideration for adding a traefik option? Nginx does not work well with netbird reverse proxy due to lack of TLS pass through.

If there's no time, I may add this option myself at a later date once this gets merged.

[1] https://docs.netbird.io/selfhosted/migration/enable-reverse-proxy

@shuuri-labs

Copy link
Copy Markdown
Author

Hey @debtquity, you're welcome :)

Two reasons for not making the switch to Traefik just yet:

  • Trying to keep this first PR (my first submission to nixpkgs) reasonable in size
  • Would like to look into adding Caddy support for NetBird's 'permiter' reverse proxy, too - arguably more 'Nix-y' than Traefik and in theory also supports the the TLS passthrough we need (with the help of a plugin)

Definitely not done with the NetBird module after this gets merged, don't worry!

@bct

bct commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

This Caddy configuration worked for me: https://github.com/bct/nix-config/blob/a147a7172527af8ae9998f6ecaede7a86cbd1956/nixos/cloud/megahost-one/netbird.nix#L70-L119

@nixpkgs-ci nixpkgs-ci Bot added the 2.status: merge conflict This PR has merge conflicts with the target branch label Mar 17, 2026

@PatrickDaG PatrickDaG left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope I didn't come of too direct in the last message. Sorry about that. I should rather thank you that someone finally adopted the netbird module and will hopefully modernize it and add support for all the new things.

The multiple commits make it much more readable, thank you for that. I've added some small questions/suggestion, but all in all looks promising.

Oh, if you want you can of course take a look at my PR #354032, or have claude take a look, then I can at least feel like all the time spend on it wasn't completely wasted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the official nixpkgs stance is on this but I feel like this basically just lists the options and doesn't really add anything apart from a whole bunch of text to the manual.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, fair enough. I've trimmed the docs to remove sections that just listed individual options

description = "Anonymize sensitive information in logs.";
};

extraEnvironment = mkOption {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why you declare all these options? If not I would think an generic RFC 42 style ´settings´ option would keep the module smaller.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'venow replaced them with the existing extraEnvironment (freeform attrsOf str) for NB_* env vars and config (freeform JSON) for config.json overrides. Removed just under 200 lines in the process :) hope this is more RFC 42 compliant!

Comment thread nixos/tests/netbird/client.nix Outdated

for name in instances:
node.wait_for_unit(f"{name}.service")
node.wait_for_file(f"/var/run/{name}/sock")

for name in instances:
wait_until_rcode(node, f"{name} status |& grep -C20 Disconnected", 0, retries=5)

# Verify environment variables are set correctly for advanced client

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like testing for the environment here is not really useful since it just tests the module options we have created instead of the interaction with the actual program that we want to test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed them along with the extra test clients. The test now checks core functionality (services start and reach 'Disconnected' state)

stopIfChanged = false;
};

systemd.services.netbird-relay.serviceConfig.EnvironmentFile = "/run/netbird-relay/env";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? According to ´systemd.exec´ it should fail because the file will be accessed befor preStart runs?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is a bug. Fixed now and tested on my actual NetBird self-hosted instance. I found a fixed a couple of extra bugs which I've detailed in my comment below

The signal server's RuntimeDirectory, StateDirectory, and
WorkingDirectory were incorrectly set to "netbird-mgmt" (copied from
the management module). Fix them to use "netbird-signal".
@shuuri-labs
shuuri-labs force-pushed the update-netbird-server-components branch from 25d56de to bd29632 Compare April 22, 2026 09:39
The relay module defaulted to pkgs.netbird (the client package), which
does not contain the netbird-relay binary. Use pkgs.netbird-relay to
match the pattern of the other server modules (signal, management,
dashboard).
Both relay and management servers default to port 9090 for metrics,
causing a bind conflict when colocated. Add an explicit metricsPort
option to the relay module defaulting to 9091.
The management module set IdpManagerConfig.ManagerType to "integrated"
which is not a valid type in the netbird binary. The embedded IDP uses a
separate top-level EmbeddedIdP config section, not IdpManagerConfig.
Set ManagerType to "none" when embedded IDP is enabled and use the
correct EmbeddedIdP key with Enabled, LocalAddress, and nested
Storage.Config fields matching the upstream Go struct.
Avoids port clash with signal server which defaults to 9091.
Management=9090, signal=9091, relay=9092.
@shuuri-labs

Copy link
Copy Markdown
Author

@PatrickDaG

Apologies, I've been out sick for the past month. I'm back now have been working on/testing this for the past couple days. I've addressed your comments and tested this module on my personal VPS which runs my own self-hosted NetBird instance. I found (and fixed) the following issues:

  1. Relay package default was wrong: relay.nix defaulted to pkgs.netbird (client) instead of pkgs.netbird-relay. The binary netbird-relay didn't exist in the client package. Added it.

  2. Embedded IDP used invalid ManagerType = "integrated" when enabled: Embedded IDP should in fact use ManagerType "none" so changed it to that.

  3. Relay had no metricsPort option: It silently bound to 9090 which conflicting with management's default metrics port. Added a configurable metricsPort (default 9091).

Other than that, everything is working well - I can connect clients, connect to and pass traffic via Relay, access the dashboard and change settings etc.

Here's the basic config that uses the new module. I've commented what needs replacing should anyone want to test it themselves: https://gist.github.com/shuuri-labs/2aeb3a7229723620375a1396765d513a

@PatrickDaG

Copy link
Copy Markdown
Contributor

nixpkgs-review result

Generated using nixpkgs-review.

Command: nixpkgs-review pr 487367 --additional-package nixosTests.netbird
Commit: eb194d92aef261873d5f6e5dbb61debce3f5a2d7


x86_64-linux

⏩ 2 packages blacklisted:
  • nixos-install-tools
  • tests.nixos-functions.nixos-test
❌ 2 packages failed to build:
  • nixosTests.netbird.server-management
  • nixosTests.netbird.server-relay
✅ 2 tests built:
  • nixosTests.netbird.client
  • nixosTests.netbird.server-signal

Error logs: `x86_64-linux`
nixosTests.netbird.server-management
managementWithPostgres # [   10.242522] netbird-mgmt[1211]: Error: failed reading provided config file: /var/lib/netbird-mgmt/management.json: failed fetching OIDC configuration from endpoint https://idp.test/.well-known/openid-configuration Get "https://idp.test/.well-known/openid-configuration": dial tcp: lookup idp.test: no such host
managementWithPostgres # [   10.247876] systemd[1]: netbird-management.service: Main process exited, code=exited, status=1/FAILURE
managementWithPostgres # [   10.250121] systemd[1]: netbird-management.service: Failed with result 'exit-code'.
managementWithPostgres # [   10.255850] dhcpcd[1067]: eth0: soliciting a DHCP lease
managementWithPostgres # [   10.423773] NET: Registered PF_PACKET protocol family
managementWithPostgres # [   10.265374] dhcpcd[1067]: eth0: offered 10.0.2.15 from 10.0.2.2
managementWithPostgres # [   10.268677] dhcpcd[1067]: eth0: probing address 10.0.2.15/24
!!! Traceback (most recent call last):
!!!   File "<string>", line 4, in <module>
!!!     management.wait_for_unit("netbird-management.service")
!!! 
!!! RequestedAssertionFailed: unit "netbird-management.service" reached state "failed"
cleanup
kill QemuMachine (pid 15)
management # qemu-system-x86_64: terminating on signal 15 from pid 6 (/nix/store/0r6k8xa2kgqyp3r4v2w7yrb80ma2iawm-python3-3.13.12/bin/python3.13)
kill QemuMachine (pid 17)
managementWithPostgres # qemu-system-x86_64: terminating on signal 15 from pid 6 (/nix/store/0r6k8xa2kgqyp3r4v2w7yrb80ma2iawm-python3-3.13.12/bin/python3.13)
kill QemuMachine (pid 16)
managementWithRelay # qemu-system-x86_64: terminating on signal 15 from pid 6 (/nix/store/0r6k8xa2kgqyp3r4v2w7yrb80ma2iawm-python3-3.13.12/bin/python3.13)
(finished: cleanup, in 0.14 seconds)
nixosTests.netbird.server-relay
relay # [    8.712583] NET: Registered PF_PACKET protocol family
relay # [    8.554365] dhcpcd[1084]: eth0: offered 10.0.2.15 from 10.0.2.2
relay # [    8.557202] dhcpcd[1084]: eth0: probing address 10.0.2.15/24
relay: (finished: waiting for unit netbird-relay.service, in 9.61 seconds)
relay: waiting for TCP port 8443 on localhost
relay # Connection to localhost (::1) 8443 port [tcp/pcsync-https] succeeded!
relay: (finished: waiting for TCP port 8443 on localhost, in 0.04 seconds)
relay: must succeed: test -d /var/lib/netbird-relay
relay: (finished: must succeed: test -d /var/lib/netbird-relay, in 0.01 seconds)
relay: must succeed: iptables -L INPUT -n | grep -q 8443
relay: output: 
!!! Traceback (most recent call last):
!!!   File "<string>", line 11, in <module>
!!!     relay.succeed("iptables -L INPUT -n | grep -q 8443")
!!! 
!!! RequestedAssertionFailed: command `iptables -L INPUT -n | grep -q 8443` failed (exit code 1)
cleanup
kill QemuMachine (pid 13)
relay # qemu-system-x86_64: terminating on signal 15 from pid 6 (/nix/store/0r6k8xa2kgqyp3r4v2w7yrb80ma2iawm-python3-3.13.12/bin/python3.13)
(finished: cleanup, in 0.05 seconds)

Remove oidcConfigEndpoint from management test nodes since idp.test
doesn't resolve in the test VM. Check all iptables chains for firewall
rules since NixOS puts them in nixos-fw, not INPUT directly.
@shuuri-labs
shuuri-labs force-pushed the update-netbird-server-components branch from d77ef1b to a10f5d6 Compare April 29, 2026 12:23
@shuuri-labs

shuuri-labs commented Apr 29, 2026

Copy link
Copy Markdown
Author

Thanks for running the tests @PatrickDaG! pushed a fix for both failures:

  • server-management - the test nodes all had oidcConfigEndpoint pointing to idp.test which obviously doesn't resolve in the test VM. the management binary tries to fetch the OIDC discovery endpoint on startup and bails. removed it from all three test nodes and the default (empty string) lets it start without attempting OIDC discovery.

  • server-relay: the iptables check was looking at the INPUT chain directly (iptables -L INPUT -n), but NixOS firewall puts the actual port rules in the nixos-fw sub-chain and just jumps to it from INPUT. changed it to iptables -L -n so it searches all chains.

@PatrickDaG

Copy link
Copy Markdown
Contributor

Can you make sure the tests run on your end?

@TheRealGramdalf

Copy link
Copy Markdown
Contributor

My netbird dashboard is broken at the moment, and I came across this when trying to fix it. Thought I'd chip in regarding the traefik configuration.

I've written the code that eventually became #490985, which is a WIP redesign to the traefik module. As you said it's probably best to leave adding a traefik proxy option to the netbird module for a separate PR; I would also recommend waiting until the aforementioned PR is merged as well. I can give pointers on how to write the traefik config in a way that takes advantage of some of the new features coming up, since the approach will be a little different.

As far as the actual traefik options, this should have the options you need: https://docs.netbird.io/selfhosted/external-reverse-proxy#combined-container-setup-v0-65-0
Just so there's a direct link when the time comes.

CredentialsTTL = "12h";
Secret = "not-secure-secret";
TimeBasedCredentials = false;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not remove this from the default if the builtin relay is now recommended? Enabling both by default seems wrong to me.

RafaelKr pushed a commit to RafaelKr/nixpkgs that referenced this pull request Jul 7, 2026
Sync the server module from NixOS#487367 (head) onto
release-26.05: introduces the netbird native relay module, wires relay
into management, and adds the embedded IdP option. Verified compatible
with the netbird 0.74.2 binary (flags and management.json schema
unchanged 0.69 -> 0.74.2).
RafaelKr added a commit to RafaelKr/nixpkgs that referenced this pull request Jul 7, 2026
The derived EmbeddedIdP settings seed the initial owner via an Owner.Password field, but netbird reads the bcrypt hash from Owner.Hash. OwnerConfig in management/server/idp/embedded.go has fields Email/Hash/Username and no Password, so the admin is never created and cannot log in. Fix the derived block and the server.md example, and switch the doc from htpasswd to mkpasswd (idiomatic, no apacheHttpd dependency, native $2b$ variant; -R 10 because the mkpasswd default cost is 5).

Verified against netbird 0.69.0 (the version this module targets) and 0.74.2.

Fix for NixOS#487367 - feel free to cherry-pick.
RafaelKr added a commit to RafaelKr/nixpkgs that referenced this pull request Jul 7, 2026
The TURNConfig.Secret world-readable check fired whenever the value is a string, regardless of whether TURN is used. On the relay path (useRelay = true, no coturn) TURNConfig.Turns is empty and the secret is never read, yet the warning still fired, including for the relay example in server.md. Gate the check on Turns being non-empty, mirroring the Relay.Secret check which is already gated on relayAddresses.

Fix for NixOS#487367 - feel free to cherry-pick.
RafaelKr pushed a commit to RafaelKr/nixpkgs that referenced this pull request Jul 8, 2026
Sync the server module from NixOS#487367 (head) onto
release-26.05: introduces the netbird native relay module, wires relay
into management, and adds the embedded IdP option. Verified compatible
with the netbird 0.74.2 binary (flags and management.json schema
unchanged 0.69 -> 0.74.2).

@RafaelKr RafaelKr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together — the native relay and embedded IdP are exactly what a self-hosted NetBird needs (no external IdP, no coturn), and it's great to have server-side tests to go with them. I've been running this module for my small home-network since last weekend, rebased onto release-26.05 and with netbird bumped to 0.74.2; the module works unchanged against that newer binary (management.json keys and CLI flags are unchanged from 0.69), which is a good forward-compat signal. The rebased branch is here: https://github.com/RafaelKr/nixpkgs/tree/nixos-26.05-netbird — each point below is already implemented as a commit there, so feel free to cherry-pick.

A few things worth addressing before merge:

  • Functional bug: the embedded-IdP owner is seeded with an Owner.Password field, but netbird only reads Owner.Hash (bcrypt) — with Password the initial admin cannot log in. This is not version drift: the embedded IdP was introduced in netbird v0.62.0, and its OwnerConfig has had Hash (never Password) from that first release through the current v0.74.2. Same key in the server.md example.
  • Invalid default secret: DataStoreEncryptionKey defaults to "very-insecure-key", which is not valid base64 — netbird base64-decodes it and log.Fatalfs on startup, so any deployment that doesn't override it already fails at runtime. Better to require it (default null + assertion), turning that runtime crash into a clear build-time error.
  • Redundant default TURN: the defaults ship a TURN server plus secret, enabled alongside the now-recommended native relay. @b12f already flagged this (#487367 (comment)). Emptying TURNConfig.Turns and nulling TURNConfig.Secret gives relay/no-TURN setups a clean config; coturn is unaffected (it overrides Turns via server.nix). Note the Stuns list should stay — STUN is needed for direct P2P even with the relay (the relay ships an embedded STUN on :3478, which the default Stuns entry targets); only Turns is relay-redundant.

Nothing blocking beyond the owner-field bug; the rest is security/UX hardening of the defaults.

@@ -75,7 +85,17 @@
Datadir = "${stateDir}/data";
DataStoreEncryptionKey = "very-insecure-key";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"very-insecure-key" is not valid base64 (it contains -). netbird base64-decodes this key (util/crypt/crypt.go, NewFieldEncrypt) and log.Fatalfs at startup, so any deployment that doesn't override the default already fails at runtime with a cryptic base64 error. Defaulting to null and asserting it is set turns that into a clear build-time error. (Also worth noting in the docs: netbird has no key-rotation command — the store is single-key AES-GCM — so the key must be chosen once with e.g. openssl rand -base64 32 and kept stable.)

Suggested change
DataStoreEncryptionKey = "very-insecure-key";
DataStoreEncryptionKey = null;

and, in the assertions list:

{
  assertion = managementConfig.DataStoreEncryptionKey != null;
  message = ''
    services.netbird.server: settings.DataStoreEncryptionKey must be set.
    Generate a key with `openssl rand -base64 32` and provide it as a secret, e.g.
    settings.DataStoreEncryptionKey._secret = "/run/secrets/netbird-datastore-key".
  '';
}

Implemented in ad57cf1 — cherry-pick-ready.

Comment on lines +166 to +170
Owner = {
Email = "";
Password = "";
Username = "";
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded IdP's owner is seeded here with a Password field.

The embedded IdP reads the owner's bcrypt hash from Owner.Hash, not Owner.Password. OwnerConfig in management/server/idp/embedded.go has exactly three fields — Email, Hash, Username (no Password) — and the owner is only seeded when c.Owner.Hash != "", so an Owner.Password key is silently dropped by the JSON decoder and the admin is never created (login fails). Renaming the key to Hash fixes it.

This is not version drift — Password has never existed. The embedded IdP was introduced in netbird v0.62.0 (embedded.go is absent in v0.61.0 and earlier), and OwnerConfig has used Hash from that first release through the current v0.74.2. The value the PR supplies is already a bcrypt hash (per the htpasswd comment), so only the key name is wrong.

Likely origin of the name: the owner is mapped into a Dex dex.Password{ Hash: []byte(c.Owner.Hash) } where it's consumed — the Dex type is named Password, but its credential field is Hash. Easy to conflate the type name with the config key.

Suggested change
Owner = {
Email = "";
Password = "";
Username = "";
};
Owner = {
Email = "";
Hash = "";
Username = "";
};

Implemented in 0e667c8 (with the server.md fix below) — cherry-pick-ready.

Comment on lines 51 to 64
TURNConfig = {
Turns = [
{
Proto = "udp";
URI = "turn:${cfg.turnDomain}:${toString cfg.turnPort}";
Username = "netbird";
Password = "netbird";
}
];

CredentialsTTL = "12h";
Secret = "not-secure-secret";
TimeBasedCredentials = false;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @b12f correctly noted, shipping a default TURN server alongside the recommended native relay is wrong — and it also drags in two placeholder secrets (the Turns plaintext password and Secret) that trip the world-readable warnings on every relay-only setup, including the relay example in this PR's own server.md:

The TURNConfig.Secret is world-readable in the Nix Store, you should provide it as a _secret.
A TURNConfig.Turns password is world-readable in the Nix Store, you should provide it as a _secret.

Emptying Turns and nulling Secret fixes both: coturn overrides Turns via server.nix (mkDefault), and relay/no-TURN setups get a clean config. With Secret = null the existing isString world-readable check is already correct, so no warning-gating is needed. (For TimeBasedCredentials, add a small assertion that Secret is set when it is enabled.) A fuller alternative is to drop the whole TURNConfig block from the defaults and populate it only under the coturn branch — cleaner separation, but more invasive. The Stuns list must stay: STUN is needed for direct P2P even with the relay (the relay ships an embedded STUN on :3478, which the default Stuns entry targets).

Suggested change
};
TURNConfig = {
Turns = [ ];
CredentialsTTL = "12h";
Secret = null;
TimeBasedCredentials = false;
};

Implemented in 2a9d1aa — cherry-pick-ready.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow the suggestion code-range in the preview is off here (should replace full 51-64). I don't know why this is. But it's correct in my commit, I you descide to cherry-pick it as is.

Comment on lines +101 to +102
# Generate with: htpasswd -bnBC 10 "" 'your-password' | tr -d ':\n'
Password._secret = "/run/secrets/admin-password-hash";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here:

  1. The key must be Hash, not Password — same bug as the derived block in management.nix (the value is already a bcrypt hash, per the comment). As written, copy-pasting this leaves the admin unable to log in.
  2. mkpasswd is the more idiomatic generator for a NixOS doc than htpasswd: it's a small standalone package (htpasswd pulls in apacheHttpd), prints just the hash (no tr -d ':\n'), emits the native $2b$ bcrypt variant, and can read the password from a prompt instead of argv. Its default cost is only 5, so pass -R 10. (htpasswd's $2y$ also works — Go's bcrypt gates on the major version only — so this is ergonomics, not a correctness fix.)
Suggested change
# Generate with: htpasswd -bnBC 10 "" 'your-password' | tr -d ':\n'
Password._secret = "/run/secrets/admin-password-hash";
# Generate with: mkpasswd -m bcrypt -R 10 (type the password when prompted)
Hash._secret = "/run/secrets/admin-password-hash";

Implemented in 0e667c8 (same commit as the management.nix owner fix) — cherry-pick-ready.

@shuuri-labs

shuuri-labs commented Jul 8, 2026

Copy link
Copy Markdown
Author

I'm no longer with netbird, and since I've switched to Tailscale in my own homelab, so I'll be ceasing work on this PR. @RafaelKr has integrated my work into his PR, so that should serve as the successor to this one. Thanks for all the reviews/contributions, everyone! Closing.

@shuuri-labs shuuri-labs closed this Jul 8, 2026
@RafaelKr

RafaelKr commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

That's unfortunate. But thank you very much for your initial effort, I really appreciate it! I probably won't have the time to become the lone maintainer of the netbird module, especially because it's pre-1.0 and moving pretty fast right now.

Still here are some notes that might be helpful:

  • For mutually exclusive options we might want to use a lib.types.attrTag instead of enable options. For example instead of using options.services.netbird.server.relay.enableNginx we would use options.services.netbird.server.relay.webserver.nginx, options.services.netbird.server.relay.webserver.traefik, ...
    This way it's even possible to pass additional options for the different webservers.
    This also applies to the IdP and TURN/relay config.
    See services.misskey.reverseProxy.webserver for an example of lib.types.attrTag.
  • It might make sense to implement this as an experimental NixOS Modular Service as it would allow for composition of the different services. This composition pattern would allow to "easily" scale the deployment across multiple hosts

@jvanbruegge

Copy link
Copy Markdown
Contributor

I'd be up to comaintain the module, but I currently don't have time to help with the inital creation

@JuliusFreudenberger

Copy link
Copy Markdown
Contributor

For me it's the same. I use netbird currently in a dockerized setup managed through virtualisation.oci-containers, but would love to migrate to a native approach and maintain the module.
While I don't have the capacity to setup another PR and make big changes, I gladly have a close look over changes and test them.

@RafaelKr

Copy link
Copy Markdown
Contributor

Just to let you know, I'm working on it and making good progress. I already implemented my proposed usage of types.attrTag for the webserver configuration and really like the outcome. I also added traefik as a webserver option and on top of that I got the netbird-proxy (the new beta Reverse Proxy) component configured and running. It's not yet polished enough to be published and it probably will take some more hours of work, but I'm getting there.

I'll do incremental PRs for easy reviewability. So first doing a polished version of the current PR state. Then do another PR for implementing traefik on top. Also for traefik we might wait on #490985. And then the proxy module in another separate PR.

In another PR Caddy could also be added, as it's the third wesberver option that upstream netbird supports, if there's a need for it. For my setup I don't need it.

@TheRealGramdalf

Copy link
Copy Markdown
Contributor

Just to let you know, I'm working on it and making good progress. I already implemented my proposed usage of types.attrTag for the webserver configuration and really like the outcome.
I also added traefik as a webserver option and on top of that I got the netbird-proxy (the new beta Reverse Proxy) component configured and running. It's not yet polished enough to be published and it probably will take some more hours of work, but I'm getting there.

When you say traefik as a webserver, do you mean as a proxy? I don't think traefik can actually host content directly, usually that requires a dedicated webserver such as nginx

Then do another PR for implementing traefik on top. Also for traefik we might wait on #490985. And then the proxy module in another separate PR.

That would be ideal, yes. When the time comes please put any routing configuration under services.traefik.extraFiles."netbird".settings, there will be further documentation on how exactly to do this coming with the PR. Feel free to ping/request my review if you wish.

@woile

woile commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Hey @RafaelKr are you still going to publish a PR?
I'm working on the relay and I was thinking of upstreaming it soon.

Instead of a big PR I want to start uploading chunks. Maybe the proxy next.

@RafaelKr

Copy link
Copy Markdown
Contributor

Yes, I would like to. Hopefully I'll find some time on the next weekend.

When talking about "proxy" we need to be careful what we're actually talking about. Because there are two things that can be called reverse proxy:

  1. The reverse proxy that sits before the netbird components (relay, signal, management, dashboard) and routes traffic depending on the Path. In the netbird documentation it's called External Reverse Proxy. In my upcoming PR I like to call this the "Ingress Proxy", but this may be called differently, if someone has a better name for it.
  2. The NetBird reverse proxy service that launched with v0.65.0.

The second one depends on the first one, as it requires traefik as its ingress proxy. (The External Reverse Proxy documentation states this in their first info-block as "This is not to be confused with the NetBird reverse proxy service that launched with v0.65.0. The NetBird reverse proxy feature is only compatible with Traefik because it requires TLS passthrough, which Traefik supports natively.")

So actually we need to rework the ingress proxy first, to be able to use traefik and only then can introduce the reverse proxy service. And some people suggested to wait for the traefik refactor in #490985 (or maybe #553100 which might become its successor) before doing it here.

So actually we should implement in the following order:

  1. Native relay + embedded IdP server (this PR)
  2. Rework the ingress proxy: Currently it only supports nginx, we need to rebuild this to support other reverse proxy software aswell, starting with additional support for traefik
  3. Now we can introduce the netbird reverse proxy service feature

I'll tag you as soon as I open the PR.

@woile

woile commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

When talking about "proxy" we need to be careful what we're actually talking about

Agreed, I generally assume reverse proxies are an external component of the netbird service, and proxy means the netbird-proxy itself.

Native relay + embedded IdP server (this PR)

Do we need an IdP? I've been running the netbird-relay fine (via podman) using using the NB_AUTH_SECRET. It doesn't seem to require an IdP.

Rework the ingress proxy
Now we can introduce the netbird reverse proxy service feature

Can't we just start with something small? let the netbird-proxy start under systemd, and let users configure their reverse proxies? It could be explained in the markdown.
Being traefik the netbird's "choice" for reverse-proxies (as proxy only works with it), it feels weird having a mix of many reverse-proxies (enableNginx, enableTraefik, etc).

Shouldn't we follow a more single-responsability-principle and focus on netbird only? And start with systemd, and if later it's needed, we can add the modular service, but at least we would have a clear and well-defined interface.

Maybe in the future, if RFC 189 is ever approved, we'll be able to specify "we need any reverse proxy with x and y features".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: documentation This PR adds or changes documentation 8.has: maintainer-list (update) This PR changes `maintainers/maintainer-list.nix` 8.has: module (update) This PR changes an existing module in `nixos/` 10.rebuild-darwin: 0 This PR does not cause any packages to rebuild on Darwin. 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux. 12.first-time contribution This PR is the author's first one; please be gentle!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants