feat: improve sentinel system

This commit is contained in:
LukeGus
2026-05-22 16:56:53 -05:00
parent ee4520a1e0
commit 6b9784c8fc
6 changed files with 98 additions and 143 deletions
+2 -9
View File
@@ -431,18 +431,11 @@ router.get(
if (credential.password) { if (credential.password) {
output.password = credential.password; output.password = credential.password;
} }
if (credential.key) { output.hasKey = !!credential.key;
output.key = credential.key; output.hasKeyPassword = !!credential.keyPassword;
}
if (credential.privateKey) {
output.privateKey = credential.privateKey;
}
if (credential.publicKey) { if (credential.publicKey) {
output.publicKey = credential.publicKey; output.publicKey = credential.publicKey;
} }
if (credential.keyPassword) {
output.keyPassword = credential.keyPassword;
}
res.json(output); res.json(output);
} catch (err) { } catch (err) {
+1 -15
View File
@@ -246,29 +246,18 @@ function normalizeImportedHost(
} }
const SENSITIVE_FIELDS = [ const SENSITIVE_FIELDS = [
"password",
"key", "key",
"keyPassword", "keyPassword",
"sudoPassword",
"autostartPassword",
"autostartKey", "autostartKey",
"autostartKeyPassword", "autostartKeyPassword",
"socks5Password",
"rdpPassword",
"vncPassword",
"telnetPassword",
]; ];
function stripSensitiveFields( function stripSensitiveFields(
host: Record<string, unknown>, host: Record<string, unknown>,
): Record<string, unknown> { ): Record<string, unknown> {
const result = { ...host }; const result = { ...host };
result.hasPassword = !!host.password;
result.hasKey = !!host.key; result.hasKey = !!host.key;
result.hasSudoPassword = !!host.sudoPassword; result.hasKeyPassword = !!host.keyPassword;
result.hasRdpPassword = !!host.rdpPassword;
result.hasVncPassword = !!host.vncPassword;
result.hasTelnetPassword = !!host.telnetPassword;
for (const field of SENSITIVE_FIELDS) { for (const field of SENSITIVE_FIELDS) {
delete result[field]; delete result[field];
} }
@@ -323,9 +312,6 @@ function transformHostResponse(
rdpIgnoreCert: !!host.rdpIgnoreCert, rdpIgnoreCert: !!host.rdpIgnoreCert,
vncUser: host.vncUser || undefined, vncUser: host.vncUser || undefined,
telnetUser: host.telnetUser || undefined, telnetUser: host.telnetUser || undefined,
hasRdpPassword: !!host.rdpPassword,
hasVncPassword: !!host.vncPassword,
hasTelnetPassword: !!host.telnetPassword,
tunnelConnections: host.tunnelConnections tunnelConnections: host.tunnelConnections
? JSON.parse(host.tunnelConnections as string) ? JSON.parse(host.tunnelConnections as string)
: [], : [],
+1 -6
View File
@@ -105,16 +105,11 @@ export interface Host {
vncUser?: string; vncUser?: string;
telnetUser?: string; telnetUser?: string;
telnetPassword?: string; telnetPassword?: string;
hasRdpPassword?: boolean;
hasVncPassword?: boolean;
hasTelnetPassword?: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
hasPassword?: boolean;
hasKey?: boolean; hasKey?: boolean;
hasSudoPassword?: boolean; hasKeyPassword?: boolean;
isShared?: boolean; isShared?: boolean;
permissionLevel?: "view"; permissionLevel?: "view";
+1 -1
View File
@@ -14,8 +14,8 @@ export type Host = {
credentialId?: string; credentialId?: string;
overrideCredentialUsername?: boolean; overrideCredentialUsername?: boolean;
password?: string; password?: string;
hasPassword?: boolean;
hasKey?: boolean; hasKey?: boolean;
hasKeyPassword?: boolean;
key?: string; key?: string;
keyPassword?: string; keyPassword?: string;
keyType?: string; keyType?: string;
+1 -1
View File
@@ -176,7 +176,6 @@
"upload": "Upload", "upload": "Upload",
"authentication": "Authentication", "authentication": "Authentication",
"password": "Password", "password": "Password",
"passwordSaved": "Password saved, type to change",
"key": "Key", "key": "Key",
"credential": "Credential", "credential": "Credential",
"none": "None", "none": "None",
@@ -297,6 +296,7 @@
"clearKey": "Clear key", "clearKey": "Clear key",
"keySaved": "SSH key saved", "keySaved": "SSH key saved",
"keyReplaceNotice": "paste a new key below to replace it", "keyReplaceNotice": "paste a new key below to replace it",
"keyPassphraseSaved": "Passphrase saved, type to change",
"replaceKey": "Replace key", "replaceKey": "Replace key",
"forceKeyboardInteractiveLabel": "Force Keyboard Interactive", "forceKeyboardInteractiveLabel": "Force Keyboard Interactive",
"forceKeyboardInteractiveShortDesc": "Force manual password entry even if keys are present", "forceKeyboardInteractiveShortDesc": "Force manual password entry even if keys are present",
+92 -111
View File
@@ -132,8 +132,8 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
tags: h.tags ?? [], tags: h.tags ?? [],
authType: h.authType, authType: h.authType,
password: h.password, password: h.password,
hasPassword: !!(h as any).hasPassword || !!h.password,
hasKey: !!(h as any).hasKey || !!(typeof h.key === "string" && h.key), hasKey: !!(h as any).hasKey || !!(typeof h.key === "string" && h.key),
hasKeyPassword: !!(h as any).hasKeyPassword || !!h.keyPassword,
key: typeof h.key === "string" ? h.key : undefined, key: typeof h.key === "string" ? h.key : undefined,
keyPassword: h.keyPassword, keyPassword: h.keyPassword,
keyType: h.keyType, keyType: h.keyType,
@@ -159,20 +159,14 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
vncPort: h.vncPort ?? (h.connectionType === "vnc" ? h.port : 5900), vncPort: h.vncPort ?? (h.connectionType === "vnc" ? h.port : 5900),
telnetPort: h.telnetPort ?? (h.connectionType === "telnet" ? h.port : 23), telnetPort: h.telnetPort ?? (h.connectionType === "telnet" ? h.port : 23),
rdpUser: h.rdpUser, rdpUser: h.rdpUser,
rdpPassword: (h as any).hasRdpPassword rdpPassword: h.rdpPassword ?? "",
? "existing_password"
: (h.rdpPassword ?? ""),
domain: h.rdpDomain, domain: h.rdpDomain,
security: h.rdpSecurity, security: h.rdpSecurity,
ignoreCert: h.rdpIgnoreCert ?? false, ignoreCert: h.rdpIgnoreCert ?? false,
vncPassword: (h as any).hasVncPassword vncPassword: h.vncPassword ?? "",
? "existing_password"
: (h.vncPassword ?? ""),
vncUser: h.vncUser, vncUser: h.vncUser,
telnetUser: h.telnetUser, telnetUser: h.telnetUser,
telnetPassword: (h as any).hasTelnetPassword telnetPassword: h.telnetPassword ?? "",
? "existing_password"
: (h.telnetPassword ?? ""),
quickActions: (h.quickActions ?? []).map((a: any) => ({ quickActions: (h.quickActions ?? []).map((a: any) => ({
name: a.name, name: a.name,
snippetId: String(a.snippetId), snippetId: String(a.snippetId),
@@ -870,10 +864,11 @@ function HostEditor({
vncPort: host?.vncPort ?? 5900, vncPort: host?.vncPort ?? 5900,
telnetPort: host?.telnetPort ?? 23, telnetPort: host?.telnetPort ?? 23,
authType: host?.authType ?? "password", authType: host?.authType ?? "password",
password: password: host?.password ?? "",
host?.password ?? (host?.hasPassword ? "existing_password" : ""),
key: host?.key ?? (host?.hasKey ? "existing_key" : ""), key: host?.key ?? (host?.hasKey ? "existing_key" : ""),
keyPassword: host?.keyPassword ?? "", keyPassword: host?.hasKeyPassword
? "existing_key_password"
: (host?.keyPassword ?? ""),
keyType: host?.keyType ?? "auto", keyType: host?.keyType ?? "auto",
keySubTab: "paste" as "paste" | "upload", keySubTab: "paste" as "paste" | "upload",
credentialId: host?.credentialId ?? "", credentialId: host?.credentialId ?? "",
@@ -951,20 +946,14 @@ function HostEditor({
quickActions: quickActions:
host?.quickActions ?? ([] as { name: string; snippetId: string }[]), host?.quickActions ?? ([] as { name: string; snippetId: string }[]),
rdpUser: host?.rdpUser ?? "", rdpUser: host?.rdpUser ?? "",
rdpPassword: (host as any)?.hasRdpPassword rdpPassword: host?.rdpPassword ?? "",
? "existing_password"
: (host?.rdpPassword ?? ""),
domain: host?.domain ?? "", domain: host?.domain ?? "",
security: host?.security ?? "", security: host?.security ?? "",
ignoreCert: host?.ignoreCert ?? false, ignoreCert: host?.ignoreCert ?? false,
vncPassword: (host as any)?.hasVncPassword vncPassword: host?.vncPassword ?? "",
? "existing_password"
: (host?.vncPassword ?? ""),
vncUser: host?.vncUser ?? "", vncUser: host?.vncUser ?? "",
telnetUser: host?.telnetUser ?? "", telnetUser: host?.telnetUser ?? "",
telnetPassword: (host as any)?.hasTelnetPassword telnetPassword: host?.telnetPassword ?? "",
? "existing_password"
: (host?.telnetPassword ?? ""),
guacamoleConfig: host?.guacamoleConfig ?? ({} as Record<string, any>), guacamoleConfig: host?.guacamoleConfig ?? ({} as Record<string, any>),
statsConfig: host?.statsConfig ?? { statsConfig: host?.statsConfig ?? {
statusCheckEnabled: true, statusCheckEnabled: true,
@@ -1092,12 +1081,12 @@ function HostEditor({
tags, tags,
pin: form.pin, pin: form.pin,
authType: form.authType, authType: form.authType,
password: password: form.password || null,
form.password === "existing_password"
? undefined
: form.password || null,
key: form.key === "existing_key" ? undefined : form.key || null, key: form.key === "existing_key" ? undefined : form.key || null,
keyPassword: form.keyPassword || null, keyPassword:
form.keyPassword === "existing_key_password"
? undefined
: form.keyPassword || null,
keyType: form.keyType !== "auto" ? form.keyType : null, keyType: form.keyType !== "auto" ? form.keyType : null,
credentialId: form.credentialId ? Number(form.credentialId) : null, credentialId: form.credentialId ? Number(form.credentialId) : null,
overrideCredentialUsername: form.overrideCredentialUsername, overrideCredentialUsername: form.overrideCredentialUsername,
@@ -1133,23 +1122,14 @@ function HostEditor({
telnetPort: Number(form.telnetPort), telnetPort: Number(form.telnetPort),
forceKeyboardInteractive: form.forceKeyboardInteractive, forceKeyboardInteractive: form.forceKeyboardInteractive,
rdpUser: form.rdpUser || null, rdpUser: form.rdpUser || null,
rdpPassword: rdpPassword: form.rdpPassword || null,
form.rdpPassword === "existing_password"
? undefined
: form.rdpPassword || null,
rdpDomain: form.domain || null, rdpDomain: form.domain || null,
rdpSecurity: form.security || null, rdpSecurity: form.security || null,
rdpIgnoreCert: form.ignoreCert, rdpIgnoreCert: form.ignoreCert,
vncPassword: vncPassword: form.vncPassword || null,
form.vncPassword === "existing_password"
? undefined
: form.vncPassword || null,
vncUser: form.vncUser || null, vncUser: form.vncUser || null,
telnetUser: form.telnetUser || null, telnetUser: form.telnetUser || null,
telnetPassword: telnetPassword: form.telnetPassword || null,
form.telnetPassword === "existing_password"
? undefined
: form.telnetPassword || null,
jumpHosts: form.jumpHosts, jumpHosts: form.jumpHosts,
portKnockSequence: form.portKnockSequence, portKnockSequence: form.portKnockSequence,
tunnelConnections: form.serverTunnels, tunnelConnections: form.serverTunnels,
@@ -1973,20 +1953,8 @@ function HostEditor({
</label> </label>
<PasswordInput <PasswordInput
className="h-8 text-xs pr-8" className="h-8 text-xs pr-8"
placeholder={ placeholder="••••••••"
form.password === "existing_password" value={form.password}
? t("hosts.passwordSaved")
: "••••••••"
}
value={
form.password === "existing_password"
? ""
: form.password
}
onFocus={() => {
if (form.password === "existing_password")
setField("password", "");
}}
onChange={(e) => setField("password", e.target.value)} onChange={(e) => setField("password", e.target.value)}
/> />
</div> </div>
@@ -2077,8 +2045,20 @@ function HostEditor({
</label> </label>
<PasswordInput <PasswordInput
className="h-8 text-xs pr-8" className="h-8 text-xs pr-8"
placeholder={t("hosts.optional")} placeholder={
value={form.keyPassword} form.keyPassword === "existing_key_password"
? t("hosts.keyPassphraseSaved")
: t("hosts.optional")
}
value={
form.keyPassword === "existing_key_password"
? ""
: form.keyPassword
}
onFocus={() => {
if (form.keyPassword === "existing_key_password")
setField("keyPassword", "");
}}
onChange={(e) => onChange={(e) =>
setField("keyPassword", e.target.value) setField("keyPassword", e.target.value)
} }
@@ -3330,20 +3310,8 @@ function HostEditor({
</label> </label>
<PasswordInput <PasswordInput
className="h-8 text-xs pr-8" className="h-8 text-xs pr-8"
placeholder={ placeholder="••••••••"
form.rdpPassword === "existing_password" value={form.rdpPassword}
? t("hosts.passwordSaved")
: "••••••••"
}
value={
form.rdpPassword === "existing_password"
? ""
: form.rdpPassword
}
onFocus={() => {
if (form.rdpPassword === "existing_password")
setField("rdpPassword", "");
}}
onChange={(e) => setField("rdpPassword", e.target.value)} onChange={(e) => setField("rdpPassword", e.target.value)}
/> />
</div> </div>
@@ -4118,20 +4086,8 @@ function HostEditor({
</label> </label>
<PasswordInput <PasswordInput
className="h-8 text-xs pr-8" className="h-8 text-xs pr-8"
placeholder={ placeholder="••••••••"
form.vncPassword === "existing_password" value={form.vncPassword}
? t("hosts.passwordSaved")
: "••••••••"
}
value={
form.vncPassword === "existing_password"
? ""
: form.vncPassword
}
onFocus={() => {
if (form.vncPassword === "existing_password")
setField("vncPassword", "");
}}
onChange={(e) => setField("vncPassword", e.target.value)} onChange={(e) => setField("vncPassword", e.target.value)}
/> />
</div> </div>
@@ -4514,20 +4470,8 @@ function HostEditor({
</label> </label>
<PasswordInput <PasswordInput
className="h-8 text-xs pr-8" className="h-8 text-xs pr-8"
placeholder={ placeholder="••••••••"
form.telnetPassword === "existing_password" value={form.telnetPassword}
? t("hosts.passwordSaved")
: "••••••••"
}
value={
form.telnetPassword === "existing_password"
? ""
: form.telnetPassword
}
onFocus={() => {
if (form.telnetPassword === "existing_password")
setField("telnetPassword", "");
}}
onChange={(e) => setField("telnetPassword", e.target.value)} onChange={(e) => setField("telnetPassword", e.target.value)}
/> />
</div> </div>
@@ -5010,9 +4954,19 @@ function CredentialEditorView({
tags: credForm.tags, tags: credForm.tags,
authType: credForm.type, authType: credForm.type,
password: credForm.type === "password" ? credForm.value : null, password: credForm.type === "password" ? credForm.value : null,
key: credForm.type === "key" ? credForm.value : null, key:
credForm.type === "key"
? credForm.value === "existing_key"
? undefined
: credForm.value || null
: null,
publicKey: credForm.type === "key" ? credForm.publicKey : null, publicKey: credForm.type === "key" ? credForm.publicKey : null,
keyPassword: credForm.type === "key" ? credForm.passphrase : null, keyPassword:
credForm.type === "key"
? credForm.passphrase === "existing_key_password"
? undefined
: credForm.passphrase || null
: null,
}; };
const saved = credential const saved = credential
? await updateCredential(Number(credential.id), data) ? await updateCredential(Number(credential.id), data)
@@ -5218,7 +5172,9 @@ function CredentialEditorView({
| "ssh-rsa" | "ssh-rsa"
| "ecdsa-sha2-nistp256", | "ecdsa-sha2-nistp256",
bits, bits,
credForm.passphrase || undefined, credForm.passphrase === "existing_key_password"
? undefined
: credForm.passphrase || undefined,
); );
if (result.success) { if (result.success) {
setCredField("value", result.privateKey); setCredField("value", result.privateKey);
@@ -5272,10 +5228,17 @@ function CredentialEditorView({
e.target.value = ""; e.target.value = "";
}} }}
/> />
{credForm.value === "existing_key" && (
<div className="px-3 py-2 text-[10px] border border-accent-brand/30 bg-accent-brand/5 text-accent-brand">
{t("hosts.keySaved")} {t("hosts.keyReplaceNotice")}
</div>
)}
<textarea <textarea
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
rows={8} rows={8}
value={credForm.value} value={
credForm.value === "existing_key" ? "" : credForm.value
}
onChange={(e) => setCredField("value", e.target.value)} onChange={(e) => setCredField("value", e.target.value)}
className="w-full px-3 py-2 text-[10px] bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono" className="w-full px-3 py-2 text-[10px] bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
/> />
@@ -5286,8 +5249,20 @@ function CredentialEditorView({
</label> </label>
<PasswordInput <PasswordInput
className="h-8 text-xs pr-8" className="h-8 text-xs pr-8"
placeholder="••••••••" placeholder={
value={credForm.passphrase} credForm.passphrase === "existing_key_password"
? t("hosts.keyPassphraseSaved")
: "••••••••"
}
value={
credForm.passphrase === "existing_key_password"
? ""
: credForm.passphrase
}
onFocus={() => {
if (credForm.passphrase === "existing_key_password")
setCredField("passphrase", "");
}}
onChange={(e) => setCredField("passphrase", e.target.value)} onChange={(e) => setCredField("passphrase", e.target.value)}
/> />
</div> </div>
@@ -5301,13 +5276,19 @@ function CredentialEditorView({
variant="outline" variant="outline"
size="sm" size="sm"
className="h-6 text-[10px] px-2 border-accent-brand/40 text-accent-brand" className="h-6 text-[10px] px-2 border-accent-brand/40 text-accent-brand"
disabled={!credForm.value || generatingPublicKey} disabled={
!credForm.value ||
credForm.value === "existing_key" ||
generatingPublicKey
}
onClick={async () => { onClick={async () => {
setGeneratingPublicKey(true); setGeneratingPublicKey(true);
try { try {
const result = await generatePublicKeyFromPrivate( const result = await generatePublicKeyFromPrivate(
credForm.value, credForm.value,
credForm.passphrase || undefined, credForm.passphrase === "existing_key_password"
? undefined
: credForm.passphrase || undefined,
); );
if (result?.publicKey) { if (result?.publicKey) {
setCredField("publicKey", result.publicKey); setCredField("publicKey", result.publicKey);
@@ -6935,12 +6916,12 @@ export function HostManager({
); );
setEditingCredential({ setEditingCredential({
...cred, ...cred,
value: value: (full as any).hasKey
(full as any).password ?? ? "existing_key"
(full as any).key ?? : ((full as any).password ?? ""),
"", passphrase: (full as any).hasKeyPassword
passphrase: ? "existing_key_password"
(full as any).keyPassword ?? "", : "",
}); });
} catch { } catch {
setEditingCredential(cred); setEditingCredential(cred);