Skip to content

knownhosts: scope HostKeyAlgorithms to the target host - #1295

Open
adonaicosta wants to merge 1 commit into
fluxcd:mainfrom
adonaicosta:fix/knownhosts-per-host-algos
Open

adonaicosta wants to merge 1 commit into
fluxcd:mainfrom
adonaicosta:fix/knownhosts-per-host-algos

Conversation

@adonaicosta

Copy link
Copy Markdown

hostKeyAlgorithms collected the key types of every entry in the known_hosts
blob, without matching them against the host being dialled, and built the
slice by ranging over a map. A blob covering several hosts with different
key-type sets therefore advertised algorithms unknown for the host being
connected to, in a random order on every call. The server may pick one of
them, which checkAddr then rejects as a key mismatch, so verification
succeeded or failed depending on the permutation.

Match the entries against the target address, skip certificate authority
entries, which hold the signing key rather than a host key algorithm the
server can offer, and sort the result so the negotiation is deterministic.
Return nil when no entry matches, leaving the negotiation to the defaults of
golang.org/x/crypto/ssh.

New() now takes the connection target, and gogit passes AuthOptions.Host.

Regression from #943, which is correct for a single host per blob.

Signed-off-by: Adonai Costa adonai.costa@gmail.com

hostKeyAlgorithms collected the key types of every entry in the known_hosts
blob, without matching them against the host being dialled, and built the
slice by ranging over a map. A blob covering several hosts with different
key-type sets therefore advertised algorithms unknown for the host being
connected to, in a random order on every call. The server may pick one of
them, which checkAddr then rejects as a key mismatch, so verification
succeeded or failed depending on the permutation.

Match the entries against the target address, skip certificate authority
entries, which hold the signing key rather than a host key algorithm the
server can offer, and sort the result so the negotiation is deterministic.
Return nil when no entry matches, leaving the negotiation to the defaults of
golang.org/x/crypto/ssh.

New() now takes the connection target, and gogit passes AuthOptions.Host.

Regression from fluxcd#943, which is correct for a single host per blob.

Signed-off-by: Adonai Costa <adonai.costa@gmail.com>
@adonaicosta
adonaicosta requested a review from a team as a code owner September 8, 2026 10:03
@adonaicosta

Copy link
Copy Markdown
Author

Fixes the non-deterministic knownhosts: key mismatch described in (#1294).

ssh/knownhosts.hostKeyAlgorithms() collected the key types of every entry in the
known_hosts blob, without matching them against the host being dialled, and built the
slice by ranging over a map:

	uniq := make(map[string]struct{})
	for _, hk := range db.hostKeys {   // every host in the blob
		uniq[hk.key.Type()] = struct{}{}
	}
	for k := range uniq {              // map range: random order
		algos = append(algos, k)
	}

A blob covering several hosts with different key-type sets therefore advertises algorithms
for which the database holds no entry for the host actually being connected to. The server
is free to pick one (RFC 4253), and checkAddr then rejects it as designed. Because the
order is randomised per call, the same GitRepository flips between Ready=True and
ssh: handshake failed: knownhosts: key mismatch with nothing changing around it.

Changes

  • hostKeyAlgorithms now takes an addr and returns only the algorithms held for it,
    sorted so the negotiation order is deterministic.
  • Certificate authority entries are skipped: they hold the key that signs host
    certificates, not a host key algorithm the server can offer.
  • Returns nil when no entry matches the host, leaving the negotiation to the defaults of
    golang.org/x/crypto/ssh rather than advertising a list that cannot match.
  • New takes the connection target (host, with or without a port); git/gogit passes
    AuthOptions.Host, which Validate() already requires for the SSH transport.

New is a signature change, but git/gogit/transport.go is its only caller in the tree,
and #943 already changed this signature one release ago.

Behaviour

Same known_hosts blob (GitLab with three key types, GitHub with one), same real
github.com, 9 consecutive dials:

before after
host key accepted 1/9 9/9
advertised algos random permutation of all 3 types [ecdsa-sha2-nistp256] every time

fluxcd/flux2#5385, the issue that #943 fixed, stays fixed: a blob holding only
ssh-rsa for its host still advertises ssh-rsa.

Tests

TestHostKeyAlgorithmsScopedToHost covers host scoping, the host vs host:port forms, a
non-matching port, an unknown host, and repeats each case 20 times to catch the random
order. TestHostKeyAlgorithmsIgnoresCertAuthority covers the @cert-authority case. Both
fail against the previous implementation:

--- FAIL: TestHostKeyAlgorithmsScopedToHost (0.00s)
    knownhosts_test.go:355: host github.com: got [ecdsa-sha2-nistp256 ssh-ed25519], want [ecdsa-sha2-nistp256]
--- FAIL: TestHostKeyAlgorithmsIgnoresCertAuthority (0.00s)
    knownhosts_test.go:370: got [ssh-ed25519 ecdsa-sha2-nistp256], want [ecdsa-sha2-nistp256]

go test ./... passes in ssh, and go build ./... && go vet ./gogit/ && go test ./gogit/
passes in git.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant