From 9961802917d359623815dea434bfb58653ca1fc2 Mon Sep 17 00:00:00 2001 From: Levente Farkas Date: Sat, 8 Aug 2026 15:52:57 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Add=20profile=20option=20to=20s?= =?UTF-8?q?witch=20NetBird=20profiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #423. A NetBird profile is simply its own config file: each one holds a separate peer identity, management URL and credentials. The add-on hardcoded a single config path, so it could only ever join one network. Add a `profile` option that selects `/config/profiles/.json`. Leaving it empty keeps the existing `/config/config.json`, so current installs are unaffected. Home Assistant can switch networks by changing the option and restarting the add-on; DOCS.md shows the rest_command/script to do that. The upstream `netbird profile` commands are not an option here: they dial the daemon over gRPC, which `netbird up --foreground-mode` never starts. The schema wraps the name grammar in an optional group so the stored empty default still validates (same shape as `hostname` after #411), and the run script re-validates the name before it becomes a file path. Co-Authored-By: Claude Opus 5 (1M context) --- netbird/DOCS.md | 77 +++++++++++++++++++ netbird/config.yaml | 2 + .../rootfs/etc/s6-overlay/s6-rc.d/netbird/run | 27 ++++++- netbird/translations/en.yaml | 11 +++ 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/netbird/DOCS.md b/netbird/DOCS.md index 53247d9..81996c8 100644 --- a/netbird/DOCS.md +++ b/netbird/DOCS.md @@ -75,6 +75,24 @@ Hostname in the NetBird network (used to during registration) This hostname will be used in the Peers to identify your machine. +### Option: `profile` + +Name of the profile to connect with (default: the single, unnamed profile) + +A profile is a separate NetBird configuration: each one keeps its own peer +identity and credentials, so switching the profile makes this client join a +different NetBird network or account. Leave the option empty to keep using the +single default configuration at `/addon_config/*_netbird/config.json`; set it to +a name (letters, digits, `-` and `_`) and the client uses +`/addon_config/*_netbird/profiles/.json` instead. + +A profile that has not been registered yet needs a `setup_key`, or the login URL +printed in the log, the first time it starts. Once registered, a profile keeps +working when you switch away and back again. + +See [Switching profiles from Home Assistant](#switching-profiles-from-home-assistant) +for changing profiles from an automation. + ### Option: `rosenpass` Rosenpass can be enabled by setting a flag on client start-up. @@ -97,6 +115,65 @@ Extra environment variables to pass to the NetBird client. This is a list of environment variables that will be passed to the NetBird client. You can use this to configure the client further. +## Switching profiles from Home Assistant + +Home Assistant can switch profiles by changing the `profile` option and +restarting the add-on. Add the following to `configuration.yaml`, replacing +`xxxxxxxx_netbird` with the slug of your installation (visible in the address bar +when the add-on page is open): + +```yaml +rest_command: + netbird_info: + url: http://supervisor/addons/xxxxxxxx_netbird/info + method: get + headers: + authorization: !env_var SUPERVISOR_TOKEN + netbird_set_options: + url: http://supervisor/addons/xxxxxxxx_netbird/options + method: post + content_type: application/json + headers: + authorization: !env_var SUPERVISOR_TOKEN + payload: '{{ {"options": options} | to_json }}' + netbird_restart: + url: http://supervisor/addons/xxxxxxxx_netbird/restart + method: post + headers: + authorization: !env_var SUPERVISOR_TOKEN + +script: + netbird_switch_profile: + alias: Switch NetBird profile + fields: + profile: + description: Profile to connect with, or empty for the default profile. + example: work + sequence: + - action: rest_command.netbird_info + response_variable: info + - action: rest_command.netbird_set_options + data: + options: "{{ info.content.data.options | combine({'profile': profile}) }}" + - action: rest_command.netbird_restart +``` + +Then call `script.netbird_switch_profile` with the profile you want: + +```yaml +action: script.netbird_switch_profile +data: + profile: work +``` + +Keep the `netbird_info` step. The Supervisor **replaces** the stored options with +whatever is posted, so sending only the profile would reset every other option +(including your `setup_key`) to its default. Reading the current options first +and merging the new profile into them avoids that. + +Switching restarts the add-on, so the connection drops for a few seconds and all +peers see the machine go offline and come back under its new identity. + ## Changelog & Releases This repository keeps a change log using [GitHub's releases][releases] diff --git a/netbird/config.yaml b/netbird/config.yaml index 09e3543..a6379eb 100644 --- a/netbird/config.yaml +++ b/netbird/config.yaml @@ -27,6 +27,7 @@ options: management_url: "" setup_key: "" hostname: "" + profile: "" rosenpass: false rosenpass_permissive: false env_vars: [] @@ -35,6 +36,7 @@ schema: management_url: str? setup_key: str? hostname: match(^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*)?$)? + profile: match(^([a-zA-Z0-9_-]{1,64})?$)? rosenpass: bool rosenpass_permissive: bool env_vars: diff --git a/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run b/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run index b4ba634..829eef6 100755 --- a/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run +++ b/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run @@ -11,18 +11,41 @@ declare value # Get the options configured in HASS GUI readonly CONFIG_OLD_PATH=/homeassistant/netbird/config.json -readonly CONFIG_PATH=/config/config.json +readonly CONFIG_DEFAULT_PATH=/config/config.json +readonly PROFILES_DIR=/config/profiles -[ -f "${CONFIG_OLD_PATH}" ] && mv "${CONFIG_OLD_PATH}" "${CONFIG_PATH}" +[ -f "${CONFIG_OLD_PATH}" ] && mv "${CONFIG_OLD_PATH}" "${CONFIG_DEFAULT_PATH}" admin_url="$(bashio::config 'admin_url')" management_url="$(bashio::config 'management_url')" setup_key="$(bashio::config 'setup_key')" hostname="$(bashio::config 'hostname')" +profile="$(bashio::config 'profile')" rosenpass="$(bashio::config 'rosenpass')" rosenpass_permissive="$(bashio::config 'rosenpass_permissive')" log_level="$(bashio::config 'log_level')" +# Select the profile to run (see issue #423). +# A NetBird profile is simply its own config file: each one holds a separate +# peer identity, management URL and credentials, so pointing the client at a +# different config file switches the whole account/network it joins. Leaving +# the option empty keeps using the single default config, so existing installs +# are unaffected. +if [ "${profile}" = "" ] || [ "${profile}" = "null" ]; then + bashio::log.info "No profile set, using the default profile" + CONFIG_PATH="${CONFIG_DEFAULT_PATH}" +else + # The profile name becomes part of a file path, so re-validate it here + # instead of relying on the options schema alone. + if [[ ! "${profile}" =~ ^[a-zA-Z0-9_-]{1,64}$ ]]; then + bashio::exit.nok "Invalid profile name: ${profile} (allowed: letters, digits, '-' and '_', up to 64 characters)" + fi + bashio::log.info "Using ${profile} as profile" + mkdir -p "${PROFILES_DIR}" + CONFIG_PATH="${PROFILES_DIR}/${profile}.json" +fi +readonly CONFIG_PATH + # Self-heal a stale NetBird Cloud management URL (see issue #401). # Older installs persisted https://app.netbird.io as the Management URL in # config.json. app.netbird.io only serves the web dashboard and does not diff --git a/netbird/translations/en.yaml b/netbird/translations/en.yaml index 8f7cecc..826f468 100644 --- a/netbird/translations/en.yaml +++ b/netbird/translations/en.yaml @@ -29,6 +29,17 @@ configuration: description: >- Hostname of the client (default "netbird-client") This is the name of the client that will be displayed in the NetBird dashboard. + profile: + name: Profile + description: >- + Name of the profile to connect with (default: the single, unnamed profile) + + Every profile keeps its own peer identity and credentials in its own + configuration file, so switching the profile makes the add-on join a + different NetBird network or account. Changing this option and restarting + the add-on switches profiles, which also lets Home Assistant automations + switch networks. A profile that has not been registered yet needs a + `setup_key`, or the login URL printed in the log, on its first start. rosenpass: name: Rosenpass description: >- From a0e61cebb8e30d45ef963e8f00e52cedb9bf7f14 Mon Sep 17 00:00:00 2001 From: Levente Farkas Date: Sat, 8 Aug 2026 15:59:15 +0200 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9B=20Reject=20"null"=20as=20a=20p?= =?UTF-8?q?rofile=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bashio prints "null" both for an option that is not set and for the literal string, and cannot tell them apart — even bashio::config.exists conflates the two. A profile named "null" would therefore have silently fallen back to the default config and joined the wrong network with no error. The script cannot resolve this, so exclude the name at the schema level, which is the layer that can. Only the exact lowercase string is excluded: "nullx", "null_1" and "NULL" stay valid, matching the script's case-sensitive compare. Co-Authored-By: Claude Opus 5 (1M context) --- netbird/config.yaml | 2 +- netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/netbird/config.yaml b/netbird/config.yaml index a6379eb..d2513f7 100644 --- a/netbird/config.yaml +++ b/netbird/config.yaml @@ -36,7 +36,7 @@ schema: management_url: str? setup_key: str? hostname: match(^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*)?$)? - profile: match(^([a-zA-Z0-9_-]{1,64})?$)? + profile: match(^(?!null$)([a-zA-Z0-9_-]{1,64})?$)? rosenpass: bool rosenpass_permissive: bool env_vars: diff --git a/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run b/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run index 829eef6..c4ce11b 100755 --- a/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run +++ b/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run @@ -31,6 +31,9 @@ log_level="$(bashio::config 'log_level')" # different config file switches the whole account/network it joins. Leaving # the option empty keeps using the single default config, so existing installs # are unaffected. +# bashio prints "null" for an option that is not set, and cannot tell that +# apart from the literal string; the schema rejects "null" as a profile name so +# the two can never collide here. if [ "${profile}" = "" ] || [ "${profile}" = "null" ]; then bashio::log.info "No profile set, using the default profile" CONFIG_PATH="${CONFIG_DEFAULT_PATH}" From 4bc58754ca953868518f68f183550e652682e3bb Mon Sep 17 00:00:00 2001 From: Levente Farkas Date: Sat, 8 Aug 2026 16:26:24 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B=20Never=20migrate=20the=20lega?= =?UTF-8?q?cy=20config=20over=20an=20existing=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-addon_config migration moved /homeassistant/netbird/config.json with an unconditional mv, so if both files existed it overwrote the current default config — discarding the live peer identity and credentials. Skip the move when the destination exists (warning, legacy file left in place for manual recovery) and fail loudly if the move itself fails, rather than starting up as an unregistered peer. Pre-existing behaviour, reported by CodeRabbit on #426 and fixed here because this change already touches that line. Co-Authored-By: Claude Opus 5 (1M context) --- netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run b/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run index c4ce11b..898f9e6 100755 --- a/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run +++ b/netbird/rootfs/etc/s6-overlay/s6-rc.d/netbird/run @@ -14,7 +14,15 @@ readonly CONFIG_OLD_PATH=/homeassistant/netbird/config.json readonly CONFIG_DEFAULT_PATH=/config/config.json readonly PROFILES_DIR=/config/profiles -[ -f "${CONFIG_OLD_PATH}" ] && mv "${CONFIG_OLD_PATH}" "${CONFIG_DEFAULT_PATH}" +# Migrate the config from the pre-addon_config location, but never on top of an +# existing one: that would discard the current peer identity and credentials. +if [ -f "${CONFIG_OLD_PATH}" ]; then + if [ -f "${CONFIG_DEFAULT_PATH}" ]; then + bashio::log.warning "Ignoring ${CONFIG_OLD_PATH}: ${CONFIG_DEFAULT_PATH} already exists" + elif ! mv "${CONFIG_OLD_PATH}" "${CONFIG_DEFAULT_PATH}"; then + bashio::exit.nok "Could not migrate ${CONFIG_OLD_PATH} to ${CONFIG_DEFAULT_PATH}" + fi +fi admin_url="$(bashio::config 'admin_url')" management_url="$(bashio::config 'management_url')" From 0ad394056db9c5b35906e5e7451b836b7efaaca9 Mon Sep 17 00:00:00 2001 From: Levente Farkas Date: Sat, 8 Aug 2026 16:26:24 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20Harden=20the=20documented=20?= =?UTF-8?q?profile-switching=20automation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the #426 review: - Use the X-Supervisor-Token header instead of Authorization. A raw token in Authorization does work (Supervisor takes the last space-separated field), but this is the purpose-built header and removes the ambiguity. - Guard both calls on a 200 before restarting. A rest_command that receives an error response logs a warning and does not fail the script, so a rejected options update was still followed by a restart that came back on the old profile. - Warn that the posted payload carries setup_key. Home Assistant logs it at warning level when a call fails, and logs headers too (including the token) at debug level. - Document the 64-character limit and the reserved name null. - Describe the add-on config directory correctly: /config inside the add-on, exposed as /addon_configs/ (plural, one directory per slug). Co-Authored-By: Claude Opus 5 (1M context) --- netbird/DOCS.md | 40 +++++++++++++++++++++++++++++------- netbird/translations/en.yaml | 3 +++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/netbird/DOCS.md b/netbird/DOCS.md index 81996c8..512ab11 100644 --- a/netbird/DOCS.md +++ b/netbird/DOCS.md @@ -28,7 +28,12 @@ comparison to installing any other Home Assistant add-on. ## Configuration -You'll see the config file at `/addon_config/*_netbird/config.json` after first boot. +After the first boot you'll see the NetBird configuration in this add-on's own +configuration directory: `/config` as seen from inside the add-on, which Home +Assistant exposes to you as `/addon_configs/xxxxxxxx_netbird`, where +`xxxxxxxx_netbird` is the slug of your installation (visible in the address bar +when the add-on page is open). It holds `config.json`, plus a `profiles` +directory if you use the [`profile`](#option-profile) option. ### Option: `log_level` @@ -82,9 +87,13 @@ Name of the profile to connect with (default: the single, unnamed profile) A profile is a separate NetBird configuration: each one keeps its own peer identity and credentials, so switching the profile makes this client join a different NetBird network or account. Leave the option empty to keep using the -single default configuration at `/addon_config/*_netbird/config.json`; set it to -a name (letters, digits, `-` and `_`) and the client uses -`/addon_config/*_netbird/profiles/.json` instead. +single default configuration at `config.json`; set it to a name and the client +uses `profiles/.json` instead. Both paths are relative to the add-on +configuration directory described under [Configuration](#configuration). + +A name may be up to 64 characters of letters, digits, `-` and `_`. The name +`null` is not allowed, because the add-on cannot tell it apart from an unset +option. A profile that has not been registered yet needs a `setup_key`, or the login URL printed in the log, the first time it starts. Once registered, a profile keeps @@ -128,19 +137,19 @@ rest_command: url: http://supervisor/addons/xxxxxxxx_netbird/info method: get headers: - authorization: !env_var SUPERVISOR_TOKEN + x-supervisor-token: !env_var SUPERVISOR_TOKEN netbird_set_options: url: http://supervisor/addons/xxxxxxxx_netbird/options method: post content_type: application/json headers: - authorization: !env_var SUPERVISOR_TOKEN + x-supervisor-token: !env_var SUPERVISOR_TOKEN payload: '{{ {"options": options} | to_json }}' netbird_restart: url: http://supervisor/addons/xxxxxxxx_netbird/restart method: post headers: - authorization: !env_var SUPERVISOR_TOKEN + x-supervisor-token: !env_var SUPERVISOR_TOKEN script: netbird_switch_profile: @@ -152,9 +161,14 @@ script: sequence: - action: rest_command.netbird_info response_variable: info + - condition: template + value_template: "{{ info.status == 200 }}" - action: rest_command.netbird_set_options + response_variable: updated data: options: "{{ info.content.data.options | combine({'profile': profile}) }}" + - condition: template + value_template: "{{ updated.status == 200 }}" - action: rest_command.netbird_restart ``` @@ -171,6 +185,18 @@ whatever is posted, so sending only the profile would reset every other option (including your `setup_key`) to its default. Reading the current options first and merging the new profile into them avoids that. +Keep the two `condition` steps as well. A `rest_command` that gets an error +response logs a warning but does not fail the script, so without them a rejected +options update would still be followed by a restart, quietly bringing the add-on +back up on the *old* profile. With them, the script stops instead and the add-on +keeps running untouched. + +Because the whole options map has to be posted back, the payload contains your +`setup_key`. Home Assistant logs that payload at warning level whenever one of +these calls fails, and logs the payload and headers (including the Supervisor +token) if you set `homeassistant.components.rest_command` to `debug`. Treat +those logs as secrets, and scrub them before sharing them. + Switching restarts the add-on, so the connection drops for a few seconds and all peers see the machine go offline and come back under its new identity. diff --git a/netbird/translations/en.yaml b/netbird/translations/en.yaml index 826f468..052ff61 100644 --- a/netbird/translations/en.yaml +++ b/netbird/translations/en.yaml @@ -40,6 +40,9 @@ configuration: the add-on switches profiles, which also lets Home Assistant automations switch networks. A profile that has not been registered yet needs a `setup_key`, or the login URL printed in the log, on its first start. + + Up to 64 characters of letters, digits, `-` and `_`. The name `null` is + not allowed, as it cannot be told apart from an unset option. rosenpass: name: Rosenpass description: >-