44 Commits

Author SHA1 Message Date
Sovran_Systems
8413093d43 Merge pull request #139 from naturallaw777/copilot/fix-flake-lock-issue
installer: pre-resolve flake lock to staging-dev instead of deleting it
2026-04-07 21:49:58 -05:00
copilot-swe-agent[bot]
1a8a1736bf fix: pre-resolve flake lock to staging-dev during installation
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/14550e27-a253-453b-b454-097575e924fa

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 02:48:59 +00:00
copilot-swe-agent[bot]
51c7d172b3 Initial plan 2026-04-08 02:46:49 +00:00
Sovran_Systems
6999ae5680 Merge pull request #138 from naturallaw777/copilot/fix-onboarding-wizard-issues
onboarding: remove scroll boxes, fix footer spacing, add per-field domain saves
2026-04-07 19:55:56 -05:00
copilot-swe-agent[bot]
0c3f74e7de Fix onboarding wizard: remove scroll boxes, fix footer spacing, add per-field save buttons
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0b500e06-d8c5-4745-9768-29523ffc99c6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:55:08 +00:00
copilot-swe-agent[bot]
d2703ff84b Initial plan 2026-04-08 00:51:40 +00:00
Sovran_Systems
1a9e0825fc Merge pull request #137 from naturallaw777/copilot/fix-onboarding-visual-consistency
Fix onboarding wizard: consistent card styling, footer spacing, and password description
2026-04-07 19:43:02 -05:00
copilot-swe-agent[bot]
284a861927 Fix onboarding wizard: consistent card styling, footer spacing, and password description
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ce004fc7-c96f-4765-bc21-87ce579352d0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:39:45 +00:00
Sovran_Systems
02b4e6b5b4 Merge pull request #136 from naturallaw777/copilot/fix-domain-configuration-in-modal
Fix: Replace dead "Feature Manager" sidebar references with inline Configure Domain button
2026-04-07 19:39:43 -05:00
copilot-swe-agent[bot]
60084c292e Initial plan 2026-04-08 00:38:42 +00:00
copilot-swe-agent[bot]
fa22a080b9 fix: replace broken Feature Manager references with Configure Domain button
- server.py: add domain_name to /api/service-detail response
- service-detail.js: replace both Feature Manager references with Configure Domain / Reconfigure Domain buttons with click handlers
- tiles.css: add .svc-detail-domain-btn class for button spacing

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ae38c98e-28bb-4d1e-8dae-78ebde64ad44

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:37:40 +00:00
copilot-swe-agent[bot]
70f0af98f6 Initial plan 2026-04-08 00:33:37 +00:00
Sovran_Systems
cd4df316ae Merge pull request #135 from naturallaw777/copilot/fix-bitcoind-i-o-error
Fix bitcoind/electrs I/O crash when second drive mounts after service start
2026-04-07 19:24:22 -05:00
copilot-swe-agent[bot]
ff55dce746 Add mount dependency for bitcoind and electrs systemd services
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1def4c7b-d90d-4b0c-87a7-87dc729661b1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:23:35 +00:00
copilot-swe-agent[bot]
5a86c03f74 Initial plan 2026-04-08 00:22:26 +00:00
1c2df46ac4 updated installer.py 2026-04-07 17:59:53 -05:00
8839620e63 updated caddy.nix 2026-04-07 17:36:26 -05:00
c03126e8f8 .iso update 2026-04-07 17:02:43 -05:00
Sovran_Systems
10ef36859d Merge pull request #132 from naturallaw777/copilot/fix-ownership-permissions
Replace tmpfiles rules with systemd oneshot service for recursive ownership fix on second drive
2026-04-07 16:41:54 -05:00
Sovran_Systems
4acb75f2bd Merge pull request #133 from naturallaw777/copilot/update-deployed-flake-url
Point installer DEPLOYED_FLAKE at staging-dev branch
2026-04-07 16:41:20 -05:00
copilot-swe-agent[bot]
77e2fb2537 Fix installer DEPLOYED_FLAKE to point to staging-dev branch
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/43e96fac-1140-42e5-9981-00069570967c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 21:40:26 +00:00
copilot-swe-agent[bot]
c7bbb97a68 Initial plan 2026-04-07 21:39:42 +00:00
copilot-swe-agent[bot]
6d1c360c02 Replace tmpfiles rules with systemd oneshot service for recursive chown on second drive
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/96b8f8fe-5a1d-42e5-8b2d-5dd5aee96044

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 21:29:33 +00:00
copilot-swe-agent[bot]
3b73eb3bd1 Initial plan 2026-04-07 21:28:36 +00:00
Sovran_Systems
6ffcc056ad Merge pull request #131 from naturallaw777/copilot/fix-sovran-legacy-security-check
Replace Python `crypt` module with `openssl passwd` (Python 3.13 compatibility)
2026-04-07 16:17:02 -05:00
copilot-swe-agent[bot]
742f680d0d fix: replace Python crypt module with openssl passwd for Python 3.13 compatibility
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9544e3d5-f7f8-4299-9198-3b5f1f835d14

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 21:11:13 +00:00
copilot-swe-agent[bot]
c872f1c6b0 Initial plan 2026-04-07 21:04:58 +00:00
Sovran_Systems
bc5a40f143 Merge pull request #130 from naturallaw777/copilot/add-sovran-auto-seal-service
Add sovran-auto-seal: automatic first-boot seal with live-system safety guards
2026-04-07 15:48:25 -05:00
copilot-swe-agent[bot]
c2bd3f6273 Add sovran-auto-seal systemd service to factory-seal.nix
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/869df8d4-3811-4a1a-b026-e978d3a81589

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:43:15 +00:00
copilot-swe-agent[bot]
343dee3576 Initial plan 2026-04-07 20:40:53 +00:00
Sovran_Systems
ebcafd3c6d Merge pull request #129 from naturallaw777/copilot/add-tmpfiles-rules-for-bitcoin-electrs
[WIP] Add tmpfiles rules for Bitcoin and Electrs data directories
2026-04-07 15:21:26 -05:00
copilot-swe-agent[bot]
5231b5ca4b Add systemd.tmpfiles.rules for Bitcoin/Electrs directory permissions
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ea46340b-7cf5-404b-9cef-b5ed1fcb2ecb

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:21:07 +00:00
Sovran_Systems
1195456bee Merge pull request #128 from naturallaw777/copilot/fix-flake-nix-references
[WIP] Fix flake.nix references after nixos-install cleanup
2026-04-07 15:21:02 -05:00
copilot-swe-agent[bot]
48de6b9821 fix(installer): improve error handling for deployed flake.nix write
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b7dfaecc-2b2e-4f5f-bb9a-f97ced90e76e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:20:36 +00:00
copilot-swe-agent[bot]
cd4a17fe31 Initial plan 2026-04-07 20:20:01 +00:00
copilot-swe-agent[bot]
d3a5b3e6ef fix(installer): write deployed flake.nix and remove flake.lock after install cleanup
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b7dfaecc-2b2e-4f5f-bb9a-f97ced90e76e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:18:36 +00:00
copilot-swe-agent[bot]
3c4c6c7389 Initial plan 2026-04-07 20:16:57 +00:00
Sovran_Systems
876f728aa2 Merge pull request #127 from naturallaw777/copilot/update-api-password-check
Use /etc/shadow as authoritative source for factory default password detection
2026-04-07 13:55:53 -05:00
copilot-swe-agent[bot]
950a6dabd8 Use /etc/shadow as source of truth for factory default password detection
- server.py: add _is_free_password_default() helper that reads /etc/shadow
  and hashes known defaults ("free", "gosovransystems") via crypt module;
  update api_password_is_default to use it instead of reading the secrets file
- factory-seal.nix: replace file-based free-password check with shadow-based
  cryptographic check using python3 + crypt module; add pkgs.python3 to path;
  pass values via env vars to avoid shell expansion of hash $ characters

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/31e6fc93-8b4b-47af-9c47-568da0905301

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 18:50:16 +00:00
copilot-swe-agent[bot]
1d9589a186 Initial plan 2026-04-07 18:46:24 +00:00
Sovran_Systems
b13fa7dc05 Merge pull request #126 from naturallaw777/copilot/fix-security-warning-reappearance
Fix legacy security warning reappearing on every reboot after password change
2026-04-07 13:29:32 -05:00
copilot-swe-agent[bot]
069f6c3ec7 Avoid storing password in variable to prevent process listing exposure
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c18311e4-609d-4edf-a2a1-a018baede373

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 18:27:32 +00:00
copilot-swe-agent[bot]
5a27b79b51 Fix security warning reappearing after every reboot
Add two early-exit checks in sovran-legacy-security-check before the
legacy fallthrough block:
1. Exit if /var/lib/sovran/onboarding-complete exists (Hub onboarding done)
2. Exit if /var/lib/secrets/free-password exists and is not "free" (password changed)

This prevents the boot-time service from overwriting the security-status
file that /api/change-password clears after a successful password change.

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c18311e4-609d-4edf-a2a1-a018baede373

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 18:26:54 +00:00
copilot-swe-agent[bot]
72453c80bf Initial plan 2026-04-07 18:25:47 +00:00
11 changed files with 428 additions and 155 deletions

View File

@@ -2162,6 +2162,7 @@ async def api_service_detail(unit: str, icon: str | None = None):
"credentials": resolved_creds,
"needs_domain": needs_domain,
"domain": domain,
"domain_name": domain_key,
"domain_status": domain_status,
"port_requirements": port_requirements,
"port_statuses": port_statuses,
@@ -2963,15 +2964,71 @@ async def api_security_status():
return {"status": status, "warning": warning}
def _is_free_password_default() -> bool:
"""Check /etc/shadow directly to see if 'free' still has a factory default password.
Hashes each known factory default against the current shadow hash so that
password changes made via GNOME, passwd, or any method other than the Hub
are detected correctly.
"""
import subprocess
import re as _re
FACTORY_DEFAULTS = ["free", "gosovransystems"]
# Map shadow algorithm IDs to openssl passwd flags (SHA-512 and SHA-256 only,
# matching the shell-script counterpart in factory-seal.nix)
ALGO_FLAGS = {"6": "-6", "5": "-5"}
try:
with open("/etc/shadow", "r") as f:
for line in f:
parts = line.strip().split(":")
if parts[0] == "free" and len(parts) > 1:
current_hash = parts[1]
if not current_hash or current_hash in ("!", "*", "!!"):
return True # locked/no password — treat as default
# Parse hash: $id$[rounds=N$]salt$hash
hash_fields = current_hash.split("$")
# hash_fields: ["", id, salt_or_rounds, ...]
if len(hash_fields) < 4:
return True # unrecognized format — assume default for safety
algo_id = hash_fields[1]
salt_field = hash_fields[2]
if algo_id not in ALGO_FLAGS:
return True # unrecognized algorithm — assume default for safety
if salt_field.startswith("rounds="):
return True # can't extract real salt simply — assume default for safety
# Validate salt contains only safe characters (alphanumeric, '.', '/', '-', '_')
# to guard against unexpected shadow file content before passing to subprocess
if not _re.fullmatch(r"[A-Za-z0-9./\-_]+", salt_field):
return True # unexpected salt format — assume default for safety
openssl_flag = ALGO_FLAGS[algo_id]
for default_pw in FACTORY_DEFAULTS:
try:
result = subprocess.run(
["openssl", "passwd", openssl_flag, "-salt", salt_field, default_pw],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0 and result.stdout.strip() == current_hash:
return True
except Exception:
return True # if openssl fails, assume default for safety
return False
except (FileNotFoundError, PermissionError):
pass
return True # if /etc/shadow is unreadable, assume default for safety
@app.get("/api/security/password-is-default")
async def api_password_is_default():
"""Check if the free account password is still the factory default."""
try:
with open("/var/lib/secrets/free-password", "r") as f:
current = f.read().strip()
return {"is_default": current == "free"}
except FileNotFoundError:
return {"is_default": True}
"""Check if the free account password is still the factory default.
Uses /etc/shadow as the authoritative source so that password changes made
via GNOME Settings, the passwd command, or any other method are detected
correctly — not just changes made through the Hub or change-free-password.
"""
return {"is_default": _is_free_password_default()}
# ── System password change ────────────────────────────────────────

View File

@@ -146,17 +146,6 @@
gap: 16px;
}
.onboarding-card--scroll {
max-height: 360px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
}
.onboarding-card--ports {
overflow: visible;
}
/* Body text */
.onboarding-body-text {
@@ -228,7 +217,9 @@
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 4px;
padding-top: 24px;
padding-bottom: 24px;
margin-top: auto;
}
.onboarding-btn-next {

View File

@@ -310,6 +310,12 @@
text-decoration: underline;
}
/* ── Service detail: Domain configure button ─────────────────────── */
.svc-detail-domain-btn {
margin-top: 12px;
}
/* ── Service detail: Addon feature toggle ────────────────────────── */
.svc-detail-addon-row {

View File

@@ -244,8 +244,8 @@ async function openServiceDetailModal(unit, name, icon) {
'<li>Find the domain you purchased for this service</li>' +
'<li>Create a Dynamic DNS record pointing to your external IP: <code>' + escHtml(ds.expected_ip || "—") + '</code></li>' +
'<li>Copy the DDNS curl command from Njal.la\'s dashboard</li>' +
'<li>You can re-enter it in the Feature Manager to update your configuration</li>' +
'</ol>' +
'<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-reconfig-domain-btn">🔄 Reconfigure Domain</button>' +
'</div>';
} else {
domainBadge = '<span class="svc-detail-domain-value">' + escHtml(data.domain) + '</span>';
@@ -257,9 +257,9 @@ async function openServiceDetailModal(unit, name, icon) {
'<p style="margin-top:8px">To get this service working:</p>' +
'<ol>' +
'<li>Purchase a subdomain at <a href="https://njal.la" target="_blank">njal.la</a> (if you haven\'t already)</li>' +
'<li>Go to the <strong>Feature Manager</strong> in the sidebar</li>' +
'<li>Find this service and configure your domain through the setup wizard</li>' +
'<li>Use the button below to configure your domain through the setup wizard</li>' +
'</ol>' +
'<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-config-domain-btn">🌐 Configure Domain</button>' +
'</div>';
}
@@ -385,6 +385,26 @@ async function openServiceDetailModal(unit, name, icon) {
});
}
}
// Configure Domain button (for non-feature services that need a domain)
var configDomainBtn = document.getElementById("svc-detail-config-domain-btn");
var reconfigDomainBtn = document.getElementById("svc-detail-reconfig-domain-btn");
var domainBtn = configDomainBtn || reconfigDomainBtn;
if (domainBtn && data.needs_domain && data.domain_name) {
var pseudoFeat = {
id: data.domain_name,
name: name,
domain_name: data.domain_name,
needs_ddns: true,
extra_fields: []
};
domainBtn.addEventListener("click", function() {
closeCredsModal();
openDomainSetupModal(pseudoFeat, function() {
openServiceDetailModal(unit, name, icon);
});
});
}
} catch (err) {
if ($credsBody) $credsBody.innerHTML = '<p class="creds-empty">Could not load service details.</p>';
}

View File

@@ -308,6 +308,8 @@ async function loadStep3() {
html += '<label class="onboarding-domain-label onboarding-domain-label--sub">Njal.la DDNS Curl Command</label>';
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl &quot;https://njal.la/update/?h=' + escHtml(d.name) + '.yourdomain.com&amp;k=abc123&amp;auto&quot;" />';
html += '<p class="onboarding-hint" style="margin-top:4px;"> Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p>';
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-domain="' + escHtml(d.name) + '" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
html += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
html += '</div>';
});
}
@@ -318,9 +320,78 @@ async function loadStep3() {
html += '<label class="onboarding-domain-label">📧 SSL Certificate Email</label>';
html += '<p class="onboarding-hint onboarding-hint--inline">Let\'s Encrypt uses this for certificate expiry notifications.</p>';
html += '<input class="onboarding-domain-input domain-field-input" type="email" id="ssl-email-input" placeholder="you@example.com" value="' + escHtml(emailVal) + '" />';
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-email="true" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
html += '<span class="onboarding-domain-save-status" id="domain-save-status-email" style="font-size:0.82rem;min-height:1.2em;"></span>';
html += '</div>';
body.innerHTML = html;
// Wire per-field save buttons for domains
body.querySelectorAll('[data-save-domain]').forEach(function(btn) {
btn.addEventListener('click', async function() {
var domainName = btn.dataset.saveDomain;
var domainInput = document.getElementById('domain-input-' + domainName);
var ddnsInput = document.getElementById('ddns-input-' + domainName);
var statusEl = document.getElementById('domain-save-status-' + domainName);
var domainVal = domainInput ? domainInput.value.trim() : '';
var ddnsVal = ddnsInput ? ddnsInput.value.trim() : '';
if (!domainVal) {
if (statusEl) { statusEl.textContent = '⚠ Enter a domain first'; statusEl.style.color = 'var(--red)'; }
return;
}
btn.disabled = true;
btn.textContent = 'Saving…';
if (statusEl) { statusEl.textContent = ''; }
try {
await apiFetch('/api/domains/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain_name: domainName, domain: domainVal, ddns_url: ddnsVal }),
});
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
} catch (err) {
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
}
btn.disabled = false;
btn.textContent = 'Save';
});
});
// Wire save button for SSL email
body.querySelectorAll('[data-save-email]').forEach(function(btn) {
btn.addEventListener('click', async function() {
var emailInput = document.getElementById('ssl-email-input');
var statusEl = document.getElementById('domain-save-status-email');
var emailVal = emailInput ? emailInput.value.trim() : '';
if (!emailVal) {
if (statusEl) { statusEl.textContent = '⚠ Enter an email first'; statusEl.style.color = 'var(--red)'; }
return;
}
btn.disabled = true;
btn.textContent = 'Saving…';
if (statusEl) { statusEl.textContent = ''; }
try {
await apiFetch('/api/domains/set-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: emailVal }),
});
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
} catch (err) {
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
}
btn.disabled = false;
btn.textContent = 'Save';
});
});
}
async function saveStep3() {

View File

@@ -78,7 +78,7 @@
<span class="onboarding-step-icon">🔒</span>
<h2 class="onboarding-step-title">Create Your Password</h2>
<p class="onboarding-step-desc">
Choose a strong password for your <strong>'free'</strong> user account. This will be your login password for the desktop, SSH, and the Hub.
Choose a strong password for your <strong>'free'</strong> user account.
</p>
</div>
<div class="onboarding-card" id="step-2-body">
@@ -105,7 +105,7 @@
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
</p>
</div>
<div class="onboarding-card onboarding-card--scroll" id="step-3-body">
<div class="onboarding-card" id="step-3-body">
<p class="onboarding-loading">Loading service information…</p>
</div>
<div id="step-3-status" class="onboarding-save-status"></div>
@@ -127,7 +127,7 @@
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
</p>
</div>
<div class="onboarding-card onboarding-card--ports" id="step-4-body">
<div class="onboarding-card" id="step-4-body">
<p class="onboarding-loading">Checking ports…</p>
</div>
<div class="onboarding-footer">

View File

@@ -14,6 +14,28 @@ LOGO = "/etc/sovran/logo.png"
LOG = "/tmp/sovran-install.log"
FLAKE = "/etc/sovran/flake"
DEPLOYED_FLAKE = """\
{
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
inputs = {
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=staging-dev";
};
outputs = { self, Sovran_Systems, ... }@inputs: {
nixosConfigurations."nixos" = Sovran_Systems.inputs.nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
./hardware-configuration.nix
./role-state.nix
./custom.nix
Sovran_Systems.nixosModules.Sovran_SystemsOS
];
};
};
}
"""
try:
logfile = open(LOG, "a")
atexit.register(logfile.close)
@@ -87,7 +109,7 @@ def symbolic_icon(name):
return icon
# ── Application ────────────────────────────────────────────────────────────────
# ── Application ──────────────────────────────────────────────────────────
class InstallerApp(Adw.Application):
def __init__(self):
@@ -99,7 +121,7 @@ class InstallerApp(Adw.Application):
self.win.present()
# ── Main Window ────────────────────────────────────────────────────────────────
# ── Main Window ──────────────────────────────────────────────────────────
class InstallerWindow(Adw.ApplicationWindow):
def __init__(self, **kwargs):
@@ -146,7 +168,7 @@ class InstallerWindow(Adw.ApplicationWindow):
break
self.push_page(title, child)
# ── Shared widgets ───────────────<EFBFBD><EFBFBD>─────────────────────────────────────
# ── Shared widgets ────────────────────────────────────────────────────
def make_scrolled_log(self):
sw = Gtk.ScrolledWindow()
@@ -834,7 +856,7 @@ class InstallerWindow(Adw.ApplicationWindow):
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"])
# ── Step 4: Ready to install ────────<EFBFBD><EFBFBD><EFBFBD>──────────────────────────────────
# ── Step 4: Ready to install ──────────────────────────────────────────
def push_ready(self):
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
@@ -933,123 +955,24 @@ class InstallerWindow(Adw.ApplicationWindow):
path = os.path.join(nixos_dir, entry)
run(["sudo", "rm", "-rf", path])
GLib.idle_add(self.push_create_password)
# ── Step 5b: Create Password ──────────────────────────────────────────
def push_create_password(self):
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
status = Adw.StatusPage()
status.set_title("Create Your Password")
status.set_description(
"Choose a password for your 'free' user account. "
"This will be your login password."
GLib.idle_add(append_text, buf, "Writing deployed flake.nix...\n")
proc = subprocess.run(
["sudo", "tee", "/mnt/etc/nixos/flake.nix"],
input=DEPLOYED_FLAKE,
capture_output=True,
text=True,
)
status.set_vexpand(True)
log(proc.stdout)
if proc.returncode != 0:
log(proc.stderr)
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
GLib.idle_add(append_text, buf, "Locking flake to staging-dev...\n")
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
"flake", "lock", "/mnt/etc/nixos"], buf)
form_group = Adw.PreferencesGroup()
form_group.set_margin_start(40)
form_group.set_margin_end(40)
GLib.idle_add(self.push_complete)
pw_row = Adw.PasswordEntryRow()
pw_row.set_title("Password")
form_group.add(pw_row)
confirm_row = Adw.PasswordEntryRow()
confirm_row.set_title("Confirm Password")
form_group.add(confirm_row)
error_lbl = Gtk.Label()
error_lbl.set_margin_start(40)
error_lbl.set_margin_end(40)
error_lbl.set_margin_top(8)
error_lbl.set_visible(False)
error_lbl.add_css_class("error")
content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
content_box.append(status)
content_box.append(form_group)
content_box.append(error_lbl)
outer.append(content_box)
def on_submit(btn):
password = pw_row.get_text()
confirm = confirm_row.get_text()
if not password:
error_lbl.set_text("Password cannot be empty.")
error_lbl.set_visible(True)
return
if len(password) < 8:
error_lbl.set_text("Password must be at least 8 characters.")
error_lbl.set_visible(True)
return
if password != confirm:
error_lbl.set_text("Passwords do not match.")
error_lbl.set_visible(True)
return
btn.set_sensitive(False)
error_lbl.set_visible(False)
try:
run(["sudo", "mkdir", "-p", "/mnt/var/lib/secrets"])
proc = subprocess.run(
["sudo", "tee", "/mnt/var/lib/secrets/free-password"],
input=password, text=True, capture_output=True
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "Failed to write password file")
run(["sudo", "chmod", "600", "/mnt/var/lib/secrets/free-password"])
# Find chpasswd in the installed system's Nix store
# We run it directly from the host with --root /mnt so it
# modifies /mnt/etc/shadow — no chroot needed.
chpasswd_find = subprocess.run(
["sudo", "find", "/mnt/nix/store", "-maxdepth", "3",
"-name", "chpasswd", "-type", "f", "-path", "*/bin/chpasswd"],
capture_output=True, text=True
)
chpasswd_paths = chpasswd_find.stdout.strip().splitlines()
if not chpasswd_paths:
raise RuntimeError("chpasswd binary not found in /mnt/nix/store")
# Use the full host path (e.g. /mnt/nix/store/...-shadow-xxx/bin/chpasswd)
chpasswd_bin = chpasswd_paths[0]
proc = subprocess.run(
["sudo", chpasswd_bin, "--root", "/mnt"],
input=f"free:{password}",
capture_output=True, text=True
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "Failed to set password")
run(["sudo", "touch", "/mnt/var/lib/sovran-customer-onboarded"])
except Exception as e:
error_lbl.set_text(str(e))
error_lbl.set_visible(True)
btn.set_sensitive(True)
return
GLib.idle_add(self.push_complete)
submit_btn = Gtk.Button(label="Set Password & Continue")
submit_btn.add_css_class("suggested-action")
submit_btn.add_css_class("pill")
submit_btn.connect("clicked", on_submit)
nav = Gtk.Box()
nav.set_margin_bottom(24)
nav.set_margin_end(40)
nav.set_halign(Gtk.Align.END)
nav.append(submit_btn)
outer.append(nav)
self.push_page("Create Password", outer)
return False
# ── Step 6: Complete ───────────────────────────────────────────────────
# ── Complete ───────────────────────────────────────────────────────────
def push_complete(self):
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
@@ -1060,7 +983,7 @@ class InstallerWindow(Adw.ApplicationWindow):
status.set_vexpand(True)
creds_group = Adw.PreferencesGroup()
creds_group.set_title("Write down your login details before rebooting")
creds_group.set_title("Important — read before rebooting")
creds_group.set_margin_start(40)
creds_group.set_margin_end(40)
@@ -1070,15 +993,15 @@ class InstallerWindow(Adw.ApplicationWindow):
creds_group.add(user_row)
pass_row = Adw.ActionRow()
pass_row.set_title("Password")
pass_row.set_subtitle("The password you just created")
pass_row.set_title("Default Password")
pass_row.set_subtitle("free — you will be prompted to change it on first boot")
creds_group.add(pass_row)
note_row = Adw.ActionRow()
note_row.set_title("App Passwords")
note_row.set_title("First Boot Setup")
note_row.set_subtitle(
"After rebooting, all app passwords (Nextcloud, Bitcoin, Matrix, etc.) "
"will be available in the Sovran Hub on your dashboard."
"After rebooting, the Sovran Hub will guide you through setting "
"your password, domains, and all app credentials."
)
creds_group.add(note_row)
@@ -1126,4 +1049,4 @@ class InstallerWindow(Adw.ApplicationWindow):
if __name__ == "__main__":
app = InstallerApp()
app.run(None)
app.run(None)

View File

@@ -69,7 +69,36 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
};
nix-bitcoin.useVersionLockedPkgs = false;
systemd.services.bitcoind = {
requires = [ "run-media-Second_Drive.mount" ];
after = [ "run-media-Second_Drive.mount" ];
};
systemd.services.electrs = {
requires = [ "run-media-Second_Drive.mount" ];
after = [ "run-media-Second_Drive.mount" ];
};
systemd.services.sovran-btc-permissions = {
description = "Fix Bitcoin/Electrs data directory ownership on second drive";
wantedBy = [ "multi-user.target" ];
after = [ "run-media-Second_Drive.mount" ];
before = [ "bitcoind.service" "electrs.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node ]; then
chown -R bitcoin:bitcoin /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node
fi
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data ]; then
chown -R electrs:electrs /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data
fi
'';
};
sovran_systemsOS.domainRequirements = [
{ name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; }
];

View File

@@ -9,7 +9,18 @@ in
enable = true;
user = "caddy";
group = "root";
configFile = "/run/caddy/Caddyfile";
};
# Override ExecStart + ExecReload to point at the runtime-generated Caddyfile
systemd.services.caddy.serviceConfig = {
ExecStart = lib.mkForce [
""
"${pkgs.caddy}/bin/caddy run --config /run/caddy/Caddyfile --adapter caddyfile"
];
ExecReload = lib.mkForce [
""
"${pkgs.caddy}/bin/caddy reload --config /run/caddy/Caddyfile --adapter caddyfile --force"
];
};
systemd.services.caddy-generate-config = {
@@ -178,4 +189,4 @@ ${extraVhosts}
CUSTOM_VHOSTS_EOF
'';
};
}
}

View File

@@ -89,6 +89,132 @@ in
{
environment.systemPackages = [ sovran-factory-seal ];
# ── Auto-seal on first customer boot ───────────────────────────────
systemd.services.sovran-auto-seal = {
description = "Auto-seal Sovran system on first customer boot";
wantedBy = [ "multi-user.target" ];
before = [ "sovran-hub.service" "sovran-legacy-security-check.service" ];
after = [ "local-fs.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.coreutils pkgs.e2fsprogs pkgs.openssl pkgs.postgresql pkgs.mariadb pkgs.shadow ];
script = ''
# Idempotency check
if [ -f /var/lib/sovran-factory-sealed ]; then
echo "sovran-auto-seal: already sealed, nothing to do."
exit 0
fi
echo "sovran-auto-seal: seal flag missing checking system state..."
# Safety guard 1: customer has already onboarded
if [ -f /var/lib/sovran-customer-onboarded ]; then
echo "sovran-auto-seal: /var/lib/sovran-customer-onboarded exists live system detected. Restoring flag and exiting."
touch /var/lib/sovran-factory-sealed
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
exit 0
fi
# Safety guard 2: onboarding was completed
if [ -f /var/lib/sovran/onboarding-complete ]; then
echo "sovran-auto-seal: /var/lib/sovran/onboarding-complete exists live system detected. Restoring flag and exiting."
touch /var/lib/sovran-factory-sealed
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
exit 0
fi
# Safety guard 3: password has been changed from factory defaults
if [ -f /etc/shadow ]; then
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then
ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2)
SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3)
STILL_DEFAULT=false
# If the salt field starts with "rounds=", we cannot extract the real salt
# with a simple cut treat as still-default for safety
if printf '%s' "$SALT" | grep -q '^rounds='; then
STILL_DEFAULT=true
else
for DEFAULT_PW in "free" "gosovransystems"; do
case "$ALGO_ID" in
6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
*)
# Unknown hash algorithm treat as still-default for safety
STILL_DEFAULT=true
break
;;
esac
if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then
STILL_DEFAULT=true
break
fi
done
fi
if [ "$STILL_DEFAULT" = "false" ]; then
echo "sovran-auto-seal: password has been changed from factory defaults live system detected. Restoring flag and exiting."
touch /var/lib/sovran-factory-sealed
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
exit 0
fi
fi
fi
# All safety guards passed: this is a fresh/unsealed system
echo "sovran-auto-seal: fresh system confirmed performing auto-seal..."
# 1. Wipe generated secrets
echo "sovran-auto-seal: wiping secrets..."
[ -d /var/lib/secrets ] && find /var/lib/secrets -mindepth 1 -delete || true
rm -rf /var/lib/matrix-synapse/registration-secret
rm -rf /var/lib/matrix-synapse/db-password
rm -rf /var/lib/gnome-remote-desktop/rdp-password
rm -rf /var/lib/gnome-remote-desktop/rdp-username
rm -rf /var/lib/gnome-remote-desktop/rdp-credentials
rm -rf /var/lib/livekit/livekit_keyFile
rm -rf /etc/nix-bitcoin-secrets/*
# 2. Wipe LND wallet data
echo "sovran-auto-seal: wiping LND wallet data..."
rm -rf /var/lib/lnd/*
# 3. Remove SSH factory key
echo "sovran-auto-seal: removing SSH factory key..."
rm -f /home/free/.ssh/factory_login /home/free/.ssh/factory_login.pub
if [ -f /root/.ssh/authorized_keys ]; then
sed -i '/factory_login/d' /root/.ssh/authorized_keys
fi
# 4. Drop application databases
echo "sovran-auto-seal: dropping application databases..."
sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"matrix-synapse\";" 2>/dev/null || true
sudo -u postgres psql -c "DROP DATABASE IF EXISTS nextclouddb;" 2>/dev/null || true
mysql -u root -e "DROP DATABASE IF EXISTS wordpressdb;" 2>/dev/null || true
# 5. Remove application config files
echo "sovran-auto-seal: removing application config files..."
rm -rf /var/lib/www/wordpress/wp-config.php
rm -rf /var/lib/www/nextcloud/config/config.php
# 6. Wipe Vaultwarden data
echo "sovran-auto-seal: wiping Vaultwarden data..."
rm -rf /var/lib/bitwarden_rs/*
rm -rf /var/lib/vaultwarden/*
# 7. Set sealed flag and make it immutable
echo "sovran-auto-seal: setting sealed flag..."
touch /var/lib/sovran-factory-sealed
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
# 8. Remove onboarded flag so onboarding runs fresh
rm -f /var/lib/sovran-customer-onboarded
echo "sovran-auto-seal: auto-seal complete. Continuing boot into onboarding."
'';
};
# ── Legacy security check: warn existing (pre-seal) machines ───────
systemd.services.sovran-legacy-security-check = {
description = "Check for legacy (pre-factory-seal) security status";
@@ -98,7 +224,7 @@ in
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.coreutils ];
path = [ pkgs.coreutils pkgs.openssl ];
script = ''
# If sealed AND onboarded fully clean, nothing to do
[ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0
@@ -116,6 +242,45 @@ EOF
exit 0
fi
# If the user completed Hub onboarding, they've addressed security
[ -f /var/lib/sovran/onboarding-complete ] && exit 0
# If the free password has been changed from ALL known factory defaults, no warning needed
if [ -f /etc/shadow ]; then
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then
ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2)
SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3)
STILL_DEFAULT=false
# If the salt field starts with "rounds=", we cannot extract the real salt
# with a simple cut treat as still-default for safety
if printf '%s' "$SALT" | grep -q '^rounds='; then
STILL_DEFAULT=true
else
for DEFAULT_PW in "free" "gosovransystems"; do
case "$ALGO_ID" in
6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
*)
# Unknown hash algorithm treat as still-default for safety
STILL_DEFAULT=true
break
;;
esac
if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then
STILL_DEFAULT=true
break
fi
done
fi
if [ "$STILL_DEFAULT" = "false" ]; then
# Password was changed clear any legacy warning and exit
rm -f /var/lib/sovran/security-status /var/lib/sovran/security-warning
exit 0
fi
fi
fi
# No flags at all + secrets exist = legacy (pre-seal era) machine
if [ -f /var/lib/secrets/root-password ]; then
mkdir -p /var/lib/sovran

2
result
View File

@@ -1 +1 @@
/nix/store/2y2saivdbfj6j3r4rpwlw7k4sys89bp1-Sovran_SystemsOS.iso
/nix/store/5g99gvpfy2ha4lvglbcx017ryqndgnli-Sovran_SystemsOS.iso