- Add btcexplorercookiefile to BTCPay deterministic config so NBXplorer
cookie authentication succeeds (fixes 401 Unauthorized)
- Set WorkingDirectory to package lib dir so ASP.NET Core can locate
wwwroot and LanguageService.ctor does not throw ArgumentNullException
- Update regression test to assert cookie file path and WorkingDirectory
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
prefetch-npm-deps runs in the deps fetcher's buildPhase and checks for
package-lock.json at CWD (repo root). postPatch runs in both the
fetcher and the main build, so we copy the lock file there. The main
build then cds into the subdir in buildPhase.
npmDepsHash values remain placeholders (lib.fakeHash).
postUnpack did not run in the npmDeps fixed-output derivation, so
prefetch-npm-deps couldn't find package-lock.json. preBuild runs in
both the fetcher and main build, ensuring consistent CWD for the lock
file lookup. Backend patch moved to prePatch/postPatch so it only
applies in the main build.
npmDepsHash values remain placeholders (lib.fakeHash).
The `patches` attribute was being applied inside the npmDeps
fixed-output derivation where patch runs interactively and fails.
sourceRoot also caused the deps fetcher to hash a different tree than
expected. Now:
- postUnpack cds into the subdir for both the fetcher and main build
- postPatch applies the mining-pool patch only in the main build
- npmDepsHash values remain placeholders (lib.fakeHash)
npm ci in buildPhase hangs because the Nix build sandbox has no network
access. buildNpmPackage pre-fetches dependencies as a fixed-output
derivation with network access, then the main build runs offline.
npmDepsHash values are placeholders (lib.fakeHash) — the first builds
will fail with hash mismatches that report the correct hashes.
buildNpmPackage is built into nixpkgs and pre-fetches dependencies as a
fixed-output derivation with network access. npmDepsHash is a
placeholder (lib.fakeHash) — the first build will fail with a hash
mismatch that reports the correct hash.
npm ci in buildPhase hangs because the Nix build sandbox has no network
access. Adopt nix-bitcoin's approach: fetchNodeModules pre-fetches
node_modules as a fixed-output derivation (which has network access),
and the main build copies them offline. Uses the same node_modules hash
as upstream nix-bitcoin for RTL 0.15.8.
ensureClauses generates ALTER ROLE clauses, not GRANT statements, so
'"DATABASE btcpayserver" = "ALL PRIVILEGES"' produced invalid SQL.
ensureDBOwnership = true makes each user own its database, which is
the intended effect.
The services.lnd.macaroons submodule has no 'enable' option — macaroons
are implicitly enabled by being defined. Replace 'enable = true' with
'user = cfg.btcpayserver.user' which is the correct option for
controlling macaroon file ownership.
The option services.postgresql.ensureUsers.*.ensurePermissions was renamed
to ensureClauses in nixpkgs unstable. Update both btcpayserver and nbxplorer
database user configurations.
Replace all pkgs-stable references with pkgs.stable (from overlay-stable in flake.nix):
- Remove pkgs-stable from function arguments
- Use pkgs.stable.nbxplorer (2.6.10 from nixos-26.05)
- Use pkgs.stable.btcpayserver (2.4.2 from nixos-26.05)
Restores the complete original flake.nix that includes:
- nixosModules.Sovran_SystemsOS (required by deployed machines)
- overlay-stable for accessing pkgs.stable packages
- nixosConfigurations.nixos for direct builds
- nixosConfigurations.sovran_systemsos-iso for ISO builds
- nixosTests and checks
1. flake.nix: Add pkgs-stable as specialArg for modules
2. btcpayserver.nix:
- Fix preStart script syntax error (closing brace on new line)
- Use pkgs-stable.btcpayserver (2.4.2) instead of pkgs.btcpayserver
- Use pkgs-stable.nbxplorer (2.6.10) instead of pkgs.nbxplorer
- Remove clightning references (Sovran is LND-only)
- Create packages/rtl/default.nix to build RTL from source
- Update modules/bitcoin/rtl.nix to use vendored package instead of pkgs.rtl
- Remove clightning/lightning-loop references (Sovran is LND-only)
- Simplify to LND-only configuration
The joinmarket-ob-watcher module was not imported from nix-bitcoin,
so referencing it in the onion-services defaults caused an
"attribute 'joinmarket-ob-watcher' missing" evaluation error.
- Previous lnd-only refactor removed the '++ optional clightning' line
but left 'extraGroups = [ cfg.nbxplorer.group ]' without trailing ';'
-> syntax error: unexpected '=', expecting ';' at home = ...
- Add ';' to fix build on both f13ff45 and 8b8c811
- lndconnect.nix: rewrite to lnd-only (320 lines removed) - remove
services.clightning.plugins.clnrest.lnconnect and
services.clightning-rest.lndconnect (Sovran only uses services.lnd.lndconnect for Zeus)
Fixes: 'services.clightning.plugins.clnrest.address does not exist' on f13ff45
and 'attribute enable missing' at lndconnect.nix:216
- btcpayserver.nix: remove liquidd (lbtc) dead branch - Sovran uses lbtc=false
* remove inherit (config.services) bitcoind liquidd -> just bitcoind
* lbtc chains/rpc handling is dead code, keep guards but no need for liquidd service
- mempool.nix: remove fulcrum branch - Sovran uses electrs only
- rtl.nix: remove lightning-loop block and clightning service block (lnd only)
- Delete modules/bitcoin/stubs.nix entirely - no dead service references left
* liquidd, fulcrum, lightning-loop, joinmarket now not referenced at all
* clightning already handled via guards, not stubs
- Result: modules/bitcoin/ is now 100% tailored to Sovran (bitcoind+knots, lnd, electrs, rtl, btcpayserver, mempool)
No more stubs whack-a-mole on any nixos-unstable
- Move modules/vendor/nix-bitcoin/* -> modules/bitcoin/* (clean, Sovran-owned)
* modules/bitcoin/default.nix imports the 6 tailored services
* modules/bitcoin/common.nix bundles secrets/onion/lib
* modules/bitcoin/stubs.nix kept minimal (no clightning)
* packages/lndinit/default.nix replaces pkgs/sovran-overlay.nix
(lnd.nix now uses pkgs.callPackage ../../packages/lndinit {})
- Remove pkgs/sovran-overlay.nix, pkgs/nbxplorer.nix, pkgs/README.md
* No global overlay - lndinit is a normal package in packages/
- Remove modules/vendor/ entirely
- Update flake.nix: drop overlay-sovran, import ./modules/bitcoin
instead of ./modules/vendor/nix-bitcoin/modules.nix
- No more random vendor/ or pkgs/ dirs - follows Sovran convention:
modules/ for NixOS modules, packages/ for packages
- f13ff45 HAS services.clightning but only as { plugins = ... }, no enable
-> 'option does not exist, did you mean plugins' (your error)
- Sovran never uses lightningBackend == clightning (uses lnd),
so just don't set services.clightning.enable at all
- Also make lnd port conflict check always true for clightning case
- Fixes both f13ff45 (has clightning/plugins) and 8b8c811 (removed)
- f13ff45 (staging-dev) HAS both services -> unconditional stubs duplicate
- Remove both from stubs.nix, keep only liquidd/fulcrum/etc.
- lndconnect.nix left as is (defines clightning-rest.lndconnect) - safe on f13ff45 where base exists
- On 8b8c811 where clightning is removed, Sovran doesn't use it anyway (lnd only), so guards in btcpayserver/lnd prevent use
nixpkgs 8b8c811 (2026-08-08) removed services.clightning.enable,
causing btcpayserver.nix:124 to throw 'option does not exist' on
nixos-rebuild (your error). Sovran uses lnd only, never clightning,
but evaluation still throws.
- Add modules/vendor/nix-bitcoin/stubs.nix to provide missing options
as false stubs: clightning, clightning-rest, liquidd, fulcrum,
lightning-loop/pool, joinmarket
- Guard btcpayserver/rtl/lnd/mempool clightning/liquidd references
with config.services ? X checks
- Trim enable-tor.nix onionServices for removed services
- Remove inputs.nix-bitcoin (fort-nix/nix-bitcoin/release) from flake.nix
- Vendor only 6 services actually used by Sovran: bitcoind, electrs,
lnd (+lndconnect), rtl, btcpayserver, mempool + supporting infra:
secrets, onion-services/addresses, operator, nodeinfo, security,
versioning
- All packages now from nixpkgs directly (pkgs.*) — no pinned pkgs
- Keep nix-bitcoin.* option namespace for compatibility
- backups.nix removed: Sovran uses rsnapshot to Second_Drive
(configuration.nix: hourly/daily to BTCEcoandBackup) — duplicity
remote backup not needed
- netns-isolation.nix replaced with stub (5 lines): original 365-line
bridge/iptables/ip-netns broke Caddy/AlbyHub/RTL a year ago and
is incompatible with nwc-wallets (requires enable=false). Stub
keeps option valid but warns if enabled.
- Add pkgs/sovran-overlay.nix for gaps only: lndinit + netns-exec stub
- Replace plain-text write of new_root_password in api_security_reset()
with scrypt-hashed storage via _hash_password(), matching how the free
password is already handled.
- Return new_root_password in the API response so the user sees it once
before it is irreversibly hashed on disk.
- Teach _resolve_credential() to detect scrypt hashes and display a
human-readable placeholder instead of raw hex in the Hub credentials UI.
- Harden root-password-setup systemd service: if the secrets file already
contains a hash, skip chpasswd so a manual restart never sets the hash
as the literal login password.
builtins.replaceStrings requires the 'from' and 'to' lists to have the
same length. The 'to' list had a single empty string while 'from' had
three entries (v, newline, CR), which made evaluating image.baseName
fail with: 'from' and 'to' arguments passed to builtins.replaceStrings
have different lengths. Add the two missing empty strings.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Replace repeated prose across 4 sections with a compact table + flow arrow:
- Top callout: 7-line paragraph → 2-line summary + link
- Development workflow: 2 bullets + 3-step list + warning → 3-row table + flow arrow
- Build from source: re-explained sync relationship → single labels on clone commands
- Contributing footer: repeated branch/host info → one sentence
No information lost; ~270 words removed.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The README and CONTRIBUTING are mirrored to both Gitea branches, so
GitHub-specific references now name the GitHub repository explicitly
instead of saying 'this repository'.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The GitHub repo mirrors Gitea's staging-dev branch; changes are tested
here and promoted to the stable branch on the self-hosted Gitea instance.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Keeps the modal-matching neutral pill styling; the badge now reads
"v1.0.5" as plain uniform text instead of the bare number.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The header badge now uses the exact visual treatment of
.creds-title-version-badge from modals.css: neutral translucent pill
(rgba(255,255,255,0.06) background, 0.08 white border, 12px radius,
--text-secondary text, 0.72rem/600/2px-10px padding), no hover
animation, and a bare version number like the modals show (e.g.
"2.8.4") instead of the green pill with a "v" prefix.
The badge markup flattens to a single text span; the .version-label
and .version-number rules are removed.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Mirrors the service modal pattern where the version badge sits to the
right of the title: .title-group switches from a centered column
(badge under the title, PR #377) to a centered row with the badge
vertically centered directly after "Sovran_SystemsOS Hub".
flex-wrap keeps a graceful fallback: on very narrow screens the badge
wraps below the title, centered — the previous stacked look.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Replace tagline with 'Bitcoin sovereignty. Sovereign computing. One system.'
- Open with both pillars as 'inseparable freedoms'
- Extend growth path beyond node/infrastructure to include private cloud and comms
- Remove standalone 'Privacy. Sovereignty. Bitcoin.' from intro (branding holds it at page close)
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Fix QR hint for zeus-connect-setup.service to use correct LND REST steps
- Expand 'How to Connect' instructions with title/intro/steps/note
- Add matching styled guide block in credentials modal
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The Manual Backup screen in the Hub always listed the Node / Server +
Desktop items (nix-bitcoin secrets, /var/lib system service data, and the
database/blockchain caveat), and the backup script mirrored /var/lib and
counted it in the free-space estimate even on the Desktop Only role.
Desktop Only systems run no server or Bitcoin services and have no
internal second data drive, so none of that applies.
Hub UI (support.js):
- 'What gets backed up' is role-aware: Desktop Only lists only the NixOS
configuration (/etc/nixos) and home directory (/home)
- Database/blockchain note hidden on Desktop Only
- Intro copy corrected: external USB copy is a second location on
Desktop Only (no internal second drive); third-location wording kept
for Node / Server + Desktop
Backup script (sovran-hub-backup.sh):
- Desktop Only runs 2 stages (1/2 /etc/nixos, 2/2 /home); secrets and
/var/lib stages no longer run on that role
- Free-space estimate skips /var/lib on Desktop Only
- BACKUP_MANIFEST.txt sources/exclusions/limitations/restore guidance
and blockchain note are role-aware
- Completion message role-aware ('second, external location' on
Desktop Only); header comments updated
Node and Server + Desktop behavior is unchanged. Added CHANGELOG entry.
Verified: bash -n / node --check, 6 role-detection cases, manifest
generation for both role groups (non-desktop output identical to
before), and simulated UI renders for all three roles.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The Caddyfile is generated at runtime by caddy-generate-config.service
from /var/lib/domains/*, but the generator only re-runs when caddy.service
starts fresh. Saving a domain while Caddy is already running therefore
never seats the new virtual host — no proxying and no ACME cert — and the
Hub's reachability check shows a misleading 'ports 80/443' router error
until the next reboot or rebuild.
api_domains_set and api_domains_set_email now restart the generator and
reload Caddy (ExecReload: caddy reload --force, no dropped connections)
right after saving. Entirely skipped when Caddy is inactive — e.g. Node
role before its first domain-based service is enabled — because the
rebuild that enables the service starts caddy.service for the first time,
runs the generator first (requiredBy), and seats the already-saved domain
on its own. Best-effort throughout: a domain save never fails because of
a Caddy reload issue.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Single source of truth (static/js/domain-prereqs.js) for the domain
prerequisite instructions so all three surfaces read identically:
- Server + Desktop first-boot onboarding wizard (step 3)
- Lightning Wallet Connections (NWC) enable modal (Node-only mode)
- BTCPay Server (web) enable modal (Node-only mode)
Both feature-enable flows share openDomainSetupModal(), which now drops
its role-branched intro and 'Option A/B' blocks in favor of the shared
renderers. All surfaces now consistently cover:
1. A domain from Njal.la (account, subdomain-vs-separate-domain,
Dynamic record with host-part-only Name field, auto-filled IP,
DDNS curl command)
2. Router access — forward ports 80 & 443 (TCP) to this computer's
internal IP, once, for HTTPS/SSL (with CGNAT note)
3. How to get Njal.la working, step by step
The reconfigure/troubleshooting modal gains the same router reminder,
since 'domain not reachable' is often the port forwarding rather than
DNS. domain-prereqs.js is loaded via asset_version cache busting and
ships automatically (installPhase copies the package wholesale).
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- CHANGELOG.md: replace v1.0.4 placeholder section with the actual changes
(Lightning Wallet Connections/NWC, Hub version badges, backup overhaul,
automated releases, security hardening, fixes)
- release-stable.sh: generate categorized Keep-a-Changelog release notes
from commits since the last tag (feat/fix/docs/security grouping),
let the user review/edit before publishing, and use the same notes for
CHANGELOG.md, the GitHub release, and the Gitea release
- Fix broken /api/ path in the v1.0.4 changelog release link
- Build Gitea API payload with jq/python so multi-line notes are JSON-safe
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- The Hub header badge could render 'vdev' because the runtime
/etc/nixos/VERSION lookup fell back to the literal string 'dev' when
the file was missing (e.g. dev/test environments, or before the
Nix-generated config carried a version at all).
- modules/core/sovran-hub.nix now reads the repo's VERSION file at
Nix eval time and bakes a real semantic version (sovran_version)
into the generated config.json and a VERSION file shipped with the
package, so the Hub always has a solid value to display.
- server.py's _get_sovran_version() now reads that baked-in
sovran_version first, and explicitly rejects a literal 'dev' value
from any of its file-based fallbacks so the badge never shows
'vdev' again.
- templates/index.html + header.css: wrapped the title and the
version badge in a '.title-group' column so the version badge sits
directly underneath 'Sovran_SystemsOS Hub', left-aligned with it.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Add release-stable.sh script with automatic tagging, CHANGELOG updates,
GitHub/Gitea release creation, and VERSION file management
- Update iso/common.nix to include version from VERSION file in ISO filename
- Add VERSION file (current: 1.0.3)
- Polish OS version badge in Sovran Hub header (top-right)
- Update README.md download link to versioned ISO
- Update CHANGELOG.md with existing tags (v1.0.0 – v1.0.3)
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Add clear guidance in the recommended hardware section explaining that
Server + Desktop mode requires router admin panel access with port
forwarding capability and ISP support for port forwarding. Framed as
empowering guidance rather than barriers to minimize onboarding friction.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Rework intro to co-headline Bitcoin self-custody and digital sovereignty
- Add table of contents
- Merge overlapping sections (middle-man pitch, day-one tools, differentiators)
into a single 'Why Sovran_SystemsOS?' section; describe each app once
- Collapse Desktop/Node/Server hardware lists into one comparison table
- Explain sovransystemsos.local once; other sections link to the Hub section
- Compress install guide (7 steps to 6) with collapsible per-OS verify blocks
- Link every upstream project at first mention
- Map features to module files in a table for developers
- Fix clone URL (naturallaw777/Sovran_SystemsOS)
- Remove repo-map rows for docs/wallet-connections.md and role-state.nix;
note install-time generated files imported by flake.nix
- Add packages/ to repo map; list Alby Hub, Mempool, Haven where relevant
- Fix H1/H2 heading hierarchy and deduplicate footer slogans
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Previously only bitcoind.service showed a version on the tile.
Now every service with a parsable Nix store path gets its version
displayed (electrs, btcpayserver, lnd, rtl, mempool, vaultwarden,
matrix-synapse, caddy, tor, etc.).
The existing _get_service_version() helper reads systemctl show
<unit> --property=ExecStart and extracts the version from the Nix
store path. Results are cached for 5 minutes per unit.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Naming: user-facing 'Wallet Connections' -> 'Lightning Wallet Connections'
across the Hub, feature registry, tile, and NixOS modules. Internal ids
(nwc-wallets, albyhub.service, /api/nwc/*) are unchanged.
UX: the service-detail modal put status, domain diagnostics, router ports,
the enable/disable toggle, restart, the liquidity guide and the whole wallet
manager in one cramped scrolling column. For this feature the modal is now
980px wide and split into two tabs:
- Wallets: wallet grid, create/share/verify flows, collapsible liquidity guide
- Service & Setup: description, status, domain checklist, ports, enable, restart
A status dot and domain chip sit in the tab bar so state is visible from both
tabs, and the modal opens on Setup when the service is off or the Lightning
Address domain is unconfigured. Wallet cards gain a balance chip, pending
badge, a prominent address row, and separated destructive actions.
Non-NWC services keep the original single-column layout and width.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Each NWC wallet connection gets its own shareable LNURL QR so the owner
can receive payments from anyone without creating invoices (the core
LNURL value proposition: a static QR that never expires).
Hub UI (service detail modal):
- New 'Share QR' button on every wallet connection card
- Dedicated share view: large QR, Lightning Address + copy, raw bech32
LNURL + copy
- Download PNG (websites/social), download SVG (vector, print-sharp at
any size), and a Print button that opens a print-ready payment card
which auto-invokes the browser print dialog
Backend (FastAPI, session-authenticated):
- GET /api/nwc/wallets/{id}/lnurl -> alias, lightning_address, LNURL
- GET /api/nwc/wallets/{id}/lnurl-qr.png (?download=1, ?scale=)
- GET /api/nwc/wallets/{id}/lnurl-qr.svg (?download=1)
- GET /api/nwc/wallets/{id}/lnurl-qr/print (print-ready HTML card)
- Stdlib bech32 encoder (BIP-173, verified against the official LUD-01
golden vector); LNURL is uppercased for QR alphanumeric-mode density
per LUD-01 guidance; QR rendered with the existing qrencode package
- Resolves wallets by app id or pubkey; 404 unknown wallet, 503 when no
Lightning domain is configured; all routes return 401 unauthenticated
No new system dependencies.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The Element Call tile still appended a synthetic 'Step 4: Router Setup
Needed' to the domain diagnostic checklist, and every port table carried a
'Sovran_SystemsOS Status' column with Ready / Not ready yet verdicts.
Both were misleading: port forwarding happens on the router, which this
computer cannot inspect. A local ss/firewall probe can neither prove nor
disprove that forwarding works — and the LiveKit TURN relay range binds on
demand, so it reported 'Not ready yet' even on a perfectly working system.
- server.py: add ROUTER_FORWARD_ONLY_UNITS ({livekit.service}); skip the
local probe for those units, drop the step-4 append, replace extra_ports
with router_ports (no status field), and exclude router-only ports from
both tile health and /api/ports/health so they can't raise false alarms.
- helpers.js: new shared renderPortForwardGuideHtml() — one intro naming the
internal IP, explicit instructions (same internal/external port, match the
protocol, use port-range fields for 30000-40000), a colour-coded
TCP / UDP / TCP+UDP badge per row, and a closing note that the only real
test is loading the service from a phone on mobile data.
- features.js: enable-time modal uses the shared guide and now always lists
every port to forward (the old local pre-filter hid ports the user still
had to open).
- service-detail.js: tile port section uses the same guide; the SSH/non-domain
branch keeps a small 'not open on this computer yet' hint, which is a real
local fact, separate from router forwarding.
- onboarding.js: step 3 router note reworded to match (same number for
internal/external, notes that Element Call adds UDP ports).
- domain-setup.css: styles for the protocol badges and instruction list.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Onboarding: remove the redundant/error-prone 'Router Setup' step (5 steps -> 4).
A compact 80/443 (+22 SSH) note now lives inside Domain Configuration,
and the Element Call ports are only shown at the moment they matter:
when enabling the feature, and afterwards on the service tile.
- Onboarding step 3: fix domain prefill bug (API returns {domains: {...}}),
make /api/network fetch best-effort so it can never block the step.
- Enable-time port modal: streamline copy (one intro + table + pointer to
the tile's live status view).
- Element Call tile detail: replace 5 repetitive prose blocks with 2 compact
notes around the live-status port table.
- Njal.la DDNS: run njalla.sh immediately when a DDNS-backed feature is
enabled (previously only ran on domain save or the 15-min cron tick).
- Harden njalla.sh handling: create the base script (shebang + IP lookup)
if missing before appending curl lines; invoke via bash explicitly.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
ReadOnlyPaths for nwc-lnurl.service now lists only the specific file
/var/lib/albyhub/unlock-password (least-privilege) instead of the
whole /var/lib/albyhub directory.
Amount-duplicate error message changed to "A single amount parameter is
required" (clearer for 2+ values than "Exactly one").
Test assertion updated to match new message text.
Blocker 1: run nwc-lnurl.service as albyhub user/group so it can read
/var/lib/albyhub/unlock-password (mode 0600, dir mode 0700).
Remove the now-unused nwc-lnurl user, group, and /var/lib/nwc-lnurl
state directory. ReadOnlyPaths updated to allow the whole albyhub dir.
Blocker 2: require exactly one amount query parameter in the LNURL
callback HTTP handler. Duplicate values now return a 400 protocol
error ("Exactly one amount parameter is required") before the helper is
called. The missing-amount and non-integer paths are unchanged.
Blocker 3: partial-funding failure message now reads "was created
successfully" instead of "already exists" to avoid confusion with a
duplicate-name error, while retaining the warning not to recreate.
Tests added:
- LnurlHandlerAmountTests — HTTP-handler level tests for duplicate (2×,
3×), single-valid, and missing amount parameters.
- test_create_partial_failure_message_says_created_successfully —
asserts exact wording of the partial-funding message.
- test_nwc_lnurl_service_runs_as_albyhub — asserts the Nix service block
sets User/Group to albyhub.
- test_nwc_module_no_separate_nwc_lnurl_user — asserts no standalone
nwc-lnurl user/group is declared.
All 229 Python tests pass (1 skipped). JS syntax clean. No secrets.
- nwc_hub_manager.py: Fix BOLT11 regex to accept only valid prefixes (bc/tb/bcrt/tbs); add path to timeout error message; add early-exit to _get_app_pending_txs pagination
- nwc_lnurl_service.py: Allow NWC_LNURL_PORT env var override for port
- nwc-wallets.nix: Remove || true from gofmt so build fails on syntax errors
- test_wallet_connections.py: Extract _call_body() helper; use c.args[] access
- sovran-hub.nix: Add nwc-lnurl launcher binary to sovran-hub-web derivation (same pattern as nwc-wallet); add options.services.sovranHub.webPackage to expose derivation to other modules
- nwc-wallets.nix: Use config.services.sovranHub.webPackage for LNURL ExecStart (fixes sys.path for sovran_systemsos_web imports); replace no-op placeholder patches with real unified diff format showing intended Go changes (Private: true, AppId *uint)
- nwc_hub_manager.py: Fix Authorization header to use real ****** (was hardcoded to literal asterisks due to display redaction)
- server.py: Use exc.args[0] instead of str(exc) in NWC error handlers to prevent CodeQL stack-trace taint flow to HTTP responses; update albyhub.service description key
- Add nwc_hub_manager.py: AlbyHubManager with real Alby Hub API (setup, auth, CRUD, drain, delete, invoice)
- Add nwc_lnurl_service.py: dedicated loopback LNURL service on port 8181
- server.py: remove JSON scaffolding (state.json, fake invoice generator, fake NWC URI, LNURL routes); replace with real manager calls; update service maps to albyhub.service; remove LNURL auth-exempt paths
- nwc_wallet_cli.py: rewrite to use real AlbyHubManager instead of JSON state
- modules/nwc-wallets.nix: replace with albyhub user/service, nwc-lnurl service, LND macaroon, unlock-password generation
- modules/core/caddy.nix: proxy LNURL routes to port 8181 (dedicated service) instead of 8937 (Hub)
- modules/core/sovran-hub.nix: service tile points to albyhub.service
- docs/wallet-connections.md: document real architecture, Alby Hub pin/patches, backup sensitivity
- test_wallet_connections.py: replace scaffolding tests with 54 real manager tests using mocked Alby Hub
- Replace environment.etc raw script with pkgs.writeShellApplication
- Declare runtimeInputs: pkgs.coreutils, pkgs.gawk, pkgs.gnugrep
- Use awk -v for safe marker variable passing (no shell interpolation)
- Point systemd ExecStart and activation script at lib.getExe hostsUpdateScript
- Keep /etc/sovran-hosts-update.sh as a source symlink for operator discoverability
- Remove environment.systemPackages reliance
- Emit warning (not silently swallow) on activation failure
- Add structural regression tests (19 new tests, all passing)
- Use set -eu (not set -euf) in sovran-hosts-update.sh
- Add sync note for domain validation regex between shell and Python
- Rename `seen` to `unique_addresses` in _resolve_all_addresses
- Improve loopback override UI message with external DNS check guidance
- Add docstring note about first-address display in _resolve_all_addresses"
- Add modules/core/local-domain-loopback.nix: systemd service and
activation script that write configured service domains to a
Sovran-managed block in /etc/hosts (127.0.0.1 / ::1) so requests
originating on this computer reach Caddy without NAT loopback.
- Import local-domain-loopback.nix in modules/modules.nix.
- server.py: add _validate_domain_value, _is_loopback_address,
_resolve_all_addresses, _trigger_hosts_update helpers.
- server.py: update _check_domain_reachable to use --resolve so
reachability is checked locally via Caddy, not via NAT loopback.
- server.py: update _evaluate_domain_checklist, api_services inline DNS
check, and api_domains_check to recognise loopback resolution as an
intentional local override rather than a DNS mismatch.
- server.py: call _trigger_hosts_update from api_domains_set after
saving a service domain so the /etc/hosts entry is applied immediately.
- Add app/tests/test_loopback_diagnostics.py with 47 tests covering
domain validation, loopback detection, diagnostic checklist logic,
composite health, and api_domains_check."
GRD 50.x invokes pkexec internally for every grdctl --system call, even
when the caller is root. An isolated systemd script PATH does not include
/run/wrappers/bin automatically, causing exit 70 at boot.
Changes:
- Prepend /run/wrappers/bin to PATH at script start so every subsequent
grdctl --system resolves the NixOS setuid pkexec wrapper.
- Add an explicit preflight check (test -x /run/wrappers/bin/pkexec) with
a clear error message before the first grdctl_system call.
- pkgs.polkit remains absent from the service path.
- Update test_setup_runs_grdctl_directly_as_root to reflect that pkexec
now appears in the preflight check (not as a direct invocation).
- Add test_run_wrappers_bin_prepended_to_path and test_pkexec_preflight_check.
Root cause: gnome-remote-desktop-setup.service ran as root but dropped
privileges via `runuser -u gnome-remote-desktop`. Non-root grdctl --system
attempts authorisation through pkexec. The Nix-store pkexec binary is not
setuid, so every system-mode credential call silently failed while the script
still printed "configured successfully". GNOME Remote Desktop then started
without applied credentials, causing Remmina to loop at the login dialog.
Fix:
- Remove `runuser -u gnome-remote-desktop --` from grdctl_system helper;
the root-run oneshot service can call grdctl --system directly.
- Remove pkgs.polkit and pkgs.util-linux from the setup service path as
neither polkit nor runuser is needed any more.
- Update tests: rename test_setup_runs_grdctl_as_gnome_remote_desktop_user
to test_setup_runs_grdctl_directly_as_root and assert that runuser,
pkexec, and sudo are absent; add
test_privilege_escalation_packages_absent_from_setup_path.
All 21 app/tests pass.
1. Add nix-bitcoin.generateSecrets = lib.mkDefault true global compat
default in role-logic.nix so Desktop Only systems can evaluate while
nix-bitcoin is still globally imported by the flake.
2. Harden Desktop Only role: change all server/node service and feature
disables from lib.mkDefault false to lib.mkForce false so they cannot
be overridden by custom.nix or option defaults.
- sovran_systemsOS.services: synapse, bitcoin, vaultwarden, wordpress, nextcloud
- sovran_systemsOS.features: haven, mempool, element-calling, bitcoin-core
- sovran_systemsOS.web.btcpayserver
3. Make Caddy conditional in caddy.nix:
enable = needsHttpsPorts || extraVhosts != ""
so Caddy does not run on Desktop Only installs with no web services.
LiveKit was exiting cleanly with "TURN domain required" because turn.enabled
was set in the build-time config but turn.domain was never provided to the
process (the old livekit-runtime-config.service wrote a YAML that nothing
read). A clean exit (status 0) meant Restart=on-failure never restarted it,
so the Hub reported the service as Inactive.
This replaces the dead runtime-config oneshot with livekit-turn-setup.service,
which at runtime:
- reads the matrix domain from /var/lib/domains/matrix (no hardcoding)
- copies Caddy's already-issued matrix cert/key into /var/lib/livekit
- generates a complete LiveKit config (incl. turn.domain + TLS cert/key)
at /run/livekit/livekit.yaml
The livekit.service ExecStart is overridden to load that runtime config
(mirroring the existing Caddy ExecStart override pattern in
modules/core/caddy.nix), since turn.domain is only known at runtime. The cert
is delivered via LoadCredential so it is readable under DynamicUser=true
without weakening the sandbox.
Also aligns the RTC media port range (rtc.port_range_start/end = 30000-40000)
so it matches the forwarded ports, and drops the now-redundant manual
30000-40000 firewall ranges (covered by services.livekit settings/openFirewall).
The previous change incorrectly split the 30000-40000 TURN relay range into two
rows and told users to create two separate forwarding rules. On most routers
this range is a single port-forwarding rule with a protocol selector set to
"Both" (or TCP/UDP). Revert to a single row (protocol "TCP & UDP"), update the
note to say it's one rule with both protocols enabled, and restore the totals to
3 required + 5 optional = 8.
The onboarding Step 4 port table listed the 30000-40000 TURN relay range as a
single "TCP/UDP" row, which is ambiguous on most routers where TCP and UDP
forwards are separate entries. Split it into two explicit rows (TCP and UDP),
add a clarifying note, and update the totals so users create both forwarding
rules.
- modules/core/roles.nix: re-declare bip110 as a nullOr bool no-op
option so existing custom.nix files with `lib.mkForce true` continue
to evaluate; add config.warnings block that fires only when the stale
flag is explicitly set
- server.py: add DEPRECATED_FEATURE_IDS constant; skip deprecated ids
in _read_hub_overrides and _write_hub_overrides; add
_migrate_strip_deprecated_features helper that rewrites the Hub
Managed section without deprecated lines on startup; add
@app.on_event("startup") handler _startup_migrate_deprecated_features
- RTL: rename 'Tor Access' → 'Tor Address — Access from anywhere via Tor Browser'
rename 'Local Network' → 'Local Network — Access on your home network only'
add 'How to Access' explanation credential
- Mempool: same label improvements + 'How to Access' credential
- Bitcoin Knots, Bitcoin Core, Electrs: update 'Tor Address' label to include
'Access from anywhere via Tor Browser' for consistency
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/63c3edb0-9fbf-4dd8-91e5-404ff6e4097d
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
The BTCPayServer hub entry was using `cfg.services.bitcoin` as its
`enabled` flag, which is `true` in node mode. This caused the Hub UI
to show BTCPayServer as enabled even though the underlying NixOS service
is correctly gated on `cfg.web.btcpayserver` (which defaults to `false`
for the node role via role-logic.nix).
Change the enabled field to `cfg.web.btcpayserver` so the Hub UI
accurately reflects the service state and BTCPayServer is disabled by
default on a fresh node-only install.
Rewrite the ss output parser to:
- Skip header lines (State/Netid) explicitly
- Only process LISTEN/UNCONN state lines
- Always read parts[3] for local address (the ss column layout is fixed)
- Defensively skip wildcard (*) port values
The previous fix (PR #235) tried both parts[3] and parts[4], but reading
parts[4] (peer address column) was unnecessary. The ss LISTEN output always
places the local address at index 3 when split by whitespace, for all address
formats: 0.0.0.0:PORT, *:PORT, [::]:PORT, 127.0.0.1:PORT.
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f7ab1d7c-d624-4f1a-9e62-5a9ce4fd4446
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
Part A (modules/core/sovran-hub.nix):
- Add restartIfChanged=false and stopIfChanged=false to sovran-hub-update service
- Add restartIfChanged=false and stopIfChanged=false to sovran-hub-rebuild service
These prevent nixos-rebuild switch from terminating an in-flight update mid-execution.
Part B (app/sovran_systemsos_web/server.py):
- Replace _recover_stale_status() with improved version
- Use MainPID + os.kill() to guard against transient is-active lies during daemon-reload
- Use ExecMainStatus (actual exit code) instead of Result (may be stale from prior run)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/63bf2cd5-9c02-4542-8926-44aa9ed63bf0
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- _recover_stale_status(): returns True when corrected; changes message from
internal '[Hub] Stale RUNNING...' to user-friendly text
- _startup_recover_stale_status(): sets _update_recovery_happened flag when
update recovery happens at startup
- api_updates_status(): uses offset=0 when recovery happened so frontend
receives the full log, not just a stale delta
- pollUpdateStatus(): when reconnecting after server-down with update done,
resets offset to 0, re-fetches full log, shows '[Server restarted — update
completed successfully.]' instead of '[Server reconnected]'
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/90b535d1-bc3b-4147-9d62-3c7a93b1c8e4
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Add security-reset-overlay HTML element to index.html that shows immediately
when the user confirms "Erase & Reset", before the synchronous API call runs
- Add .security-reset-overlay CSS to security.css (reuses reboot-card styles,
adds fade-in animation, z-index 1000 to sit above all other content)
- Fix reboot overlay class bug: classList.add("open") → classList.add("visible")
so the overlay actually renders per the .reboot-overlay.visible CSS rule
- Show overlay step text "Erasing data and resetting credentials…" during wipe,
update to "Reset complete. Rebooting now…" when API returns
- Call waitForServerReboot() (globally defined in update.js) after reset so the
page auto-reloads when the system comes back online
- Hide the security-reset-overlay and re-enable the button on error
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/185d0b41-d54d-4ea2-93d6-bfb7c15b8aed
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Replace all "Sovran SystemsOS" (with space) with "Sovran_SystemsOS"
- Line 241: landing page title
- Line 262: internet notice text
- Line 328: welcome/role-selection hero title (was "Sovran Systems")
- Line 910: install progress title
- Replace push_complete method: remove credentials screen (username,
password, first-boot note, reboot button) and replace with a simple
status page that says "Rebooting…" then auto-reboots after 3 seconds
via GLib.timeout_add_seconds
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2a39f2d5-6aef-42cf-a94a-e1db5c6a601a
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- server.py: add _is_free_password_default() helper that reads /etc/shadow
and hashes known defaults ("free", "gosovransystems") via crypt module;
update api_password_is_default to use it instead of reading the secrets file
- factory-seal.nix: replace file-based free-password check with shadow-based
cryptographic check using python3 + crypt module; add pkgs.python3 to path;
pass values via env vars to avoid shell expansion of hash $ characters
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/31e6fc93-8b4b-47af-9c47-568da0905301
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
Add two early-exit checks in sovran-legacy-security-check before the
legacy fallthrough block:
1. Exit if /var/lib/sovran/onboarding-complete exists (Hub onboarding done)
2. Exit if /var/lib/secrets/free-password exists and is not "free" (password changed)
This prevents the boot-time service from overwriting the security-status
file that /api/change-password clears after a successful password change.
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c18311e4-609d-4edf-a2a1-a018baede373
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Add app/sovran_systemsos_web/static/sovran-hub-icon.svg: a new square
256x256 app icon for the GNOME dock/dash. Uses the Sovran brand dark
green (#0d3320) rounded-rectangle background, concentric arc rings in
brand greens (#1C9954, #077233), and a white bold "S" letterform
centered — visible at small sizes on both light and dark panels.
- Update modules/core/sovran-hub.nix line 266 to copy the new icon file
to the hicolor icon path instead of reusing logo-light.svg.
- logo-light.svg is left untouched; it continues to serve the Hub web UI.
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e3f466ae-eee1-4ba8-b93c-00fe04c7054d
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Add dedicated `sovran-support` restricted user (non-root) for SSH sessions
- Apply POSIX ACLs via setfacl to block support user from wallet directories
(LND, Sparrow, Bisq, nix-bitcoin-secrets) by default
- Graceful fallback to root authorized_keys if user creation fails (with UI warning)
- Add time-limited wallet unlock consent: POST /api/support/wallet-unlock
- Add wallet re-lock: POST /api/support/wallet-lock
- Add audit log: GET /api/support/audit-log (append-only, all events logged)
- Expand /api/support/status with wallet_protected, wallet_unlocked,
wallet_unlocked_until, protected_paths, acl_applied fields
- Update frontend to show wallet protection status box with protected path list
- Show wallet unlock/re-lock controls with duration selector (30min/1h/2h)
- Show audit log viewer in support modal (toggleable)
- Add wallet unlock expiry auto-refresh timer in JS
- Add CSS styles for wallet protection box, unlock/lock buttons, audit log
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/70330ce3-1ed7-46b1-ac66-4cdc50de6017
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Replace complex per-service/health-check UI with a clear, hardcoded
table of required ports (80, 443, 22, 8448) and an optional Element
Calling section (7881 TCP, 7882-7894 UDP, 5349 TCP, 3478 UDP,
30000-40000 TCP/UDP).
- Add totals line: 4 openings without Element Calling, 9 with.
- Drop /api/ports/health fetch and all dynamic breakdowns (affected
services loop, closed-port warnings, "View All Required Ports" table).
- Keep internal-IP display box, SSL-cert warning, and "How to set up
port forwarding" collapsible section.
- Add prominent note that each port only needs to be forwarded once.
- Update Step 3 header description in onboarding.html to match.
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/523e0770-f144-4f47-932b-c0d40782a35b
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Add <aside class="sidebar"> with #sidebar-support and #sidebar-features to index.html
- Restyle .main-content as flex layout (sidebar left, tiles right)
- Body is now display:flex column with overflow:hidden for independent scroll panels
- Sidebar (270px fixed) with overflow-y:auto scrolls independently
- Tiles area (flex:1) scrolls independently
- New sidebar support button (.sidebar-support-btn) replaces support tile in main grid
- Feature Manager now renders into #sidebar-features instead of $tilesArea
- Compact sidebar overrides for .feature-card padding/font-size
- Remove 'support' and 'feature-manager' from CATEGORY_ORDER
- Responsive: sidebar becomes full-width above tiles at <=768px
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4304350a-bc4f-4698-82b5-8ee28f0ad960
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Installer VM compatibility: legacy BIOS VM boots now install with GRUB, UEFI VM installs avoid depending on NVRAM boot-entry writes, VM users get clearer disk/resource guidance, and the internet check falls back to HTTPS when ICMP is blocked.
- **Manual Backup now matches the system role**: on the Desktop Only role the Hub's
"What gets backed up" list no longer shows Node / Server + Desktop items
(nix-bitcoin secrets, `/var/lib` system service data, and the database/blockchain
caveats), and the backup script skips those stages entirely — Desktop Only backups
now mirror only the NixOS configuration (`/etc/nixos`) and home directory (`/home`).
The free-space estimate, stage numbering, backup manifest, and completion message
are all role-aware; Node and Server + Desktop backups are unchanged.
in keeping with the self-sovereignty ethos of Sovran_SystemsOS.
The workflow is:
1. Most development and project collaboration — including issues, pull
requests, and reviews — happens through GitHub, with completed work
integrated into `main`.
2. Work may also originate locally or on Gitea `staging-dev`. Accepted changes
are synchronized between GitHub `main` and Gitea `staging-dev`.
3. When changes are complete and tested, they are promoted to Gitea `stable`,
which is the code used by released Sovran_SystemsOS builds.
Please note: GitHub `main` and Gitea `staging-dev` may contain new features and
code not yet in `stable`, and that code may not be fully tested.
Moreover, Sovran_SystemsOS has been improved with the help of AI. We have used Copilot and Arean.Ai to work through significant coding challenges and troubleshooting hurdles. We will continue to use AI to help keep Sovran_SystemsOS stable and maintained.
## How Can I Contribute?
### 🐛 Reporting Bugs
- Open an [Issue](https://github.com/naturallaw777/Sovran_SystemsOS/issues)
- Use a clear and descriptive title
- Describe the steps to reproduce the bug
- Include your OS version, hardware specs, and any relevant logs
### 💡 Suggesting Features
- Open an [Issue](https://github.com/naturallaw777/Sovran_SystemsOS/issues) with the tag `enhancement`
- Explain the feature and why it would be useful
- Be as detailed as possible
### 🔧 Submitting Code Changes
#### 1. Fork the Repository
Click the "Fork" button at the top right of the GitHub repository.
This document outlines the security policy, vulnerability disclosure process, threat model, and security best practices for **Sovran_SystemsOS**.
---
## 1. Supported Versions
We actively monitor and patch security vulnerabilities in Sovran_SystemsOS. Because the operating system is built on [NixOS](https://nixos.org), stable releases receive security updates backported from upstream Nixpkgs as well as our custom software updates.
| Version | Supported | Notes |
|:---|:---:|:---|
| **1.0.x** (Stable) | Yes | Active stable release line. Patches are backported regularly. |
| **staging-dev** | Yes | Development line; updated frequently but may contain untested code. |
| **Legacy (< 1.0.0)** | No | Please upgrade to the latest stable release to ensure you have active security updates. |
---
## 2. Reporting a Vulnerability
We take the security of self-custody systems, private clouds, and communications servers extremely seriously. If you discover a vulnerability, **please do not open a public issue, forum thread, or submit a public pull request.**
Instead, report vulnerabilities privately through one of the following secure channels:
1.**Email:** Send an encrypted or plain-text email to [support@sovransystems.com](mailto:support@sovransystems.com).
- If sending sensitive information, please contact us first to establish a secure PGP-encrypted communication channel.
2.**GitHub Private Vulnerability Reporting:** If you are viewing this on GitHub, you can report vulnerabilities privately via the **Security** tab of this repository under **Vulnerability reporting**.
### What to Include in a Report:
To help us triage and resolve the issue quickly, please include:
- A clear description of the vulnerability and its potential impact.
- Step-by-step instructions to reproduce the issue (including any scripts or configuration snippets).
- The version of Sovran_SystemsOS you tested (found in the Hub header or `/etc/nixos/VERSION`).
- Any potential remediation steps or suggestions you may have.
### Our Response and Disclosure Process:
1.**Acknowledgment:** We will acknowledge receipt of your report within **24–48 hours** and provide a tracking reference.
2.**Triage:** We will investigate and verify the vulnerability privately. We may reach out to you for clarifying details.
3.**Remediation:** If verified, we will develop a patch on a private security branch.
4.**Release:** We will coordinate a release date with you and publish the patch to Gitea `stable` and GitHub `main`.
5.**Advisory:** A public security advisory will be published, giving full credit to you for the discovery (unless you request anonymity).
We ask that you practice **coordinated vulnerability disclosure**, giving us reasonable time to patch the vulnerability before disclosing it publicly to protect other operators' funds and data.
---
## 3. The Sovran_SystemsOS Security Architecture
Understanding our underlying threat model and architectural decisions will help you evaluate the system's security.
### A. Local-First Threat Model
- **No Cloud Dependency:** Your data, keys, and credentials live entirely on your physical hardware.
- **Local network Hub Access:** The Sovran Hub is served directly on your local network (`sovransystemsos.local`). It is protected by authentication and **never** automatically exposed to the public internet.
- **Localhost Auto-Login:** Auto-login to the Hub is restricted strictly to local connections from `127.0.0.1` or `::1` (e.g., when accessing it directly from the local GNOME desktop). LAN and WAN clients must authenticate via password.
### B. Declarative & Reproducible NixOS Foundation
- **No Supply Chain Drift:** Dependencies, packages, and system components are strictly pinned using Nix flakes (`flake.lock`).
- **Reproducible Integrity Checks:** Operators can verify system integrity at any time via the Hub (`/api/security/verify-integrity`), which runs `nix store verify --all` to check binary consistency and rebuilds the system state from local files to guarantee it matches the expected configuration.
- **Default-Hardened Base:** Firewalls are enabled by default (`networking.firewall.enable = true`), public SSH and remote desktop are disabled by default, and legacy kernel modules like `rxrpc` are blacklisted to reduce the kernel's attack surface.
### C. Restricted Tech Support ("Zero-Trust Support")
To prevent support staff or malicious actors from accessing your private wallets, the operating system employs a unique **"Zero-Trust Support"** architecture:
- **Restricted System Account:** Support sessions use a restricted, non-root account (`sovran-support`) with heavily scoped shell access and limited `sudo` privileges (restricted strictly to viewing logs, editing configuration, and rebuilding or restarting services).
- **POSIX ACL Wallet Lockout:** Enabling support immediately triggers POSIX ACLs (`setfacl -m u:sovran-support:---`) to block the support user from reading, writing, or traversing sensitive directories (such as `/etc/nix-bitcoin-secrets`, `/var/lib/bitcoind`, `/var/lib/lnd`, and `/home`).
- **User-Controlled Timed Unlock:** Support staff can *only* access wallet files if the operator explicitly grants a time-limited unlock from the Hub interface. This unlock automatically expires, re-locking the directories.
- **Tamper-Evident Auditing:** All support actions (enabling, disabling, wallet unlocking, or locking) are logged in a tamper-evident audit trail (`/var/log/sovran-support-audit.log`) readable directly by the operator.
### D. Native Cryptographic Isolation
- **Tor Network Integration:** Upstream Bitcoin services (Knots/Core, Electrs, LND) utilize native Tor integration, preventing your home IP address from leaking to the public network or third-party node providers.
- **Isolated Lightning Wallets:** Nostr Wallet Connect (NWC) uses isolated, sandboxed Lightning wallets with specific spending limits and access presets (e.g., receive-only), keeping your main node funds completely isolated from individual apps.
---
## 4. Security Best Practices for Operators (Users)
No software can provide absolute security. You are the ultimate sovereign of your machine. Follow these best practices to keep your system and assets secure:
### 🔑 1. Split Wallet Entropy
When setting up a Bitcoin wallet, **generate your two entropy sources on separate, unconnected hardware**:
- **Seed Phrase:** Generated by Sparrow Wallet on your Sovran_SystemsOS machine.
- **BIP-39 Passphrase (128-bit):** Generated on a separate device (e.g., a GrapheneOS phone using a trusted password generator or offline Diceware dice).
- *Why?* If either device is compromised, your wallet remains secure because an attacker needs **both** independent sources of randomness to recover it.
### 💾 2. Implement a Robust Backup Strategy
- **Off-Machine Backups:** Never store your only wallet backups, seed phrases, or descriptor backups on the same computer as your running wallet.
- **Descriptor & Channel Backups:** If using Lightning (LND), regularly back up your Static Channel Backups (SCB) and store them off-site. A node hardware failure can permanently lock up channel funds without a fresh SCB.
- **Ext4 Backups:** If using the Hub's Manual Backup utility, ensure you are backing up to a dedicated **ext4-formatted** external drive. This preserves Linux-native security metadata, permissions, and POSIX ACLs.
### 🛡️ 3. Maintain Network and Access Hygiene
- **Public SSH Off:** Keep SSH remote access disabled in the Hub unless you actively need it. If enabled, Fail2Ban is active, but you should still enforce a strong system password.
- **Strong Diceware Passwords:** Ensure the system `free` and `root` users have strong passphrases. Avoid re-using passwords across services.
- **Guest Networks:** Do not access the Sovran Hub over public or guest Wi-Fi networks. These networks often lack isolation, exposing local network traffic to other devices.
- **Disable RDP When Done:** Turn off Remote Desktop (RDP) once your administrative session is finished.
### 🧑💻 4. Hardening Support Sessions
If you require technical support from Sovran Systems:
- **Enable SSH and Support on-demand:** Only enable SSH Remote Access and Support sessions when actively working with a technician, and **disable** them immediately when the session is over.
- **Confirm Removal:** The Hub's support page includes a "Verify Removal" feature. Always use it to ensure public keys have been fully stripped from the system.
- **Restrict Wallet Access:** Only unlock your wallet directories if absolutely necessary for the support session, and choose the shortest possible duration. Re-lock manually as soon as the troubleshooting steps are complete.
- **Review Audit Logs:** Periodically inspect the support audit log via the Hub interface to review exactly when support sessions were toggled or unlocked.
---
Thank you for helping us maintain a highly secure, private, and sovereign computing operating system!
[[ -n "$fstype"]]|| fail "Could not determine filesystem type for '$target'."
if[["$fstype" !="ext4"]];then
fail "Target '$target' must be formatted as ext4 (detected filesystem: $fstype). Manual Backup requires an ext4-formatted external drive for Linux metadata preservation. exFAT, FAT32, and NTFS are not supported."
html+="<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
+"— privacy-friendly, no personal details required, accepts Bitcoin. "
+dpEsc(serviceName)+" gets its own hostname: a subdomain (e.g. <code>"+hostExample+".yourdomain.com</code>) "
+"or a separate domain — your choice. Subdomains are free, and one domain can have many.</li>";
html+="<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
+"to this computer once. All domain-based services share these two ports; "
+"they're required for HTTPS and SSL certificates.</li>";
html+="</ol>";
}else{
html+="<p><strong>Each service below needs two things set up:</strong></p>";
html+="<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
+"— privacy-friendly, no personal details required, accepts Bitcoin. "
+"Each service gets its own hostname: its own subdomain (e.g. <code>"+hostExample+".yourdomain.com</code>) "
+"or a separate domain — your choice. Subdomains are free, and one domain can have many, "
+"so a single domain can serve every service.</li>";
html+="<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
+"to this computer once. All domain-based services share these two ports; "
+"they're required for HTTPS and SSL certificates.</li>";
html+="</ol>";
}
returnhtml;
}
/* ── "How to set it up at Njal.la" steps ───────────────────────────
opts.hostExample — host part used in the examples (default "call").
opts.pasteHint — where the curl command goes:
"below" (single service) or "next to its service below"
html+="<li>Create an account at <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">https://njal.la</a> and buy a domain.</li>";
html+="<li>Add a <strong>Dynamic</strong> record for the hostname:"
'<li>Add a Dynamic record as in Option A. If this domain is dedicated to the service, leave the Name field blank or use <code>@</code>.</li>'+
'<li>Copy the curl command Njal.la gives you.</li>'+
'</ol>'+
'</details>'+
'<p style="margin-top:10px;">Below, enter the full domain for this service — a subdomain (e.g. call.yourdomain.com) or a separate domain (e.g. call.com) — and paste its curl command.</p>'+
'<p style="margin-top:10px;">Below, enter the full domain for this service — a subdomain (e.g. '+domainLabelExample+') or a separate domain — and paste its curl command.</p>'+
'<li>If the IP is wrong or the record is missing, update it</li>'+
'<li>If you changed the DDNS curl command, paste the updated one below</li>'+
'<li>Confirm ports <strong>80</strong> and <strong>443</strong> (TCP) are still forwarded on your router to this computer — see the reminder below:</li>'+
?'<p class="port-req-intro">Forward each port below <strong>to this machine\'s internal IP: <code class="port-req-internal-ip">'+escHtml(internalIp)+'</code></strong></p>'
:"<p class=\"port-req-intro\">Forward each port below to this machine's internal LAN IP in your router's port forwarding settings.</p>";
?'For <strong>'+escHtml(opts.serviceName)+'</strong> to be reachable from outside your home network, open'
:'Open';
return'<p class="'+introClass+'">'+
forWhat+' the ports below in your router’s <strong>port forwarding</strong> settings '+
'and point them at '+ipHtml+'.'+
'</p>'+
'<ul class="port-req-steps">'+
'<li>Set the <strong>internal (private) port</strong> and the <strong>external (public) port</strong> to the <strong>same number</strong>.</li>'+
'<li>Match the <strong>protocol</strong> exactly — a rule set to TCP will not pass UDP traffic. Where the table says <strong>TCP + UDP</strong>, create both rules (or pick “Both”/“TCP/UDP” if your router offers it).</li>'+
'<li>For a range such as <strong>30000-40000</strong>, use your router’s port-range fields — start 30000, end 40000 — rather than one rule per port.</li>'+
// ── Service detail modal ──────────────────────────────────────────
function_getZeusConnectGuideHtml(){
return'<div class="nwc-connect-guide">'+
'<div class="nwc-connect-guide-title">📱 Connect to Zeus</div>'+
'<p class="nwc-connect-guide-intro">This connection URL is an <strong>LND REST</strong> connection — the direct way to use Zeus with your node for full admin access. It connects securely through Tor, allowing you to manage channels, balances, and your full node on the go.</p>'+
'<div class="nwc-connect-steps">'+
'<div class="nwc-connect-step"><div class="nwc-step-num">1</div><div><strong>Download Zeus</strong> from the App Store or Google Play.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">2</div><div>Open Zeus and open the <strong>Wallets</strong> screen.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">3</div><div>Tap the <strong>+ (Add Wallet)</strong> button in the top-right corner.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">4</div><div>On <strong>Wallet Configuration</strong>, tap the <strong>scan icon</strong> in the top-right corner, then scan the QR code above.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the LND REST QR and fills in the connection details. Review them, then tap <strong>Save Wallet Config</strong>.</div></div>'+
'</div>'+
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the NWC pairing QR shown in Lightning Wallet Connections — that gives your wallet sandboxed, limited access for everyday spending. LND REST connects Zeus directly to your node for full admin control.</div>'+
'</div>';
}
function_renderCredsHtml(credentials,unit){
varhtml="";
for(vari=0;i<credentials.length;i++){
@@ -9,7 +24,10 @@ function _renderCredsHtml(credentials, unit) {
qrBlock='<div class="creds-qr-wrap"><img class="creds-qr-img" src="'+cred.qrcode+'" alt="QR Code for '+escHtml(cred.label)+'"><div class="creds-qr-hint">Scan with Zeus app on your phone</div></div>';
varqrHint=(unit==="zeus-connect-setup.service")
?"This is an <strong>LND REST</strong> connection QR — in Zeus, add a wallet and use the scan icon (see steps below)."
:"In Zeus: <em>Wallets → + → scan icon</em>. This is an <strong>LND REST</strong> QR for direct node access.";
qrBlock='<div class="creds-qr-wrap"><img class="creds-qr-img" src="'+cred.qrcode+'" alt="QR Code for '+escHtml(cred.label)+'"><div class="creds-qr-hint">'+qrHint+'</div></div>';
}
// If qronly, render the label + QR block only — skip value and copy button
if(cred.qronly){
@@ -58,8 +76,562 @@ function _attachCopyHandlers(container) {
});
}
var_nwcModalState=null;
function_isNwcServiceUnit(unit){
// The dashboard tile is backed by Alby Hub, while older drafts referred to
// the logical feature as nwc-wallets.service. Support both so the Wallet
// Connections manager appears from the actual tile and any legacy configs.
'<div class="nwc-tab-intro-title">Create a Wallet</div>'+
'<p class="nwc-tab-intro-desc">Create a secure, sandboxed wallet for a specific app. Each wallet gets its own Lightning Address and optional spending limit.</p>'+
'<div class="creds-qr-wrap"><img class="creds-qr-img" src="'+created.pairing_qrcode+'" alt="QR code for Lightning Wallet Connections pairing secret"><div class="creds-qr-hint">This is an <strong>NWC</strong> pairing QR — in Zeus, add a wallet and use the scan icon (see steps below).</div></div></div>';
'<div class="nwc-connect-guide-title">📱 Connect to Zeus</div>'+
'<p class="nwc-connect-guide-intro">This pairing URI is an <strong>NWC (Nostr Wallet Connect)</strong> connection — the modern, mobile-friendly way to use Zeus with your node. It connects directly through your Lightning domain, so no Tor or port forwarding is needed on your phone.</p>'+
'<div class="nwc-connect-steps">'+
'<div class="nwc-connect-step"><div class="nwc-step-num">1</div><div><strong>Download Zeus</strong> from the App Store or Google Play.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">2</div><div>Open Zeus and open the <strong>Wallets</strong> screen.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">3</div><div>Tap the <strong>+ (Add Wallet)</strong> button in the top-right corner.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">4</div><div>On <strong>Wallet Configuration</strong>, tap the <strong>scan icon</strong> in the top-right corner, then scan the QR code above.</div></div>'+
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the NWC QR and fills in <strong>Nostr Wallet Connect</strong>. Review it, then tap <strong>Save Wallet Config</strong>.</div></div>'+
'</div>'+
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the LND REST / Tor QR shown on your LND tile — that connects Zeus directly to your Lightning node for full admin control. NWC gives your wallet sandboxed, limited access for everyday spending.</div>'+
'</div>';
html+='<div class="matrix-form-actions">'+
'<button class="matrix-form-back" id="nwc-created-another-btn"'+(state.busy?" disabled":"")+'>Create Another Wallet</button>'+
'<button class="matrix-form-submit" id="nwc-created-continue-btn"'+(state.busy?" disabled":"")+'>I Saved This Secret</button>'+
html+='<p class="svc-detail-desc">Share the Lightning Address for <strong>'+escHtml(sw.name||"Wallet")+'</strong>. This QR never expires — anyone can scan it with any Lightning wallet to send sats, as many times as they like.</p>'+
'<div class="creds-qr-wrap">'+
'<img class="creds-qr-img" src="'+pngUrl+'" alt="LNURL QR code for '+escHtml(swAddress)+'">'+
'<div class="creds-qr-hint">Scan with any Lightning wallet to pay '+escHtml(swAddress)+'</div>'+
'<div class="nwc-domain-required-body">Set up a unique hostname such as <strong>lightning.yourdomain.com</strong> before creating wallets.</div>'+
'<button class="btn btn-primary nwc-domain-required-btn" id="nwc-domain-setup-link">⚙ Go to Service & Setup</button>'+
'</div>';
}
if(!state.wallets||state.wallets.length===0){
html+='<div class="nwc-empty-state">'+
'<div class="nwc-empty-icon">⚡</div>'+
'<div class="nwc-empty-title">Ready to start spending</div>'+
'<p class="nwc-empty-desc">Create your first isolated wallet to connect to apps like Zeus (via NWC) or Nostr. Experience faster, more secure Lightning payments today.</p>'+
varinternalIpHtml=internalIp?escHtml(internalIp):"Could not detect";
varrouterIpHelp=internalIp
?"Use this IP address as the destination/internal IP when creating each router forwarding rule."
:"Use this computer’s internal IP as the destination/internal IP when creating each router forwarding rule.";
varrouterNextStep=internalIp
?'Next step: Log in to your router and create forwarding rules for the ports above. Set the destination/internal IP to <strong>'+internalIpHtml+'</strong>.'
:'Next step: Log in to your router and create forwarding rules for the ports above. Use this computer’s internal IP as the destination/internal IP.';
'<div class="svc-detail-port-note">The Hub can check whether Sovran_SystemsOS is ready on this computer, but full public port verification requires an outside internet check.</div>'+
'<div class="svc-detail-port-note">This shows whether Sovran_SystemsOS is ready to use this port on this computer. If you need access from outside your home network, forward this port in your router.</div>'+
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>'+
'<div class="svc-detail-port-note">Only needed if you want to reach this service from <strong>outside</strong> your home network. On your local network it already works without any router changes.</div>'+
'<p class="nwc-tab-intro-desc">Powerful, isolated wallets for your daily spending and modern apps.</p>'+
'</div>'+
'<div class="nwc-benefits-grid">'+
'<div class="nwc-benefit-item">'+
'<div class="nwc-benefit-icon">🛡️</div>'+
'<div class="nwc-benefit-content">'+
'<strong>Isolated & Secure</strong>'+
'<p>Create sandboxed wallets for quick spending while keeping your main node protected. Isolated access is faster than Tor and perfect for budgeting.</p>'+
'</div>'+
'</div>'+
'<div class="nwc-benefit-item">'+
'<div class="nwc-benefit-icon">🌐</div>'+
'<div class="nwc-benefit-content">'+
'<strong>Modern Ecosystem</strong>'+
'<p>Easily spend and receive bitcoin using LNURL and Nostr (NWC) across the growing ecosystem of decentralized apps.</p>'+
'</div>'+
'</div>'+
'<div class="nwc-benefit-item">'+
'<div class="nwc-benefit-icon">📱</div>'+
'<div class="nwc-benefit-content">'+
'<strong>Zeus on the Go (via NWC)</strong>'+
'<p>Connect Zeus to your wallet using NWC — no Tor, no port forwarding. Create a wallet below, scan the pairing QR, and start spending from your phone.</p>'+
'<strong>Lightning Channel & Liquidity Guide</strong>'+
'<span class="nwc-liquidity-subtitle">Why a brand-new node can\'t send or receive yet</span>'+
'</span>'+
'</summary>'+
'<div class="nwc-liquidity-guide-card">'+
'<p class="nwc-liquidity-body">'+
'Your wallets, pairing keys, and Lightning Address endpoints (like <code>name@yourdomain.com</code>) work the moment you create them. Moving actual bitcoin, though, needs a Lightning node with open channels.'+
'</p>'+
'<div class="nwc-liquidity-steps">'+
'<div class="nwc-liquidity-step">'+
'<div class="step-num">1</div>'+
'<div>'+
'<strong>Outbound Liquidity (Sending)</strong>'+
'<p class="nwc-liquidity-step-desc">Required so connected apps can pay invoices and send funds.</p>'+
'</div>'+
'</div>'+
'<div class="nwc-liquidity-step">'+
'<div class="step-num">2</div>'+
'<div>'+
'<strong>Inbound Liquidity (Receiving)</strong>'+
'<p class="nwc-liquidity-step-desc">Required so your Lightning Address can receive payments from anyone.</p>'+
'</div>'+
'</div>'+
'</div>'+
'<div class="nwc-liquidity-action">'+
'<p class="nwc-liquidity-analogy">💧 <strong>The Plumbing Analogy:</strong> Channels are two-way water pipes. To pour water in (receive) or pump it out (send), you need open channels with liquidity on the right side.</p>'+
'<button class="matrix-action-btn nwc-rtl-btn" id="nwc-open-rtl-btn">🚀 Open Ride The Lightning (RTL) to Manage Channels</button>'+
'</div>'+
'</div>'+
'</details>';
}else{
html+='<div class="nwc-disabled-state">'+
'<div class="nwc-disabled-icon">⚡</div>'+
'<div class="nwc-disabled-title">Lightning Wallet Connections is turned off</div>'+
'<p class="nwc-disabled-desc">Enable it on the <strong>Service & Setup</strong> tab and rebuild, then come back here to create and manage wallets.</p>'+
'<button class="btn btn-primary" id="nwc-goto-setup-btn">⚙ Go to Service & Setup</button>'+
'<span class="support-wallet-title">Database and Blockchain Data</span>',
'</div>',
'<p class="support-wallet-desc">Application databases stored in PostgreSQL or MariaDB/MySQL are <strong>not included</strong> in Manual Backup. Bitcoin blockchain and Electrs index data are also excluded (they are stored on the internal second drive). If you use Nextcloud, Matrix, or other database-backed applications, back up those databases separately with their native tools.</p>',
'<li>USB hard drive plugged into one of the open USB ports on your Sovran Pro</li>',
'<li>At least 500 GB of free space on the drive</li>',
'<li>Drive must be formatted as <strong>exFAT</strong></li>',
'<li>Enough free space for your data (the backup checks this before starting)</li>',
'<li>Drive must be formatted as <strong>ext4</strong> (a Linux filesystem). Drives with exFAT, FAT32, or NTFS are not supported. To format a drive as ext4, use a Linux tool such as GParted or <code>mkfs.ext4</code> — note that formatting erases all data on the drive.</li>',
'<li>The drive is intended for Linux/Sovran recovery. It may not be directly readable by Windows or macOS without additional software.</li>',
'<p class="support-wallet-desc">This backup can take <strong>up to 4 hours</strong> depending on the amount of data stored on your Sovran Pro and the speed of your external hard drive. Be patient\u2026</p>',
'<p class="support-wallet-desc">The first backup may take a while depending on how much data you have. Later backups are much faster because only changed or new files are copied. Files are stored directly on the drive and can be browsed without any special software.</p>',
'</div>',
driveSelector,
@@ -584,9 +638,10 @@ async function pollBackupStatus() {
logDiv.scrollTop=logDiv.scrollHeight;
}
_backupLogOffset=data.offset;
if(!data.running){
constresult=(data.result||"").toLowerCase();
if(result==="success"||result==="failed"){
stopBackupPoll();
renderBackupDone(data.result==="success");
renderBackupDone(result==="success");
}
}catch(_){}
}
@@ -618,7 +673,7 @@ function renderBackupDone(success) {
'<p class="support-desc">The backup did not complete successfully. Please check that the USB drive is still connected, has enough free space, and is formatted as exFAT. Then try again.</p>',
'<p class="support-desc">The backup did not complete successfully. Please check that the USB drive is still connected, has enough free space, and is formatted as ext4. Then try again.</p>',
+'<li>Create an account at <a href="https://njal.la" target="_blank" style="color:var(--accent-color);">https://njal.la</a>.</li>'
+'<li>Buy at least one domain. Each service below needs its own domain — you can either give each service its own subdomain of a single domain you buy (subdomains are free, and one domain can have many), OR use a separate domain for each. Your choice.</li>'
+'<li>For each service, add a <strong>Dynamic</strong> record in Njal.la:'
+'<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>'
+'(Example only, your choice — for "call.yourdomain.com" you'd type just: <code>call</code>.)<br>'
+'If you bought a whole separate domain just for this service, leave Name blank or use <code>@</code>.<br>'
+'⚠ Do NOT type the full domain in the Name field — Njal.la adds it automatically.</li>'
+'<li>A Dynamic record has NO IP field. You don't enter an IP anywhere — it auto-fills once Sovran_SystemsOS updates it (on save, and again after reboot).</li>'
+renderNjallaStepsHtml({hostExample:"call",pasteHint:"next to its service below"})
+'</div>';
html+='<p class="onboarding-hint">Enter each service\'s full domain — a subdomain (e.g. <code>call.yourdomain.com</code>) or a separate domain (e.g. <code>call.com</code>) — and its Njal.la DDNS curl command.</p>';
// Router note (the same wording is shown again, per-service, whenever a
// domain-based feature is enabled, and lives on each service tile afterwards)
html+='<div style="font-size:0.88em;margin-bottom:8px;color:var(--color-text-muted,#888);">Only add these if you enable Element Call. These ports help video and audio calls connect reliably.</div>';
html+='<tr><td class="port-req-port">30000-40000</td><td class="port-req-proto">TCP & UDP</td><td class="port-req-internal-ip">'+ip+'</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>';
html+='</tbody></table>';
html+='<div style="font-size:0.85em;margin-top:6px;color:var(--color-text-muted,#888);">ℹ The <strong>30000-40000</strong> range is a single forwarding rule — just set its protocol to <strong>both TCP and UDP</strong> (often shown as "Both" or "TCP/UDP" on your router).</div>';
html+='</div>';
// Totals
html+='<div class="onboarding-port-totals">';
html+='<strong>Total port openings: 3</strong> (without Element Call)<br>';
html+='<strong>Total port openings: 8</strong> (with Element Call — 3 required + 5 optional)';
+'<strong>Important:</strong> The Hub can show which ports Sovran_SystemsOS needs, but it cannot fully confirm router forwarding from inside your home network. Full public port verification requires an outside internet check.'
Sovran_SystemsOS uses <strong><ahref="https://njal.la"target="_blank"style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
Create an account at Njal.la, then for each service below, add a <strong>Dynamic</strong> record — no IP needed, it auto-populates once the DDNS curl command runs.
Paste the curl command from your Njal.la dashboard for each service.
Sovran_SystemsOS uses <strong><ahref="https://njal.la"target="_blank"style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS, and your router needs ports <strong>80</strong> and <strong>443</strong> (TCP) forwarded to this computer.
Everything you need — Njal.la account, Dynamic records, and the one router task — is laid out step by step below.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.