Fix terminal failing for SSH hosts with a non-cert "certificate" secret (#26)

The Terminal page failed to open a shell for one host (Linode) while
Host Metrics worked fine for the same host, and other hosts (pve1/pve2)
worked everywhere. Root cause: the terminal route takes a special
certificate-auth path whenever an SSH integration has ANY `certificate`
secret set, and that path shells out to the system `ssh` binary under a
pty instead of using the ssh2 library. The metrics path always uses
ssh2, which is why it was unaffected.

That host's `certificate` secret was actually a plain public key
(`ssh-ed25519 AAAA...`), not an OpenSSH certificate. ssh discarded it
("is not a certificate") and then could not load the private key under
the container's libcrypto ("error in libcrypto: unsupported"), ending in
"Permission denied (publickey)". With ssh2 (the metrics path), the same
private key authenticates fine.

Two fixes:
- Only take the cert-auth path when the secret is a genuine OpenSSH
  certificate (key type ends in `-cert-v01@openssh.com`). A plain public
  key now falls through to the normal ssh2 key/password path, which
  already works (proven by the metrics endpoint using the same key).
- Add `-o IdentitiesOnly=yes` to the cert-auth ssh invocation so it only
  offers the provided key/cert and isn't confused by a stray file.

No server-side or key changes were needed on the affected host; this is
purely a routing/robustness fix in the terminal WebSocket handler.

Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
This commit is contained in:
Samuel James 2026-06-20 11:29:36 -04:00 committed by GitHub
parent b2600e2577
commit 993792e193
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -17,6 +17,18 @@ interface ClientMessage {
const TMUX_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/ 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' const SESSION_LOG_DIR = process.env.ARCHNEST_SESSION_LOG_DIR ?? './data/session-logs'
function send(socket: { send: (data: string) => void }, payload: Record<string, unknown>) { function send(socket: { send: (data: string) => void }, payload: Record<string, unknown>) {
@ -50,6 +62,7 @@ function connectWithCertificate(
'-p', String(Number(target.config.port) || 22), '-p', String(Number(target.config.port) || 22),
'-i', keyFile, '-i', keyFile,
'-o', `CertificateFile=${certFile}`, '-o', `CertificateFile=${certFile}`,
'-o', 'IdentitiesOnly=yes',
'-o', 'StrictHostKeyChecking=accept-new', '-o', 'StrictHostKeyChecking=accept-new',
'-o', `UserKnownHostsFile=${join(keyDir, 'known_hosts')}`, '-o', `UserKnownHostsFile=${join(keyDir, 'known_hosts')}`,
`${target.config.username}@${target.config.host}`, `${target.config.username}@${target.config.host}`,
@ -156,7 +169,7 @@ export async function terminalRoutes(app: FastifyInstance) {
const cols = msg.cols ?? 80 const cols = msg.cols ?? 80
const rows = msg.rows ?? 24 const rows = msg.rows ?? 24
if (target.secrets.certificate) { if (isOpenSshCertificate(target.secrets.certificate)) {
connectWithCertificate( connectWithCertificate(
target, target,
cols, cols,