feat: harden console sessions + complete protocol support
- single-use session tokens (atomic ConsumeSessionToken) + user-bound tunnel (actor must match session opener) - wire VNC end-to-end (stash/consume password, connect+tunnel, frontend password field) - passphrase-protected SSH keys: passphrase_enc on Key model, capture on upload, decrypt + pass to guacd
This commit is contained in:
+15
-1
@@ -12,9 +12,10 @@ function UploadKeyModal({ onClose }: { onClose: () => void }) {
|
||||
const [label, setLabel] = useState("");
|
||||
const [publicKey, setPublicKey] = useState("");
|
||||
const [privateKey, setPrivateKey] = useState("");
|
||||
const [passphrase, setPassphrase] = useState("");
|
||||
|
||||
const { mutate: upload, isPending, error } = useMutation({
|
||||
mutationFn: () => api.uploadKey(label.trim(), publicKey.trim(), privateKey.trim() || undefined),
|
||||
mutationFn: () => api.uploadKey(label.trim(), publicKey.trim(), privateKey.trim() || undefined, passphrase || undefined),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["keys"] });
|
||||
onClose();
|
||||
@@ -70,6 +71,19 @@ function UploadKeyModal({ onClose }: { onClose: () => void }) {
|
||||
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 font-mono text-xs text-text-primary placeholder-text-secondary/50 focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
Passphrase{" "}
|
||||
<span className="text-text-tertiary font-normal">(optional — for an encrypted private key)</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passphrase}
|
||||
onChange={(e) => setPassphrase(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder-text-secondary/50 focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function ServerConsolePage() {
|
||||
const [sshUsername, setSshUsername] = useState<string>("root");
|
||||
const [rdpUsername, setRdpUsername] = useState("");
|
||||
const [rdpPassword, setRdpPassword] = useState("");
|
||||
const [vncPassword, setVncPassword] = useState("");
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -77,6 +78,8 @@ export default function ServerConsolePage() {
|
||||
} else if (protocol === "rdp") {
|
||||
body.rdp_username = rdpUsername || undefined;
|
||||
body.rdp_password = rdpPassword || undefined;
|
||||
} else if (protocol === "vnc") {
|
||||
body.rdp_password = vncPassword || undefined;
|
||||
}
|
||||
|
||||
const { token, ws_path } = await api.connectConsole(body);
|
||||
@@ -205,6 +208,19 @@ export default function ServerConsolePage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{protocol === "vnc" && (
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-text-secondary">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={vncPassword}
|
||||
onChange={(e) => setVncPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button variant="primary" loading={connecting} disabled={!protocol} onClick={handleConnect}>
|
||||
Connect
|
||||
</Button>
|
||||
|
||||
+8
-2
@@ -46,6 +46,7 @@ export interface Key {
|
||||
source: KeySource;
|
||||
generated_by_server_id?: string;
|
||||
has_private_key: boolean;
|
||||
has_passphrase?: boolean;
|
||||
created_at: string;
|
||||
assigned_count?: number;
|
||||
}
|
||||
@@ -278,10 +279,15 @@ export const api = {
|
||||
return request<KeyWithAssignments>(`/keys/${keyId}`);
|
||||
},
|
||||
|
||||
uploadKey(label: string, public_key: string, private_key?: string): Promise<Key> {
|
||||
uploadKey(label: string, public_key: string, private_key?: string, passphrase?: string): Promise<Key> {
|
||||
return request<Key>("/keys", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ label, public_key, private_key: private_key || undefined }),
|
||||
body: JSON.stringify({
|
||||
label,
|
||||
public_key,
|
||||
private_key: private_key || undefined,
|
||||
passphrase: passphrase || undefined,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
+21
-6
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -10,7 +14,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -18,9 +22,20 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user