Skip to content

Commit 94e97a7

Browse files
fix: implement persistent SSH key storage with key generation
- Fix 'error in libcrypto' issue by using persistent key files instead of temporary ones - Add SSH key pair generation feature with 'Generate Key Pair' button - Add 'View Public Key' button for generated keys with copy-to-clipboard functionality - Remove confusing 'both' authentication option, now only supports 'password' OR 'key' - Add persistent storage in data/ssh-keys/ directory with proper permissions - Update database schema with ssh_key_path and key_generated columns - Add API endpoints for key generation and public key retrieval - Enhance UX by hiding manual key input when key pair is generated - Update HelpModal documentation to reflect new SSH key features - Fix all TypeScript compilation errors and linting issues Resolves SSH authentication failures during script execution
1 parent 0e95c12 commit 94e97a7

File tree

16 files changed

+872
-269
lines changed

16 files changed

+872
-269
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<!-- e1958379-99ce-42d2-8fe6-2c5007b3f52a cd8f1eb6-6ae5-4b21-9ca9-c208d4e80622 -->
2+
# Persistent SSH Keys with Simplified Authentication
3+
4+
## Overview
5+
6+
Simplify SSH authentication to only support password OR key (remove confusing 'both' option). Use persistent key files instead of temporary files. Add on-the-fly key generation with public key viewing/copying.
7+
8+
## Implementation Steps
9+
10+
### 1. Create SSH Keys Directory Structure
11+
12+
- Add `data/ssh-keys/` directory for persistent SSH key files
13+
- Name format: `server_{id}_key` and `server_{id}_key.pub`
14+
- Set permissions: 0700 for directory, 0600 for key files
15+
- Add to `.gitignore` to prevent committing keys
16+
17+
### 2. Update Database Schema (`src/server/database.js`)
18+
19+
- Change auth_type CHECK constraint: only allow 'password' or 'key' (remove 'both')
20+
- Add `ssh_key_path` TEXT column for file path
21+
- Add `key_generated` INTEGER (0/1) to track generated vs user-provided keys
22+
- Migration: Convert existing 'both' auth_type to 'key'
23+
- Update `addServer()`: Write key to persistent file, store path
24+
- Update `updateServer()`: Handle key changes (write new, delete old)
25+
- Update `deleteServer()`: Clean up key files
26+
27+
### 3. SSH Key Generation Feature (`src/server/ssh-service.js`)
28+
29+
- Add `generateKeyPair(serverId)` method using `ssh-keygen` command
30+
- Command: `ssh-keygen -t ed25519 -f data/ssh-keys/server_{id}_key -N "" -C "pve-scripts-local"`
31+
- Return both private and public key content
32+
- Add `getPublicKey(keyPath)` to extract public key from private key
33+
34+
### 4. Backend API Endpoints (New Files)
35+
36+
- `POST /api/servers/generate-keypair`
37+
- Generate temporary key pair (not yet saved to server)
38+
- Return `{ privateKey, publicKey }`
39+
- `GET /api/servers/[id]/public-key`
40+
- Only if `key_generated === 1`
41+
- Return `{ publicKey, serverName, serverIp }`
42+
43+
### 5. Frontend: ServerForm Component
44+
45+
- **Remove 'both' from auth_type dropdown** - only show Password/SSH Key
46+
- Add "Generate Key Pair" button (visible when auth_type === 'key')
47+
- On generate: populate SSH key field, show modal with public key
48+
- Add PublicKeyModal component with copy-to-clipboard
49+
- Disable manual key entry when using generated key
50+
51+
### 6. Frontend: ServerList Component
52+
53+
- Add "View Public Key" button between Test Connection and Edit
54+
- Only show when `server.key_generated === true`
55+
- Click opens PublicKeyModal with copy button
56+
- Show instructions: "Add this to /root/.ssh/authorized_keys on your server"
57+
58+
### 7. Update SSH Service (`src/server/ssh-service.js`)
59+
60+
- **Remove all 'both' auth_type handling**
61+
- Remove temp file creation from `testWithSSHKey()`
62+
- Use persistent `ssh_key_path` from database
63+
- Simplify `testConnection()`: only handle 'password' or 'key'
64+
65+
### 8. Update SSH Execution Service (`src/server/ssh-execution-service.js`)
66+
67+
- **Remove `createTempKeyFile()` method**
68+
- **Remove all 'both' auth_type cases**
69+
- Update `buildSSHCommand()`: use `ssh_key_path`, only handle 'password'/'key'
70+
- Update `transferScriptsFolder()`: use `ssh_key_path` in rsync
71+
- Update `executeCommand()`: use `ssh_key_path`
72+
- Remove all temp file cleanup code
73+
74+
### 9. Files to Create/Modify
75+
76+
- `src/server/database.js` - Schema changes, persistence
77+
- `src/server/ssh-service.js` - Key generation, remove temp files, remove 'both'
78+
- `src/server/ssh-execution-service.js` - Use persistent keys, remove 'both'
79+
- `src/app/api/servers/generate-keypair/route.ts` - NEW
80+
- `src/app/api/servers/[id]/public-key/route.ts` - NEW
81+
- `src/app/_components/ServerForm.tsx` - Generate button, remove 'both'
82+
- `src/app/_components/ServerList.tsx` - View public key button
83+
- `src/app/_components/PublicKeyModal.tsx` - NEW component
84+
- `.gitignore` - Add `data/ssh-keys/`
85+
86+
### 11. Migration & Initialization
87+
88+
- On startup: create `data/ssh-keys/` if missing
89+
- Existing 'both' servers: convert to 'key' auth_type
90+
- Existing ssh_key content: migrate to persistent files on first use
91+
- Set `key_generated = 0` for migrated servers
92+
93+
## Benefits
94+
95+
- Simpler auth model (no confusing 'both' option)
96+
- Fixes "error in libcrypto" timing issues
97+
- User-friendly key generation
98+
- Easy public key access for setup
99+
- Less code complexity
100+
101+
### To-dos
102+
103+
- [ ] Create data/ssh-keys directory structure and update .gitignore
104+
- [ ] Add ssh_key_path column to database and implement migration
105+
- [ ] Modify addServer/updateServer/deleteServer to handle persistent key files
106+
- [ ] Remove temp key creation from ssh-service.js and use persistent paths
107+
- [ ] Remove createTempKeyFile and update all methods to use persistent keys
108+
- [ ] Test with existing servers that have SSH keys to ensure migration works
109+
- [ ] Modify the HelpModal to reflect the changes Made to the SSH auth
110+
- [ ] Create a fix branch

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
db.sqlite
1717
data/settings.db
1818

19+
# ssh keys (sensitive)
20+
data/ssh-keys/
21+
1922
# next.js
2023
/.next/
2124
/out/

scripts/ct/debian.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env bash
2+
SCRIPT_DIR="$(dirname "$0")"
3+
source "$SCRIPT_DIR/../core/build.func"
4+
# Copyright (c) 2021-2025 tteck
5+
# Author: tteck (tteckster)
6+
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
7+
# Source: https://www.debian.org/
8+
9+
APP="Debian"
10+
var_tags="${var_tags:-os}"
11+
var_cpu="${var_cpu:-1}"
12+
var_ram="${var_ram:-512}"
13+
var_disk="${var_disk:-2}"
14+
var_os="${var_os:-debian}"
15+
var_version="${var_version:-13}"
16+
var_unprivileged="${var_unprivileged:-1}"
17+
18+
header_info "$APP"
19+
variables
20+
color
21+
catch_errors
22+
23+
function update_script() {
24+
header_info
25+
check_container_storage
26+
check_container_resources
27+
if [[ ! -d /var ]]; then
28+
msg_error "No ${APP} Installation Found!"
29+
exit
30+
fi
31+
msg_info "Updating $APP LXC"
32+
$STD apt update
33+
$STD apt -y upgrade
34+
msg_ok "Updated $APP LXC"
35+
exit
36+
}
37+
38+
start
39+
build_container
40+
description
41+
42+
msg_ok "Completed Successfully!\n"
43+
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"

scripts/install/debian-install.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
3+
# Copyright (c) 2021-2025 tteck
4+
# Author: tteck (tteckster)
5+
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
6+
# Source: https://www.debian.org/
7+
8+
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
9+
color
10+
verb_ip6
11+
catch_errors
12+
setting_up_container
13+
network_check
14+
update_os
15+
16+
motd_ssh
17+
customize
18+
19+
msg_info "Cleaning up"
20+
$STD apt -y autoremove
21+
$STD apt -y autoclean
22+
$STD apt -y clean
23+
msg_ok "Cleaned"
24+

src/app/_components/HelpModal.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,15 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'
5555
<ul className="text-sm text-muted-foreground space-y-2">
5656
<li><strong>Password:</strong> Use username and password authentication</li>
5757
<li><strong>SSH Key:</strong> Use SSH key pair for secure authentication</li>
58-
<li><strong>Both:</strong> Try SSH key first, fallback to password if needed</li>
5958
</ul>
59+
<div className="mt-3 p-3 bg-blue-50 dark:bg-blue-950/20 rounded-md">
60+
<h5 className="font-medium text-blue-900 dark:text-blue-100 mb-2">SSH Key Features:</h5>
61+
<ul className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
62+
<li><strong>Generate Key Pair:</strong> Create new SSH keys automatically</li>
63+
<li><strong>View Public Key:</strong> Copy public key for server setup</li>
64+
<li><strong>Persistent Storage:</strong> Keys are stored securely on disk</li>
65+
</ul>
66+
</div>
6067
</div>
6168

6269
<div className="p-4 border border-border rounded-lg">
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { X, Copy, Check, Server, Globe } from 'lucide-react';
5+
import { Button } from './ui/button';
6+
7+
interface PublicKeyModalProps {
8+
isOpen: boolean;
9+
onClose: () => void;
10+
publicKey: string;
11+
serverName: string;
12+
serverIp: string;
13+
}
14+
15+
export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
16+
const [copied, setCopied] = useState(false);
17+
18+
if (!isOpen) return null;
19+
20+
const handleCopy = async () => {
21+
try {
22+
// Try modern clipboard API first
23+
if (navigator.clipboard && window.isSecureContext) {
24+
await navigator.clipboard.writeText(publicKey);
25+
setCopied(true);
26+
setTimeout(() => setCopied(false), 2000);
27+
} else {
28+
// Fallback for older browsers or non-HTTPS
29+
const textArea = document.createElement('textarea');
30+
textArea.value = publicKey;
31+
textArea.style.position = 'fixed';
32+
textArea.style.left = '-999999px';
33+
textArea.style.top = '-999999px';
34+
document.body.appendChild(textArea);
35+
textArea.focus();
36+
textArea.select();
37+
38+
try {
39+
document.execCommand('copy');
40+
setCopied(true);
41+
setTimeout(() => setCopied(false), 2000);
42+
} catch (fallbackError) {
43+
console.error('Fallback copy failed:', fallbackError);
44+
// If all else fails, show the key in an alert
45+
alert('Please manually copy this key:\n\n' + publicKey);
46+
}
47+
48+
document.body.removeChild(textArea);
49+
}
50+
} catch (error) {
51+
console.error('Failed to copy to clipboard:', error);
52+
// Fallback: show the key in an alert
53+
alert('Please manually copy this key:\n\n' + publicKey);
54+
}
55+
};
56+
57+
return (
58+
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
59+
<div className="bg-card rounded-lg shadow-xl max-w-2xl w-full border border-border">
60+
{/* Header */}
61+
<div className="flex items-center justify-between p-6 border-b border-border">
62+
<div className="flex items-center gap-3">
63+
<div className="p-2 bg-blue-100 rounded-lg">
64+
<Server className="h-6 w-6 text-blue-600" />
65+
</div>
66+
<div>
67+
<h2 className="text-xl font-semibold text-card-foreground">SSH Public Key</h2>
68+
<p className="text-sm text-muted-foreground">Add this key to your server&apos;s authorized_keys</p>
69+
</div>
70+
</div>
71+
<Button
72+
variant="ghost"
73+
size="icon"
74+
onClick={onClose}
75+
className="h-8 w-8"
76+
>
77+
<X className="h-4 w-4" />
78+
</Button>
79+
</div>
80+
81+
{/* Content */}
82+
<div className="p-6 space-y-6">
83+
{/* Server Info */}
84+
<div className="flex items-center gap-4 p-4 bg-muted/50 rounded-lg">
85+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
86+
<Server className="h-4 w-4" />
87+
<span className="font-medium">{serverName}</span>
88+
</div>
89+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
90+
<Globe className="h-4 w-4" />
91+
<span>{serverIp}</span>
92+
</div>
93+
</div>
94+
95+
{/* Instructions */}
96+
<div className="space-y-2">
97+
<h3 className="font-medium text-foreground">Instructions:</h3>
98+
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
99+
<li>Copy the public key below</li>
100+
<li>SSH into your server: <code className="bg-muted px-1 rounded">ssh root@{serverIp}</code></li>
101+
<li>Add the key to authorized_keys: <code className="bg-muted px-1 rounded">echo &quot;&lt;paste-key&gt;&quot; &gt;&gt; ~/.ssh/authorized_keys</code></li>
102+
<li>Set proper permissions: <code className="bg-muted px-1 rounded">chmod 600 ~/.ssh/authorized_keys</code></li>
103+
</ol>
104+
</div>
105+
106+
{/* Public Key */}
107+
<div className="space-y-2">
108+
<div className="flex items-center justify-between">
109+
<label className="text-sm font-medium text-foreground">Public Key:</label>
110+
<Button
111+
variant="outline"
112+
size="sm"
113+
onClick={handleCopy}
114+
className="gap-2"
115+
>
116+
{copied ? (
117+
<>
118+
<Check className="h-4 w-4" />
119+
Copied!
120+
</>
121+
) : (
122+
<>
123+
<Copy className="h-4 w-4" />
124+
Copy
125+
</>
126+
)}
127+
</Button>
128+
</div>
129+
<textarea
130+
value={publicKey}
131+
readOnly
132+
className="w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground font-mono text-xs min-h-[120px] resize-none border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
133+
placeholder="Public key will appear here..."
134+
/>
135+
</div>
136+
137+
{/* Footer */}
138+
<div className="flex justify-end gap-3 pt-4 border-t border-border">
139+
<Button variant="outline" onClick={onClose}>
140+
Close
141+
</Button>
142+
</div>
143+
</div>
144+
</div>
145+
</div>
146+
);
147+
}

0 commit comments

Comments
 (0)