Files
keywarden/web/templates/admin_settings.html

577 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "content"}}
<div class="row row-deck row-cards">
<!-- Login Page Customization -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-palette"></i> Login Page Customization</h3>
</div>
<div class="card-body">
<!-- Background Image -->
<h4 class="mb-3"><i class="ti ti-photo"></i> Background Image</h4>
{{if loginBgImage}}
<div class="mb-3">
<div class="row align-items-center">
<div class="col-auto">
<img src="{{loginBgImage}}" class="rounded border" style="max-width: 240px; max-height: 140px; object-fit: cover;">
</div>
<div class="col-auto">
<form action="/admin/branding/remove-bg" method="post" onsubmit="return confirm('Remove the login background image?');">
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="ti ti-trash"></i> Remove Image
</button>
</form>
</div>
</div>
</div>
{{end}}
<form action="/admin/branding/upload" method="post" enctype="multipart/form-data">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Upload Background Image</label>
<input type="file" name="login_bg" class="form-control" accept="image/png,image/jpeg,image/webp">
<small class="form-hint">Max 5 MB. JPEG, PNG or WebP. The image is centered and fills the screen without distortion.</small>
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">
<i class="ti ti-upload"></i> Upload
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Application Settings -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-settings"></i> Application Settings</h3>
</div>
<div class="card-body">
<form action="/admin/settings" method="post">
<input type="hidden" name="form_type" value="app_settings">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Application Name</label>
<input type="text" name="app_name" class="form-control" value="{{index .Settings "app_name"}}" placeholder="Keywarden">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Default Key Type</label>
<select name="default_key_type" class="form-select">
<option value="ed25519" {{if eq (index .Settings "default_key_type") "ed25519"}}selected{{end}}>Ed25519</option>
<option value="rsa" {{if eq (index .Settings "default_key_type") "rsa"}}selected{{end}}>RSA</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Default RSA Key Bits</label>
<select name="default_key_bits" class="form-select">
<option value="4096" {{if eq (index .Settings "default_key_bits") "4096"}}selected{{end}}>4096</option>
<option value="2048" {{if eq (index .Settings "default_key_bits") "2048"}}selected{{end}}>2048</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Session Timeout (minutes)</label>
<input type="number" name="session_timeout" class="form-control" value="{{index .Settings "session_timeout"}}" placeholder="60" min="5" max="1440">
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">
<i class="ti ti-device-floppy"></i> Save Settings
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Security Settings -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-shield-lock"></i> Security Settings</h3>
</div>
<div class="card-body">
<form action="/admin/settings" method="post">
<input type="hidden" name="form_type" value="security_settings">
<!-- Password Policy -->
<h4 class="mb-3"><i class="ti ti-lock"></i> Password Policy</h4>
<div class="row mb-3">
<div class="col-md-4 mb-3">
<label class="form-label">Minimum Password Length</label>
<input type="number" name="pw_min_length" class="form-control"
value="{{if index .Settings "pw_min_length"}}{{index .Settings "pw_min_length"}}{{else}}8{{end}}"
min="4" max="128" placeholder="8">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="pw_require_upper" value="true"
{{if or (eq (index .Settings "pw_require_upper") "true") (eq (index .Settings "pw_require_upper") "")}}checked{{end}}>
<span class="form-check-label">Require uppercase letter (A-Z)</span>
</label>
</div>
<div class="col-md-6">
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="pw_require_lower" value="true"
{{if or (eq (index .Settings "pw_require_lower") "true") (eq (index .Settings "pw_require_lower") "")}}checked{{end}}>
<span class="form-check-label">Require lowercase letter (a-z)</span>
</label>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="pw_require_digit" value="true"
{{if or (eq (index .Settings "pw_require_digit") "true") (eq (index .Settings "pw_require_digit") "")}}checked{{end}}>
<span class="form-check-label">Require digit (0-9)</span>
</label>
</div>
<div class="col-md-6">
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="pw_require_special" value="true"
{{if eq (index .Settings "pw_require_special") "true"}}checked{{end}}>
<span class="form-check-label">Require special character (!@#$...)</span>
</label>
</div>
</div>
<hr class="my-4">
<!-- MFA Enforcement -->
<h4 class="mb-3"><i class="ti ti-shield-check"></i> MFA Enforcement</h4>
<div class="row mb-3">
<div class="col-md-8">
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="mfa_required" value="true"
{{if eq (index .Settings "mfa_required") "true"}}checked{{end}}>
<span class="form-check-label">Require MFA for all users</span>
<span class="form-check-description">When enabled, all users must set up two-factor authentication before using the application. Users without MFA will be redirected to the MFA setup page.</span>
</label>
</div>
</div>
<hr class="my-4">
<!-- Account Lockout -->
<h4 class="mb-3"><i class="ti ti-lock-access"></i> Account Lockout</h4>
<div class="row mb-3">
<div class="col-md-4 mb-3">
<label class="form-label">Failed Attempts Before Lockout</label>
<input type="number" name="lockout_attempts" class="form-control"
value="{{if index .Settings "lockout_attempts"}}{{index .Settings "lockout_attempts"}}{{else}}5{{end}}"
min="0" max="100" placeholder="5">
<small class="form-hint">Set to 0 to disable account lockout.</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Lockout Duration (minutes)</label>
<input type="number" name="lockout_duration" class="form-control"
value="{{if index .Settings "lockout_duration"}}{{index .Settings "lockout_duration"}}{{else}}15{{end}}"
min="1" max="1440" placeholder="15">
<small class="form-hint">How long to lock the account after too many failed attempts.</small>
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">
<i class="ti ti-device-floppy"></i> Save Security Settings
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Email / SMTP Configuration -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-mail"></i> Email / SMTP</h3>
</div>
<div class="card-body">
{{if .EmailEnabled}}
<div class="alert alert-success">
<div class="d-flex">
<div><i class="ti ti-check icon alert-icon"></i></div>
<div>
<h4 class="alert-title">SMTP is configured</h4>
<div class="text-secondary">Email notifications are available. SMTP settings are managed via environment variables.</div>
</div>
</div>
</div>
<form action="/admin/settings/email/test" method="post">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Send Test Email</label>
<div class="input-icon">
<span class="input-icon-addon"><i class="ti ti-mail"></i></span>
<input type="email" name="test_email" class="form-control" placeholder="recipient@example.com" required>
</div>
</div>
<div class="col-auto mb-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary">
<i class="ti ti-send"></i> Send Test
</button>
</div>
</div>
<small class="form-hint mt-n2 d-block mb-3">Send a test email to verify your SMTP configuration.</small>
</form>
{{else}}
<div class="alert alert-warning">
<div class="d-flex">
<div><i class="ti ti-alert-triangle icon alert-icon"></i></div>
<div>
<h4 class="alert-title">SMTP is not configured</h4>
<div class="text-secondary">
Set the <code>KEYWARDEN_SMTP_HOST</code> environment variable to enable email notifications.
See the <a href="https://git.techniverse.net/scriptos/keywarden/src/branch/master/docs/email.md" target="_blank">Email documentation</a> for details.
</div>
</div>
</div>
</div>
{{end}}
</div>
</div>
</div>
<!-- System Master Key -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-key"></i> System Master Key</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
<div class="d-flex">
<div><i class="ti ti-info-circle icon alert-icon"></i></div>
<div>
<h4 class="alert-title">About the System Master Key</h4>
<div class="text-secondary">
The system master key is used by Keywarden to authenticate against remote servers for
key deployments and assignment syncs. You must add this public key to the
<code>~/.ssh/authorized_keys</code> file of the admin user on each target server.
</div>
</div>
</div>
</div>
{{if .MasterKeyPublic}}
<input type="hidden" id="masterKeyValue" value="{{.MasterKeyPublic}}">
<div class="mb-3">
<label class="form-label">Public Key</label>
<div class="input-group">
<input type="password" class="form-control" id="masterKeyDisplay" value="{{.MasterKeyPublic}}" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="toggleMasterKey()" title="Show/Hide">
<i class="ti ti-eye" id="masterKeyEyeIcon"></i>
</button>
<button class="btn btn-outline-primary" type="button" onclick="copyMasterKey(this)" title="Copy">
<i class="ti ti-copy"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Fingerprint</label>
<code>{{.MasterKeyFingerprint}}</code>
</div>
<script>
function copyMasterKey(btn) {
var text = document.getElementById('masterKeyValue').value;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(function() {
btn.innerHTML = '<i class="ti ti-check"></i>';
setTimeout(function() { btn.innerHTML = '<i class="ti ti-copy"></i>'; }, 2000);
});
} else {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.innerHTML = '<i class="ti ti-check"></i>';
setTimeout(function() { btn.innerHTML = '<i class="ti ti-copy"></i>'; }, 2000);
}
}
function toggleMasterKey() {
var input = document.getElementById('masterKeyDisplay');
var icon = document.getElementById('masterKeyEyeIcon');
if (input.type === 'password') {
input.type = 'text';
icon.className = 'ti ti-eye-off';
} else {
input.type = 'password';
icon.className = 'ti ti-eye';
}
}
</script>
{{else}}
<div class="alert alert-danger">
<i class="ti ti-alert-triangle"></i> System master key not found. Please restart Keywarden to generate it.
</div>
{{end}}
<hr>
<h4 class="mb-3"><i class="ti ti-refresh"></i> Regenerate Master Key</h4>
<div class="alert alert-warning">
<div class="d-flex">
<div><i class="ti ti-alert-triangle icon alert-icon"></i></div>
<div>
<h4 class="alert-title">Warning</h4>
<div class="text-secondary">
Regenerating the master key will invalidate all existing server connections.
You must re-deploy the new public key to all target servers. This action cannot be undone.
</div>
</div>
</div>
</div>
<form action="/admin/masterkey/regenerate" method="post" onsubmit="return confirm('Are you absolutely sure you want to regenerate the system master key? All existing server connections will break until the new key is deployed.');">
<div class="row align-items-end">
<div class="col-md-6 mb-3">
<label class="form-label">Confirm your password</label>
<input type="password" name="confirm_password" class="form-control" placeholder="Enter your password" required>
</div>
<div class="col-auto mb-3">
<button type="submit" class="btn btn-danger">
<i class="ti ti-refresh"></i> Regenerate Master Key
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Key Enforcement (Bastillion-Style) -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-shield-check"></i> Key Enforcement</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
<div class="d-flex">
<div><i class="ti ti-info-circle icon alert-icon"></i></div>
<div>
<h4 class="alert-title">Enforced Key Management</h4>
<div class="text-secondary">
When enabled, Keywarden periodically connects to all managed servers and verifies that only
authorized SSH keys (managed by Keywarden + the system master key) are present in
<code>authorized_keys</code>. Unauthorized keys are detected and optionally removed automatically.
<br><br>
<strong>Monitor mode:</strong> Detects unauthorized keys and logs them in the audit log, but does not remove them.<br>
<strong>Enforce mode:</strong> Detects unauthorized keys and <em>removes them automatically</em>, keeping only Keywarden-managed keys.
</div>
</div>
</div>
</div>
<form action="/admin/settings" method="post">
<input type="hidden" name="form_type" value="enforcement_settings">
<div class="row mb-3">
<div class="col-md-4 mb-3">
<label class="form-label">Enforcement Mode</label>
<select name="enforce_mode" class="form-select">
<option value="disabled" {{if or (not .EnforcementStatus) (eq (index .EnforcementStatus "mode") "disabled")}}selected{{end}}>Disabled</option>
<option value="monitor" {{if and .EnforcementStatus (eq (index .EnforcementStatus "mode") "monitor")}}selected{{end}}>Monitor (detect only)</option>
<option value="enforce" {{if and .EnforcementStatus (eq (index .EnforcementStatus "mode") "enforce")}}selected{{end}}>Enforce (detect &amp; remove)</option>
</select>
<small class="form-hint">Choose how Keywarden handles unauthorized keys on your servers.</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Check Interval (minutes)</label>
<input type="number" name="enforce_interval" class="form-control"
value="{{if and .EnforcementStatus (index .EnforcementStatus "interval")}}{{index .EnforcementStatus "interval"}}{{else}}15{{end}}"
min="1" max="1440" placeholder="15">
<small class="form-hint">How often Keywarden checks the servers (11440 minutes).</small>
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">
<i class="ti ti-device-floppy"></i> Save Enforcement Settings
</button>
</div>
</form>
{{if and .EnforcementStatus (index .EnforcementStatus "last_run")}}
<hr class="my-4">
<h4 class="mb-3"><i class="ti ti-history"></i> Last Enforcement Run</h4>
<div class="datagrid mb-3">
<div class="datagrid-item">
<div class="datagrid-title">Last Run</div>
<div class="datagrid-content">{{index .EnforcementStatus "last_run"}}</div>
</div>
<div class="datagrid-item">
<div class="datagrid-title">Result</div>
<div class="datagrid-content">{{index .EnforcementStatus "last_result"}}</div>
</div>
</div>
{{end}}
{{if and .EnforcementStatus (ne (index .EnforcementStatus "mode") "disabled")}}
<hr class="my-4">
<h4 class="mb-3"><i class="ti ti-player-play"></i> Manual Run</h4>
<form action="/admin/enforcement/run" method="post" onsubmit="return confirm('Start a key enforcement run now? This will connect to all managed servers.');">
<button type="submit" class="btn btn-warning">
<i class="ti ti-player-play"></i> Run Enforcement Now
</button>
<small class="form-hint d-inline-block ms-2">Trigger an immediate enforcement check on all servers.</small>
</form>
{{end}}
</div>
</div>
</div>
<!-- Backup & Restore -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-database-export"></i> Backup & Restore</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
<div class="d-flex">
<div><i class="ti ti-info-circle icon alert-icon"></i></div>
<div>
<h4 class="alert-title">About Backups</h4>
<div class="text-secondary">
Backups contain all system data including users, SSH keys, servers, groups, assignments,
cron jobs, settings, and audit logs. The backup file is encrypted with AES-256-GCM using
the password you provide. Keep the password safe — without it, the backup cannot be restored.
</div>
</div>
</div>
</div>
<!-- Export Backup -->
<h4 class="mb-3"><i class="ti ti-download"></i> Export Backup</h4>
<form action="/admin/backup/export" method="post" id="backupExportForm">
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">Backup Password</label>
<input type="password" name="backup_password" id="backup_password" class="form-control" placeholder="Enter a secure password" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Confirm Password</label>
<input type="password" name="backup_password_confirm" id="backup_password_confirm" class="form-control" placeholder="Confirm password" required>
</div>
<div class="col-auto mb-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary" onclick="return validateBackupForm()">
<i class="ti ti-download"></i> Export Backup
</button>
</div>
</div>
<small class="form-hint mt-n2 d-block mb-3">Password must meet the configured password policy.</small>
</form>
<hr class="my-4">
<!-- Import Backup -->
<h4 class="mb-3"><i class="ti ti-upload"></i> Restore Backup</h4>
<div class="alert alert-warning">
<div class="d-flex">
<div><i class="ti ti-alert-triangle icon alert-icon"></i></div>
<div>
<h4 class="alert-title">Warning</h4>
<div class="text-secondary">
Restoring a backup will <strong>replace all current data</strong> with the data from the backup file.
This action cannot be undone. Make sure to export a current backup first if needed.
</div>
</div>
</div>
</div>
<form action="/admin/backup/import" method="post" enctype="multipart/form-data" onsubmit="return confirm('Are you absolutely sure you want to restore this backup? ALL current data will be replaced. This action cannot be undone!');">
<div class="row">
<div class="col-md-5 mb-3">
<label class="form-label">Backup File (.kwbak)</label>
<input type="file" name="backup_file" class="form-control" accept=".kwbak" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Backup Password</label>
<input type="password" name="restore_password" class="form-control" placeholder="Enter the backup password" required>
</div>
<div class="col-auto mb-3 d-flex align-items-end">
<button type="submit" class="btn btn-danger">
<i class="ti ti-upload"></i> Restore Backup
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
function validateBackupForm() {
var pw = document.getElementById('backup_password').value;
var pwConfirm = document.getElementById('backup_password_confirm').value;
if (pw !== pwConfirm) {
alert('Passwords do not match.');
return false;
}
if (pw.length < 4) {
alert('Password is too short.');
return false;
}
return true;
}
// Track unsaved changes on forms (except backup/masterkey forms)
(function() {
var settingsForms = document.querySelectorAll('form[action="/admin/settings"]');
settingsForms.forEach(function(form) {
var inputs = form.querySelectorAll('input, select, textarea');
var saveBtn = form.querySelector('button[type="submit"]');
var banner = null;
function markDirty() {
form.dataset.dirty = 'true';
// Highlight save button
if (saveBtn) {
saveBtn.classList.remove('btn-primary');
saveBtn.classList.add('btn-warning');
saveBtn.innerHTML = '<i class="ti ti-alert-triangle"></i> Unsaved changes Save now!';
}
// Show banner if not already visible
if (!banner) {
banner = document.createElement('div');
banner.className = 'alert alert-warning alert-dismissible mt-2 mb-0 py-2';
banner.setAttribute('role', 'alert');
banner.innerHTML = '<i class="ti ti-alert-triangle"></i> You have unsaved changes. Click the save button to apply them.';
form.querySelector('.form-footer').insertAdjacentElement('beforebegin', banner);
}
}
inputs.forEach(function(input) {
if (input.type === 'hidden') return;
input.addEventListener('change', markDirty);
input.addEventListener('input', markDirty);
});
form.addEventListener('submit', function() {
form.dataset.dirty = '';
window._adminSettingsSaving = true;
});
});
// Warn before leaving with unsaved changes
window.addEventListener('beforeunload', function(e) {
if (window._adminSettingsSaving) return;
var dirty = document.querySelector('form[data-dirty="true"]');
if (dirty) {
e.preventDefault();
e.returnValue = '';
}
});
})();
</script>
{{end}}