Compare commits
194 Commits
46b8c23578
..
stable
| Author | SHA1 | Date | |
|---|---|---|---|
| 417456485a | |||
| 0945092dde | |||
| 6f12117521 | |||
| 8bf8814fa7 | |||
| d90b5b091b | |||
| 4275ac1d2f | |||
| 07b36d62d2 | |||
| 5fb8279d61 | |||
| 6eb63d3f85 | |||
| 2702854513 | |||
| 106537cc63 | |||
| dabb96e1b3 | |||
| 2b5a154b99 | |||
| e475b0f47d | |||
| 8f81f8f1e2 | |||
| cd753a7e28 | |||
| 7ac1985508 | |||
| 0ecf2eb651 | |||
| 18c7095aaf | |||
| dcad276c59 | |||
| 06988d0ff0 | |||
| 69b84153b4 | |||
| df08a7c413 | |||
| 602464189f | |||
| 67f4cdc99e | |||
| f8c717db25 | |||
| 268abddb28 | |||
| c1119b03a8 | |||
| 0c273b758d | |||
| 6f98c478e8 | |||
| 875a6a9297 | |||
| 1dbfe3cd94 | |||
| e0d4b3544d | |||
| 3e3fbed470 | |||
| ada9f25c41 | |||
| 66cacaaf9d | |||
| 15cd07d12f | |||
| fae57c0375 | |||
| 3745eedd74 | |||
| 1cd4fc8b40 | |||
| 732ab6f2aa | |||
| bea26c55c3 | |||
| a841665b07 | |||
| d574f96379 | |||
| 2388039b63 | |||
| aa69d40f08 | |||
| 31cb48cc2b | |||
| 170bd14a34 | |||
| 2553e0dce0 | |||
| b8e7b2b4cc | |||
| 24098a209a | |||
| 8ad7509b02 | |||
| a350d4e2f7 | |||
| ec3782991d | |||
| bc4b4630a3 | |||
| 342f60ce0d | |||
| 8c8e8f43a2 | |||
| 559e0218eb | |||
| fd2e12ced1 | |||
| efdf1e05d0 | |||
| cd3ab47aa0 | |||
| c12a680d27 | |||
| c728eee924 | |||
| ca704b24a2 | |||
| db068ba994 | |||
| 53ee31c5d2 | |||
| 283b439d59 | |||
| 9e09f9eb40 | |||
| 08bfa73e74 | |||
| 2b76a766ad | |||
| d245e2ce0b | |||
| bb2603bea0 | |||
| 8f96625c26 | |||
| 9c0ddf0dbe | |||
| 2a352c35f9 | |||
| 56b965b847 | |||
| 8e5bb766a6 | |||
| 646d877c4d | |||
| 045c5d6a12 | |||
| f0f690eae4 | |||
| 459536d478 | |||
| 6c5f261d8a | |||
| 91cc0152ba | |||
| bfc60eeb2c | |||
| 976d8f3609 | |||
| 10a1d0f7ba | |||
| 3a8e9a2dd0 | |||
| 6872c8d820 | |||
| 175f48ef37 | |||
| 49912a2760 | |||
| c450dcab9e | |||
| 953fb04671 | |||
| c02655a840 | |||
| f87e9982b0 | |||
| 02e662454c | |||
| 4d8eaf71ca | |||
| a0f42d3e7b | |||
| ef683a6aa9 | |||
| 02a9dbf39c | |||
| 6d72f70fe5 | |||
| c2887b60b2 | |||
| 9e76bad58a | |||
| b3f6efef8a | |||
| 53813e775d | |||
| 060f81393c | |||
| c1c0827604 | |||
| b5715e05c6 | |||
| 68c3aa95fd | |||
| 281b08dcd4 | |||
| ca1ff3ee20 | |||
| 1cd5bd4496 | |||
| 6512bf4356 | |||
| 7da0463dce | |||
| 466582bcdc | |||
| c23ae5543d | |||
| 569e0de59d | |||
| 4a7d9615db | |||
| 7c1b603200 | |||
| 4dae7836dd | |||
| e821da6c2a | |||
| 17fbd5fd2c | |||
| 8712ac43c6 | |||
| 38e4a296ee | |||
| d1ef6ba1cd | |||
| ffd2029852 | |||
| 761af09166 | |||
| 48d7e8a459 | |||
| d3327e05d4 | |||
| 2c8dd91cf0 | |||
| 448c4b9094 | |||
| e147fd8f4d | |||
| b8feea3711 | |||
| 0cc1f50aa4 | |||
| 71d8eae6d8 | |||
| 878392d998 | |||
| d6471aad55 | |||
| 4b939affaf | |||
| c1b02e0562 | |||
| 6b962ff51d | |||
| 3843f8ea22 | |||
| c85eea719d | |||
| 5309618747 | |||
| 725aad3aac | |||
| 070ab61131 | |||
| 164f052b1f | |||
| 8841a8d628 | |||
| d500d15e12 | |||
| 16898e8eb9 | |||
| c809045014 | |||
| 158d369371 | |||
| c40db26e6f | |||
| 42305f7f22 | |||
| 539ede00cb | |||
| 5324344eed | |||
| d2c9dd1fbd | |||
| a1db5773fc | |||
| 5e33a250d5 | |||
| 7e0cda17f3 | |||
| 15821207dc | |||
| cdb93ad8dc | |||
| 0c596fb396 | |||
| a3c1b849f2 | |||
| 7262694425 | |||
| ff1defcaab | |||
| 6ac9a7cd4c | |||
| cb9172d069 | |||
| 92dd718362 | |||
| 7d15b67463 | |||
| 12b2d85fb4 | |||
| 8657bdc23a | |||
| 59cbc8d4e9 | |||
| 80fea3301b | |||
| 91a3e68119 | |||
| bda9c3cd0e | |||
| a84e958182 | |||
| 21e0f284b6 | |||
| e83b4ff5b1 | |||
| 0445a1c1cc | |||
| dc1d89b441 | |||
| 0da964bfca | |||
| b5e89c38f8 | |||
| c37816d257 | |||
| fce4608647 | |||
| 8fd08057d8 | |||
| 0563c6b96b | |||
| b29ed2cce7 | |||
| f8ecbf3ee3 | |||
| 20aa66a160 | |||
| 976d3b0fa7 | |||
| 2e9d989444 | |||
| 38207e8b2f | |||
| 5fe2ecd56d | |||
| 846e2af705 | |||
| c8eb452a70 |
@@ -1 +1,159 @@
|
|||||||
### Testing Branch
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="iso/assets/sovran-hub-icon.svg" alt="Sovran Systems" width="160" />
|
||||||
|
|
||||||
|
# Sovran_SystemsOS
|
||||||
|
|
||||||
|
`Base Development` · NixOS Flake · AGPL-3.0
|
||||||
|
|
||||||
|
[Sovran Systems](https://sovransystems.com)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="assets/desktop-screenshot.png" alt="Sovran_SystemsOS desktop showing application dock and PRIVACY. SOVEREIGNTY. BITCOIN. tagline" width="800" />
|
||||||
|
|
||||||
|
*The Sovran_SystemsOS desktop — "Privacy. Sovereignty. Bitcoin."*
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [What This Repo Is](#what-this-repo-is)
|
||||||
|
2. [Architecture](#architecture)
|
||||||
|
3. [Module Catalog](#module-catalog)
|
||||||
|
4. [The Three Modes (internal reference)](#the-three-modes-internal-reference)
|
||||||
|
5. [Build & Deploy Reference](#build--deploy-reference)
|
||||||
|
6. [Networking & Reverse Proxy](#networking--reverse-proxy)
|
||||||
|
7. [Security Posture](#security-posture)
|
||||||
|
8. [Backups & Recovery](#backups--recovery)
|
||||||
|
9. [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What This Repo Is
|
||||||
|
|
||||||
|
Sovran_SystemsOS is defined entirely as a **Nix flake** (`flake.nix`) and built from source. There is no pre-built binary — the System Installer is produced from this tree. Everything the system does is declared here.
|
||||||
|
|
||||||
|
The control center is the **Hub** — a built-in panel that lets the operator launch, monitor, and toggle services without touching a terminal. Under the hood, the Hub writes to `custom.nix`, which feeds back into the flake.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ flake.nix │
|
||||||
|
│ inputs: nixpkgs, │
|
||||||
|
│ nix-bitcoin, nixvim, │
|
||||||
|
│ btc-clients │
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│ nixosModules.Sovran_SystemsOS
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐ imports ┌──────────────────────────┐
|
||||||
|
│ configuration.nix │────────────▶│ modules/modules.nix │
|
||||||
|
│ boot / fs / users / │ │ core/* + services + opt │
|
||||||
|
│ desktop / nix settings │ │ features │
|
||||||
|
└──────────────────────────┘ └──────────┬───────────────┘
|
||||||
|
▲ │
|
||||||
|
│ ./role-state.nix (mode/role) ▼
|
||||||
|
│ ./custom.nix (user overrides) ┌────────────────────┐
|
||||||
|
│ │ modules/*.nix │
|
||||||
|
└───────── sovran-hub writes ───────▶│ synapse / wordpress│
|
||||||
|
│ nextcloud / etc. │
|
||||||
|
└────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`flake.nix`** declares two NixOS configurations:
|
||||||
|
- `nixosConfigurations.nixos` — the running system.
|
||||||
|
- `nixosConfigurations.sovran_systemsos-iso` — the System Installer.
|
||||||
|
- **`configuration.nix`** owns host concerns (boot, filesystems, users, desktop, locale, Nix settings, firewall, audio, backups).
|
||||||
|
- **`modules/modules.nix`** is the service router. Every other module is opt-in via flags read from `role-state.nix` and `custom.nix`.
|
||||||
|
|
||||||
|
## Module Catalog
|
||||||
|
|
||||||
|
Defaults follow the import order in `modules/modules.nix`. Toggles live in `custom.nix` (the Hub writes them) and `role-state.nix`.
|
||||||
|
|
||||||
|
| Module | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `core/*` | **on** | Roles, Caddy, Njalla, Hub, desktop, perf, ssh-bootstrap |
|
||||||
|
| `php.nix`, `credentials.nix` | **on** | Required by web services & secrets |
|
||||||
|
| `synapse.nix` | **on** | Matrix homeserver |
|
||||||
|
| `wordpress.nix` | **on** | WordPress + PHP-FPM vhost |
|
||||||
|
| `nextcloud.nix` | **on** | Files / calendar / contacts |
|
||||||
|
| `vaultwarden.nix` | **on** | Bitwarden-compatible secrets vault |
|
||||||
|
| `bitcoinecosystem.nix` | **on** | bitcoind/electrs/LND/RTL/BTCPay (over Tor) |
|
||||||
|
| `wallet-autoconnect.nix` | **on** | Sparrow/Bisq ↔ node handshake |
|
||||||
|
| `haven.nix` | off | Nostr relay |
|
||||||
|
| `element-calling.nix` | off | LiveKit + JWT for E2E calling |
|
||||||
|
| `mempool.nix` | off | Mempool.space dashboard |
|
||||||
|
| `bitcoin-core.nix` | off | Switch node to Bitcoin Core (replaces default Bitcoin Knots + BIP110) |
|
||||||
|
| `rdp.nix` | off | xrdp remote desktop |
|
||||||
|
| `sshd.nix` | off | Public-facing OpenSSH |
|
||||||
|
|
||||||
|
> Tor is wired directly into the Bitcoin stack. In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true` and `tor.proxy = true`, and onion services are exposed for them.
|
||||||
|
|
||||||
|
## The Three Modes (internal reference)
|
||||||
|
|
||||||
|
Selected by `role-state.nix`, resolved by `modules/core/role-logic.nix`. All three configurations are produced from this same flake.
|
||||||
|
|
||||||
|
| Mode | What's enabled on top of the base NixOS + GNOME |
|
||||||
|
|---|---|
|
||||||
|
| **Desktop** | Private daily-driver. Sparrow + Bisq included. |
|
||||||
|
| **Node** | Desktop + full Bitcoin stack (bitcoind/electrs/LND/RTL/BTCPay over Tor). |
|
||||||
|
| **Server+Desktop** | Node + self-hosting services (Synapse, Nextcloud, WordPress, Vaultwarden, Element Calling, etc.). |
|
||||||
|
|
||||||
|
## Build & Deploy Reference
|
||||||
|
|
||||||
|
Internal commands. Run from the flake root.
|
||||||
|
|
||||||
|
| Action | Command |
|
||||||
|
|---|---|
|
||||||
|
| Build the System Installer | `nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage` |
|
||||||
|
| Switch now | `sudo nixos-rebuild switch --flake .#nixos` |
|
||||||
|
| Test in current boot only | `sudo nixos-rebuild test --flake .#nixos` |
|
||||||
|
| Stage for next boot | `sudo nixos-rebuild boot --flake .#nixos` |
|
||||||
|
| Build only (no activation) | `nixos-rebuild build --flake .#nixos` |
|
||||||
|
| Update pinned inputs | `nix flake update` (then rebuild) |
|
||||||
|
| Rollback last switch | `sudo nixos-rebuild switch --rollback` |
|
||||||
|
| Garbage-collect (>7 days) | Automatic weekly; manual: `sudo nix-collect-garbage -d` |
|
||||||
|
|
||||||
|
## Networking & Reverse Proxy
|
||||||
|
|
||||||
|
- **Firewall on by default** (`networking.firewall.enable = true`). Port are opened by the module that needs it.
|
||||||
|
- **Caddy** (`modules/core/caddy.nix`) terminates TLS for all HTTP services.
|
||||||
|
- **Njalla** dynamic DNS (`modules/core/njalla.nix`) keeps records in sync via a 15-minute cron job.
|
||||||
|
- **Tor** is enabled with `torsocks` available. The Bitcoin stack uses it directly — see [Security Posture](#security-posture).
|
||||||
|
- **SSH:** localhost-only by default (`core/sshd-localhost.nix`).
|
||||||
|
|
||||||
|
## Security Posture
|
||||||
|
|
||||||
|
Facts about the defaults, straight from `configuration.nix` and the modules:
|
||||||
|
|
||||||
|
- **Reproducible builds.** Every artifact derives from `flake.lock`. The same commit produces the same OS.
|
||||||
|
- **Bitcoin stack over Tor.** In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true`, and onion services are exposed for `bitcoind`, `electrs`, `lnd`, and friends.
|
||||||
|
- **Firewall on, public sshd off, RDP off, auto-login off, fail2bain active**
|
||||||
|
- **Kernel surface trimmed.** `boot.blacklistedKernelModules = [ "rxrpc" ];`
|
||||||
|
- **Weekly garbage collection** with `--delete-older-than 7d`.
|
||||||
|
|
||||||
|
## Backups & Recovery
|
||||||
|
|
||||||
|
`services.rsnapshot` snapshots hourly and daily to `/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup`:
|
||||||
|
|
||||||
|
```
|
||||||
|
backup /home/ localhost/
|
||||||
|
backup /var/lib/ localhost/
|
||||||
|
backup /etc/nixos/ localhost/
|
||||||
|
backup /etc/nix-bitcoin-secrets/ localhost/
|
||||||
|
retain hourly 5
|
||||||
|
retain daily 5
|
||||||
|
cron hourly 0 * * * *
|
||||||
|
cron daily 50 21 * * *
|
||||||
|
```
|
||||||
|
|
||||||
|
The second drive is mounted by label (`BTCEcoandBackup`) with `nofail` so a missing drive doesn't block boot.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the **GNU Affero General Public License v3.0** — see [`LICENSE`](./LICENSE).
|
||||||
|
|||||||
@@ -239,33 +239,13 @@ mkdir -p "$BACKUP_DIR/secrets"
|
|||||||
|
|
||||||
if [[ "$ROLE" == "desktop" ]]; then
|
if [[ "$ROLE" == "desktop" ]]; then
|
||||||
log "Skipping /etc/nix-bitcoin-secrets — not applicable for Desktop Only role."
|
log "Skipping /etc/nix-bitcoin-secrets — not applicable for Desktop Only role."
|
||||||
# /var/lib/domains is still backed up if present (hub state)
|
|
||||||
for SRC in /var/lib/domains; do
|
|
||||||
if [[ -e "$SRC" ]]; then
|
|
||||||
rsync -a --info=progress2 "$SRC" "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
|
||||||
log "WARNING: Could not copy $SRC — continuing."
|
|
||||||
else
|
|
||||||
log " (not found: $SRC — skipping)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
else
|
||||||
for SRC in /etc/nix-bitcoin-secrets /var/lib/domains; do
|
if [[ -e /etc/nix-bitcoin-secrets ]]; then
|
||||||
if [[ -e "$SRC" ]]; then
|
rsync -a --info=progress2 /etc/nix-bitcoin-secrets "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||||
rsync -a --info=progress2 "$SRC" "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
log "WARNING: Could not copy /etc/nix-bitcoin-secrets — continuing."
|
||||||
log "WARNING: Could not copy $SRC — continuing."
|
else
|
||||||
else
|
log " (not found: /etc/nix-bitcoin-secrets — skipping)"
|
||||||
log " (not found: $SRC — skipping)"
|
fi
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Hub state files from /var/lib/secrets/ (backed up for all roles)
|
|
||||||
if [[ -d /var/lib/secrets ]]; then
|
|
||||||
mkdir -p "$BACKUP_DIR/secrets/hub-state"
|
|
||||||
rsync -a --info=progress2 /var/lib/secrets/ "$BACKUP_DIR/secrets/hub-state/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
|
||||||
log "WARNING: Could not copy /var/lib/secrets — continuing."
|
|
||||||
else
|
|
||||||
log " (not found: /var/lib/secrets — skipping)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Stage 2 complete."
|
log "Stage 2 complete."
|
||||||
@@ -286,20 +266,35 @@ else
|
|||||||
log "WARNING: /home not found — skipping."
|
log "WARNING: /home not found — skipping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Stage 4/4: Wallet and node data ─────────────────────────────
|
# ── Stage 4/4: System data ───────────────────────────────────────
|
||||||
|
|
||||||
log ""
|
log ""
|
||||||
log "── Stage 4/4: Wallet and node data (/var/lib/lnd) ──────────"
|
log "── Stage 4/4: System data (/var/lib) ────────────────────────"
|
||||||
if [[ "$ROLE" == "desktop" ]]; then
|
if [[ "$ROLE" == "desktop" ]]; then
|
||||||
log "Skipping Stage 4 (LND wallet data) — not applicable for Desktop Only role."
|
if [[ -d /var/lib ]]; then
|
||||||
elif [[ -d /var/lib/lnd ]]; then
|
rsync -a --info=progress2 \
|
||||||
|
--filter='- /lnd/***' \
|
||||||
|
--exclude='logs/' \
|
||||||
|
--exclude='log/' \
|
||||||
|
--exclude='*/logs/' \
|
||||||
|
--exclude='*/log/' \
|
||||||
|
/var/lib/ "$BACKUP_DIR/var-lib/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||||
|
fail "Stage 4 failed while copying /var/lib for Desktop Only role"
|
||||||
|
log "Stage 4 complete (Desktop Only role excludes /var/lib/lnd)."
|
||||||
|
else
|
||||||
|
log "WARNING: /var/lib not found — skipping."
|
||||||
|
fi
|
||||||
|
elif [[ -d /var/lib ]]; then
|
||||||
rsync -a --info=progress2 \
|
rsync -a --info=progress2 \
|
||||||
--exclude='logs/' \
|
--exclude='logs/' \
|
||||||
/var/lib/lnd/ "$BACKUP_DIR/lnd/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
--exclude='log/' \
|
||||||
fail "Stage 4 failed while copying /var/lib/lnd"
|
--exclude='*/logs/' \
|
||||||
|
--exclude='*/log/' \
|
||||||
|
/var/lib/ "$BACKUP_DIR/var-lib/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||||
|
fail "Stage 4 failed while copying /var/lib"
|
||||||
log "Stage 4 complete."
|
log "Stage 4 complete."
|
||||||
else
|
else
|
||||||
log "WARNING: /var/lib/lnd not found — skipping."
|
log "WARNING: /var/lib not found — skipping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Generate manifest ────────────────────────────────────────────
|
# ── Generate manifest ────────────────────────────────────────────
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import glob
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
@@ -64,6 +65,10 @@ _DOMAIN_REACHABILITY_STARTUP_DELAY = 5
|
|||||||
_domain_reachability_task: asyncio.Task | None = None
|
_domain_reachability_task: asyncio.Task | None = None
|
||||||
_domain_reachability_task_lock = asyncio.Lock()
|
_domain_reachability_task_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
# Units to start after the next successful rebuild (feature enable flow)
|
||||||
|
_pending_service_starts: set[str] = set()
|
||||||
|
_pending_service_starts_lock = Lock()
|
||||||
|
|
||||||
BACKUP_LOG = "/var/log/sovran-hub-backup.log"
|
BACKUP_LOG = "/var/log/sovran-hub-backup.log"
|
||||||
BACKUP_STATUS = "/var/log/sovran-hub-backup.status"
|
BACKUP_STATUS = "/var/log/sovran-hub-backup.status"
|
||||||
BACKUP_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scripts", "sovran-hub-backup.sh")
|
BACKUP_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scripts", "sovran-hub-backup.sh")
|
||||||
@@ -84,6 +89,7 @@ AUTOLAUNCH_DISABLE_FLAG = "/var/lib/sovran/hub-autolaunch-disabled"
|
|||||||
# ── Hub web authentication ────────────────────────────────────────
|
# ── Hub web authentication ────────────────────────────────────────
|
||||||
|
|
||||||
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"
|
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"
|
||||||
|
MIGRATION_NEWPASS_FILE = "/var/lib/secrets/free-password-migration-newpass"
|
||||||
HUB_SESSION_SECRET_FILE = "/var/lib/secrets/hub-session-secret"
|
HUB_SESSION_SECRET_FILE = "/var/lib/secrets/hub-session-secret"
|
||||||
SESSION_COOKIE_NAME = "hub_session"
|
SESSION_COOKIE_NAME = "hub_session"
|
||||||
SESSION_MAX_AGE = 86400 # 24 hours
|
SESSION_MAX_AGE = 86400 # 24 hours
|
||||||
@@ -98,7 +104,7 @@ LOGIN_FAIL_WINDOW = 60.0 # rolling window (seconds) for counting failures
|
|||||||
LOGIN_FAIL_MAX = 10 # max failures in window before extra delay
|
LOGIN_FAIL_MAX = 10 # max failures in window before extra delay
|
||||||
|
|
||||||
# Public paths that are accessible without a valid session
|
# Public paths that are accessible without a valid session
|
||||||
_AUTH_EXEMPT_PATHS = {"/login", "/api/login", "/api/updates/status", "/api/rebuild/status", "/auto-login", "/api/ping"}
|
_AUTH_EXEMPT_PATHS = {"/login", "/api/login", "/api/updates/status", "/api/rebuild/status", "/auto-login", "/api/ping", "/api/reboot"}
|
||||||
# Prefixes for static assets required by the login page
|
# Prefixes for static assets required by the login page
|
||||||
_AUTH_EXEMPT_PREFIXES = ("/static/css/", "/static/sovran-hub-icon.svg")
|
_AUTH_EXEMPT_PREFIXES = ("/static/css/", "/static/sovran-hub-icon.svg")
|
||||||
|
|
||||||
@@ -198,7 +204,7 @@ FEATURE_REGISTRY = [
|
|||||||
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
|
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
|
||||||
{"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"},
|
{"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"},
|
||||||
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
||||||
{"port": "7882-7894", "protocol": "UDP", "description": "LiveKit media streams"},
|
{"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
|
||||||
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
||||||
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
||||||
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
||||||
@@ -216,28 +222,16 @@ FEATURE_REGISTRY = [
|
|||||||
"conflicts_with": [],
|
"conflicts_with": [],
|
||||||
"port_requirements": [],
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "bip110",
|
|
||||||
"name": "Bitcoin Knots + BIP110",
|
|
||||||
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Knots + BIP110 consensus changes. It will disable the currently active alternative.",
|
|
||||||
"category": "bitcoin",
|
|
||||||
"needs_domain": False,
|
|
||||||
"domain_name": None,
|
|
||||||
"needs_ddns": False,
|
|
||||||
"extra_fields": [],
|
|
||||||
"conflicts_with": ["bitcoin-core"],
|
|
||||||
"port_requirements": [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "bitcoin-core",
|
"id": "bitcoin-core",
|
||||||
"name": "Bitcoin Core",
|
"name": "Bitcoin Core",
|
||||||
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Core. It will disable the currently active alternative.",
|
"description": "Only one Bitcoin node implementation can be active: Bitcoin Knots + BIP110 (default) or Bitcoin Core. Enabling this replaces Knots + BIP110 with Bitcoin Core. Your timechain data is preserved.",
|
||||||
"category": "bitcoin",
|
"category": "bitcoin",
|
||||||
"needs_domain": False,
|
"needs_domain": False,
|
||||||
"domain_name": None,
|
"domain_name": None,
|
||||||
"needs_ddns": False,
|
"needs_ddns": False,
|
||||||
"extra_fields": [],
|
"extra_fields": [],
|
||||||
"conflicts_with": ["bip110"],
|
"conflicts_with": [],
|
||||||
"port_requirements": [],
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -271,13 +265,16 @@ FEATURE_REGISTRY = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Feature ids that have been removed/deprecated. The Hub must never write these
|
||||||
|
# back into custom.nix, and should strip any it finds (see startup migration).
|
||||||
|
DEPRECATED_FEATURE_IDS: set[str] = {"bip110"}
|
||||||
|
|
||||||
# Map feature IDs to their systemd units in config.json
|
# Map feature IDs to their systemd units in config.json
|
||||||
FEATURE_SERVICE_MAP = {
|
FEATURE_SERVICE_MAP = {
|
||||||
"rdp": "gnome-remote-desktop.service",
|
"rdp": "gnome-remote-desktop.service",
|
||||||
"haven": "haven-relay.service",
|
"haven": "haven-relay.service",
|
||||||
"element-calling": "livekit.service",
|
"element-calling": "livekit.service",
|
||||||
"mempool": "mempool.service",
|
"mempool": "mempool.service",
|
||||||
"bip110": None,
|
|
||||||
"bitcoin-core": None,
|
"bitcoin-core": None,
|
||||||
"btcpay-web": "btcpayserver.service",
|
"btcpay-web": "btcpayserver.service",
|
||||||
"sshd": "sshd.service",
|
"sshd": "sshd.service",
|
||||||
@@ -289,7 +286,7 @@ _PORTS_MATRIX_FEDERATION = [
|
|||||||
]
|
]
|
||||||
_PORTS_ELEMENT_CALLING = [
|
_PORTS_ELEMENT_CALLING = [
|
||||||
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
||||||
{"port": "7882-7894", "protocol": "UDP", "description": "LiveKit media streams"},
|
{"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
|
||||||
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
||||||
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
||||||
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
||||||
@@ -325,7 +322,6 @@ SERVICE_DOMAIN_MAP: dict[str, str] = {
|
|||||||
|
|
||||||
# For features that share a unit, disambiguate by icon field
|
# For features that share a unit, disambiguate by icon field
|
||||||
FEATURE_ICON_MAP = {
|
FEATURE_ICON_MAP = {
|
||||||
"bip110": "bip110",
|
|
||||||
"bitcoin-core": "bitcoin-core",
|
"bitcoin-core": "bitcoin-core",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +342,7 @@ ROLE_CATEGORIES: dict[str, set[str] | None] = {
|
|||||||
ROLE_FEATURES: dict[str, set[str] | None] = {
|
ROLE_FEATURES: dict[str, set[str] | None] = {
|
||||||
"server_plus_desktop": None,
|
"server_plus_desktop": None,
|
||||||
"desktop": {"rdp", "sshd"},
|
"desktop": {"rdp", "sshd"},
|
||||||
"node": {"rdp", "bip110", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
|
"node": {"rdp", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
|
||||||
}
|
}
|
||||||
|
|
||||||
SERVICE_DESCRIPTIONS: dict[str, str] = {
|
SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||||
@@ -448,14 +444,6 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
|
|||||||
"To use Sparrow Wallet, open it directly from your desktop — it's already installed and "
|
"To use Sparrow Wallet, open it directly from your desktop — it's already installed and "
|
||||||
"auto-configured to connect to your local Electrs server."
|
"auto-configured to connect to your local Electrs server."
|
||||||
),
|
),
|
||||||
"bisq-autoconnect.service": (
|
|
||||||
"Bisq is a decentralized, peer-to-peer Bitcoin exchange — buy and sell Bitcoin "
|
|
||||||
"with no KYC and no middleman. Sovran_SystemsOS automatically connects it to your "
|
|
||||||
"local Bitcoin node on first boot, routing all traffic through Tor. Your trades are "
|
|
||||||
"verified by your own node, keeping you fully sovereign.\n\n"
|
|
||||||
"To use Bisq, open it directly from your desktop — it's already installed and "
|
|
||||||
"auto-configured to connect to your local Bitcoin node."
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Diceware password generation ─────────────────────────────────
|
# ── Diceware password generation ─────────────────────────────────
|
||||||
@@ -582,6 +570,18 @@ def _check_password(submitted: str) -> bool:
|
|||||||
return hmac.compare_digest(submitted.encode(), stored.encode())
|
return hmac.compare_digest(submitted.encode(), stored.encode())
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_onboarding_reopened_for_migration() -> None:
|
||||||
|
"""Re-open onboarding when a migration password disclosure is pending."""
|
||||||
|
if not os.path.isfile(MIGRATION_NEWPASS_FILE):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
os.remove(ONBOARDING_FLAG)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning("Could not clear onboarding flag for migration flow: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
def _record_failure(client_ip: str) -> None:
|
def _record_failure(client_ip: str) -> None:
|
||||||
"""Record a failed login attempt and apply a rate-limit delay.
|
"""Record a failed login attempt and apply a rate-limit delay.
|
||||||
|
|
||||||
@@ -642,6 +642,37 @@ templates = Jinja2Templates(directory=os.path.join(_BASE_DIR, "templates"))
|
|||||||
|
|
||||||
# ── Static asset cache-busting ────────────────────────────────────
|
# ── Static asset cache-busting ────────────────────────────────────
|
||||||
|
|
||||||
|
def _compute_asset_version() -> str:
|
||||||
|
"""Return a 16-char asset version from Nix store hash or static/template metadata."""
|
||||||
|
nix_match = re.search(r"/nix/store/([a-z0-9]{32})-", os.path.realpath(_BASE_DIR))
|
||||||
|
if nix_match:
|
||||||
|
return nix_match.group(1)[:16]
|
||||||
|
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
for root in (
|
||||||
|
os.path.join(_BASE_DIR, "static"),
|
||||||
|
os.path.join(_BASE_DIR, "templates"),
|
||||||
|
):
|
||||||
|
if not os.path.isdir(root):
|
||||||
|
continue
|
||||||
|
for dirpath, dirnames, filenames in os.walk(root):
|
||||||
|
dirnames.sort()
|
||||||
|
for filename in sorted(filenames):
|
||||||
|
path = os.path.join(dirpath, filename)
|
||||||
|
try:
|
||||||
|
stat = os.stat(path)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
hasher.update(path.encode())
|
||||||
|
hasher.update(b"\0")
|
||||||
|
hasher.update(f"{stat.st_mtime_ns}:{stat.st_size}".encode())
|
||||||
|
hasher.update(b"\0")
|
||||||
|
return hasher.hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
ASSET_VERSION = _compute_asset_version()
|
||||||
|
|
||||||
|
|
||||||
def _file_hash(filename: str) -> str:
|
def _file_hash(filename: str) -> str:
|
||||||
"""Return first 8 chars of the MD5 hex digest for a static file."""
|
"""Return first 8 chars of the MD5 hex digest for a static file."""
|
||||||
path = os.path.join(_BASE_DIR, "static", filename)
|
path = os.path.join(_BASE_DIR, "static", filename)
|
||||||
@@ -884,7 +915,7 @@ def _get_firewall_allowed_ports() -> dict[str, set[int]]:
|
|||||||
|
|
||||||
|
|
||||||
def _port_range_to_ints(port_str: str) -> list[int]:
|
def _port_range_to_ints(port_str: str) -> list[int]:
|
||||||
"""Convert a port string like ``"443"``, ``"7882-7894"`` to a list of ints."""
|
"""Convert a port string like ``"443"``, ``"30000-40000"`` to a list of ints."""
|
||||||
port_str = port_str.strip()
|
port_str = port_str.strip()
|
||||||
if re.match(r'^\d+$', port_str):
|
if re.match(r'^\d+$', port_str):
|
||||||
return [int(port_str)]
|
return [int(port_str)]
|
||||||
@@ -1234,6 +1265,7 @@ def _resolve_credential(cred: dict) -> dict | None:
|
|||||||
extract = cred.get("extract", "")
|
extract = cred.get("extract", "")
|
||||||
multiline = cred.get("multiline", False)
|
multiline = cred.get("multiline", False)
|
||||||
qrcode = cred.get("qrcode", False)
|
qrcode = cred.get("qrcode", False)
|
||||||
|
qronly = cred.get("qronly", False)
|
||||||
|
|
||||||
# Static value
|
# Static value
|
||||||
if "value" in cred:
|
if "value" in cred:
|
||||||
@@ -1242,6 +1274,8 @@ def _resolve_credential(cred: dict) -> dict | None:
|
|||||||
qr_data = _generate_qr_base64(result["value"])
|
qr_data = _generate_qr_base64(result["value"])
|
||||||
if qr_data:
|
if qr_data:
|
||||||
result["qrcode"] = qr_data
|
result["qrcode"] = qr_data
|
||||||
|
if qronly:
|
||||||
|
result["qronly"] = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# File-based value
|
# File-based value
|
||||||
@@ -1271,6 +1305,9 @@ def _resolve_credential(cred: dict) -> dict | None:
|
|||||||
if qr_data:
|
if qr_data:
|
||||||
result["qrcode"] = qr_data
|
result["qrcode"] = qr_data
|
||||||
|
|
||||||
|
if qronly:
|
||||||
|
result["qronly"] = True
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -1472,7 +1509,9 @@ def _read_hub_overrides() -> tuple[dict, str | None, str | None, str | None]:
|
|||||||
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
||||||
section,
|
section,
|
||||||
):
|
):
|
||||||
features[m.group(1)] = m.group(2) == "true"
|
feat_id = m.group(1)
|
||||||
|
if feat_id not in DEPRECATED_FEATURE_IDS:
|
||||||
|
features[feat_id] = m.group(2) == "true"
|
||||||
for m in re.finditer(
|
for m in re.finditer(
|
||||||
r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
||||||
section,
|
section,
|
||||||
@@ -1505,6 +1544,8 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
|
|||||||
"""Write the Hub Managed section inside custom.nix."""
|
"""Write the Hub Managed section inside custom.nix."""
|
||||||
lines = []
|
lines = []
|
||||||
for feat_id, enabled in features.items():
|
for feat_id, enabled in features.items():
|
||||||
|
if feat_id in DEPRECATED_FEATURE_IDS:
|
||||||
|
continue
|
||||||
val = "true" if enabled else "false"
|
val = "true" if enabled else "false"
|
||||||
if feat_id == "btcpay-web":
|
if feat_id == "btcpay-web":
|
||||||
lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};")
|
lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};")
|
||||||
@@ -1550,6 +1591,40 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
|
|||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_strip_deprecated_features() -> None:
|
||||||
|
"""One-time migration: remove deprecated feature lines from the Hub Managed
|
||||||
|
section of custom.nix. Any feature id in DEPRECATED_FEATURE_IDS is dropped
|
||||||
|
while all other Hub-managed settings (other features, nostr_npub, timezone,
|
||||||
|
locale) are preserved byte-for-byte in meaning.
|
||||||
|
|
||||||
|
This is a no-op (and never raises) if CUSTOM_NIX is missing, unreadable, or
|
||||||
|
contains no deprecated lines.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(CUSTOM_NIX, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Quick-exit: if none of the deprecated ids appear, nothing to do.
|
||||||
|
hub_begin = content.find(HUB_BEGIN)
|
||||||
|
hub_end = content.find(HUB_END)
|
||||||
|
if hub_begin == -1 or hub_end == -1:
|
||||||
|
return
|
||||||
|
section = content[hub_begin:hub_end]
|
||||||
|
if not any(f"features.{dep_id}" in section for dep_id in DEPRECATED_FEATURE_IDS):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
features, nostr_npub, timezone, locale = _read_hub_overrides()
|
||||||
|
# _read_hub_overrides already excludes DEPRECATED_FEATURE_IDS, so
|
||||||
|
# calling _write_hub_overrides with its output drops the stale lines.
|
||||||
|
_write_hub_overrides(features, nostr_npub, timezone, locale)
|
||||||
|
except Exception:
|
||||||
|
# Never let a migration failure break startup.
|
||||||
|
logger.exception("_migrate_strip_deprecated_features: unexpected error (non-fatal)")
|
||||||
|
|
||||||
|
|
||||||
# ── Feature status helpers ─────────────────────────────────────────
|
# ── Feature status helpers ─────────────────────────────────────────
|
||||||
|
|
||||||
def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
||||||
@@ -1559,7 +1634,7 @@ def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
|||||||
return False # Default off in Node role; only on via explicit hub toggle
|
return False # Default off in Node role; only on via explicit hub toggle
|
||||||
unit = FEATURE_SERVICE_MAP.get(feature_id)
|
unit = FEATURE_SERVICE_MAP.get(feature_id)
|
||||||
if unit is None:
|
if unit is None:
|
||||||
return None # bip110, bitcoin-core — can't determine from config
|
return None # bitcoin-core — can't determine from config
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
for svc in cfg.get("services", []):
|
for svc in cfg.get("services", []):
|
||||||
if svc.get("unit") == unit:
|
if svc.get("unit") == unit:
|
||||||
@@ -1878,7 +1953,10 @@ def _verify_support_removed() -> bool:
|
|||||||
|
|
||||||
@app.get("/login", response_class=HTMLResponse)
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
async def login_page(request: Request):
|
async def login_page(request: Request):
|
||||||
return templates.TemplateResponse("login.html", {"request": request})
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request,
|
||||||
|
"asset_version": ASSET_VERSION,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/auto-login")
|
@app.get("/auto-login")
|
||||||
@@ -1945,19 +2023,23 @@ async def api_logout(request: Request):
|
|||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
return templates.TemplateResponse("index.html", {
|
return templates.TemplateResponse("index.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
|
"asset_version": ASSET_VERSION,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/onboarding", response_class=HTMLResponse)
|
@app.get("/onboarding", response_class=HTMLResponse)
|
||||||
async def onboarding(request: Request):
|
async def onboarding(request: Request):
|
||||||
|
_ensure_onboarding_reopened_for_migration()
|
||||||
return templates.TemplateResponse("onboarding.html", {
|
return templates.TemplateResponse("onboarding.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
|
"asset_version": ASSET_VERSION,
|
||||||
"onboarding_js_hash": _ONBOARDING_JS_HASH,
|
"onboarding_js_hash": _ONBOARDING_JS_HASH,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/status")
|
@app.get("/api/onboarding/status")
|
||||||
async def api_onboarding_status():
|
async def api_onboarding_status():
|
||||||
|
_ensure_onboarding_reopened_for_migration()
|
||||||
complete = os.path.exists(ONBOARDING_FLAG)
|
complete = os.path.exists(ONBOARDING_FLAG)
|
||||||
return {"complete": complete}
|
return {"complete": complete}
|
||||||
|
|
||||||
@@ -1994,6 +2076,75 @@ async def api_onboarding_complete():
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/migration/password-status")
|
||||||
|
async def api_migration_password_status():
|
||||||
|
"""Return whether a migration-generated password is awaiting acknowledgement."""
|
||||||
|
try:
|
||||||
|
with open(MIGRATION_NEWPASS_FILE, "r") as f:
|
||||||
|
return {"pending": True, "password": f.read().strip()}
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {"pending": False}
|
||||||
|
except OSError as exc:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Could not read migration password: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/migration/password-acknowledge")
|
||||||
|
async def api_migration_password_acknowledge():
|
||||||
|
"""Acknowledge the migration password and update /etc/shadow to match."""
|
||||||
|
# Read the new password before deleting the file
|
||||||
|
new_password = None
|
||||||
|
try:
|
||||||
|
with open(MIGRATION_NEWPASS_FILE, "r") as f:
|
||||||
|
new_password = f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError as exc:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Could not read migration password: {exc}")
|
||||||
|
|
||||||
|
# Update /etc/shadow so GDM accepts the new password going forward
|
||||||
|
if new_password:
|
||||||
|
chpasswd_bin = (
|
||||||
|
shutil.which("chpasswd")
|
||||||
|
or ("/run/current-system/sw/bin/chpasswd"
|
||||||
|
if os.path.isfile("/run/current-system/sw/bin/chpasswd") else None)
|
||||||
|
)
|
||||||
|
if chpasswd_bin:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[chpasswd_bin],
|
||||||
|
input=f"free:{new_password}",
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.warning(
|
||||||
|
"chpasswd failed during migration acknowledge (rc=%d): %s",
|
||||||
|
result.returncode,
|
||||||
|
(result.stderr or result.stdout).strip(),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("chpasswd exception during migration acknowledge: %s", exc)
|
||||||
|
|
||||||
|
# Clear only the locked keyring databases, leaving the directory and 'default' pointer intact.
|
||||||
|
keyring_dir = "/home/free/.local/share/keyrings"
|
||||||
|
keyring_files = glob.glob(os.path.join(keyring_dir, "*.keyring"))
|
||||||
|
for kf in keyring_files:
|
||||||
|
try:
|
||||||
|
os.remove(kf)
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning("Could not remove old keyring file %s: %s", kf, exc)
|
||||||
|
|
||||||
|
# Clear the pending marker
|
||||||
|
try:
|
||||||
|
os.remove(MIGRATION_NEWPASS_FILE)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError as exc:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Could not clear migration password: {exc}")
|
||||||
|
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
# ── Auto-launch endpoints ─────────────────────────────────────────
|
# ── Auto-launch endpoints ─────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/api/autolaunch/status")
|
@app.get("/api/autolaunch/status")
|
||||||
@@ -2102,6 +2253,16 @@ _BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
|
|||||||
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
|
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
|
||||||
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
|
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
|
||||||
|
|
||||||
|
# Cache for ``bitcoin-cli getdeploymentinfo`` output (BIP-110 live status)
|
||||||
|
_btc_deployment_cache: tuple[float, dict | None] = (0.0, None)
|
||||||
|
|
||||||
|
# Bitcoin Knots exposes BIP-110 as the `reduced_data` versionbits deployment
|
||||||
|
# (RDTS, bit 4) in getdeploymentinfo. See Knots src/deploymentinfo.cpp,
|
||||||
|
# src/kernel/chainparams.cpp, and doc/bips.md.
|
||||||
|
BIP110_DEPLOYMENT_NAMES = {"reduced_data", "rdts", "bip110", "uasf-bip110"}
|
||||||
|
BIP110_VERSIONBITS_BIT = 4
|
||||||
|
BIP110_SUBVERSION_MARKERS = {"bip110", "uasf-bip110", "reduced_data", "rdts"}
|
||||||
|
|
||||||
|
|
||||||
# ── Generic service version detection (NixOS store path) ─────────
|
# ── Generic service version detection (NixOS store path) ─────────
|
||||||
|
|
||||||
@@ -2216,12 +2377,160 @@ def _get_bitcoin_version_info() -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bitcoin_deployment_info() -> dict | None:
|
||||||
|
"""Call bitcoin-cli getdeploymentinfo and return parsed JSON, or None on error.
|
||||||
|
|
||||||
|
Results are cached for _BTC_VERSION_CACHE_TTL seconds. Never raises.
|
||||||
|
"""
|
||||||
|
global _btc_deployment_cache
|
||||||
|
now = time.monotonic()
|
||||||
|
cached_at, cached_val = _btc_deployment_cache
|
||||||
|
if now - cached_at < _BTC_VERSION_CACHE_TTL:
|
||||||
|
return cached_val
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getdeploymentinfo"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
_btc_deployment_cache = (now, None)
|
||||||
|
return None
|
||||||
|
info = json.loads(result.stdout)
|
||||||
|
_btc_deployment_cache = (now, info)
|
||||||
|
return info
|
||||||
|
except Exception:
|
||||||
|
_btc_deployment_cache = (now, None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bip110_status() -> dict:
|
||||||
|
"""Return a dict describing the live BIP-110 deployment/signaling state.
|
||||||
|
|
||||||
|
The returned struct has four stable keys::
|
||||||
|
|
||||||
|
{
|
||||||
|
"supported": bool, # node build is BIP-110-capable
|
||||||
|
"signaling": bool, # node is actively signaling / locked-in / active
|
||||||
|
"state": str, # "active" | "locked_in" | "signaling" |
|
||||||
|
# "not_signaling" | "unsupported" | "unknown"
|
||||||
|
"source": str, # "getdeploymentinfo" | "subversion" | "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
Resolution order (authoritative → fallback → honest unknown):
|
||||||
|
|
||||||
|
1. ``getdeploymentinfo`` (authoritative) — scan ``deployments`` for BIP-110.
|
||||||
|
Bitcoin Knots currently exposes BIP-110 as ``reduced_data`` (RDTS, bit 4;
|
||||||
|
see Knots deploymentinfo.cpp / chainparams.cpp / doc/bips.md), so matching
|
||||||
|
first uses known deployment names, then falls back to versionbits bit 4.
|
||||||
|
|
||||||
|
2. Subversion fallback — if getdeploymentinfo is unavailable or yields no
|
||||||
|
recognisable BIP-110 entry, inspect the ``subversion`` field from
|
||||||
|
``getnetworkinfo``. A case-insensitive match for known BIP-110 markers
|
||||||
|
(including "bip110", "uasf-bip110", "reduced_data", "rdts") is treated as
|
||||||
|
"signaling".
|
||||||
|
|
||||||
|
3. Unknown — if the node is entirely unreachable or neither source is
|
||||||
|
conclusive, return state="unknown", signaling=False, source="none".
|
||||||
|
"""
|
||||||
|
_unknown: dict = {"supported": False, "signaling": False, "state": "unknown", "source": "none"}
|
||||||
|
|
||||||
|
def _deployment_bit(entry: dict) -> int | None:
|
||||||
|
bip9 = entry.get("bip9", {}) or {}
|
||||||
|
bip8 = entry.get("bip8", {}) or {}
|
||||||
|
bit = bip9.get("bit")
|
||||||
|
if bit is None:
|
||||||
|
bit = bip8.get("bit")
|
||||||
|
if bit is None:
|
||||||
|
bit = entry.get("bit")
|
||||||
|
return bit
|
||||||
|
|
||||||
|
# ── 1. getdeploymentinfo (authoritative) ──────────────────────────
|
||||||
|
deploy_info = _get_bitcoin_deployment_info()
|
||||||
|
if deploy_info is not None:
|
||||||
|
deployments = deploy_info.get("deployments", {})
|
||||||
|
if isinstance(deployments, dict):
|
||||||
|
matched_entry: dict | None = None
|
||||||
|
|
||||||
|
# Primary match: known deployment names (case-insensitive exact match)
|
||||||
|
for key, entry in deployments.items():
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
key_lower = key.lower()
|
||||||
|
if key_lower not in BIP110_DEPLOYMENT_NAMES:
|
||||||
|
continue
|
||||||
|
matched_entry = entry
|
||||||
|
break
|
||||||
|
|
||||||
|
# Secondary match: versionbits bit (fallback only)
|
||||||
|
if matched_entry is None:
|
||||||
|
for _, entry in deployments.items():
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
if _deployment_bit(entry) != BIP110_VERSIONBITS_BIT:
|
||||||
|
continue
|
||||||
|
matched_entry = entry
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched_entry is not None:
|
||||||
|
entry = matched_entry
|
||||||
|
|
||||||
|
# bip9 / bip8 status field
|
||||||
|
bip9 = entry.get("bip9", {}) or {}
|
||||||
|
bip8 = entry.get("bip8", {}) or {}
|
||||||
|
status = (
|
||||||
|
bip9.get("status")
|
||||||
|
or bip8.get("status")
|
||||||
|
or entry.get("status")
|
||||||
|
or ""
|
||||||
|
).lower()
|
||||||
|
active = entry.get("active", False)
|
||||||
|
|
||||||
|
if active or status == "active":
|
||||||
|
return {"supported": True, "signaling": True, "state": "active", "source": "getdeploymentinfo"}
|
||||||
|
if status == "locked_in":
|
||||||
|
return {"supported": True, "signaling": True, "state": "locked_in", "source": "getdeploymentinfo"}
|
||||||
|
if status in ("started", "defined"):
|
||||||
|
# Check whether deployment is currently signaling in this period.
|
||||||
|
stats = bip9.get("statistics") or bip8.get("statistics") or {}
|
||||||
|
# Some Knots outputs expose only ``count`` (not explicit signaling bool),
|
||||||
|
# so treat count>0 as a conservative signaling indicator for this period.
|
||||||
|
count = stats.get("count")
|
||||||
|
signaling = bool(
|
||||||
|
stats.get("signaling")
|
||||||
|
or stats.get("signalling")
|
||||||
|
or (isinstance(count, int) and count > 0)
|
||||||
|
)
|
||||||
|
if signaling:
|
||||||
|
return {"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"}
|
||||||
|
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
|
||||||
|
if status == "failed":
|
||||||
|
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
|
||||||
|
# Entry found but status unrecognised — node supports BIP-110 but state unclear
|
||||||
|
return {"supported": True, "signaling": False, "state": "unknown", "source": "getdeploymentinfo"}
|
||||||
|
|
||||||
|
# ── 2. Subversion fallback ─────────────────────────────────────────
|
||||||
|
net_info = _get_bitcoin_version_info()
|
||||||
|
if net_info is not None:
|
||||||
|
subversion = net_info.get("subversion", "") or ""
|
||||||
|
sv_lower = subversion.lower()
|
||||||
|
if any(marker in sv_lower for marker in BIP110_SUBVERSION_MARKERS):
|
||||||
|
return {"supported": True, "signaling": True, "state": "signaling", "source": "subversion"}
|
||||||
|
# Node is reachable via RPC but no BIP-110 marker found anywhere
|
||||||
|
return {"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"}
|
||||||
|
|
||||||
|
# ── 3. Node unreachable / RPC not ready ───────────────────────────
|
||||||
|
return _unknown
|
||||||
|
|
||||||
|
|
||||||
def _get_bitcoind_version() -> str | None:
|
def _get_bitcoind_version() -> str | None:
|
||||||
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
|
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
|
||||||
|
|
||||||
Parses the first output line to extract the token after "version ".
|
Parses the first output line to extract the token after "version ".
|
||||||
For example: "Bitcoin Knots daemon version v29.3.knots20260210+bip110-v0.4.1"
|
For example: "Bitcoin Knots daemon version v29.3.knots20260508"
|
||||||
returns "v29.3.knots20260210+bip110-v0.4.1".
|
returns "v29.3.knots20260508".
|
||||||
|
|
||||||
Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
|
Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
|
||||||
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
|
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
|
||||||
@@ -2256,26 +2565,13 @@ def _get_bitcoind_version() -> str | None:
|
|||||||
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
|
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
|
||||||
"""Format a raw version string from ``bitcoind --version`` for tile display.
|
"""Format a raw version string from ``bitcoind --version`` for tile display.
|
||||||
|
|
||||||
Strips the ``+bip110-vX.Y.Z`` patch suffix so the base version is shown
|
For the BIP110 tile (icon == "bip110") a " (bip110)" tag is appended,
|
||||||
cleanly (e.g. "v29.3.knots20260210+bip110-v0.4.1" → "v29.3.knots20260210").
|
since mainline Bitcoin Knots (29.3.knots20260508+) now includes BIP-110
|
||||||
For the BIP110 tile (icon == "bip110") a " (bip110 vX.Y.Z)" tag is appended
|
and no longer carries a separate ``+bip110-vX.Y.Z`` suffix.
|
||||||
including the patch version.
|
|
||||||
"""
|
"""
|
||||||
# Extract the BIP110 patch version before stripping the suffix
|
display = raw_version
|
||||||
bip110_ver = ""
|
if icon == "bip110" and "(bip110)" not in display.lower():
|
||||||
bip_match = re.search(r"\+bip110-v(\S+)", raw_version)
|
display += " (bip110)"
|
||||||
if bip_match:
|
|
||||||
bip110_ver = bip_match.group(1)
|
|
||||||
|
|
||||||
# Strip the +bip110... suffix for the base Knots version
|
|
||||||
display = re.sub(r"\+bip110\S*", "", raw_version)
|
|
||||||
|
|
||||||
# For BIP110 tile, append both the tag and the patch version
|
|
||||||
if icon == "bip110":
|
|
||||||
if bip110_ver:
|
|
||||||
display += f" (bip110 v{bip110_ver})"
|
|
||||||
elif "(bip110)" not in display.lower():
|
|
||||||
display += " (bip110)"
|
|
||||||
return display
|
return display
|
||||||
|
|
||||||
|
|
||||||
@@ -2343,6 +2639,19 @@ async def api_bitcoin_version():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/bitcoin/bip110")
|
||||||
|
async def api_bitcoin_bip110():
|
||||||
|
"""Return live BIP-110 deployment/signaling status from bitcoin-cli.
|
||||||
|
|
||||||
|
Always returns HTTP 200. When bitcoind is unreachable or the node is mid-IBD
|
||||||
|
the response will contain ``state = "unknown"`` so the UI can render a neutral
|
||||||
|
badge rather than an error toast.
|
||||||
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
status = await loop.run_in_executor(None, _get_bip110_status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/services")
|
@app.get("/api/services")
|
||||||
async def api_services():
|
async def api_services():
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
@@ -2523,6 +2832,8 @@ async def api_services():
|
|||||||
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
||||||
service_data["bitcoin_version"] = btc_ver # backwards compat
|
service_data["bitcoin_version"] = btc_ver # backwards compat
|
||||||
service_data["version"] = btc_ver
|
service_data["version"] = btc_ver
|
||||||
|
if icon == "bip110":
|
||||||
|
service_data["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
|
||||||
return service_data
|
return service_data
|
||||||
|
|
||||||
results = await asyncio.gather(*[get_status(s) for s in services])
|
results = await asyncio.gather(*[get_status(s) for s in services])
|
||||||
@@ -2807,6 +3118,8 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
|||||||
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
||||||
service_detail["bitcoin_version"] = btc_ver # backwards compat
|
service_detail["bitcoin_version"] = btc_ver # backwards compat
|
||||||
service_detail["version"] = btc_ver
|
service_detail["version"] = btc_ver
|
||||||
|
if icon == "bip110":
|
||||||
|
service_detail["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
|
||||||
return service_detail
|
return service_detail
|
||||||
|
|
||||||
|
|
||||||
@@ -2963,6 +3276,35 @@ async def api_ping():
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/service/{unit}/restart")
|
||||||
|
async def api_service_restart(unit: str):
|
||||||
|
cfg = load_config()
|
||||||
|
services = cfg.get("services", [])
|
||||||
|
allowed_units = {
|
||||||
|
str(s.get("unit", "")).strip()
|
||||||
|
for s in services
|
||||||
|
if s.get("unit")
|
||||||
|
}
|
||||||
|
if unit not in allowed_units:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"systemctl", "restart", unit,
|
||||||
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
_, stderr = await proc.communicate()
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to restart service: {exc}")
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
detail = stderr.decode(errors="ignore").strip() or "systemctl restart failed"
|
||||||
|
raise HTTPException(status_code=500, detail=detail)
|
||||||
|
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/reboot")
|
@app.post("/api/reboot")
|
||||||
async def api_reboot():
|
async def api_reboot():
|
||||||
try:
|
try:
|
||||||
@@ -3418,6 +3760,12 @@ async def api_features_toggle(req: FeatureToggleRequest):
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Queue the unit for auto-start once the rebuild succeeds
|
||||||
|
unit_to_start = FEATURE_SERVICE_MAP.get(req.feature)
|
||||||
|
if req.enabled and unit_to_start is not None:
|
||||||
|
with _pending_service_starts_lock:
|
||||||
|
_pending_service_starts.add(unit_to_start)
|
||||||
|
|
||||||
# Start the rebuild service
|
# Start the rebuild service
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
"systemctl", "reset-failed", REBUILD_UNIT,
|
"systemctl", "reset-failed", REBUILD_UNIT,
|
||||||
@@ -3442,6 +3790,23 @@ async def api_rebuild_status(offset: int = 0):
|
|||||||
new_log, new_offset = await loop.run_in_executor(None, _read_rebuild_log, offset)
|
new_log, new_offset = await loop.run_in_executor(None, _read_rebuild_log, offset)
|
||||||
running = status == "RUNNING"
|
running = status == "RUNNING"
|
||||||
result = "pending" if running else status.lower()
|
result = "pending" if running else status.lower()
|
||||||
|
|
||||||
|
# Auto-start any services that were just enabled by a feature toggle
|
||||||
|
if result == "success":
|
||||||
|
with _pending_service_starts_lock:
|
||||||
|
units_to_start = set(_pending_service_starts)
|
||||||
|
_pending_service_starts.clear()
|
||||||
|
for unit in units_to_start:
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"systemctl", "start", unit,
|
||||||
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
await proc.wait()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"running": running,
|
"running": running,
|
||||||
"result": result,
|
"result": result,
|
||||||
@@ -3656,6 +4021,9 @@ async def api_security_reset():
|
|||||||
"/home/free/.ssh",
|
"/home/free/.ssh",
|
||||||
"/var/lib/lnd",
|
"/var/lib/lnd",
|
||||||
"/var/lib/vaultwarden",
|
"/var/lib/vaultwarden",
|
||||||
|
"/etc/nix-bitcoin-secrets",
|
||||||
|
"/home/free/.local/share/Bisq",
|
||||||
|
"/home/free/.bisq",
|
||||||
]
|
]
|
||||||
|
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
@@ -3761,22 +4129,14 @@ async def api_security_reset():
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"write root-password: {exc}")
|
errors.append(f"write root-password: {exc}")
|
||||||
|
|
||||||
# Delete GNOME Keyring files so a fresh keyring is created with the new
|
# Clear only the locked keyring databases, leaving the directory and 'default' pointer intact.
|
||||||
# password on the next GDM login (PAM unlocks it automatically).
|
|
||||||
keyring_dir = "/home/free/.local/share/keyrings"
|
keyring_dir = "/home/free/.local/share/keyrings"
|
||||||
try:
|
keyring_files = glob.glob(os.path.join(keyring_dir, "*.keyring"))
|
||||||
if os.path.isdir(keyring_dir):
|
for kf in keyring_files:
|
||||||
for entry in os.listdir(keyring_dir):
|
try:
|
||||||
entry_path = os.path.join(keyring_dir, entry)
|
os.remove(kf)
|
||||||
try:
|
except OSError as exc:
|
||||||
if os.path.isfile(entry_path) or os.path.islink(entry_path):
|
errors.append(f"keyring wipe: {kf}: {exc}")
|
||||||
os.unlink(entry_path)
|
|
||||||
elif os.path.isdir(entry_path):
|
|
||||||
shutil.rmtree(entry_path, ignore_errors=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"keyring wipe: {exc}")
|
|
||||||
|
|
||||||
# The user performed a full security reset — the banner's purpose is served.
|
# The user performed a full security reset — the banner's purpose is served.
|
||||||
try:
|
try:
|
||||||
@@ -3944,22 +4304,13 @@ async def api_change_password(req: ChangePasswordRequest):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to write secrets file: {exc}")
|
raise HTTPException(status_code=500, detail=f"Failed to write secrets file: {exc}")
|
||||||
|
|
||||||
# Delete GNOME Keyring files so a fresh keyring is created with the new
|
# Clear only the locked keyring databases, leaving the directory and 'default' pointer intact.
|
||||||
# password on the next GDM login (PAM will unlock it automatically).
|
|
||||||
keyring_dir = "/home/free/.local/share/keyrings"
|
keyring_dir = "/home/free/.local/share/keyrings"
|
||||||
try:
|
for kf in glob.glob(os.path.join(keyring_dir, "*.keyring")):
|
||||||
if os.path.isdir(keyring_dir):
|
try:
|
||||||
for entry in os.listdir(keyring_dir):
|
os.remove(kf)
|
||||||
entry_path = os.path.join(keyring_dir, entry)
|
except OSError:
|
||||||
try:
|
pass # Non-fatal: keyring will be re-created on next login regardless
|
||||||
if os.path.isfile(entry_path) or os.path.islink(entry_path):
|
|
||||||
os.unlink(entry_path)
|
|
||||||
elif os.path.isdir(entry_path):
|
|
||||||
shutil.rmtree(entry_path, ignore_errors=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass # Non-fatal: keyring will be re-created on next login regardless
|
|
||||||
|
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
@@ -4436,6 +4787,14 @@ async def _startup_recover_stale_status():
|
|||||||
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
|
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def _startup_migrate_deprecated_features():
|
||||||
|
"""Strip deprecated feature lines (e.g. bip110) from the Hub Managed section
|
||||||
|
of custom.nix so they are never re-written and do not cause stale warnings."""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
await loop.run_in_executor(None, _migrate_strip_deprecated_features)
|
||||||
|
|
||||||
|
|
||||||
async def _background_domain_reachability_checker():
|
async def _background_domain_reachability_checker():
|
||||||
"""Periodically curl configured domains and cache reachability results."""
|
"""Periodically curl configured domains and cache reachability results."""
|
||||||
await asyncio.sleep(_DOMAIN_REACHABILITY_STARTUP_DELAY)
|
await asyncio.sleep(_DOMAIN_REACHABILITY_STARTUP_DELAY)
|
||||||
|
|||||||
@@ -155,6 +155,69 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── BIP-110 status badge (tile + detail modal) ───────────────────── */
|
||||||
|
|
||||||
|
.tile-bip110-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--active {
|
||||||
|
background: rgba(109, 191, 139, 0.18);
|
||||||
|
color: var(--green);
|
||||||
|
border: 1px solid rgba(109, 191, 139, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--locked_in {
|
||||||
|
background: rgba(94, 173, 138, 0.15);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid rgba(94, 173, 138, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--signaling {
|
||||||
|
background: rgba(94, 173, 138, 0.12);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid rgba(94, 173, 138, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--not_signaling {
|
||||||
|
background: rgba(229, 165, 10, 0.12);
|
||||||
|
color: var(--yellow);
|
||||||
|
border: 1px solid rgba(229, 165, 10, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--unsupported {
|
||||||
|
background: rgba(94, 122, 106, 0.12);
|
||||||
|
color: var(--grey);
|
||||||
|
border: 1px solid rgba(94, 122, 106, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--unknown {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-dim);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bip110-status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bip110-source-label {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Service detail modal sections ───────────────────────────────── */
|
/* ── Service detail modal sections ───────────────────────────────── */
|
||||||
|
|
||||||
.svc-detail-section {
|
.svc-detail-section {
|
||||||
@@ -353,6 +416,47 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background-color: #d97706;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover:not(:disabled) {
|
||||||
|
background-color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svc-detail-restart-section {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svc-detail-restart-btn {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svc-detail-restart-result {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svc-detail-restart-result.success {
|
||||||
|
background-color: rgba(109, 191, 139, 0.12);
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
color: var(--green);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svc-detail-restart-result.error {
|
||||||
|
background-color: rgba(239, 68, 68, 0.12);
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #f87171;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── Desktop launch buttons ──────────────────────────────────────── */
|
/* ── Desktop launch buttons ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
|||||||
@@ -413,16 +413,11 @@ function handleFeatureToggle(feat, newEnabled) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conflictNames.length > 0) {
|
if (feat.id === "bitcoin-core") {
|
||||||
var confirmMsg;
|
var confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will replace Bitcoin Knots + BIP110 as the active node. Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
|
||||||
if (feat.id === "bip110") {
|
|
||||||
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Knots + BIP110 will disable Bitcoin Core (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
|
|
||||||
} else if (feat.id === "bitcoin-core") {
|
|
||||||
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will disable Bitcoin Knots + BIP110 (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
|
|
||||||
} else {
|
|
||||||
confirmMsg = "This will disable " + conflictNames.join(", ") + ". Continue?";
|
|
||||||
}
|
|
||||||
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
|
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
|
||||||
|
} else if (conflictNames.length > 0) {
|
||||||
|
openFeatureConfirm("This will disable " + conflictNames.join(", ") + ". Continue?", proceedAfterConflictCheck);
|
||||||
} else {
|
} else {
|
||||||
proceedAfterConflictCheck();
|
proceedAfterConflictCheck();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,3 +60,17 @@ async function apiFetch(path, options) {
|
|||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── BIP-110 badge state config ────────────────────────────────────
|
||||||
|
// Shared lookup used by tiles.js and service-detail.js.
|
||||||
|
// Keys match the "state" values returned by /api/bitcoin/bip110.
|
||||||
|
|
||||||
|
var BIP110_BADGE_CONFIG = {
|
||||||
|
active: { cls: 'tile-bip110-badge--active', label: 'Active', title: 'BIP-110 is active on this node' },
|
||||||
|
locked_in: { cls: 'tile-bip110-badge--locked_in', label: 'Locked In', title: 'BIP-110 is locked in and will activate shortly' },
|
||||||
|
signaling: { cls: 'tile-bip110-badge--signaling', label: 'Signaling', title: 'Node is signaling readiness for BIP-110' },
|
||||||
|
not_signaling: { cls: 'tile-bip110-badge--not_signaling',label: 'Not Signaling', title: 'Node supports BIP-110 but is not signaling this period' },
|
||||||
|
unsupported: { cls: 'tile-bip110-badge--unsupported', label: 'Not Supported', title: 'This node build does not include BIP-110' },
|
||||||
|
unknown: { cls: 'tile-bip110-badge--unknown', label: '\u2014', title: 'Status unavailable (node syncing or RPC not ready)' }
|
||||||
|
};
|
||||||
|
|||||||
@@ -51,19 +51,26 @@ async function pollRebuildStatus() {
|
|||||||
if (data.running) return;
|
if (data.running) return;
|
||||||
_rebuildFinished = true;
|
_rebuildFinished = true;
|
||||||
stopRebuildPoll();
|
stopRebuildPoll();
|
||||||
onRebuildDone(data.result === "success");
|
if (data.result === "reboot_required") {
|
||||||
|
onRebuildDone("reboot_required");
|
||||||
|
} else {
|
||||||
|
onRebuildDone(data.result === "success");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; }
|
if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRebuildDone(success) {
|
function onRebuildDone(result) {
|
||||||
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
|
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
|
||||||
if ($rebuildClose) $rebuildClose.disabled = false;
|
if ($rebuildClose) $rebuildClose.disabled = false;
|
||||||
if (success) {
|
if (result === true) {
|
||||||
if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done";
|
if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done";
|
||||||
// Auto-reload the page after a short delay so tiles and toggles reflect the new state
|
// Auto-reload the page after a short delay so tiles and toggles reflect the new state
|
||||||
setTimeout(function() { window.location.reload(); }, 1200);
|
setTimeout(function() { window.location.reload(); }, 1200);
|
||||||
|
} else if (result === "reboot_required") {
|
||||||
|
if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done — reboot required";
|
||||||
|
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
|
||||||
} else {
|
} else {
|
||||||
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong";
|
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong";
|
||||||
if ($rebuildSave) $rebuildSave.style.display = "inline-flex";
|
if ($rebuildSave) $rebuildSave.style.display = "inline-flex";
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ function _renderCredsHtml(credentials, unit) {
|
|||||||
for (var i = 0; i < credentials.length; i++) {
|
for (var i = 0; i < credentials.length; i++) {
|
||||||
var cred = credentials[i];
|
var cred = credentials[i];
|
||||||
var id = "cred-" + Math.random().toString(36).substring(2, 8);
|
var id = "cred-" + Math.random().toString(36).substring(2, 8);
|
||||||
var displayValue = linkify(cred.value);
|
|
||||||
var qrBlock = "";
|
var qrBlock = "";
|
||||||
if (cred.qrcode) {
|
if (cred.qrcode) {
|
||||||
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>';
|
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>';
|
||||||
}
|
}
|
||||||
|
// If qronly, render the label + QR block only — skip value and copy button
|
||||||
|
if (cred.qronly) {
|
||||||
|
html += '<div class="creds-row"><div class="creds-label">' + escHtml(cred.label) + '</div>' + qrBlock + '</div>';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var displayValue = linkify(cred.value);
|
||||||
html += '<div class="creds-row"><div class="creds-label">' + escHtml(cred.label) + '</div>' + qrBlock + '<div class="creds-value-wrap"><div class="creds-value" id="' + id + '">' + displayValue + '</div><button class="creds-copy-btn" data-target="' + id + '">Copy</button></div></div>';
|
html += '<div class="creds-row"><div class="creds-label">' + escHtml(cred.label) + '</div>' + qrBlock + '<div class="creds-value-wrap"><div class="creds-value" id="' + id + '">' + displayValue + '</div><button class="creds-copy-btn" data-target="' + id + '">Copy</button></div></div>';
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
@@ -102,6 +107,21 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
|
// Section B2: BIP-110 live status (bip110 tile only)
|
||||||
|
if (icon === 'bip110' && data.bip110) {
|
||||||
|
var bip110 = data.bip110;
|
||||||
|
var bip110State = bip110.state || 'unknown';
|
||||||
|
var bip110Cfg = BIP110_BADGE_CONFIG[bip110State] || BIP110_BADGE_CONFIG.unknown;
|
||||||
|
var bip110Source = bip110.source ? ' <span class="bip110-source-label">(source: ' + escHtml(bip110.source) + ')</span>' : '';
|
||||||
|
html += '<div class="svc-detail-section">' +
|
||||||
|
'<div class="svc-detail-section-title">BIP-110 Deployment Status</div>' +
|
||||||
|
'<div class="bip110-status-row">' +
|
||||||
|
'<span class="tile-bip110-badge ' + bip110Cfg.cls + '" title="' + escHtml(bip110Cfg.title) + '">' + escHtml(bip110Cfg.label) + '</span>' +
|
||||||
|
bip110Source +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// Section C: Domain diagnostics (domain services)
|
// Section C: Domain diagnostics (domain services)
|
||||||
if (data.needs_domain) {
|
if (data.needs_domain) {
|
||||||
var steps = data.domain_check_steps || [];
|
var steps = data.domain_check_steps || [];
|
||||||
@@ -237,7 +257,7 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
|
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
|
||||||
|
|
||||||
// Section title: use a more specific label for mutually-exclusive Bitcoin node features
|
// Section title: use a more specific label for mutually-exclusive Bitcoin node features
|
||||||
var addonSectionTitle = (feat.id === "bip110" || feat.id === "bitcoin-core")
|
var addonSectionTitle = (feat.id === "bitcoin-core")
|
||||||
? "\u20BF Bitcoin Node Selection"
|
? "\u20BF Bitcoin Node Selection"
|
||||||
: "\uD83D\uDD27 Addon Feature";
|
: "\uD83D\uDD27 Addon Feature";
|
||||||
|
|
||||||
@@ -270,6 +290,15 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") {
|
||||||
|
html += '<div class="svc-detail-section svc-detail-restart-section">' +
|
||||||
|
'<div class="svc-detail-section-title">Troubleshooting</div>' +
|
||||||
|
'<p class="svc-detail-desc">If you\'re experiencing issues with this service, try restarting it.</p>' +
|
||||||
|
'<button class="btn btn-warning svc-detail-restart-btn" id="svc-detail-restart-btn">🔄 Restart Service</button>' +
|
||||||
|
'<div class="svc-detail-restart-result" id="svc-detail-restart-result"></div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
$credsBody.innerHTML = html;
|
$credsBody.innerHTML = html;
|
||||||
_attachCopyHandlers($credsBody);
|
_attachCopyHandlers($credsBody);
|
||||||
|
|
||||||
@@ -296,6 +325,34 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var restartBtn = document.getElementById("svc-detail-restart-btn");
|
||||||
|
var restartResult = document.getElementById("svc-detail-restart-result");
|
||||||
|
if (restartBtn && restartResult) {
|
||||||
|
var RESTART_REFRESH_DELAY_MS = 3000;
|
||||||
|
restartBtn.addEventListener("click", async function() {
|
||||||
|
restartBtn.disabled = true;
|
||||||
|
restartBtn.textContent = "Restarting…";
|
||||||
|
restartResult.className = "svc-detail-restart-result";
|
||||||
|
restartResult.textContent = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiFetch("/api/service/" + encodeURIComponent(unit) + "/restart", { method: "POST" });
|
||||||
|
restartResult.classList.add("success");
|
||||||
|
restartResult.textContent = "✅ Service restarted successfully.";
|
||||||
|
restartBtn.disabled = false;
|
||||||
|
restartBtn.textContent = "🔄 Restart Service";
|
||||||
|
setTimeout(function() {
|
||||||
|
openServiceDetailModal(unit, name, icon);
|
||||||
|
}, RESTART_REFRESH_DELAY_MS);
|
||||||
|
} catch (e) {
|
||||||
|
restartResult.classList.add("error");
|
||||||
|
restartResult.textContent = e && e.message ? e.message : "Failed to restart service. Please check service logs and try again.";
|
||||||
|
restartBtn.disabled = false;
|
||||||
|
restartBtn.textContent = "🔄 Restart Service";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Configure / Reconfigure Domain buttons (for non-feature services that need a domain)
|
// Configure / Reconfigure Domain buttons (for non-feature services that need a domain)
|
||||||
var configDomainBtn = document.getElementById("svc-detail-config-domain-btn");
|
var configDomainBtn = document.getElementById("svc-detail-config-domain-btn");
|
||||||
var reconfigDomainBtn = document.getElementById("svc-detail-reconfig-domain-btn");
|
var reconfigDomainBtn = document.getElementById("svc-detail-reconfig-domain-btn");
|
||||||
@@ -497,7 +554,7 @@ function openSystemChangePasswordModal(unit, name, icon) {
|
|||||||
'<input class="matrix-form-input" type="password" id="sys-chpw-confirm" placeholder="Confirm new password" autocomplete="new-password">' +
|
'<input class="matrix-form-input" type="password" id="sys-chpw-confirm" placeholder="Confirm new password" autocomplete="new-password">' +
|
||||||
'<button type="button" class="pw-toggle-btn" id="sys-chpw-confirm-toggle" aria-label="Toggle password visibility">👁</button>' +
|
'<button type="button" class="pw-toggle-btn" id="sys-chpw-confirm-toggle" aria-label="Toggle password visibility">👁</button>' +
|
||||||
'</div></div>' +
|
'</div></div>' +
|
||||||
'<div class="pw-credentials-note">⚠ This will change both your desktop login and Hub login password. After changing, your updated password will appear in the System Passwords credentials tile. Make sure to remember it — you will need it to sign back into the Hub.</div>' +
|
'<div class="pw-credentials-note">⚠ This will change both your desktop login and Hub login password. After changing, your updated password will appear in the System Passwords credentials tile.</div>' +
|
||||||
'<div class="matrix-form-actions">' +
|
'<div class="matrix-form-actions">' +
|
||||||
'<button class="matrix-form-back" id="sys-chpw-back-btn">← Back</button>' +
|
'<button class="matrix-form-back" id="sys-chpw-back-btn">← Back</button>' +
|
||||||
'<button class="matrix-form-submit" id="sys-chpw-submit-btn">Change Password</button>' +
|
'<button class="matrix-form-submit" id="sys-chpw-submit-btn">Change Password</button>' +
|
||||||
|
|||||||
@@ -500,9 +500,8 @@ function renderBackupReady(drives) {
|
|||||||
'<div class="support-steps-title">What gets backed up</div>',
|
'<div class="support-steps-title">What gets backed up</div>',
|
||||||
'<ol class="support-backup-steps">',
|
'<ol class="support-backup-steps">',
|
||||||
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
|
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
|
||||||
'<li>Bitcoin & Lightning wallet data (<code>/var/lib/lnd</code>)</li>',
|
|
||||||
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
|
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
|
||||||
'<li>Domain configurations (<code>/var/lib/domains</code>)</li>',
|
'<li>System service data (<code>/var/lib</code>) including Vaultwarden, bitcoind, LND, sovran-hub, domains, and secrets</li>',
|
||||||
'<li>Home directory (<code>/home</code>)</li>',
|
'<li>Home directory (<code>/home</code>)</li>',
|
||||||
'</ol>',
|
'</ol>',
|
||||||
'</div>',
|
'</div>',
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
// Keyed by tileId: { progress: float, timestamp: ms }
|
// Keyed by tileId: { progress: float, timestamp: ms }
|
||||||
var _btcSyncPrev = {};
|
var _btcSyncPrev = {};
|
||||||
|
|
||||||
|
// ── BIP-110 badge helper ──────────────────────────────────────────
|
||||||
|
|
||||||
|
function _renderBip110Badge(bip110) {
|
||||||
|
if (!bip110) return '';
|
||||||
|
var state = bip110.state || 'unknown';
|
||||||
|
var cfg = BIP110_BADGE_CONFIG[state] || BIP110_BADGE_CONFIG.unknown;
|
||||||
|
return '<div class="tile-bip110-badge ' + cfg.cls + '" title="' + escHtml(cfg.title) + '">' + escHtml(cfg.label) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _firstElementFromHtml(html) {
|
||||||
|
var tmp = document.createElement("div");
|
||||||
|
tmp.innerHTML = html;
|
||||||
|
return tmp.firstElementChild || null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Render: initial build ─────────────────────────────────────────
|
// ── Render: initial build ─────────────────────────────────────────
|
||||||
|
|
||||||
function buildTiles(services, categoryLabels) {
|
function buildTiles(services, categoryLabels) {
|
||||||
@@ -165,7 +180,8 @@ function buildTile(svc) {
|
|||||||
|
|
||||||
var ver = svc.version || svc.bitcoin_version || '';
|
var ver = svc.version || svc.bitcoin_version || '';
|
||||||
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
|
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
|
||||||
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
|
var bip110Badge = (svc.icon === 'bip110') ? _renderBip110Badge(svc.bip110) : '';
|
||||||
|
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + bip110Badge + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
|
||||||
|
|
||||||
tile.style.cursor = "pointer";
|
tile.style.cursor = "pointer";
|
||||||
tile.addEventListener("click", function() {
|
tile.addEventListener("click", function() {
|
||||||
@@ -265,6 +281,23 @@ function updateTiles(services) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Update BIP-110 badge for bip110 tiles
|
||||||
|
if (svc.icon === 'bip110') {
|
||||||
|
var badgeHtml = _renderBip110Badge(svc.bip110);
|
||||||
|
var badgeEl = tile.querySelector(".tile-bip110-badge");
|
||||||
|
if (badgeEl) {
|
||||||
|
// Replace existing badge in-place
|
||||||
|
var newBadge = _firstElementFromHtml(badgeHtml);
|
||||||
|
if (newBadge) { badgeEl.replaceWith(newBadge); } else { badgeEl.remove(); }
|
||||||
|
} else if (badgeHtml) {
|
||||||
|
// Insert badge after version label (or after tile-name if no version)
|
||||||
|
var anchorEl = tile.querySelector(".tile-version") || tile.querySelector(".tile-name");
|
||||||
|
if (anchorEl) {
|
||||||
|
var newBadgeEl = _firstElementFromHtml(badgeHtml);
|
||||||
|
if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,14 +111,20 @@ async function pollUpdateStatus() {
|
|||||||
if (data.log) appendLog(data.log);
|
if (data.log) appendLog(data.log);
|
||||||
_updateLogOffset = data.offset;
|
_updateLogOffset = data.offset;
|
||||||
}
|
}
|
||||||
if (data.result === "success") {
|
if (data.result === "reboot_required") {
|
||||||
|
appendLog("[Server restarted — update completed, reboot required.]\n");
|
||||||
|
} else if (data.result === "success") {
|
||||||
appendLog("[Server restarted — update completed successfully.]\n");
|
appendLog("[Server restarted — update completed successfully.]\n");
|
||||||
} else {
|
} else {
|
||||||
appendLog("[Server restarted — update encountered an error.]\n");
|
appendLog("[Server restarted — update encountered an error.]\n");
|
||||||
}
|
}
|
||||||
_updateFinished = true;
|
_updateFinished = true;
|
||||||
stopUpdatePoll();
|
stopUpdatePoll();
|
||||||
onUpdateDone(data.result === "success");
|
if (data.result === "reboot_required") {
|
||||||
|
onUpdateDone("reboot_required");
|
||||||
|
} else {
|
||||||
|
onUpdateDone(data.result === "success");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appendLog("[Server reconnected]\n");
|
appendLog("[Server reconnected]\n");
|
||||||
@@ -129,19 +135,27 @@ async function pollUpdateStatus() {
|
|||||||
if (data.running) return;
|
if (data.running) return;
|
||||||
_updateFinished = true;
|
_updateFinished = true;
|
||||||
stopUpdatePoll();
|
stopUpdatePoll();
|
||||||
if (data.result === "success") onUpdateDone(true);
|
if (data.result === "reboot_required") {
|
||||||
else onUpdateDone(false);
|
onUpdateDone("reboot_required");
|
||||||
|
} else if (data.result === "success") {
|
||||||
|
onUpdateDone(true);
|
||||||
|
} else {
|
||||||
|
onUpdateDone(false);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!_serverWasDown) { _serverWasDown = true; appendLog("\n[Server restarting — waiting for it to come back…]\n"); if ($modalStatus) $modalStatus.textContent = "Server restarting…"; }
|
if (!_serverWasDown) { _serverWasDown = true; appendLog("\n[Server restarting — waiting for it to come back…]\n"); if ($modalStatus) $modalStatus.textContent = "Server restarting…"; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUpdateDone(success) {
|
function onUpdateDone(result) {
|
||||||
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
||||||
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
||||||
if (success) {
|
if (result === true) {
|
||||||
if ($modalStatus) $modalStatus.textContent = "✓ Update complete";
|
if ($modalStatus) $modalStatus.textContent = "✓ Update complete";
|
||||||
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
||||||
|
} else if (result === "reboot_required") {
|
||||||
|
if ($modalStatus) $modalStatus.textContent = "✓ Update complete — reboot required";
|
||||||
|
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
||||||
} else {
|
} else {
|
||||||
if ($modalStatus) $modalStatus.textContent = "✗ Update failed";
|
if ($modalStatus) $modalStatus.textContent = "✗ Update failed";
|
||||||
if ($btnSave) $btnSave.style.display = "inline-flex";
|
if ($btnSave) $btnSave.style.display = "inline-flex";
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const DOMAIN_DEFS = [
|
|||||||
var _currentStep = 1;
|
var _currentStep = 1;
|
||||||
var _servicesData = null;
|
var _servicesData = null;
|
||||||
var _domainsData = null;
|
var _domainsData = null;
|
||||||
|
var _migrationOccurred = false;
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -65,6 +66,48 @@ function setStatus(elId, msg, type) {
|
|||||||
el.className = "onboarding-save-status" + (type ? " onboarding-save-status--" + type : "");
|
el.className = "onboarding-save-status" + (type ? " onboarding-save-status--" + type : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateStep5Checklist() {
|
||||||
|
var checklist = document.getElementById("onboarding-checklist");
|
||||||
|
if (!checklist) return;
|
||||||
|
var existing = document.getElementById("onboarding-migration-check");
|
||||||
|
if (_migrationOccurred) {
|
||||||
|
if (!existing) {
|
||||||
|
var li = document.createElement("li");
|
||||||
|
li.id = "onboarding-migration-check";
|
||||||
|
li.textContent = "✅ Migration password noted";
|
||||||
|
checklist.appendChild(li);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existing) existing.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMigrationStep(password) {
|
||||||
|
for (var i = 1; i <= TOTAL_STEPS; i++) {
|
||||||
|
var panel = document.getElementById("step-" + i);
|
||||||
|
if (panel) panel.style.display = "none";
|
||||||
|
}
|
||||||
|
var migrationPanel = document.getElementById("step-migration");
|
||||||
|
if (migrationPanel) migrationPanel.style.display = "";
|
||||||
|
var pw = document.getElementById("migration-password-value");
|
||||||
|
if (pw) pw.textContent = password || "";
|
||||||
|
var progressBar = document.getElementById("onboarding-progress-bar");
|
||||||
|
if (progressBar) progressBar.style.display = "none";
|
||||||
|
var nav = document.getElementById("onboarding-steps-nav");
|
||||||
|
if (nav) nav.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStep1FromMigration() {
|
||||||
|
var migrationPanel = document.getElementById("step-migration");
|
||||||
|
if (migrationPanel) migrationPanel.style.display = "none";
|
||||||
|
var progressBar = document.getElementById("onboarding-progress-bar");
|
||||||
|
if (progressBar) progressBar.style.display = "";
|
||||||
|
var nav = document.getElementById("onboarding-steps-nav");
|
||||||
|
if (nav) nav.style.display = "";
|
||||||
|
showStep(1);
|
||||||
|
loadStep1();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Progress / step navigation ────────────────────────────────────
|
// ── Progress / step navigation ────────────────────────────────────
|
||||||
|
|
||||||
function updateProgress(step) {
|
function updateProgress(step) {
|
||||||
@@ -514,7 +557,7 @@ async function loadStep4() {
|
|||||||
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward to</th><th>Purpose</th></tr></thead>';
|
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward to</th><th>Purpose</th></tr></thead>';
|
||||||
html += '<tbody>';
|
html += '<tbody>';
|
||||||
html += '<tr><td class="port-req-port">7881</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit WebRTC signalling</td></tr>';
|
html += '<tr><td class="port-req-port">7881</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit WebRTC signalling</td></tr>';
|
||||||
html += '<tr><td class="port-req-port">7882–7894</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media streams</td></tr>';
|
html += '<tr><td class="port-req-port">7882</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media (UDP mux)</td></tr>';
|
||||||
html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>';
|
html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>';
|
||||||
html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>';
|
html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>';
|
||||||
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 += '<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>';
|
||||||
@@ -566,6 +609,23 @@ async function completeOnboarding() {
|
|||||||
// ── Event wiring ──────────────────────────────────────────────────
|
// ── Event wiring ──────────────────────────────────────────────────
|
||||||
|
|
||||||
function wireNavButtons() {
|
function wireNavButtons() {
|
||||||
|
var migrationContinue = document.getElementById("migration-password-continue");
|
||||||
|
if (migrationContinue) migrationContinue.addEventListener("click", async function() {
|
||||||
|
migrationContinue.disabled = true;
|
||||||
|
migrationContinue.textContent = "Continuing…";
|
||||||
|
setStatus("migration-password-status", "Saving acknowledgement…", "info");
|
||||||
|
try {
|
||||||
|
await apiFetch("/api/migration/password-acknowledge", { method: "POST" });
|
||||||
|
_migrationOccurred = true;
|
||||||
|
updateStep5Checklist();
|
||||||
|
showStep1FromMigration();
|
||||||
|
} catch (err) {
|
||||||
|
setStatus("migration-password-status", "⚠ " + err.message, "error");
|
||||||
|
migrationContinue.disabled = false;
|
||||||
|
migrationContinue.textContent = "I've written it down — Continue →";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Step 1 → next
|
// Step 1 → next
|
||||||
var s1next = document.getElementById("step-1-next");
|
var s1next = document.getElementById("step-1-next");
|
||||||
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
|
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
|
||||||
@@ -627,6 +687,17 @@ document.addEventListener("DOMContentLoaded", async function() {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
wireNavButtons();
|
wireNavButtons();
|
||||||
updateProgress(1);
|
|
||||||
|
try {
|
||||||
|
var migration = await apiFetch("/api/migration/password-status");
|
||||||
|
if (migration && migration.pending) {
|
||||||
|
updateStep5Checklist();
|
||||||
|
showMigrationStep(migration.password || "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
updateStep5Checklist();
|
||||||
|
showStep(1);
|
||||||
loadStep1();
|
loadStep1();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sovran_SystemsOS Hub</title>
|
<title>Sovran_SystemsOS Hub</title>
|
||||||
<link rel="stylesheet" href="/static/css/base.css" />
|
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/buttons.css" />
|
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/header.css" />
|
<link rel="stylesheet" href="/static/css/header.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/layout.css" />
|
<link rel="stylesheet" href="/static/css/layout.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/tiles.css" />
|
<link rel="stylesheet" href="/static/css/tiles.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/modals.css" />
|
<link rel="stylesheet" href="/static/css/modals.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/features.css" />
|
<link rel="stylesheet" href="/static/css/features.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/onboarding.css" />
|
<link rel="stylesheet" href="/static/css/onboarding.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/support.css" />
|
<link rel="stylesheet" href="/static/css/support.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/domain-setup.css" />
|
<link rel="stylesheet" href="/static/css/domain-setup.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/security.css" />
|
<link rel="stylesheet" href="/static/css/security.css?v={{ asset_version }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
<div class="upgrade-info-box">
|
<div class="upgrade-info-box">
|
||||||
<p class="upgrade-info-title">⚠ What you should know:</p>
|
<p class="upgrade-info-title">⚠ What you should know:</p>
|
||||||
<ul class="upgrade-info-list">
|
<ul class="upgrade-info-list">
|
||||||
<li>You will need to purchase domains for your services (we recommend <a href="https://njal.la" target="_blank" rel="noopener noreferrer">njal.la</a>)</li>
|
<li>You will need to purchase domains for your services — <a href="https://njal.la" target="_blank" rel="noopener noreferrer">Njal.la</a> is the only supported domain provider</li>
|
||||||
<li>Some services require ports to be opened on your router</li>
|
<li>Some services require ports to be opened on your router</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,16 +263,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/constants.js"></script>
|
<script src="/static/js/constants.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/state.js"></script>
|
<script src="/static/js/state.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/helpers.js"></script>
|
<script src="/static/js/helpers.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/tiles.js"></script>
|
<script src="/static/js/tiles.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/service-detail.js"></script>
|
<script src="/static/js/service-detail.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/support.js"></script>
|
<script src="/static/js/support.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/update.js"></script>
|
<script src="/static/js/update.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/rebuild.js"></script>
|
<script src="/static/js/rebuild.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/features.js"></script>
|
<script src="/static/js/features.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/security.js"></script>
|
<script src="/static/js/security.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/events.js"></script>
|
<script src="/static/js/events.js?v={{ asset_version }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sovran Hub — Login</title>
|
<title>Sovran Hub — Login</title>
|
||||||
<link rel="stylesheet" href="/static/css/base.css" />
|
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/buttons.css" />
|
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-wrapper">
|
<div class="login-wrapper">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<div class="onboarding-shell">
|
<div class="onboarding-shell">
|
||||||
|
|
||||||
<!-- Progress bar -->
|
<!-- Progress bar -->
|
||||||
<div class="onboarding-progress-bar">
|
<div class="onboarding-progress-bar" id="onboarding-progress-bar">
|
||||||
<div class="onboarding-progress-fill" id="onboarding-progress-fill"></div>
|
<div class="onboarding-progress-fill" id="onboarding-progress-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,6 +41,33 @@
|
|||||||
<!-- Step panels -->
|
<!-- Step panels -->
|
||||||
<div class="onboarding-panel-wrap">
|
<div class="onboarding-panel-wrap">
|
||||||
|
|
||||||
|
<!-- ── Migration Password Gate (pre-step) ── -->
|
||||||
|
<div class="onboarding-panel" id="step-migration" style="display:none">
|
||||||
|
<div class="onboarding-hero">
|
||||||
|
<div class="onboarding-logo">🔐</div>
|
||||||
|
<h1 class="onboarding-title">Your system has been migrated to Sovran_SystemsOS</h1>
|
||||||
|
<p class="onboarding-subtitle">Important password update required</p>
|
||||||
|
</div>
|
||||||
|
<div class="onboarding-card">
|
||||||
|
<p class="onboarding-body-text" style="text-align:center; margin-bottom:4px;">
|
||||||
|
Your new login password is:
|
||||||
|
</p>
|
||||||
|
<div id="migration-password-value" style="font-family:monospace; font-size:1.35rem; font-weight:700; color:var(--text-primary); background:rgba(109, 191, 139, 0.10); border:1.5px solid rgba(109, 191, 139, 0.35); border-radius:8px; padding:14px 24px; letter-spacing:0.04em; text-align:center; word-break:break-all; margin-bottom:8px;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="padding:10px 14px; background-color:rgba(229, 165, 10, 0.1); border:1px solid rgba(229, 165, 10, 0.35); border-radius:8px; font-size:0.92rem; color:var(--yellow); line-height:1.55;">
|
||||||
|
⚠ Write this password down! You will need it to log in next time. This is also your Sovran Hub login password.
|
||||||
|
</div>
|
||||||
|
<div id="migration-password-status" class="onboarding-save-status" style="margin-top:8px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="onboarding-footer">
|
||||||
|
<div></div>
|
||||||
|
<button class="btn btn-primary" id="migration-password-continue">
|
||||||
|
I've written it down — Continue →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Step 1: Welcome ── -->
|
<!-- ── Step 1: Welcome ── -->
|
||||||
<div class="onboarding-panel" id="step-1">
|
<div class="onboarding-panel" id="step-1">
|
||||||
<div class="onboarding-hero">
|
<div class="onboarding-hero">
|
||||||
@@ -170,4 +197,4 @@
|
|||||||
|
|
||||||
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
|
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
|
||||||
|
def _install_web_stubs():
|
||||||
|
if "fastapi" in sys.modules:
|
||||||
|
return
|
||||||
|
|
||||||
|
class _HTTPException(Exception):
|
||||||
|
def __init__(self, status_code=None, detail=None):
|
||||||
|
super().__init__(detail)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.detail = detail
|
||||||
|
|
||||||
|
class _FastAPI:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def mount(self, *args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_middleware(self, *args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __getattr__(self, _name):
|
||||||
|
def _decorator_factory(*args, **kwargs):
|
||||||
|
def _decorator(func):
|
||||||
|
return func
|
||||||
|
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
return _decorator_factory
|
||||||
|
|
||||||
|
class _BaseModel:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _StaticFiles:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _Jinja2Templates:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _BaseHTTPMiddleware:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fastapi_module = types.ModuleType("fastapi")
|
||||||
|
fastapi_module.FastAPI = _FastAPI
|
||||||
|
fastapi_module.HTTPException = _HTTPException
|
||||||
|
sys.modules["fastapi"] = fastapi_module
|
||||||
|
|
||||||
|
responses_module = types.ModuleType("fastapi.responses")
|
||||||
|
responses_module.HTMLResponse = object
|
||||||
|
responses_module.JSONResponse = object
|
||||||
|
responses_module.RedirectResponse = object
|
||||||
|
sys.modules["fastapi.responses"] = responses_module
|
||||||
|
|
||||||
|
staticfiles_module = types.ModuleType("fastapi.staticfiles")
|
||||||
|
staticfiles_module.StaticFiles = _StaticFiles
|
||||||
|
sys.modules["fastapi.staticfiles"] = staticfiles_module
|
||||||
|
|
||||||
|
templating_module = types.ModuleType("fastapi.templating")
|
||||||
|
templating_module.Jinja2Templates = _Jinja2Templates
|
||||||
|
sys.modules["fastapi.templating"] = templating_module
|
||||||
|
|
||||||
|
requests_module = types.ModuleType("fastapi.requests")
|
||||||
|
requests_module.Request = object
|
||||||
|
sys.modules["fastapi.requests"] = requests_module
|
||||||
|
|
||||||
|
pydantic_module = types.ModuleType("pydantic")
|
||||||
|
pydantic_module.BaseModel = _BaseModel
|
||||||
|
sys.modules["pydantic"] = pydantic_module
|
||||||
|
|
||||||
|
starlette_base_module = types.ModuleType("starlette.middleware.base")
|
||||||
|
starlette_base_module.BaseHTTPMiddleware = _BaseHTTPMiddleware
|
||||||
|
sys.modules["starlette.middleware.base"] = starlette_base_module
|
||||||
|
|
||||||
|
starlette_middleware_module = types.ModuleType("starlette.middleware")
|
||||||
|
starlette_middleware_module.base = starlette_base_module
|
||||||
|
sys.modules["starlette.middleware"] = starlette_middleware_module
|
||||||
|
|
||||||
|
starlette_module = types.ModuleType("starlette")
|
||||||
|
starlette_module.middleware = starlette_middleware_module
|
||||||
|
sys.modules["starlette"] = starlette_module
|
||||||
|
|
||||||
|
|
||||||
|
_install_web_stubs()
|
||||||
|
from sovran_systemsos_web import server
|
||||||
|
|
||||||
|
|
||||||
|
class Bip110StatusTests(unittest.TestCase):
|
||||||
|
def _status(self, deploy_info, net_info):
|
||||||
|
with patch.object(server, "_get_bitcoin_deployment_info", return_value=deploy_info), patch.object(
|
||||||
|
server, "_get_bitcoin_version_info", return_value=net_info
|
||||||
|
):
|
||||||
|
return server._get_bip110_status()
|
||||||
|
|
||||||
|
def test_started_reduced_data_reports_signaling(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {
|
||||||
|
"reduced_data": {
|
||||||
|
"type": "bip9",
|
||||||
|
"active": False,
|
||||||
|
"bip9": {
|
||||||
|
"bit": 4,
|
||||||
|
"status": "started",
|
||||||
|
"statistics": {"elapsed": 833, "count": 4, "threshold": 1109},
|
||||||
|
"signalling": "--#--",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
{"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_active_reduced_data_reports_active(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {"reduced_data": {"active": True, "bip9": {"bit": 4, "status": "active"}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
|
||||||
|
self.assertEqual(result["state"], "active")
|
||||||
|
self.assertTrue(result["supported"])
|
||||||
|
self.assertTrue(result["signaling"])
|
||||||
|
self.assertEqual(result["source"], "getdeploymentinfo")
|
||||||
|
|
||||||
|
def test_locked_in_reduced_data_reports_locked_in(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {"reduced_data": {"active": False, "bip9": {"bit": 4, "status": "locked_in"}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
|
||||||
|
self.assertEqual(result["state"], "locked_in")
|
||||||
|
self.assertTrue(result["supported"])
|
||||||
|
self.assertTrue(result["signaling"])
|
||||||
|
self.assertEqual(result["source"], "getdeploymentinfo")
|
||||||
|
|
||||||
|
def test_no_bip110_deployment_and_plain_subversion_reports_unsupported(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {
|
||||||
|
"taproot": {"type": "bip9", "active": True, "bip9": {"bit": 2, "status": "active"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:27.0.0/"})
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
{"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_node_unreachable_reports_unknown(self):
|
||||||
|
result = self._status(None, None)
|
||||||
|
self.assertEqual(result, {"supported": False, "signaling": False, "state": "unknown", "source": "none"})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
@@ -0,0 +1,52 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#153126"/>
|
||||||
|
<stop offset="55%" stop-color="#0F241B"/>
|
||||||
|
<stop offset="100%" stop-color="#091C14"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#42F39A"/>
|
||||||
|
<stop offset="45%" stop-color="#28D978"/>
|
||||||
|
<stop offset="100%" stop-color="#1AA45D"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#27C86F"/>
|
||||||
|
<stop offset="100%" stop-color="#157E49"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feOffset dx="0" dy="2"/>
|
||||||
|
<feGaussianBlur stdDeviation="5" result="blur"/>
|
||||||
|
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 .18 0"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
|
||||||
|
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
|
||||||
|
fill="none" stroke="rgba(255,255,255,0.08)"/>
|
||||||
|
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
|
||||||
|
fill="none" filter="url(#innerShade)"/>
|
||||||
|
|
||||||
|
<path d="M128 32 A96 96 0 1 1 58 196"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#outerArc)"
|
||||||
|
stroke-width="12"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<path d="M128 56 A72 72 0 1 1 76 178"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#innerArc)"
|
||||||
|
stroke-width="10"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
|
||||||
|
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
+33
-6
@@ -3,7 +3,6 @@
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./modules/modules.nix
|
./modules/modules.nix
|
||||||
./iso/branding.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# ── Boot ────────────────────────────────────────────────────
|
# ── Boot ────────────────────────────────────────────────────
|
||||||
@@ -11,6 +10,8 @@
|
|||||||
boot.loader.efi.canTouchEfiVariables = true;
|
boot.loader.efi.canTouchEfiVariables = true;
|
||||||
boot.loader.efi.efiSysMountPoint = "/boot/efi";
|
boot.loader.efi.efiSysMountPoint = "/boot/efi";
|
||||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||||
|
boot.kernelParams = [ "quiet" "loglevel=3" "rd.systemd.show_status=false" "udev.log_level=3" ];
|
||||||
|
boot.blacklistedKernelModules = [ "rxrpc" ];
|
||||||
|
|
||||||
# ── Filesystems ─────────────────────────────────────────────
|
# ── Filesystems ─────────────────────────────────────────────
|
||||||
fileSystems."/run/media/Second_Drive" = {
|
fileSystems."/run/media/Second_Drive" = {
|
||||||
@@ -25,6 +26,13 @@
|
|||||||
nix.settings = {
|
nix.settings = {
|
||||||
experimental-features = [ "nix-command" "flakes" ];
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
download-buffer-size = 524288000;
|
download-buffer-size = 524288000;
|
||||||
|
|
||||||
|
# Network resilience for cache.nixos.org (Fastly) flakiness.
|
||||||
|
connect-timeout = 10; # fail-fast on dead TCP connects (default: 0 = unlimited)
|
||||||
|
stalled-download-timeout = 90; # default 300s; retry sooner on stalled transfers
|
||||||
|
download-attempts = 7; # default 5
|
||||||
|
http-connections = 25; # cap concurrency (helps MTU/middlebox paths)
|
||||||
|
fallback = true; # build locally if a substitute can't be fetched
|
||||||
};
|
};
|
||||||
|
|
||||||
# ── Networking ──────────────────────────────────────────────
|
# ── Networking ──────────────────────────────────────────────
|
||||||
@@ -62,7 +70,6 @@
|
|||||||
# ── Desktop ────────────────────────────────────────────────
|
# ── Desktop ────────────────────────────────────────────────
|
||||||
services.displayManager.gdm.enable = true;
|
services.displayManager.gdm.enable = true;
|
||||||
services.displayManager.gdm.autoSuspend = false;
|
services.displayManager.gdm.autoSuspend = false;
|
||||||
services.displayManager.gdm.wayland = true;
|
|
||||||
services.desktopManager.gnome.enable = true;
|
services.desktopManager.gnome.enable = true;
|
||||||
services.printing.enable = true;
|
services.printing.enable = true;
|
||||||
systemd.enableEmergencyMode = false;
|
systemd.enableEmergencyMode = false;
|
||||||
@@ -70,6 +77,16 @@
|
|||||||
security.pam.services.gdm-password.enableGnomeKeyring = true;
|
security.pam.services.gdm-password.enableGnomeKeyring = true;
|
||||||
security.pam.services.gdm-autologin.enableGnomeKeyring = true;
|
security.pam.services.gdm-autologin.enableGnomeKeyring = true;
|
||||||
|
|
||||||
|
# Declaratively guarantee the GNOME Keyring default pointer exists.
|
||||||
|
# Defining the full path ensures root doesn't accidentally lock the user out of .local
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /home/free/.local 0700 free users -"
|
||||||
|
"d /home/free/.local/share 0700 free users -"
|
||||||
|
"d /home/free/.local/share/keyrings 0700 free users -"
|
||||||
|
"f /home/free/.local/share/keyrings/default 0600 free users - login\n"
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
# ── Audio ──────────────────────────────────────────────────
|
# ── Audio ──────────────────────────────────────────────────
|
||||||
services.pulseaudio.enable = false;
|
services.pulseaudio.enable = false;
|
||||||
security.rtkit.enable = true;
|
security.rtkit.enable = true;
|
||||||
@@ -93,9 +110,19 @@
|
|||||||
services.flatpak.enable = true;
|
services.flatpak.enable = true;
|
||||||
systemd.services.flatpak-repo = {
|
systemd.services.flatpak-repo = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network-online.target" ];
|
after = [ "network-online.target" "nss-lookup.target" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" "nss-lookup.target" ];
|
||||||
path = [ pkgs.flatpak ];
|
path = [ pkgs.flatpak ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "15s";
|
||||||
|
};
|
||||||
|
unitConfig = {
|
||||||
|
StartLimitIntervalSec = 120;
|
||||||
|
StartLimitBurst = 5;
|
||||||
|
};
|
||||||
script = ''
|
script = ''
|
||||||
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
'';
|
'';
|
||||||
@@ -118,10 +145,10 @@
|
|||||||
ranger fastfetch gedit openssl pwgen
|
ranger fastfetch gedit openssl pwgen
|
||||||
aspell aspellDicts.en lm_sensors
|
aspell aspellDicts.en lm_sensors
|
||||||
hunspell hunspellDicts.en_US
|
hunspell hunspellDicts.en_US
|
||||||
synadm brave dua bitwarden-desktop
|
synadm brave dua
|
||||||
gparted pv unzip parted screen zenity
|
gparted pv unzip parted screen zenity
|
||||||
libargon2 gnome-terminal libreoffice-fresh
|
libargon2 gnome-terminal libreoffice-fresh
|
||||||
dig firefox element-desktop wp-cli axel
|
dig firefox wp-cli axel
|
||||||
lk-jwt-service livekit-libwebrtc livekit-cli livekit
|
lk-jwt-service livekit-libwebrtc livekit-cli livekit
|
||||||
matrix-synapse age
|
matrix-synapse age
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
# Sovran Hub — Manual Backup
|
|
||||||
|
|
||||||
The manual backup service copies critical system data from your Sovran Pro to an external USB drive, providing a third copy of your data (your Sovran Pro already maintains an automatic internal backup on its second drive).
|
|
||||||
|
|
||||||
Backups are written to:
|
|
||||||
|
|
||||||
```
|
|
||||||
<USB drive>/Sovran_SystemsOS_Backup/<timestamp>/
|
|
||||||
```
|
|
||||||
|
|
||||||
where `<timestamp>` is formatted as `YYYYMMDD_HHMMSS`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backup Stages
|
|
||||||
|
|
||||||
The script always attempts all four stages, but skips stages that are irrelevant to the system's configured role (see [Per-Role Breakdown](#per-role-breakdown) below).
|
|
||||||
|
|
||||||
| Stage | Directory | Contents |
|
|
||||||
|-------|-----------|----------|
|
|
||||||
| **1/4 — NixOS config** | `/etc/nixos/` | Full NixOS system configuration: `role-state.nix`, `custom.nix`, flake files, and any other config managed by the Hub |
|
|
||||||
| **2/4 — Secrets** | `/etc/nix-bitcoin-secrets`, `/var/lib/domains`, `/var/lib/secrets` | Bitcoin/LND secrets, domain configurations for all web services, and Hub state files |
|
|
||||||
| **3/4 — Home directory** | `/home/` | All user home directories (`.cache/` and Trash are excluded) |
|
|
||||||
| **4/4 — LND wallet data** | `/var/lib/lnd/` | Lightning Network node wallet and channel data (log files excluded) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Per-Role Breakdown
|
|
||||||
|
|
||||||
The script detects the system role at runtime by reading `/var/lib/sovran-hub/config.json` (falling back to `/etc/nixos/role-state.nix`) and adjusts its behaviour accordingly.
|
|
||||||
|
|
||||||
### Server + Desktop (default)
|
|
||||||
|
|
||||||
All services are enabled: Bitcoin, Matrix Synapse, Vaultwarden, WordPress, Nextcloud.
|
|
||||||
|
|
||||||
| Stage | Status | Notes |
|
|
||||||
|-------|--------|-------|
|
|
||||||
| Stage 1 — NixOS config | ✅ Backed up | Full server configuration |
|
|
||||||
| Stage 2 — Secrets | ✅ Backed up | Bitcoin secrets, domain configs, and Hub state |
|
|
||||||
| Stage 3 — Home directory | ✅ Backed up | Desktop user data |
|
|
||||||
| Stage 4 — LND wallet | ✅ Backed up | Lightning wallet and channel data |
|
|
||||||
|
|
||||||
This produces the largest backup. All four stages generate meaningful data.
|
|
||||||
|
|
||||||
### Desktop Only
|
|
||||||
|
|
||||||
All server services are disabled (`bitcoin = false`, `synapse = false`, `vaultwarden = false`, `wordpress = false`, `nextcloud = false`). Only GNOME desktop is active.
|
|
||||||
|
|
||||||
| Stage | Status | Notes |
|
|
||||||
|-------|--------|-------|
|
|
||||||
| Stage 1 — NixOS config | ✅ Backed up | Simpler config (no server services) |
|
|
||||||
| Stage 2 — Secrets | ⚠️ Partial | `/etc/nix-bitcoin-secrets` is **skipped** (not applicable for Desktop Only role). `/var/lib/domains` and `/var/lib/secrets` (Hub state) are still backed up if present |
|
|
||||||
| Stage 3 — Home directory | ✅ Backed up | **The most important data for this role** |
|
|
||||||
| Stage 4 — LND wallet | ⏭️ Skipped | Explicitly skipped — not applicable for Desktop Only role |
|
|
||||||
|
|
||||||
This produces the smallest and fastest backup. Stages 1 and 3 are the primary sources of meaningful data.
|
|
||||||
|
|
||||||
### Node (Bitcoin-only)
|
|
||||||
|
|
||||||
Only the Bitcoin ecosystem is active: `bitcoind`, `electrs`, `lnd`, `rtl`, `btcpay`, `mempool`, and `bip110`. All other server services are disabled.
|
|
||||||
|
|
||||||
| Stage | Status | Notes |
|
|
||||||
|-------|--------|-------|
|
|
||||||
| Stage 1 — NixOS config | ✅ Backed up | Node-specific configuration |
|
|
||||||
| Stage 2 — Secrets | ✅ Backed up | Bitcoin secrets and Hub state. `/var/lib/domains` may be minimal (BTCPay runs but is not exposed via Caddy) |
|
|
||||||
| Stage 3 — Home directory | ✅ Backed up | User data |
|
|
||||||
| Stage 4 — LND wallet | ✅ Backed up | **Critical** — Lightning wallet and channel data |
|
|
||||||
|
|
||||||
All four stages run, matching Server + Desktop behaviour. The `/var/lib/domains` directory may be sparsely populated since non-Bitcoin web services are not configured.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backup Manifest
|
|
||||||
|
|
||||||
After all stages complete, the script writes a `BACKUP_MANIFEST.txt` file inside the timestamped backup directory. This file records the date, hostname, detected role, target drive, and a directory listing of everything that was backed up.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Running the Backup
|
|
||||||
|
|
||||||
The backup is triggered from the Sovran Hub web UI. You can also run it directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Auto-detect the first external USB drive
|
|
||||||
sudo bash /path/to/sovran-hub-backup.sh
|
|
||||||
|
|
||||||
# Specify a target drive explicitly
|
|
||||||
sudo BACKUP_TARGET=/run/media/<user>/<drive> bash /path/to/sovran-hub-backup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The script requires at least **10 GB** of free space on the target drive and will refuse to write to internal system drives.
|
|
||||||
|
|
||||||
Logs are written to `/var/log/sovran-hub-backup.log` and the current status (`RUNNING`, `SUCCESS`, or `FAILED`) is tracked in `/var/log/sovran-hub-backup.status`.
|
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
# Remote Deployment via Headscale (Self-Hosted Tailscale)
|
|
||||||
|
|
||||||
This guide covers the Sovran Systems remote deployment system built on [Headscale](https://headscale.net) — a self-hosted, open-source implementation of the Tailscale coordination server. Freshly booted ISOs automatically join a private WireGuard mesh VPN without any per-machine key pre-generation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Internet │
|
|
||||||
└────────────┬─────────────────────┬──────────────────────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌────────────────────┐ ┌─────────────────────────────────┐
|
|
||||||
│ Admin Workstation │ │ Sovran VPS │
|
|
||||||
│ │ │ ┌─────────────────────────────┐ │
|
|
||||||
│ tailscale up │ │ │ Headscale (port 8080) │ │
|
|
||||||
│ --login-server │◄──┼─►│ Coordination server │ │
|
|
||||||
│ hs.example.com │ │ ├─────────────────────────────┤ │
|
|
||||||
│ │ │ │ Provisioning API (9090) │ │
|
|
||||||
└────────────────────┘ │ │ POST /register │ │
|
|
||||||
│ │ GET /machines │ │
|
|
||||||
│ │ GET /health │ │
|
|
||||||
│ ├─────────────────────────────┤ │
|
|
||||||
│ │ Caddy (80/443) │ │
|
|
||||||
│ │ hs.example.com → :8080 │ │
|
|
||||||
│ │ prov.example.com → :9090 │ │
|
|
||||||
│ └─────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
▲
|
|
||||||
│ WireGuard mesh (Tailnet)
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────┐
|
|
||||||
│ Deploy Target Machine │
|
|
||||||
│ │
|
|
||||||
│ Boot live ISO → │
|
|
||||||
│ sovran-auto-provision → │
|
|
||||||
│ POST /register → │
|
|
||||||
│ tailscale up --authkey=... │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Components:**
|
|
||||||
- **`sovran-provisioner.nix`** — NixOS module deployed on a separate VPS; runs Headscale + provisioning API + Caddy.
|
|
||||||
- **Live ISO** (`iso/common.nix`) — Auto-registers with the provisioning server and joins the Tailnet on boot.
|
|
||||||
- **`remote-deploy.nix`** — Post-install NixOS module that uses Tailscale/Headscale for ongoing access.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 1: VPS Setup — Deploy `sovran-provisioner.nix`
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- A NixOS VPS (any provider) with a public IP
|
|
||||||
- Two DNS A records pointing to your VPS:
|
|
||||||
- `hs.yourdomain.com` → VPS IP (Headscale coordination server)
|
|
||||||
- `prov.yourdomain.com` → VPS IP (Provisioning API)
|
|
||||||
- Ports 80, 443 (TCP) and 3478 (UDP, STUN/DERP) open in your VPS firewall
|
|
||||||
|
|
||||||
### DNS Records
|
|
||||||
|
|
||||||
| Type | Name | Value |
|
|
||||||
|------|-----------------------|------------|
|
|
||||||
| A | `hs.yourdomain.com` | `<VPS IP>` |
|
|
||||||
| A | `prov.yourdomain.com` | `<VPS IP>` |
|
|
||||||
|
|
||||||
### NixOS Configuration
|
|
||||||
|
|
||||||
Add the following to your VPS's `/etc/nixos/configuration.nix`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./hardware-configuration.nix
|
|
||||||
/path/to/sovran-provisioner.nix # or fetch from the repo
|
|
||||||
];
|
|
||||||
|
|
||||||
sovranProvisioner = {
|
|
||||||
enable = true;
|
|
||||||
domain = "prov.yourdomain.com";
|
|
||||||
headscaleDomain = "hs.yourdomain.com";
|
|
||||||
|
|
||||||
# Optional: customise defaults
|
|
||||||
headscaleUser = "sovran-deploy"; # namespace for deploy machines
|
|
||||||
adminUser = "admin"; # namespace for your workstation
|
|
||||||
keyExpiry = "1h"; # pre-auth keys expire after 1 hour
|
|
||||||
rateLimitMax = 10; # max registrations per window
|
|
||||||
rateLimitWindow = 60; # window in seconds
|
|
||||||
};
|
|
||||||
|
|
||||||
# Required for Caddy ACME (Let's Encrypt)
|
|
||||||
networking.hostName = "sovran-vps";
|
|
||||||
system.stateVersion = "24.11";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nixos-rebuild switch
|
|
||||||
```
|
|
||||||
|
|
||||||
Caddy will automatically obtain TLS certificates via Let's Encrypt.
|
|
||||||
|
|
||||||
### Retrieve the Enrollment Token
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat /var/lib/sovran-provisioner/enroll-token
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep this token secret — it is used to authenticate ISO registrations. The token is auto-generated on first boot and stored at this path. You never need to set it manually. Just `cat` it from the VPS and copy it to `iso/secrets/enroll-token` before building the ISO.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 2: Admin Workstation Setup
|
|
||||||
|
|
||||||
Join your Tailnet as an admin so you can reach deployed machines:
|
|
||||||
|
|
||||||
### Install Tailscale
|
|
||||||
|
|
||||||
Follow the [Tailscale installation guide](https://tailscale.com/download) for your OS, or on NixOS:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
services.tailscale.enable = true;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Join the Tailnet
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tailscale up --login-server https://hs.yourdomain.com --accept-dns=false
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note:** The `--accept-dns=false` flag prevents Tailscale from taking over your system DNS resolver. This is important if you are behind a VPN (see [Troubleshooting](#troubleshooting) below).
|
|
||||||
|
|
||||||
Tailscale prints a URL. Open it and copy the node key (starts with `mkey:`).
|
|
||||||
|
|
||||||
### Approve the Node in Headscale
|
|
||||||
|
|
||||||
On the VPS, first find the numeric user ID for the `admin` user, then register the node:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Look up the numeric ID for the admin user (Headscale 0.28.0 requires -u <id>)
|
|
||||||
headscale users list -o json
|
|
||||||
|
|
||||||
# Register the node using the numeric user ID
|
|
||||||
headscale nodes register -u <admin-user-id> --key mkey:xxxxxxxxxxxxxxxx
|
|
||||||
```
|
|
||||||
|
|
||||||
Your workstation is now on the Tailnet. You can list nodes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
headscale nodes list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 3: Building the Deploy ISO
|
|
||||||
|
|
||||||
### Add Secrets (gitignored)
|
|
||||||
|
|
||||||
The secrets directory `iso/secrets/` is gitignored. Populate it before building:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy the enrollment token from the VPS
|
|
||||||
ssh root@<VPS> cat /var/lib/sovran-provisioner/enroll-token > iso/secrets/enroll-token
|
|
||||||
|
|
||||||
# Set the provisioner URL
|
|
||||||
echo "https://prov.yourdomain.com" > iso/secrets/provisioner-url
|
|
||||||
```
|
|
||||||
|
|
||||||
These files are baked into the ISO at build time. If the files are absent the ISO still builds — the auto-provision service exits cleanly with "No enroll token found, skipping auto-provision", leaving DIY users unaffected.
|
|
||||||
|
|
||||||
### Build the ISO
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting ISO is in `./result/iso/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 4: Deployment Workflow
|
|
||||||
|
|
||||||
### Step-by-Step
|
|
||||||
|
|
||||||
1. **Hand the ISO to the remote person** — they burn it to a USB drive and boot.
|
|
||||||
|
|
||||||
2. **ISO boots and auto-registers** — `sovran-auto-provision.service` runs automatically:
|
|
||||||
- Reads `enroll-token` and `provisioner-url` from `/etc/sovran/`
|
|
||||||
- `POST https://prov.yourdomain.com/register` with hostname + MAC
|
|
||||||
- Receives a Headscale pre-auth key
|
|
||||||
- Runs `tailscale up --login-server=... --authkey=...`
|
|
||||||
- The machine appears in `headscale nodes list` within ~30 seconds
|
|
||||||
|
|
||||||
3. **Approve the node (if not using auto-approve)** — on the VPS:
|
|
||||||
```bash
|
|
||||||
headscale nodes list
|
|
||||||
# Note the node key for the new machine
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **SSH from your workstation** — once the machine is on the Tailnet:
|
|
||||||
```bash
|
|
||||||
# Get the machine's Tailscale IP
|
|
||||||
headscale nodes list | grep sovran-deploy-
|
|
||||||
|
|
||||||
# SSH in
|
|
||||||
ssh root@100.64.x.x # password: sovran-remote (live ISO default)
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Run the headless installer**:
|
|
||||||
|
|
||||||
The `--deploy-key` is your SSH public key that gets injected into `root`'s `authorized_keys` on the deployed machine. This grants full root access for initial setup. Generate it once on your workstation if you haven't already:
|
|
||||||
```bash
|
|
||||||
ssh-keygen -t ed25519 -f ~/.ssh/sovran-deploy -C "sovran-deploy"
|
|
||||||
```
|
|
||||||
After deployment is complete and you disable deploy mode, this key is removed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo sovran-install-headless.sh \
|
|
||||||
--disk /dev/sda \
|
|
||||||
--role server \
|
|
||||||
--deploy-key "$(cat ~/.ssh/sovran-deploy.pub)" \
|
|
||||||
--headscale-server "https://hs.yourdomain.com" \
|
|
||||||
--headscale-key "$(headscale preauthkeys create -u $(headscale users list -o json | jq -r '.[] | select(.name=="sovran-deploy") | .id') -e 2h -o json | jq -r '.key')"
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Machine reboots into Sovran_SystemsOS** — `deploy-tailscale-connect.service` runs:
|
|
||||||
- Reads `/var/lib/secrets/headscale-authkey`
|
|
||||||
- Joins the Tailnet with a deterministic hostname (`sovran-<hostname>`)
|
|
||||||
|
|
||||||
7. **Post-install SSH and RDP**:
|
|
||||||
```bash
|
|
||||||
# SSH over Tailnet
|
|
||||||
ssh root@<tailscale-ip>
|
|
||||||
|
|
||||||
# RDP over Tailnet (desktop role) — Sovran_SystemsOS uses GNOME Remote Desktop (native Wayland RDP)
|
|
||||||
# Retrieve the auto-generated RDP password:
|
|
||||||
ssh root@<tailscale-ip> cat /var/lib/gnome-remote-desktop/rdp-password
|
|
||||||
# Then connect with any RDP client (Remmina, GNOME Connections, Microsoft Remote Desktop):
|
|
||||||
# Host: <tailscale-ip>:3389 User: sovran Password: <from above>
|
|
||||||
```
|
|
||||||
|
|
||||||
8. **Disable deploy mode** — edit `/etc/nixos/custom.nix` on the target, set `enable = false`, then:
|
|
||||||
```bash
|
|
||||||
sudo nixos-rebuild switch
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 5: Post-Install Access
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Over Tailnet
|
|
||||||
ssh root@100.64.x.x
|
|
||||||
```
|
|
||||||
|
|
||||||
### RDP (desktop/server roles)
|
|
||||||
|
|
||||||
Sovran_SystemsOS uses **GNOME Remote Desktop** (native Wayland RDP — not xfreerdp). The RDP service auto-generates credentials on first boot.
|
|
||||||
|
|
||||||
**Username:** `sovran`
|
|
||||||
**Password:** auto-generated — retrieve it via SSH:
|
|
||||||
```bash
|
|
||||||
ssh root@<tailscale-ip> cat /var/lib/gnome-remote-desktop/rdp-password
|
|
||||||
```
|
|
||||||
|
|
||||||
Connect using any RDP client (Remmina, GNOME Connections, Microsoft Remote Desktop) to `<tailscale-ip>:3389`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Model
|
|
||||||
|
|
||||||
| Concern | Mitigation |
|
|
||||||
|---------|-----------|
|
|
||||||
| Enrollment token theft | Token only triggers key generation; it does not grant access to the machine itself |
|
|
||||||
| Rogue device joins Tailnet | Visible in `headscale nodes list`; removable instantly with `headscale nodes delete` |
|
|
||||||
| Pre-auth key reuse | Keys are ephemeral and expire in 1 hour (configurable via `keyExpiry`) |
|
|
||||||
| Rate limiting | Provisioning API limits to 10 registrations/minute by default (configurable) |
|
|
||||||
| SSH access | Requires ed25519 key injected at install time; password authentication disabled |
|
|
||||||
| Credential storage | Auth key written to `/var/lib/secrets/headscale-authkey` (mode 600) on the installed OS |
|
|
||||||
|
|
||||||
### Token Rotation
|
|
||||||
|
|
||||||
To rotate the enrollment token:
|
|
||||||
|
|
||||||
1. On the VPS:
|
|
||||||
```bash
|
|
||||||
openssl rand -hex 32 > /var/lib/sovran-provisioner/enroll-token
|
|
||||||
chmod 600 /var/lib/sovran-provisioner/enroll-token
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Update `iso/secrets/enroll-token` and rebuild the ISO.
|
|
||||||
|
|
||||||
Old ISOs with the previous token will fail to register (receive 401).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
### List Active Tailnet Nodes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# On the VPS
|
|
||||||
headscale nodes list
|
|
||||||
```
|
|
||||||
|
|
||||||
### List Registered Machines (Provisioning API)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s -H "Authorization: Bearer $(cat /var/lib/sovran-provisioner/enroll-token)" \
|
|
||||||
https://prov.yourdomain.com/machines | jq .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl https://prov.yourdomain.com/health
|
|
||||||
# {"status": "ok"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provisioner Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
journalctl -u sovran-provisioner -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Headscale Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
journalctl -u headscale -f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cleanup
|
|
||||||
|
|
||||||
### Remove a Machine from the Tailnet
|
|
||||||
|
|
||||||
```bash
|
|
||||||
headscale nodes list
|
|
||||||
headscale nodes delete --identifier <id>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Disable Deploy Mode on an Installed Machine
|
|
||||||
|
|
||||||
Edit `/etc/nixos/custom.nix`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
sovran_systemsOS.deploy.enable = false;
|
|
||||||
```
|
|
||||||
|
|
||||||
Then rebuild:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nixos-rebuild switch
|
|
||||||
```
|
|
||||||
|
|
||||||
This stops the Tailscale connect service.
|
|
||||||
|
|
||||||
### Revoke All Active Pre-Auth Keys
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List pre-auth keys (Headscale 0.28.0: no --user flag on list)
|
|
||||||
headscale preauthkeys list
|
|
||||||
|
|
||||||
# Expire a specific key — use numeric user ID (-u <id>)
|
|
||||||
# First find the user ID:
|
|
||||||
headscale users list -o json
|
|
||||||
# Then expire the key:
|
|
||||||
headscale preauthkeys expire -u <user-id> --key <key>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### VPN Conflicts (Mullvad, WireGuard, etc.)
|
|
||||||
|
|
||||||
**Symptom:** `tailscale up` hangs or fails with `connection refused` on port 443, even though `curl https://hs.yourdomain.com/health` works fine.
|
|
||||||
|
|
||||||
**Cause:** VPNs like Mullvad route all traffic — including Tailscale's control-plane connections — through the VPN tunnel. Additionally, Tailscale's DNS handler (`--accept-dns=true` by default) hijacks DNS resolution and may prevent correct resolution of your Headscale server even when logged out.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Disconnect your VPN temporarily and retry `tailscale up`.
|
|
||||||
2. If you need the VPN active, use split tunneling to exclude `tailscaled`:
|
|
||||||
```bash
|
|
||||||
# Mullvad CLI
|
|
||||||
mullvad split-tunnel add $(pidof tailscaled)
|
|
||||||
```
|
|
||||||
Or in the Mullvad GUI: **Settings → Split tunneling → Add tailscaled**.
|
|
||||||
3. Always pass `--accept-dns=false` when enrolling to avoid DNS hijacking:
|
|
||||||
```bash
|
|
||||||
sudo tailscale up --login-server https://hs.yourdomain.com --authkey <key> --accept-dns=false
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### "RATELIMIT" in tailscaled Logs
|
|
||||||
|
|
||||||
**Symptom:** `journalctl -u tailscaled` shows lines like:
|
|
||||||
```
|
|
||||||
[RATELIMIT] format("Received error: %v")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause:** This is **NOT** a server-side rate limit from Headscale. It is tailscaled's internal log suppressor de-duplicating repeated connection-refused error messages. The real underlying error is `connection refused`.
|
|
||||||
|
|
||||||
**What to check:**
|
|
||||||
1. Is Headscale actually running? `curl https://hs.yourdomain.com/health`
|
|
||||||
2. Is your VPN blocking the connection? (see VPN Conflicts above)
|
|
||||||
3. Is there a firewall blocking port 443?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### "connection refused" on Port 443
|
|
||||||
|
|
||||||
If `tailscale up` fails but `curl` works, the issue is usually DNS or VPN:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Does curl reach Headscale successfully?
|
|
||||||
curl -v https://hs.yourdomain.com/health
|
|
||||||
|
|
||||||
# Force IPv4 vs IPv6 to identify if it's an address-family issue
|
|
||||||
curl -4 https://hs.yourdomain.com/health
|
|
||||||
curl -6 https://hs.yourdomain.com/health
|
|
||||||
|
|
||||||
# Check what IP headscale resolves to
|
|
||||||
dig +short hs.yourdomain.com
|
|
||||||
|
|
||||||
# What resolver is the system using?
|
|
||||||
cat /etc/resolv.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
If curl works but tailscale doesn't, tailscaled may be using a different DNS resolver (e.g. its own `100.100.100.100` stub resolver). Fix: pass `--accept-dns=false`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Headscale User ID Lookup (0.28.0)
|
|
||||||
|
|
||||||
Headscale 0.28.0 removed `--user <name>` in favour of `-u <numeric-id>`. To find the numeric ID for a user:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
headscale users list -o json
|
|
||||||
# Output: [{"id": "1", "name": "sovran-deploy", ...}, ...]
|
|
||||||
|
|
||||||
# One-liner to get the ID for a specific user
|
|
||||||
headscale users list -o json | jq -r '.[] | select(.name=="sovran-deploy") | .id'
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use the numeric ID in subsequent commands:
|
|
||||||
```bash
|
|
||||||
headscale preauthkeys create -u 1 -e 1h -o json
|
|
||||||
headscale nodes register -u 1 --key mkey:xxxx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
| Component | Port | Protocol | Description |
|
|
||||||
|-----------|------|----------|-------------|
|
|
||||||
| Caddy | 80 | TCP | HTTP → HTTPS redirect |
|
|
||||||
| Caddy | 443 | TCP | HTTPS (Let's Encrypt) |
|
|
||||||
| Headscale | 8080 | TCP | Coordination server (proxied by Caddy) |
|
|
||||||
| Provisioner | 9090 | TCP | Registration API (proxied by Caddy) |
|
|
||||||
| DERP/STUN | 3478 | UDP | WireGuard relay fallback |
|
|
||||||
| Tailscale | N/A | WireGuard | Mesh VPN between nodes |
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
# Tech Support: Security Design, User Flow, and Incident Response
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Sovran Hub includes a **Tech Support** feature that lets Sovran Systems
|
|
||||||
staff remotely diagnose and fix issues on a user's machine via SSH — without
|
|
||||||
ever having access to private keys or wallet funds.
|
|
||||||
|
|
||||||
Wallet protection is the default. The user must make an active, time-limited
|
|
||||||
choice to grant support staff access to wallet files, and can revoke that
|
|
||||||
access at any time.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Restricted User Instead of Root
|
|
||||||
|
|
||||||
When a user enables support access the Hub:
|
|
||||||
|
|
||||||
1. Ensures the `sovran-support` system user exists (declared declaratively in
|
|
||||||
`modules/core/tech-support.nix`; the Hub also provisions it on demand as a
|
|
||||||
fallback on non-NixOS systems).
|
|
||||||
2. Writes the Sovran Systems public SSH key **only** to
|
|
||||||
`/var/lib/sovran-support/.ssh/authorized_keys`, not to root's
|
|
||||||
`authorized_keys`.
|
|
||||||
3. Applies POSIX ACLs (`setfacl -R -m u:sovran-support:---`) to every wallet
|
|
||||||
directory that exists on disk, denying all access by the support user.
|
|
||||||
4. Records a timestamped `SUPPORT_ENABLED` event in the audit log at
|
|
||||||
`/var/log/sovran-support-audit.log`.
|
|
||||||
|
|
||||||
When the session ends (or if the Hub cannot create the restricted user), the
|
|
||||||
key is removed and all ACLs are revoked immediately.
|
|
||||||
|
|
||||||
### Protected Wallet Paths
|
|
||||||
|
|
||||||
The following directories are locked by default when a support session starts:
|
|
||||||
|
|
||||||
| Path | Contents |
|
|
||||||
|------|----------|
|
|
||||||
| `/etc/nix-bitcoin-secrets` | nix-bitcoin generated secrets |
|
|
||||||
| `/var/lib/bitcoind` | Bitcoin Core chainstate and wallet |
|
|
||||||
| `/var/lib/lnd` | LND wallet and channel database |
|
|
||||||
| `/home` | User home directories |
|
|
||||||
|
|
||||||
Paths are only locked if they exist on disk at the time the session starts.
|
|
||||||
|
|
||||||
### POSIX ACL Mechanics
|
|
||||||
|
|
||||||
POSIX ACLs on Linux handle access checks in this order:
|
|
||||||
|
|
||||||
1. If the process UID matches the file owner UID → use owner permissions
|
|
||||||
2. **If there is a matching named-user ACL entry → use that entry's
|
|
||||||
permissions** (clamped by the mask entry)
|
|
||||||
3. If any group matches → use group permissions
|
|
||||||
4. Otherwise → use "other" permissions
|
|
||||||
|
|
||||||
Setting `u:sovran-support:---` creates a named-user ACL entry with no
|
|
||||||
permissions. Because the named-user entry is checked before the group/other
|
|
||||||
entries, the support user cannot access those directories regardless of the
|
|
||||||
"other" permission bits.
|
|
||||||
|
|
||||||
`setfacl` and `getfacl` are provided by the `acl` package, which is added to
|
|
||||||
`environment.systemPackages` by `modules/core/tech-support.nix`.
|
|
||||||
|
|
||||||
### Fallback to Root (When Restricted User Cannot Be Created)
|
|
||||||
|
|
||||||
If the `sovran-support` user does not exist and cannot be created (e.g.,
|
|
||||||
`users.mutableUsers = false` and the declarative module has not been deployed
|
|
||||||
yet), the Hub falls back to adding the support key to root's
|
|
||||||
`authorized_keys`. The modal prominently warns the user when this has happened
|
|
||||||
so they can decide whether to end the session.
|
|
||||||
|
|
||||||
### Audit Log
|
|
||||||
|
|
||||||
Every session event is appended to `/var/log/sovran-support-audit.log`:
|
|
||||||
|
|
||||||
```
|
|
||||||
[2025-01-15 14:32:01 UTC] SUPPORT_ENABLED: restricted_user=True acl_applied=True protected_paths=4
|
|
||||||
[2025-01-15 14:45:00 UTC] WALLET_UNLOCKED: duration=3600s expires=2025-01-15 15:45:00 UTC
|
|
||||||
[2025-01-15 15:45:00 UTC] WALLET_RELOCKED: auto-expired
|
|
||||||
[2025-01-15 16:01:22 UTC] SUPPORT_DISABLED
|
|
||||||
```
|
|
||||||
|
|
||||||
The last 100 lines of this log are accessible from the Hub UI while a session
|
|
||||||
is active (or after it ends, until the page is refreshed).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Tradeoffs
|
|
||||||
|
|
||||||
### What This Protects Against
|
|
||||||
|
|
||||||
- **Accidental wallet exposure** — support staff cannot read wallet files
|
|
||||||
during a normal session; they must ask the user to explicitly grant access.
|
|
||||||
- **Credential theft** — private keys in the wallet directories are not
|
|
||||||
visible to the `sovran-support` user by default.
|
|
||||||
- **Scope creep** — the restricted user account limits the blast radius of an
|
|
||||||
SSH session compared to direct root access.
|
|
||||||
|
|
||||||
### Known Limitations
|
|
||||||
|
|
||||||
| Limitation | Mitigation |
|
|
||||||
|------------|------------|
|
|
||||||
| Support user still has system-wide bash access | Restrict with `ForceCommand` or AppArmor in the NixOS config if a narrower scope is required |
|
|
||||||
| ACLs apply only to directories that exist at session start | If new wallet directories are created during a session, they are not auto-protected. Re-lock and re-enable support to pick up new paths |
|
|
||||||
| Root fallback grants full access | The Hub UI warns the user prominently; users should end the session if they are uncomfortable |
|
|
||||||
| `setfacl` / ACL filesystem support required | The `acl` package is declared in `tech-support.nix`; most Linux filesystems (ext4, btrfs, xfs) support ACLs by default |
|
|
||||||
| Wallet access grant is time-limited but lazy-expired | Expiry is checked on the next `/api/support/status` poll (every 10 seconds in the UI); there is a small window after expiry |
|
|
||||||
|
|
||||||
### Defense-in-Depth Recommendations
|
|
||||||
|
|
||||||
For environments that require stronger isolation, consider layering one or
|
|
||||||
more additional controls:
|
|
||||||
|
|
||||||
- **`ForceCommand`** in `sshd_config` (or `~/.ssh/authorized_keys` command
|
|
||||||
prefix) to restrict the support user to a specific diagnostic script.
|
|
||||||
- **`ChrootDirectory`** in the `sshd_config` `Match User sovran-support` block
|
|
||||||
to confine the session to a prepared directory tree.
|
|
||||||
- **AppArmor or SELinux** profiles that deny the support process read access
|
|
||||||
to wallet paths at the kernel level.
|
|
||||||
- **Namespace/bind-mount overlays** (e.g., via a wrapper systemd unit) to
|
|
||||||
present a sanitized filesystem view.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User opens Hub → Clicks "Tech Support" in sidebar
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Modal: "Need help from Sovran Systems?"
|
|
||||||
• Explains what will happen
|
|
||||||
• Shows Wallet Protection notice
|
|
||||||
• User clicks "Enable Support Access"
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: 1. Creates / verifies sovran-support user
|
|
||||||
2. Writes SSH key to that user's authorized_keys
|
|
||||||
3. Applies POSIX ACL deny on all existing wallet paths
|
|
||||||
4. Saves session metadata + writes SUPPORT_ENABLED to audit log
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Modal: "Support Access is Active"
|
|
||||||
• Live session duration timer
|
|
||||||
• Wallet Files: Protected panel
|
|
||||||
– Optional: "Grant Wallet Access" (time-limited, user-chosen)
|
|
||||||
• "End Support Session" button
|
|
||||||
• "View Audit Log" button
|
|
||||||
│
|
|
||||||
(User grants wallet access)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: • Removes ACL deny entries
|
|
||||||
• Records WALLET_UNLOCKED event with expiry time
|
|
||||||
• Starts countdown timer in UI
|
|
||||||
│
|
|
||||||
(Timer expires or user clicks "Re-lock Wallet Now")
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: • Re-applies ACL deny entries
|
|
||||||
• Removes WALLET_UNLOCK_FILE
|
|
||||||
• Records WALLET_RELOCKED event
|
|
||||||
│
|
|
||||||
(User clicks "End Support Session")
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: 1. Removes SSH key from sovran-support authorized_keys
|
|
||||||
2. Removes SSH key from root authorized_keys (legacy cleanup)
|
|
||||||
3. Revokes any wallet unlock, re-applies ACL deny
|
|
||||||
4. Verifies key is gone
|
|
||||||
5. Records SUPPORT_DISABLED event
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Modal: "Support Session Ended — SSH key removed"
|
|
||||||
• Shows verified removal status
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Incident Response
|
|
||||||
|
|
||||||
### Scenario 1 — You accidentally granted wallet access and are unsure what was copied
|
|
||||||
|
|
||||||
**Immediate steps:**
|
|
||||||
|
|
||||||
1. Click **"Re-lock Wallet Now"** in the Hub modal, or click
|
|
||||||
**"End Support Session"** to simultaneously revoke SSH access and wallet
|
|
||||||
access.
|
|
||||||
2. Open the **Audit Log** from the Hub modal and note the timestamps of
|
|
||||||
`WALLET_UNLOCKED` and `WALLET_RELOCKED` events.
|
|
||||||
3. Check `/var/log/auth.log` (or `journalctl -u sshd`) for SSH login events
|
|
||||||
by `sovran-support` during the unlocked window.
|
|
||||||
|
|
||||||
**Assessment:**
|
|
||||||
|
|
||||||
- If no SSH login occurred during the wallet-unlocked window, your keys are
|
|
||||||
safe.
|
|
||||||
- If an SSH login did occur, treat private keys as potentially compromised.
|
|
||||||
|
|
||||||
**Recovery if keys may be compromised:**
|
|
||||||
|
|
||||||
| Wallet | Recovery action |
|
|
||||||
|--------|----------------|
|
|
||||||
| LND | Move all funds out using `lncli sendcoins` to a freshly generated on-chain address; close channels; recreate wallet |
|
|
||||||
| Sparrow | Sweep funds to a new wallet generated on an air-gapped device |
|
|
||||||
| Bisq | Withdraw all BSQ and BTC to external wallets; delete the Bisq data directory and recreate |
|
|
||||||
| nix-bitcoin secrets | Rotate all secrets with `nix-bitcoin-secrets generate` and redeploy |
|
|
||||||
|
|
||||||
**Report the incident:**
|
|
||||||
|
|
||||||
Contact Sovran Systems immediately at support@sovransystems.com with:
|
|
||||||
- The audit log output (`/var/log/sovran-support-audit.log`)
|
|
||||||
- The SSH auth log for the affected time window
|
|
||||||
- A description of what you were troubleshooting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Scenario 2 — Support session cannot be ended (button fails or server is unresponsive)
|
|
||||||
|
|
||||||
**Manual key removal (run as root on the device):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Remove from support user's authorized_keys
|
|
||||||
rm -f /var/lib/sovran-support/.ssh/authorized_keys
|
|
||||||
|
|
||||||
# Remove from root's authorized_keys (fallback / legacy)
|
|
||||||
sed -i '/sovransystemsos-support/d' /root/.ssh/authorized_keys
|
|
||||||
|
|
||||||
# Remove wallet unlock state
|
|
||||||
rm -f /var/lib/secrets/support-wallet-unlock
|
|
||||||
|
|
||||||
# Re-apply wallet ACL protections
|
|
||||||
setfacl -R -m u:sovran-support:--- /etc/nix-bitcoin-secrets \
|
|
||||||
/var/lib/bitcoind /var/lib/lnd /home 2>/dev/null || true
|
|
||||||
|
|
||||||
# Restart sshd to drop any active connections
|
|
||||||
systemctl restart sshd
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Scenario 3 — You see an unexpected SUPPORT_ENABLED in the audit log
|
|
||||||
|
|
||||||
This should never happen without physical or remote access to the Hub web
|
|
||||||
interface. If you see an unexpected entry:
|
|
||||||
|
|
||||||
1. Immediately run the manual key removal commands above.
|
|
||||||
2. Change the Sovran Hub web interface password.
|
|
||||||
3. Check `/var/log/nginx/access.log` (or Caddy access logs) for unexpected
|
|
||||||
requests to `/api/support/enable`.
|
|
||||||
4. Consider rebooting the device to clear any in-memory state.
|
|
||||||
5. Report the incident to Sovran Systems.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This document is part of the Sovran_SystemsOS repository. For the
|
|
||||||
authoritative and up-to-date version, see the repository.*
|
|
||||||
Generated
+44
-95
@@ -1,34 +1,15 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"bip110": {
|
"btc-clients": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775155316,
|
"lastModified": 1781013869,
|
||||||
"narHash": "sha256-4H8aEChZ6rra9jd8OcVHgHs3IuzKzpDt4PPtsPJrkyM=",
|
"narHash": "sha256-XlEUtL+8M6kbPdmIh4sQQ7G02/1CwHQEk1RPvIMEWOs=",
|
||||||
"owner": "emmanuelrosa",
|
|
||||||
"repo": "bitcoin-knots-bip-110-nix",
|
|
||||||
"rev": "663ea34f6f846f48c385a73d4581ba599bb5bbc0",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "emmanuelrosa",
|
|
||||||
"repo": "bitcoin-knots-bip-110-nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"btc-clients": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs_2",
|
|
||||||
"oldNixpkgs": "oldNixpkgs"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1775833364,
|
|
||||||
"narHash": "sha256-RsaXYEUUF1g/a5ET0QKTX1p3SCaCIAZYCZDLe8htv88=",
|
|
||||||
"owner": "emmanuelrosa",
|
"owner": "emmanuelrosa",
|
||||||
"repo": "btc-clients-nix",
|
"repo": "btc-clients-nix",
|
||||||
"rev": "17f676710a6e9483f30b24eb2948bf51c961203a",
|
"rev": "9a6c78204dc8961840375b110bca595b1f6f084c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -71,11 +52,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772408722,
|
"lastModified": 1778716662,
|
||||||
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -106,16 +87,16 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"extra-container": "extra-container",
|
"extra-container": "extra-container",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs_3",
|
"nixpkgs": "nixpkgs_2",
|
||||||
"nixpkgs-25_05": "nixpkgs-25_05",
|
"nixpkgs-25_05": "nixpkgs-25_05",
|
||||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767721199,
|
"lastModified": 1779253922,
|
||||||
"narHash": "sha256-UzRxDiJlopBGPTjyhCdMP+QdTwXK+l+y45urXCyH69A=",
|
"narHash": "sha256-k5DpYVfyy27ELuEiV+51EfVg7B6vKUW63NWeA6eKGd0=",
|
||||||
"owner": "fort-nix",
|
"owner": "fort-nix",
|
||||||
"repo": "nix-bitcoin",
|
"repo": "nix-bitcoin",
|
||||||
"rev": "5b532698ce9e8bd79b07d77ab4fc60e1a8408f73",
|
"rev": "1496f842477976c085cd96f1837ea12444014088",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -127,27 +108,26 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775054576,
|
"lastModified": 1780218263,
|
||||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
"narHash": "sha256-T/f0pPDrH3Qc1VXyQXbK7yfHWRn90l3xwplc/nsxin4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
"rev": "7fc393d1b46fa000d48ff14e8b6a3c9985f03af0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "master",
|
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-25_05": {
|
"nixpkgs-25_05": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767051569,
|
"lastModified": 1767313136,
|
||||||
"narHash": "sha256-0MnuWoN+n1UYaGBIpqpPs9I9ZHW4kynits4mrnh1Pk4=",
|
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "40ee5e1944bebdd128f9fbada44faefddfde29bd",
|
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -159,27 +139,27 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751274312,
|
"lastModified": 1780902259,
|
||||||
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
|
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
|
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixos-24.11",
|
"ref": "nixos-26.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767364772,
|
"lastModified": 1778869304,
|
||||||
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
|
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
|
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -191,26 +171,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775054576,
|
"lastModified": 1778737229,
|
||||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
"narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
"rev": "d7a713c0b7e47c908258e71cba7a2d77cc8d71d5",
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_3": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767480499,
|
|
||||||
"narHash": "sha256-8IQQUorUGiSmFaPnLSo2+T+rjHtiNWc+OAzeHck7N48=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "30a3c519afcf3f99e2c6df3b359aec5692054d92",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -220,13 +185,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_4": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775710090,
|
"lastModified": 1780749050,
|
||||||
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -236,13 +201,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_5": {
|
"nixpkgs_4": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1774701658,
|
"lastModified": 1780336545,
|
||||||
"narHash": "sha256-CIS/4AMUSwUyC8X5g+5JsMRvIUL3YUfewe8K4VrbsSQ=",
|
"narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b63fe7f000adcfa269967eeff72c64cafecbbebe",
|
"rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -255,15 +220,15 @@
|
|||||||
"nixvim": {
|
"nixvim": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"nixpkgs": "nixpkgs_5",
|
"nixpkgs": "nixpkgs_4",
|
||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776128025,
|
"lastModified": 1780995253,
|
||||||
"narHash": "sha256-spZM5zll0cBPHHSZPioZREArzCsllurKQsJME08nnXY=",
|
"narHash": "sha256-6Lsoyw2XPvY8YNMCtPnsyw0JVVtHsXP2xtrFJBBTAOQ=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixvim",
|
"repo": "nixvim",
|
||||||
"rev": "0a12693297d23f1b3af04ba6112b5936e2eba41b",
|
"rev": "43a7e6f82978ac975c3bba6728869b231e7a1ba0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -272,28 +237,11 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oldNixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1727619874,
|
|
||||||
"narHash": "sha256-a4Jcd+vjQAzF675/7B1LN3U2ay22jfDAVA8pOml5J/0=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "6710d0dd013f55809648dfb1265b8f85447d30a6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "6710d0dd013f55809648dfb1265b8f85447d30a6",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"bip110": "bip110",
|
|
||||||
"btc-clients": "btc-clients",
|
"btc-clients": "btc-clients",
|
||||||
"nix-bitcoin": "nix-bitcoin",
|
"nix-bitcoin": "nix-bitcoin",
|
||||||
"nixpkgs": "nixpkgs_4",
|
"nixpkgs": "nixpkgs_3",
|
||||||
"nixpkgs-stable": "nixpkgs-stable",
|
"nixpkgs-stable": "nixpkgs-stable",
|
||||||
"nixvim": "nixvim"
|
"nixvim": "nixvim"
|
||||||
}
|
}
|
||||||
@@ -315,15 +263,16 @@
|
|||||||
},
|
},
|
||||||
"systems_2": {
|
"systems_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1774449309,
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
|
||||||
"owner": "nix-systems",
|
"owner": "nix-systems",
|
||||||
"repo": "default",
|
"repo": "default",
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-systems",
|
"owner": "nix-systems",
|
||||||
|
"ref": "future-26.11",
|
||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,10 @@
|
|||||||
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
|
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
|
||||||
nixvim.url = "github:nix-community/nixvim";
|
nixvim.url = "github:nix-community/nixvim";
|
||||||
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
|
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
|
||||||
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-24.11";
|
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
|
||||||
bip110.url = "github:emmanuelrosa/bitcoin-knots-bip-110-nix";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, bip110, ... }:
|
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
overlay-stable = final: prev: {
|
overlay-stable = final: prev: {
|
||||||
@@ -56,7 +55,6 @@
|
|||||||
btc-clients.packages.${pkgs.system}.bisq2
|
btc-clients.packages.${pkgs.system}.bisq2
|
||||||
btc-clients.packages.${pkgs.system}.sparrow
|
btc-clients.packages.${pkgs.system}.sparrow
|
||||||
];
|
];
|
||||||
sovran_systemsOS.packages.bip110 = bip110.packages.${pkgs.system}.bitcoind-knots-bip-110;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#153126"/>
|
||||||
|
<stop offset="55%" stop-color="#0F241B"/>
|
||||||
|
<stop offset="100%" stop-color="#091C14"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#42F39A"/>
|
||||||
|
<stop offset="45%" stop-color="#28D978"/>
|
||||||
|
<stop offset="100%" stop-color="#1AA45D"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#27C86F"/>
|
||||||
|
<stop offset="100%" stop-color="#157E49"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feOffset dx="0" dy="2"/>
|
||||||
|
<feGaussianBlur stdDeviation="5" result="blur"/>
|
||||||
|
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 .18 0"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
|
||||||
|
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
|
||||||
|
fill="none" stroke="rgba(255,255,255,0.08)"/>
|
||||||
|
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
|
||||||
|
fill="none" filter="url(#innerShade)"/>
|
||||||
|
|
||||||
|
<path d="M128 32 A96 96 0 1 1 58 196"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#outerArc)"
|
||||||
|
stroke-width="12"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<path d="M128 56 A72 72 0 1 1 76 178"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#innerArc)"
|
||||||
|
stroke-width="10"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
|
||||||
|
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
theme = pkgs.callPackage ./plymouth-theme.nix {};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
boot.plymouth.enable = true;
|
|
||||||
boot.plymouth.theme = "sovran";
|
|
||||||
boot.plymouth.themePackages = [ theme ];
|
|
||||||
boot.kernelParams = [ "quiet" "splash" ];
|
|
||||||
boot.initrd.systemd.enable = true;
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ in
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
|
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
|
||||||
./branding.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
image.baseName = lib.mkForce "Sovran_SystemsOS";
|
image.baseName = lib.mkForce "Sovran_SystemsOS";
|
||||||
|
|||||||
+119
-13
@@ -8,6 +8,7 @@ import os
|
|||||||
import secrets
|
import secrets
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ DEPLOYED_FLAKE = """\
|
|||||||
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
|
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=staging-dev";
|
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=stable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, Sovran_Systems, ... }@inputs: {
|
outputs = { self, Sovran_Systems, ... }@inputs: {
|
||||||
@@ -156,6 +157,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
self.boot_size = None
|
self.boot_size = None
|
||||||
self.data_disk = None
|
self.data_disk = None
|
||||||
self.data_size = None
|
self.data_size = None
|
||||||
|
self.data_drive_has_timechain = False
|
||||||
self.free_password = None
|
self.free_password = None
|
||||||
|
|
||||||
# Root navigation view
|
# Root navigation view
|
||||||
@@ -361,6 +363,40 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
sep.set_margin_end(40)
|
sep.set_margin_end(40)
|
||||||
outer.append(sep)
|
outer.append(sep)
|
||||||
|
|
||||||
|
notice_frame = Gtk.Frame()
|
||||||
|
notice_frame.add_css_class("card")
|
||||||
|
notice_frame.set_margin_start(40)
|
||||||
|
notice_frame.set_margin_end(40)
|
||||||
|
notice_frame.set_margin_top(20)
|
||||||
|
notice_frame.set_margin_bottom(4)
|
||||||
|
|
||||||
|
notice_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
|
notice_box.set_margin_top(12)
|
||||||
|
notice_box.set_margin_bottom(12)
|
||||||
|
notice_box.set_margin_start(16)
|
||||||
|
notice_box.set_margin_end(16)
|
||||||
|
|
||||||
|
notice_icon = symbolic_icon("dialog-information-symbolic")
|
||||||
|
notice_icon.set_valign(Gtk.Align.START)
|
||||||
|
notice_box.append(notice_icon)
|
||||||
|
|
||||||
|
notice_lbl = Gtk.Label()
|
||||||
|
notice_lbl.set_use_markup(True)
|
||||||
|
notice_lbl.set_wrap(True)
|
||||||
|
notice_lbl.set_xalign(0)
|
||||||
|
notice_lbl.set_halign(Gtk.Align.FILL)
|
||||||
|
notice_lbl.set_markup(
|
||||||
|
"<span weight='bold'>Heads up — Server + Desktop prerequisites</span>\n"
|
||||||
|
"• A domain or subdomain from <span weight='bold'>https://njal.la</span>\n"
|
||||||
|
"• The ability to open / forward ports on your router\n\n"
|
||||||
|
"Don't worry — after install, the onboarding wizard walks you through every step.\n"
|
||||||
|
"<span size='small'>Desktop Only and Node Only do not require a domain or port forwarding.</span>"
|
||||||
|
)
|
||||||
|
notice_box.append(notice_lbl)
|
||||||
|
|
||||||
|
notice_frame.set_child(notice_box)
|
||||||
|
outer.append(notice_frame)
|
||||||
|
|
||||||
# Role label
|
# Role label
|
||||||
role_lbl = Gtk.Label()
|
role_lbl = Gtk.Label()
|
||||||
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
|
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
|
||||||
@@ -667,11 +703,19 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
def push_disk_confirm(self):
|
def push_disk_confirm(self):
|
||||||
"""Show the selected drives and ask the user to type ERASE to confirm."""
|
"""Show the selected drives and ask the user to type ERASE to confirm."""
|
||||||
|
self.data_drive_has_timechain = False
|
||||||
|
if self.data_disk:
|
||||||
|
data_path = f"/dev/{self.data_disk}"
|
||||||
|
self.data_drive_has_timechain = self.detect_existing_timechain_data(data_path)
|
||||||
|
|
||||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
|
||||||
# Disk info group
|
# Disk info group
|
||||||
disk_group = Adw.PreferencesGroup()
|
disk_group = Adw.PreferencesGroup()
|
||||||
disk_group.set_title("Drives to be erased")
|
if self.data_disk and self.data_drive_has_timechain:
|
||||||
|
disk_group.set_title("OS drive to be erased (data drive preserved)")
|
||||||
|
else:
|
||||||
|
disk_group.set_title("Drives to be erased")
|
||||||
disk_group.set_margin_top(24)
|
disk_group.set_margin_top(24)
|
||||||
disk_group.set_margin_start(40)
|
disk_group.set_margin_start(40)
|
||||||
disk_group.set_margin_end(40)
|
disk_group.set_margin_end(40)
|
||||||
@@ -688,12 +732,21 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
data_row.set_subtitle(f"/dev/{self.data_disk} — {human_size(self.data_size)}")
|
data_row.set_subtitle(f"/dev/{self.data_disk} — {human_size(self.data_size)}")
|
||||||
data_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
data_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||||
disk_group.add(data_row)
|
disk_group.add(data_row)
|
||||||
|
if self.data_drive_has_timechain:
|
||||||
|
note_row = Adw.ActionRow()
|
||||||
|
note_row.set_title(f"Existing Bitcoin timechain detected on /dev/{self.data_disk}")
|
||||||
|
note_row.set_subtitle("Data will be preserved and mounted as-is.")
|
||||||
|
note_row.add_prefix(symbolic_icon("emblem-ok-symbolic"))
|
||||||
|
disk_group.add(note_row)
|
||||||
|
|
||||||
outer.append(disk_group)
|
outer.append(disk_group)
|
||||||
|
|
||||||
# Warning banner
|
# Warning banner
|
||||||
banner = Adw.Banner()
|
banner = Adw.Banner()
|
||||||
banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.")
|
if self.data_disk and self.data_drive_has_timechain:
|
||||||
|
banner.set_title("⚠ All data on the OS disk will be permanently destroyed. Existing Bitcoin data disk will be preserved.")
|
||||||
|
else:
|
||||||
|
banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.")
|
||||||
banner.set_revealed(True)
|
banner.set_revealed(True)
|
||||||
banner.set_margin_top(16)
|
banner.set_margin_top(16)
|
||||||
banner.set_margin_start(40)
|
banner.set_margin_start(40)
|
||||||
@@ -775,9 +828,61 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
# ── Worker: partition ─────────────────────────────────────────────────
|
# ── Worker: partition ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def partition_path(self, dev_path, num):
|
||||||
|
return f"{dev_path}p{num}" if "nvme" in dev_path else f"{dev_path}{num}"
|
||||||
|
|
||||||
|
def detect_existing_timechain_data(self, data_path, buf=None):
|
||||||
|
data_p1 = self.partition_path(data_path, 1)
|
||||||
|
if not os.path.exists(data_p1):
|
||||||
|
return False
|
||||||
|
|
||||||
|
label = ""
|
||||||
|
for cmd in (
|
||||||
|
["sudo", "lsblk", "-no", "LABEL", data_p1],
|
||||||
|
["sudo", "blkid", "-o", "value", "-s", "LABEL", data_p1],
|
||||||
|
):
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
stdout = proc.stdout.strip()
|
||||||
|
label = stdout.splitlines()[0] if stdout else ""
|
||||||
|
if label:
|
||||||
|
break
|
||||||
|
|
||||||
|
if label != "BTCEcoandBackup":
|
||||||
|
return False
|
||||||
|
|
||||||
|
check_mount = tempfile.mkdtemp(prefix="sovran-installer-data-check-")
|
||||||
|
mounted = False
|
||||||
|
try:
|
||||||
|
run(["sudo", "mount", "-o", "ro", data_p1, check_mount])
|
||||||
|
mounted = True
|
||||||
|
|
||||||
|
has_bitcoin = os.path.isdir(f"{check_mount}/BTCEcoandBackup/Bitcoin_Node")
|
||||||
|
has_electrs = os.path.isdir(f"{check_mount}/BTCEcoandBackup/Electrs_Data")
|
||||||
|
if has_bitcoin and has_electrs:
|
||||||
|
if buf is not None:
|
||||||
|
GLib.idle_add(
|
||||||
|
append_text,
|
||||||
|
buf,
|
||||||
|
"=== Existing Bitcoin timechain detected on data drive — preserving data ===\n",
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Timechain detection failed for {data_p1} at mount/check step ({check_mount}): {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if mounted:
|
||||||
|
subprocess.run(["sudo", "umount", check_mount], capture_output=True, text=True)
|
||||||
|
subprocess.run(["sudo", "rmdir", check_mount], capture_output=True, text=True)
|
||||||
|
|
||||||
def do_partition(self, buf):
|
def do_partition(self, buf):
|
||||||
boot_path = f"/dev/{self.boot_disk}"
|
boot_path = f"/dev/{self.boot_disk}"
|
||||||
data_path = f"/dev/{self.data_disk}" if self.data_disk else None
|
data_path = f"/dev/{self.data_disk}" if self.data_disk else None
|
||||||
|
self.data_drive_has_timechain = False
|
||||||
|
|
||||||
|
if data_path:
|
||||||
|
self.data_drive_has_timechain = self.detect_existing_timechain_data(data_path, buf)
|
||||||
|
|
||||||
# ── Wipe disk(s) ──
|
# ── Wipe disk(s) ──
|
||||||
GLib.idle_add(append_text, buf, "=== Wiping disk(s) ===\n")
|
GLib.idle_add(append_text, buf, "=== Wiping disk(s) ===\n")
|
||||||
@@ -785,12 +890,12 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
run_stream(["sudo", "sgdisk", "--zap-all", boot_path], buf)
|
run_stream(["sudo", "sgdisk", "--zap-all", boot_path], buf)
|
||||||
run_stream(["sudo", "wipefs", "--all", "--force", boot_path], buf)
|
run_stream(["sudo", "wipefs", "--all", "--force", boot_path], buf)
|
||||||
|
|
||||||
if data_path:
|
if data_path and not self.data_drive_has_timechain:
|
||||||
run_stream(["sudo", "sgdisk", "--zap-all", data_path], buf)
|
run_stream(["sudo", "sgdisk", "--zap-all", data_path], buf)
|
||||||
run_stream(["sudo", "wipefs", "--all", "--force", data_path], buf)
|
run_stream(["sudo", "wipefs", "--all", "--force", data_path], buf)
|
||||||
|
|
||||||
run_stream(["sudo", "partprobe", boot_path], buf)
|
run_stream(["sudo", "partprobe", boot_path], buf)
|
||||||
if data_path:
|
if data_path and not self.data_drive_has_timechain:
|
||||||
run_stream(["sudo", "partprobe", data_path], buf)
|
run_stream(["sudo", "partprobe", data_path], buf)
|
||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@@ -806,7 +911,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# ── Partition data disk (if selected) ──
|
# ── Partition data disk (if selected) ──
|
||||||
if data_path:
|
if data_path and not self.data_drive_has_timechain:
|
||||||
GLib.idle_add(append_text, buf, "\n=== Partitioning data disk ===\n")
|
GLib.idle_add(append_text, buf, "\n=== Partitioning data disk ===\n")
|
||||||
run_stream(["sudo", "sgdisk",
|
run_stream(["sudo", "sgdisk",
|
||||||
"-n", "1:1M:0", "-t", "1:8300", "-c", "1:primary",
|
"-n", "1:1M:0", "-t", "1:8300", "-c", "1:primary",
|
||||||
@@ -817,14 +922,14 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
# ── Format partitions ──
|
# ── Format partitions ──
|
||||||
GLib.idle_add(append_text, buf, "\n=== Formatting partitions ===\n")
|
GLib.idle_add(append_text, buf, "\n=== Formatting partitions ===\n")
|
||||||
boot_p1 = f"{boot_path}p1" if "nvme" in boot_path else f"{boot_path}1"
|
boot_p1 = self.partition_path(boot_path, 1)
|
||||||
boot_p2 = f"{boot_path}p2" if "nvme" in boot_path else f"{boot_path}2"
|
boot_p2 = self.partition_path(boot_path, 2)
|
||||||
|
|
||||||
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
|
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
|
||||||
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
|
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
|
||||||
|
|
||||||
if data_path:
|
if data_path and not self.data_drive_has_timechain:
|
||||||
data_p1 = f"{data_path}p1" if "nvme" in data_path else f"{data_path}1"
|
data_p1 = self.partition_path(data_path, 1)
|
||||||
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "BTCEcoandBackup", data_p1], buf)
|
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "BTCEcoandBackup", data_p1], buf)
|
||||||
|
|
||||||
# ── Mount filesystems ──
|
# ── Mount filesystems ──
|
||||||
@@ -834,7 +939,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
|
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
|
||||||
|
|
||||||
if data_path:
|
if data_path:
|
||||||
data_p1 = f"{data_path}p1" if "nvme" in data_path else f"{data_path}1"
|
data_p1 = self.partition_path(data_path, 1)
|
||||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive"], buf)
|
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive"], buf)
|
||||||
run_stream(["sudo", "mount", data_p1, "/mnt/run/media/Second_Drive"], buf)
|
run_stream(["sudo", "mount", data_p1, "/mnt/run/media/Second_Drive"], buf)
|
||||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"], buf)
|
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"], buf)
|
||||||
@@ -877,6 +982,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
||||||
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
|
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
|
||||||
|
run(["sudo", "chmod", "644", "/mnt/etc/nixos/custom.nix"])
|
||||||
|
|
||||||
# ── Step 4: Ready to install ──────────────────────────────────────────
|
# ── Step 4: Ready to install ──────────────────────────────────────────
|
||||||
|
|
||||||
@@ -988,7 +1094,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
log(proc.stderr)
|
log(proc.stderr)
|
||||||
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
|
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
|
||||||
GLib.idle_add(append_text, buf, "Locking flake to staging-dev...\n")
|
GLib.idle_add(append_text, buf, "Locking flake to stable...\n")
|
||||||
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
|
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
|
||||||
"flake", "lock", "/mnt/etc/nixos"], buf)
|
"flake", "lock", "/mnt/etc/nixos"], buf)
|
||||||
|
|
||||||
@@ -1097,4 +1203,4 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = InstallerApp()
|
app = InstallerApp()
|
||||||
app.run(None)
|
app.run(None)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
{ pkgs, lib }:
|
|
||||||
|
|
||||||
pkgs.stdenv.mkDerivation {
|
|
||||||
pname = "sovran-plymouth-theme";
|
|
||||||
version = "1.0";
|
|
||||||
|
|
||||||
src = ./.;
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/share/plymouth/themes/sovran
|
|
||||||
cp ${./assets/splash-logo.png} $out/share/plymouth/themes/sovran/logo.png
|
|
||||||
|
|
||||||
cat > $out/share/plymouth/themes/sovran/sovran.plymouth <<EOF
|
|
||||||
[Plymouth Theme]
|
|
||||||
Name=Sovran Systems
|
|
||||||
Description=Sovran Systems Splash
|
|
||||||
ModuleName=script
|
|
||||||
|
|
||||||
[script]
|
|
||||||
ImageDir=$out/share/plymouth/themes/sovran
|
|
||||||
ScriptFile=$out/share/plymouth/themes/sovran/sovran.script
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > $out/share/plymouth/themes/sovran/sovran.script <<'EOF'
|
|
||||||
# Background color: #CFFFD7 (RGB 207,255,215)
|
|
||||||
bg_r = 207/255.0
|
|
||||||
bg_g = 255/255.0
|
|
||||||
bg_b = 215/255.0
|
|
||||||
|
|
||||||
Window.SetBackgroundTopColor (bg_r, bg_g, bg_b);
|
|
||||||
Window.SetBackgroundBottomColor (bg_r, bg_g, bg_b);
|
|
||||||
|
|
||||||
logo = Image("logo.png");
|
|
||||||
logo_sprite = Sprite(logo);
|
|
||||||
logo_sprite.SetX((Window.GetWidth() - logo.GetWidth()) / 2);
|
|
||||||
logo_sprite.SetY((Window.GetHeight() - logo.GetHeight()) / 2);
|
|
||||||
EOF
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -24,6 +24,7 @@ ROLE="server"
|
|||||||
DEPLOY_KEY=""
|
DEPLOY_KEY=""
|
||||||
HEADSCALE_SERVER=""
|
HEADSCALE_SERVER=""
|
||||||
HEADSCALE_KEY=""
|
HEADSCALE_KEY=""
|
||||||
|
DATA_DISK_HAS_TIMECHAIN=false
|
||||||
|
|
||||||
FLAKE="/etc/sovran/flake"
|
FLAKE="/etc/sovran/flake"
|
||||||
LOG="/tmp/sovran-headless-install.log"
|
LOG="/tmp/sovran-headless-install.log"
|
||||||
@@ -102,19 +103,42 @@ part_suffix() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Detect existing Bitcoin timechain data on data disk ───────────────────────
|
||||||
|
if [[ -n "$DATA_DISK" ]]; then
|
||||||
|
DATA_P1=$(part_suffix "$DATA_DISK" 1)
|
||||||
|
if [[ -b "$DATA_P1" ]]; then
|
||||||
|
DATA_LABEL=$(lsblk -no LABEL "$DATA_P1" 2>/dev/null | head -n1 || true)
|
||||||
|
if [[ -z "$DATA_LABEL" ]]; then
|
||||||
|
DATA_LABEL=$(blkid -o value -s LABEL "$DATA_P1" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DATA_LABEL" == "BTCEcoandBackup" ]]; then
|
||||||
|
CHECK_MOUNT=$(mktemp -d /tmp/sovran-data-check.XXXXXX)
|
||||||
|
if mount -o ro "$DATA_P1" "$CHECK_MOUNT" 2>/dev/null; then
|
||||||
|
if [[ -d "$CHECK_MOUNT/BTCEcoandBackup/Bitcoin_Node" && -d "$CHECK_MOUNT/BTCEcoandBackup/Electrs_Data" ]]; then
|
||||||
|
DATA_DISK_HAS_TIMECHAIN=true
|
||||||
|
log "Existing Bitcoin timechain detected on data drive — preserving data"
|
||||||
|
fi
|
||||||
|
umount "$CHECK_MOUNT" || true
|
||||||
|
fi
|
||||||
|
rmdir "$CHECK_MOUNT" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Step 1: Wipe disks ────────────────────────────────────────────────────────
|
# ── Step 1: Wipe disks ────────────────────────────────────────────────────────
|
||||||
log "=== Wiping disk(s) ==="
|
log "=== Wiping disk(s) ==="
|
||||||
|
|
||||||
sgdisk --zap-all "$DISK"
|
sgdisk --zap-all "$DISK"
|
||||||
wipefs --all --force "$DISK"
|
wipefs --all --force "$DISK"
|
||||||
|
|
||||||
if [[ -n "$DATA_DISK" ]]; then
|
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
|
||||||
sgdisk --zap-all "$DATA_DISK"
|
sgdisk --zap-all "$DATA_DISK"
|
||||||
wipefs --all --force "$DATA_DISK"
|
wipefs --all --force "$DATA_DISK"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
partprobe "$DISK"
|
partprobe "$DISK"
|
||||||
[[ -n "$DATA_DISK" ]] && partprobe "$DATA_DISK"
|
[[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]] && partprobe "$DATA_DISK"
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# ── Step 2: Partition OS disk ─────────────────────────────────────────────────
|
# ── Step 2: Partition OS disk ─────────────────────────────────────────────────
|
||||||
@@ -129,7 +153,7 @@ partprobe "$DISK"
|
|||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# ── Step 3: Partition data disk (if present) ──────────────────────────────────
|
# ── Step 3: Partition data disk (if present) ──────────────────────────────────
|
||||||
if [[ -n "$DATA_DISK" ]]; then
|
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
|
||||||
log "=== Partitioning data disk ==="
|
log "=== Partitioning data disk ==="
|
||||||
sgdisk \
|
sgdisk \
|
||||||
-n "1:1M:0" -t "1:8300" -c "1:primary" \
|
-n "1:1M:0" -t "1:8300" -c "1:primary" \
|
||||||
@@ -147,7 +171,7 @@ BOOT_P2=$(part_suffix "$DISK" 2)
|
|||||||
mkfs.vfat -F 32 "$BOOT_P1"
|
mkfs.vfat -F 32 "$BOOT_P1"
|
||||||
mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2"
|
mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2"
|
||||||
|
|
||||||
if [[ -n "$DATA_DISK" ]]; then
|
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
|
||||||
DATA_P1=$(part_suffix "$DATA_DISK" 1)
|
DATA_P1=$(part_suffix "$DATA_DISK" 1)
|
||||||
mkfs.ext4 -F -L BTCEcoandBackup "$DATA_P1"
|
mkfs.ext4 -F -L BTCEcoandBackup "$DATA_P1"
|
||||||
fi
|
fi
|
||||||
@@ -221,6 +245,7 @@ if [[ -n "$DEPLOY_KEY" || -n "$HEADSCALE_SERVER" ]]; then
|
|||||||
} > /mnt/etc/nixos/custom.nix
|
} > /mnt/etc/nixos/custom.nix
|
||||||
else
|
else
|
||||||
cp /mnt/etc/nixos/custom.template.nix /mnt/etc/nixos/custom.nix
|
cp /mnt/etc/nixos/custom.template.nix /mnt/etc/nixos/custom.nix
|
||||||
|
chmod 644 /mnt/etc/nixos/custom.nix
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Write Headscale auth key if provided ─────────────────────────────────────
|
# ── Write Headscale auth key if provided ─────────────────────────────────────
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.sovran_systemsOS;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.sovran_systemsOS.packages.bip110 = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.package;
|
|
||||||
default = null;
|
|
||||||
description = "BIP110 Bitcoin package";
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf (
|
|
||||||
cfg.features.bip110 &&
|
|
||||||
cfg.packages.bip110 != null
|
|
||||||
) {
|
|
||||||
services.bitcoind.package = lib.mkForce cfg.packages.bip110;
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
cfg.packages.bip110
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
|
|
||||||
services.bitcoind = {
|
services.bitcoind = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = config.nix-bitcoin.pkgs.bitcoind-knots;
|
package = pkgs.bitcoind-knots;
|
||||||
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
|
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
|
||||||
txindex = true;
|
txindex = true;
|
||||||
tor.proxy = true;
|
tor.proxy = true;
|
||||||
@@ -73,11 +73,19 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
systemd.services.bitcoind = {
|
systemd.services.bitcoind = {
|
||||||
requires = [ "run-media-Second_Drive.mount" ];
|
requires = [ "run-media-Second_Drive.mount" ];
|
||||||
after = [ "run-media-Second_Drive.mount" ];
|
after = [ "run-media-Second_Drive.mount" ];
|
||||||
|
serviceConfig.PrivateUsers = lib.mkForce false;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.electrs = {
|
systemd.services.electrs = {
|
||||||
requires = [ "run-media-Second_Drive.mount" ];
|
requires = lib.mkForce [ "run-media-Second_Drive.mount" ];
|
||||||
after = [ "run-media-Second_Drive.mount" ];
|
after = [ "run-media-Second_Drive.mount" "bitcoind.service" ];
|
||||||
|
wants = [ "bitcoind.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.lnd = {
|
||||||
|
wants = [ "bitcoind.service" ];
|
||||||
|
# requires for bitcoind set by nix-bitcoin; mkForce removes it
|
||||||
|
requires = lib.mkForce [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.sovran-btc-permissions = {
|
systemd.services.sovran-btc-permissions = {
|
||||||
@@ -102,7 +110,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
networking.firewall.allowedTCPPorts = [ 3051 ];
|
networking.firewall.allowedTCPPorts = [ 3051 ];
|
||||||
networking.firewall.allowedUDPPorts = [ 3051 ];
|
networking.firewall.allowedUDPPorts = [ 3051 ];
|
||||||
|
|
||||||
sovran_systemsOS.domainRequirements = [
|
sovran_systemsOS.domainRequirements = [
|
||||||
{ name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; }
|
{ name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ EOF
|
|||||||
$WORDPRESS {
|
$WORDPRESS {
|
||||||
encode gzip zstd
|
encode gzip zstd
|
||||||
root * /var/lib/www/wordpress
|
root * /var/lib/www/wordpress
|
||||||
php_fastcgi unix//run/phpfpm/mypool.sock
|
php_fastcgi unix//run/phpfpm/wordpress.sock
|
||||||
file_server browse
|
file_server browse
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
@@ -123,7 +123,7 @@ EOF
|
|||||||
$NEXTCLOUD {
|
$NEXTCLOUD {
|
||||||
encode gzip zstd
|
encode gzip zstd
|
||||||
root * /var/lib/www/nextcloud
|
root * /var/lib/www/nextcloud
|
||||||
php_fastcgi unix//run/phpfpm/mypool.sock {
|
php_fastcgi unix//run/phpfpm/nextcloud.sock {
|
||||||
trusted_proxies private_ranges
|
trusted_proxies private_ranges
|
||||||
}
|
}
|
||||||
file_server
|
file_server
|
||||||
@@ -189,7 +189,7 @@ EOF
|
|||||||
http://sovransystemsos.local {
|
http://sovransystemsos.local {
|
||||||
reverse_proxy localhost:8937
|
reverse_proxy localhost:8937
|
||||||
header {
|
header {
|
||||||
Clear-Site-Data "\"cache\", \"cookies\", \"storage\""
|
Clear-Site-Data "\"cache\""
|
||||||
Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||||
Pragma "no-cache"
|
Pragma "no-cache"
|
||||||
Expires "0"
|
Expires "0"
|
||||||
@@ -221,4 +221,4 @@ ${extraVhosts}
|
|||||||
CUSTOM_VHOSTS_EOF
|
CUSTOM_VHOSTS_EOF
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# ── modules/core/cpu-performance.nix ──────────────────────────────────────────
|
||||||
|
# Forces all CPU cores to run at maximum frequency on node and server_plus_desktop
|
||||||
|
# roles. Desktop-only installs retain normal OS power management behaviour.
|
||||||
|
#
|
||||||
|
# Three layers:
|
||||||
|
# 1. power-profiles-daemon disabled — removes the GNOME power profile picker;
|
||||||
|
# no user can switch profiles
|
||||||
|
# 2. cpufreq performance governor — pins every core to max frequency via
|
||||||
|
# kernel, enforced at boot by a oneshot unit
|
||||||
|
# 3. systemd oneshot enforcement — belt-and-suspenders; applies the governor
|
||||||
|
# after every boot even if module loads late
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
config = lib.mkIf (!config.sovran_systemsOS.roles.desktop) {
|
||||||
|
|
||||||
|
# ── Layer 1: disable power-profiles-daemon ───────────────────────────────
|
||||||
|
# This removes the power-profile switcher from GNOME Settings entirely.
|
||||||
|
services.power-profiles-daemon.enable = false;
|
||||||
|
|
||||||
|
# ── Layer 2: set cpufreq governor to performance ─────────────────────────
|
||||||
|
# Pins all cores to max frequency. Works on Intel (intel_pstate) and AMD
|
||||||
|
# (amd-pstate / acpi-cpufreq) alike.
|
||||||
|
powerManagement.cpuFreqGovernor = "performance";
|
||||||
|
|
||||||
|
# ── Layer 3: enforce at boot via systemd oneshot ─────────────────────────
|
||||||
|
# Belt-and-suspenders: ensures the governor is applied after every boot even
|
||||||
|
# if the kernel module loads late.
|
||||||
|
systemd.services.cpu-performance = {
|
||||||
|
description = "Set CPU governor to performance on all cores";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "systemd-modules-load.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
found=0
|
||||||
|
for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
||||||
|
if [ -w "$gov" ]; then
|
||||||
|
echo performance > "$gov"
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
echo "cpu-performance: no writable cpufreq governors found (VM or unsupported hardware)" >&2
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# ── modules/core/no-sleep.nix ─────────────────────────────────────────────────
|
||||||
|
# Prevents the machine from ever sleeping or suspending at the system level.
|
||||||
|
#
|
||||||
|
# Only applies to server_plus_desktop and node roles. Desktop-only installs
|
||||||
|
# retain normal OS sleep/suspend behaviour.
|
||||||
|
#
|
||||||
|
# This operates at two layers below GNOME:
|
||||||
|
# 1. systemd-logind — ignores all hardware power events (lid, suspend key, etc.)
|
||||||
|
# 2. systemd targets — masks sleep/suspend/hibernate targets so nothing can
|
||||||
|
# trigger them, not even `systemctl suspend` or D-Bus calls.
|
||||||
|
#
|
||||||
|
# This is intentional for a 24/7 server/node. The GNOME-layer power settings in
|
||||||
|
# sovran_systemsos-desktop.nix remain in place as a belt-and-suspenders complement
|
||||||
|
# for active user sessions.
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
config = lib.mkIf (!config.sovran_systemsOS.roles.desktop) {
|
||||||
|
|
||||||
|
# ── Layer 1: logind hardware event handling ──────────────────────────────
|
||||||
|
services.logind.settings.Login = {
|
||||||
|
HandleLidSwitch = "ignore";
|
||||||
|
HandleLidSwitchDocked = "ignore";
|
||||||
|
HandleLidSwitchExternalPower = "ignore";
|
||||||
|
HandleSuspendKey = "ignore";
|
||||||
|
HandleHibernateKey = "ignore";
|
||||||
|
HandlePowerKey = "ignore";
|
||||||
|
IdleAction = "ignore";
|
||||||
|
IdleActionSec = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── Layer 2: mask systemd sleep targets ─────────────────────────────────
|
||||||
|
# Nothing on the system can suspend/hibernate — not root, not GNOME, not D-Bus.
|
||||||
|
systemd.targets.sleep.enable = false;
|
||||||
|
systemd.targets.suspend.enable = false;
|
||||||
|
systemd.targets.hibernate.enable = false;
|
||||||
|
systemd.targets.hybrid-sleep.enable = false;
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
# ── Bitcoin Node Only Role ────────────────────────────────
|
# ── Bitcoin Node Only Role ────────────────────────────────
|
||||||
# Bitcoin ecosystem + mempool + bip110, BTCPay runs but not exposed via Caddy
|
# Bitcoin ecosystem + mempool, BTCPay runs but not exposed via Caddy
|
||||||
(lib.mkIf config.sovran_systemsOS.roles.node {
|
(lib.mkIf config.sovran_systemsOS.roles.node {
|
||||||
sovran_systemsOS.services = {
|
sovran_systemsOS.services = {
|
||||||
bitcoin = lib.mkDefault true;
|
bitcoin = lib.mkDefault true;
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
|
|
||||||
sovran_systemsOS.features = {
|
sovran_systemsOS.features = {
|
||||||
mempool = lib.mkDefault true;
|
mempool = lib.mkDefault true;
|
||||||
bip110 = lib.mkDefault true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sovran_systemsOS.web.btcpayserver = lib.mkDefault false;
|
sovran_systemsOS.web.btcpayserver = lib.mkDefault false;
|
||||||
|
|||||||
+24
-1
@@ -43,12 +43,24 @@
|
|||||||
# ── Features (default OFF — user can enable in custom.nix) ──
|
# ── Features (default OFF — user can enable in custom.nix) ──
|
||||||
features = {
|
features = {
|
||||||
haven = lib.mkEnableOption "Haven NOSTR relay";
|
haven = lib.mkEnableOption "Haven NOSTR relay";
|
||||||
bip110 = lib.mkEnableOption "BIP-110 Bitcoin Better Money";
|
|
||||||
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
|
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
|
||||||
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
||||||
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
|
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
|
||||||
rdp = lib.mkEnableOption "Gnome Remote Desktop";
|
rdp = lib.mkEnableOption "Gnome Remote Desktop";
|
||||||
sshd = lib.mkEnableOption "SSH remote access";
|
sshd = lib.mkEnableOption "SSH remote access";
|
||||||
|
|
||||||
|
# Deprecated: BIP-110 is now built into mainline Bitcoin Knots and is the
|
||||||
|
# default node. This option is retained ONLY so that existing machines with
|
||||||
|
# `sovran_systemsOS.features.bip110 = lib.mkForce true;` left in their local
|
||||||
|
# custom.nix continue to evaluate. It has no effect and will be removed in a
|
||||||
|
# future release once the Hub has cleaned up old custom.nix files.
|
||||||
|
bip110 = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.bool;
|
||||||
|
default = null;
|
||||||
|
internal = true;
|
||||||
|
visible = false;
|
||||||
|
description = "(Deprecated, no-op) BIP-110 is now built into Bitcoin Knots.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# ── Web exposure (controls Caddy vhosts) ──────────────────
|
# ── Web exposure (controls Caddy vhosts) ──────────────────
|
||||||
@@ -89,4 +101,15 @@
|
|||||||
description = "Nostr public key (npub1...) for Haven relay";
|
description = "Nostr public key (npub1...) for Haven relay";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf (config.sovran_systemsOS.features.bip110 != null) {
|
||||||
|
warnings = [
|
||||||
|
''
|
||||||
|
sovran_systemsOS.features.bip110 is deprecated and has no effect:
|
||||||
|
BIP-110 is now built into mainline Bitcoin Knots, which is the default node.
|
||||||
|
You can safely remove the `sovran_systemsOS.features.bip110` line from
|
||||||
|
/etc/nixos/custom.nix. The Sovran Hub will also remove it automatically.
|
||||||
|
''
|
||||||
|
];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-23
@@ -24,52 +24,47 @@ let
|
|||||||
{ label = "Username"; file = "/var/lib/gnome-remote-desktop/rdp-username"; }
|
{ label = "Username"; file = "/var/lib/gnome-remote-desktop/rdp-username"; }
|
||||||
{ label = "Password"; file = "/var/lib/gnome-remote-desktop/rdp-password"; }
|
{ label = "Password"; file = "/var/lib/gnome-remote-desktop/rdp-password"; }
|
||||||
{ label = "Address"; file = "/var/lib/secrets/internal-ip"; suffix = ":3389"; }
|
{ label = "Address"; file = "/var/lib/secrets/internal-ip"; suffix = ":3389"; }
|
||||||
{ label = "How to Connect"; value = "1. Install an RDP client (e.g. Remmina, Microsoft Remote Desktop)\n2. Create a new RDP connection\n3. Enter the Address above as the host\n4. Enter the Username and Password above\n5. Connect — you will see your desktop remotely"; }
|
{ label = "How to Connect"; value = "1. Install an RDP client (e.g. Remmina, Microsoft Remote Desktop)\n2. Create a new RDP connection\n3. Enter the Address above as the host\n4. Enter the Username and Password above"; }
|
||||||
]; }
|
]; }
|
||||||
]
|
]
|
||||||
# ── Bitcoin Base (node implementations) ────────────────────
|
# ── Bitcoin Base (node implementations) ────────────────────
|
||||||
++ lib.optionals cfg.services.bitcoin [
|
++ lib.optionals cfg.services.bitcoin [
|
||||||
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.features.bip110; category = "bitcoin-base"; credentials = [
|
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
|
||||||
{ label = "Tor Address"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
||||||
]; }
|
|
||||||
{ name = "Bitcoin Knots"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core && !cfg.features.bip110; category = "bitcoin-base"; credentials = [
|
|
||||||
{ label = "Tor Address"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
|
||||||
]; }
|
]; }
|
||||||
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
|
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
|
||||||
{ label = "Tor Address"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
||||||
]; }
|
]; }
|
||||||
]
|
]
|
||||||
# ── Bitcoin Apps (services on top of the node) ─────────────
|
# ── Bitcoin Apps (services on top of the node) ─────────────
|
||||||
++ lib.optionals cfg.services.bitcoin [
|
++ lib.optionals cfg.services.bitcoin [
|
||||||
{ name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
{ name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||||
{ label = "Tor Address"; file = "/var/lib/tor/onion/electrs/hostname"; prefix = "http://"; }
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/electrs/hostname"; prefix = "http://"; }
|
||||||
{ label = "Port"; value = "50001"; }
|
{ label = "Port"; value = "50001"; }
|
||||||
]; }
|
]; }
|
||||||
{ name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = []; }
|
{ name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = []; }
|
||||||
{ name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
{ name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||||
{ label = "Tor Access"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
|
||||||
{ label = "Local Network"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3051"; }
|
{ label = "Local Network — Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3051"; }
|
||||||
{ label = "Password"; file = "/etc/nix-bitcoin-secrets/rtl-password"; }
|
{ label = "Password"; file = "/etc/nix-bitcoin-secrets/rtl-password"; }
|
||||||
|
{ label = "How to Access"; value = "• Tor Address: Open in Tor Browser from any device, anywhere in the world\n• Local Network: Open in any browser, but only when connected to your home network"; }
|
||||||
]; }
|
]; }
|
||||||
{ name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
{ name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.web.btcpayserver; category = "bitcoin-apps"; credentials = [
|
||||||
{ label = "URL"; file = "/var/lib/domains/btcpayserver"; prefix = "https://"; }
|
{ label = "URL"; file = "/var/lib/domains/btcpayserver"; prefix = "https://"; }
|
||||||
{ label = "Note"; value = "Create your admin account on first visit"; }
|
{ label = "Note"; value = "Create your admin account on first visit"; }
|
||||||
]; }
|
]; }
|
||||||
{ name = "Zeus Connect"; unit = "zeus-connect-setup.service"; type = "system"; icon = "zeus"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
{ name = "Zeus Connect"; unit = "zeus-connect-setup.service"; type = "system"; icon = "zeus"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||||
{ label = "Connection URL"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; }
|
{ label = "QR Code"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; qronly = true; }
|
||||||
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus → Scan Node Config\n3. Scan the QR code above or paste the Connection URL"; }
|
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus → Scan Node Config\n3. Scan the QR code above"; }
|
||||||
]; }
|
]; }
|
||||||
{ name = "Sparrow Auto-Link"; unit = "sparrow-autoconnect.service"; type = "system"; icon = "sparrow"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
{ name = "Sparrow Auto-Link"; unit = "sparrow-autoconnect.service"; type = "system"; icon = "sparrow"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||||
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
|
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
|
||||||
{ label = "Status"; value = "Auto-configured on first boot"; }
|
{ label = "Status"; value = "Auto-configured on first boot"; }
|
||||||
]; }
|
]; }
|
||||||
{ name = "Bisq Auto-Link"; unit = "bisq-autoconnect.service"; type = "system"; icon = "bisq"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
|
||||||
{ label = "Node"; value = "127.0.0.1:8333 (Bitcoin Core)"; }
|
|
||||||
{ label = "Status"; value = "Auto-configured on first boot"; }
|
|
||||||
]; }
|
|
||||||
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
|
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
|
||||||
{ label = "Tor Access"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
|
||||||
{ label = "Local Network"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
|
{ label = "Local Network — Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
|
||||||
|
{ label = "How to Access"; value = "• Tor Address: Open in Tor Browser from any device, anywhere in the world\n• Local Network: Open in any browser, but only when connected to your home network"; }
|
||||||
]; }
|
]; }
|
||||||
]
|
]
|
||||||
# ── Communication (server+desktop only) ────────────────────
|
# ── Communication (server+desktop only) ────────────────────
|
||||||
@@ -140,15 +135,43 @@ let
|
|||||||
RC=0
|
RC=0
|
||||||
|
|
||||||
echo "── Step 1/3: nix flake update ────────────────────"
|
echo "── Step 1/3: nix flake update ────────────────────"
|
||||||
if ! nix flake update --flake /etc/nixos --print-build-logs 2>&1; then
|
if ! nix flake update --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1; then
|
||||||
echo "[ERROR] nix flake update failed"
|
echo "[ERROR] nix flake update failed"
|
||||||
RC=1
|
RC=1
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ "$RC" -eq 0 ]; then
|
if [ "$RC" -eq 0 ]; then
|
||||||
echo "── Step 2/3: nixos-rebuild switch ──────────────────"
|
echo "── Step 2/3: nixos-rebuild ──────────────────────────"
|
||||||
if ! nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1; then
|
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1)
|
||||||
|
SWITCH_RC=$?
|
||||||
|
echo "$SWITCH_OUT"
|
||||||
|
if [ "$SWITCH_RC" -eq 0 ]; then
|
||||||
|
echo "[OK] switch succeeded"
|
||||||
|
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
|
||||||
|
echo ""
|
||||||
|
echo " ✓ Build succeeded — a reboot is required to apply this update"
|
||||||
|
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
||||||
|
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1; then
|
||||||
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "[ERROR] nixos-rebuild boot also failed"
|
||||||
|
RC=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
echo "[ERROR] nixos-rebuild switch failed"
|
echo "[ERROR] nixos-rebuild switch failed"
|
||||||
RC=1
|
RC=1
|
||||||
fi
|
fi
|
||||||
@@ -195,12 +218,34 @@ let
|
|||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Rebuilding system configuration ──────────────"
|
echo "── Rebuilding system configuration ──────────────"
|
||||||
if nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1; then
|
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1)
|
||||||
|
SWITCH_RC=$?
|
||||||
|
echo "$SWITCH_OUT"
|
||||||
|
if [ "$SWITCH_RC" -eq 0 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " ✓ Rebuild completed successfully"
|
echo " ✓ Rebuild completed successfully"
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo "SUCCESS" > "$STATUS"
|
echo "SUCCESS" > "$STATUS"
|
||||||
|
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
|
||||||
|
echo ""
|
||||||
|
echo " ✓ Build succeeded — a reboot is required to apply this rebuild"
|
||||||
|
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
||||||
|
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1; then
|
||||||
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
||||||
|
else
|
||||||
|
echo "[ERROR] nixos-rebuild boot also failed"
|
||||||
|
echo "FAILED" > "$STATUS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ let
|
|||||||
|
|
||||||
# Fresh install — no user-db exists yet, apply full Sovran theme below
|
# Fresh install — no user-db exists yet, apply full Sovran theme below
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.config"
|
||||||
|
cat > "$HOME/.config/mimeapps.list" << EOF
|
||||||
|
[Default Applications]
|
||||||
|
text/html=brave-browser.desktop
|
||||||
|
x-scheme-handler/http=brave-browser.desktop
|
||||||
|
x-scheme-handler/https=brave-browser.desktop
|
||||||
|
x-scheme-handler/about=brave-browser.desktop
|
||||||
|
x-scheme-handler/unknown=brave-browser.desktop
|
||||||
|
EOF
|
||||||
|
|
||||||
${pkgs.dconf}/bin/dconf load / << EOF
|
${pkgs.dconf}/bin/dconf load / << EOF
|
||||||
[org/gnome/desktop/interface]
|
[org/gnome/desktop/interface]
|
||||||
color-scheme='prefer-dark'
|
color-scheme='prefer-dark'
|
||||||
@@ -422,4 +432,14 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
xdg.mime.defaultApplications = {
|
||||||
|
"text/html" = "brave-browser.desktop";
|
||||||
|
"x-scheme-handler/http" = "brave-browser.desktop";
|
||||||
|
"x-scheme-handler/https" = "brave-browser.desktop";
|
||||||
|
"x-scheme-handler/about" = "brave-browser.desktop";
|
||||||
|
"x-scheme-handler/unknown" = "brave-browser.desktop";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.sessionVariables.BROWSER = "brave-browser";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-24
@@ -33,8 +33,8 @@ let
|
|||||||
echo "$NEW_PASS" > "$SECRET_FILE"
|
echo "$NEW_PASS" > "$SECRET_FILE"
|
||||||
chmod 600 "$SECRET_FILE"
|
chmod 600 "$SECRET_FILE"
|
||||||
echo "Password for 'free' updated and saved."
|
echo "Password for 'free' updated and saved."
|
||||||
# Delete the old GNOME Keyring so it is recreated with the new password on next GDM login.
|
# Delete the old GNOME Keyring databases so a fresh one is created on next GDM login.
|
||||||
rm -rf /home/free/.local/share/keyrings/*
|
rm -f /home/free/.local/share/keyrings/*.keyring
|
||||||
echo "GNOME Keyring files cleared — a fresh keyring will be created on next login."
|
echo "GNOME Keyring files cleared — a fresh keyring will be created on next login."
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
@@ -87,6 +87,7 @@ in
|
|||||||
};
|
};
|
||||||
path = [ pkgs.shadow pkgs.coreutils ];
|
path = [ pkgs.shadow pkgs.coreutils ];
|
||||||
script = ''
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
SECRET_FILE="/var/lib/secrets/root-password"
|
SECRET_FILE="/var/lib/secrets/root-password"
|
||||||
if [ ! -f "$SECRET_FILE" ]; then
|
if [ ! -f "$SECRET_FILE" ]; then
|
||||||
mkdir -p /var/lib/secrets
|
mkdir -p /var/lib/secrets
|
||||||
@@ -118,36 +119,117 @@ in
|
|||||||
systemd.services.free-password-setup = {
|
systemd.services.free-password-setup = {
|
||||||
description = "Generate and set a random 'free' user password";
|
description = "Generate and set a random 'free' user password";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "display-manager.service" ];
|
||||||
|
after = [ "systemd-user-sessions.service" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
};
|
};
|
||||||
path = [ pkgs.shadow pkgs.coreutils ];
|
path = [ pkgs.shadow pkgs.coreutils ];
|
||||||
script = ''
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
SECRET_FILE="/var/lib/secrets/free-password"
|
SECRET_FILE="/var/lib/secrets/free-password"
|
||||||
if [ ! -f "$SECRET_FILE" ]; then
|
PENDING_FILE="/var/lib/secrets/free-password-migration-pending"
|
||||||
mkdir -p /var/lib/secrets
|
|
||||||
# Generate a diceware-style passphrase: word-word-word-N
|
if [ -f "$SECRET_FILE" ]; then
|
||||||
WORDS="apple barn brook cabin cedar cloud coral crane delta eagle ember \
|
echo "free:$(cat "$SECRET_FILE")" | chpasswd
|
||||||
fern field flame flora flint frost grove haven hedge holly heron \
|
exit 0
|
||||||
jade juniper kelp larch lemon lilac linden loch lotus maple marsh \
|
|
||||||
meadow mist mossy mount oak ocean olive petal pine pixel plum pond \
|
|
||||||
prism quartz raven ridge river robin rocky rose rowan sage sand \
|
|
||||||
sierra silver slate snow solar spark spruce stone storm summit \
|
|
||||||
swift thorn tide timber torch trout vale vault vine walnut wave \
|
|
||||||
willow wren amber aspen birch blaze bloom bluff coast copper crest \
|
|
||||||
dune elder fjord forge glade glen glow gulf"
|
|
||||||
WORD_ARRAY=($WORDS)
|
|
||||||
COUNT=''${#WORD_ARRAY[@]}
|
|
||||||
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
|
||||||
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
|
||||||
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
|
||||||
DIGIT=$((RANDOM % 10))
|
|
||||||
FREE_PASS="$W1-$W2-$W3-$DIGIT"
|
|
||||||
echo "$FREE_PASS" > "$SECRET_FILE"
|
|
||||||
chmod 600 "$SECRET_FILE"
|
|
||||||
fi
|
fi
|
||||||
echo "free:$(cat "$SECRET_FILE")" | chpasswd
|
|
||||||
|
SHADOW_HASH=""
|
||||||
|
while IFS=: read -r user hash _; do
|
||||||
|
if [ "$user" = "free" ]; then
|
||||||
|
SHADOW_HASH="$hash"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done < /etc/shadow
|
||||||
|
|
||||||
|
HAS_REAL_HASH=0
|
||||||
|
case "$SHADOW_HASH" in
|
||||||
|
""|"!"|"*"|"!!"|"!"*|"*"*)
|
||||||
|
HAS_REAL_HASH=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
HAS_REAL_HASH=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$HAS_REAL_HASH" -eq 1 ]; then
|
||||||
|
mkdir -p /var/lib/secrets
|
||||||
|
touch "$PENDING_FILE"
|
||||||
|
chmod 600 "$PENDING_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /var/lib/secrets
|
||||||
|
# Generate a diceware-style passphrase: word-word-word-N
|
||||||
|
WORDS="apple barn brook cabin cedar cloud coral crane delta eagle ember \
|
||||||
|
fern field flame flora flint frost grove haven hedge holly heron \
|
||||||
|
jade juniper kelp larch lemon lilac linden loch lotus maple marsh \
|
||||||
|
meadow mist mossy mount oak ocean olive petal pine pixel plum pond \
|
||||||
|
prism quartz raven ridge river robin rocky rose rowan sage sand \
|
||||||
|
sierra silver slate snow solar spark spruce stone storm summit \
|
||||||
|
swift thorn tide timber torch trout vale vault vine walnut wave \
|
||||||
|
willow wren amber aspen birch blaze bloom bluff coast copper crest \
|
||||||
|
dune elder fjord forge glade glen glow gulf"
|
||||||
|
WORD_ARRAY=($WORDS)
|
||||||
|
COUNT=''${#WORD_ARRAY[@]}
|
||||||
|
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
DIGIT=$((RANDOM % 10))
|
||||||
|
FREE_PASS="$W1-$W2-$W3-$DIGIT"
|
||||||
|
echo "$FREE_PASS" > "$SECRET_FILE"
|
||||||
|
chmod 600 "$SECRET_FILE"
|
||||||
|
echo "free:$FREE_PASS" | chpasswd
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.free-password-migration = {
|
||||||
|
description = "Generate and set 'free' password for migrated machines";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "display-manager.service" ];
|
||||||
|
after = [ "systemd-user-sessions.service" "free-password-setup.service" ];
|
||||||
|
requires = [ "free-password-setup.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
path = [ pkgs.shadow pkgs.coreutils ];
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PENDING_FILE="/var/lib/secrets/free-password-migration-pending"
|
||||||
|
SECRET_FILE="/var/lib/secrets/free-password"
|
||||||
|
NEWPASS_FILE="/var/lib/secrets/free-password-migration-newpass"
|
||||||
|
|
||||||
|
[ -f "$PENDING_FILE" ] || exit 0
|
||||||
|
|
||||||
|
mkdir -p /var/lib/secrets
|
||||||
|
|
||||||
|
WORDS="apple barn brook cabin cedar cloud coral crane delta eagle ember \
|
||||||
|
fern field flame flora flint frost grove haven hedge holly heron \
|
||||||
|
jade juniper kelp larch lemon lilac linden loch lotus maple marsh \
|
||||||
|
meadow mist mossy mount oak ocean olive petal pine pixel plum pond \
|
||||||
|
prism quartz raven ridge river robin rocky rose rowan sage sand \
|
||||||
|
sierra silver slate snow solar spark spruce stone storm summit \
|
||||||
|
swift thorn tide timber torch trout vale vault vine walnut wave \
|
||||||
|
willow wren amber aspen birch blaze bloom bluff coast copper crest \
|
||||||
|
dune elder fjord forge glade glen glow gulf"
|
||||||
|
WORD_ARRAY=($WORDS)
|
||||||
|
COUNT=''${#WORD_ARRAY[@]}
|
||||||
|
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
DIGIT=$((RANDOM % 10))
|
||||||
|
FREE_PASS="$W1-$W2-$W3-$DIGIT"
|
||||||
|
|
||||||
|
printf '%s\n' "$FREE_PASS" > "$SECRET_FILE"
|
||||||
|
chmod 600 "$SECRET_FILE"
|
||||||
|
|
||||||
|
printf '%s\n' "$FREE_PASS" > "$NEWPASS_FILE"
|
||||||
|
chmod 600 "$NEWPASS_FILE"
|
||||||
|
rm -f "$PENDING_FILE"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ EOF
|
|||||||
keyFile = livekitKeyFile;
|
keyFile = livekitKeyFile;
|
||||||
settings = {
|
settings = {
|
||||||
rtc.use_external_ip = true;
|
rtc.use_external_ip = true;
|
||||||
rtc.udp_port = "7882-7894";
|
rtc.udp_port = 7882;
|
||||||
room.auto_create = false;
|
room.auto_create = false;
|
||||||
turn = {
|
turn = {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
@@ -141,10 +141,9 @@ EOF
|
|||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
||||||
networking.firewall.allowedUDPPorts = [ 3478 ];
|
networking.firewall.allowedUDPPorts = [ 3478 7882 ];
|
||||||
networking.firewall.allowedUDPPortRanges = [
|
networking.firewall.allowedUDPPortRanges = [
|
||||||
{ from = 7882; to = 7894; }
|
{ from = 30000; to = 40000; }
|
||||||
{ from = 30000; to = 40000;}
|
|
||||||
];
|
];
|
||||||
networking.firewall.allowedTCPPortRanges = [
|
networking.firewall.allowedTCPPortRanges = [
|
||||||
{ from = 30000; to = 40000; }
|
{ from = 30000; to = 40000; }
|
||||||
|
|||||||
+2
-1
@@ -14,6 +14,8 @@
|
|||||||
./core/sovran-hub.nix
|
./core/sovran-hub.nix
|
||||||
./core/legacy-cleanup.nix
|
./core/legacy-cleanup.nix
|
||||||
./core/remote-deploy.nix
|
./core/remote-deploy.nix
|
||||||
|
./core/no-sleep.nix
|
||||||
|
./core/cpu-performance.nix
|
||||||
|
|
||||||
# ── Always on (no flag) ───────────────────────────────────
|
# ── Always on (no flag) ───────────────────────────────────
|
||||||
./php.nix
|
./php.nix
|
||||||
@@ -29,7 +31,6 @@
|
|||||||
|
|
||||||
# ── Features (default OFF — enable in custom.nix) ─────────
|
# ── Features (default OFF — enable in custom.nix) ─────────
|
||||||
./haven.nix
|
./haven.nix
|
||||||
./bip110.nix
|
|
||||||
./element-calling.nix
|
./element-calling.nix
|
||||||
./mempool.nix
|
./mempool.nix
|
||||||
./bitcoin-core.nix
|
./bitcoin-core.nix
|
||||||
|
|||||||
+67
-4
@@ -53,7 +53,7 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
|||||||
# ── Fully automated Nextcloud setup ───────────────────────
|
# ── Fully automated Nextcloud setup ───────────────────────
|
||||||
systemd.services.nextcloud-init = {
|
systemd.services.nextcloud-init = {
|
||||||
description = "Download, extract, and fully configure Nextcloud";
|
description = "Download, extract, and fully configure Nextcloud";
|
||||||
after = [ "network-online.target" "postgresql.service" "phpfpm-mypool.service" "nextcloud-db-init.service" ];
|
after = [ "network-online.target" "postgresql.service" "phpfpm-nextcloud.service" "nextcloud-db-init.service" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
requires = [ "postgresql.service" "nextcloud-db-init.service" ];
|
requires = [ "postgresql.service" "nextcloud-db-init.service" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
@@ -199,6 +199,54 @@ CREDS
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services.nextcloud-detect-existing = {
|
||||||
|
description = "Detect pre-existing Nextcloud installation and populate hub credentials";
|
||||||
|
after = [ "postgresql.service" ];
|
||||||
|
wants = [ "postgresql.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
unitConfig = {
|
||||||
|
ConditionPathExists = [
|
||||||
|
"/var/lib/www/nextcloud/config/config.php"
|
||||||
|
"!/var/lib/secrets/nextcloud-admin"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
path = with pkgs; [ coreutils gnused ];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CREDS_FILE="/var/lib/secrets/nextcloud-admin"
|
||||||
|
DOMAIN_FILE="/var/lib/domains/nextcloud"
|
||||||
|
DOMAIN="your-domain"
|
||||||
|
|
||||||
|
if [ -f "$DOMAIN_FILE" ]; then
|
||||||
|
FILE_DOMAIN="$(sed -n '1{s/^[[:space:]]*//;s/[[:space:]]*$//;p;}' "$DOMAIN_FILE")"
|
||||||
|
if [ -n "$FILE_DOMAIN" ]; then
|
||||||
|
DOMAIN="$FILE_DOMAIN"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /var/lib/secrets
|
||||||
|
|
||||||
|
cat > "$CREDS_FILE" << CREDS
|
||||||
|
Nextcloud (Pre-existing Installation)
|
||||||
|
═══════════════════════════════════════
|
||||||
|
URL: https://$DOMAIN/
|
||||||
|
Note: This Nextcloud was installed before Sovran_SystemsOS.
|
||||||
|
Use your existing admin credentials to log in.
|
||||||
|
Reset: sudo -u caddy php /var/lib/www/nextcloud/occ user:resetpassword <username>
|
||||||
|
CREDS
|
||||||
|
chmod 600 "$CREDS_FILE"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
services.cron.systemCronJobs = [
|
services.cron.systemCronJobs = [
|
||||||
"*/5 * * * * caddy /run/current-system/sw/bin/php -f /var/lib/www/nextcloud/cron.php"
|
"*/5 * * * * caddy /run/current-system/sw/bin/php -f /var/lib/www/nextcloud/cron.php"
|
||||||
];
|
];
|
||||||
@@ -209,9 +257,24 @@ CREDS
|
|||||||
"d /var/lib/nextcloud 0770 caddy php -"
|
"d /var/lib/nextcloud 0770 caddy php -"
|
||||||
];
|
];
|
||||||
|
|
||||||
services.phpfpm.pools.mypool.phpOptions = lib.mkAfter ''
|
services.phpfpm.pools.nextcloud = {
|
||||||
output_buffering = 0
|
user = "caddy";
|
||||||
'';
|
group = "php";
|
||||||
|
phpPackage = config.sovran_systemsOS.phpPackage;
|
||||||
|
phpOptions = lib.mkAfter ''
|
||||||
|
output_buffering = 0
|
||||||
|
'';
|
||||||
|
settings = {
|
||||||
|
"pm" = "dynamic";
|
||||||
|
"pm.max_children" = 75;
|
||||||
|
"pm.start_servers" = 10;
|
||||||
|
"pm.min_spare_servers" = 5;
|
||||||
|
"pm.max_spare_servers" = 20;
|
||||||
|
"pm.max_requests" = 500;
|
||||||
|
"clear_env" = "no";
|
||||||
|
"listen" = "/run/phpfpm/nextcloud.sock";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [ unzip ];
|
environment.systemPackages = with pkgs; [ unzip ];
|
||||||
|
|
||||||
|
|||||||
+21
-30
@@ -28,39 +28,30 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
||||||
{
|
{
|
||||||
users.users = {
|
options.sovran_systemsOS.phpPackage = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
php = {
|
default = custom-php;
|
||||||
isSystemUser = true;
|
description = "Shared PHP package with all extensions for Sovran_SystemsOS services";
|
||||||
createHome = false;
|
|
||||||
uid = 7777;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
users.users.php.group = "php";
|
config = {
|
||||||
|
users.users = {
|
||||||
users.groups.php = {};
|
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
php = {
|
||||||
|
isSystemUser = true;
|
||||||
custom-php
|
createHome = false;
|
||||||
];
|
uid = 7777;
|
||||||
|
|
||||||
services.phpfpm.pools = {
|
|
||||||
mypool = {
|
|
||||||
user = "caddy";
|
|
||||||
group = "php";
|
|
||||||
phpPackage = custom-php;
|
|
||||||
settings = {
|
|
||||||
"pm" = "dynamic";
|
|
||||||
"pm.max_children" = 75;
|
|
||||||
"pm.start_servers" = 10;
|
|
||||||
"pm.min_spare_servers" = 5;
|
|
||||||
"pm.max_spare_servers" = 20;
|
|
||||||
"pm.max_requests" = 500;
|
|
||||||
"clear_env" = "no";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
users.users.php.group = "php";
|
||||||
|
|
||||||
|
users.groups.php = {};
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
|
||||||
|
custom-php
|
||||||
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ EOF
|
|||||||
"198.18.0.0/15" "198.51.100.0/24" "2001:db8::/32" "203.0.113.0/24"
|
"198.18.0.0/15" "198.51.100.0/24" "2001:db8::/32" "203.0.113.0/24"
|
||||||
"224.0.0.0/4" "::1/128" "fc00::/7" "fe80::/10" "fec0::/10" "ff00::/8"
|
"224.0.0.0/4" "::1/128" "fc00::/7" "fe80::/10" "fec0::/10" "ff00::/8"
|
||||||
];
|
];
|
||||||
url_preview_ip_ranger_whitelist = [ "127.0.0.1" ];
|
|
||||||
presence.enabled = true;
|
presence.enabled = true;
|
||||||
enable_registration = false;
|
enable_registration = false;
|
||||||
listeners = [
|
listeners = [
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
|
|
||||||
cat > "$CONFIG_FILE" << 'EOF'
|
cat > "$CONFIG_FILE" << 'EOF'
|
||||||
{
|
{
|
||||||
|
"mode": "ONLINE",
|
||||||
"serverType": "ELECTRUM_SERVER",
|
"serverType": "ELECTRUM_SERVER",
|
||||||
"electrumServer": "tcp://127.0.0.1:50001",
|
"electrumServer": "tcp://127.0.0.1:50001",
|
||||||
"useProxy": false
|
"useProxy": false
|
||||||
@@ -42,44 +43,6 @@ EOF
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# ── Bisq 1 Auto-Connect ─────────────────────────────────────
|
|
||||||
systemd.services.bisq-autoconnect = {
|
|
||||||
description = "Auto-configure Bisq to use local Bitcoin node";
|
|
||||||
after = [ "bitcoind.service" ];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
};
|
|
||||||
path = [ pkgs.coreutils pkgs.iproute2 ];
|
|
||||||
script = ''
|
|
||||||
BISQ_CONF="/home/free/.local/share/Bisq/bisq.properties"
|
|
||||||
|
|
||||||
if [ -f "$BISQ_CONF" ]; then
|
|
||||||
echo "Bisq config already exists, skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Wait for bitcoind RPC to be ready (up to 30 attempts)
|
|
||||||
ATTEMPTS=0
|
|
||||||
until ss -ltn 2>/dev/null | grep -q ':8333' || [ "$ATTEMPTS" -ge 30 ]; do
|
|
||||||
ATTEMPTS=$((ATTEMPTS + 1))
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
mkdir -p /home/free/.local/share/Bisq
|
|
||||||
|
|
||||||
cat > "$BISQ_CONF" << 'EOF'
|
|
||||||
btcNodes=127.0.0.1:8333
|
|
||||||
useTorForBtc=true
|
|
||||||
useCustomBtcNodes=true
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chown -R free:users /home/free/.local/share/Bisq
|
|
||||||
echo "Bisq auto-configured to use local Bitcoin node"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# ── Zeus Connect (lndconnect URL for mobile wallet) ──────────
|
# ── Zeus Connect (lndconnect URL for mobile wallet) ──────────
|
||||||
systemd.services.zeus-connect-setup = {
|
systemd.services.zeus-connect-setup = {
|
||||||
description = "Save Zeus lndconnect URL";
|
description = "Save Zeus lndconnect URL";
|
||||||
|
|||||||
+70
-6
@@ -46,7 +46,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
|||||||
# ── Fully automated WordPress setup ───────────────────────
|
# ── Fully automated WordPress setup ───────────────────────
|
||||||
systemd.services.wordpress-init = {
|
systemd.services.wordpress-init = {
|
||||||
description = "Download, extract, and fully configure WordPress";
|
description = "Download, extract, and fully configure WordPress";
|
||||||
after = [ "network-online.target" "mysql.service" "phpfpm-mypool.service" "wordpress-db-init.service" ];
|
after = [ "network-online.target" "mysql.service" "phpfpm-wordpress.service" "wordpress-db-init.service" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
requires = [ "mysql.service" "wordpress-db-init.service" ];
|
requires = [ "mysql.service" "wordpress-db-init.service" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
@@ -94,10 +94,10 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
|||||||
echo "Download complete."
|
echo "Download complete."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R caddy:root "$INSTALL_DIR"
|
chown -R caddy:php "$INSTALL_DIR"
|
||||||
find "$INSTALL_DIR" -type d -exec chmod 755 {} \;
|
find "$INSTALL_DIR" -type d -exec chmod 750 {} \;
|
||||||
find "$INSTALL_DIR" -type f -exec chmod 644 {} \;
|
find "$INSTALL_DIR" -type f -exec chmod 640 {} \;
|
||||||
chmod -R 775 "$INSTALL_DIR/wp-content"
|
chmod -R 770 "$INSTALL_DIR/wp-content"
|
||||||
|
|
||||||
echo "Generating wp-config.php..."
|
echo "Generating wp-config.php..."
|
||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
@@ -162,9 +162,73 @@ CREDS
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services.wordpress-detect-existing = {
|
||||||
|
description = "Detect pre-existing WordPress installation and populate hub credentials";
|
||||||
|
after = [ "mysql.service" ];
|
||||||
|
wants = [ "mysql.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
unitConfig = {
|
||||||
|
ConditionPathExists = [
|
||||||
|
"/var/lib/www/wordpress/wp-config.php"
|
||||||
|
"!/var/lib/secrets/wordpress-admin"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
path = with pkgs; [ coreutils gnused ];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CREDS_FILE="/var/lib/secrets/wordpress-admin"
|
||||||
|
DOMAIN_FILE="/var/lib/domains/wordpress"
|
||||||
|
DOMAIN="your-domain"
|
||||||
|
|
||||||
|
if [ -f "$DOMAIN_FILE" ]; then
|
||||||
|
FILE_DOMAIN="$(sed -n '1{s/^[[:space:]]*//;s/[[:space:]]*$//;p;}' "$DOMAIN_FILE")"
|
||||||
|
if [ -n "$FILE_DOMAIN" ]; then
|
||||||
|
DOMAIN="$FILE_DOMAIN"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /var/lib/secrets
|
||||||
|
|
||||||
|
cat > "$CREDS_FILE" << CREDS
|
||||||
|
WordPress (Pre-existing Installation)
|
||||||
|
═══════════════════════════════════════
|
||||||
|
URL: https://$DOMAIN/wp-admin/
|
||||||
|
Note: This WordPress was installed before Sovran_SystemsOS.
|
||||||
|
Use your existing admin credentials to log in.
|
||||||
|
Reset: wp user update <username> --user_pass=<new-password>
|
||||||
|
CREDS
|
||||||
|
chmod 600 "$CREDS_FILE"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.phpfpm.pools.wordpress = {
|
||||||
|
user = "caddy";
|
||||||
|
group = "php";
|
||||||
|
phpPackage = config.sovran_systemsOS.phpPackage;
|
||||||
|
settings = {
|
||||||
|
"pm" = "dynamic";
|
||||||
|
"pm.max_children" = 75;
|
||||||
|
"pm.start_servers" = 10;
|
||||||
|
"pm.min_spare_servers" = 5;
|
||||||
|
"pm.max_spare_servers" = 20;
|
||||||
|
"pm.max_requests" = 500;
|
||||||
|
"clear_env" = "no";
|
||||||
|
"listen" = "/run/phpfpm/wordpress.sock";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d /var/lib/www 0755 caddy root -"
|
"d /var/lib/www 0755 caddy root -"
|
||||||
"d /var/lib/www/wordpress 0755 caddy root -"
|
"d /var/lib/www/wordpress 0750 caddy php -"
|
||||||
];
|
];
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [ wp-cli unzip ];
|
environment.systemPackages = with pkgs; [ wp-cli unzip ];
|
||||||
|
|||||||
Reference in New Issue
Block a user