The Nojoin team and community take all security vulnerabilities seriously. Thank you for your efforts to improve the security of Nojoin. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
Nojoin is still in active development and all releases should be considered pre-release. There may be security vulnerabilities in the application. Nojoin's maintainers are not responsible for any data loss or security breaches that may occur as a result of using the application. We also advise users to take additional security measures in general but especially when deploying Nojoin over a publicly accessible URL. For example, we recommend using a VPN or a reverse proxy to secure your Nojoin instance.
Nojoin requires an operator-defined FIRST_RUN_PASSWORD before the first successful system initialisation can occur.
- The bootstrap password is only used while the system is uninitialised.
- The web client sends the bootstrap secret via the standard
Authorizationheader using theBootstrapscheme rather than a bespoke setup header. - If
FIRST_RUN_PASSWORDis missing, first-run setup fails closed until the operator sets it and restarts or redeploys. - The setup surface does not disclose initialisation state. The sign-in page never references setup, the
/setuppage renders the same unlock gate in every state, and every anonymous setup denial returns the same403with the detailFirst-run setup access denied.whether the bootstrap password is wrong,FIRST_RUN_PASSWORDis unset, or the system is already initialised. The specific reason is written to the server log only, and the API startup log points operators at/setupwhile the system is uninitialised. - Setup endpoints are rate limited per client address before any state-dependent processing, so throttling behaviour is also identical across states and the bootstrap password cannot be brute-forced quickly.
- After initialisation, normal authenticated admin operations do not use the bootstrap password path.
- The bootstrap password is never returned by the API or persisted into Nojoin configuration.
- Application log output redacts
Authorization, cookies, bootstrap credentials, passwords, tokens, and API-key fields if they are accidentally included in a log record.
Operators should treat FIRST_RUN_PASSWORD as a secret and ensure reverse proxies, ingress layers, and HTTP logging do not record Authorization headers or setup request bodies.
Nojoin also warns operators when FIRST_RUN_PASSWORD, DATA_ENCRYPTION_KEY,
or the tracked Redis/PostgreSQL placeholder secrets still match the shipped
deployment-template values. Those warnings appear in API or worker startup logs
and in the authenticated frontend, but they are advisory only and do not block
startup or first-run setup.
Nojoin's normal browser session uses a Secure HttpOnly cookie, but state-changing browser requests are not trusted solely because that cookie is present.
- Cookie-authenticated browser requests using unsafe methods must come from the trusted Nojoin web origin.
- The backend validates the standard
Originheader, or falls back toRefererwhen needed, for those cookie-authenticated unsafe requests. - Explicit bearer-token API clients are not subject to that browser-origin check.
- The session cookie remains
SameSite=Laxto preserve expected top-level redirect flows such as OAuth callbacks, so unsafe GET-style side effects should continue to be avoided.
Standard browser session, explicit API, and MCP connector JWTs (token types session, api, and mcp) support active invalidation in addition to natural expiry.
- Every issued session/API JWT carries a unique
jti(token id), aniat(issued-at) timestamp, and atvclaim that mirrors the user's currenttoken_version. - Verification rejects a token whose
tvno longer matches the user record. Bumpingtoken_versiontherefore acts as an immediate kill-switch for every previously issued session and API token belonging to that user. token_versionis bumped automatically when the user changes their own password, when an admin resets the password, and when an account is deactivated.- Admins and Owners may also call
POST /api/v1/users/{user_id}/sessions/revoke-allto forcibly invalidate every session for another user. Users themselves may callPOST /api/v1/users/me/sessions/revoke-allto sign out of all other devices. - Calling
POST /api/v1/login/logoutrecords the cookie token'sjtiin a server-side denylist (revoked_jwts), so the captured JWT stops verifying immediately even if a copy was made outside the browser cookie. - The denylist also supports targeted revocation of an individual
jti. Expired entries are pruned opportunistically. Recording capture uses the normal browser session token. There is no separate desktop-helper JWT path for live recording.
Nojoin's built-in MCP connector (MCP.md) issues OAuth 2.1 access tokens of type mcp through Nojoin's own authorization server.
- MCP tokens authenticate only the
/mcpendpoint. They carry themcp:readscope (all read tools) and, for grants consented since the write tools were introduced, themcp:writescope, plus a resource binding; they are rejected by every general API route regardless of scope. - The
mcp:writescope gates only additive tools:import_people(create/update People records),set_speaker_name(name a recording's speaker and link it to a person), andappend_meeting_notes(append to a recording's user notes). None delete data, modify voiceprints, or alter recordings, transcripts, or AI-generated notes. Grants that predatemcp:writekeep working for every read tool; the write tools refuse them until the user reconnects and consents to the wider scope. - Authorization codes are single-use, PKCE-bound (S256), and expire after 60 seconds. Refresh tokens are stored hashed and rotate on every use; reuse of a rotated refresh token revokes the entire grant family.
- Dynamic client registration is open (as the MCP specification requires) but rate limited, accepts only public PKCE clients with HTTPS redirect URIs, and grants no access by itself — every grant requires an interactive, origin-checked consent by a signed-in user.
- Users can list and revoke connector grants under Personal settings. Password changes, admin resets, and session revocation invalidate outstanding MCP access tokens through the shared
token_versionmechanism. - Operators can remove the entire connector surface with
MCP_ENABLED=false(see DEPLOYMENT.md); every connector endpoint then responds404.
Nojoin can route a user's AI inference through their own Claude (Pro/Max) or ChatGPT (Plus/Pro) subscription (the per-user CLI OAuth AI-routing option, off by default). See ADR-0002 for the accepted-risk decision — this uses a consumer subscription contrary to the provider's terms (Anthropic's, or OpenAI's for ChatGPT/Codex).
- The subscription OAuth token is stored encrypted at rest (
encrypt_secret, one row per(user, provider)), never inUser.settings, never logged, and never returned by the API. - Inference runs as a tools-off, single-turn subprocess in the
worker-iolane — the Claude Code CLI, orcodex exec --sandbox read-onlyfor ChatGPT — as the image's non-root user, under a wall-clock timeout. With tools disabled / read-only, an untrusted transcript can only produce text, not act — so no dedicated OS sandbox is used (see ADR-0002). - The subprocess environment is scrubbed so an install-wide key cannot out-rank the user's subscription token: for Claude,
ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN/CLAUDE_CODE_USE_*are removed and onlyCLAUDE_CODE_OAUTH_TOKENis injected; for ChatGPT,OPENAI_API_KEY/CODEX_API_KEYare excluded from the Codex subprocess env (or Codex would bill the API instead of the subscription). Both scrub invariants are locked by unit tests. - CLI OAuth is swappable and non-load-bearing: any failure (auth, usage limit) degrades to the user's configured BYOK/Ollama secondary. Usage limits set a best-effort reset time surfaced in Settings.
- Usage accounting stores aggregate counts only: a per-user daily rollup of token counts and request totals, plus the latest rate-limit reading, so administrators can review subscription usage. Prompt and response content is never stored, and the SDK's notional per-turn cost is kept for reference but never surfaced as money (a subscription is flat-rate).
- Disconnecting (Settings > AI) deletes the encrypted credential and wipes the user's per-user CLI working directory.
JWT signing material is stored as a small keyring rather than a single static value.
- The keyring is persisted to
<user_data>/.secret_keys.jsonand contains anactivekey id (kid) plus any prior keys that are still trusted. - Every issued JWT carries a
kidheader. Verification picks the matching key from the keyring; if thekidis not known the token is rejected. - Operators with shell access can rotate the key by calling
backend.core.security.rotate_signing_key(). Rotation generates a freshkid, makes it active, and (by default) keeps the previous key in the ring so currently outstanding tokens keep verifying until their natural expiry. - After enough time has passed for outstanding tokens to expire,
prune_signing_keys()removes retired keys from the keyring. Any token still signed by a removed key fails verification immediately, providing a hard cut-over. - Setting the
SECRET_KEYenvironment variable overrides the keyring with a single static key (intended for advanced deployments and tests). In that mode the rotation API is disabled. - Existing single-key installs are migrated automatically: the legacy
<user_data>/.secret_keyfile is loaded into the keyring askid="legacy"on first startup and the legacy file is renamed.
Live recording is initiated and controlled by the authenticated web app.
- Browser recording endpoints use the normal Secure HttpOnly session cookie and enforce recording ownership on every init, segment, pause, resume, discard, and finalize request.
- Unsafe cookie-authenticated requests still require the trusted Nojoin web origin through the origin checks described above.
- Browser capture permissions are mediated by the browser. Nojoin cannot silently capture screen, tab, system audio, or microphone input without the user granting permission.
- The browser share picker determines the visible surface and whether shared audio is included on desktop. Nojoin warns when the browser does not grant a shared-audio track and then records microphone audio only. Mobile Chrome capture is microphone-only and does not expose tab, app, or system audio to Nojoin.
- A paused recording blocks new capture starts for that user until it is resumed or discarded, preventing overlapping segment streams.
- Refreshing, closing, or navigating away from the Nojoin tab moves the recording to
PAUSED; uploaded segments remain server-side and only the current in-memory tail is dropped. - Switching focus to another tab, window, or application does not pause recording.
- WebM/Opus, Ogg/Opus, and MP4 audio browser segments are transcoded in worker tasks before final WAV concatenation and final processing.
- Retired native-helper endpoints return structured
410 Goneresponses and do not issue credentials or accept uploads. - Calendar push webhook endpoints (
/api/v1/calendar/webhooks/googleand/api/v1/calendar/webhooks/microsoft) are called by Google and Microsoft rather than by an authenticated user, so they carry no session auth. Each inbound notification is authenticated by a per-subscription secret stored encrypted at rest (the Google channel token or MicrosoftclientState); the payload is treated only as a change signal, and the handler merely enqueues an incremental sync that runs with the account's own stored tokens. Unrecognised or mismatched notifications are ignored, the endpoints never echo whether a subscription exists, and they are rate limited.
For end-user capture setup and troubleshooting, see CAPTURE.md.
Published container images are scanned for known vulnerabilities by Trivy before their rolling tags are published. The policy balances strong assurance against the reality that the worker image is built on a large CUDA/PyTorch base with a slow-moving, inherited CVE surface.
- The release pipeline scans each image and fails the release on CRITICAL or HIGH findings that have a fix available (
ignore-unfixed: true). Such findings are addressable by us — usually by pulling in a patched base image or dependency — so they block publication. - Findings without an upstream fix do not block the release. They cannot be actioned by Nojoin and are unavoidable for the GPU worker base image. They remain visible in scan output for tracking.
- A fixed CRITICAL/HIGH finding may be temporarily accepted only by an explicit, documented exception in .trivyignore, recording the CVE id, the reason, the owner, and a review-by date. Exceptions are expected to be short-lived and are removed once the fix is pulled in, typically through a Dependabot base-image or dependency update.
- Pull requests additionally run an informational dependency and configuration scan that surfaces CVEs without blocking merges.
As Nojoin is in active development, only the latest version is supported. We encourage all users to use the most up-to-date version of the application.
| Version | Supported |
|---|---|
| latest | ✅ |
Please report any security vulnerabilities privately. Do not open public GitHub issues or discuss vulnerabilities in public forums before they have been resolved.
We use GitHub's Private Vulnerability Reporting feature to receive security disclosures securely and privately.
- Submit a Report: Visit GitHub Private Vulnerability Report to submit your report.
- Provide Details: Please include a detailed description of the vulnerability, steps to reproduce, and any proof of concept (PoC) or exploit code.
- Acknowledgement: You will receive an initial response confirming receipt of your report within 48 hours.
- Evaluation: The maintainers will investigate the report and determine the severity and scope of the vulnerability.
- Remediation: If the vulnerability is confirmed, we will work on a patch. A security advisory will be drafted, and a fix will be released.
- Disclosure: Once a patch is available and deployed, we will coordinate public disclosure through a GitHub Security Advisory.
When a confirmed vulnerability requires an out-of-band fix, maintainers follow an expedited release path. The goal is to ship a patched, verifiable image quickly without bypassing the integrity controls that make a release trustworthy.
- Draft privately. Develop the fix on a private branch or through a GitHub Security Advisory draft so the change is not disclosed before an image is available.
- Bump and tag. Update
docs/VERSIONand push a strictvX.Y.Ztag exactly as for a normal release. The same release pipeline runs: full test, lint, build, documentation, and migration gates, followed by image build, vulnerability scan, the health and non-root smoke, and cosign signing. The security urgency does not remove these gates; it only compresses the schedule. - Publish notes. The automated release notes are generated for the tag. Add an explicit security section describing the affected versions, severity, and required operator action (for example, "upgrade immediately").
- Advisory. Publish or update the GitHub Security Advisory, request a CVE where appropriate, and credit the reporter per the coordinated-disclosure workflow above.
If a published image is found to be compromised or dangerously vulnerable, revoke it rather than relying on a newer tag alone, because deployments may still be pulling the old tag or digest.
- Stop the bleeding. Repoint the rolling tags (
latest,major.minor) to the patched image immediately by cutting the patched release; never leavelatestpointing at a known-bad image. - Remove or mark the bad version. Delete the affected immutable version tag and its digest from the GitHub Container Registry package, or, where deletion would break audit history, mark the version as deprecated and document it in the advisory. Note that container digests cannot be silently rewritten — a removed digest fails to pull, which is the intended fail-closed behaviour.
- Invalidate trust signals. Because images are signed by digest, a revoked digest should be called out in the advisory as no-longer-to-be-trusted. Operators verifying signatures should treat the advisory's listed bad digests as untrusted even if a historical signature exists.
- Notify operators. Communicate the revocation through the GitHub Security Advisory and the release notes, including the bad digests, the fixed digest to upgrade to, and any data-safety steps. Operators who pin by digest must be told the exact digest to move to.
- Rotate exposed secrets. If the incident could have exposed signing material, deployment credentials, or registry tokens, rotate them and record the rotation in the advisory timeline.
Operators should subscribe to repository releases and security advisories so that emergency releases and revocations reach them promptly.