Skip to content

Releases: trakbridge/trakbridge

v2.2.1

Choose a tag to compare

@emfourdev emfourdev released this 20 Oct 12:33

TrakBridge v2.2.1 — Threat-Model Follow-Up & Sideload Signature Enforcement

Security patch release closing every item deferred from the v2.2.0 threat-model triage: LDAP secure-by-default, an anonymous-injection gate on inbound endpoints, plugin signature enforcement on sideload paths, an admin unlock action, UDP source-filter validation, an image-scan vulnerability sweep, and the fix that makes OIDC login actually work from the shipped config.

Straight upgrade from v2.2.0 — no schema migration, no new required secrets. Three behaviours change in ways that can affect running deployments; read Before you upgrade first.


Before you upgrade

LDAP: use_tls: true now really negotiates TLS. In every prior release it was a silent no-op — credentials went to port 389 in cleartext. If your directory server does not accept StartTLS on 389, binds will start failing after upgrade. Fix one of three ways: enable StartTLS on the server, switch to LDAPS on 636 (use_ssl: true), or, as an audited last resort, set allow_insecure: true in the LDAP provider config to keep the old behaviour with a startup WARNING on every connection.

Inbound streams with auth_mode: none need an ip_allowlist. Existing streams keep running on their stored config until restart; on restart, or the next time you save them, they are refused until an allowlist is set. 0.0.0.0/0 preserves the old world-accessible behaviour as an explicit, auditable choice.

Loose .py files in external_plugins/ are skipped unless signed. Signed packages load unchanged. To keep loading unsigned community-tier plugins, set plugin_settings.allow_community_sideload: true in config/settings/plugins.yaml; every unsigned load logs a WARNING naming the file. Unsigned pro/enterprise plugins are always refused.

Rebuild or re-pull the container image to pick up the pip, setuptools and msgpack version floors from the vulnerability sweep. Running containers keep their old versions until replaced.


Security

LDAP StartTLS enforced on every bind (T4.1)

_get_connection() now issues start_tls() before any bind whenever use_tls=true and use_ssl=false; a failed upgrade raises before a single credential is sent. The validator refuses plaintext LDAP and validate_cert: false unless allow_insecure: true is set, and when it is, logs a WARNING per connection naming the server and the insecure option. Insecurity stays possible but never accidental.

Inbound auth_mode: none requires an allowlist (T1.3)

The generic JSON and XML inbound plugins now refuse auth_mode: none without an ip_allowlist, with a readable error at save time. The runtime check accepts both the legacy JSON-array format and the new comma/newline-separated text; malformed entries fail closed. Validation now also runs on stream create, not just update, so a bad config is rejected in the form rather than at listener startup.

Plugin signatures enforced on sideload (V1/V2)

Files in external_plugins/ previously loaded at startup with no cryptographic check — filesystem write meant code execution at next restart. Both sideload paths now gate on the same Ed25519 trust root the ZIP installer already uses; loose .py + .py.sig pairs are verified by a new single-file helper. Where signatures come from is documented in the new Plugin Signing section of docs/PLUGIN_SECURITY.md.

Admin unlock without side-effects (T1.4)

New Unlock button on the admin user page, shown only when an account is locked and still active. It clears the lockout without changing status — previously admins had to click Enable, which did more than they meant. config/schema.py now agrees with the runtime: lockout_duration_minutes: int = 30, bounds 1–1440.

UDP multicast source filter validated at save (T1.2)

Invalid source_filter entries and non-multicast multicast_group addresses are caught at config save with a clear message, instead of crashing the socket join at startup with an obscure inet_aton error. Entries must be exact IPv4 host addresses (CIDRs rejected — IP_ADD_SOURCE_MEMBERSHIP needs a host). The kernel-level source-specific join was already in place; help text now explains that the kernel drops unauthorised senders before userspace ever sees the packet.

IRC handler strips CRLF (T5.2)

CoT-derived values (callsign, remarks, group) now have CR/LF replaced and NUL dropped at the socket-write chokepoint, closing IRC command injection via crafted field values. Channel names get the same treatment.

Coordinate validator rejects NaN and infinity (T5.4)

float('nan') passed the inbound bounds check because every comparison against it is False. A math.isfinite() guard now rejects NaN, ±inf and stray strings with an error naming the value.

Image scan cleared

Trivy flagged 9 CVEs across pip, setuptools and msgpack. Version floors (not pins) added to Dockerfile, pyproject.toml, requirements.txt and the PEP-517 build requirements. Closes GHSA-6v7p-g79w-8964, GHSA-4xh5-x5gv-qwph, GHSA-qwm4-qh6w-59xr, GHSA-58qw-9mgm-455v, GHSA-jp4c-xjxw-mgf9, GHSA-wf93-45jw-7689, GHSA-6vgw-5pg2-w6jp, CVE-2025-47273, CVE-2026-59890.

Fixed

OIDC can actually be enabled

Setting OIDC_ENABLED=true always failed at startup with No OIDC provider is enabled. The provider only understood a nested multi-provider config schema, while authentication.yaml, the docs and docker-compose.yml all produce a flat one. It now reads the flat schema: the discovery URL is derived from OIDC_ISSUER using the standard /.well-known/openid-configuration path, scopes are joined into the OIDC scope string, and role_mapping is honoured. Two further breaks hiding behind it are fixed too — a cache initialised after validation ran, and the public application URL never reaching the login route — so the redirect to your identity provider now completes. Covered end-to-end by a test against a real local HTTP identity provider.

Lockouts are actually temporary

Locked accounts auto-unlock once the lockout window passes. Previously the 30-minute window was decorative and every lockout needed an admin.

Rate-limit responses return 429, not 500

Hitting the login rate limit now returns a proper 429 with Retry-After (JSON for API clients, a new 429.html for browsers) instead of a misleading server error. 429s log a WARNING with source IP and endpoint so brute-force signals stay visible.

Error pages match the rest of the app

Error templates now use the app's palette, Inter font and icons. The 404 and 503 pages, which had been wrapped in HTML comments, render again.

Added

  • plugin_settings.allow_community_sideload config key (default false)
  • allow_insecure field on the LDAP provider (default false)
  • ip_allowlist field on both generic inbound plugins
  • Plugin Signing section in docs/PLUGIN_SECURITY.md

Backwards-compatible

  • Existing streams, TAK servers, users and sessions preserved
  • Nested OIDC provider configs continue to work alongside the flat schema
  • Legacy JSON-array ip_allowlist values still parse
  • Signed plugin packages load unchanged

Known follow-ups

  • provider_priority in authentication.yaml is not yet honoured; provider order currently falls back to alphabetical. Tracked for the next release together with an auth-chain change so redirect-only providers never block password logins.

Full changelog: CHANGELOG • Detailed release notes: RELEASE_NOTES.md • Docker images: ghcr.io/emfoursolutions/trakbridge:2.2.1 (multi-arch)

v2.2.0

Choose a tag to compare

@emfourdev emfourdev released this 07 Sep 07:36

TrakBridge v2.2.0 — API Keys, Self-Healing Connections & Security Hardening

TrakBridge.io website now live

Three major workstreams: scoped API key authentication, a ground-up rework of outage recovery so streams heal without operator intervention, and seven security fixes from a full external threat model of v2.1.1.


REQUIRED BEFORE UPGRADING — API_KEY_PEPPER secret

Production deployments will refuse to boot without an API_KEY_PEPPER secret set. This is a hard failure at startup, not a warning — the app exits with a clear error message and will not accept traffic.

Generate a value and provide it BEFORE you pull the v2.2.0 image:

openssl rand -base64 48 > secrets/api_key_pepper
chmod 600 secrets/api_key_pepper

Then provide it to the app via any of:

  • Docker secret file at ./secrets/api_key_pepper (the release docker-compose.yml already references this path)
  • API_KEY_PEPPER environment variable
  • API_KEY_PEPPER_FILE environment variable pointing at any readable file

Rules once set:

  • Treat this secret with the same care as TB_MASTER_KEY — it is mixed into every API-key HMAC
  • Rotating the pepper invalidates every existing API key by design — coordinate with any scripted consumers before rotating
  • Non-production environments (FLASK_ENV != production) generate an ephemeral pepper per process with a startup WARNING — API keys created there die on every restart. This is intentional for local dev and CI

If you skip this and try to start the container in production, you will see:

ERROR: API_KEY_PEPPER is required in production but is not set.
       Generate one with: openssl rand -base64 48
       Provide via API_KEY_PEPPER, API_KEY_PEPPER_FILE, or the Docker secret
       at /app/secrets/api_key_pepper.

Also required: schema migration (add_user_api_keys) — Docker deployments run it automatically via the entrypoint; source deployments run flask db upgrade.

TLS material for nginx — the release docker-compose.yml now ships nginx as the reverse proxy. Drop trakbridge.crt and trakbridge.key into ./init/nginx/ssl/ before bringing the stack up. If you sit behind an external reverse proxy (Traefik, cloud LB, existing nginx), remove the nginx service from the compose file and re-attach trakbridge to your proxy's network. See docs/INSTALLATION.md and init/nginx/ssl/README.md.


API Key Authentication

Personal access tokens for scripts and integrations

Users can create scoped API keys at /auth/api-keys — each key carries explicit permissions (streams:read, api:read, tak_servers:read, etc.), an optional expiry, and last-used tracking. Use as Authorization: Bearer tb_pat_... alongside existing session auth. Tokens are revealed once at creation and stored only as an HMAC-SHA256 hash mixed with the server-side pepper (see above) — a database-only compromise cannot verify or forge tokens.

Admin cross-user view at /admin/api-keys with grouped scope chips. Foreign-prefix (ghp_...) and malformed tokens are rejected before the resolver runs, and tb_pat_ values are redacted from log output.

Privileged surfaces refuse bearer auth

Admin routes, credential mutation, logout, and key-management pages are guarded by @session_only — they return 401 to any bearer request regardless of the token's validity or scopes. A leaked API key cannot manage keys, change passwords, or reach the admin surface.

Documented API Surface

OpenAPI 3.1 spec at /api/openapi.json, interactive Swagger UI at /api/docs — both auth-gated (session or bearer) to keep the API description out of anonymous-scanner reconnaissance. API_REFERENCE.md rewritten from the spec.

Self-Healing Connections

TAK server outages recover without intervention

The headline structural fix. Previously a lost TAK connection opened the per-server circuit breaker and it never recovered — streams stayed dead until an operator edited and re-saved them. Root cause: _transmit_batch swallowed socket errors into a False return, so the TX loop never broke, the reconnect loop never ran, and the breaker's recovery probe wrote to the same dead socket forever.

The rework:

  • Breaker gates connection establishment only — the one operation that can prove recovery
  • Socket errors propagate, the worker abandons the dead socket, and reconnects with exponential backoff (5s→120s)
  • While the server is down, the breaker sits cleanly OPEN — no wasted 30-second timeouts, no OPEN/HALF_OPEN flapping
  • One successful reconnection closes the breaker

Verified live against a real TAK Server with cross-checked subscription logs: pull the network, watch the breaker open; restore it, transmission resumes within a couple of minutes with zero human involvement.

Streams never permanently deactivate

Five consecutive poll errors previously flipped is_active = False permanently. Streams now retry forever with backoff capped at five minutes, log at ERROR at the threshold (and every 10th failure after), and surface the condition through the stream's Last Error field, which clears itself on recovery.

Feed outages are visible

Plugins report connection failures using an internal sentinel format the poll loop didn't understand — it counted them as retrieved locations, so a total feed outage showed a green stream delivering nothing. Sentinel results now register as failed polls and drive the same retry/backoff/Last Error machinery.

Circuit breaker core hardening

  • Trips on consecutive failures (previously a lifetime cumulative count, so 3 failures over days could open a breaker)
  • HALF_OPEN probe slots are refunded when calls complete
  • Health checks run in HALF_OPEN as well as OPEN, tolerate sync callables, log crashes at ERROR
  • Health probes no longer failed by TAK Server's slow TLS close handshake — probe cleanup is best-effort with a 2s cap
  • New tuning knobs documented in performance.yaml: timeout, half_open_max_calls, success_threshold, health_check_interval, health_check_timeout

Security Hardening

Seven items from an external threat model of v2.1.1, all with focused unit-test coverage (37 new tests). The full threat model is in dev/TRAKBRIDGE-THREAT-MODEL.pdf.

Preview endpoints now require authentication

GET/DELETE /api/inbound/<id>/preview and POST /preview/remap accepted anonymous calls. Any caller with network reach could read captured payloads (including headers and source IPs of reporting devices), wipe the buffer, or re-run the parser with an attacker-supplied plugin_config merged over the stream's decrypted config. All three endpoints now require @api_key_or_auth_required.

Reconnaissance endpoints tightened

/api/status, /api/version, /api/health/ready, and /api/health/live returned application state and version to unauthenticated callers. All four now require auth. /api/health remains the sole public probe — the single canonical health check for containers and load balancers.

Outbound plugin SSL fallback removed

Garmin, Discord and Slack plugins caught ssl.SSLError and retried the same request with ssl=False, re-sending the Garmin account password or webhook URL over an unverified TLS channel. A MITM only needed to present an invalid cert. All fallback blocks are deleted — SSL errors now propagate as ERROR entries and test-connection reports the real failure instead of a misleading "success (insecure)" result.

Stream detail page no longer leaks decrypted plugin config

The Test Connection button at /streams/<id> iterated stream.get_plugin_config() (the decrypting accessor) and embedded every value into an inline <script>. Any user who could load the page — including a VIEWER — could Ctrl-U and read Garmin passwords, API keys, MQTT broker passwords, and Discord/Slack webhook URLs in plaintext. The button now calls the server-side /streams/<id>/test endpoint with only the stream id; no decrypted value crosses the wire back through the browser.

Orphan recovery subsystem deleted

services/recovery_service.py + services/recovery_implementations.py (1,181 lines) shipped alongside the circuit breaker but were never wired up: initialize_recovery_system() had no callers and /api/health/recovery published a fabricated success rate from an idle singleton. Both modules deleted; /api/health/recovery now derives status directly from CircuitBreakerManager.get_all_status(). URL unchanged; monitoring pointed at it now gets truthful data.

TAK server delete guard extended to many-to-many

The delete guard only checked the legacy Stream.tak_server_id foreign key. The newer stream_tak_servers M2M association uses ondelete=CASCADE, so a stream connected only via that table had its association row cascaded away when the server was deleted — the stream kept reporting success while quietly delivering to one fewer destination. Guard now checks both relationships.

nginx re-wired as release reverse proxy

The release docker-compose.yml had dropped its nginx service during dev/staging contamination, leaving operators on plain HTTP. nginx is back as the shipped ingress on ports 80/443 with TLS termination, HTTP→HTTPS redirect, rate limiting, and WebSocket support. trakbridge no longer publishes port 5000 to the host.

Also fixed

  • Duplicate monitoring: key in performance.yaml — the second block silently shadowed the first, discarding the fallback-statistics settings at load
  • Parallel config systems collapsed into ConfigLoader — utils/config_manager.py deleted (−366 lines), single loading path for all configuration
  • Backups directory created on first run — setup no longer assumes it exists
  • Swagger UI external validator badge disabled — no spec URL...
Read more

v2.1.1

Choose a tag to compare

@emfourdev emfourdev released this 16 Aug 20:40

TrakBridge v2.1.1 — LDAP Login & HTTP Session Fixes

Targeted patch release for two production-blocking authentication bugs, plus cleanup on plugin registry sync, startup logging, and CI version stamping. Straight upgrade from 2.1.0 — no schema migration, no configuration change required.

Authentication Fixes

LDAP/OIDC users unblocked from force-password-change flow

force_password_change_required returned True for any user whose password_changed_at was NULL — but external-provider users legitimately have that column NULL (they have no TrakBridge-managed password to timestamp). Every LDAP and OIDC user was redirected to /auth/force_password_change on first login, a page they had no way to complete.

The check now short-circuits False for any user whose auth_provider is not AuthProvider.LOCAL. Local users are unaffected.

SESSION_COOKIE_SECURE is now bidirectional

The previous logic (_is_production or _secure_override in ("1","true","yes")) could only force Secure on — no value would turn it off in production. Operators behind non-TLS-terminating proxies (plain HTTP) had no supported escape hatch, and browsers silently drop Secure cookies on HTTP, breaking every session-authenticated request after login.

  • SESSION_COOKIE_SECURE=true/1/yes → force Secure on
  • SESSION_COOKIE_SECURE=false/0/no → force Secure off (new)
  • Unset → defaults to on in production, off elsewhere

A WARNING is logged at startup when Secure is explicitly disabled in production.

Fixed

  • Duplicate startup log lines — Flask's default handler on app.logger was emitting each record twice; the default handler is now removed on startup so records propagate exactly once.
  • Startup banner reported "unknown" for Python version, platform, and user — banner was reading fields from the wrong level of get_version_info(), and container deployments typically have USER/USERNAME unset. Fields are now read from the nested environment block, and current user is resolved via getpass/pwd fallbacks.

Changed

  • Plugin registry sync reconciles manifest drift — sync_plugin_registry now detects when an on-disk plugin.yaml differs from the stored InstalledPlugin row and updates in place, writing a PluginAuditLog entry with action metadata-synced. Previously drift accumulated silently until reinstall.
  • Plugin version-compare no longer swallows real bugs — _version_satisfied narrowed its blind except Exception to (ValueError, AttributeError, TypeError). Real bugs (ImportError, etc.) now surface with real tracebacks instead of being logged as parse failures.

Internal

  • CI setuptools-scm pretend version sourced from pyproject.toml — dev/staging/main build jobs in .gitlab/ci/build.yml no longer hardcode 0.1.0.dev0. The version being worked toward now lives at [tool.trakbridge.release].next in pyproject.toml and is read by CI. Tag builds continue to use $CI_COMMIT_TAG. Bumping the next-release target is now a one-line pyproject change.

Upgrade Notes

  • Straight upgrade from 2.1.0. No schema migration, no configuration change required.
  • Operators running TrakBridge over plain HTTP in production (e.g. behind a non-TLS-terminating proxy) can now set SESSION_COOKIE_SECURE=false to make session cookies functional. Not recommended — terminate TLS at a reverse proxy in front of TrakBridge for production deployments — and a WARNING is logged at startup when it's used.
  • Existing LDAP/OIDC users previously trapped in the force-password-change redirect can now log in normally. If you manually set password_changed_at in the database as a workaround, no action is required.

v2.1.0

Choose a tag to compare

@emfourdev emfourdev released this 09 Aug 23:05

TrakBridge 2.1.0 — Security Hardening

Security defaults tightened across the admin UI, session handling, plugin distribution, and multi-arch container publishing. No schema migration, no configuration change required — straight upgrade from 2.0.1.

Full release notes: RELEASE_NOTES.md · CHANGELOG · Upgrade Guide

Security

  • CSRF protection on by default — session-authenticated POST/PUT/PATCH/DELETE require a valid CSRF token. Bundled admin UI ships with a global fetch() interceptor for AJAX. Five bearer/identity endpoints explicitly exempted.
  • Explicit session cookie flags — HttpOnly and SameSite=Lax always; Secure in production (env-var overridable elsewhere).
  • Unauthenticated API surface trimmed — eight health/monitoring/utility endpoints now require authentication. GET /api/status retained as the sole unauthenticated probe.
  • Signed-plugin gate for premium tiers — plugins declaring tier: pro or tier: enterprise must carry a valid Emfour Ed25519 signature. Community plugins install unchanged.
  • Plugin allow-list hardening — file mode 0o600, symlink writes refused at target and parent.
  • CA cert upload filename sanitisation — path-traversal filenames rejected.
  • Master key value no longer logged at DEBUG — see rotation advisory below.
  • Plugin manager licence tier badge — deployment tier surfaced in the header and upload modal.

Packaging

  • Multi-arch container images restored — GHCR and Docker Hub publish preserves amd64+arm64 manifests. Regression since 2.0.0.

Behavioural changes

  • Any script or integration posting to session-authenticated endpoints with only a saved session cookie (no CSRF token) will now receive 400 Bad Request. Migrate to bearer/API-key auth or include X-CSRFToken header.
  • The eight previously unauthenticated health/monitoring/utility endpoints now return 401 to unauthenticated callers.
  • New logins receive session cookies with HttpOnly; SameSite=Lax (and Secure in production).

Upgrade checklist

  1. Deploy the new image — all Phase 1 security changes take effect immediately.
  2. Verify admin UI login still works (confirms CSRF token flow intact).
  3. If your deployment previously ran at LOG_LEVEL=DEBUG with a runtime-generated master key, rotate the master key via Admin → Key Rotation (prior versions logged the generated key value).
  4. Existing user sessions keep their pre-2.1 cookies until expiry or logout; no forced re-login required.

Pull

docker pull ghcr.io/trakbridge/trakbridge:v2.1.0
docker pull emfoursolutions/trakbridge:v2.1.0

v2.0.1

Choose a tag to compare

@emfoursolutions emfoursolutions released this 22 Jul 22:13

Version 2.0.1 - Multi-Server RX Dispatch & Worker Stability

Release Date: July 23, 2026
Focus: Silent data-loss fix for multi-server streams and a long-standing worker-recycling bug that surfaced in 2.0.0

Two hotfixes for issues introduced or first observed in 2.0.0. The RX dispatch bug caused output plugins to silently miss traffic from every TAK server beyond the first when a stream was attached to multiple servers — the postgres archiver made this visible for the first time. The Hypercorn worker was silently recycling every ~1000 requests (roughly every 30 minutes on a healthy deployment), tearing down long-lived stream state and interrupting CoT delivery; this has been present since Hypercorn was introduced but only became observable in 2.0.0 because plugin whitelist gating and buffering output plugins now depend on continuous worker uptime.

No schema changes. No new configuration. Straight upgrade from 2.0.0.


BUG FIXES

RX plugin dispatch now respects the multi-server junction table

Silent data loss for output plugins on multi-server streams — critical

The 2.0.0 many-to-many stream_tak_servers junction table was correctly consulted on the TX side and during worker startup, but the RX-side plugin dispatch query in services/cot_service_integration.py still filtered streams by the legacy single-server tak_server_id column. When a stream was attached to multiple TAK servers, every CoT event received from any server other than the one referenced by the legacy foreign key was silently discarded — the output plugin was never called.

  • Symptom: postgres archiver (or any output plugin) captured traffic from TAK server 1 only; server 2's events were missing entirely with no error log
  • Blast radius: every output plugin — archivers, forwarders, notifiers, external HTTP outputs — attached to any multi-server stream
  • Fix: the RX dispatch query now matches streams via either the legacy foreign key or membership in stream_tak_servers, mirroring the "multi-first, legacy-fallback" pattern already used elsewhere in the codebase. Backward compatible: single-server streams continue to work unchanged. No migration required.

Hypercorn worker no longer recycles every ~1000 requests

Long-lived stream state and buffered plugin data preserved across worker lifetime

The Docker entrypoint launched Hypercorn with --max-requests 1000, a stateless-web-worker recycling pattern that fights TrakBridge's single-long-lived-worker architecture. Every recycle:

  • Tore down every stream worker and TAK server connection
  • Triggered graceful shutdown of the plugin cache (draining buffered state)
  • Interrupted CoT delivery for the ~1–2 second graceful-shutdown window
  • Re-triggered plugin whitelist reload — which in 2.0.0 became a fragile path

The recycling was silent in earlier releases because output plugins had no buffered state and no whitelist gating existed. In 2.0.0 the combination surfaced the bug as observable data loss (buffered rows in the postgres archiver) and unrecoverable startup failures (whitelist reload permission errors on some deployments).

--max-requests has been removed from the Hypercorn command line. The HYPERCORN_MAX_REQUESTS environment variable is no longer honoured; any override in existing compose files is silently ignored. Worker hygiene is now handled by graceful shutdown and container health checks alone.

Full COT service shutdown on process teardown

Output plugin cleanup and pending output stats now flushed on SIGTERM

_cleanup_persistent_cot_service() in services/stream_manager.py only stopped in-flight workers on shutdown; it never awaited QueuedCOTService.shutdown(). That meant:

  • cleanup() hooks on cached output plugins were never called
  • Pending output-stat writes were never flushed to the database
  • Buffering plugins (like the postgres archiver) lost any in-memory queue on SIGTERM

The teardown path now drives the full async shutdown via run_coroutine_threadsafe on the stream manager's event loop, matching the pattern already used for stop_all() and _cleanup_monitoring_services(). A worker-stop fallback remains for the edge case where no event loop is available, with a warning logged to make skipped plugin cleanup visible.


PLUGIN SDK

No SDK version bump. The lifecycle hooks (start(), cleanup()) documented in the 2.0.0 SDK are now correctly driven by core on every configured teardown path: stream reconfig, stream deletion, stream stop/restart, and clean process shutdown.


UPGRADE NOTES

Straight upgrade from 2.0.0. No schema migration, no configuration change required.

Behavioural change: workers now stay up until the container is explicitly restarted. Health-check based hygiene continues as before. If you were relying on periodic worker recycling for any reason (memory cleanup, garbage collection, socket refresh), your Hypercorn worker will now behave like a genuinely long-lived process — plan container restart cadence accordingly.

The HYPERCORN_MAX_REQUESTS environment variable is no longer read. Override entries in existing docker-compose.yml files can be removed at your convenience; leaving them in place is harmless.

v2.0.0

Choose a tag to compare

@emfoursolutions emfoursolutions released this 19 Jul 08:06

Version 2.0.0 - Plugin SDK, Tiered Licensing & Plugin Manager

Release Date: July 18, 2026
Focus: Public Plugin SDK, Offline Licence Service, Admin Plugin Manager with Signature Verification and Tier Gating

TrakBridge 2.0 introduces the foundation for a tiered product model. Plugin base classes are now published as a standalone SDK on PyPI so third parties can build their own plugins. A new offline licence system maps deployments to Community, Pro, or Enterprise tiers. The admin UI gains a plugin manager that installs, verifies, and gates plugins by tier without touching the shell. Together these lay the groundwork for premium plugin distribution while keeping every existing Community capability free and unchanged.


NEW FEATURES

trakbridge-plugin-sdk — Public Plugin SDK on PyPI

Third parties can now write TrakBridge plugins without a fork

Plugin base classes (BaseGPSPlugin, BaseOutputPlugin, BaseInboundPlugin) and their supporting config/metadata infrastructure have been extracted from the core repository into a standalone Apache-2.0 licensed package: trakbridge-plugin-sdk on PyPI.

  • pip install trakbridge-plugin-sdk and subclass the base class of your choice — a working plugin is 30-40 lines of Python
  • Runtime provider registry — core services (encryption, plugin metadata, circuit breaker, CoT delivery) are injected into the SDK at startup via trakbridge_sdk.configure(...). Plugins never import core modules directly, so the SDK works standalone in unit tests
  • Contract documentation and worked examples for GPS, output, and inbound plugin types ship with the SDK
  • 17 built-in plugins now use the SDK through a re-export shim at plugins/base_plugin.py; no behaviour change for existing installs

Offline Licence Service

Ed25519-signed licence files, air-gap friendly, fail-secure

New services/license_service.py verifies signed licence files against an embedded Emfour public key. Zero network calls; no phone-home.

  • Three tiers: Community (default, no licence required), Pro, Enterprise
  • Licence file location: secrets/tb_license.json by default, overridable via TRAKBRIDGE_LICENSE_FILE
  • Fails securely: missing, tampered, expired, or malformed licences degrade to Community with a logged warning — the app never crashes over licensing
  • Live expiry: the tier is re-checked on every query, so a licence that lapses while the app is running downgrades in place without a restart
  • Admin about page shows current tier badge, licence status, customer, and expiry
  • In-app licence installation — admins upload signed licence files at /admin/about; the file is verified before anything is written, rejected uploads preserve the current licence, and installs take effect immediately with an audit log entry

Admin Plugin Manager

Install, enable, disable, and uninstall plugins from the browser

New /admin/plugins blueprint replaces the previous "SSH in and place a .py file in external_plugins/" workflow.

  • Package format: plugins ship as .zip or .tar.gz archives containing a plugin.yaml manifest, an entry-point Python file, and any supporting modules
  • 12-step install validation chain: archive safety (traversal, symlinks, decompression bomb caps), manifest structure, minimum TrakBridge version, tier gate, signature verification, AST code scan (rejects exec/eval/os.system/subprocess/ctypes), base-class inheritance verification, and identity-rule enforcement — a rejected package leaves no trace on disk, in the database, or in the whitelist
  • Signature verification: packages signed with the Emfour Ed25519 key show a green "Verified" badge; tampered signatures are rejected outright. Unsigned third-party packages install with an explicit "UNVERIFIED" warning
  • Tier gating: plugin.yaml gains an optional tier field enforced at BOTH install time and load time — a premium plugin cannot be loaded on a Community deployment even if the file is manually copied into external_plugins/
  • Lifecycle actions: enable/disable toggles the whitelist without deleting files; uninstall removes files, database record, and whitelist entry. Both block cleanly if any active stream references the plugin
  • Audit logging on every action (install accepted/rejected, enable/disable/uninstall) with admin username, plugin id, tier, and verification status
  • Sidebar link: admin users see a new "Plugins" entry alongside Key Rotation

Whitelist Gating (Security Hardening)

External plugins now require explicit allow-listing

Previously, any Python file dropped into external_plugins/ would be loaded automatically by the plugin manager. This release removes that blanket allowance: each external plugin now requires an explicit external_plugins.<plugin_id> entry in plugins.yaml — managed automatically by the plugin manager UI.

Backwards compatible: on startup, the plugin manager auto-adds whitelist entries for any pre-existing external plugins so current deployments continue to work without manual intervention. See Upgrade Notes below.


DATABASE CHANGES

New tables (added via Alembic migration add_plugin_management_tables):

  • installed_plugins — tracks packaged external plugins (id, version, tier, verified flag, enabled state, install metadata)
  • plugin_audit_log — records every plugin lifecycle action (install/enable/disable/uninstall) with admin username, licence id where applicable, and outcome

Docker deployments run migrations automatically via docker/entrypoint.sh; source deployments need flask db upgrade.


BUG FIXES

  • add_ca_cert_to_streams migration now uses safe_add_column/safe_drop_column and no longer crashes with DuplicateColumn on partially-populated Postgres schemas
  • Plugin category display in the stream edit UI correctly reflects the CoT Forwarding vs Notifications split introduced in 1.3.0

v1.3.0

Choose a tag to compare

@emfoursolutions emfoursolutions released this 05 Jul 08:59

Version 1.3.0 - Inbound Streams, Outbound Plugins & Multicast Bridge

Release Date: July 5, 2026
Focus: Push-Based Inbound Streams, New Outbound Plugins, UDP Multicast CoT Bridge, TAK Worker Stability, Auth Hardening


LICENSE CHANGE

TrakBridge is now licensed under the Apache License 2.0.

From v1.3.0 onward, TrakBridge is distributed under Apache-2.0 (previously LGPL-3.0). Versions released before v1.3.0 remain available under their original licence. The change gives users a simpler, more permissive licence and aligns the project licence across the codebase (the README previously misstated GPLv3 against an LGPL LICENSE file — both are now consistent). No action is required from users; Apache-2.0 places fewer obligations on redistribution than LGPL.


NEW FEATURES

Inbound Stream Architecture

Receive Data from External Sources

TrakBridge can now accept push-based data from external systems via a dedicated inbound stream pipeline.

  • HTTP push endpoint — POST /api/inbound/<stream_id>/data receives location payloads and routes them through the CoT pipeline to configured TAK servers. Security controls include anti-enumeration responses (consistent timing regardless of stream existence), IP allowlist enforcement, coordinate validation, payload size limits, per-stream rate limiting, and API key masking in logs.
  • Capture buffer & preview mode — Inbound streams can capture received payloads into a bounded buffer for inspection via the stream detail page before committing to live routing. Useful for validating field mappings against real data.
  • InboundStreamWorker — Dedicated worker class manages the lifecycle of push-based streams inside StreamManager, with the same health monitoring and registry cleanup as outbound workers.
  • Inbound stream UI — Stream detail and create/edit pages surface inbound-specific configuration: API key (with generate button), rate limiting, IP allowlist, and Preview Mode badge.

Active-Connect Inbound Plugins

TrakBridge Connects Out to Pull Data In

  • MQTT active-connect — TrakBridge connects to an MQTT broker, subscribes to a configurable topic, and forwards received CoT to TAK servers. Supports plain and TLS/mTLS connections via the existing certificate infrastructure (cert_utils.build_ssl_context()).
  • WebSocket active-connect — TrakBridge connects to a WebSocket server and forwards received CoT messages.
  • Generic inbound plugins — generic_xml_inbound and generic_inbound provide a baseline push-receive implementation registered in the plugin manager, available for all stream types.
  • CA cert upload — Inbound streams now support uploading a CA certificate bundle for TLS verification of upstream sources.

Inbound Plugin Display Names

Inbound plugin display names have been updated to be more descriptive and consistent:

Plugin Previous name New name
generic_inbound Generic Inbound JSON Receiver
generic_xml_inbound Generic XML Inbound XML Receiver
inbound_http Inbound HTTP HTTP Location Endpoint
inbound_active Inbound Active MQTT / WebSocket Client

UDP Multicast CoT Bridge

Bridge LAN Multicast Across VPN/WAN Links

The udp_multicast_listener inbound plugin joins a UDP multicast group and forwards every received CoT event to all TAK servers configured on the stream — bridging ATAK Mesh SA traffic across network segments where IP multicast does not route.

Wire format support:

  • CoT XML — forwarded verbatim to the TAK output queue
  • TAK Protocol v1 Mesh SA protobuf — detected by magic bytes (0xbf 0x01 0xbf), decoded via takproto to CoT XML, then forwarded through the standard TAK output path

Network configuration:

  • Joins IGMPv2 by default; IGMPv3 source-specific multicast available via source_filter
  • Configurable bind interface for multi-homed hosts
  • Per-source token-bucket rate limiting; first five dropped datagrams per stream sampled into logs with source IP and 200-byte hex preview for triage

Docker guidance:

  • macvlan network (recommended): container gets a LAN IP, keeps reverse-proxy reachability
  • network_mode: host (simplest): no proxy/reverse-proxy required

Note: ATAK Mesh SA AES encryption is intentionally not yet implemented — wire layout needs verification against ATAK-CIV source before any crypto is written.

Plugin Category Split: CoT Forwarding & Notifications

Output plugins are now organised into two distinct categories in the stream creation UI:

CoT Forwarding — plugins that forward raw CoT events to external systems:
outbound_http, outbound_mqtt, outbound_websocket, udp_multicast_publisher

Notifications — plugins that post human-readable alerts to messaging platforms:
discord_handler, slack_handler, irc_handler

Previously all seven appeared under a single "Output Handlers" category. Splitting them makes it easier to find the right plugin for the job.

CoT Forwarding Plugins

Forward CoT to External Systems

Three focused forwarding plugins replace the legacy webhook_handler:

OutboundHTTP — POSTs or PUTs CoT events to any HTTP/HTTPS endpoint:

  • Payload formats: JSON, raw XML, or custom template
  • Full message rules, UID filtering, geofence, and deduplication pipeline
  • Scheme allow-listing (http/https only) prevents non-HTTP URLs reaching network I/O
  • CRLF injection prevention in custom headers at the field level

OutboundMQTT — Publishes CoT events to an MQTT broker topic:

  • Persistent paho-mqtt connection with loop_start/loop_stop
  • TLS/mTLS via cert_utils.build_ssl_context() for mqtts:// URLs
  • Bounded queue with oldest-drop semantics; every drop increments events_dropped
  • Bad-auth detection: plugin stays disconnected and logs clearly when credentials are wrong

OutboundWebSocket — Maintains a persistent WebSocket connection and streams CoT events:

  • Exponential backoff reconnect (cap 30s) with background reader for proactive server-close detection
  • Bounded queue with oldest-drop semantics
  • URL credential redaction in all log statements
  • Scheme allow-listing (ws/wss only)

UDP Multicast Publisher — publishes CoT events to a UDP multicast group.
Forwarded event counts are now persisted to the stream DB record via a batched flush (every 30 seconds or on plugin stop), fixing the "0 Messages Sent" display on the stream detail page.

Shared output-plugin helpers (services/output_plugin_helpers.py):

  • CoT variable extraction, template formatting, message rule filtering, geofence evaluation, deduplication, rate limiting, and payload building extracted into a reusable module
  • All forwarding plugins compose against this module; no logic duplication

The legacy webhook_handler.py plugin and its test files have been removed. Documentation updated throughout.

CoT Identity Heartbeat Improvements

  • how attribute set to h-e (human-entered) for team-member CoT types — TAK Server was rejecting machine-generated m-g for these events
  • precisionlocation element added to team-member heartbeats for well-formed XML acceptance
  • __group always emitted with sensible team colour/role defaults when identity is enabled
  • Identity CoT now sent before draining the event queue at TX start — TAK Server sees TrakBridge in the roster before any event stream begins

BUG FIXES

Expired Password Redirect & Admin Reset Lockout

Users with an expired password were previously dropped into the application with an inconsistent session state instead of being redirected to the change-password flow. Admins triggering a force-reset were similarly affected.

The root cause was that admin-initiated resets set password_changed_at to None, which the expiry check treated as "never changed" — immediately expired. This locked the user in a redirect loop on next login.

Fix: A new must_change_password boolean column on the users table decouples forced-change from expiry state. Setting this flag redirects the user to the change-password page on their next login without touching password_changed_at. The expiry check only fires for genuinely expired passwords; the forced-change flag fires independently.

A database migration (add_must_change_password) adds the column automatically on startup.

False-Positive Unhealthy Warnings for Output Plugin Workers

The stream manager's periodic health check emitted WARNING-level logs for output plugin workers (Discord, Slack, IRC, HTTP, MQTT, WebSocket, UDP Multicast Publisher) that were operating normally. These plugins do not hold a persistent TAK connection — they receive dispatched CoT events rather than maintaining a long-lived socket — so the absence of a connection is expected behaviour, not a fault.

Fix: Output plugin workers are now recognised as healthy when their task is running, regardless of TAK connection state. Genuine failures (task exited, plugin crashed) still surface as WARNING.

TAK Worker Task Leak on Stream Save (Critical)

Saving a stream configuration triggered restart_worker, which stopped the old worker and started a new one. The old _enhanced_transmission_worker caught CancelledError and broke out of its loop — but did not cancel the inner _tx_loop/_rx_worker child tasks it had spawned. Those orphaned tasks continued running against the now-removed queue and stale writer for 2+ seconds after stop_worker returned.

What operators saw: ~20 Queue N does not exist ERROR logs at 10Hz after every stream save, followed by the orphaned loop's writer failing — each failure charged to the circuit breaker for the TAK server ID the new healthy worker was also using. After three failures the breaker opened, locki...

Read more

v1.2.3

Choose a tag to compare

@emfoursolutions emfoursolutions released this 24 Mar 19:18

v1.2.3 — TAK Connection Auto-Recovery

Release Date: March 24, 2026

This release fixes two critical TAK server connectivity issues that required manual restarts to recover from.


Bug Fixes

TX Worker Auto-Reconnect on Connectivity Loss

When a TAK server connection was lost (network outage, server restart, etc.), the TX worker exited permanently. The circuit breaker would transition to OPEN but could never recover because no worker was alive to record successes. A full TrakBridge restart was required.

Fix: The TX worker now wraps its connection lifecycle in a reconnection loop with exponential backoff (5s → 10s → 20s → ... capped at 2 minutes). When connectivity is restored, the worker reconnects and resumes transmitting automatically. Backoff resets on successful connection.

Circuit Breaker Not Recovering After Stream Edit

Editing and saving a stream caused permanent connection loss. The stream worker teardown generated connection errors that tripped the circuit breaker to OPEN. Since circuit breakers are keyed per TAK server (not per stream), the restarted worker inherited the stale OPEN state and could never connect.

Fix:

  • Circuit breaker is reset to CLOSED during stop_worker(), giving restarted streams a clean slate
  • Dead worker cleanup in start_worker() also resets the circuit breaker (belt-and-suspenders)
  • Stale connection references are removed from the connection registry on worker stop

v1.2.2

Choose a tag to compare

@emfoursolutions emfoursolutions released this 22 Mar 17:50

v1.2.2 — Plugin Poll Interval, TAK Connection Stability & Queue Fixes

Release Date: March 22, 2026


Enhancements

  • Plugin-overridable minimum poll interval — Plugins can now declare a min_poll_interval metadata key as low as 1 second, overriding the previous 30-second floor. The stream create/edit forms dynamically adjust the input minimum based on plugin metadata. Plugins without the key default to 30 seconds.
  • Queue capacity increase — Max queue size increased from 500 to 600 events and warning threshold raised to 600, accommodating high-volume streams like AIS that produce 400+ events per cycle.

Bug Fixes

TAK Connection Stability

  • TX loop clean exit on dead connections — Connection errors (ConnectionError, OSError, SSLError) now cause the TX loop to break immediately instead of retrying on a dead socket. Eliminates error spam when stream config changes tear down SSL connections.
  • Identity heartbeat propagates connection errors — A dead socket during heartbeat write now exits the TX loop cleanly instead of being silently caught.
  • Transmit batch aborts on connection loss — Remaining events in a batch are skipped when the socket is dead, instead of logging errors for each one.

RX Worker Resilience

  • Exponential backoff on RX errors — RX worker now backs off from 1s to 60s on repeated errors instead of retrying every second.
  • SSL error give-up threshold — After 5 consecutive SSL failures, the RX worker stops cleanly with a log message suggesting certificate check or disabling enable_rx.
  • Improved RX error logging — Error messages now include the TAK server ID, attempt count, and next retry delay.

Queue Configuration

  • Performance.yaml settings now applied — Queue manager was ignoring max_size, queue_warning_threshold, and transmission settings from performance.yaml due to config keys living under separate YAML sections that weren't merged before being passed to the queue manager.
  • Stale hardcoded defaults updated — Internal fallback defaults updated from 500/400 to 600/600 to match performance.yaml.

Poll Interval

  • Absolute floor lowered — Global minimum poll interval reduced from 5s to 1s across JSON validator, config schema, and HTML templates.

Documentation

  • Plugin Development Guide (wiki and docs) updated with min_poll_interval usage, behaviour table, and code examples.
  • CHANGELOG and RELEASE_NOTES.md updated for v1.2.2.

v1.2.1

Choose a tag to compare

@emfoursolutions emfoursolutions released this 08 Mar 06:50

v1.2.1

Security

  • Database credential sanitization — Database URIs displayed in the Encryption Key Rotation page and startup logs are now sanitized to prevent plaintext password exposure. Credentials are replaced with **** in all UI and log output.

Bug Fixes

  • LiveUAMap region IDs corrected — Completely rebuilt the LiveUAMap plugin region-to-API-ID mapping using verified data from the LiveUAMap source. The previous mappings were incorrect, causing 403 errors when querying regions. All 180+ regions now use correct API resid values.
  • Migration startup race condition — Fixed a crash during v1.1.0 → v1.2.0 upgrades where the startup thread would query tak_servers.enable_rx before Alembic migrations had created the column. Migrations now set TRAKBRIDGE_MIGRATION_MODE to defer the startup thread.
  • TrakBridge identity echo filtering — Identity heartbeat CoT messages sent by TrakBridge are now filtered when echoed back by the TAK server, preventing them from being forwarded to output handler plugins (Discord, Slack, IRC).
  • Handler plugin log noise — Reduced per-message logging in Discord, Slack, and IRC handler plugins from INFO to DEBUG, significantly reducing log volume in production.

Infrastructure

  • Docker networking — Removed external port mappings from dev and staging environments; all communication now uses Docker internal networking.