feat: move sshd into its own Nix feature module, gate Tech Support behind it
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d45dc36f-0b3b-48bb-950f-700afe45dd06 Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
109c92a33a
commit
df2768c6fc
@@ -192,6 +192,20 @@ FEATURE_REGISTRY = [
|
||||
"conflicts_with": ["bip110"],
|
||||
"port_requirements": [],
|
||||
},
|
||||
{
|
||||
"id": "sshd",
|
||||
"name": "SSH Remote Access",
|
||||
"description": "Enable SSH for remote terminal access. Required for Tech Support. Disabled by default for security — enable only when needed.",
|
||||
"category": "support",
|
||||
"needs_domain": False,
|
||||
"domain_name": None,
|
||||
"needs_ddns": False,
|
||||
"extra_fields": [],
|
||||
"conflicts_with": [],
|
||||
"port_requirements": [
|
||||
{"port": "22", "protocol": "TCP", "description": "SSH"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "btcpay-web",
|
||||
"name": "BTCPay Server Web Access",
|
||||
@@ -218,6 +232,7 @@ FEATURE_SERVICE_MAP = {
|
||||
"bip110": None,
|
||||
"bitcoin-core": None,
|
||||
"btcpay-web": "btcpayserver.service",
|
||||
"sshd": "sshd.service",
|
||||
}
|
||||
|
||||
# Port requirements for service tiles (keyed by unit name or icon)
|
||||
@@ -249,6 +264,8 @@ SERVICE_PORT_REQUIREMENTS: dict[str, list[dict]] = {
|
||||
"phpfpm-nextcloud.service": _PORTS_WEB,
|
||||
"phpfpm-wordpress.service": _PORTS_WEB,
|
||||
"haven-relay.service": _PORTS_WEB,
|
||||
# SSH (only open when feature is enabled)
|
||||
"sshd.service": [{"port": "22", "protocol": "TCP", "description": "SSH"}],
|
||||
}
|
||||
|
||||
# Maps service unit names to their domain file name in DOMAINS_DIR.
|
||||
@@ -285,8 +302,8 @@ ROLE_CATEGORIES: dict[str, set[str] | None] = {
|
||||
# Features shown per role (None = show all)
|
||||
ROLE_FEATURES: dict[str, set[str] | None] = {
|
||||
"server_plus_desktop": None,
|
||||
"desktop": {"rdp"},
|
||||
"node": {"rdp", "bip110", "bitcoin-core", "mempool", "btcpay-web"},
|
||||
"desktop": {"rdp", "sshd"},
|
||||
"node": {"rdp", "bip110", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
|
||||
}
|
||||
|
||||
SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
@@ -370,6 +387,12 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
"Manage your system visually without being physically present. "
|
||||
"Sovran_SystemsOS sets up secure remote access with generated credentials — connect and go."
|
||||
),
|
||||
"sshd.service": (
|
||||
"Secure Shell (SSH) remote access. When enabled, authorized users can connect "
|
||||
"to your machine over the network via encrypted terminal sessions. "
|
||||
"Sovran_SystemsOS keeps SSH disabled by default for maximum security — "
|
||||
"enable it only when you need remote access or Tech Support."
|
||||
),
|
||||
"root-password-setup.service": (
|
||||
"Your system account credentials. These are the keys to your Sovran_SystemsOS machine — "
|
||||
"root access, user accounts, and SSH passphrases. Keep them safe."
|
||||
@@ -1026,6 +1049,15 @@ def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
||||
return None
|
||||
|
||||
|
||||
def _is_sshd_feature_enabled() -> bool:
|
||||
"""Check if the sshd feature is enabled via hub overrides or config."""
|
||||
overrides, _ = _read_hub_overrides()
|
||||
if "sshd" in overrides:
|
||||
return bool(overrides["sshd"])
|
||||
config_state = _is_feature_enabled_in_config("sshd")
|
||||
return bool(config_state) if config_state is not None else False
|
||||
|
||||
|
||||
# ── Tech Support helpers ──────────────────────────────────────────
|
||||
|
||||
def _is_support_active() -> bool:
|
||||
@@ -2091,11 +2123,13 @@ async def api_support_status():
|
||||
"""Check if tech support SSH access is currently enabled."""
|
||||
loop = asyncio.get_event_loop()
|
||||
active = await loop.run_in_executor(None, _is_support_active)
|
||||
sshd_enabled = await loop.run_in_executor(None, _is_sshd_feature_enabled)
|
||||
session = await loop.run_in_executor(None, _get_support_session_info)
|
||||
unlock_info = await loop.run_in_executor(None, _get_wallet_unlock_info)
|
||||
wallet_unlocked = bool(unlock_info)
|
||||
return {
|
||||
"active": active,
|
||||
"sshd_enabled": sshd_enabled,
|
||||
"enabled_at": session.get("enabled_at"),
|
||||
"enabled_at_human": session.get("enabled_at_human"),
|
||||
"wallet_protected": session.get("wallet_protected", False),
|
||||
@@ -2109,8 +2143,18 @@ async def api_support_status():
|
||||
|
||||
@app.post("/api/support/enable")
|
||||
async def api_support_enable():
|
||||
"""Add the Sovran support SSH key to allow remote tech support."""
|
||||
"""Add the Sovran support SSH key to allow remote tech support.
|
||||
Requires the sshd feature to be enabled first."""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Gate: SSH feature must be enabled before support can be activated
|
||||
sshd_on = await loop.run_in_executor(None, _is_sshd_feature_enabled)
|
||||
if not sshd_on:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="SSH must be enabled first. Please enable SSH Remote Access in the Feature Manager, then try again.",
|
||||
)
|
||||
|
||||
ok = await loop.run_in_executor(None, _enable_support)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=500, detail="Failed to enable support access")
|
||||
|
||||
@@ -10,12 +10,84 @@ async function openSupportModal() {
|
||||
var status = await apiFetch("/api/support/status");
|
||||
_supportStatus = status;
|
||||
if (status.active) { _supportEnabledAt = status.enabled_at; renderSupportActive(status); }
|
||||
else if (!status.sshd_enabled) { renderSupportSshdOff(); }
|
||||
else { renderSupportInactive(); }
|
||||
} catch (err) {
|
||||
$supportBody.innerHTML = '<p class="creds-empty">Could not check support status.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSupportSshdOff() {
|
||||
stopSupportTimer();
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">🛟</div>',
|
||||
'<h3 class="support-heading">Need help from Sovran Systems?</h3>',
|
||||
'<p class="support-desc">To get Tech Support, SSH must be enabled first. SSH is <strong>off by default</strong> for maximum security — it only needs to be on during a support session.</p>',
|
||||
'<div class="support-wallet-box support-wallet-protected">',
|
||||
'<div class="support-wallet-header"><span class="support-wallet-icon">🔐</span><span class="support-wallet-title">SSH is Off</span></div>',
|
||||
'<p class="support-wallet-desc">SSH (remote login) is <strong>disabled by default</strong> on your Sovran Pro. Clicking the button below will enable SSH and trigger a system rebuild. Once complete, you can then grant support access.</p>',
|
||||
'<p class="support-wallet-desc">When you end the support session, you can disable SSH again from the Feature Manager to return to the default secure state.</p>',
|
||||
'</div>',
|
||||
'<div class="support-steps"><div class="support-steps-title">Steps:</div><ol>',
|
||||
'<li>Enable SSH (triggers a system rebuild — takes a few minutes)</li>',
|
||||
'<li>Grant Sovran Systems temporary support access</li>',
|
||||
'<li>End the session when done — SSH can be disabled again from the Feature Manager</li>',
|
||||
'</ol></div>',
|
||||
'<button class="btn support-btn-enable" id="btn-sshd-enable">Enable SSH</button>',
|
||||
'<p class="support-fine-print">This will trigger a NixOS rebuild. Your machine will remain operational during the rebuild.</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
document.getElementById("btn-sshd-enable").addEventListener("click", enableSshd);
|
||||
}
|
||||
|
||||
async function enableSshd() {
|
||||
var btn = document.getElementById("btn-sshd-enable");
|
||||
if (btn) { btn.disabled = true; btn.textContent = "Enabling SSH…"; }
|
||||
try {
|
||||
await apiFetch("/api/features/toggle", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ feature: "sshd", enabled: true }),
|
||||
});
|
||||
// Poll until rebuild completes and sshd_enabled is true
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">⚙️</div>',
|
||||
'<h3 class="support-heading">Enabling SSH…</h3>',
|
||||
'<p class="support-desc">A system rebuild is in progress. This may take a few minutes. The page will update automatically when SSH is ready.</p>',
|
||||
'<p class="creds-loading" id="sshd-rebuild-status">Rebuilding system…</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
pollForSshdReady();
|
||||
} catch (err) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = "Enable SSH"; }
|
||||
alert("Failed to enable SSH. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
function pollForSshdReady() {
|
||||
var attempts = 0;
|
||||
var maxAttempts = 60; // 5 minutes (5s interval)
|
||||
var interval = setInterval(async function() {
|
||||
attempts++;
|
||||
try {
|
||||
var status = await apiFetch("/api/support/status");
|
||||
var el = document.getElementById("sshd-rebuild-status");
|
||||
if (status.sshd_enabled) {
|
||||
clearInterval(interval);
|
||||
_supportStatus = status;
|
||||
renderSupportInactive();
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(interval);
|
||||
if (el) el.textContent = "Rebuild is taking longer than expected. Please close this dialog and try again.";
|
||||
} else {
|
||||
if (el) el.textContent = "Rebuilding system… (" + attempts * 5 + "s)";
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function renderSupportInactive() {
|
||||
stopSupportTimer();
|
||||
var ip = _cachedExternalIp || "loading…";
|
||||
@@ -24,6 +96,10 @@ function renderSupportInactive() {
|
||||
'<div class="support-icon-big">🛟</div>',
|
||||
'<h3 class="support-heading">Need help from Sovran Systems?</h3>',
|
||||
'<p class="support-desc">This will temporarily grant our support team SSH access to your machine so we can help diagnose and fix issues.</p>',
|
||||
'<div class="support-wallet-box support-wallet-protected">',
|
||||
'<div class="support-wallet-header"><span class="support-wallet-icon">✅</span><span class="support-wallet-title">SSH is Active</span></div>',
|
||||
'<p class="support-wallet-desc">SSH is enabled on your machine. You can now grant Sovran Systems temporary access below.</p>',
|
||||
'</div>',
|
||||
'<div class="support-info-box">',
|
||||
'<div class="support-info-row"><span class="support-info-label">Your IP</span><span class="support-info-value">' + escHtml(ip) + '</span></div>',
|
||||
'<div class="support-info-hint">This IP will be shared with Sovran Systems support</div>',
|
||||
@@ -40,7 +116,7 @@ function renderSupportInactive() {
|
||||
'<li>All session events are logged for your audit</li>',
|
||||
'</ol></div>',
|
||||
'<button class="btn support-btn-enable" id="btn-support-enable">Enable Support Access</button>',
|
||||
'<p class="support-fine-print">You can revoke access at any time. Wallet files are protected unless you unlock them.</p>',
|
||||
'<p class="support-fine-print">You can revoke access at any time. When finished, you can disable SSH from the Feature Manager to return to the default secure state.</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
document.getElementById("btn-support-enable").addEventListener("click", enableSupport);
|
||||
@@ -131,7 +207,7 @@ function renderSupportRemoved(verified) {
|
||||
var msg = verified ? "The Sovran Systems SSH key has been completely removed from your machine. We no longer have any access." : "The key removal was requested but could not be fully verified. Please reboot to ensure it is gone.";
|
||||
var vclass = verified ? "verified-gone" : "verify-warning";
|
||||
var vlabel = verified ? "✓ Removed — No access" : "⚠ Verify by rebooting";
|
||||
$supportBody.innerHTML = '<div class="support-section"><div class="support-icon-big">' + icon + '</div><h3 class="support-heading">Support Session Ended</h3><p class="support-desc">' + escHtml(msg) + '</p><div class="support-verify-box"><span class="support-verify-label">SSH Key Status:</span><span class="support-verify-value ' + vclass + '">' + vlabel + '</span></div><button class="btn support-btn-done" id="btn-support-done">Done</button></div>';
|
||||
$supportBody.innerHTML = '<div class="support-section"><div class="support-icon-big">' + icon + '</div><h3 class="support-heading">Support Session Ended</h3><p class="support-desc">' + escHtml(msg) + '</p><div class="support-verify-box"><span class="support-verify-label">SSH Key Status:</span><span class="support-verify-value ' + vclass + '">' + vlabel + '</span></div><div class="support-wallet-box support-wallet-protected" style="margin-top:12px;"><div class="support-wallet-header"><span class="support-wallet-icon">🔐</span><span class="support-wallet-title">Disable SSH When Done</span></div><p class="support-wallet-desc">SSH is still enabled on your machine. For maximum security, disable it from the <strong>Feature Manager</strong> when you no longer need remote access.</p></div><button class="btn support-btn-done" id="btn-support-done">Done</button></div>';
|
||||
document.getElementById("btn-support-done").addEventListener("click", closeSupportModal);
|
||||
}
|
||||
|
||||
|
||||
@@ -167,22 +167,6 @@ backup /etc/nix-bitcoin-secrets/ localhost/
|
||||
# ── Tor ────────────────────────────────────────────────────
|
||||
services.tor = { enable = true; client.enable = true; torsocks.enable = true; };
|
||||
|
||||
# ── SSH ────────────────────────────────────────────────────
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
# ── Fail2Ban ───────────────────────────────────────────────
|
||||
services.fail2ban = {
|
||||
enable = true;
|
||||
ignoreIP = [ "127.0.0.0/8" "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" "8.8.8.8" ];
|
||||
};
|
||||
|
||||
# ── Garbage Collection ─────────────────────────────────────
|
||||
nix.gc = { automatic = true; dates = "weekly"; options = "--delete-older-than 7d"; };
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
# │ element-calling │ LiveKit server for Matrix │
|
||||
# │ rdp │ GNOME Remote Desktop (RDP) │
|
||||
# │ bitcoin-core │ Bitcoin Core GUI desktop app │
|
||||
# │ sshd │ SSH remote access (for support) │
|
||||
# └─────────────────────┴─────<E29480><E29480><EFBFBD>──────────────────────────┘
|
||||
#
|
||||
# Example — enable element video calling:
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
||||
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
|
||||
rdp = lib.mkEnableOption "Gnome Remote Desktop";
|
||||
sshd = lib.mkEnableOption "SSH remote access";
|
||||
};
|
||||
|
||||
# ── Web exposure (controls Caddy vhosts) ──────────────────
|
||||
|
||||
@@ -32,5 +32,6 @@
|
||||
./mempool.nix
|
||||
./bitcoin-core.nix
|
||||
./rdp.nix
|
||||
./sshd.nix
|
||||
];
|
||||
}
|
||||
23
modules/sshd.nix
Normal file
23
modules/sshd.nix
Normal file
@@ -0,0 +1,23 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
lib.mkIf config.sovran_systemsOS.features.sshd {
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
# Only open port 22 when SSH is actually enabled
|
||||
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||
|
||||
# Fail2Ban protects SSH when it's active
|
||||
services.fail2ban = {
|
||||
enable = true;
|
||||
ignoreIP = [ "127.0.0.0/8" "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" ];
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user