-
Notifications
You must be signed in to change notification settings - Fork 5
Description
HostKeyAlgorithms from known_hosts @cert-authority entries should include non-cert fallback algorithms
Summary
used Claude code to make the minimal recreation and root cause explanation
When a known_hosts file contains a @cert-authority entry for a host, the skeema/knownhosts library (commonly used with x/crypto/ssh) returns only certificate-based algorithms (e.g., ssh-ed25519-cert-v01@openssh.com). This causes connection failures to servers that support the underlying key type (ssh-ed25519) but not certificate-based host key authentication.
Expected behavior: When a host has a @cert-authority entry, the client should offer both the certificate algorithm and the plain algorithm as a fallback, allowing connections to servers that don't support certificate-based host keys.
Reproduction
Clone podman
git clone https://github.com/containers/podman.git
cd podman
git switch -c skeema/knownhosts/issues/16 559dce7bf836cf78458cff3aa1410517b4003825Now, create a poc.go in the podman director (to use its dependencies) and run it with go run pod.go
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"net"
"os"
"path/filepath"
"time"
"github.com/skeema/knownhosts"
"golang.org/x/crypto/ssh"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "ssh-poc-*")
defer os.RemoveAll(tmpDir)
// Generate server host key (ed25519)
_, hostPrivKey, _ := ed25519.GenerateKey(rand.Reader)
hostSigner, _ := ssh.NewSignerFromKey(hostPrivKey)
// Start SSH server that only supports plain ssh-ed25519 (not certs)
serverConfig := &ssh.ServerConfig{NoClientAuth: true}
serverConfig.AddHostKey(hostSigner)
listener, _ := net.Listen("tcp", "127.0.0.1:0")
defer listener.Close()
addr := listener.Addr().String()
_, port, _ := net.SplitHostPort(addr)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go func(c net.Conn) {
defer c.Close()
ssh.NewServerConn(c, serverConfig)
}(conn)
}
}()
time.Sleep(50 * time.Millisecond)
// Create @cert-authority known_hosts entry
certLine := fmt.Sprintf("@cert-authority [127.0.0.1]:%s %s %s\n",
port,
hostSigner.PublicKey().Type(),
base64.StdEncoding.EncodeToString(hostSigner.PublicKey().Marshal()))
knownHostsFile := filepath.Join(tmpDir, "known_hosts")
os.WriteFile(knownHostsFile, []byte(certLine), 0600)
// Load known_hosts - this is where the problem occurs
kh, _ := knownhosts.NewDB(knownHostsFile)
algos := kh.HostKeyAlgorithms(addr)
fmt.Printf("Server supports: [%s]\n", hostSigner.PublicKey().Type())
fmt.Printf("Client will offer: %v\n", algos)
fmt.Printf("known_hosts content: %s", certLine)
// Try to connect
clientConfig := &ssh.ClientConfig{
User: "test",
Auth: []ssh.AuthMethod{ssh.Password("test")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
HostKeyAlgorithms: algos,
}
conn, err := ssh.Dial("tcp", addr, clientConfig)
if err != nil {
fmt.Printf("\nConnection FAILED: %v\n", err)
return
}
conn.Close()
fmt.Println("\nConnection succeeded")
}Output
Server supports: [ssh-ed25519]
Client will offer: [ssh-ed25519-cert-v01@openssh.com]
known_hosts content: @cert-authority [127.0.0.1]:56361 ssh-ed25519 AAAAC3...
Connection FAILED: ssh: handshake failed: ssh: no common algorithm for host key;
we offered: [ssh-ed25519-cert-v01@openssh.com],
peer offered: [ssh-ed25519]
Real-World Impact
This issue affects Podman on macOS when users have SSH certificate authority configuration in their ~/.ssh/known_hosts. The error manifests as:
$ podman info
Error: unable to connect to Podman socket: failed to connect: ssh: handshake failed:
ssh: no common algorithm for host key; we offered: [ssh-ed25519-cert-v01@openssh.com],
peer offered: [rsa-sha2-512 rsa-sha2-256 ecdsa-sha2-nistp256 ssh-ed25519]
The Podman VM's SSH server supports plain ssh-ed25519, but because the user has a @cert-authority entry (possibly a wildcard * entry for their organization's CA), Podman's Go SSH client only offers the certificate algorithm.
Root Cause
In skeema/knownhosts, the HostKeyAlgorithms() function converts key types to certificate algorithms when the entry has @cert-authority:
// knownhosts.go:202-205
addAlgo := func(typ string, cert bool) {
if cert {
typ = keyTypeToCertAlgo(typ) // Converts ssh-ed25519 → ssh-ed25519-cert-v01@openssh.com
}
// ...
}This conversion is correct for the certificate case, but it excludes the plain algorithm entirely.
Proposed Solution
When a @cert-authority entry exists, HostKeyAlgorithms() should return both the certificate algorithm (preferred) and the plain algorithm (fallback):
addAlgo := func(typ string, cert bool) {
if cert {
// Add certificate algorithm first (preferred)
certTyp := keyTypeToCertAlgo(typ)
if _, already := seen[certTyp]; !already {
algos = append(algos, certTyp)
seen[certTyp] = struct{}{}
}
// Also add plain algorithm as fallback
}
if _, already := seen[typ]; !already {
algos = append(algos, typ)
seen[typ] = struct{}{}
}
}This would produce:
Client will offer: [ssh-ed25519-cert-v01@openssh.com, ssh-ed25519]
The SSH handshake would then:
- Try the certificate algorithm first (for servers that support it)
- Fall back to the plain algorithm (for servers that don't)
Why This Should Be the Default Behavior
- Backward compatibility: Users with
@cert-authorityentries expect to connect to both cert-enabled and plain servers - OpenSSH behavior: OpenSSH clients can connect to servers regardless of whether they support the CA
- Least surprise: A
known_hostsconfiguration shouldn't prevent connections that would otherwise work - Defense in depth: The CA entry provides trust for cert-based auth, but shouldn't exclude plain host key verification
Environment
- Go version: go1.22+
- OS: darwin/arm64 (also affects Linux)
golang.org/x/crypto: latestgithub.com/skeema/knownhosts: v1.3.0
Note: While the immediate fix would be in skeema/knownhosts, this report is filed here because:
- The
x/crypto/sshpackage defines the algorithm constants and negotiation - A more robust solution might involve
x/crypto/ssh/knownhostsproviding better support for mixed cert/non-cert scenarios - The Go SSH ecosystem should have consistent behavior for this common use case