Compare commits
33 Commits
1195456bee
...
staging-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8413093d43 | ||
|
|
1a8a1736bf | ||
|
|
51c7d172b3 | ||
|
|
6999ae5680 | ||
|
|
0c3f74e7de | ||
|
|
d2703ff84b | ||
|
|
1a9e0825fc | ||
|
|
284a861927 | ||
|
|
02b4e6b5b4 | ||
|
|
60084c292e | ||
|
|
fa22a080b9 | ||
|
|
70f0af98f6 | ||
|
|
cd4df316ae | ||
|
|
ff55dce746 | ||
|
|
5a86c03f74 | ||
| 1c2df46ac4 | |||
| 8839620e63 | |||
| c03126e8f8 | |||
|
|
10ef36859d | ||
|
|
4acb75f2bd | ||
|
|
77e2fb2537 | ||
|
|
c7bbb97a68 | ||
|
|
6d1c360c02 | ||
|
|
3b73eb3bd1 | ||
|
|
6ffcc056ad | ||
|
|
742f680d0d | ||
|
|
c872f1c6b0 | ||
|
|
bc5a40f143 | ||
|
|
c2bd3f6273 | ||
|
|
343dee3576 | ||
|
|
ebcafd3c6d | ||
|
|
5231b5ca4b | ||
|
|
cd4a17fe31 |
@@ -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,
|
||||
@@ -2970,9 +2971,13 @@ def _is_free_password_default() -> bool:
|
||||
password changes made via GNOME, passwd, or any method other than the Hub
|
||||
are detected correctly.
|
||||
"""
|
||||
import crypt # available in Python stdlib (deprecated in 3.13 but present on NixOS)
|
||||
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:
|
||||
@@ -2981,9 +2986,34 @@ def _is_free_password_default() -> bool:
|
||||
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:
|
||||
if crypt.crypt(default_pw, current_hash) == current_hash:
|
||||
return True
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
@@ -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 "https://njal.la/update/?h=' + escHtml(d.name) + '.yourdomain.com&k=abc123&auto"" />';
|
||||
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() {
|
||||
|
||||
@@ -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">
|
||||
|
||||
146
iso/installer.py
146
iso/installer.py
@@ -19,7 +19,7 @@ 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";
|
||||
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=staging-dev";
|
||||
};
|
||||
|
||||
outputs = { self, Sovran_Systems, ... }@inputs: {
|
||||
@@ -109,7 +109,7 @@ def symbolic_icon(name):
|
||||
return icon
|
||||
|
||||
|
||||
# ── Application ────────────────────────────────────────────────────────────────
|
||||
# ── Application ──────────────────────────────────────────────────────────
|
||||
|
||||
class InstallerApp(Adw.Application):
|
||||
def __init__(self):
|
||||
@@ -121,7 +121,7 @@ class InstallerApp(Adw.Application):
|
||||
self.win.present()
|
||||
|
||||
|
||||
# ── Main Window ────────────────────────────────────────────────────────────────
|
||||
# ── Main Window ──────────────────────────────────────────────────────────
|
||||
|
||||
class InstallerWindow(Adw.ApplicationWindow):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -168,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()
|
||||
@@ -856,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)
|
||||
@@ -966,125 +966,13 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
if proc.returncode != 0:
|
||||
log(proc.stderr)
|
||||
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
|
||||
run(["sudo", "rm", "-f", "/mnt/etc/nixos/flake.lock"])
|
||||
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)
|
||||
|
||||
GLib.idle_add(self.push_create_password)
|
||||
GLib.idle_add(self.push_complete)
|
||||
|
||||
# ── 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."
|
||||
)
|
||||
status.set_vexpand(True)
|
||||
|
||||
form_group = Adw.PreferencesGroup()
|
||||
form_group.set_margin_start(40)
|
||||
form_group.set_margin_end(40)
|
||||
|
||||
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)
|
||||
@@ -1095,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)
|
||||
|
||||
@@ -1105,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)
|
||||
|
||||
@@ -1161,4 +1049,4 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = InstallerApp()
|
||||
app.run(None)
|
||||
app.run(None)
|
||||
@@ -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"; }
|
||||
];
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 pkgs.python3 ];
|
||||
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
|
||||
@@ -123,15 +249,30 @@ EOF
|
||||
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
|
||||
for DEFAULT_PW in "free" "gosovransystems"; do
|
||||
EXPECTED=$(DEFAULT_PW="$DEFAULT_PW" FREE_HASH="$FREE_HASH" python3 -c \
|
||||
"import crypt, os; print(crypt.crypt(os.environ['DEFAULT_PW'], os.environ['FREE_HASH']))")
|
||||
if [ "$EXPECTED" = "$FREE_HASH" ]; then
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user