Displays when the per-customer token was last (re-)issued and its
computed expiry (365 days from renewal) on the Auto-Updates card, so it's
visible at a glance without checking the database.
The tokens captured for central update control expire (NetBird enforces a
365-day max on Personal Access Tokens), and nothing was renewing them —
discovered that the 47 tokens created via the browser-automation bulk
onboarding were actually only 30-day tokens (left the UI's default
expiration field untouched instead of setting 365), so they would have
silently broken automatic-update control next month with no warning.
- Bumped all existing tokens to fresh 365-day ones via the API (using the
still-valid old token as bearer — no re-login needed)
- Added netbird_api_token_renewed_at per deployment
- Scheduler now checks daily and renews any token older than 300 days
automatically, so this never needs to be done by hand again
Lets the MSP admin control NetBird's own "Settings > Clients > Automatic
Updates" feature (client/peer auto-update, v0.61.0+) for every customer from
one place, instead of logging into each customer's dashboard individually.
- New deployments automatically capture a Personal Access Token during the
existing /api/setup bootstrap call (create_pat=true), requiring
NB_SETUP_PAT_ENABLED=true on the management container (now set by default
in the compose template). Token is encrypted at rest per customer.
- Existing customers (deployed before this existed) can have a token pasted
in manually from their own dashboard — verified before being stored.
- Settings > Docker Images: master default (version + force-update toggle)
plus "Apply to All Customers" which pushes it to everyone with a token.
- Customer detail page: shows the customer's live current setting (read
from their NetBird API, not cached) with per-customer override or
"sync from default".
- New app/services/netbird_client_update_service.py wraps the customer's
NetBird Management API (GET/PUT /api/accounts) for this.
Customer container status checks looked up containers by an exact expected
name. When a docker compose recreate got interrupted (e.g. a hung command
previously killed the whole update-all batch on timeout), Compose could leave
the old container renamed with a random hash prefix instead of removed. The
exact-name lookup then found nothing, returned None, and that silently
counted as "up to date" (green "Aktuell") instead of surfacing as unknown —
affecting 5 customers on the appliance whose containers were actually still
running under orphaned names.
- _run_cmd no longer raises on subprocess timeout, so one stuck customer
can't abort the rest of a batch update
- repair_container_naming() self-heals orphaned hash-renamed containers by
renaming them back before every status check and before recreate
- update-all loop now catches per-customer exceptions instead of aborting
- status responses expose "unknown" separately from "needs_update" so the UI
shows a distinct grey badge instead of a false-positive green one
- new settings: automatic daily update check (on/off + time), with an
independent toggle for whether it also auto-recreates customer containers
- Customer list now defaults to ascending ID order instead of newest-first,
so the table starts at customer #1 instead of the highest ID
- Add sort_by/sort_order query params to GET /customers (whitelisted column
map to prevent SQL injection via arbitrary column names)
- Make ID/Name/Subdomain/Status/Devices/Created column headers clickable,
toggling asc/desc with a visual arrow indicator
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The update endpoint ran the entire git pull + docker build (up to 10 min)
synchronously inside the request handler, blocking the whole server for
everyone while it ran. Separately, the frontend spinner was only hidden on
error, never on success, so it spun forever even when the update worked.
- Run the update in a background thread; the request returns immediately
- Add GET /settings/update/status for progress polling (backup/pull/build/restart)
- Frontend polls status, then waits for the app to come back after the
container restart, and shows a clear done/timeout message instead of an
endless spinner
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Customer search and detail loads were intermittently slow because every
customer-table render (including each search keystroke) triggered
/monitoring/customers/local-update-status, which looped synchronously over
all customers doing blocking `docker inspect` subprocess calls on the event
loop — stalling all other in-flight requests, including search itself.
- Offload per-service image/container inspection to the thread pool and run
checks concurrently instead of sequentially (image_service, docker_service)
- Reuse a single Docker SDK client instead of reconnecting per customer
- Cache local-update-status results for 20s since the underlying data only
changes after an image pull, not on every keystroke
- Parallelize /monitoring/customers container status lookups
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Add a confirmation modal when clicking Redeploy that lets the user choose:
- Keep Data: containers are recreated without wiping the instance directory.
NetBird database, peer configs, and encryption keys are preserved.
- Fresh Deploy: full undeploy (removes all data) then redeploy from scratch.
Backend changes:
- POST /customers/{id}/deploy accepts keep_data query param (default false)
- When keep_data=true, undeploy_customer is skipped entirely
- deploy_customer now reuses existing npm_proxy_id/stream_id when the
deployment record is still present (avoids duplicate NPM proxy entries)
- DNS record creation is skipped on keep_data redeploy (already exists)
Frontend changes:
- customerAction('deploy') opens the redeploy modal instead of calling API
- showRedeployModal(id) shows the two-option confirmation card dialog
- confirmRedeploy(keepData) calls the API with the correct parameter
- i18n keys added in en.json and de.json
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Dashboard: update badge (orange) injected lazily into customer Status cell
after table renders via GET /monitoring/customers/local-update-status
(local-only Docker inspect, no Hub call on every page load)
- Customer detail Deployment tab: "Update Images" button with spinner,
shows success/error inline without page reload
- Monitoring Update All: now synchronous + sequential (one customer at a
time), shows live spinner + per-customer results table on completion
- Settings > Docker Images: "Pull from Docker Hub" button with spinner
and inline status message
- /monitoring/customers/local-update-status: new lightweight endpoint
(no network, pure local Docker inspect)
- /monitoring/customers/update-all: removed BackgroundTasks, now awaits
each customer sequentially and returns detailed per-customer results
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- New image_service.py: Docker Hub digest check (no pull), local digest/ID
comparison, pull_all_images, per-customer container image status, and
update_customer_containers (docker compose up -d, data-safe)
- Monitoring endpoints: GET /images/check (hub vs local + per-customer
needs_update), POST /images/pull (background), POST /customers/update-all
- Deployment endpoint: POST /{id}/update-images (single-customer update)
- Monitoring page: "NetBird Container Updates" card with Check / Pull / Update
All buttons; image status table and per-customer update table with inline
update buttons
- i18n: added keys in en.json and de.json
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Backend: add admin-only guard + role validation to PUT /users/{id}
- Backend: prevent admins from changing their own role
- Frontend: role toggle button (person-check / person-dash) per user row
- Frontend: admin badge green, viewer badge secondary, ldap badge blue
- i18n: add makeAdmin / makeViewer translations (de + en)
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Bake version info (commit, branch, date) into /app/version.json at build time
via Docker ARG GIT_COMMIT/GIT_BRANCH/GIT_COMMIT_DATE
- Mount source directory as /app-source for in-container git operations
- Add git config safe.directory for /app-source (ownership mismatch fix)
- Add SystemConfig fields: git_repo_url, git_branch, git_token_encrypted
- Add DB migrations for the three new columns
- Add git_token encryption in update_settings() handler
- New endpoints:
GET /api/settings/version — current version + latest from Gitea API
POST /api/settings/update — DB backup + git pull + docker compose rebuild
- New service: app/services/update_service.py
get_current_version() — reads /app/version.json
check_for_updates() — queries Gitea API for latest commit on branch
backup_database() — timestamped SQLite copy to /app/backups/
trigger_update() — git pull + fire-and-forget compose rebuild
- New script: update.sh — SSH-based manual update with health check
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Windows DNS (WinRM):
- New dns_service.py: create/delete A-records via PowerShell over WinRM (NTLM)
- Idempotent create (removes existing record first), graceful delete
- DNS failures are non-fatal — deployment continues, error logged
- test-dns endpoint: GET /api/settings/test-dns
- Integrated into deploy_customer() and undeploy_customer()
LDAP / Active Directory auth:
- New ldap_service.py: service-account bind + user search + user bind (ldap3)
- Optional AD group restriction via ldap_group_dn
- Login flow: LDAP first → local fallback (prevents admin lockout)
- LDAP users auto-created with auth_provider="ldap" and role="viewer"
- test-ldap endpoint: GET /api/settings/test-ldap
- reset-password/reset-mfa guards extended to block LDAP users
All credentials (dns_password, ldap_bind_password) encrypted with Fernet.
New DB columns added via backwards-compatible migrations.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- CORS: remove allow_origins=["*"]; restrict to ALLOWED_ORIGINS env var
(comma-separated list); default is no cross-origin access. Removed
allow_credentials=True and method/header wildcards.
- Security headers middleware: add X-Content-Type-Options, X-Frame-Options,
X-XSS-Protection, Referrer-Policy, Strict-Transport-Security to all
responses.
- users.py: guard POST /api/users so only users with role="admin" can
create new accounts (prevents privilege escalation by non-admin roles).
- auth.py: remove raw exception detail from Azure AD 500 response to
avoid leaking internal error messages / stack traces to clients.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Extract shared SlowAPI limiter to app/limiter.py to break circular
import between app.main and app.routers.auth
- Seed default SystemConfig row (id=1) on first DB init so settings
page works out of the box
- Make all docker_service.compose_* functions async (run_in_executor)
so long docker pulls/stops no longer block the async event loop
- Propagate async to netbird_service stop/start/restart and await
callers in deployments router
- Move customer delete to BackgroundTasks so the HTTP response returns
immediately and avoids frontend "Network error" on slow machines
- docker-compose: add :z SELinux labels, mount docker.sock directly,
add security_opt label:disable for socket access, extra_hosts for
host.docker.internal, enable DELETE/VOLUMES on socket proxy
- npm_service: auto-detect outbound host IP via UDP socket when
HOST_IP env var is not set
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Fix#1 - SECRET_KEY startup validation (config.py, .env):
- App refuses to start if SECRET_KEY is missing, shorter than 32 chars,
or matches a known insecure default value
- .env: replaced hardcoded test key with placeholder + generation hint
Fix#2 - Docker socket proxy (docker-compose.yml):
- Add tecnativa/docker-socket-proxy sidecar
- Only expose required Docker API endpoints (CONTAINERS, IMAGES,
NETWORKS, POST, EXEC); dangerous endpoints explicitly blocked
- Remove direct /var/run/docker.sock mount from main container
- Route Docker API via DOCKER_HOST=tcp://docker-socket-proxy:2375
Fix#3 - Azure AD group whitelist (auth.py, models.py, validators.py):
- New azure_allowed_group_id field in SystemConfig
- After token exchange, verify group membership via Graph API /me/memberOf
- Deny login with HTTP 403 if user is not in the required group
- New Azure AD users now get role 'viewer' instead of 'admin'
Fix#4 - Rate limiting on login (main.py, auth.py, requirements.txt):
- Add slowapi==0.1.9 dependency
- Initialize SlowAPI limiter in main.py with 429 exception handler
- Apply 10 requests/minute limit per IP on /login and /mfa/verify
Settings > NPM Integration now allows choosing between per-customer
Let's Encrypt certificates (default) or a shared wildcard certificate
already uploaded in NPM. Includes backend, frontend UI, and i18n support.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Global MFA toggle in Security settings, QR code setup on first login,
6-digit TOTP verification on subsequent logins. Azure AD users exempt.
Admins can reset user MFA. TOTP secrets encrypted at rest with Fernet.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Multi-language support (EN/DE) with i18n engine and language files
- Configurable branding (name, subtitle, logo) in Settings
- Global default language and per-user language preference
- User management router with CRUD endpoints
- Customer status sync on start/stop/restart
- Health check fixes: derive status from container state, remove broken wget healthcheck
- Caddy reverse proxy and dashboard env templates for customer stacks
- Updated README with real hardware specs, prerequisites, and new features
- Removed .claude settings (JWT tokens) and build artifacts from tracking
- Updated .gitignore for .claude/ and Windows artifacts
Co-Authored-By: Claude Opus 4.6 <[email protected]>