Merge pull request #76 from naturallaw777/copilot/add-sshd-feature-module

[WIP] Add SSH feature module with default off setting
This commit is contained in:
Sovran_Systems
2026-04-05 10:11:11 -05:00
committed by GitHub
7 changed files with 151 additions and 21 deletions

View File

@@ -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")

View File

@@ -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);
}

View File

@@ -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"; };

View File

@@ -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:

View File

@@ -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) ──────────────────

View File

@@ -32,5 +32,6 @@
./mempool.nix
./bitcoin-core.nix
./rdp.nix
./sshd.nix
];
}

23
modules/sshd.nix Normal file
View 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" ];
};
}