From df2768c6fc7caa7482fbc45368d49582e7e8f17a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:09:02 +0000 Subject: [PATCH] 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> --- app/sovran_systemsos_web/server.py | 50 +++++++++++- app/sovran_systemsos_web/static/js/support.js | 80 ++++++++++++++++++- configuration.nix | 16 ---- custom.template.nix | 1 + modules/core/roles.nix | 1 + modules/modules.nix | 1 + modules/sshd.nix | 23 ++++++ 7 files changed, 151 insertions(+), 21 deletions(-) create mode 100644 modules/sshd.nix diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index c04ffac..37244d3 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -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") diff --git a/app/sovran_systemsos_web/static/js/support.js b/app/sovran_systemsos_web/static/js/support.js index 0735b12..9e0dc6c 100644 --- a/app/sovran_systemsos_web/static/js/support.js +++ b/app/sovran_systemsos_web/static/js/support.js @@ -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 = '

Could not check support status.

'; } } +function renderSupportSshdOff() { + stopSupportTimer(); + $supportBody.innerHTML = [ + '
', + '
🛟
', + '

Need help from Sovran Systems?

', + '

To get Tech Support, SSH must be enabled first. SSH is off by default for maximum security — it only needs to be on during a support session.

', + '
', + '
🔐SSH is Off
', + '

SSH (remote login) is disabled by default on your Sovran Pro. Clicking the button below will enable SSH and trigger a system rebuild. Once complete, you can then grant support access.

', + '

When you end the support session, you can disable SSH again from the Feature Manager to return to the default secure state.

', + '
', + '
Steps:
    ', + '
  1. Enable SSH (triggers a system rebuild — takes a few minutes)
  2. ', + '
  3. Grant Sovran Systems temporary support access
  4. ', + '
  5. End the session when done — SSH can be disabled again from the Feature Manager
  6. ', + '
', + '', + '

This will trigger a NixOS rebuild. Your machine will remain operational during the rebuild.

', + '
', + ].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 = [ + '
', + '
⚙️
', + '

Enabling SSH…

', + '

A system rebuild is in progress. This may take a few minutes. The page will update automatically when SSH is ready.

', + '

Rebuilding system…

', + '
', + ].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() { '
🛟
', '

Need help from Sovran Systems?

', '

This will temporarily grant our support team SSH access to your machine so we can help diagnose and fix issues.

', + '
', + '
SSH is Active
', + '

SSH is enabled on your machine. You can now grant Sovran Systems temporary access below.

', + '
', '
', '
Your IP' + escHtml(ip) + '
', '
This IP will be shared with Sovran Systems support
', @@ -40,7 +116,7 @@ function renderSupportInactive() { '
  • All session events are logged for your audit
  • ', '
    ', '', - '

    You can revoke access at any time. Wallet files are protected unless you unlock them.

    ', + '

    You can revoke access at any time. When finished, you can disable SSH from the Feature Manager to return to the default secure state.

    ', '', ].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 = '
    ' + icon + '

    Support Session Ended

    ' + escHtml(msg) + '

    SSH Key Status:' + vlabel + '
    '; + $supportBody.innerHTML = '
    ' + icon + '

    Support Session Ended

    ' + escHtml(msg) + '

    SSH Key Status:' + vlabel + '
    🔐Disable SSH When Done

    SSH is still enabled on your machine. For maximum security, disable it from the Feature Manager when you no longer need remote access.

    '; document.getElementById("btn-support-done").addEventListener("click", closeSupportModal); } diff --git a/configuration.nix b/configuration.nix index 11c9911..bea699b 100644 --- a/configuration.nix +++ b/configuration.nix @@ -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"; }; diff --git a/custom.template.nix b/custom.template.nix index 652dcc5..7df86f2 100644 --- a/custom.template.nix +++ b/custom.template.nix @@ -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) │ # └─────────────────────┴─────���──────────────────────────┘ # # Example — enable element video calling: diff --git a/modules/core/roles.nix b/modules/core/roles.nix index b9fc6c8..2de80d5 100755 --- a/modules/core/roles.nix +++ b/modules/core/roles.nix @@ -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) ────────────────── diff --git a/modules/modules.nix b/modules/modules.nix index 16dc0a9..bf20afc 100755 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -32,5 +32,6 @@ ./mempool.nix ./bitcoin-core.nix ./rdp.nix + ./sshd.nix ]; } \ No newline at end of file diff --git a/modules/sshd.nix b/modules/sshd.nix new file mode 100644 index 0000000..a4665ad --- /dev/null +++ b/modules/sshd.nix @@ -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" ]; + }; + +}