diff --git a/backend/src/routes/terminal.ts b/backend/src/routes/terminal.ts index 6a4c2b2..458f973 100644 --- a/backend/src/routes/terminal.ts +++ b/backend/src/routes/terminal.ts @@ -17,6 +17,18 @@ interface ClientMessage { const TMUX_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/ +/** + * True only if the stored secret is a genuine OpenSSH certificate, i.e. its key type ends in + * `-cert-v01@openssh.com` (e.g. `ssh-ed25519-cert-v01@openssh.com AAAA...`). A plain public key + * (`ssh-ed25519 AAAA...`) is NOT a certificate — storing one in the `certificate` field must not + * trigger the cert-auth path, which shells out to the system `ssh` binary and fails to load some + * key formats. In that case we fall through to the normal ssh2 key/password path instead. */ +function isOpenSshCertificate(value: string | undefined): boolean { + if (!value) return false + const firstToken = value.trim().split(/\s+/)[0] ?? '' + return firstToken.endsWith('-cert-v01@openssh.com') +} + const SESSION_LOG_DIR = process.env.ARCHNEST_SESSION_LOG_DIR ?? './data/session-logs' function send(socket: { send: (data: string) => void }, payload: Record) { @@ -50,6 +62,7 @@ function connectWithCertificate( '-p', String(Number(target.config.port) || 22), '-i', keyFile, '-o', `CertificateFile=${certFile}`, + '-o', 'IdentitiesOnly=yes', '-o', 'StrictHostKeyChecking=accept-new', '-o', `UserKnownHostsFile=${join(keyDir, 'known_hosts')}`, `${target.config.username}@${target.config.host}`, @@ -156,7 +169,7 @@ export async function terminalRoutes(app: FastifyInstance) { const cols = msg.cols ?? 80 const rows = msg.rows ?? 24 - if (target.secrets.certificate) { + if (isOpenSshCertificate(target.secrets.certificate)) { connectWithCertificate( target, cols,