Files
autarch/web/templates/ssh_manager.html

1046 lines
54 KiB
HTML
Raw Permalink Normal View History

{% extends "base.html" %}
{% block title %}SSH Manager — AUTARCH{% endblock %}
{% block content %}
<div class="page-header">
<h1>SSH / SSHD Manager</h1>
<p class="text-muted" style="font-size:0.85rem;color:var(--text-secondary)">
Configure and harden your SSH server — audit settings, manage keys, and apply security best practices
</p>
</div>
<!-- Tab Bar -->
<div class="tab-bar">
<button class="tab active" data-tab-group="ssh" data-tab="status" onclick="sshTab('status')">Status &amp; Scan</button>
<button class="tab" data-tab-group="ssh" data-tab="config" onclick="sshTab('config')">Config Editor</button>
<button class="tab" data-tab-group="ssh" data-tab="keys" onclick="sshTab('keys')">Keys</button>
<button class="tab" data-tab-group="ssh" data-tab="harden" onclick="sshTab('harden')">Quick Harden</button>
<button class="tab" data-tab-group="ssh" data-tab="fail2ban" onclick="sshTab('fail2ban')">Fail2Ban</button>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 1: STATUS & SCAN -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="tab-content active" data-tab-group="ssh" data-tab="status">
<div class="section">
<h2>Service Status</h2>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px;flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:8px">
<span id="ssh-status-dot" style="width:10px;height:10px;border-radius:50%;background:var(--text-muted);display:inline-block"></span>
<span id="ssh-status-text" style="font-weight:600">Checking...</span>
</div>
<div style="font-size:0.85rem;color:var(--text-secondary)">
Enabled: <span id="ssh-enabled-text" style="font-weight:600"></span>
</div>
<div style="font-size:0.85rem;color:var(--text-secondary)">
Version: <span id="ssh-version-text" style="font-family:monospace"></span>
</div>
</div>
<div class="tool-actions" style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-primary btn-sm" onclick="sshServiceAction('start')">Start</button>
<button class="btn btn-sm" onclick="sshServiceAction('stop')">Stop</button>
<button class="btn btn-sm" onclick="sshServiceAction('restart')">Restart</button>
<button class="btn btn-sm" onclick="sshServiceAction('enable')">Enable</button>
<button class="btn btn-sm" onclick="sshServiceAction('disable')">Disable</button>
</div>
</div>
<div class="section">
<h2>Security Scan</h2>
<button class="btn btn-primary" id="btn-ssh-scan" onclick="sshRunScan()">Run Security Scan</button>
<div id="ssh-scan-results" style="margin-top:16px"></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 2: CONFIG EDITOR -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="tab-content" data-tab-group="ssh" data-tab="config">
<!-- Form Editor -->
<div class="section">
<h2>Form Editor</h2>
<!-- Connection -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Connection</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>Port</label>
<input type="number" id="cfg-Port" class="form-control" value="22" min="1" max="65535">
</div>
<div class="form-group">
<label>AddressFamily</label>
<select id="cfg-AddressFamily" class="form-control">
<option value="any">any</option>
<option value="inet">inet</option>
<option value="inet6">inet6</option>
</select>
</div>
<div class="form-group">
<label>ListenAddress</label>
<input type="text" id="cfg-ListenAddress" class="form-control" placeholder="0.0.0.0">
</div>
</div>
</fieldset>
<!-- Authentication -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Authentication</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>PermitRootLogin</label>
<select id="cfg-PermitRootLogin" class="form-control">
<option value="yes">yes</option>
<option value="no">no</option>
<option value="prohibit-password">prohibit-password</option>
<option value="forced-commands-only">forced-commands-only</option>
</select>
</div>
<div class="form-group">
<label>PasswordAuthentication</label>
<select id="cfg-PasswordAuthentication" class="form-control">
<option value="yes">yes</option>
<option value="no">no</option>
</select>
</div>
<div class="form-group">
<label>PubkeyAuthentication</label>
<select id="cfg-PubkeyAuthentication" class="form-control">
<option value="yes" selected>yes</option>
<option value="no">no</option>
</select>
</div>
<div class="form-group">
<label>PermitEmptyPasswords</label>
<select id="cfg-PermitEmptyPasswords" class="form-control">
<option value="yes">yes</option>
<option value="no" selected>no</option>
</select>
</div>
<div class="form-group">
<label>MaxAuthTries</label>
<input type="number" id="cfg-MaxAuthTries" class="form-control" value="6" min="1" max="100">
</div>
<div class="form-group">
<label>LoginGraceTime</label>
<input type="text" id="cfg-LoginGraceTime" class="form-control" placeholder="120" value="120">
</div>
<div class="form-group">
<label>UsePAM</label>
<select id="cfg-UsePAM" class="form-control">
<option value="yes" selected>yes</option>
<option value="no">no</option>
</select>
</div>
</div>
</fieldset>
<!-- Session -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Session</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>MaxSessions</label>
<input type="number" id="cfg-MaxSessions" class="form-control" value="10" min="1" max="100">
</div>
<div class="form-group">
<label>ClientAliveInterval</label>
<input type="number" id="cfg-ClientAliveInterval" class="form-control" value="0" min="0">
</div>
<div class="form-group">
<label>ClientAliveCountMax</label>
<input type="number" id="cfg-ClientAliveCountMax" class="form-control" value="3" min="0">
</div>
</div>
</fieldset>
<!-- Access Control -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Access Control</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>AllowUsers</label>
<input type="text" id="cfg-AllowUsers" class="form-control" placeholder="user1 user2">
</div>
<div class="form-group">
<label>AllowGroups</label>
<input type="text" id="cfg-AllowGroups" class="form-control" placeholder="sshusers admin">
</div>
<div class="form-group">
<label>DenyUsers</label>
<input type="text" id="cfg-DenyUsers" class="form-control" placeholder="nobody">
</div>
<div class="form-group">
<label>DenyGroups</label>
<input type="text" id="cfg-DenyGroups" class="form-control" placeholder="nogroup">
</div>
</div>
</fieldset>
<!-- Forwarding -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Forwarding</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>AllowTcpForwarding</label>
<select id="cfg-AllowTcpForwarding" class="form-control">
<option value="yes" selected>yes</option>
<option value="no">no</option>
<option value="local">local</option>
<option value="remote">remote</option>
</select>
</div>
<div class="form-group">
<label>X11Forwarding</label>
<select id="cfg-X11Forwarding" class="form-control">
<option value="yes">yes</option>
<option value="no" selected>no</option>
</select>
</div>
<div class="form-group">
<label>GatewayPorts</label>
<select id="cfg-GatewayPorts" class="form-control">
<option value="yes">yes</option>
<option value="no" selected>no</option>
</select>
</div>
</div>
</fieldset>
<!-- Logging -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Logging</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>SyslogFacility</label>
<select id="cfg-SyslogFacility" class="form-control">
<option value="AUTH" selected>AUTH</option>
<option value="AUTHPRIV">AUTHPRIV</option>
<option value="DAEMON">DAEMON</option>
<option value="USER">USER</option>
<option value="LOCAL0">LOCAL0</option>
<option value="LOCAL1">LOCAL1</option>
<option value="LOCAL2">LOCAL2</option>
<option value="LOCAL3">LOCAL3</option>
<option value="LOCAL4">LOCAL4</option>
<option value="LOCAL5">LOCAL5</option>
<option value="LOCAL6">LOCAL6</option>
<option value="LOCAL7">LOCAL7</option>
</select>
</div>
<div class="form-group">
<label>LogLevel</label>
<select id="cfg-LogLevel" class="form-control">
<option value="QUIET">QUIET</option>
<option value="FATAL">FATAL</option>
<option value="ERROR">ERROR</option>
<option value="INFO" selected>INFO</option>
<option value="VERBOSE">VERBOSE</option>
<option value="DEBUG">DEBUG</option>
<option value="DEBUG1">DEBUG1</option>
<option value="DEBUG2">DEBUG2</option>
<option value="DEBUG3">DEBUG3</option>
</select>
</div>
</div>
</fieldset>
<!-- Security -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Security</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>StrictModes</label>
<select id="cfg-StrictModes" class="form-control">
<option value="yes" selected>yes</option>
<option value="no">no</option>
</select>
</div>
<div class="form-group" style="grid-column:1/-1">
<label>Ciphers</label>
<input type="text" id="cfg-Ciphers" class="form-control" placeholder="chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com">
</div>
<div class="form-group" style="grid-column:1/-1">
<label>MACs</label>
<input type="text" id="cfg-MACs" class="form-control" placeholder="hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com">
</div>
<div class="form-group" style="grid-column:1/-1">
<label>KexAlgorithms</label>
<input type="text" id="cfg-KexAlgorithms" class="form-control" placeholder="curve25519-sha256,curve25519-sha256@libssh.org">
</div>
</div>
</fieldset>
<!-- Other -->
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px;background:var(--bg-card)">
<legend style="color:var(--accent);font-weight:600;padding:0 8px;font-size:0.9rem">Other</legend>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div class="form-group">
<label>Banner</label>
<input type="text" id="cfg-Banner" class="form-control" placeholder="/etc/ssh/banner.txt">
</div>
<div class="form-group">
<label>Subsystem</label>
<input type="text" id="cfg-Subsystem" class="form-control" value="sftp /usr/lib/openssh/sftp-server">
</div>
<div class="form-group">
<label>UseDNS</label>
<select id="cfg-UseDNS" class="form-control">
<option value="yes">yes</option>
<option value="no" selected>no</option>
</select>
</div>
<div class="form-group">
<label>Compression</label>
<select id="cfg-Compression" class="form-control">
<option value="yes">yes</option>
<option value="no" selected>no</option>
<option value="delayed">delayed</option>
</select>
</div>
</div>
</fieldset>
<button class="btn btn-primary" onclick="sshGenerateConfig()">Generate Config</button>
</div>
<!-- Raw Editor -->
<div class="section">
<h2>Raw Config Editor</h2>
<textarea id="ssh-config-raw" style="width:100%;min-height:400px;background:var(--bg-primary);color:#c8d3f5;
border:1px solid var(--border);border-radius:var(--radius);padding:14px;
font-family:'Cascadia Code','Fira Code','Consolas',monospace;font-size:0.82rem;
line-height:1.6;resize:vertical" placeholder="# sshd_config will appear here..."></textarea>
<div style="display:flex;gap:8px;margin-top:12px;align-items:center;flex-wrap:wrap">
<button class="btn btn-sm" onclick="sshLoadConfig()">Load Current Config</button>
<button class="btn btn-primary btn-sm" onclick="sshSaveConfig()">Save Config</button>
<span id="ssh-config-status" style="font-size:0.82rem;color:var(--text-secondary)"></span>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 3: KEYS -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="tab-content" data-tab-group="ssh" data-tab="keys">
<!-- Host Keys -->
<div class="section">
<h2>Host Keys</h2>
<button class="btn btn-sm" onclick="sshLoadHostKeys()" style="margin-bottom:12px">Refresh</button>
<div id="ssh-host-keys">
<span class="text-muted">Click Refresh to load host keys.</span>
</div>
</div>
<!-- Generate Key Pair -->
<div class="section">
<h2>Generate Key Pair</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:12px">
<div class="form-group">
<label>Type</label>
<select id="keygen-type" class="form-control" onchange="sshKeygenTypeChanged()">
<option value="ed25519" selected>ed25519</option>
<option value="rsa">rsa</option>
</select>
</div>
<div class="form-group" id="keygen-bits-group" style="display:none">
<label>Bits</label>
<input type="number" id="keygen-bits" class="form-control" value="4096" min="2048" max="16384" step="1024">
</div>
<div class="form-group">
<label>Comment</label>
<input type="text" id="keygen-comment" class="form-control" placeholder="user@hostname">
</div>
<div class="form-group">
<label>Passphrase (optional)</label>
<input type="password" id="keygen-passphrase" class="form-control" placeholder="Leave empty for no passphrase">
</div>
</div>
<button class="btn btn-primary btn-sm" onclick="sshGenerateKey()">Generate</button>
<div id="ssh-keygen-result" style="margin-top:16px"></div>
</div>
<!-- Authorized Keys -->
<div class="section">
<h2>Authorized Keys</h2>
<div id="ssh-authkeys-list" style="margin-bottom:16px">
<span class="text-muted">Loading...</span>
</div>
<div style="display:flex;gap:8px;align-items:end;flex-wrap:wrap">
<div class="form-group" style="flex:1;min-width:300px;margin-bottom:0">
<label>Add Public Key</label>
<input type="text" id="ssh-new-authkey" class="form-control" placeholder="ssh-ed25519 AAAA... user@host" style="font-family:monospace;font-size:0.8rem">
</div>
<button class="btn btn-primary btn-sm" onclick="sshAddAuthKey()">Add Key</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 4: QUICK HARDEN -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="tab-content" data-tab-group="ssh" data-tab="harden">
<div class="section">
<h2>Quick Harden</h2>
<p style="color:var(--text-secondary);font-size:0.85rem;margin-bottom:16px">
Apply a hardened SSH configuration with security best practices in one click.
Review the preview below before applying.
</p>
<button class="btn btn-primary" onclick="sshPreviewHarden()">Preview Hardened Config</button>
</div>
<div class="section" id="ssh-harden-preview-section" style="display:none">
<h2>Hardened Config Preview</h2>
<pre id="ssh-harden-preview" style="background:var(--bg-primary);color:#c8d3f5;
border:1px solid var(--border);border-radius:var(--radius);padding:14px;
font-family:'Cascadia Code','Fira Code','Consolas',monospace;font-size:0.82rem;
line-height:1.6;overflow-x:auto;max-height:500px;overflow-y:auto"></pre>
<div style="margin-top:16px;padding:12px;background:rgba(239,68,68,0.1);border:1px solid var(--danger);border-radius:var(--radius)">
<strong style="color:var(--danger)">Warning:</strong>
<span style="color:var(--text-secondary);font-size:0.85rem">
This will overwrite your current sshd_config. Ensure you have key-based authentication
configured before disabling password login, or you may be locked out.
</span>
</div>
<button class="btn btn-primary" style="margin-top:12px;background:var(--danger);border-color:var(--danger)" onclick="sshApplyHarden()">
Apply Hardened Config
</button>
<span id="ssh-harden-status" style="margin-left:12px;font-size:0.85rem;color:var(--text-secondary)"></span>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- FAIL2BAN TAB -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="tab-content" data-tab-group="ssh" data-tab="fail2ban">
<!-- Status -->
<div class="section">
<h2>Fail2Ban</h2>
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:1rem">
<div id="f2b-status-dot" style="width:10px;height:10px;border-radius:50%;background:var(--text-muted)"></div>
<span id="f2b-status-text" style="font-size:0.85rem">Checking...</span>
<button class="btn btn-sm" onclick="f2bService('start')">Start</button>
<button class="btn btn-sm" onclick="f2bService('stop')">Stop</button>
<button class="btn btn-sm" onclick="f2bService('restart')">Restart</button>
<button class="btn btn-sm" onclick="f2bRefresh()">Refresh</button>
</div>
<!-- Jails -->
<h3>Active Jails</h3>
<div id="f2b-jails" style="margin-bottom:1rem"><p style="color:var(--text-muted)">Click Refresh to load jails.</p></div>
<!-- Banned IPs -->
<h3>Banned IPs</h3>
<div id="f2b-banned" style="margin-bottom:1rem"></div>
<!-- Search -->
<h3>IP Search</h3>
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.75rem">
<input type="text" id="f2b-search-ip" placeholder="Search IP across all jails..." style="flex:1;max-width:300px;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-input);color:inherit;font-size:0.82rem">
<button class="btn btn-sm" onclick="f2bSearch()">Search</button>
</div>
<div id="f2b-search-results"></div>
<!-- Manual Ban -->
<h3>Manual Ban / Unban</h3>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.75rem">
<input type="text" id="f2b-manual-ip" placeholder="IP address" style="width:180px;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-input);color:inherit;font-size:0.82rem">
<input type="text" id="f2b-manual-jail" placeholder="Jail (default: sshd)" value="sshd" style="width:150px;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-input);color:inherit;font-size:0.82rem">
<button class="btn btn-sm" style="border-color:var(--danger);color:var(--danger)" onclick="f2bManualBan()">Ban</button>
<button class="btn btn-sm" onclick="f2bManualUnban()">Unban</button>
</div>
<div id="f2b-manual-result" style="font-size:0.82rem"></div>
</div>
<!-- App Scanner & Auto-Config -->
<div class="section">
<h3>Application Scanner</h3>
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.75rem">Scan for installed services and auto-generate jails.</p>
<div style="display:flex;gap:0.5rem;margin-bottom:0.75rem">
<button class="btn btn-primary btn-sm" onclick="f2bScanApps()">Scan for Applications</button>
<button class="btn btn-sm" style="border-color:var(--accent);color:var(--accent)" onclick="f2bAutoConfig(false)">Preview Auto-Config</button>
<button class="btn btn-sm" style="border-color:var(--danger);color:var(--danger)" onclick="f2bAutoConfig(true)">Apply Auto-Config</button>
</div>
<div id="f2b-apps-result"></div>
</div>
<!-- Create Jail -->
<div class="section">
<h3>Create New Jail</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem;font-size:0.82rem">
<div class="form-group"><label>Jail Name</label><input type="text" id="f2b-jail-name" placeholder="my-custom-jail"></div>
<div class="form-group"><label>Filter</label><input type="text" id="f2b-jail-filter" placeholder="filter name"></div>
<div class="form-group"><label>Log Path</label><input type="text" id="f2b-jail-logpath" placeholder="/var/log/..."></div>
<div class="form-group"><label>Max Retry</label><input type="number" id="f2b-jail-maxretry" value="5"></div>
<div class="form-group"><label>Find Time</label><input type="text" id="f2b-jail-findtime" value="10m"></div>
<div class="form-group"><label>Ban Time</label><input type="text" id="f2b-jail-bantime" value="1h"></div>
<div class="form-group"><label>
<input type="checkbox" id="f2b-jail-enabled" checked style="margin-right:0.3rem">Enabled
</label></div>
</div>
<button class="btn btn-primary btn-sm" onclick="f2bCreateJail()" style="margin-top:0.5rem">Create Jail</button>
<div id="f2b-create-result" style="font-size:0.82rem;margin-top:0.5rem"></div>
</div>
</div>
<style>
.ssh-severity {
display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.72rem;
font-weight:700;text-transform:uppercase;letter-spacing:0.04em;
}
.ssh-severity.critical { background:rgba(239,68,68,0.2);color:#f87171; }
.ssh-severity.warning { background:rgba(251,191,36,0.2);color:#fbbf24; }
.ssh-severity.info { background:rgba(96,165,250,0.2);color:#60a5fa; }
.ssh-severity.pass { background:rgba(74,222,128,0.2);color:#4ade80; }
.ssh-check-card {
padding:12px 16px;background:var(--bg-card);border:1px solid var(--border);
border-radius:var(--radius);margin-bottom:8px;
}
.ssh-check-card .check-header {
display:flex;align-items:center;gap:10px;margin-bottom:4px;
}
.ssh-check-card .check-name { font-weight:600;font-size:0.88rem; }
.ssh-check-card .check-values {
font-size:0.8rem;color:var(--text-secondary);font-family:monospace;margin-bottom:2px;
}
.ssh-check-card .check-desc {
font-size:0.8rem;color:var(--text-muted);
}
.ssh-key-output {
position:relative;
}
.ssh-key-output textarea {
width:100%;min-height:80px;background:var(--bg-primary);color:#c8d3f5;
border:1px solid var(--border);border-radius:var(--radius);padding:10px;
font-family:'Cascadia Code','Fira Code','Consolas',monospace;font-size:0.78rem;
line-height:1.5;resize:vertical;
}
.ssh-key-output .copy-btn {
position:absolute;top:8px;right:8px;font-size:0.7rem;padding:3px 8px;
background:var(--bg-input);border:1px solid var(--border);border-radius:4px;
color:var(--text-secondary);cursor:pointer;
}
.ssh-key-output .copy-btn:hover { color:var(--accent);border-color:var(--accent); }
</style>
<script>
/* ── Tab Switching ── */
function sshTab(name) {
showTab('ssh', name);
}
/* ── Helpers ── */
function sshPost(url, body) {
return fetch('/ssh' + url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body || {})
}).then(function(r) { return r.json(); });
}
function sshGet(url) {
return fetch('/ssh' + url).then(function(r) { return r.json(); });
}
/* ── Tab 1: Status & Scan ── */
function sshLoadStatus() {
sshGet('/status').then(function(data) {
var dot = document.getElementById('ssh-status-dot');
var txt = document.getElementById('ssh-status-text');
if (data.active) {
dot.style.background = '#4ade80';
txt.textContent = 'Active (running)';
txt.style.color = '#4ade80';
} else {
dot.style.background = '#f87171';
txt.textContent = 'Inactive';
txt.style.color = '#f87171';
}
document.getElementById('ssh-enabled-text').textContent = data.enabled ? 'Yes' : 'No';
document.getElementById('ssh-version-text').textContent = data.version || '—';
}).catch(function() {
document.getElementById('ssh-status-text').textContent = 'Error loading status';
});
}
function sshServiceAction(action) {
sshPost('/service/' + action).then(function(data) {
if (data.ok) {
sshLoadStatus();
} else {
alert(data.error || 'Action failed');
}
}).catch(function(e) { alert('Request failed: ' + e.message); });
}
function sshRunScan() {
var btn = document.getElementById('btn-ssh-scan');
var results = document.getElementById('ssh-scan-results');
btn.disabled = true;
btn.textContent = 'Scanning...';
results.innerHTML = '<span class="text-muted">Running security scan...</span>';
sshPost('/scan').then(function(data) {
btn.disabled = false;
btn.textContent = 'Run Security Scan';
if (!data.ok && data.error) {
results.innerHTML = '<span style="color:var(--danger)">' + escapeHtml(data.error) + '</span>';
return;
}
var checks = data.checks || [];
if (!checks.length) {
results.innerHTML = '<span class="text-muted">No checks returned.</span>';
return;
}
var h = '';
checks.forEach(function(c) {
var sev = (c.severity || 'info').toLowerCase();
h += '<div class="ssh-check-card">';
h += '<div class="check-header">';
h += '<span class="ssh-severity ' + sev + '">' + escapeHtml(c.severity || 'INFO') + '</span>';
h += '<span class="check-name">' + escapeHtml(c.name || '') + '</span>';
h += '</div>';
h += '<div class="check-values">Current: <strong>' + escapeHtml(c.current || '—') + '</strong>';
if (c.recommended) {
h += ' &nbsp;|&nbsp; Recommended: <strong>' + escapeHtml(c.recommended) + '</strong>';
}
h += '</div>';
if (c.description) {
h += '<div class="check-desc">' + escapeHtml(c.description) + '</div>';
}
h += '</div>';
});
results.innerHTML = h;
halAnalyze('SSH Security Scan', JSON.stringify(data, null, 2), 'ssh audit', 'defense');
}).catch(function(e) {
btn.disabled = false;
btn.textContent = 'Run Security Scan';
results.innerHTML = '<span style="color:var(--danger)">Request failed: ' + escapeHtml(e.message) + '</span>';
});
}
/* ── Tab 2: Config Editor ── */
var sshConfigFields = [
'Port','AddressFamily','ListenAddress',
'PermitRootLogin','PasswordAuthentication','PubkeyAuthentication','PermitEmptyPasswords',
'MaxAuthTries','LoginGraceTime','UsePAM',
'MaxSessions','ClientAliveInterval','ClientAliveCountMax',
'AllowUsers','AllowGroups','DenyUsers','DenyGroups',
'AllowTcpForwarding','X11Forwarding','GatewayPorts',
'SyslogFacility','LogLevel',
'StrictModes','Ciphers','MACs','KexAlgorithms',
'Banner','Subsystem','UseDNS','Compression'
];
function sshGenerateConfig() {
var config = {};
sshConfigFields.forEach(function(key) {
var el = document.getElementById('cfg-' + key);
if (!el) return;
var val = el.value.trim();
if (val) config[key] = val;
});
sshPost('/config/generate', config).then(function(data) {
if (data.config) {
document.getElementById('ssh-config-raw').value = data.config;
document.getElementById('ssh-config-status').textContent = 'Config generated from form values.';
document.getElementById('ssh-config-status').style.color = '#4ade80';
} else if (data.error) {
document.getElementById('ssh-config-status').textContent = data.error;
document.getElementById('ssh-config-status').style.color = 'var(--danger)';
}
}).catch(function(e) {
document.getElementById('ssh-config-status').textContent = 'Request failed: ' + e.message;
document.getElementById('ssh-config-status').style.color = 'var(--danger)';
});
}
function sshLoadConfig() {
sshGet('/config').then(function(data) {
if (data.config !== undefined) {
document.getElementById('ssh-config-raw').value = data.config;
document.getElementById('ssh-config-status').textContent = 'Config loaded.';
document.getElementById('ssh-config-status').style.color = '#4ade80';
} else if (data.error) {
document.getElementById('ssh-config-status').textContent = data.error;
document.getElementById('ssh-config-status').style.color = 'var(--danger)';
}
}).catch(function(e) {
document.getElementById('ssh-config-status').textContent = 'Failed to load: ' + e.message;
document.getElementById('ssh-config-status').style.color = 'var(--danger)';
});
}
function sshSaveConfig() {
var content = document.getElementById('ssh-config-raw').value;
if (!content.trim()) {
document.getElementById('ssh-config-status').textContent = 'Config is empty — nothing to save.';
document.getElementById('ssh-config-status').style.color = 'var(--danger)';
return;
}
sshPost('/config/save', {config: content}).then(function(data) {
if (data.ok) {
document.getElementById('ssh-config-status').textContent = 'Config saved successfully.';
document.getElementById('ssh-config-status').style.color = '#4ade80';
} else {
var msg = data.error || 'Save failed';
if (data.errors && data.errors.length) {
msg += ': ' + data.errors.join('; ');
}
document.getElementById('ssh-config-status').textContent = msg;
document.getElementById('ssh-config-status').style.color = 'var(--danger)';
}
halAnalyze('SSH Config Save', JSON.stringify(data, null, 2), 'ssh config', 'defense');
}).catch(function(e) {
document.getElementById('ssh-config-status').textContent = 'Request failed: ' + e.message;
document.getElementById('ssh-config-status').style.color = 'var(--danger)';
});
}
/* ── Tab 3: Keys ── */
function sshLoadHostKeys() {
sshGet('/keys/host').then(function(data) {
var el = document.getElementById('ssh-host-keys');
var keys = data.keys || [];
if (!keys.length) {
el.innerHTML = '<span class="text-muted">No host keys found.</span>';
return;
}
var h = '<table class="data-table"><thead><tr><th>Type</th><th>Fingerprint</th><th>File</th></tr></thead><tbody>';
keys.forEach(function(k) {
h += '<tr>';
h += '<td><strong>' + escapeHtml(k.type || '') + '</strong></td>';
h += '<td style="font-family:monospace;font-size:0.78rem;word-break:break-all">' + escapeHtml(k.fingerprint || '') + '</td>';
h += '<td style="font-family:monospace;font-size:0.78rem;color:var(--text-secondary)">' + escapeHtml(k.path || '') + '</td>';
h += '</tr>';
});
h += '</tbody></table>';
el.innerHTML = h;
}).catch(function() {
document.getElementById('ssh-host-keys').innerHTML = '<span style="color:var(--danger)">Failed to load host keys.</span>';
});
}
function sshKeygenTypeChanged() {
var type = document.getElementById('keygen-type').value;
document.getElementById('keygen-bits-group').style.display = type === 'rsa' ? '' : 'none';
}
function sshGenerateKey() {
var payload = {
type: document.getElementById('keygen-type').value,
bits: parseInt(document.getElementById('keygen-bits').value) || 4096,
comment: document.getElementById('keygen-comment').value.trim(),
passphrase: document.getElementById('keygen-passphrase').value
};
var result = document.getElementById('ssh-keygen-result');
result.innerHTML = '<span class="text-muted">Generating key pair...</span>';
sshPost('/keys/generate', payload).then(function(data) {
if (!data.ok) {
result.innerHTML = '<span style="color:var(--danger)">' + escapeHtml(data.error || 'Generation failed') + '</span>';
return;
}
var h = '';
h += '<div style="margin-bottom:12px"><strong>Public Key</strong></div>';
h += '<div class="ssh-key-output">';
h += '<textarea readonly id="keygen-pubkey">' + escapeHtml(data.public_key || '') + '</textarea>';
h += '<button class="copy-btn" onclick="sshCopyKey(\'keygen-pubkey\')">Copy</button>';
h += '</div>';
h += '<div style="margin:12px 0"><strong>Private Key</strong></div>';
h += '<div class="ssh-key-output">';
h += '<textarea readonly id="keygen-privkey" style="min-height:160px">' + escapeHtml(data.private_key || '') + '</textarea>';
h += '<button class="copy-btn" onclick="sshCopyKey(\'keygen-privkey\')">Copy</button>';
h += '</div>';
result.innerHTML = h;
}).catch(function(e) {
result.innerHTML = '<span style="color:var(--danger)">Request failed: ' + escapeHtml(e.message) + '</span>';
});
}
function sshCopyKey(id) {
var el = document.getElementById(id);
if (el) {
navigator.clipboard.writeText(el.value).then(function() {
var btn = el.parentElement.querySelector('.copy-btn');
if (btn) { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 1500); }
});
}
}
function sshLoadAuthKeys() {
sshGet('/keys/authorized').then(function(data) {
var el = document.getElementById('ssh-authkeys-list');
var keys = data.keys || [];
if (!keys.length) {
el.innerHTML = '<span class="text-muted">No authorized keys found.</span>';
return;
}
var h = '<table class="data-table"><thead><tr><th style="width:80px">Type</th><th>Key (truncated)</th><th>Comment</th><th style="width:80px">Action</th></tr></thead><tbody>';
keys.forEach(function(k, i) {
var parts = (k.line || '').split(/\s+/);
var ktype = parts[0] || '—';
var kdata = parts[1] ? (parts[1].substring(0, 32) + '...') : '—';
var kcomment = parts.slice(2).join(' ') || '—';
h += '<tr>';
h += '<td style="font-family:monospace;font-size:0.78rem">' + escapeHtml(ktype) + '</td>';
h += '<td style="font-family:monospace;font-size:0.78rem;color:var(--text-secondary)">' + escapeHtml(kdata) + '</td>';
h += '<td>' + escapeHtml(kcomment) + '</td>';
h += '<td><button class="btn btn-sm" style="color:var(--danger);border-color:var(--danger)" onclick="sshRemoveAuthKey(' + i + ')">Remove</button></td>';
h += '</tr>';
});
h += '</tbody></table>';
el.innerHTML = h;
}).catch(function() {
document.getElementById('ssh-authkeys-list').innerHTML = '<span style="color:var(--danger)">Failed to load authorized keys.</span>';
});
}
function sshAddAuthKey() {
var key = document.getElementById('ssh-new-authkey').value.trim();
if (!key) { alert('Enter a public key to add.'); return; }
sshPost('/keys/authorized/add', {key: key}).then(function(data) {
if (data.ok) {
document.getElementById('ssh-new-authkey').value = '';
sshLoadAuthKeys();
} else {
alert(data.error || 'Failed to add key');
}
}).catch(function(e) { alert('Request failed: ' + e.message); });
}
function sshRemoveAuthKey(index) {
if (!confirm('Remove this authorized key?')) return;
sshPost('/keys/authorized/remove', {index: index}).then(function(data) {
if (data.ok) {
sshLoadAuthKeys();
} else {
alert(data.error || 'Failed to remove key');
}
}).catch(function(e) { alert('Request failed: ' + e.message); });
}
/* ── Tab 4: Quick Harden ── */
var sshHardenedConfig = [
'# AUTARCH Hardened SSH Configuration',
'# Generated: ' + new Date().toISOString(),
'',
'Protocol 2',
'',
'# Authentication',
'PermitRootLogin no',
'PasswordAuthentication no',
'PermitEmptyPasswords no',
'PubkeyAuthentication yes',
'MaxAuthTries 3',
'LoginGraceTime 60',
'UsePAM yes',
'',
'# Session',
'ClientAliveInterval 300',
'ClientAliveCountMax 2',
'',
'# Forwarding',
'AllowTcpForwarding no',
'X11Forwarding no',
'',
'# Security',
'StrictModes yes',
'UseDNS no',
'',
'# Cryptography',
'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com',
'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com',
'KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org',
'',
'# Subsystem',
'Subsystem sftp /usr/lib/openssh/sftp-server'
].join('\n');
function sshPreviewHarden() {
document.getElementById('ssh-harden-preview-section').style.display = '';
document.getElementById('ssh-harden-preview').textContent = sshHardenedConfig;
document.getElementById('ssh-harden-status').textContent = '';
}
function sshApplyHarden() {
if (!confirm('This will overwrite your current sshd_config with the hardened version. Are you sure?')) return;
var status = document.getElementById('ssh-harden-status');
status.textContent = 'Applying...';
status.style.color = 'var(--text-secondary)';
sshPost('/config/save', {config: sshHardenedConfig}).then(function(data) {
if (data.ok) {
status.textContent = 'Hardened config applied successfully. Restart SSH to activate.';
status.style.color = '#4ade80';
} else {
var msg = data.error || 'Apply failed';
if (data.errors && data.errors.length) {
msg += ': ' + data.errors.join('; ');
}
status.textContent = msg;
status.style.color = 'var(--danger)';
}
halAnalyze('SSH Quick Harden', JSON.stringify(data, null, 2), 'ssh hardening', 'defense');
}).catch(function(e) {
status.textContent = 'Request failed: ' + e.message;
status.style.color = 'var(--danger)';
});
}
/* ── Init ── */
document.addEventListener('DOMContentLoaded', function() {
sshLoadStatus();
sshLoadAuthKeys();
});
/* ── Fail2Ban ── */
function f2bRefresh() {
fetch('/ssh/fail2ban/status').then(function(r){return r.json()}).then(function(d) {
var dot = document.getElementById('f2b-status-dot');
var text = document.getElementById('f2b-status-text');
if (d.ok) {
dot.style.background = d.active ? 'var(--success,#34c759)' : 'var(--danger,#ff3b30)';
text.textContent = (d.active?'Active':'Inactive') + ' — ' + d.jail_count + ' jails, ' + d.total_banned + ' banned IPs';
// Render jails
var jhtml = '';
(d.jails||[]).forEach(function(j) {
jhtml += '<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.6rem 0.8rem;margin-bottom:0.4rem;background:var(--bg-card);display:flex;justify-content:space-between;align-items:center">'
+ '<div><strong style="color:var(--accent)">' + escapeHtml(j.name) + '</strong>'
+ ' <span style="font-size:0.75rem;color:var(--text-muted)">' + j.banned + ' banned</span></div>'
+ '<div>' + (j.banned_ips||[]).map(function(ip){return '<span style="font-size:0.72rem;background:rgba(255,59,48,0.15);color:#f87171;padding:1px 6px;border-radius:3px;margin-left:4px">' + escapeHtml(ip) + '</span>'}).join('') + '</div>'
+ '</div>';
});
document.getElementById('f2b-jails').innerHTML = jhtml || '<p style="color:var(--text-muted)">No active jails.</p>';
} else {
dot.style.background = 'var(--text-muted)';
text.textContent = d.error || 'Not running';
document.getElementById('f2b-jails').innerHTML = '<p style="color:var(--text-muted)">' + escapeHtml(d.error||'') + '</p>';
}
});
// Banned
fetch('/ssh/fail2ban/banned').then(function(r){return r.json()}).then(function(d) {
if (!d.ok || !d.banned || !d.banned.length) {
document.getElementById('f2b-banned').innerHTML = '<p style="color:var(--text-muted)">No banned IPs.</p>';
return;
}
var html = '<table class="data-table" style="font-size:0.82rem"><thead><tr><th>IP</th><th>Jail</th><th>Action</th></tr></thead><tbody>';
d.banned.forEach(function(b) {
html += '<tr><td>' + escapeHtml(b.ip) + '</td><td>' + escapeHtml(b.jail) + '</td>'
+ '<td><button class="btn btn-sm" style="font-size:0.65rem;padding:1px 6px" onclick="f2bUnban(\'' + escapeHtml(b.ip) + '\',\'' + escapeHtml(b.jail) + '\')">Unban</button></td></tr>';
});
html += '</tbody></table>';
document.getElementById('f2b-banned').innerHTML = html;
});
}
function f2bService(action) {
fetch('/ssh/fail2ban/service/' + action, {method:'POST'}).then(function(r){return r.json()}).then(function(d) {
f2bRefresh();
});
}
function f2bUnban(ip, jail) {
fetch('/ssh/fail2ban/unban', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ip:ip, jail:jail})})
.then(function(r){return r.json()}).then(function(d) { f2bRefresh(); });
}
function f2bManualBan() {
var ip = document.getElementById('f2b-manual-ip').value.trim();
var jail = document.getElementById('f2b-manual-jail').value.trim() || 'sshd';
if (!ip) return;
fetch('/ssh/fail2ban/ban', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ip:ip, jail:jail})})
.then(function(r){return r.json()}).then(function(d) {
document.getElementById('f2b-manual-result').innerHTML = d.ok ? '<span style="color:var(--success)">Banned ' + escapeHtml(ip) + ' in ' + escapeHtml(jail) + '</span>' : '<span style="color:var(--danger)">' + escapeHtml(d.error||d.output||'Failed') + '</span>';
f2bRefresh();
});
}
function f2bManualUnban() {
var ip = document.getElementById('f2b-manual-ip').value.trim();
if (!ip) return;
fetch('/ssh/fail2ban/unban', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ip:ip})})
.then(function(r){return r.json()}).then(function(d) {
document.getElementById('f2b-manual-result').innerHTML = d.ok ? '<span style="color:var(--success)">Unbanned ' + escapeHtml(ip) + '</span>' : '<span style="color:var(--danger)">' + escapeHtml(d.error||'Failed') + '</span>';
f2bRefresh();
});
}
function f2bSearch() {
var ip = document.getElementById('f2b-search-ip').value.trim();
if (!ip) return;
var el = document.getElementById('f2b-search-results');
el.innerHTML = '<p style="color:var(--text-muted)">Searching...</p>';
fetch('/ssh/fail2ban/search', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ip:ip})})
.then(function(r){return r.json()}).then(function(d) {
if (!d.ok) { el.textContent = d.error; return; }
var html = '<strong>Results for ' + escapeHtml(ip) + ':</strong><br>';
if (d.active_bans.length) {
html += '<div style="margin:0.4rem 0">Currently banned in: ' + d.active_bans.map(function(b){return '<span style="color:var(--danger)">' + escapeHtml(b.jail) + '</span>'}).join(', ') + '</div>';
} else {
html += '<div style="margin:0.4rem 0;color:var(--text-muted)">Not currently banned in any jail.</div>';
}
if (d.log_entries.length) {
html += '<div style="margin-top:0.5rem"><strong>Log history:</strong></div><pre style="font-size:0.72rem;max-height:150px;overflow-y:auto;background:var(--bg-card);padding:0.5rem;border-radius:var(--radius);border:1px solid var(--border)">';
d.log_entries.forEach(function(l) { html += escapeHtml(l) + '\n'; });
html += '</pre>';
}
el.innerHTML = html;
halAnalyze('Fail2Ban: IP Search', JSON.stringify(d, null, 2), 'fail2ban search', 'defense');
});
}
function f2bScanApps() {
var el = document.getElementById('f2b-apps-result');
el.innerHTML = '<p style="color:var(--text-muted)">Scanning installed applications...</p>';
fetch('/ssh/fail2ban/scan-apps', {method:'POST'}).then(function(r){return r.json()}).then(function(d) {
if (!d.ok) { el.textContent = d.error; return; }
var html = '<table class="data-table" style="font-size:0.82rem"><thead><tr><th>Service</th><th>Installed</th><th>Log Exists</th><th>Has Jail</th><th>Filter</th></tr></thead><tbody>';
(d.apps||[]).forEach(function(a) {
var ic = a.installed ? 'var(--success)' : 'var(--text-muted)';
var lc = a.log_exists ? 'var(--success)' : 'var(--text-muted)';
var jc = a.has_jail ? 'var(--success)' : a.installed ? 'var(--danger)' : 'var(--text-muted)';
html += '<tr><td><strong>' + escapeHtml(a.service) + '</strong></td>'
+ '<td style="color:' + ic + '">' + (a.installed?'Yes':'No') + '</td>'
+ '<td style="color:' + lc + '">' + (a.log_exists?'Yes':'No') + '</td>'
+ '<td style="color:' + jc + '">' + (a.has_jail?'Yes':'No') + '</td>'
+ '<td style="font-family:monospace;font-size:0.75rem">' + escapeHtml(a.filter) + '</td></tr>';
});
html += '</tbody></table>';
el.innerHTML = html;
halAnalyze('Fail2Ban: App Scan', JSON.stringify(d, null, 2), 'fail2ban app detection', 'defense');
});
}
function f2bAutoConfig(apply) {
if (apply && !confirm('This will generate and apply fail2ban jails for all detected services. Continue?')) return;
var el = document.getElementById('f2b-apps-result');
el.innerHTML = '<p style="color:var(--text-muted)">' + (apply?'Applying':'Generating preview') + '...</p>';
fetch('/ssh/fail2ban/auto-config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({apply:apply})})
.then(function(r){return r.json()}).then(function(d) {
var html = '<strong>' + d.count + ' jail(s) ' + (d.applied?'applied':'generated') + ':</strong>';
(d.generated||[]).forEach(function(g) {
html += '<pre style="font-size:0.72rem;background:var(--bg-card);padding:0.5rem;border-radius:var(--radius);border:1px solid var(--border);margin:0.4rem 0">' + escapeHtml(g.config) + '</pre>';
});
if (d.applied) { html += '<div style="color:var(--success);margin-top:0.5rem">Applied and reloaded fail2ban.</div>'; f2bRefresh(); }
el.innerHTML = html;
});
}
function f2bCreateJail() {
var data = {
name: document.getElementById('f2b-jail-name').value.trim(),
filter: document.getElementById('f2b-jail-filter').value.trim(),
logpath: document.getElementById('f2b-jail-logpath').value.trim(),
maxretry: document.getElementById('f2b-jail-maxretry').value,
findtime: document.getElementById('f2b-jail-findtime').value,
bantime: document.getElementById('f2b-jail-bantime').value,
enabled: document.getElementById('f2b-jail-enabled').checked,
};
if (!data.name) { alert('Jail name required'); return; }
fetch('/ssh/fail2ban/jail/create', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)})
.then(function(r){return r.json()}).then(function(d) {
var el = document.getElementById('f2b-create-result');
el.innerHTML = d.ok ? '<span style="color:var(--success)">Jail created and fail2ban reloaded.</span><pre style="font-size:0.72rem;margin-top:0.3rem">' + escapeHtml(d.config) + '</pre>' : '<span style="color:var(--danger)">' + escapeHtml(d.error) + '</span>';
if (d.ok) f2bRefresh();
});
}
</script>
{% endblock %}