Merge remote-tracking branch 'origin/main' into docs-lazy-conn-env

# Conflicts:
#	src/pages/client/environment-variables.mdx
#	src/pages/manage/peers/lazy-connection.mdx
This commit is contained in:
Viktor Liu
2026-08-05 19:40:22 +02:00
198 changed files with 17779 additions and 2476 deletions

View File

@@ -1,10 +1,16 @@
.git/
.gitignore
.next/ # Existing Next.js builds
.dockerignore
Dockerfile
docker-compose.yml
node_modules/ # Installed inside container
LICENSE
README.md
AUTHORS
AUTHORS
# Root deps aren't shipped — the image uses .next/standalone's traced
# node_modules. Root-anchored so the nested standalone node_modules is kept.
/node_modules/
# The built .next output is copied in (built on the CI runner, see
# build_n_push.yml); only the build cache is excluded — it is not served.
.next/cache/

View File

@@ -3,31 +3,115 @@ on:
push:
branches:
- main
# Build-only (no push) on PRs that touch the build/deploy pipeline, so
# Dockerfile / workflow changes are validated before they reach main.
pull_request:
paths:
- 'docker/**'
- '.dockerignore'
- '.github/workflows/build_n_push.yml'
- 'next.config.mjs'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
# Serialise non-PR runs while allowing superseded PR validation to be canceled.
# Builds publish only an immutable SHA tag; the mutable ref tag is promoted
# separately after the build and a final ref check.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
docs_build_n_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
-
name: Docker meta
- uses: actions/checkout@v4
with:
# Full history so gen:last-updated / gen:sitemap can read real
# per-file commit dates (a shallow clone would yield wrong dates).
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Restore Next.js build cache
uses: actions/cache@v4
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**', 'mdx/**', 'next.config.mjs') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-
- name: Build
run: npm run build
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v5
with:
images: netbirdio/docs.netbird.io
-
name: Login to DockerHub
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
-
name: Docker build and push
uses: docker/build-push-action@v2
# Non-PR runs publish an immutable image first. A canceled or stale run
# can leave this tag behind, but it cannot change :main (or another
# mutable ref tag).
- name: Docker build and publish immutable image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
# Keep a single-arch manifest (no attestation index) so the server's
# `docker compose pull` stays happy.
provenance: false
tags: netbirdio/docs.netbird.io:${{ github.sha }}
labels: ${{ steps.meta.outputs.labels }}
# Promotion is a single remote manifest-tag update after the expensive
# build. Combined with serialisation and the final ref comparison, an
# older run cannot replace the mutable tag after a newer run publishes.
- name: Promote current image to ref tag
if: github.event_name != 'pull_request'
env:
SOURCE_IMAGE: netbirdio/docs.netbird.io:${{ github.sha }}
TARGET_TAGS: ${{ steps.meta.outputs.tags }}
run: |
remote_sha="$(git ls-remote origin "${GITHUB_REF}" | awk 'NR == 1 { print $1 }')"
if [ -z "$remote_sha" ]; then
echo "Could not resolve ${GITHUB_REF} on origin" >&2
exit 1
fi
if [ "$GITHUB_SHA" != "$remote_sha" ]; then
echo "Skipping tag promotion: ${GITHUB_SHA} is stale; ${GITHUB_REF} is now ${remote_sha}"
exit 0
fi
if [ -z "$TARGET_TAGS" ]; then
echo "Docker metadata produced no target tags" >&2
exit 1
fi
while IFS= read -r target_tag; do
if [ -n "$target_tag" ]; then
docker buildx imagetools create \
--prefer-index=false \
--tag "$target_tag" \
"$SOURCE_IMAGE"
fi
done <<< "$TARGET_TAGS"

View File

@@ -8,6 +8,19 @@ on:
default: "refs/tags/vX.Y.Z"
type: string
permissions:
actions: read
contents: read
# One run at a time: overlapping dispatches (several release tags in a day
# happen — see run history) regenerate the same files and would collide. A
# queued run superseded by a newer dispatch is fine: every run regenerates the
# whole directory from its own tag, so the latest dispatch is the end state.
# NB "latest dispatched", not "newest tag" — re-dispatching an older tag after
# a newer one regresses the pages to the older spec.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
generate_api_pages:
runs-on: macos-latest
@@ -23,6 +36,16 @@ jobs:
with:
token: ${{ secrets.DEV_GITHUB_TOKEN }}
# A run queued behind another (see concurrency above) checks out the
# commit pinned at its dispatch time, not the branch's current tip.
# Sync before generating so the diff is computed against reality —
# otherwise the pre-push rebase replays a stale-base snapshot, and a
# file the newer spec removed could silently survive from the prior run.
- name: Sync to branch tip
run: |
git fetch origin "${GITHUB_REF_NAME}"
git reset --hard "origin/${GITHUB_REF_NAME}"
- name: Create directory
run: mkdir -p generator/openapi
@@ -43,8 +66,17 @@ jobs:
- name: Remove old generated files
run: rm -rf src/pages/ipa/resources/*
- name: Npm install
run: npm install
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# npm ci: this workflow never changes dependencies, so install exactly
# the committed lockfile and never mutate it (macos-latest's npm can
# differ from the one that generated package-lock.json).
- name: Install dependencies
run: npm ci
- name: Generate api pages for netbird main openapi definition
run: npx ts-node generator/index.ts gen --input generator/openapi/expanded.yml --output src/pages/ipa/resources
@@ -52,18 +84,53 @@ jobs:
- name: Check git diff and untracked files
id: git_diff
run: |
if [ -n "$(git status --porcelain)" ]; then
if [ -n "$(git status --porcelain src/pages/ipa/resources)" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Commit and push changes
# Concurrency serialises runs but does not guarantee dispatch order. A
# delayed older run must not overwrite output from a newer dispatch.
- name: Check whether this is the latest dispatch
id: freshness
if: steps.git_diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
latest_run_id="$(
curl --fail --silent --show-error \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
--get \
--data-urlencode "event=workflow_dispatch" \
--data-urlencode "branch=${GITHUB_REF_NAME}" \
--data-urlencode "per_page=1" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/generate_api_pages.yml/runs" \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["workflow_runs"][0]["id"])'
)"
if [ "$GITHUB_RUN_ID" = "$latest_run_id" ]; then
echo "push=true" >> "$GITHUB_OUTPUT"
else
echo "push=false" >> "$GITHUB_OUTPUT"
echo "Skipping generated-page commit: run ${GITHUB_RUN_ID} was superseded by ${latest_run_id}"
fi
- name: Commit and push changes
if: steps.git_diff.outputs.changed == 'true' && steps.freshness.outputs.push == 'true'
run: |
git config --global user.email "dev@netbird.io"
git config --global user.name "netbirddev"
git add -A
# Stage only the regenerated API pages — never sweep up incidental
# changes like a rewritten package-lock.json.
git add src/pages/ipa/resources
git commit -m "Update API pages with v${{ steps.semver_parser.outputs.fullversion }}"
git push --force
# The run takes minutes; if the branch moved meanwhile, replay our
# single generated-files commit on top instead of failing the push.
# A conflict is only possible against a concurrent edit of the
# generated files themselves and fails the run loudly.
git pull --rebase origin "${GITHUB_REF_NAME}"
git push

View File

@@ -22,9 +22,10 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm install
run: npm ci
- name: Lint MDX heading hierarchy
run: npm run lint:mdx

5
.gitignore vendored
View File

@@ -21,7 +21,6 @@ yarn-error.log*
.idea
package-lock.json
/.next/
/yarn.lock
/generator/openapi/
@@ -29,10 +28,6 @@ package-lock.json
/generator/expandOpenAPIRef
/generator/tsconfig.tsbuildinfo
# LLM documentation (generated)
/public/llms/
/public/llms.txt
# Sitemap (generated by scripts/generate-sitemap.mjs)
/public/sitemap.xml

View File

@@ -8,16 +8,17 @@ Documentation website for [NetBird](https://netbird.io), an open-source WireGuar
There is no test suite in this project. Validate changes with `npm run build`.
`package-lock.json` is committed, so installs are pinned. When you change dependencies in `package.json`, regenerate the lockfile (`npm install`) and commit it in the same change — otherwise local, CI, and Docker builds resolve different trees.
## Common Commands
```bash
npm install # Install dependencies
npm run dev # Start dev server (also runs gen:llm, gen:edit-routes, gen:last-updated, gen:sitemap)
npm run build # Production build (also runs gen:llm, gen:edit-routes, gen:last-updated, gen:sitemap)
npm run start # Serve the production build
npm run dev # Start dev server (also runs gen:edit-routes, gen:last-updated, gen:sitemap)
npm run build # Production build (also runs gen:edit-routes, gen:last-updated, gen:sitemap)
npm run start # Serve the production build (warns under `output: 'standalone'` — safe to ignore locally; prod runs `node server.js` from `.next/standalone`)
npm run lint # ESLint (next/core-web-vitals) on src/
npm run gen # Regenerate API docs from NetBird OpenAPI spec
npm run gen:llm # Regenerate LLM-friendly markdown (auto-runs with dev/build)
npm run gen:edit-routes # Regenerate edit-on-GitHub routes (auto-runs with dev/build)
npm run gen:last-updated # Regenerate per-page git last-modified dates (auto-runs with dev/build)
npm run gen:sitemap # Regenerate public/sitemap.xml (auto-runs with dev/build)
@@ -74,10 +75,6 @@ Custom components available in MDX files (see `README.md` for full usage example
- `mdx/rehype.mjs` - Rehype plugins (syntax highlighting via Shiki)
- `mdx/recma.mjs` - Recma plugins
### LLM Documentation
- `scripts/generate-llm-docs.mjs` generates clean markdown to `public/llms/` (gitignored)
- Runs automatically with `dev` and `build`
### Generated build data in `src/lib/`
Some files in `src/lib/` look hand-written but are generated by `gen:*` scripts and gitignored:
- `src/lib/edit-on-github-routes.js` — written by `scripts/generate-github-routes.mjs`

View File

@@ -1,32 +1,50 @@
FROM node:20-alpine
FROM node:20-slim
# tini runs as PID 1 and forwards signals to node (which doesn't install its
# own SIGTERM handler, and as PID 1 would otherwise ignore it) — so
# `docker stop` terminates in ~1s instead of waiting out the 10s kill grace.
RUN apt-get update \
&& apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /usr/app
# Install PM2 globally
RUN npm install --global pm2
ENV NODE_ENV=production
# Next's standalone server binds to localhost by default; listen on all
# interfaces inside the container, and pin the port.
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
# Copy package.json and package-lock.json before other files
# Utilise Docker cache to save re-installing dependencies if unchanged
COPY ./package*.json ./
# Next's standalone output (built on the CI runner, see build_n_push.yml)
# bundles a minimal, traced node_modules plus server.js — there is nothing to
# install here. The output is traced against the runner (Ubuntu/glibc), so this
# runtime image must also be glibc (node:20-slim, NOT alpine/musl) or the traced
# native binaries won't load.
#
# Files are chowned to the non-root `node` user because entrypoint.sh rewrites
# the DocSearch placeholders in .next with `sed -i` at runtime — that needs
# write access under the runtime user.
COPY --chown=node:node .next/standalone ./
# standalone does not include static assets or the public dir — copy them in.
COPY --chown=node:node .next/static ./.next/static
COPY --chown=node:node public ./public
# Install dependencies
RUN npm install --production
COPY --chown=node:node docker/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
# Copy all files
COPY ./ ./
# Run as the base image's built-in non-root user (UID 1000). Port 3000 is
# unprivileged, so no extra capability is needed.
USER node
COPY /docker/entrypoint.sh ./entrypoint.sh
# Build app
RUN npm run build
RUN chmod u+x /usr/app/entrypoint.sh
# Expose the listening port
EXPOSE 3000
# apply env variables to the Nextjs .env file
ENTRYPOINT ["/usr/app/entrypoint.sh"]
# Surfaces crash-loops and dead servers in `docker ps` / compose --wait /
# Watchtower instead of them sitting silently "Up".
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD npm run start
# entrypoint.sh substitutes the APP_NEXT_PUBLIC_DOCSEARCH_* placeholders baked
# into .next with real values from the container env, then execs the CMD.
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/app/entrypoint.sh"]
CMD ["node", "server.js"]

View File

@@ -1,17 +1,47 @@
#!/bin/sh
# this script will check for the following NEXT_* environment variables passed via Docker environment (-e) and apply them
# to the Nextjs.
# The properties that will be replaced and have to start with APP_ prefix in the .env file
# Substitutes the APP_NEXT_PUBLIC_DOCSEARCH_* placeholders baked into the
# client bundle at build time (from the committed .env) with the real values
# passed via the container environment, then starts the server.
#
# NEXT_PUBLIC_* values are compiled into the client bundle, so this rewrite is
# what lets one image serve any environment's DocSearch credentials.
set -eu
set -ex
NEXT_PUBLIC_DOCSEARCH_APP_ID=${NEXT_PUBLIC_DOCSEARCH_APP_ID:-"none"}
NEXT_PUBLIC_DOCSEARCH_API_KEY=${NEXT_PUBLIC_DOCSEARCH_API_KEY:-"none"}
NEXT_PUBLIC_DOCSEARCH_INDEX_NAME=${NEXT_PUBLIC_DOCSEARCH_INDEX_NAME:"none"}
NEXT_PUBLIC_DOCSEARCH_INDEX_NAME=${NEXT_PUBLIC_DOCSEARCH_INDEX_NAME:-"none"}
find /usr/app/.next \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#APP_NEXT_PUBLIC_DOCSEARCH_APP_ID#${NEXT_PUBLIC_DOCSEARCH_APP_ID}#g"
find /usr/app/.next \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#APP_NEXT_PUBLIC_DOCSEARCH_API_KEY#${NEXT_PUBLIC_DOCSEARCH_API_KEY}#g"
find /usr/app/.next \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#APP_NEXT_PUBLIC_DOCSEARCH_INDEX_NAME#${NEXT_PUBLIC_DOCSEARCH_INDEX_NAME}#g"
# Escape the characters that are special in a sed replacement (\ and &) and
# our s### delimiter (#), so values containing them substitute literally.
# CR/LF are stripped first: a one-line s### command cannot carry a raw
# newline, and no legitimate DocSearch token contains one.
escape() {
printf '%s' "$1" | tr -d '\r\n' | sed -e 's/[\\&#]/\\&/g'
}
echo "starting Nextjs"
exec "$@"
# Rewrite only the files that still contain the placeholder — after the first
# boot substituted everything, restarts touch nothing. grep exiting 1 on zero
# matches is fine: xargs -r then runs nothing and the pipeline succeeds.
substitute() {
grep -rlZ "$1" /usr/app/.next | xargs -0 -r sed -i "s#$1#$2#g"
}
# Each substitution runs independently: one failing value must not stop the
# remaining placeholders from being applied.
ok=1
substitute APP_NEXT_PUBLIC_DOCSEARCH_APP_ID "$(escape "$NEXT_PUBLIC_DOCSEARCH_APP_ID")" || ok=0
substitute APP_NEXT_PUBLIC_DOCSEARCH_API_KEY "$(escape "$NEXT_PUBLIC_DOCSEARCH_API_KEY")" || ok=0
substitute APP_NEXT_PUBLIC_DOCSEARCH_INDEX_NAME "$(escape "$NEXT_PUBLIC_DOCSEARCH_INDEX_NAME")" || ok=0
if [ "$ok" = 1 ]; then
echo "DocSearch configuration applied"
else
# Serve the docs even if search wiring failed — a docs site with broken
# search beats a crash-looping container. The warning makes it visible.
echo "WARNING: DocSearch placeholder substitution failed; search may be broken" >&2
fi
echo "starting Next.js"
exec "$@"

View File

@@ -16,6 +16,10 @@ const withMDX = nextMDX({
/** @type {import('next').NextConfig} */
const nextConfig = {
// Emit a self-contained server (.next/standalone) with a traced, minimal
// node_modules, so the Docker image ships just the server + static assets
// instead of the full source and dependency tree.
output: 'standalone',
assetPrefix: undefined,
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
@@ -23,7 +27,7 @@ const nextConfig = {
return [
{
source: '/slack-url',
destination: 'https://join.slack.com/t/netbirdio/shared_invite/zt-3wwkb0b9y-opbG_pFSoOgP41KUV00MtA',
destination: 'https://join.slack.com/t/netbirdio/shared_invite/zt-43j76787p-otf3d0nMcJHAQYx46H3jsg',
permanent: false,
},
{
@@ -38,7 +42,7 @@ const nextConfig = {
},
{
source: '/manage/network-routes/use-cases/by-scenario/site-to-vpn',
destination: '/manage/networks/use-cases/site-to-vpn',
destination: '/use-cases/remote-access/site-to-vpn',
permanent: true,
},
{
@@ -99,7 +103,16 @@ const nextConfig = {
{
source: '/ipa/:path*',
destination: '/api/:path*',
permanent: true
permanent: true,
// Client-side navigations fetch page props from
// /_next/data/<buildId>/ipa/... — redirecting those requests
// strips pageProps (title, sections), so skip data requests.
missing: [
{
type: 'header',
key: 'x-nextjs-data',
},
],
},
// documentation redirects for about
{
@@ -283,12 +296,12 @@ const nextConfig = {
},
{
source: '/how-to/routing-peers-and-kubernetes',
destination: '/use-cases/cloud/routing-peers-and-kubernetes',
destination: '/use-cases/kubernetes/routing-peers-and-kubernetes',
permanent: true,
},
{
source: '/how-to/client-on-mikrotik-router',
destination: '/use-cases/homelab/client-on-mikrotik-router',
destination: '/get-started/install/mikrotik',
permanent: true,
},
{
@@ -319,7 +332,7 @@ const nextConfig = {
},
{
source: '/how-to/access-home-network',
destination: '/manage/networks/use-cases/access-home-devices',
destination: '/use-cases/remote-access/access-home-devices',
permanent: true,
},
// documentation redirects for network-routes
@@ -330,7 +343,7 @@ const nextConfig = {
},
{
source: '/how-to/configuring-default-routes-for-internet-traffic',
destination: '/manage/network-routes/use-cases/exit-nodes',
destination: '/use-cases/remote-access/exit-nodes',
permanent: true,
},
{
@@ -499,7 +512,7 @@ const nextConfig = {
},
{
source: '/how-to/kubernetes-operator',
destination: '/manage/integrations/kubernetes',
destination: '/use-cases/kubernetes',
permanent: true,
},
// documentation redirects for public-api
@@ -551,10 +564,101 @@ const nextConfig = {
destination: '/manage/dns/custom-zones',
permanent: true,
},
// Use cases reorg: scenario guides consolidated under /use-cases
{
source: '/manage/integrations/kubernetes',
destination: '/use-cases/kubernetes',
permanent: true,
},
{
source: '/manage/integrations/kubernetes/routing-peer',
destination: '/use-cases/kubernetes/routing-peer',
permanent: true,
},
{
source: '/manage/integrations/kubernetes/client-sidecar',
destination: '/use-cases/kubernetes/client-sidecar',
permanent: true,
},
{
source: '/manage/integrations/kubernetes/api-server-proxy',
destination: '/use-cases/kubernetes/api-server-proxy',
permanent: true,
},
{
source: '/manage/integrations/kubernetes/gateway-api',
destination: '/use-cases/kubernetes/gateway-api',
permanent: true,
},
{
source: '/use-cases/cloud/route-to-a-kubernetes-service',
destination: '/use-cases/kubernetes/route-to-a-kubernetes-service',
permanent: true,
},
{
source: '/use-cases/cloud/routing-peers-and-kubernetes',
destination: '/use-cases/kubernetes/routing-peers-and-kubernetes',
permanent: true,
},
{
source: '/use-cases/site-to-site',
destination: '/use-cases/remote-access',
permanent: true,
},
{
source: '/manage/networks/use-cases/site-to-site',
destination: '/use-cases/remote-access/site-to-site',
permanent: true,
},
{
source: '/manage/networks/use-cases/site-to-vpn',
destination: '/use-cases/remote-access/site-to-vpn',
permanent: true,
},
{
source: '/manage/networks/use-cases/cloud-to-on-premise',
destination: '/use-cases/remote-access/cloud-to-on-premise',
permanent: true,
},
{
source: '/manage/networks/use-cases/access-home-devices',
destination: '/use-cases/remote-access/access-home-devices',
permanent: true,
},
{
source: '/manage/networks/use-cases/reach-services-on-the-routing-peer',
destination: '/use-cases/remote-access/reach-services-on-the-routing-peer',
permanent: true,
},
{
source: '/manage/networks/use-cases/active-directory',
destination: '/use-cases/remote-access/active-directory',
permanent: true,
},
{
source: '/manage/network-routes/use-cases/exit-nodes',
destination: '/use-cases/remote-access/exit-nodes',
permanent: true,
},
{
source: '/manage/integrations/kubernetes/use-cases/route-to-a-kubernetes-service',
destination: '/use-cases/kubernetes/route-to-a-kubernetes-service',
permanent: true,
},
{
source: '/manage/reverse-proxy/use-cases/private-no-inbound',
destination: '/use-cases/security/private-no-inbound',
permanent: true,
},
{
source: '/use-cases/homelab/client-on-mikrotik-router',
destination: '/get-started/install/mikrotik',
permanent: true,
},
// Site-to-site documentation restructure redirects
{
source: '/use-cases/setup-site-to-site-access',
destination: '/use-cases/site-to-site',
destination: '/use-cases/remote-access',
permanent: true,
},
{
@@ -569,17 +673,17 @@ const nextConfig = {
},
{
source: '/manage/networks/homelab/access-home-network',
destination: '/manage/networks/use-cases/access-home-devices',
destination: '/use-cases/remote-access/access-home-devices',
permanent: true,
},
{
source: '/manage/networks/use-cases/by-scenario/access-home-devices',
destination: '/manage/networks/use-cases/access-home-devices',
destination: '/use-cases/remote-access/access-home-devices',
permanent: true,
},
{
source: '/manage/networks/use-cases/by-scenario/cloud-to-on-premise',
destination: '/manage/networks/use-cases/cloud-to-on-premise',
destination: '/use-cases/remote-access/cloud-to-on-premise',
permanent: true,
},
// Networks guides moved to use-cases
@@ -636,7 +740,7 @@ const nextConfig = {
},
{
source: '/manage/network-routes/configuring-default-routes-for-internet-traffic',
destination: '/manage/network-routes/use-cases/exit-nodes',
destination: '/use-cases/remote-access/exit-nodes',
permanent: true,
},
{
@@ -671,7 +775,7 @@ const nextConfig = {
},
{
source: '/manage/network-routes/use-cases/by-scenario/exit-nodes',
destination: '/manage/network-routes/use-cases/exit-nodes',
destination: '/use-cases/remote-access/exit-nodes',
permanent: true,
},
{
@@ -692,22 +796,22 @@ const nextConfig = {
// Site-to-Site section redirects (overview and comprehensive guides)
{
source: '/manage/site-to-site',
destination: '/use-cases/site-to-site',
destination: '/use-cases/remote-access',
permanent: true,
},
{
source: '/manage/site-to-site/connect-home-networks',
destination: '/use-cases/site-to-site',
destination: '/use-cases/remote-access',
permanent: true,
},
{
source: '/manage/site-to-site/connect-office-networks',
destination: '/use-cases/site-to-site',
destination: '/use-cases/remote-access',
permanent: true,
},
{
source: '/manage/site-to-site/connect-cloud-environments',
destination: '/use-cases/site-to-site',
destination: '/use-cases/remote-access',
permanent: true,
},
{
@@ -728,7 +832,7 @@ const nextConfig = {
},
{
source: '/use-cases/routing-peers-and-kubernetes',
destination: '/use-cases/cloud/routing-peers-and-kubernetes',
destination: '/use-cases/kubernetes/routing-peers-and-kubernetes',
permanent: true,
},
{
@@ -738,7 +842,7 @@ const nextConfig = {
},
{
source: '/use-cases/client-on-mikrotik-router',
destination: '/use-cases/homelab/client-on-mikrotik-router',
destination: '/get-started/install/mikrotik',
permanent: true,
},
{

10641
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,12 @@
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "npm run gen:llm && npm run gen:edit-routes && npm run gen:last-updated && npm run gen:sitemap && next dev --webpack",
"build": "npm run gen:llm && npm run gen:edit-routes && npm run gen:last-updated && npm run gen:sitemap && next build --webpack",
"dev": "npm run gen:edit-routes && npm run gen:last-updated && npm run gen:sitemap && next dev --webpack",
"build": "npm run gen:edit-routes && npm run gen:last-updated && npm run gen:sitemap && next build --webpack",
"gen:edit-routes": "node scripts/generate-github-routes.mjs",
"gen:last-updated": "node scripts/generate-last-updated.mjs",
"gen:sitemap": "node scripts/generate-sitemap.mjs",
"gen": "swagger-codegen generate -i https://raw.githubusercontent.com/netbirdio/netbird/main/management/server/http/api/openapi.yml -l openapi -o generator/openapi && npx ts-node generator/index.ts gen --input generator/openapi/openapi.json --output src/pages/ipa/resources",
"gen:llm": "node scripts/generate-llm-docs.mjs",
"gen": "mkdir -p generator/openapi && curl -L -o generator/openapi/openapi.yml https://raw.githubusercontent.com/netbirdio/netbird/main/shared/management/http/api/openapi.yml && (cd generator && go run .) && npx ts-node generator/index.ts gen --input generator/openapi/expanded.yml --output src/pages/ipa/resources",
"start": "next start",
"lint": "eslint src/",
"lint:mdx": "node scripts/lint-mdx-headings.mjs"

View File

@@ -66,6 +66,9 @@
<key>disableAutoConnect</key>
<false/>
<key>disableAutostart</key>
<false/>
<key>disableClientRoutes</key>
<false/>

View File

@@ -103,6 +103,8 @@
<!--
<key>disableAutoConnect</key>
<false/>
<key>disableAutostart</key>
<false/>
<key>disableClientRoutes</key>
<false/>
<key>disableServerRoutes</key>

View File

@@ -58,6 +58,7 @@ preSharedKey="$NULL" # secret; redacted in log
allowServerSSH='true'
blockInbound="$NULL"
disableAutoConnect="$NULL"
disableAutostart="$NULL"
disableClientRoutes="$NULL"
disableServerRoutes="$NULL"
disableMetricsCollection="$NULL"
@@ -167,6 +168,7 @@ main() {
is_set "$allowServerSSH" && emit_bool allowServerSSH "$allowServerSSH"
is_set "$blockInbound" && emit_bool blockInbound "$blockInbound"
is_set "$disableAutoConnect" && emit_bool disableAutoConnect "$disableAutoConnect"
is_set "$disableAutostart" && emit_bool disableAutostart "$disableAutostart"
is_set "$disableClientRoutes" && emit_bool disableClientRoutes "$disableClientRoutes"
is_set "$disableServerRoutes" && emit_bool disableServerRoutes "$disableServerRoutes"
is_set "$disableMetricsCollection" && emit_bool disableMetricsCollection "$disableMetricsCollection"

View File

@@ -24,6 +24,9 @@
<string id="DisableAutoConnect_Name">Disable auto-connect</string>
<string id="DisableAutoConnect_Help">When enabled, the NetBird tunnel does not auto-connect at daemon startup. Equivalent to --disable-auto-connect.</string>
<string id="DisableAutostart_Name">Disable autostart</string>
<string id="DisableAutostart_Help">When enabled, the NetBird GUI is prevented from registering itself as an OS autostart entry on fresh installs, and any existing OS autostart entry registration is removed on the next GUI launch (Windows Registry Run key, macOS Login Item, Linux .desktop). Once the admin lifts the policy, the setting stays off until the user re-enables it in Settings.</string>
<string id="DisableClientRoutes_Name">Disable client routes</string>
<string id="DisableClientRoutes_Help">When enabled, this client will not consume routes advertised by routing peers. Equivalent to --disable-client-routes.</string>

View File

@@ -64,6 +64,18 @@
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableAutostart"
class="Machine"
displayName="$(string.DisableAutostart_Name)"
explainText="$(string.DisableAutostart_Help)"
key="Software\Policies\NetBird"
valueName="DisableAutostart">
<parentCategory ref="NetBird" />
<supportedOn ref="SUPPORTED_NetBird_All" />
<enabledValue><decimal value="1" /></enabledValue>
<disabledValue><decimal value="0" /></disabledValue>
</policy>
<policy name="DisableClientRoutes"
class="Machine"
displayName="$(string.DisableClientRoutes_Name)"

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 480" width="880" height="480" font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif">
<defs>
<marker id="ah" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="context-stroke"/>
</marker>
</defs>
<!-- card background -->
<rect x="2" y="2" width="876" height="476" rx="16" fill="#0d1117" stroke="#21262d" stroke-width="1.5"/>
<text x="40" y="46" font-size="21" font-weight="600" fill="#e6edf3">Highly available routing peers</text>
<!-- NetBird overlay zone -->
<rect x="40" y="80" width="230" height="356" rx="14" fill="#0f1b2d" stroke="#1f4f8f" stroke-width="1.5"/>
<text x="62" y="110" font-size="15" font-weight="600" fill="#58a6ff">NetBird overlay</text>
<!-- client -->
<rect x="66" y="236" width="180" height="78" rx="12" fill="#15294a" stroke="#4d8bf0" stroke-width="1.5"/>
<text x="156" y="270" text-anchor="middle" font-size="14" fill="#dbeafe">Your laptop</text>
<text x="156" y="290" text-anchor="middle" font-size="12" fill="#93b6e6">NetBird client</text>
<!-- Kubernetes cluster zone -->
<rect x="330" y="80" width="510" height="356" rx="14" fill="#0d1f15" stroke="#1f6f3a" stroke-width="1.5"/>
<text x="352" y="110" font-size="15" font-weight="600" fill="#3fb950">Kubernetes cluster</text>
<!-- node 1 -->
<rect x="360" y="132" width="330" height="84" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<text x="376" y="154" font-size="12" fill="#8b949e">node-1</text>
<rect x="470" y="152" width="200" height="44" rx="8" fill="#122a1c" stroke="#2ea043" stroke-width="1.5"/>
<text x="570" y="179" text-anchor="middle" font-size="13" fill="#56d364">routing-peer pod</text>
<!-- node 2 -->
<rect x="360" y="240" width="330" height="84" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<text x="376" y="262" font-size="12" fill="#8b949e">node-2</text>
<rect x="470" y="260" width="200" height="44" rx="8" fill="#122a1c" stroke="#2ea043" stroke-width="1.5"/>
<text x="570" y="287" text-anchor="middle" font-size="13" fill="#56d364">routing-peer pod</text>
<!-- node 3 -->
<rect x="360" y="348" width="330" height="84" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<text x="376" y="370" font-size="12" fill="#8b949e">node-3</text>
<rect x="470" y="368" width="200" height="44" rx="8" fill="#122a1c" stroke="#2ea043" stroke-width="1.5"/>
<text x="570" y="395" text-anchor="middle" font-size="13" fill="#56d364">routing-peer pod</text>
<!-- service -->
<rect x="735" y="235" width="92" height="80" rx="12" fill="#11272b" stroke="#2bb3c0" stroke-width="1.5"/>
<text x="781" y="269" text-anchor="middle" font-size="14" fill="#9fe7ef">Service</text>
<text x="781" y="289" text-anchor="middle" font-size="13" fill="#5fcdd8">nginx</text>
<!-- client -> routing peers (overlay) -->
<polyline points="246,274 470,174" fill="none" stroke="#4d8bf0" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="246,274 470,282" fill="none" stroke="#4d8bf0" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="246,274 470,390" fill="none" stroke="#4d8bf0" stroke-width="2" marker-end="url(#ah)"/>
<!-- routing peers -> service -->
<polyline points="670,174 735,266" fill="none" stroke="#3fb950" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="670,282 735,274" fill="none" stroke="#3fb950" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="670,390 735,283" fill="none" stroke="#3fb950" stroke-width="2" marker-end="url(#ah)"/>
<text x="40" y="463" font-size="13" fill="#8b949e">Clients reach the Service through any routing peer — lose a peer or a node and traffic fails over automatically.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -13,7 +13,7 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { getGitLastModified } from './git-dates.mjs'
import { buildGitDateMap } from './git-dates.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROOT = path.join(__dirname, '..')
@@ -45,9 +45,10 @@ const entries = findMdxRoutes(PAGES_DIR)
.filter((r) => r.route !== '/' && r.route !== '')
.sort((a, b) => a.route.localeCompare(b.route))
const gitDates = buildGitDateMap()
const map = {}
for (const { route, filePath } of entries) {
const date = getGitLastModified(filePath)
const date = gitDates.get(path.relative(ROOT, filePath))
if (date) map[route] = date
}

View File

@@ -1,238 +0,0 @@
#!/usr/bin/env node
/**
* LLM Documentation Generator
*
* Generates clean markdown files from MDX pages for LLM indexing.
* Creates:
* - public/llms/*.md - Clean markdown versions of each page
* - public/llms.txt - Index file linking to all pages
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.join(__dirname, '..');
const PAGES_DIR = path.join(ROOT_DIR, 'src/pages');
const OUTPUT_DIR = path.join(ROOT_DIR, 'public/llms');
const LLMS_TXT_PATH = path.join(ROOT_DIR, 'public/llms.txt');
// Base URL for the docs site
const BASE_URL = 'https://docs.netbird.io';
/**
* Strip JSX/React components from MDX content, keeping clean markdown
*/
function stripJsx(content) {
let result = content;
// Remove import statements
result = result.replace(/^import\s+.*?[;\n]/gm, '');
// Remove export statements (but keep the content if it's a description)
result = result.replace(/^export\s+const\s+description\s*=\s*(['"`])(.+?)\1;?\s*$/gm, '');
result = result.replace(/^export\s+.*?[;\n]/gm, '');
// Remove JSX self-closing tags like <Note />, <Button />, etc.
result = result.replace(/<[A-Z][a-zA-Z]*\s*\/>/g, '');
// Remove JSX components with children like <Note>...</Note>
// Handle multi-line JSX blocks
result = result.replace(/<([A-Z][a-zA-Z]*)[^>]*>[\s\S]*?<\/\1>/g, (match, tagName) => {
// For Note components, try to extract the text content
if (tagName === 'Note' || tagName === 'Warning' || tagName === 'Info') {
const innerContent = match.replace(/<[^>]+>/g, '').trim();
if (innerContent) {
return `> **Note:** ${innerContent}\n`;
}
}
return '';
});
// Remove remaining JSX tags but keep inner text for simple cases
result = result.replace(/<([a-z][a-zA-Z]*)[^>]*>([\s\S]*?)<\/\1>/g, '$2');
// Remove self-closing HTML/JSX tags with attributes
result = result.replace(/<[a-zA-Z]+[^>]*\/>/g, '');
// Remove div tags with className (common in MDX)
result = result.replace(/<div[^>]*className[^>]*>[\s\S]*?<\/div>/g, '');
// Remove iframe embeds (videos, etc.)
result = result.replace(/<iframe[\s\S]*?<\/iframe>/g, '[Video content]');
result = result.replace(/<iframe[^>]*\/>/g, '[Video content]');
// Remove Button components but note the link
result = result.replace(/<Button[^>]*href="([^"]*)"[^>]*>([^<]*)<\/Button>/g, '[$2]($1)');
result = result.replace(/<Button[^>]*children="([^"]*)"[^>]*href="([^"]*)"[^>]*\/>/g, '[$1]($2)');
result = result.replace(/<Button[^>]*href="([^"]*)"[^>]*children="([^"]*)"[^>]*\/>/g, '[$2]($1)');
// Clean up excessive newlines
result = result.replace(/\n{3,}/g, '\n\n');
// Clean up leading/trailing whitespace
result = result.trim();
return result;
}
/**
* Extract title from MDX content
*/
function extractTitle(content) {
// Try to find # heading
const h1Match = content.match(/^#\s+(.+)$/m);
if (h1Match) {
return h1Match[1].trim();
}
return null;
}
/**
* Get all MDX files recursively
*/
function getMdxFiles(dir, baseDir = dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip api/ipa directories if you want (they're auto-generated)
files.push(...getMdxFiles(fullPath, baseDir));
} else if (entry.name.endsWith('.mdx')) {
const relativePath = path.relative(baseDir, fullPath);
files.push({
fullPath,
relativePath,
slug: relativePath.replace(/\.mdx$/, '').replace(/\/index$/, ''),
});
}
}
return files;
}
/**
* Main generation function
*/
async function generate() {
console.log('Generating LLM documentation...\n');
// Ensure output directory exists
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true });
}
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
// Get all MDX files
const mdxFiles = getMdxFiles(PAGES_DIR);
console.log(`Found ${mdxFiles.length} MDX files\n`);
const pages = [];
for (const file of mdxFiles) {
const content = fs.readFileSync(file.fullPath, 'utf-8');
const cleanContent = stripJsx(content);
const title = extractTitle(cleanContent) || file.slug.split('/').pop();
// Create output path maintaining directory structure
const outputPath = path.join(OUTPUT_DIR, `${file.slug}.md`);
const outputDir = path.dirname(outputPath);
// Ensure directory exists
fs.mkdirSync(outputDir, { recursive: true });
// Add metadata header
const finalContent = `# ${title}\n\nSource: ${BASE_URL}/${file.slug}\n\n---\n\n${cleanContent}`;
fs.writeFileSync(outputPath, finalContent);
pages.push({
title,
slug: file.slug,
path: `/llms/${file.slug}.md`,
});
console.log(` Generated: ${file.slug}.md`);
}
// Generate llms.txt
const llmsTxt = generateLlmsTxt(pages);
fs.writeFileSync(LLMS_TXT_PATH, llmsTxt);
console.log(`\nGenerated llms.txt with ${pages.length} pages`);
console.log('\nDone!');
}
/**
* Generate llms.txt content
*/
function generateLlmsTxt(pages) {
// Group pages by section
const sections = {};
for (const page of pages) {
const parts = page.slug.split('/');
const section = parts[0] || 'root';
if (!sections[section]) {
sections[section] = [];
}
sections[section].push(page);
}
let content = `# NetBird Documentation
> NetBird is an open-source WireGuard-based mesh VPN platform that creates secure private networks.
> This file provides an index of all documentation pages in markdown format for LLM consumption.
## About
- Website: https://netbird.io
- Documentation: ${BASE_URL}
- GitHub: https://github.com/netbirdio/netbird
## Documentation Index
Each link below points to a clean markdown version of the documentation page.
`;
// Section display names
const sectionNames = {
'introduction': 'Introduction',
'about-netbird': 'About NetBird',
'get-started': 'Getting Started',
'manage': 'Managing NetBird',
'selfhosted': 'Self-Hosting',
'ipa': 'API Reference',
'use-cases': 'Use Cases',
'client': 'Client',
'help': 'Help & Troubleshooting',
};
for (const [section, sectionPages] of Object.entries(sections)) {
const sectionTitle = sectionNames[section] || section.charAt(0).toUpperCase() + section.slice(1);
content += `### ${sectionTitle}\n\n`;
for (const page of sectionPages) {
content += `- [${page.title}](${BASE_URL}${page.path})\n`;
}
content += '\n';
}
content += `---
## Usage
LLMs can fetch individual markdown files to get detailed information about specific topics.
Each markdown file contains the clean documentation content with a link back to the original page.
Generated: ${new Date().toISOString()}
`;
return content;
}
// Run
generate().catch(console.error);

View File

@@ -10,7 +10,7 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { getGitLastModified } from './git-dates.mjs'
import { buildGitDateMap } from './git-dates.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROOT = path.join(__dirname, '..')
@@ -56,10 +56,11 @@ function escapeXml(s) {
.replace(/'/g, '&apos;')
}
const gitDates = buildGitDateMap()
const entries = findMdxFiles(PAGES_DIR)
.map(({ route, filePath }) => ({
url: toPublicUrl(route),
lastmod: getGitLastModified(filePath),
lastmod: gitDates.get(path.relative(ROOT, filePath)) ?? null,
}))
.sort((a, b) => a.url.localeCompare(b.url))

View File

@@ -1,18 +1,64 @@
import { execSync } from 'child_process'
let _dateMapCache
/**
* Get the last modified date for a file from git history.
* Returns YYYY-MM-DD or null if the file is not tracked / git is unavailable.
* Build a map of repo-relative path -> last commit date (YYYY-MM-DD) for every
* file in history, in a SINGLE git process, instead of one `git log` per file.
* Memoised for the lifetime of the process.
*
* git log is reverse-chronological, so the first date seen for a path is its
* most recent commit — the same value `git log -1 -- <path>` returns. Paths are
* repo-relative with forward slashes, matching path.relative(repoRoot, file).
*
* Note: `git log --name-only` lists no files for merge commits, so content
* introduced by a conflict-resolving merge is attributed to its source
* commits. That matches this repo's squash-merge workflow; revisit with
* `--diff-merges=first-parent` if long-lived branches are ever merged.
*
* Returns an empty map — with a warning, since every page then renders without
* an "Updated" date and the sitemap loses <lastmod> — when git is unavailable
* or the checkout is shallow (e.g. actions/checkout without fetch-depth: 0,
* where git log would attribute one identical, wrong date to every file).
*/
export function getGitLastModified(filePath) {
export function buildGitDateMap() {
if (_dateMapCache) return _dateMapCache
const map = new Map()
try {
const date = execSync(`git log -1 --format=%cI -- "${filePath}"`, {
const shallow = execSync('git rev-parse --is-shallow-repository', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'],
}).trim()
return date ? date.split('T')[0] : null
} catch {
return null
if (shallow === 'true') {
console.warn(
'[git-dates] shallow clone detected — emitting no per-page dates (fetch full history to enable them)'
)
_dateMapCache = map
return map
}
// core.quotePath=false keeps non-ASCII paths unquoted so line parsing is safe.
const out = execSync(
'git -c core.quotePath=false log --format=%cI --name-only',
{
encoding: 'utf-8',
maxBuffer: 128 * 1024 * 1024,
stdio: ['pipe', 'pipe', 'ignore'],
}
)
let currentDate = null
for (const line of out.split('\n')) {
if (line === '') continue
if (/^\d{4}-\d{2}-\d{2}T/.test(line)) {
currentDate = line.slice(0, 10)
continue
}
if (currentDate && !map.has(line)) map.set(line, currentDate)
}
} catch (err) {
console.warn(
`[git-dates] could not read git history — emitting no per-page dates: ${err.message}`
)
}
_dateMapCache = map
return map
}

View File

@@ -6,6 +6,7 @@ import { Transition } from '@headlessui/react'
import { Button } from '@/components/Button'
import {apiNavigation, flattenNavItems} from '@/components/NavigationAPI'
import {docsNavigation} from "@/components/NavigationDocs";
import {useCookieConsent} from '@/components/cookie-consent/CookieConsentProvider'
function CheckIcon(props) {
return (
@@ -227,11 +228,20 @@ function SocialLink({ href, icon: Icon, children }) {
}
function SmallPrint() {
const { openCookieSettings } = useCookieConsent()
return (
<div className="flex flex-col items-center justify-between gap-5 border-t border-zinc-900/5 pt-8 dark:border-white/5 sm:flex-row">
<p className="text-xs text-zinc-600 dark:text-zinc-400">
&copy; Copyright {new Date().getFullYear()}. All rights reserved.
</p>
<div className="flex items-center gap-4 text-xs text-zinc-600 dark:text-zinc-400">
<p>&copy; Copyright {new Date().getFullYear()}. All rights reserved.</p>
<button
type="button"
onClick={openCookieSettings}
className="underline underline-offset-4 transition hover:text-zinc-900 dark:hover:text-white"
>
Cookie Settings
</button>
</div>
<div className="flex gap-4">
<SocialLink href="https://x.com/netbird" icon={TwitterIcon}>
Follow us on X

View File

@@ -4,10 +4,14 @@ import Script from "next/script";
// Google Tag Manager ID
const GTM_ID = "GTM-PGWDPDN3";
export const GoogleTagManagerHeadScript = () => {
export const GoogleTagManager = ({ consentGiven }) => {
if (!consentGiven) {
return null;
}
return (
<Script id="gtm-script" strategy="afterInteractive">
{`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
{`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
@@ -15,17 +19,3 @@ export const GoogleTagManagerHeadScript = () => {
</Script>
);
};
export const GoogleTageManagerBodyScript = () => {
return (
<noscript>
<iframe
title={"Google Tag Manager"}
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style={{ display: "none", visibility: "hidden" }}
/>
</noscript>
);
};

View File

@@ -1,16 +1,50 @@
import Script from "next/script";
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/router'
export function MatomoTagManager({ consentGiven }) {
const router = useRouter()
const isFirstNavigation = useRef(true)
useEffect(() => {
if (!consentGiven) return
// The container tracks the initial hard load. Subsequent Next.js routes
// are emitted after the new document title has committed so Matomo does
// not associate the previous page's title with the new URL.
if (isFirstNavigation.current) {
isFirstNavigation.current = false
return
}
const id = window.setTimeout(() => {
window._mtm = window._mtm || []
window._mtm.push({
event: 'mtm.SpaPageView',
'mtm.pageUrl': window.location.href,
'mtm.pageTitle': document.title,
})
}, 0)
return () => window.clearTimeout(id)
}, [consentGiven, router.asPath])
if (!consentGiven) {
return null
}
return (
<Script id="matomo-tag-manager" strategy="afterInteractive">
{`var _paq = window._paq = window._paq || [];
_paq.push(['requireCookieConsent']);
${consentGiven ? "_paq.push(['setCookieConsentGiven']);" : ""}
_paq.push(['requireConsent']);
var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
(function() {
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='https://cdn.matomo.cloud/netbird.matomo.cloud/container_hvVzPZGH.js'; s.parentNode.insertBefore(g,s);
g.async=true;
g.src='https://cdn.matomo.cloud/netbird.matomo.cloud/container_hvVzPZGH.js';
g.onload=function(){window._paq.push(['setConsentGiven']);};
s.parentNode.insertBefore(g,s);
})();`}
</Script>
);

View File

@@ -38,6 +38,7 @@ export const apiNavigation = [
{ title: 'DNS', href: '/api/resources/dns' },
{ title: 'DNS Zones', href: '/api/resources/dns-zones' },
{ title: 'Services', href: '/api/resources/services' },
{ title: 'Agent Network', href: '/api/resources/agent-network' },
{ title: 'Events', href: '/api/resources/events' },
{
title: 'Event Streaming',

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,28 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
import { useRouter } from 'next/router'
const STORAGE_KEY = 'cookie-consent'
const ACCEPT_EXPIRY_DAYS = 90
const DECLINE_EXPIRY_DAYS = 1
const CONSENT_EXPIRY_DAYS = 180
const TRACKING_COOKIE_PREFIXES = [
'_ga',
'_gid',
'_hj',
'_clck',
'_clsk',
'_gcl_',
'_pk_',
'__hst',
'hubspotutk',
'messagesUtk',
]
const CookieConsentContext = createContext({
isAccepted: false,
isDeclined: false,
showConsent: false,
acceptCookies: () => {},
declineCookies: () => {},
openCookieSettings: () => {},
})
function getStoredConsent() {
@@ -40,9 +54,34 @@ function storeConsent(value, days) {
} catch {}
}
function removeTrackingCookies() {
const cookieNames = document.cookie
.split(';')
.map((cookie) => cookie.split('=')[0].trim())
.filter(Boolean)
for (const name of cookieNames) {
if (!TRACKING_COOKIE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
continue
}
const expiredCookie = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax`
document.cookie = expiredCookie
document.cookie = `${expiredCookie}; domain=${window.location.hostname}`
if (
window.location.hostname === 'netbird.io' ||
window.location.hostname.endsWith('.netbird.io')
) {
document.cookie = `${expiredCookie}; domain=.netbird.io`
}
}
}
export function CookieConsentProvider({ children }) {
const router = useRouter()
const [consent, setConsent] = useState(() => getStoredConsent())
// Resolve browser storage after hydration so the server and the first client
// render agree. Analytics remains disabled while the choice is loading.
const [consent, setConsent] = useState(null)
const [showConsent, setShowConsent] = useState(false)
useEffect(() => {
@@ -59,32 +98,44 @@ export function CookieConsentProvider({ children }) {
}, [router.pathname])
const acceptCookies = useCallback(() => {
storeConsent('accepted', ACCEPT_EXPIRY_DAYS)
storeConsent('accepted', CONSENT_EXPIRY_DAYS)
setConsent('accepted')
setShowConsent(false)
// Enable Matomo cookies
window._paq = window._paq || []
window._paq.push(['setCookieConsentGiven'])
}, [])
const declineCookies = useCallback(() => {
storeConsent('declined', DECLINE_EXPIRY_DAYS)
const trackersWereActive = consent === 'accepted'
storeConsent('declined', CONSENT_EXPIRY_DAYS)
setConsent('declined')
setShowConsent(false)
// Tell Matomo to forget consent and delete its cookies
window._paq = window._paq || []
window._paq.push(['forgetCookieConsentGiven'])
removeTrackingCookies()
// Scripts already loaded by an accepted visitor cannot be reliably
// unloaded. Reload into the persisted declined state so no optional
// analytics script is mounted again.
if (trackersWereActive) {
window._paq = window._paq || []
window._paq.push(['forgetConsentGiven'])
window._paq.push(['deleteCookies'])
window.location.reload()
}
}, [consent])
const openCookieSettings = useCallback(() => {
setShowConsent(true)
}, [])
return (
<CookieConsentContext.Provider
value={{
isAccepted: consent === 'accepted',
isDeclined: consent === 'declined',
showConsent,
acceptCookies,
declineCookies,
openCookieSettings,
}}
>
{children}

View File

@@ -15,6 +15,7 @@ import {dom} from "@fortawesome/fontawesome-svg-core";
import {AnnouncementBannerProvider} from "@/components/announcement-banner/AnnouncementBannerProvider";
import {ImageZoom} from "@/components/ImageZoom";
import {MatomoTagManager} from "@/components/Matomo";
import {GoogleTagManager} from "@/components/GoogleTagManager";
import {CookieConsentProvider, useCookieConsent} from "@/components/cookie-consent/CookieConsentProvider";
import {CookieConsent} from "@/components/cookie-consent/CookieConsent";
@@ -33,6 +34,7 @@ function AppInner({ Component, pageProps }) {
return (
<>
<MatomoTagManager consentGiven={isAccepted} />
<GoogleTagManager consentGiven={isAccepted} />
<Head>
<style>{dom.css()}</style>
{router.route.startsWith('/ipa') ?

View File

@@ -1,5 +1,4 @@
import { Head, Html, Main, NextScript } from 'next/document'
import {GoogleTageManagerBodyScript, GoogleTagManagerHeadScript} from "@/components/GoogleTagManager";
const modeScript = `
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
@@ -40,12 +39,10 @@ export default function Document() {
return (
<Html lang="en">
<Head>
<GoogleTagManagerHeadScript />
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
<link rel="shortcut icon" href="/docs-static/img/favicon.ico" />
</Head>
<body className="bg-white antialiased dark:bg-[#181A1D]">
<GoogleTageManagerBodyScript />
<Main />
<NextScript />
</body>

View File

@@ -0,0 +1,40 @@
import { Note } from '@/components/mdx'
export const description =
'Proxy clusters serve the Agent Network endpoint, routing your agents\' traffic to LLM APIs and gateways on infrastructure you run. Add multiple clusters to scale the environment.'
# Clusters
A **cluster** is a set of reverse proxies that serve your [Agent Network endpoint](/agent-network/how-it-works).
Agents point at the endpoint hostname over the WireGuard tunnel; the proxy cluster terminates that
traffic, enforces identity, policies, limits, and guardrails, and forwards each request to the
upstream LLM API or gateway.
Clusters run on **your own infrastructure**, so agent traffic terminates on machines you operate —
giving you control over the data path, geographic placement, and TLS. Add **multiple proxies and clusters** to
scale the environment: spread load across more proxy instances, place them closer to your agents or
upstreams, and keep the endpoint available if one goes down.
<Note>
Agent Network clusters are the same mechanism as Reverse Proxy clusters, scoped to your account
(**account clusters**). See [Bring Your Own Proxy](/manage/reverse-proxy/bring-your-own-proxy) for
the full cluster model.
</Note>
## Manage Clusters
Go to **Agent Network → Configuration → Clusters** to see every cluster serving your account, along
with the number of connected proxies, online status, and the capabilities each proxy reports.
<p>
<img src="/docs-static/img/agent-network/clusters/agent-network-clusters-list.png" alt="Agent Network clusters list under Configuration" className="imagewrapper-big" />
</p>
Use **Setup Self-Hosted Cluster** to register a new one. Enter a domain for the cluster and pick a
**Deployment Method**: run it yourself with Docker, or use one of the **1-click deployments** for
popular cloud providers to stand up a proxy in a few clicks. The wizard then walks you through the
DNS records to add and starting the proxy.
<p>
<img src="/docs-static/img/agent-network/clusters/agent-network-setup-cluster.png" alt="Setup Cluster wizard with domain and deployment method" className="imagewrapper" />
</p>

View File

@@ -4,7 +4,7 @@ export const description =
# Global Limits
Global limits are account-wide caps on token usage and spend that apply across
**every** policy and **every provider** a backstop independent of any single policy's
**every** policy and **every provider**, a backstop independent of any single policy's
limits. They are **limit-only** rules: unlike a policy, a global limit never selects a
provider or authorizes traffic, and it isn't tied to one. It caps a caller's total
consumption no matter which provider or gateway the request is routed to.
@@ -21,9 +21,9 @@ caller's effective limit and never loosen it, adding one is always safe.
Each rule targets who it applies to:
- **Target groups** the rule binds when the caller's groups intersect the rule's groups.
- **Target users** the rule binds a specific user directly.
- **Untargeted** a rule with no target groups or users applies to **every** caller (the
- **Target groups**: the rule binds when the caller's groups intersect the rule's groups.
- **Target users**: the rule binds a specific user directly.
- **Untargeted**: a rule with no target groups or users applies to **every** caller (the
account-wide default).
A request can be bound by several rules at once (for example an account-wide rule plus a
@@ -39,7 +39,7 @@ A global limit carries the same cap shape as a [policy limit](/agent-network/pol
Windows and counting work exactly as described in
[Token & Budget Limits](/agent-network/policies/limits#how-the-window-works): caps apply to
a fixed, epoch-aligned window, and the check is run **before** the request against usage
already accumulated so a request that starts under the cap is allowed even if it crosses
already accumulated, so a request that starts under the cap is allowed even if it crosses
it, and the next one is blocked.
## How Enforcement Works

View File

@@ -18,7 +18,7 @@ calling:
- **Internal resources** such as databases, internal APIs, and self-hosted models, are
reached directly over **peer-to-peer WireGuard tunnels**, the same way any NetBird peer
reaches another. This traffic is governed by the same identities and access policies
but does not pass through the proxy, so there is no endpoint or key injection — the
but does not pass through the proxy, so there is no endpoint or key injection. The
agent connects straight to the resource over the overlay.
## Architecture
@@ -30,7 +30,7 @@ service adds an identity-aware control plane for AI traffic.
### LLM APIs and AI Gateways
The diagram below illustrates the **first path** an LLM request: the agent reaches the
The diagram below illustrates the **first path**, an LLM request: the agent reaches the
endpoint over the WireGuard overlay, the proxy enforces identity, policies, limits, and guardrails
against the management control plane, injects the provider key, and forwards to the
upstream API or gateway. The proxy can also inject the calling agent's identity into the
@@ -43,17 +43,17 @@ can apply tag budgets and per-user attribution.
<img src="/docs-static/img/agent-network/how-it-works/agent-network-diagram-llm-apis.png" alt="agent network LLM request path through the NetBird proxy" className="imagewrapper-big" />
</p>
- **NetBird client** the agent's device joins the overlay as a peer. Its requests to
- **NetBird client**: the agent's device joins the overlay as a peer. Its requests to
the endpoint are routed through the WireGuard tunnel, not the public internet.
- **Proxy peer** handles LLM traffic only. It terminates the request, establishes
- **Proxy peer**: handles LLM traffic only. It terminates the request, establishes
the caller's identity, runs the routing and policy pipeline, injects the provider key,
and forwards to the upstream API or gateway.
- **Management service** the control plane. It holds providers, policies, guardrails,
- **Management service**: the control plane. It holds providers, policies, guardrails,
and limits; resolves identities against your IdP; answers the proxy's per-request
policy checks; and records usage and access logs.
- **Identity provider** your existing IdP (Okta, Microsoft Entra ID, Google, …)
- **Identity provider**: your existing IdP (Okta, Microsoft Entra ID, Google, …)
supplies the identities and group memberships that policies are written against.
- **Upstreams** for LLM traffic, the proxy forwards to LLM APIs and AI gateways.
- **Upstreams**: for LLM traffic, the proxy forwards to LLM APIs and AI gateways.
The endpoint hostname itself (for example `https://sailcloth.netbird.ai`) is generated
when you connect your first provider and is only reachable from inside your overlay.
@@ -62,14 +62,14 @@ overlay.
### Internal Resources
The **second path** covers everything that isn't an LLM API internal databases,
The **second path** covers everything that isn't an LLM API: internal databases,
internal APIs, and self-hosted models on a GPU host. Here the proxy is not involved at
all. The agent connects to the target's overlay address **directly over a peer-to-peer
WireGuard tunnel**, exactly the way any NetBird peer reaches another. Access is still
identity-based: the agent's peer identity and group membership are matched against your
access policies, so it can reach only the resources it is authorized for. Because the
traffic never passes through the proxy, this path has no agent network endpoint, no
provider-key injection, and no token, budget, or per-request LLM logging — it is governed
provider-key injection, and no token, budget, or per-request LLM logging. It is governed
like standard NetBird peer-to-peer access. This keeps internal traffic fast and private,
flowing straight between the two peers. Because NetBird is a peer-to-peer network, this
also works in reverse, so a resource can reach back to an agent when needed, such as to
@@ -82,7 +82,7 @@ deliver a callback or webhook.
## The Lifecycle of an LLM Request
This pipeline applies to LLM traffic requests to the agent network endpoint.
This pipeline applies to LLM traffic: requests to the agent network endpoint.
Access to internal resources skips it entirely and flows peer-to-peer (see [Internal
Resources](#internal-resources)).
@@ -90,22 +90,25 @@ The proxy runs each request through an ordered chain of middleware. On the way t
upstream:
1. **Establish identity.** The request arrives over the WireGuard tunnel, so the proxy
maps it to the calling NetBird peer and its identity tied to your IdP for a human
user, or the peer's own NetBird identity for an autonomous agent together with its
maps it to the calling NetBird peer and its identity, tied to your IdP for a human
user, or the peer's own NetBird identity for an autonomous agent, together with its
group membership. See [Identity and Authentication](#identity-and-authentication).
2. **Parse the request.** Read the target model and stream flag from the body, and
capture the prompt if prompt collection is enabled.
3. **Route and inject the key.** Match the model to a provider the caller's groups are
authorized to use, rewrite the upstream target, strip any client-supplied auth headers,
and inject the provider's key from server-side storage — see
and inject the provider's key from server-side storage. See
[Routing](#routing-matching-a-request-to-a-provider) and [Keyless Access](#keyless-access).
4. **Check policy and limits.** Ask management to select the matching policy and evaluate
account- and policy-level token and budget caps. If unauthorized or a cap is exhausted,
the request is denied here — see [Policies, Limits, and
the request is denied here. See [Policies, Limits, and
Guardrails](#policies-limits-and-guardrails).
5. **Stamp identity for the gateway.** Add the caller's identity to the upstream request
(for example into `metadata.tags` and `x-litellm-end-user-id`) for gateways that key
their own budgets and attribution off it.
their own budgets and attribution off it, or into a provider's own cost-allocation
metadata such as AWS Bedrock's `X-Amzn-Bedrock-Request-Metadata`. This is on by default
and can be turned off per provider. See [Identity
Metadata](/agent-network/providers#identity-metadata).
6. **Apply guardrails.** Enforce the model allowlist and the prompt-capture rules.
The request is then forwarded to the upstream API or gateway. On the response leg, in
@@ -113,7 +116,7 @@ reverse:
7. **Meter.** Extract token counts from the response and convert them to cost.
8. **Record.** Post the usage back to management to update the limit counters. Usage is
always recorded; a full access-log entry is written when log collection is on — see
always recorded; a full access-log entry is written when log collection is on. See
[Usage and Access Logs](#usage-and-access-logs).
A denial at any gate returns `403` to the client with a machine-readable reason, and the
@@ -126,14 +129,14 @@ comes from the **NetBird tunnel**. Because the request arrives over WireGuard, t
maps its source to the enrolled peer and resolves the peer's NetBird identity and group
membership:
- For a **human user** for example someone running Claude Code the NetBird identity is
- For a **human user**: for example someone running Claude Code, the NetBird identity is
tied to your identity provider (Okta, Microsoft Entra ID, Google, …), so the request
carries that user and the groups they belong to.
- For an **autonomous agent**, the identity is the agent's own NetBird peer identity and
the groups assigned to that peer.
Either way the request carries a real identity and its **group membership**, captured at
request time. There is no API key or separate login on the client — the tunnel is the
request time. There is no API key or separate login on the client. The tunnel is the
credential. Policies are written against those groups, so access to AI follows the same
identities your organization already manages.
@@ -158,7 +161,7 @@ was selected and which groups authorized it.
## Policies, Limits, and Guardrails
Routing decides *where* a request can go; policies decide *whether it may* and *under what
budget*. By default nothing is allowed a policy must connect a **source group** to one
budget*. By default nothing is allowed: a policy must connect a **source group** to one
or more **providers**.
At request time, management evaluates, in order:
@@ -203,11 +206,11 @@ rotating a provider key is a single server-side change.
Agent Network separates lightweight accounting from full audit detail:
- **Usage** is recorded for **every** served request identity, provider, model, tokens,
and cost regardless of any logging setting. This always-on stream powers the usage
- **Usage** is recorded for **every** served request: identity, provider, model, tokens,
and cost, regardless of any logging setting. This always-on stream powers the usage
dashboards and the limit counters, and is retained indefinitely.
- **Access logs** add the full per-request detail (method, path, status, duration, and
when prompt capture is on the prompt and completion). Full access-log entries are
- **Access logs** add the full per-request detail (method, path, status, duration, and,
when prompt capture is on, the prompt and completion). Full access-log entries are
written only when **log collection** is enabled for the account, and are swept after a
configurable **retention period**. Prompts can be redacted for PII.
@@ -224,7 +227,7 @@ This is also where the two paths differ:
- **LLM traffic** rides the overlay to reach the **proxy** peer, which then applies the
pipeline above and forwards to the upstream API or gateway.
- **Internal resources** databases, APIs, and self-hosted models are reached over a
- **Internal resources**: databases, APIs, and self-hosted models, are reached over a
**direct peer-to-peer tunnel** between the agent and the target peer, with no proxy in
between. Access is governed by the same identities and access policies as any other
NetBird peer, so an agent reaches only the resources its identity is allowed to.

View File

@@ -1,7 +1,7 @@
import { Note } from '@/components/mdx'
export const description =
'Agent Network is NetBird\'s control layer for AI agents a keyless gateway to LLM APIs and scoped, identity-based access to your internal resources, all over the tunnel with per-identity policies, limits, and audit.'
'Agent Network is NetBird\'s control layer for AI agents, a keyless gateway to LLM APIs and scoped, identity-based access to your internal resources, all over the tunnel with per-identity policies, limits, and audit.'
# What is NetBird Agent Network?
@@ -77,6 +77,14 @@ That makes agent access a natural responsibility for IT.
Because NetBird connects seamlessly with existing identity providers, IT teams can integrate NetBird Agent Network into
their enterprise stack with minimal changes.
## Focused Dashboard View
Some accounts open directly into Agent Network, with the rest of the NetBird Dashboard (such as Peers, Networks, and DNS) hidden to keep the experience focused on agents. This is the default for accounts onboarded specifically for Agent Network.
To use the full NetBird platform, open **Settings** and turn off **Agent Network focused view**. You can re-enable it from the same setting at any time.
For API-managed accounts, `agent_network_only` requires `dashboard_features.agent_network` to be enabled. See the [Accounts API reference](/ipa/resources/accounts) for the account settings schema.
## Next steps
- [Quickstart](/agent-network/quickstart). Deploy NetBird Agent Network and make your first routed LLM call.

View File

@@ -11,7 +11,7 @@ agents keyless access over the tunnel: NetBird holds the Bedrock API key server-
every request to a real identity from your IdP, and applies your policies, limits, and audit
on the way to Bedrock.
Bedrock authenticates with a **Bedrock API key** a long-term key you generate in AWS that
Bedrock authenticates with a **Bedrock API key**, a long-term key you generate in AWS that
NetBird injects as a bearer token on every request. You create the key once, hand it to
NetBird, and it stays server-side.
@@ -29,7 +29,7 @@ See [Bedrock API keys](https://docs.aws.amazon.com/bedrock/latest/userguide/api-
for details.
<Warning>
The Bedrock API key grants access to models in your AWS account. Treat it as a secret
The Bedrock API key grants access to models in your AWS account. Treat it as a secret:
store it securely, never commit it to source control, and delete the local copy once it's
stored in NetBird.
</Warning>
@@ -37,22 +37,39 @@ for details.
## Connect the Provider
1. Go to **Agent Network → Providers** and click **Connect Provider**.
2. Select **AWS Bedrock**. Set the **Upstream URL** to your region's Bedrock runtime host
2. Select **AWS Bedrock**. Set the **Upstream URL** to your region's Bedrock runtime host,
for example `https://bedrock-runtime.us-east-1.amazonaws.com`. Bedrock is region-specific,
so the host must include the region you enabled model access in.
3. Paste the **Bedrock API key**. NetBird stores it encrypted server-side, injects it as
`Authorization: Bearer …` on each request, and never returns it to callers.
4. _(Optional)_ Restrict the **allowed models** for example `anthropic.claude-opus-4-8`,
4. _(Optional)_ Restrict the **allowed models**, for example `anthropic.claude-opus-4-8`,
`anthropic.claude-sonnet-4-6`, `meta.llama3-3-70b-instruct`, or `amazon.nova-pro`. Leaving
the list empty allows any catalog model.
5. Save the provider. The key is now held server-side the next step authorizes who can use
5. Save the provider. The key is now held server-side, the next step authorizes who can use
it.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-bedrock-connect.png" alt="Connect the AWS Bedrock provider in NetBird Agent Network" className="imagewrapper" />
</p>
See [Providers](/agent-network/providers) for details.
### Cross-Region Inference Profiles
Bedrock also exposes cross-region inference profiles, model IDs prefixed with a geography
code such as `eu.` or `us.`, that route a request across a set of regions instead of pinning
it to one. If the profile you need isn't in the picker, add it the same way as any
[model not in the catalog](/agent-network/providers#adding-a-model-not-in-the-catalog): type
the full profile ID into the **Model** field on the **Models** tab, for example
`eu.anthropic.claude-sonnet-5`, and set its input/output pricing yourself.
<p>
<img src="/docs-static/img/agent-network/providers/agent-network-custom-model.png" alt="Models tab with a geo-specific Bedrock inference profile ID and manually entered input/output pricing" className="imagewrapper" />
</p>
## Create a Policy
By default nothing is allowed a policy must connect a source group to the Bedrock provider
By default nothing is allowed: a policy must connect a source group to the Bedrock provider
before anyone can route through it.
1. Go to **Agent Network → Policies** and add a policy.
@@ -64,10 +81,33 @@ before anyone can route through it.
See [Policies](/agent-network/policies) for details.
## Cost Allocation
NetBird forwards the caller's identity to every provider by default. See
[Identity Metadata](/agent-network/providers#identity-metadata) for the general behavior and
how to turn it off. For Bedrock that identity lands in the
[`X-Amzn-Bedrock-Request-Metadata`](https://docs.aws.amazon.com/bedrock/latest/userguide/cost-mgmt-request-metadata.html)
header, the one AWS reads for cost-allocation tags, carrying the caller's **user** and the
**group that authorized the request**:
```
X-Amzn-Bedrock-Request-Metadata: {"user": "user@example.com", "group": "engineering"}
```
Activate the matching **cost-allocation tags** in **AWS Billing and Cost Management → Cost
allocation tags**; Bedrock spend in AWS Cost Explorer can then be broken down by NetBird user
and group. Values are sanitized to Bedrock's accepted character set before they are sent.
The provider's **Mappings** tab shows exactly what NetBird sends:
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-bedrock-mappings.png" alt="Bedrock identity metadata mapping: user to user email and group to groups in the X-Amzn-Bedrock-Request-Metadata header" className="imagewrapper" />
</p>
## Use with Claude Code
To route [Claude Code](/agent-network/integrations/claude-code) through this Bedrock provider
instead of the Anthropic API, see [Use Claude on AWS Bedrock](/agent-network/integrations/claude-code#use-claude-on-aws-bedrock)
instead of the Anthropic API. See [Use Claude on AWS Bedrock](/agent-network/integrations/claude-code#use-claude-on-aws-bedrock)
on the Claude Code integration page.
<Note>

View File

@@ -1,5 +1,5 @@
export const description =
'Route Claude Code through NetBird Agent Network by pointing it at your agent network endpoint as the Anthropic base URL — no API key on the client.'
'Route Claude Code through NetBird Agent Network by pointing it at your agent network endpoint as the Anthropic base URL. No API key on the client.'
# Keyless Access to Claude Code
@@ -14,7 +14,7 @@ Anthropic key into one your team reaches with their existing identity:
any developer's machine. Each person runs Claude Code over the NetBird tunnel, and the
request is tied to their real identity from your identity provider (Okta, Microsoft
Entra ID, Google, …). Onboarding and offboarding follow the same IdP groups you already
manage — there's no key to hand out, copy, or revoke.
manage. There's no key to hand out, copy, or revoke.
- **Usage tracking per developer and group.** Every request is metered by identity, model,
tokens, and cost, so you can see exactly who is using Claude Code and how much it costs,
broken down per person and aggregated per IdP group (team, department, project) in
@@ -30,7 +30,7 @@ your endpoint.
1. Go to **Agent Network → Providers** and click **Connect Provider**.
2. Select **Anthropic** and paste your Anthropic API key.
3. Save the provider. The key is now held server-side the next step authorizes who can use it.
3. Save the provider. The key is now held server-side, the next step authorizes who can use it.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-connect-anthropic.png" alt="connect Anthropic provider in NetBird Agent Network" className="imagewrapper" />
@@ -40,7 +40,7 @@ See [Providers](/agent-network/providers) for details.
## Create a Policy
By default nothing is allowed a policy must connect a source group to the Anthropic
By default nothing is allowed: a policy must connect a source group to the Anthropic
provider before anyone can route Claude Code through it.
1. Go to **Agent Network → Policies** and add a policy.
@@ -60,7 +60,7 @@ See [Policies](/agent-network/policies) for details.
## Configure with `settings.json`
Next to your agent network endpoint in the NetBird dashboard, click **Agent Config**. The
modal shows tabs for Claude Code, Codex, OpenAI SDK, and cURL — pick **Claude Code** and copy
modal shows tabs for Claude Code, Codex, OpenAI SDK, and cURL. Pick **Claude Code** and copy
the pre-filled configuration.
<p>
@@ -68,7 +68,7 @@ the pre-filled configuration.
</p>
Add the following to `~/.claude/settings.json`. The `apiKeyHelper` returns a dummy value so
Claude Code doesn't prompt for a key NetBird supplies the real one.
Claude Code doesn't prompt for a key. NetBird supplies the real one.
```json
{
@@ -89,9 +89,9 @@ export ANTHROPIC_API_KEY=none
claude
```
That's it Claude Code now sends every request over the NetBird tunnel, where it's tied to
That's it. Claude Code now sends every request over the NetBird tunnel, where it's tied to
your identity, checked against your policies and limits, and recorded in
[Usage & Logs](/agent-network/usage-and-logs) broken down per developer and aggregated
[Usage & Logs](/agent-network/usage-and-logs), broken down per developer and aggregated
per IdP group.
<p>
@@ -103,11 +103,11 @@ per IdP group.
If you reach Claude through **Google Vertex AI** instead of the Anthropic API, point Claude
Code's Vertex backend at your agent network endpoint. NetBird holds the Google service
account credential server-side and mints the Vertex access token, so Claude Code skips
Google authentication entirely — the client stays keyless.
Google authentication entirely. The client stays keyless.
First connect a [Google Vertex AI provider](/agent-network/integrations/vertex-ai) in NetBird.
Set its upstream URL to the region-less host `https://aiplatform.googleapis.com` not the
`<region>-aiplatform.googleapis.com` form so it matches `CLOUD_ML_REGION=global` below.
Set its upstream URL to the region-less host `https://aiplatform.googleapis.com`, not the
`<region>-aiplatform.googleapis.com` form, so it matches `CLOUD_ML_REGION=global` below.
Then add the following to `~/.claude/settings.json`:
@@ -124,7 +124,7 @@ Then add the following to `~/.claude/settings.json`:
```
- `CLAUDE_CODE_USE_VERTEX=1` routes Claude Code through the Vertex backend.
- `CLAUDE_CODE_SKIP_VERTEX_AUTH=1` skips Google auth on the client NetBird injects the
- `CLAUDE_CODE_SKIP_VERTEX_AUTH=1` skips Google auth on the client: NetBird injects the
OAuth token server-side.
- `ANTHROPIC_VERTEX_BASE_URL` is your agent network endpoint with the `/v1` suffix.
- `CLOUD_ML_REGION=global` pairs with the region-less provider URL above.
@@ -133,7 +133,7 @@ Then add the following to `~/.claude/settings.json`:
If you reach Claude through **AWS Bedrock** instead of the Anthropic API, point Claude Code's
Bedrock backend at your agent network endpoint. NetBird holds the Bedrock API key
server-side and injects it, so Claude Code skips AWS authentication entirely — the client
server-side and injects it, so Claude Code skips AWS authentication entirely. The client
stays keyless.
First connect an [AWS Bedrock provider](/agent-network/integrations/bedrock) in NetBird.
@@ -151,11 +151,54 @@ Then add the following to `~/.claude/settings.json`:
```
- `CLAUDE_CODE_USE_BEDROCK=1` routes Claude Code through the Bedrock backend.
- `CLAUDE_CODE_SKIP_BEDROCK_AUTH=1` skips AWS auth on the client NetBird injects the
- `CLAUDE_CODE_SKIP_BEDROCK_AUTH=1` skips AWS auth on the client: NetBird injects the
Bedrock API key server-side.
- `ANTHROPIC_BEDROCK_BASE_URL` is your agent network endpoint with the `/bedrock` suffix
(the optional gateway-namespace prefix that disambiguates Bedrock from other providers).
- `ANTHROPIC_MODEL` is the full Bedrock model ID including the region prefix (e.g.
`eu.anthropic.claude-sonnet-4-5-20250929-v1:0`). Some models may not be available in all
regions — if the model above doesn't work, switch to one in your provider's allowed list,
regions. If the model above doesn't work, switch to one in your provider's allowed list,
or change it in Claude Code with `/model <model-id>`.
## Use Kimi (Moonshot AI)
If you reach Claude through **Moonshot AI** instead of the Anthropic API, point Claude Code's
Anthropic backend at your agent network endpoint. Moonshot serves the Anthropic Messages API
under the `/anthropic` path, so Claude Code talks to the Kimi models through the same
interface it uses for Claude. NetBird holds the Moonshot API key server-side and injects it,
so the client stays keyless.
First connect a [Kimi (Moonshot AI) provider](/agent-network/integrations/kimi) in NetBird,
keeping the default upstream URL `https://api.moonshot.ai`. Then add the following to
`~/.claude/settings.json`:
```json
{
"apiKeyHelper": "echo '-'",
"env": {
"ANTHROPIC_BASE_URL": "https://<your-endpoint>/anthropic",
"ANTHROPIC_MODEL": "kimi-k3",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-k3",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k3",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "kimi-k3",
"CLAUDE_CODE_SUBAGENT_MODEL": "kimi-k3",
"ENABLE_TOOL_SEARCH": "false"
}
}
```
- `ANTHROPIC_BASE_URL` is your agent network endpoint with the `/anthropic` suffix. That is
the path Moonshot serves the Anthropic Messages API under, and NetBird rides it through to
the bare `https://api.moonshot.ai` upstream.
- The `ANTHROPIC_DEFAULT_*` and `CLAUDE_CODE_SUBAGENT_MODEL` variables pin every model tier
(opus, sonnet, haiku, and subagents) to `kimi-k3`, so no Claude model names leak into
requests Moonshot can't serve.
- `ENABLE_TOOL_SEARCH=false` turns off tool search, whose `tool_reference` blocks Moonshot
rejects.
In the **Configure Your Agent** modal, pick **Kimi (Moonshot AI)** on the **Claude Code** tab
to copy this configuration with your endpoint filled in:
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-configure-agent-claude-code-kimi.png" alt="NetBird Configure Your Agent modal showing the Claude Code settings.json configuration for Kimi (Moonshot AI)" className="imagewrapper" />
</p>

View File

@@ -1,5 +1,5 @@
export const description =
'Point the Codex CLI at your NetBird Agent Network endpoint with a custom model provider — keyless, over the tunnel.'
'Point the Codex CLI at your NetBird Agent Network endpoint with a custom model provider. Keyless, over the tunnel.'
# Keyless Access to Codex
@@ -13,8 +13,8 @@ into one your team reaches with their existing identity:
- **Keyless access through your IdP.** No OpenAI API key is distributed to or stored on any
developer's machine. Each person runs Codex over the NetBird tunnel, and the request is
tied to their real identity from your identity provider (Okta, Microsoft Entra ID,
Google, …). Onboarding and offboarding follow the same IdP groups you already manage
there's no key to hand out, copy, or revoke.
Google, …). Onboarding and offboarding follow the same IdP groups you already manage.
There's no key to hand out, copy, or revoke.
- **Usage tracking per developer and group.** Every request is metered by identity, model,
tokens, and cost, so you can see exactly who is using Codex and how much it costs, broken
down per person and aggregated per IdP group (team, department, project) in
@@ -30,7 +30,7 @@ endpoint.
1. Go to **Agent Network → Providers** and click **Connect Provider**.
2. Select **OpenAI** (or another OpenAI-compatible provider or gateway) and paste its API key.
3. Save the provider. The key is now held server-side the next step authorizes who can use it.
3. Save the provider. The key is now held server-side, the next step authorizes who can use it.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-connect-openai.png" alt="connect OpenAI provider in NetBird Agent Network" className="imagewrapper" />
@@ -40,7 +40,7 @@ See [Providers](/agent-network/providers) for details.
## Create a Policy
By default nothing is allowed a policy must connect a source group to the OpenAI provider
By default nothing is allowed: a policy must connect a source group to the OpenAI provider
before anyone can route Codex through it.
1. Go to **Agent Network → Policies** and add a policy.

View File

@@ -1,11 +1,11 @@
export const description =
'Connect specific agent tools and gateways to NetBird Agent Network Claude Code, Codex, and LiteLLM.'
'Connect specific agent tools and gateways to NetBird Agent Network: Claude Code, Codex, and LiteLLM.'
# Integrations
These guides show how to point common AI tools and gateways at your
[agent network endpoint](/agent-network/how-it-works#llm-apis-and-ai-gateways). In every
case the client holds no provider API key NetBird authorizes the request against your
case the client holds no provider API key. NetBird authorizes the request against your
[policies](/agent-network/policies) and injects the upstream key server-side.
Replace `<your-endpoint>` in the snippets below with the endpoint shown on the
@@ -13,11 +13,15 @@ Replace `<your-endpoint>` in the snippets below with the endpoint shown on the
## In This Section
- [Claude Code](/agent-network/integrations/claude-code) route Claude Code through NetBird.
- [Codex](/agent-network/integrations/codex) point the Codex CLI at the endpoint.
- [LiteLLM](/agent-network/integrations/litellm) use a LiteLLM gateway with identity-based
- [Claude Code](/agent-network/integrations/claude-code): route Claude Code through NetBird.
- [Codex](/agent-network/integrations/codex): point the Codex CLI at the endpoint.
- [LiteLLM](/agent-network/integrations/litellm): use a LiteLLM gateway with identity-based
attribution and budgets.
- [Google Vertex AI](/agent-network/integrations/vertex-ai) — connect Gemini and Claude on
- [vLLM](/agent-network/integrations/vllm): connect a self-hosted, OpenAI-compatible vLLM
server.
- [Google Vertex AI](/agent-network/integrations/vertex-ai): connect Gemini and Claude on
Vertex AI with a Google Cloud service account.
- [AWS Bedrock](/agent-network/integrations/bedrock) connect Claude, Llama, and Nova on
- [AWS Bedrock](/agent-network/integrations/bedrock): connect Claude, Llama, and Nova on
Bedrock with a Bedrock API key.
- [Kimi (Moonshot AI)](/agent-network/integrations/kimi): connect Moonshot's Kimi models and
point Kimi CLI or Claude Code at the endpoint.

View File

@@ -0,0 +1,109 @@
import { Note } from '@/components/mdx'
export const description =
'Connect Moonshot AI (Kimi) to NetBird Agent Network with a single API key, then point Kimi CLI or Claude Code at your keyless agent network endpoint.'
# Kimi (Moonshot AI)
[Moonshot AI](https://www.moonshot.ai/) serves the **Kimi** models from its cloud platform.
Connecting it behind NetBird gives your agents keyless access over the tunnel: NetBird holds
the Moonshot API key server-side, ties every request to a real identity from your IdP, and
applies your policies, limits, and audit on the way to Moonshot.
One Moonshot API key serves both API shapes, the native **Anthropic Messages API** and the
**OpenAI-compatible API**, so a single Kimi provider covers every client. The path a client
calls picks the shape, and NetBird forwards that path through to Moonshot unchanged:
- Clients that speak the Anthropic Messages API (Claude Code, Kimi CLI's `anthropic` provider
type) call the `/anthropic` path.
- OpenAI-shaped clients (the OpenAI SDK, cURL) call the `/v1` path.
## Prerequisites
- A **Moonshot AI** account on the [international platform](https://platform.moonshot.ai/)
with an API key. Accounts on the mainland-China platform
([platform.moonshot.cn](https://platform.moonshot.cn/)) use a different host. See the note
under [Connect the Provider](#connect-the-provider).
## Connect the Provider
1. Go to **Agent Network → Providers** and click **Connect Provider**.
2. Select **Kimi (Moonshot AI) API**. Keep the default **Upstream URL** `https://api.moonshot.ai`, the bare
international host. Don't add a path suffix: the client picks the API shape with the path
it calls (`/anthropic` or `/v1`), and NetBird rides that path through to Moonshot.
3. Paste the **Moonshot API key**. NetBird stores it encrypted server-side, injects it as
`Authorization: Bearer …` on each request, and never returns it to callers.
4. _(Optional)_ Restrict the **allowed models**, for example `kimi-k3`. Leaving the list
empty allows any catalog model.
5. Save the provider. The key is now held server-side. The next step authorizes who can use
it.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-connect-kimi.png" alt="Connect the Kimi (Moonshot AI) provider in NetBird Agent Network" className="imagewrapper" />
</p>
<Note>
Mainland-China accounts use the `https://api.moonshot.cn` host instead. Set that as the
**Upstream URL** if your API key was issued on `platform.moonshot.cn`.
</Note>
See [Providers](/agent-network/providers) for details.
## Create a Policy
By default nothing is allowed. A policy must connect a source group to the Kimi provider
before anyone can route through it.
1. Go to **Agent Network → Policies** and add a policy.
2. Set the **Source** to the users or agents who should be able to reach Kimi (for example
your `Engineering` group from your IdP).
3. Set the **Provider** to the Kimi provider you just connected.
4. Optionally attach per-user or per-group [token and budget limits](/agent-network/policies/limits)
and [guardrails](/agent-network/policies/guardrails) such as a model allowlist.
See [Policies](/agent-network/policies) for details.
## Use with Kimi CLI
[Kimi CLI](https://www.kimi.com/) reads providers from `~/.kimi/config.toml`. Point it at your
agent network endpoint with the `anthropic` provider type and no `/anthropic` prefix in
`base_url`; NetBird forwards the path Kimi CLI calls through to Moonshot. The `api_key` is a
placeholder because NetBird injects the real key server-side.
Add the following to `~/.kimi/config.toml`:
```toml
default_model = "kimi-k3"
[providers.netbird]
type = "anthropic"
base_url = "https://<your-endpoint>"
api_key = "-"
[models.kimi-k3]
provider = "netbird"
model = "kimi-k3"
max_context_size = 1000000
```
For the OpenAI shape instead, set `type = "openai_legacy"` and
`base_url = "https://<your-endpoint>/v1"`.
## Use with Claude Code
Claude Code speaks the Anthropic Messages API, which Moonshot serves under the `/anthropic`
path, so you can route Claude Code through this Kimi provider instead of the Anthropic API.
See [Use Kimi (Moonshot AI)](/agent-network/integrations/claude-code#use-kimi-moonshot-ai)
on the Claude Code integration page for the configuration.
## Result
Agents point at the NetBird endpoint with no key and call Kimi models by name. NetBird
enforces your policies, limits, and guardrails, then forwards the request to Moonshot. Every
call is recorded in [Usage & Logs](/agent-network/usage-and-logs) with the caller's identity,
auth group, model, tokens, cost, and status.
<Note>
Rotating the key is a single server-side change in NetBird: generate a new Moonshot API key,
update the provider's credential, then revoke the old key.
</Note>

View File

@@ -20,7 +20,7 @@ same network as the proxy so the proxy can reach it directly (for example
1. Go to **Agent Network → Providers** and click **Connect Provider**.
2. Select **LiteLLM Proxy** and set the **Upstream URL** to your self-hosted LiteLLM instance.
3. Paste a LiteLLM **virtual key** as the API key. NetBird stores it server-side.
4. Save the provider. The key is now held server-side the next step authorizes who can use it.
4. Save the provider. The key is now held server-side, the next step authorizes who can use it.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-connect-litellm.png" alt="connect a self-hosted LiteLLM Proxy provider in NetBird Agent Network" className="imagewrapper" />
@@ -28,7 +28,7 @@ same network as the proxy so the proxy can reach it directly (for example
## Create a Policy
By default nothing is allowed a policy must connect a source group to the LiteLLM provider
By default nothing is allowed: a policy must connect a source group to the LiteLLM provider
before anyone can route through it.
1. Go to **Agent Network → Policies** and add a policy.
@@ -54,7 +54,13 @@ the gateway can attribute usage and enforce its own controls:
budgets and rate limits.
- The **user identity** is sent in the `x-litellm-end-user-id` header.
The proxy strips any client-supplied value first, so an app can't spoof its identity.
The proxy strips any client-supplied value first, so an app can't spoof its identity. To stop
forwarding identity to LiteLLM, turn off
[Forward identity metadata](/agent-network/providers#identity-metadata) on the provider.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-litellm-mappings.png" alt="LiteLLM identity mappings: x-litellm-end-user-id to user email and metadata.tags to groups" className="imagewrapper" />
</p>
<Note>
The configured key must be a LiteLLM **virtual key** with `metadata.allow_client_tags: true`,
@@ -83,7 +89,7 @@ each NetBird IdP group is listed as a tag passed dynamically in the request.
## Result
Agents point at the NetBird endpoint with no key. NetBird enforces your policies, limits,
and guardrails first, then LiteLLM applies its own tag and end-user budgets on top driven
and guardrails first, then LiteLLM applies its own tag and end-user budgets on top, driven
by the same NetBird identity. Every call is recorded in
[Usage & Logs](/agent-network/usage-and-logs), where each LiteLLM request shows the caller's
identity, auth group, model, tokens, cost, and status.

View File

@@ -41,7 +41,7 @@ gcloud iam service-accounts create netbird-vertex \
## Grant IAM Roles
The service account needs two roles one to call Vertex AI models, and one to consume the
The service account needs two roles, one to call Vertex AI models, and one to consume the
project's enabled services:
```bash
@@ -62,7 +62,7 @@ gcloud iam service-accounts keys create netbird-vertex-key.json \
```
<Warning>
The key file grants access to Vertex AI in your project. Treat it as a secret store it
The key file grants access to Vertex AI in your project. Treat it as a secret: store it
securely, never commit it to source control, and delete the local copy once it's stored in
NetBird.
</Warning>
@@ -89,17 +89,17 @@ cat netbird-vertex-key.json | base64 -w 0
2. Select **Google Vertex AI**. NetBird pre-fills the upstream URL
(`https://aiplatform.googleapis.com`) and the correct auth handling for Vertex.
3. Provide the **service account key** you generated (`netbird-vertex-key.json`). NetBird stores it encrypted server-side and never returns it to callers.
4. _(Optional)_ Restrict the **allowed models** and set per-model pricing for example
4. _(Optional)_ Restrict the **allowed models** and set per-model pricing, for example
`gemini-2.5-pro`, `gemini-2.5-flash`, `claude-sonnet-4-6`, or
`claude-opus-4-7`. Leaving the list empty allows any catalog model.
5. Save the provider. The credential is now held server-side the next step authorizes who
5. Save the provider. The credential is now held server-side, the next step authorizes who
can use it.
See [Providers](/agent-network/providers) for details.
## Create a Policy
By default nothing is allowed a policy must connect a source group to the Vertex AI
By default nothing is allowed: a policy must connect a source group to the Vertex AI
provider before anyone can route through it.
1. Go to **Agent Network → Policies** and add a policy.
@@ -114,7 +114,7 @@ See [Policies](/agent-network/policies) for details.
## Use with Claude Code
To route [Claude Code](/agent-network/integrations/claude-code) through this Vertex AI
provider instead of the Anthropic API, see [Use Claude on Vertex AI](/agent-network/integrations/claude-code#use-claude-on-vertex-ai)
provider instead of the Anthropic API. See [Use Claude on Vertex AI](/agent-network/integrations/claude-code#use-claude-on-vertex-ai)
on the Claude Code integration page.
## Manage Service Account Keys

View File

@@ -0,0 +1,71 @@
import { Note } from '@/components/mdx'
export const description =
'Serve a self-hosted vLLM model behind NetBird Agent Network: keyless, tunnel-only access to your OpenAI-compatible vLLM endpoint with policy-based authorization.'
# vLLM
[vLLM](https://docs.vllm.ai) is a high-throughput inference server for self-hosted models
that exposes an **OpenAI-compatible API**. You run it yourself, typically on a GPU host
inside the same network as the NetBird proxy, and connect it as a provider so agents reach
it keyless over the tunnel, authorized by your [policies](/agent-network/policies).
This gives you keyless access to your private vLLM from anywhere: the endpoint stays
off the public internet, and any authorized user or agent reaches it over the NetBird
tunnel without shared API keys.
vLLM behaves like a [custom / self-hosted provider](/agent-network/providers#custom-and-self-hosted-providers);
it simply has its own named entry in the provider picker.
## Connect vLLM as a Provider
Because vLLM is self-hosted, the upstream URL points at your own instance. Host it in the
same network as the proxy so the proxy can reach it directly.
1. Go to **Agent Network → Providers** and click **Connect Provider**.
2. Select **vLLM** and set the **Upstream URL** to your vLLM server's OpenAI-compatible base
URL, for example `https://vllm.internal:8000`.
3. If your vLLM server was started with an API key (`--api-key`), paste it as the **API key**;
NetBird stores it server-side and sends it as a bearer token. Leave it empty if the server
requires none.
4. _(Optional)_ Enable **Skip TLS Verification** if your vLLM endpoint serves a self-signed
certificate. See [Skip TLS Verification](/agent-network/providers#skip-tls-verification).
Prefer mounting trusted certificates on your proxy instances for anything beyond testing.
5. _(Optional)_ List the **models** your server exposes with per-model pricing so cost shows
up in [Usage & Logs](/agent-network/usage-and-logs). Leaving the list empty accepts any
model name the server serves.
6. Save the provider.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-connect-vllm.png" alt="connect a self-hosted vLLM provider in NetBird Agent Network" className="imagewrapper" />
</p>
## Create a Policy
By default nothing is allowed: a policy must connect a source group to the vLLM provider
before anyone can route through it.
1. Go to **Agent Network → Policies** and add a policy.
2. Set the **Source** to the users or agents who should be able to use vLLM (for example your
`Engineering` group from your IdP).
3. Set the **Provider** to the vLLM provider you just connected.
4. Optionally attach per-user or per-group [token and budget limits](/agent-network/policies/limits)
and [guardrails](/agent-network/policies/guardrails) such as a model allowlist.
<p>
<img src="/docs-static/img/agent-network/integrations/agent-network-create-policy-vllm.png" alt="create a NetBird Agent Network policy authorizing vLLM" className="imagewrapper" />
</p>
See [Policies](/agent-network/policies) for details.
## Result
Agents point at the NetBird endpoint with no key and call your vLLM models by name. NetBird
enforces your policies, limits, and guardrails, then forwards the request to your vLLM server.
Every call is recorded in [Usage & Logs](/agent-network/usage-and-logs) with the caller's
identity, auth group, model, tokens, cost, and status.
<Note>
vLLM speaks the OpenAI API shape, so point OpenAI-compatible clients at
`https://<your-endpoint>` and select a model your server hosts.
</Note>

View File

@@ -19,7 +19,7 @@ denied.
Optionally store request prompts and response completions on logged requests.
Prompt capture only runs when **both** the account-level prompt collection
setting and a policy guardrail enable it — see
setting and a policy guardrail enable it. See
[Log Collection & Retention](/agent-network/usage-and-logs/log-collection).
### PII Redaction

View File

@@ -3,7 +3,7 @@ export const description =
# Policies
Policies connect users and agents (source groups) to AI providers controlling
Policies connect users and agents (source groups) to AI providers, controlling
which identities can reach which providers and models, with optional limits and
guardrails.
@@ -13,16 +13,16 @@ guardrails.
<Note>
This page explains how to create and manage access to AI providers and gateways. If you are
looking for a guide on how to manage access to internal resources, see
looking for a guide on how to manage access to internal resources. See
[Access Control](/manage/access-control).
</Note>
## How Policies Work
- **Source groups** the users/agents the policy applies to.
- **Destination providers** the providers the policy grants access to.
- **Limits** optional per-user and per-group token and budget caps.
- **Guardrails** optional model allowlist and prompt capture.
- **Source groups**: the users/agents the policy applies to.
- **Destination providers**: the providers the policy grants access to.
- **Limits**: optional per-user and per-group token and budget caps.
- **Guardrails**: optional model allowlist and prompt capture.
A request is allowed when a policy connects the caller's groups to the resolved
provider and no applicable limit is exhausted.

View File

@@ -15,15 +15,15 @@ on the token and/or budget limit, and set the per-user and per-group caps and wi
## Token Limits
- **User cap** maximum tokens per user in the window.
- **Group cap** maximum tokens per group in the window.
- **Window** the fixed period the cap applies to (see [below](#how-the-window-works)).
- **User cap**: maximum tokens per user in the window.
- **Group cap**: maximum tokens per group in the window.
- **Window**: the fixed period the cap applies to (see [below](#how-the-window-works)).
## Budget Limits
- **User cap (USD)** maximum spend per user in the window.
- **Group cap (USD)** maximum spend per group in the window.
- **Window** the fixed period the cap applies to (see [below](#how-the-window-works)).
- **User cap (USD)**: maximum spend per user in the window.
- **Group cap (USD)**: maximum spend per group in the window.
- **Window**: the fixed period the cap applies to (see [below](#how-the-window-works)).
## How the Window Works
@@ -44,10 +44,10 @@ This alignment is deliberate: because every node derives the same boundaries fro
clock, usage adds up consistently across a clustered deployment instead of drifting with
each node's first request.
**Example a 1-hour (3600s) window.** Buckets run from the top of each hour in UTC:
**Example: a 1-hour (3600s) window.** Buckets run from the top of each hour in UTC:
`13:00:0013:59:59`, `14:00:0014:59:59`, and so on. A request at `13:45` counts toward the
`13:00` bucket; a request at `14:02` lands in a fresh `14:00` bucket with the counter back
at zero regardless of when the user's first request of the day was.
at zero, regardless of when the user's first request of the day was.
## How Enforcement Works
@@ -63,13 +63,13 @@ request that crosses the line still completes; the **next** one is blocked.
**Example.** A token cap is set to `1000` for a group, and the group has used `999` in the
current window:
- The next request is checked: `999 >= 1000` is false, so it **goes through** even though
- The next request is checked: `999 >= 1000` is false, so it **goes through**: even though
it then consumes, say, 250 tokens and pushes the total to `1249`.
- The following request is checked: `1249 >= 1000` is true, so it is **blocked** until the
window rolls over.
So a cap is a floor for *when blocking starts*, not a hard ceiling on the exact total — plan
So a cap is a floor for *when blocking starts*, not a hard ceiling on the exact total. Plan
caps with a little headroom if you need a strict upper bound.
For account-wide caps that apply across all policies, see
For account-wide caps that apply across all policies. See
[Global Limits](/agent-network/global-limits).

View File

@@ -1,5 +1,5 @@
export const description =
'Connect AI providers and gateways OpenAI, Anthropic, Azure OpenAI, AWS Bedrock, Google Vertex AI, Mistral, LiteLLM, Portkey, Bifrost, Cloudflare, Vercel, OpenRouter, or any OpenAI-compatible endpoint to NetBird Agent Network and expose a single keyless endpoint.'
'Connect AI providers and gateways (OpenAI, Anthropic, Azure OpenAI, AWS Bedrock, Google Vertex AI, Mistral, Kimi (Moonshot AI), LiteLLM, Portkey, Bifrost, Cloudflare, Vercel, OpenRouter, or any OpenAI-compatible endpoint) to NetBird Agent Network and expose a single keyless endpoint.'
# Providers
@@ -27,6 +27,7 @@ First-party vendor APIs:
- AWS Bedrock
- Google Vertex AI
- Mistral
- Kimi (Moonshot AI)
### AI Gateways
@@ -43,7 +44,7 @@ and budgets (see [How It Works](/agent-network/how-it-works#llm-apis-and-ai-gate
### Custom
- Custom / Self-hosted any OpenAI-compatible endpoint, including local models served by
- Custom / Self-hosted: any OpenAI-compatible endpoint, including local models served by
Ollama, vLLM, or a private GPU host.
## Connect a Provider
@@ -63,14 +64,89 @@ and budgets (see [How It Works](/agent-network/how-it-works#llm-apis-and-ai-gate
<img src="/docs-static/img/agent-network/providers/agent-network-create-provider.png" alt="agent network connect provider modal" className="imagewrapper" />
</p>
## Custom & Self-hosted Providers
Pick **Custom / Self-hosted** for any OpenAI-compatible endpoint that isn't a first-party
vendor or a named gateway, a private inference server, an on-prem deployment, or a local
model runtime like Ollama or vLLM (vLLM also has its own named entry). NetBird talks to it
the same way it talks to OpenAI: you provide the **Upstream URL** where requests are
forwarded and, if the endpoint requires one, an **API key** sent as a bearer token.
<p>
<img src="/docs-static/img/agent-network/providers/agent-network-custom-provider.png" alt="custom provider settings with the Skip TLS Verification switch" className="imagewrapper" />
</p>
### Skip TLS Verification
Self-hosted endpoints often serve HTTPS with a self-signed or otherwise untrusted
certificate, which makes the proxy reject the connection with an unknown-certificate error.
Enable **Skip TLS Verification** on a custom provider to disable upstream TLS certificate
validation so requests go through anyway.
<Warning>
This turns off certificate checks for that provider's upstream traffic, which removes
protection against man-in-the-middle attacks. Use it only for quick testing. For anything
beyond that, mount your CA / trusted certificates on your proxy instances instead of
skipping verification.
</Warning>
The switch appears only for custom (self-hosted) providers and is **off by default**.
### Identity Metadata
By default NetBird stamps the caller's **user** and the **group that authorized the request**
onto each upstream request, so the provider or gateway can attribute usage to the real caller
instead of the shared API key. The exact header or field is provider-specific. See the
provider's [integration guide](/agent-network/integrations) for details (for example, AWS
Bedrock carries it in a header used for [cost-allocation tags](/agent-network/integrations/bedrock#cost-allocation),
and AI gateways receive their own attribution headers).
This is controlled by the **Forward identity metadata** toggle on the provider, which is **on by
default** and shown only for providers that support it (first-party APIs such as OpenAI or
Anthropic have no such metadata channel, so the toggle doesn't appear for them). Turn it off
to keep the caller's identity out of the upstream request.
<p>
<img src="/docs-static/img/agent-network/providers/agent-network-provider-metadata.png" alt="Connect Provider modal with the Disable identity metadata toggle" className="imagewrapper" />
</p>
## Models and Pricing
Each provider carries a list of models it serves. Leaving the list empty makes the
provider a catch-all that accepts any model (typical for gateways); listing specific models
restricts routing to them. Per-model input/output prices drive the cost figures shown in
restricts routing to them. Per-model prices drive the cost figures shown in
[Usage & Logs](/agent-network/usage-and-logs); adjust them if your negotiated rates differ
from the catalog defaults.
Each model has an **input** and **output** price per 1k tokens, plus optional
**cache** rates that match how the provider bills prompt caching. Which cache
fields apply depends on the provider's pricing surface: OpenAI-shape providers
use a single **cached input** rate (cached tokens are a subset of the prompt),
while Anthropic-shape providers, including Claude on Amazon Bedrock and Google
Vertex AI, use separate **cache read** and **cache creation** rates (additive
buckets on top of input). Gateways and custom providers expose all cache fields,
since NetBird can't know the upstream shape ahead of time.
Leave a cache rate **blank** to inherit NetBird's default for that model when one
exists; set it to **0** to bill that cache bucket at the plain input rate (no
discount). See [how caching is metered](/agent-network/usage-and-logs/access-logs)
for how these buckets appear in the cost breakdown.
Self-hosters can seed the catalog defaults these fields prefill from with a
pricing file. See
[`server.agentNetwork.pricingDefaultsFile`](/selfhosted/maintenance/configuration-files#agent-network-settings).
### Adding a Model Not in the Catalog
If the model you need isn't in the picker, type its model ID directly into the **Model** field
instead of selecting from the list, for example `eu.anthropic.claude-sonnet-5`. A model NetBird
doesn't know has no catalog defaults, so set its **input** and **output** prices (and cache
rates, if applicable) yourself for usage and logs to report accurate costs.
<p>
<img src="/docs-static/img/agent-network/providers/agent-network-custom-model.png" alt="Models tab with a custom model ID and manually entered input/output pricing" className="imagewrapper" />
</p>
## The Keyless Endpoint
All connected providers share a single account endpoint, generated when you connect your

View File

@@ -29,8 +29,8 @@ and manage the platform.
## Software Requirements
- Docker with the docker-compose plugin v2 or higher ([Docker installation guide](https://docs.docker.com/engine/install/))
- [jq](https://jqlang.github.io/jq/) install with `sudo apt install jq` or `sudo yum install jq`
- [curl](https://curl.se/) install with `sudo apt install curl` or `sudo yum install curl`
- [jq](https://jqlang.github.io/jq/): install with `sudo apt install jq` or `sudo yum install jq`
- [curl](https://curl.se/): install with `sudo apt install curl` or `sudo yum install curl`
### Installation script
@@ -96,7 +96,7 @@ is needed on the client. NetBird authorizes each request against your policies a
injects the upstream provider key server-side.
1. Next to your agent network endpoint, click **Agent Config**.
2. Pick the tab that matches your tool Claude Code, Codex, OpenAI SDK, or cURL. The
2. Pick the tab that matches your tool, Claude Code, Codex, OpenAI SDK, or cURL. The
dashboard pre-fills your endpoint for you.
3. Copy the snippet and apply it. Claude Code reads `~/.claude/settings.json` and Codex
reads `~/.codex/config.toml`.

View File

@@ -1,5 +1,5 @@
export const description =
'The per-request Agent Network access log: caller identity, provider, model, tokens, cost, decision, and reason, with server-side filtering.'
'The per-request Agent Network access log: caller identity, provider, model, tokens, cost, decision, and reason, with server-side filtering and a grouped-by-session view.'
# Access Logs
@@ -14,14 +14,108 @@ and the reason a request was allowed or denied.
## Columns
- **Time**
- **User / Agent** the resolved caller identity.
- **Auth Group** the groups that authorized the request.
- **Provider** resolved provider and model.
- **Tokens** input and output.
- **Cost**
- **Status** and **Reason** — for denials, the mapped policy reason; for
- **User / Agent**: the resolved caller identity.
- **Auth Group**: the groups that authorized the request.
- **Provider**: resolved provider and model.
- **Tokens**: input and output. Hover for the full breakdown, including
prompt-cache reads and writes when the request used caching.
- **Cost**: hover for the per-bucket breakdown of input, output, cache read,
and cache write, plus the total.
- **Status** and **Reason**: for denials, the mapped policy reason. For
allowed requests, a link to the policy that authorized it.
## Prompt caching
Agents that use provider-side prompt caching (for example Claude Code caches
its system prompt and tools) are billed for cache reads and writes on top of
regular input tokens. Agent Network records each bucket per request, so the
cache share of your spend is visible instead of hidden inside the total.
Caching applies to the **prompt only**. Output tokens are generated fresh on
every call, so no provider discounts them and there is no cached-output bucket.
Every request is metered against four separately priced token buckets: input
(prompt tokens not served from cache), output (the completion), cache read
(prompt tokens replayed from the cache, usually about a tenth of the input
rate), and cache write (prompt tokens written into the cache, usually 1.25× the
input rate).
Because cache writes bill at a premium, the first request of a session can cost
noticeably more than its input and output counts alone suggest, while the
follow-up requests that read that cache cost much less.
Hovering **Tokens** shows which buckets a request actually used. Here a
follow-up request sent only 16 fresh input tokens but replayed 3,980 tokens from
the cache, wrote none, and produced 192 of output, for 4,188 tokens in total:
<p>
<img src="/docs-static/img/agent-network/usage-and-logs/agent-network-access-log-token-breakdown.png" alt="agent network access log token breakdown with prompt-cache reads and writes" className="imagewrapper-small" />
</p>
Hovering **Cost** breaks the same request down in dollars. Most of the spend
here is output, with the 3,980 cached tokens costing only $0.0020 at the reduced
cache-read rate:
<p>
<img src="/docs-static/img/agent-network/usage-and-logs/agent-network-access-log-cost-breakdown.png" alt="agent network access log cost breakdown by token bucket" className="imagewrapper-small" />
</p>
All four buckets are listed even when one costs nothing, so a zero cache-write
line tells you this request read the cache rather than populating it. The total
is always the sum of the four, so the breakdown and the headline figure can
never disagree.
<Note>
Providers report cache tokens differently, which affects how the **Tokens**
column reads. Anthropic-shape providers, including Claude on Amazon Bedrock
and Google Vertex AI, report cache reads and writes *in addition to*
`input_tokens`, so the total is input + output + both cache buckets. OpenAI
instead counts cached tokens as a *subset* of the prompt, so they are already
inside `input_tokens`. The cost breakdown normalizes this: the input line is
always the non-cached remainder, so the same tokens are never billed twice.
</Note>
The same buckets are summed per session in the **Sessions** view, and the
[usage overview](/agent-network/usage-and-logs/usage-overview) shows the cache
share of each day's spend.
## Sessions
Use the **Requests / Sessions** switch above the log to change the view. The
**Sessions** view groups related requests into a single session and shows one
row per session, most recent activity first, so you can see the latest sessions
at a glance.
<p>
<img src="/docs-static/img/agent-network/usage-and-logs/agent-network-access-log-sessions.png" alt="agent network access logs grouped by session" className="imagewrapper-big" />
</p>
Some agents keep a session across many separate calls. For example,
[Claude Code](/agent-network/integrations/claude-code) tags every request in a
coding session with the same session id. Agent Network groups those requests
together, so a whole session shows up as one row with its totals instead of
dozens of individual entries.
Each session row summarizes:
- **Activity**: the session's time span, from its first request to its last.
- **User / Agent** and **Auth Group**: the caller and the authorizing groups.
- **Provider**: the provider and the model(s) used (e.g. `2 models`; hover to
see the full list).
- **Requests**: how many requests the session made, and over what duration.
- **Tokens** and **Cost**: summed across the whole session. Hover for the
breakdown, including the session's prompt-cache tokens and the same four cost
buckets as a single request.
- **Reason**: the session's policy decision, a denial if any request in the
session was blocked.
Expand a session to see its individual requests, each with its timestamp, path,
status, duration, model, tokens, and cost. Expand a request for its full detail,
including the prompt and response when prompt capture is on.
Requests from agents that don't set a session id each appear as their own
single-request row.
## Filtering
<p>
@@ -30,17 +124,18 @@ and the reason a request was allowed or denied.
Filter the log by:
- **Date** defaults to the last 14 days.
- **Date**: defaults to the last 14 days.
- **User**
- **Group**
- **Provider**
- **Model**
- **Path** match requests whose path starts with a given prefix (e.g. `/v1/messages`).
- **Path**: match requests whose path starts with a given prefix (e.g. `/v1/messages`).
All filtering is applied server-side.
All filtering is applied server-side and works in both the Requests and Sessions
views.
## Availability
Access logs are retained only when log collection is enabled for the account.
Usage and cost are still recorded when it's off — see
Usage and cost are still recorded when it's off. See
[Log Collection & Retention](/agent-network/usage-and-logs/log-collection).

View File

@@ -8,11 +8,11 @@ identity, cost attribution, and token usage.
## In This Section
- [Usage Overview](/agent-network/usage-and-logs/usage-overview) token and
- [Usage Overview](/agent-network/usage-and-logs/usage-overview): token and
cost trends over time.
- [Access Logs](/agent-network/usage-and-logs/access-logs) per-request log
- [Access Logs](/agent-network/usage-and-logs/access-logs): per-request log
with server-side filtering.
- [Log Collection & Retention](/agent-network/usage-and-logs/log-collection)
- [Log Collection & Retention](/agent-network/usage-and-logs/log-collection):
enable/disable logging, set retention, and control prompt capture.
## Usage vs. Logs

View File

@@ -14,7 +14,7 @@ Tokens / Cost switch, plus a breakdown table.
Filter the view by:
- **Date** defaults to the last 14 days.
- **Date**: defaults to the last 14 days.
- **User**
- **Group**
- **Provider**
@@ -28,6 +28,10 @@ Switch between input/output token totals and estimated USD spend. Cost is
derived from the per-model pricing configured on each
[provider](/agent-network/providers).
Hovering a day, in the chart or in the breakdown table, also shows its
prompt-cache token buckets (cache reads and writes) and the cache share of the
day's cost.
## Always Collected
Usage is recorded on every request independently of access-log collection, so

View File

@@ -28,13 +28,11 @@ When Block Inbound Connections is enabled, the client will not add any inbound f
This setting overrides all policies from the management service. Even if an access control policy explicitly allows traffic to this peer, inbound connections will still be blocked.
</Note>
## Enabling via the system tray
## Changing the setting in the desktop app
1. Click the NetBird icon in the system tray.
2. Go to **Settings**.
3. Click **Block Inbound Connections** to toggle the setting.
When enabled, a checkmark will appear next to the menu item.
1. Open the NetBird desktop app.
2. Go to **Settings → Security**.
3. Toggle **Block Inbound Traffic**.
## Enabling via the CLI
@@ -49,7 +47,3 @@ To disable it, run:
```bash
netbird up --block-inbound=false
```
<Note>
When toggling this setting via the CLI, the system tray UI may not reflect the change until the NetBird GUI is restarted.
</Note>

View File

@@ -18,20 +18,20 @@ When Connect on Startup is **enabled** (default behavior):
When Connect on Startup is **disabled**:
- The NetBird service starts but does not connect automatically.
- You must manually run `netbird up` or use the system tray to connect.
- You must manually run `netbird up` or click **Connect** in the desktop app or tray menu.
- The peer remains offline until an explicit connection is initiated.
<Note>
Whether the device successfully connects also depends on the <a href="/manage/settings/enforce-periodic-user-authentication">Peer Session Expiration</a> setting under Settings &gt; Authentication. If the peer's session has expired, the user must reauthenticate before the peer can connect.
</Note>
## Enabling via the system tray
## Changing the setting in the desktop app
1. Click the NetBird icon in the system tray.
2. Go to **Settings**.
3. Click **Connect on Startup** to toggle the setting.
1. Open the NetBird desktop app.
2. Go to **Settings → General**.
3. Toggle **Connect on Startup**.
When enabled, a checkmark will appear next to the menu item.
This setting controls the background network connection. It is separate from **Launch NetBird UI at Login**, which controls only whether the graphical interface opens when you sign in to the operating system.
## Enabling via the CLI
@@ -46,7 +46,3 @@ To re-enable it:
```bash
netbird up --disable-auto-connect=false
```
<Note>
When toggling this setting via the CLI, the system tray UI may not reflect the change until the NetBird GUI is restarted.
</Note>

View File

@@ -4,11 +4,13 @@ export const description = "Overview of the redesigned NetBird desktop app: Defa
# NetBird Desktop App
<Note>
The redesigned desktop app described on this page is currently available as a **release candidate**. Behavior and screenshots may change before the stable release. To try it, download the latest release candidate from [pkgs.netbird.io](https://pkgs.netbird.io/releases/rc).
</Note>
Starting with NetBird v0.75.0, the desktop app uses a [Wails](https://wails.io/) webview with a React frontend. The management server and wire protocol are unchanged, but the UI and agent need to be updated together. If their versions do not match, the app displays an update notification.
The NetBird desktop app has been rebuilt from the ground up. It runs on a [Wails](https://wails.io/) webview with a React frontend, while the Go backend that talks to the daemon stays the same. The management server and wire protocol are unchanged. The redesigned app is not backward compatible with older agents, though, so the app and the agent need to be updated together. If their versions do not match, the app shows a notification prompting you to update.
## First Launch
On first launch, the welcome dialog points you to the NetBird icon in the Windows or Linux system tray, or the macOS menu bar. Choose whether to connect to **NetBird Cloud** or a **Self-hosted** deployment. For a self-hosted deployment, enter its management URL before connecting.
Click **Connect** in the main window or tray menu. NetBird opens your browser to authenticate the device. Once authorization completes, the desktop app connects and displays the device's NetBird IP address.
## Default and Advanced Views
@@ -24,7 +26,7 @@ Switch to the **Advanced** view and the window expands with a peers and resource
<img src="/docs-static/img/client/desktop-app/peers-view.png" alt="Advanced view showing the Peers tab" className="imagewrapper"/>
</p>
The **Resources** tab lists the network resources available to you.
The **Resources** tab lists the network resources available to you. You can search resources, show only active or overlapping entries, toggle individual resources, or enable and disable all resources at once.
<p>
<img src="/docs-static/img/client/desktop-app/resources-view.png" alt="Advanced view showing the Resources tab" className="imagewrapper"/>
@@ -50,16 +52,23 @@ A colored status dot sits next to the tray icon so you can read the state of you
<img src="/docs-static/img/client/desktop-app/tray-status.png" alt="NetBird system tray status indicator" className="imagewrapper"/>
</p>
The app also includes per-platform refinements: improved behavior on Linux X11 desktops, a macOS Dock icon that only appears when an app window is open, and improved status rendering on Windows.
The tray menu also provides profile switching, session-renewal access, exit-node selection, Settings, Help & Support, and **Quit NetBird**. Closing the main window only hides it while the daemon and network connection continue running. Choosing **Quit NetBird** disconnects the daemon before the desktop app exits.
## Exit Nodes
The compact view shows the active exit node. Open the exit-node selector in the main window or tray menu to select another available exit node or return to a direct connection by deselecting it.
## Settings
The settings page uses a tabbed layout that groups options by what they control. The tabs you see depend on your role and on any [MDM policy](#mdm-driven-ui) in effect. The full set is:
* **General** for everyday toggles: auto-start, notifications, and display language.
* **Network** and **Security** for connection-side options such as PSK, custom DNS port, lazy connections, and server SSH.
* **General** for everyday toggles: Connect on Startup, Launch NetBird UI at Login, notifications, and display language.
* **Network** for connectivity, DNS, routes, and IPv6 settings.
* **Security** for inbound traffic, LAN access, and quantum-resistant encryption settings.
* **Profiles** for managing the accounts the app knows about. See [Profiles](/client/profiles).
* **SSH** and **Advanced** for options such as log level, network monitor, and native SSH.
* **SSH** for the native SSH server and its optional features. Some of these
require privileges: see the note below.
* **Advanced** for log-level and custom configuration options.
* **Troubleshoot** for [capturing a debug bundle](#capturing-a-debug-bundle).
* **About** for version information, useful links, and updating the app.
@@ -67,12 +76,28 @@ The settings page uses a tabbed layout that groups options by what they control.
<img src="/docs-static/img/client/desktop-app/settings-language.png" alt="Settings page with the General tab and Display Language picker" className="imagewrapper"/>
</p>
<Note>
The changes that decide who may obtain a shell on the machine, **Enable SSH
Server**, **Enable Root Login** and turning SSH authentication off, can only be
made by a privileged caller, because the background service runs as root on Linux and macOS
and as LocalSystem on Windows while the app runs as you. The app shows those
switches as unavailable and puts the equivalent command next to them: run it with
`sudo` on Linux and macOS, or from an elevated prompt on Windows. Without those
rights, an administrator has to run it. The SSH server alone can also be turned on
from [MDM](#mdm-driven-ui) policy with `allowServerSSH`, which the client applies
itself; root login and SSH authentication have no policy key. A switch already in its safe
state stays operable: turning the SSH server off, turning root login off, and
re-enabling SSH authentication never require privileges.
</Note>
**Connect on Startup** controls whether the background service reconnects when it starts. **Launch NetBird UI at Login** controls only whether the graphical interface opens when you sign in to the operating system. On a fresh desktop installation, launch at login is enabled once by default. Upgrades preserve the user's existing preference. Administrators can manage this behavior with [`disableAutostart`](/client/mdm-integration#disableAutostart).
## MDM-Driven UI
If you roll out NetBird through MDM, the app reads the policy in effect on the device and adjusts itself to match, so users only see the options they are allowed to use. For deploying NetBird through an MDM provider, see the [MDM deployment guides](/manage/integrations/mdm-deployment/intune-netbird-integration).
* **Hide whole views.** `DisableAdvancedView` hides the Advanced view of the main window, so managed users only see the compact one.
* **Gate specific capabilities.** Flags such as `AllowServerSSH` control whether the corresponding toggle is available in the app.
* **Hide whole views.** `disableAdvancedView` hides the Advanced view of the main window, so managed users only see the compact one.
* **Gate specific capabilities.** Keys such as `allowServerSSH` control whether the corresponding toggle is available in the app.
* **Refresh in the background.** Restrictions are re-read when the window becomes visible or the connection status changes, so MDM-pushed policy updates apply without a restart.
<Note>
@@ -81,7 +106,7 @@ If you roll out NetBird through MDM, the app reads the policy in effect on the d
## Session Expiration and Renewal
When a session is about to expire, the app shows a dialog with a countdown and two clear actions: renew or log out. An operating system notification fires ahead of time, so the dialog is not the first signal you get.
When a session is about to expire, the app shows a dialog with a countdown and two clear actions: renew or log out. An operating system notification fires ahead of time, and the session deadline remains visible across reconnects.
<p>
<img src="/docs-static/img/client/desktop-app/session-expiration.png" alt="Session expiry dialog with a countdown and renew or logout actions" className="imagewrapper"/>
@@ -91,10 +116,14 @@ The renewal flow follows one rule: re-authenticating never closes an active conn
## Localization
You can switch languages from the **Display Language** picker in **Settings → General**, and the change applies instantly. The release candidate ships with initial translations for English, French, German, Hungarian, Italian, Portuguese, Russian, Simplified Chinese, and Spanish.
You can switch languages from the **Display Language** picker in **Settings → General**, and the change applies instantly. NetBird v0.75 ships with English, French, German, Hungarian, Italian, Japanese, Portuguese, Russian, Simplified Chinese, and Spanish.
These initial translations are still a work in progress. If your language is missing or a phrase reads incorrectly, please [open a pull request](https://github.com/netbirdio/netbird). Feedback from native speakers is exactly what helps here.
Translations are a continuing community effort. If your language is missing or a phrase reads incorrectly, please [open a pull request](https://github.com/netbirdio/netbird).
## Capturing a Debug Bundle
The **Troubleshoot** tab can capture a debug bundle that collects both the daemon logs and the UI logs at the current log level in one step. Attach the bundle when you report an issue so the team has the full picture.
The **Troubleshoot** tab can capture a debug bundle that collects daemon and UI logs in one step. You can collect new trace logs for 130 minutes, cycle the connection, include a packet capture and system information, anonymize sensitive data, and either upload the result to NetBird or keep it locally. See [Troubleshooting the client](/help/troubleshooting-client#debug-bundle-uploads-with-gui) for the complete flow.
<p>
<img src="/docs-static/img/help/troubleshooting-client/ui-bundle-local-success.png" alt="Completed debug bundle saved locally in the NetBird desktop app" className="imagewrapper"/>
</p>

View File

@@ -46,6 +46,7 @@ To clear all saved service parameters (including env vars), run `sudo netbird se
| `NB_USE_NETSTACK_MODE` | All | Run WireGuard on top of a userspace TCP/IP stack (gVisor netstack) instead of a TUN device. Required for environments without TUN support (e.g. unprivileged containers). |
| `NB_NETSTACK_SKIP_PROXY` | All | When using netstack mode, do not start the built-in SOCKS5 proxy that exposes the WireGuard network to local applications. |
| `NB_SOCKS5_LISTENER_PORT` | All | Override the port the netstack SOCKS5 proxy listens on (default: `1080`). Only relevant when netstack mode is active. |
| `NB_SOCKS5_LISTENER_ADDRESS` | All | Override the host/IP the netstack SOCKS5 proxy binds to (default: `127.0.0.1`). The proxy is unauthenticated and meant for local applications only, so it listens on loopback. Set this (e.g. to `0.0.0.0`) only when the proxy must be reachable from other hosts, such as a container gateway — this exposes an unauthenticated proxy on that address. Only relevant when netstack mode is active. |
## Firewall
@@ -71,7 +72,7 @@ To clear all saved service parameters (including env vars), run `sudo netbird se
| `NB_ICE_FAILED_TIMEOUT_SEC` | All | Seconds of silence before ICE gives up on the connection entirely and falls back to relay (default: `6`). |
| `NB_ICE_RELAY_ACCEPTANCE_MIN_WAIT_SEC` | All | Minimum seconds ICE waits for a direct (P2P) candidate before accepting a relay candidate. Higher values give direct connections more time to succeed at the cost of slower initial connection (default: `2`). |
| `NB_ICE_MONITOR_PERIOD` | All | Interval between ICE connection health checks that verify handshake freshness and trigger reconnection if needed (Go duration, default: `5m`). |
| `NB_LAZY_CONN` | All | Local override for the lazy connection feature, which opens WireGuard tunnels to peers only when traffic is detected. `on` forces it enabled, `off` forces it disabled, unset defers to the management setting. Overrides the management configuration in both directions. Replaces the removed `NB_ENABLE_EXPERIMENTAL_LAZY_CONN`. |
| `NB_LAZY_CONN` | All | Local lazy-connection override. `on` forces lazy connections enabled, `off` forces them disabled, and leaving it unset defers to the Management or MDM setting. Overrides MDM when both are configured. Replaces the removed `NB_ENABLE_EXPERIMENTAL_LAZY_CONN`; the deprecated `--enable-lazy-connection` CLI flag is inert. |
| `NB_LAZY_CONN_INACTIVITY_THRESHOLD` | All | How long a lazy connection can be idle before it is torn down (Go duration, default: `15m`). Only applies when lazy connections are enabled. |
## DNS
@@ -80,6 +81,7 @@ To clear all saved service parameters (including env vars), run `sudo netbird se
|---|---|---|
| `NB_DNS_FORWARDER_PORT` | All | Override the port the internal DNS forwarder binds to for handling DNS routes received from clients (default: `22054`). The forwarder is separate from the local resolver. |
| `NB_SKIP_DNS_PROBE` | All | Skip the startup check that sends a test query to the local resolver to verify it is reachable. Set this if the probe causes delays (e.g. in air-gapped networks). |
| `NB_DNS_LAZY_WARMUP_TIMEOUT` | All | Per-query budget for waking an idle peer when the local resolver returns one of its A or AAAA records (Go duration, default: `2s`). The value must be positive; invalid, zero, or negative values use the default. |
| `NB_UNCLEAN_SHUTDOWN_RESOLV_FILE` | Linux, FreeBSD | Custom file path to store a backup of `/etc/resolv.conf` before the client modifies it. Used to restore the original on next startup if the client crashed without cleaning up (default: `<state-dir>/resolv.conf`, i.e. `/var/lib/netbird/resolv.conf` on Linux, `/var/db/netbird/resolv.conf` on FreeBSD). |
## Connection retry

View File

@@ -0,0 +1,316 @@
import { Note, Warning } from '@/components/mdx'
export const description =
'Connect applications and development tools to the local NetBird daemon through its gRPC socket.'
# gRPC Daemon Socket
The NetBird daemon exposes a local gRPC API that the NetBird CLI and desktop app use. Your integration can use the same socket to check status, manage the connection, inspect configuration, work with profiles and networks, and call other local daemon operations.
The gRPC socket is the primary local daemon API. If your integration cannot use gRPC or generated Protocol Buffer bindings, use the [HTTP/JSON daemon socket](/client/json-socket) instead.
<Note>
The gRPC socket controls the **local NetBird daemon**. It is separate from the
NetBird Management API and does not replace it.
</Note>
## Default Addresses
The HTTP/JSON gateway is optional. The gRPC socket is available whenever the NetBird daemon is running.
| Platform | Default address |
| --------------- | ------------------------------ |
| Linux and macOS | `unix:///var/run/netbird.sock` |
| Windows | `npipe://netbird` |
On Windows the daemon serves a named pipe. Which path that is depends on what the
daemon may create: running as a service or elevated it serves
`\\.\pipe\ProtectedPrefix\Administrators\netbird`, a namespace only administrators
can create in, and otherwise it falls back to `\\.\pipe\netbird`. Clients try both
and check who owns the pipe before using the plain name, so passing
`npipe://netbird` is enough. Windows installations that predate the named pipe are
migrated from `tcp://127.0.0.1:41731` automatically.
<Warning>
**Security warning:** The default Unix socket allows read and write access for
local users, so any local user can read status and configuration and perform
operations that do not require privileges. Operations that decide who may
obtain a shell on the machine are refused unless the caller is root, or an
administrator on Windows: see [Privileged
operations](#privileged-operations). If you require local-user isolation
beyond that, place a custom socket inside a restricted directory and ensure
that the directory is recreated securely at boot.
</Warning>
On Linux installations that use an instance-specific systemd service, the socket can instead be under `/var/run/netbird/<instance>.sock`. The NetBird CLI automatically uses the only socket in that directory when the default socket does not exist. If multiple instance sockets exist, pass the intended address explicitly with `--daemon-addr`.
You can see the address option in the CLI help:
```shell
netbird --help
```
## Configure the Address
Use the global `--daemon-addr` option when installing or reconfiguring the service. The address must use one of these formats:
```text
unix:///path/to/netbird.sock
tcp://host:port
npipe://name
```
<Warning>
**Connectivity warning:** Reconfiguring the service restarts NetBird and can
briefly interrupt tunnel connectivity, routes, and DNS.
</Warning>
### Use a Custom Unix Socket
```shell
sudo netbird service reconfigure \
--daemon-addr unix:///var/run/netbird-integration-grpc.sock
```
The parent directory must already exist and be writable by the NetBird service. If you use a dedicated directory to restrict access, configure systemd or tmpfiles to recreate it securely because directories under `/run` do not persist across reboots.
After changing the socket, pass the same address to NetBird CLI commands that need to contact the daemon:
```shell
netbird --daemon-addr unix:///var/run/netbird-integration-grpc.sock status
```
<Note>
This `--daemon-addr` command only works after configuring the custom gRPC
listener above. If the daemon uses its default listener, run `netbird status`
instead. Do not pass an HTTP/JSON socket to `--daemon-addr`.
</Note>
### Use a TCP Socket
```shell
sudo netbird service reconfigure \
--daemon-addr tcp://127.0.0.1:41731
```
Then specify that address when using the CLI on platforms where it is not the default:
```shell
netbird --daemon-addr tcp://127.0.0.1:41731 status
```
<Warning>
**Security warning:** The daemon gRPC server does not add authentication or
TLS. Keep TCP listeners bound to a trusted interface such as `127.0.0.1` and
do not expose them to an untrusted network. The API includes operations that
can read or change the local NetBird daemon's state.
A TCP connection also carries no caller identity, so the daemon cannot tell
who is calling and refuses every [privileged
operation](#privileged-operations) on that socket, whoever runs the client.
Use a Unix socket, or `npipe://` on Windows, if your integration needs them.
</Warning>
## Privileged Operations
On the default sockets any local user can connect, so the daemon authorizes operations
by the identity of whoever calls it, read from the kernel rather than supplied by
the client: `SO_PEERCRED` on Linux, `LOCAL_PEERCRED` on macOS, and the named-pipe
client token on Windows. A caller whose identity cannot be established is refused.
Since 0.76.0 the following require root, or an administrator on Windows, because
they decide who may obtain a shell on the machine:
| Change | Refused when |
| --- | --- |
| Enable the NetBird SSH server | the caller is not privileged |
| Enable SSH root login | the caller is not privileged |
| Disable SSH authentication | the caller is not privileged |
| Change the management URL | the caller is not privileged and that profile has the SSH server enabled |
| Deregister the peer (logout) | the caller is not privileged and that profile has the SSH server enabled |
Only the direction that creates the capability is guarded. Turning the SSH server or
root login off, and re-enabling SSH authentication, are always allowed, and restating a
value that is already set is not a change, so an integration that submits a whole
settings form does not start failing once an administrator enables SSH.
Removing a profile is not refused. An unprivileged caller removes it locally and the
daemon skips the deregistration, which leaves the peer registered on the management
server rather than detached from it.
A profile written before the SSH server flag existed counts as having it enabled,
because the daemon reads an unset flag the same way the engine does. The management URL
and deregistration guards therefore apply on those installations even though nobody
enabled SSH explicitly.
A refusal comes back as gRPC `PermissionDenied` carrying a `google.rpc.ErrorInfo`
detail, so an integration can recognise it without parsing the message:
```text
reason: PRIVILEGE_REQUIRED
domain: daemon.netbird.io
metadata: summary = "Enabling the NetBird SSH server requires root."
command = "sudo netbird down; sudo netbird up --allow-server-ssh"
```
Render `summary` and `command` rather than the raw error: `command` is the same
operation with the privileges it needs, ready to run.
<Note>
When the daemon itself runs unprivileged, as in a rootless container or on
Windows in netstack mode, a caller running as the daemon's own user is treated
as privileged. Such a caller can already rewrite the configuration the daemon
reads, so refusing it would protect nothing.
</Note>
## Service Definition
The socket provides `daemon.DaemonService`. NetBird's
[`client/proto/daemon.proto`](https://github.com/netbirdio/netbird/blob/main/client/proto/daemon.proto)
file lists the available methods, streaming types, and request and response schemas.
Here are a few common methods. Check `daemon.proto` for the full list.
| Method | Type | Purpose |
| ----------------- | ---------------- | --------------------------------------------------------- |
| `Status` | Unary | Read connection status and peer information. |
| `SubscribeStatus` | Server streaming | Receive the current status and subsequent status changes. |
| `GetConfig` | Unary | Read the active daemon configuration. |
| `ListNetworks` | Unary | List networks available to the client. |
| `Up` | Unary | Start the NetBird connection. |
| `Down` | Unary | Stop the NetBird connection. |
| `ListProfiles` | Unary | List configured client profiles. |
| `SubscribeEvents` | Server streaming | Receive daemon system events. |
The service also includes methods that change configuration, profiles, network selection, logging, and other daemon state. Only give socket access to processes that you trust to control the local NetBird client.
<Note>
The daemon does not enable gRPC server reflection. Tools and integrations must
use `daemon.proto`, generated client bindings, or a compiled descriptor set to
learn the service schema.
</Note>
## Test the Socket with grpcurl
[`grpcurl`](https://github.com/fullstorydev/grpcurl) is a command-line tool for invoking gRPC methods. Because it needs access to the `.proto` files to perform requests, we need to download the repository at the same version that the daemon to not have a mismatch on the Protocol Buffer definition.
```shell
export NETBIRD_VERSION=v$(netbird version)
git clone --depth 1 \
--branch "$NETBIRD_VERSION" \
https://github.com/netbirdio/netbird.git
cd netbird
```
Run the following examples from the root of the cloned `netbird` repository.
### Query Status Through the Unix Socket
```shell
grpcurl \
-plaintext \
-unix \
-import-path ./client/proto \
-proto daemon.proto \
-d '{}' \
unix:///var/run/netbird.sock \
daemon.DaemonService/Status
```
To request full peer status, set fields from `StatusRequest` in the JSON request body:
**Sensitive output:** Full peer status may contain network topology, endpoint, route, and peer information. Review or redact it before sharing the output.
```shell
grpcurl \
-plaintext \
-unix \
-import-path ./client/proto \
-proto daemon.proto \
-d '{"getFullPeerStatus": true}' \
unix:///var/run/netbird.sock \
daemon.DaemonService/Status
```
### Query Status Through a TCP Socket
This applies to a custom loopback TCP listener, since `grpcurl` cannot dial a
Windows named pipe and the Windows default is therefore not reachable this way. Use
a client that can open the pipe, or configure a TCP listener for testing and accept
that [privileged operations](#privileged-operations) are refused on it:
```shell
grpcurl \
-plaintext \
-import-path ./client/proto \
-proto daemon.proto \
-d '{}' \
127.0.0.1:41731 \
daemon.DaemonService/Status
```
`-plaintext` is required because the local daemon socket does not use TLS. For a Unix socket, pass the full `unix:///...` URI and include `-unix`. The URI form also avoids a Unix-socket regression in some `grpcurl` 1.9.x builds.
### Subscribe to Status Changes
Server-streaming methods remain connected and print each response as it arrives. For example:
```shell
grpcurl \
-plaintext \
-unix \
-import-path ./client/proto \
-proto daemon.proto \
-d '{}' \
unix:///var/run/netbird.sock \
daemon.DaemonService/SubscribeStatus
```
Press `Ctrl+C` to end the stream.
## Build an Integration
For a long-running integration, generate a gRPC client from the same `daemon.proto` revision as the installed NetBird client:
1. Download or vendor `client/proto/daemon.proto` from the NetBird repository.
2. Generate client bindings with the Protocol Buffer and gRPC tools for your language.
3. Create a local gRPC channel to the configured Unix or TCP address without TLS. That does not make the socket safe to expose remotely.
4. Create a `daemon.DaemonService` client from that channel.
5. Call unary methods or consume server streams using the generated request and response types.
Use a protobuf definition and generated bindings that match your deployed NetBird version. A newer client may call methods or use fields that an older daemon does not support.
The HTTP/JSON gateway exposes this service for environments where generated gRPC clients are unavailable. See [HTTP/JSON Daemon Socket](/client/json-socket) for setup and HTTP examples.
## Troubleshooting
### The Unix Socket Does Not Exist
Check that the service is running:
```shell
sudo netbird service status
```
If you use an instance-specific service, inspect `/var/run/netbird/` for its socket or pass the configured address with `--daemon-addr`.
### A Tool Reports That Reflection Is Unsupported
This is expected. Supply `daemon.proto` with your tool's equivalent of the `grpcurl -proto` and `-import-path` options, or use a descriptor set generated from that file.
### A Client Receives an Unavailable or Connection-Refused Error
Confirm that the daemon is running and that the integration uses the same socket address as the service. For a Unix socket, also verify access to the socket and each parent directory. For TCP, verify the host and port and ensure the listener remains bound to a trusted interface.
### A Call Is Refused With PermissionDenied
The operation is one of the [privileged operations](#privileged-operations) and the
caller is not root, or not an administrator on Windows. Check the `ErrorInfo` detail
on the error: `PRIVILEGE_REQUIRED` in domain `daemon.netbird.io` means the daemon
identified the caller and refused the change, rather than failing to reach it. On a
TCP socket every such operation is refused, because the transport carries no caller
identity.
### A Method or Field Is Unimplemented
The integration's generated bindings may be newer than the installed NetBird daemon. Compare the installed client version with the revision of `daemon.proto` used to generate the bindings, then use a compatible schema or upgrade NetBird.

View File

@@ -0,0 +1,267 @@
import { Note, Warning } from '@/components/mdx'
export const description =
'Use the NetBird daemon HTTP/JSON socket to build local integrations in environments where gRPC is unavailable.'
# HTTP/JSON Daemon Socket
The NetBird daemon can expose its local API over HTTP with JSON request and response bodies. A [gRPC-Gateway](https://grpc-ecosystem.github.io/grpc-gateway/) receives those HTTP/JSON requests and passes them to the daemon's gRPC API.
The daemon's primary local API is documented in [gRPC Daemon Socket](/client/grpc-socket). Use that socket when your integration supports gRPC and generated Protocol Buffer bindings.
Use the JSON socket when your integration cannot use gRPC. For example, it works well in runtimes that only have an HTTP client, local monitoring agents, and application sandboxes where adding a gRPC client and generated protobuf bindings is not practical.
<Note>
The HTTP/JSON daemon socket requires NetBird client v0.75.0 or later.
</Note>
<Note>
The HTTP/JSON socket controls the **local NetBird daemon**. It is separate
from the NetBird Management API and does not replace it.
</Note>
## Enable the JSON Socket
The JSON socket is disabled by default. To enable it while installing the NetBird service, run:
```shell
sudo netbird service install --enable-json-socket
```
The default address is:
```text
unix:///var/run/netbird-http.sock
```
<Warning>
**Security warning:** The default Unix socket allows read and write access for
local users. The API exposes control operations as well as status. If you
require local-user isolation, place a custom socket inside a restricted
directory and ensure that the directory is recreated securely at boot.
</Warning>
The gateway runs inside the daemon and re-dials it locally, so it reads the identity
of its own HTTP client and forwards it, which is what lets the daemon authorize the
request as that user rather than as the daemon itself. The
[privileged operations](/client/grpc-socket#privileged-operations) therefore behave
the same over HTTP as over gRPC, and a refusal comes back as HTTP 403 with the same
`PRIVILEGE_REQUIRED` detail. Metadata headers reserved for that forwarding are
dropped when a client supplies them.
To enable it on an existing installation, reconfigure the service:
<Warning>
**Connectivity warning:** Every `netbird service reconfigure` command on this
page restarts NetBird and can briefly interrupt tunnel connectivity, routes,
and DNS.
</Warning>
```shell
sudo netbird service reconfigure --enable-json-socket
```
NetBird saves this setting with the service configuration, so it stays enabled after a restart.
### Use a Custom Unix Socket
Pass an address with the `unix://` scheme to change the socket path:
```shell
sudo netbird service reconfigure \
--enable-json-socket \
--json-socket unix:///var/run/netbird-integration-http.sock
```
The parent directory must already exist and be writable by the NetBird service. If you use a dedicated directory to restrict access, configure systemd or tmpfiles to recreate it securely because directories under `/run` do not persist across reboots.
`--json-socket` configures an HTTP endpoint. Query it with an HTTP client such as `curl`; do not pass this address to `netbird --daemon-addr`.
```shell
curl --silent --show-error \
--unix-socket /var/run/netbird-integration-http.sock \
--request POST \
--header 'Content-Type: application/json' \
--data '{}' \
--write-out '\n' \
http://localhost/daemon.DaemonService/Status
```
### Use a TCP Socket
Pass an address with the `tcp://` scheme to expose the gateway over TCP:
```shell
sudo netbird service reconfigure \
--enable-json-socket \
--json-socket tcp://127.0.0.1:8080
```
<Warning>
**Security warning:** The gateway does not add authentication or TLS. Keep TCP
listeners bound to a trusted interface such as `127.0.0.1` and do not expose
them to an untrusted network. The API includes operations that can read or
change the local NetBird daemon's state.
A TCP connection carries no caller identity, so the gateway cannot tell who is
calling and every
[privileged operation](/client/grpc-socket#privileged-operations) is refused on
such a socket, whoever runs the client.
</Warning>
To disable the gateway again, run:
```shell
sudo netbird service reconfigure --enable-json-socket=false
```
## Make Requests
Each daemon RPC is exposed as an HTTP `POST` endpoint using this path format:
```text
/daemon.DaemonService/<MethodName>
```
Send the request message as JSON with the `Content-Type: application/json` header. If an RPC has no required request fields, send an empty JSON object (`{}`). Responses use the standard [Protocol Buffers JSON mapping](https://protobuf.dev/programming-guides/json/).
### Query Status Through the Unix Socket
With the default socket path:
```shell
curl --silent --show-error \
--unix-socket /var/run/netbird-http.sock \
--request POST \
--header 'Content-Type: application/json' \
--data '{}' \
--write-out '\n' \
http://localhost/daemon.DaemonService/Status
```
The response is a JSON representation of the daemon's status response. For example, it includes the daemon status and version:
```json
{
"status": "Connected",
"daemonVersion": "..."
}
```
The exact fields and values depend on the client version and current connection state.
### Query Status Through a TCP Socket
If the gateway is listening on `tcp://127.0.0.1:8080`, use a normal HTTP request:
```shell
curl --silent --show-error \
--request POST \
--header 'Content-Type: application/json' \
--data '{}' \
--write-out '\n' \
http://127.0.0.1:8080/daemon.DaemonService/Status
```
### Call the API from an Integration
The following Python example calls the status endpoint over a Unix socket using only the standard library:
```python
from http import client
from json import dumps, load
from socket import AF_UNIX, SOCK_STREAM, socket
class UnixHTTPConnection(client.HTTPConnection):
def __init__(self, socket_path):
super().__init__("localhost")
self.socket_path = socket_path
def connect(self):
self.sock = socket(AF_UNIX, SOCK_STREAM)
self.sock.connect(self.socket_path)
connection = UnixHTTPConnection("/var/run/netbird-http.sock")
connection.request(
"POST",
"/daemon.DaemonService/Status",
body=dumps({}),
headers={"Content-Type": "application/json"},
)
response = connection.getresponse()
if response.status != 200:
raise RuntimeError(f"NetBird returned HTTP {response.status}: {response.read().decode()}")
status = load(response)
print(status["status"])
connection.close()
```
For a TCP listener, any standard HTTP client can call the same path and JSON body without Unix-socket support.
## Map gRPC Methods to HTTP
The gateway exposes every method in `daemon.DaemonService`, and every endpoint uses
`POST`. Add the method name to the service path:
```text
daemon.DaemonService/Status
/daemon.DaemonService/Status
```
For example:
| gRPC method | HTTP endpoint |
| -------------- | ------------------------------------ |
| `Status` | `/daemon.DaemonService/Status` |
| `GetConfig` | `/daemon.DaemonService/GetConfig` |
| `ListNetworks` | `/daemon.DaemonService/ListNetworks` |
| `Up` | `/daemon.DaemonService/Up` |
| `Down` | `/daemon.DaemonService/Down` |
The [`DaemonService` protobuf definition](https://github.com/netbirdio/netbird/blob/main/client/proto/daemon.proto)
is the complete API reference for method names and request and response schemas.
See [gRPC Daemon Socket](/client/grpc-socket#service-definition) for an overview of
the service and guidance on using the protobuf definition.
### Server-Streaming Methods
Server-streaming RPCs use the same path. The HTTP request stays open and returns
JSON messages until the stream ends or the caller closes the connection. Check the
protobuf definition for streaming methods and their response types.
The gateway exposes control operations as well as read-only methods. Only give
socket access to processes that you trust to control the local NetBird client.
## Troubleshooting
### The Socket File Does Not Exist
Confirm that the service was installed or reconfigured with `--enable-json-socket`, then check the service status:
```shell
sudo netbird service status
```
If you supplied `--json-socket` without `--enable-json-socket`, NetBird rejects the configuration. Setting a custom address does not enable the gateway on its own.
### Curl Reports Permission Denied
Verify that the process running the integration can access the socket and every parent directory in its path. A custom restricted directory can prevent access even when the socket itself allows it.
### A Request Is Refused With 403
The operation is one of the
[privileged operations](/client/grpc-socket#privileged-operations) and the HTTP
client is not root, or not an administrator on Windows. The response body carries a
`PRIVILEGE_REQUIRED` detail in domain `daemon.netbird.io`, with a summary and the
command that performs the same operation with the privileges it needs. On a TCP
gateway socket these operations are always refused.
### A TCP Request Cannot Connect
Confirm that the host and port in the request match the value passed to `--json-socket`. Prefer `127.0.0.1` over `0.0.0.0` unless remote access is explicitly required and protected by an additional security boundary.

View File

@@ -71,6 +71,8 @@ PascalCase variant in the Group Policy Editor — both are recognized.
| `wireguardPort` | integer | UDP port the local WireGuard interface binds to. Range `165535`. |
| `allowServerSSH` | boolean | Allow the embedded NetBird SSH server on this peer. |
| `disableAutoConnect` | boolean | Skip auto-connecting on startup; require an explicit `netbird up`. |
| <span id="disableAutostart"></span>`disableAutostart` | boolean | Prevent the GUI from registering itself as an OS autostart entry on fresh installs, and — when enabled at any later point — remove an existing registration on the next GUI launch (Windows Registry `Run` key, macOS Login Item, Linux `.desktop`). Desktop GUIs only; no-op on iOS/Android. Once the admin lifts the policy, the setting stays off until the user re-enables it in Settings. |
| `lazyConnection` | boolean | Local override for lazy connections. `true` forces lazy connections on, `false` forces them off, and an absent key defers to the Management setting. `NB_LAZY_CONN` takes precedence when both are configured. |
| `rosenpassEnabled` | boolean | Turn on the post-quantum Rosenpass key exchange. |
| `rosenpassPermissive` | boolean | Permissive mode for Rosenpass (interop with non-Rosenpass peers). |
| `blockInbound` | boolean | Drop all inbound traffic except established/related — kill-switch style. |
@@ -96,6 +98,11 @@ PascalCase variant in the Group Policy Editor — both are recognized.
`VpnService.Builder.addAllowedApplication()` flow; on Windows and
macOS the daemon parses the keys but ignores them. They are safe to
ship in a cross-platform payload.
- `disableAutostart` only affects the desktop GUI's OS autostart entry
(Windows Registry `Run` key, macOS Login Item, Linux `.desktop`).
iOS and Android have no equivalent user-space autostart mechanism —
the daemon runs as a system service — so the key is parsed and
ignored there. Safe to ship in cross-platform payloads.
- The `disableMetricsCollection` key is reserved for an upcoming
metrics integration; the client recognizes it today but no metrics
pipeline is shipped yet.

View File

@@ -25,11 +25,11 @@ that automatically rotates and applies WireGuard pre-shared keys to every point-
This is still an experimental feature, may contain bugs, and is not supported on mobile devices.
</Note>
### Enabling via the system tray
### Enabling via the desktop app
1. Click the NetBird icon in the system tray.
2. Go to **Settings**.
3. Click **Enable Quantum-Resistance** to toggle the setting.
1. Open the NetBird desktop app.
2. Go to **Settings → Security**.
3. Toggle **Enable Quantum-Resistance**.
### Enabling via the CLI

View File

@@ -7,47 +7,29 @@ This feature also allows you to switch between self-hosted and cloud-hosted NetB
to juggle multiple config files.
<p>
<img src="/docs-static/img/client/profiles/profiles.png" alt="profiles" className="imagewrapper"/>
<img src="/docs-static/img/client/profiles/profiles.png" alt="Profile selector in the NetBird desktop app" className="imagewrapper"/>
</p>
Watch a short demo GIF demonstrating how profile switching works [here](/docs-static/img/client/profiles/profiles.gif).
## NetBird Profiles GUI Quickstart
To get started with NetBird profiles:
1. Open the desktop app. NetBird creates a `default` profile automatically.
2. Open **Settings → Profiles** and select **Add profile**.
3. Give the profile a recognizable name and configure whether it connects to NetBird Cloud or a self-hosted management URL.
4. Select the profile from the selector in the main window or tray menu.
- Upgrade your client application to the latest NetBird version.
- Run the GUI app
You will see a `default` profile created automatically.
Add more profiles by hovering over the default profile and clicking "Manage Profiles".
After adding a new profile, select it to make it active.
You can now change the NetBird settings, e.g., providing a self-hosted
instance URL or allowing SSH. The new settings will be saved in the new profile. Click "Connect" to bring up the new profile.
The consequent selection of your profiles from the menu will automatically trigger the NetBird client to connect to the network and authentication
if needed.
Selecting a profile makes it active and connects automatically. The first time you use a profile, NetBird opens the browser authentication flow if needed. Later switches reuse that profile's saved login state. While the Profiles settings page is open for management, selecting a profile does not interrupt the profile you are editing.
## Manage Profiles in the GUI
* **Add** a new profile with a friendly name input. Names need not be unique, each profile is tracked by its own generated ID.
* **Delete** any inactive profile (trash icon).
* **Active and default** profiles cannot be removed.
Open **Settings → Profiles** to:
* **Add** a profile with a friendly name. Names need not be unique because each profile has a generated ID.
* **Edit or rename** an existing profile, including its management URL.
* **Delete** any inactive profile. Active and default profiles cannot be removed.
* Recognize profiles by their assigned Work, Home, Default, or other type icon.
<p>
<img src="/docs-static/img/client/profiles/manage-profiles.png" alt="profiles" className="imagewrapper"/>
</p>
<Note>
The redesigned desktop app, currently available as a **release candidate**, expands profile management. You can now **rename and edit** existing profiles, not just add and delete them. Profile names accept uppercase letters and spaces, so names like `Work Prod` or `Home Lab` are valid. Each profile is assigned an **icon based on its type** (Work, Home, Default, and others) so the active profile is easy to recognize in the tray and switcher.
In the new app, profiles are managed from the dedicated **Profiles** tab in Settings.
</Note>
<p>
<img src="/docs-static/img/client/profiles/profiles-edit.png" alt="Editing and renaming profiles in the new desktop app" className="imagewrapper"/>
<img src="/docs-static/img/client/profiles/profiles-edit.png" alt="Profiles settings in the NetBird desktop app" className="imagewrapper"/>
</p>
## What Is a Profile?
@@ -74,13 +56,15 @@ Each profile has two parts:
The `default` profile is special: its ID is always `default`. You can rename its display label,
but it keeps that reserved ID and cannot be removed.
Profiles live in your system or user config folders, stored as `<id>.json`:
The default profile is stored as `default.json` in NetBird's state directory. Additional service profiles are stored under a per-user subdirectory as `<id>.json`:
| OS | Config path |
| ------ | --------------------------------- |
| Linux | `/var/lib/netbird/...` |
| macOS | `/var/lib/netbird...`|
| Windows| `%ProgramData%\Netbird\profiles\` |
| OS | Default state directory | Additional profile example |
| --- | --- | --- |
| Linux and macOS | `/var/lib/netbird/` | `/var/lib/netbird/<username>/<id>.json` |
| FreeBSD | `/var/db/netbird/` | `/var/db/netbird/<username>/<id>.json` |
| Windows | `%ProgramData%\Netbird\` | `%ProgramData%\Netbird\<username>\<id>.json` |
`NB_STATE_DIR` overrides the default state directory. Treat profile files as credentials: stop the service before manual maintenance, restrict access, and prefer the GUI or CLI for normal profile management.
---

View File

@@ -42,7 +42,7 @@ Below is the list of global flags:
--admin-url string Admin Panel URL [http|https]://[host]:[port] (default "https://app.netbird.io:443")
-A, --anonymize anonymize IP addresses and non-netbird.io domains in logs and status output
-c, --config string Overrides the default profile file location. Deprecated on `up` and `login`; use `--service-env NB_CONFIG=<path>` instead.
--daemon-addr string Daemon service address to serve CLI requests [unix|tcp]://[path|host:port] (default "unix:///var/run/netbird.sock")
--daemon-addr string Daemon service address to serve CLI requests [unix|tcp|npipe]://[path|host:port|name] (default "unix:///var/run/netbird.sock", "npipe://netbird" on Windows)
-n, --hostname string Sets a custom hostname for the device
--log-file console Sets NetBird log paths written to simultaneously. If "console" is specified the log will be output to stdout. If "syslog" is specified the log will be sent to the syslog daemon. You can pass the flag multiple times or separate entries by comma (default [/var/log/netbird/client.log])
-l, --log-level string Sets NetBird log level (default "info")
@@ -102,7 +102,7 @@ The command will check if the peer is logged in and connect to the management se
--disable-ssh-auth Disable SSH JWT authentication. If enabled, any peer with network access can connect without user authentication.
--dns-resolver-address string Sets a custom address for NetBird's local DNS resolver. If set, the agent won't attempt to discover the best IP and port to listen on. An empty string "" clears the previous configuration. E.g. --dns-resolver-address 127.0.0.1:5053 or --dns-resolver-address ""
--dns-router-interval duration DNS route update interval (default 1m0s)
--enable-lazy-connection [Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand. Note: this setting may be overridden by management configuration.
--enable-lazy-connection Deprecated and no longer used. Lazy connections are controlled by Management and the NB_LAZY_CONN environment variable.
--enable-rosenpass [Experimental] Enable Rosenpass feature. If enabled, the connection will be post-quantum secured via Rosenpass.
--enable-ssh-local-port-forwarding Enable local port forwarding for SSH server
--enable-ssh-remote-port-forwarding Enable remote port forwarding for SSH server
@@ -427,6 +427,26 @@ Peers count: 2/3 Connected
The peer with IP `100.119.85.4` wasn't returned because it was not connected
</Note>
#### Health checks
Use `netbird status --check` to run a health probe from a script or container orchestrator. These probes do not display the normal status report. A successful probe produces no output and exits with code `0`; a failed probe exits with code `1`.
- `live`: succeeds when the daemon responds to the status request.
- `ready`: succeeds when the daemon status is `Idle`, `Connecting`, or `Connected`. Authentication-required states (`NeedsLogin`, `LoginFailed`, and `SessionExpired`) and unexpected states fail the check.
- `startup`: succeeds when management and signal are connected and, if any NetBird relays are configured, at least one relay is available.
For example, the following loop makes each probe result visible:
```shell
for check in live ready startup; do
if netbird status --check "$check"; then
echo "$check: PASS"
else
echo "$check: FAIL ($?)"
fi
done
```
### ssh
Command to connect via SSH to a remote peer in your NetBird network. The `ssh` command has several subcommands for different operations.
@@ -489,9 +509,11 @@ For SFTP and SCP, use native clients (`sftp` and `scp` commands) which work with
**Connection fails:**
- Ensure SSH is enabled on the target peer:
- Ensure SSH is enabled on the target peer, which requires root or an administrator.
The commands below use `sudo` for Linux and macOS; on Windows run the same ones
without it, from an elevated prompt:
```shell
netbird up --allow-server-ssh
sudo netbird down; sudo netbird up --allow-server-ssh
```
- Verify SSH Access is enabled in the dashboard (Peers > your_peer > SSH Access)
- Check that an ACL policy allows TCP port 22022
@@ -500,13 +522,16 @@ For SFTP and SCP, use native clients (`sftp` and `scp` commands) which work with
- Complete the OIDC flow when prompted (browser window will open)
- Verify your IdP is properly configured
- To disable JWT authentication: `netbird up --allow-server-ssh --disable-ssh-auth`
- To disable JWT authentication, as root or an administrator: `sudo netbird down; sudo netbird up --allow-server-ssh --disable-ssh-auth`.
This drops per-user authentication for SSH on that peer, leaving any peer the ACL
policy allows able to connect, so use it to isolate a problem and re-enable it with
`sudo netbird down; sudo netbird up --allow-server-ssh` afterwards.
**Port forwarding not working:**
- Ensure the server has the appropriate flags:
```shell
netbird up --allow-server-ssh \
sudo netbird down; sudo netbird up --allow-server-ssh \
--enable-ssh-local-port-forwarding \
--enable-ssh-remote-port-forwarding
```
@@ -779,8 +804,15 @@ netbird debug [command]
### debug bundle
Generates a compressed archive containing diagnostic information, which can be used for troubleshooting.
The file will be generated in a temporary directory and the path will be printed to the console.
The file is only accessible as root/Administrator.
The file will be generated in the daemon's temporary directory and the path will be printed to the console. With a
standard service installation, the location is `/tmp/netbird.debug.<number>.zip` on Linux and macOS, and
`C:\Windows\Temp\netbird.debug.<number>.zip` on Windows. A custom operating-system temporary directory can change this
path.
The local ZIP is created whether or not `--upload-bundle` is used. With `--upload-bundle`, the command also prints an
upload key that you can share with the NetBird team.
The file belongs to the daemon service account (`root` on standard Linux and macOS installations and `LocalSystem` on
Windows), so elevated permissions may be required to access it directly.
#### Usage
To create a debug bundle:
@@ -806,6 +838,7 @@ This will output:
-S, --system-info Adds system information to the debug bundle (default true)
-U, --upload-bundle Uploads the debug bundle to a server
--upload-bundle-url string Service URL to get an upload URL for the debug bundle (default "https://upload.debug.netbird.io/upload-url")
--upload-bundle-insecure Allow uploading to an http or untrusted-TLS upload server (self-hosted); requires root on Linux and macOS, or an administrator on Windows
```
### debug for
@@ -845,6 +878,7 @@ Log level restored to INFO
-S, --system-info Adds system information to the debug bundle (default true)
-U, --upload-bundle Uploads the debug bundle to a server
--upload-bundle-url string Service URL to get an upload URL for the debug bundle (default "https://upload.debug.netbird.io/upload-url")
--upload-bundle-insecure Allow uploading to an http or untrusted-TLS upload server (self-hosted); requires root on Linux and macOS, or an administrator on Windows
```
### debug log

View File

@@ -5,6 +5,10 @@ import {Note} from "@/components/mdx";
Welcome to NetBird! This guide will walk you through our new onboarding process to create your account, connect your first devices,
and build a secure peer-to-peer overlay network in less than ten minutes.
<Note>
This guide covers NetBird Cloud, the managed version of NetBird. If you prefer to run NetBird on your own infrastructure, follow the [Self-Hosting Quickstart](/selfhosted/selfhosted-quickstart) instead.
</Note>
<YouTube videoId="dr0u-u9uD84" />
## Create Your Account
@@ -41,10 +45,10 @@ With the client installed, you now need to connect it to your network.
![Connect NetBird Client](/docs-static/img/get-started/onboarding/04_connect-client.jpeg)
1. After installation, find the NetBird icon in your system tray or menu bar.
2. Click the icon and select **Connect**.
3. This will open a new browser tab, prompting you to authorize the new device. Authenticate using the same IdP you used to sign up.
4. Once authorized, you will see a "Login successful" message. The onboarding UI will update to show that your first peer is connected, displaying its name and assigned NetBird IP address.
1. Launch the NetBird desktop app. On first launch, the welcome screen points you to the system tray on Windows and Linux or the menu bar on macOS, then asks whether you use NetBird Cloud or a self-hosted deployment.
2. Click **Connect** in the main window or tray/menu-bar menu.
3. NetBird opens a browser tab so you can authorize the device with the same identity provider you used to sign up.
4. Once authorized, the app displays the connected state and assigned NetBird IP address. The onboarding UI also updates to show that your first peer is connected.
### Add a Second Peer (Headless Linux Server)
Next, let's add a second, headless peer, like a Linux server or a Raspberry Pi. For devices without a graphical interface, we use a [Setup Key](https://docs.netbird.io/manage/peers/register-machines-using-setup-keys).
@@ -150,7 +154,7 @@ After running the second command, the terminal will confirm Connected. Your head
Now, set up the device you will use to connect to your private network.
1. Back in the web UI, the wizard will prompt you to "Time to add your client device." Click Install NetBird.
2. Download and run the installer for your client machine's OS (e.g., macOS).
3. Once installed, find the NetBird icon in your system tray or menu bar, click it, and select Connect.
3. Once installed, launch the desktop app and click **Connect** in the main window or tray/menu-bar menu.
4. Authorize this new device in the browser tab that opens.
### Test the Connection

View File

@@ -81,6 +81,6 @@ the SSO flow, the QR code dialog in the NetBird app will automatically dismiss i
## What's next?
- Manage your device's [access](/manage/access-control/manage-network-access) to the network
- Use your device for [remote access access to your home network](/manage/networks/use-cases/access-home-devices)
- Use your device as an [exit node](/manage/network-routes/use-cases/exit-nodes#make-the-peer-an-exit-node-routing-peer)
- Use your device for [remote access to your home network](/use-cases/remote-access/access-home-devices)
- Use your device as an [exit node](/use-cases/remote-access/exit-nodes#make-the-peer-an-exit-node-routing-peer)

Some files were not shown because too many files have changed in this diff Show More