a73x

6fede806

deps: x/crypto v0.56.0, and the gate names the option it accepts

a73x   2026-09-05 17:43

Commit message
deps: x/crypto v0.56.0, and the gate names the option it accepts

Two SSH advisories (GO-2026-6354, GO-2026-6355) land on symbols the plane
actually calls — sshgate.Gate.handleConn and vmssh.Dialer.Dial — so the
bump is not optional.

v0.56.0 also moves where `source-address` is exempted. It used to be
skipped inside CheckCert itself; now CertChecker.Authenticate clones the
checker and appends the option before calling CheckCert, so the exemption
belongs to Authenticate rather than to every caller.

The gate calls CheckCert directly — deliberately, so a cert's principals
are not bound to the outer SSH username on the jump hop — and named no
supported options, relying on the old unconditional skip. Under v0.56.0
that made every source-address cert fail as an unsupported critical
option: a restriction a tenant CA is entitled to set became a way to lock
its own certs out of the gate.

So the gate now names `source-address` as the one option it supports.
That only declares the option understood; enforcement stays where it
always was, in serverAuthenticate, against the address the connection
actually came from. Every other critical option is still refused, which
is the promise the gate has to keep: an option it cannot honour must not
be silently accepted.

go.mod
Old New
@@ -13,7 +13,7 @@ require (
13 github.com/quic-go/quic-go v0.49.1 13 github.com/quic-go/quic-go v0.49.1
14 github.com/stretchr/testify v1.11.1 14 github.com/stretchr/testify v1.11.1
15 github.com/yuin/goldmark v1.8.4 15 github.com/yuin/goldmark v1.8.4
16 golang.org/x/crypto v0.55.0 16 golang.org/x/crypto v0.56.0
17 golang.org/x/net v0.57.0 17 golang.org/x/net v0.57.0
18 golang.org/x/oauth2 v0.36.0 18 golang.org/x/oauth2 v0.36.0
19 golang.org/x/sync v0.22.0 19 golang.org/x/sync v0.22.0
go.sum
Old New
@@ -95,8 +95,8 @@ github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
95 github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 95 github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
96 go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 96 go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
97 go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 97 go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
98 golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= 98 golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
99 golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= 99 golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
100 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 100 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
101 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 101 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
102 golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= 102 golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
internal/server/sshgate/gate.go
Old New
@@ -125,6 +125,21 @@ const (
125 func New(hostKey ssh.Signer, userCAs UserCALookup, resolve Resolver, authorize Authorizer, dial Dialer, isRevoked Revoker, audit Audit) *Gate { 125 func New(hostKey ssh.Signer, userCAs UserCALookup, resolve Resolver, authorize Authorizer, dial Dialer, isRevoked Revoker, audit Audit) *Gate {
126 checker := &ssh.CertChecker{ 126 checker := &ssh.CertChecker{
127 IsUserAuthority: func(auth ssh.PublicKey) bool { _, ok := userCAs(auth); return ok }, 127 IsUserAuthority: func(auth ssh.PublicKey) bool { _, ok := userCAs(auth); return ok },
128 // `source-address` is the ONE critical option this gate accepts, and it
129 // has to be named here because we call CheckCert directly rather than
130 // going through CertChecker.Authenticate (see the principal argument
131 // below for why). Authenticate is what exempts source-address on a
132 // caller's behalf — it clones the checker and appends this same
133 // constant — so a direct CheckCert caller that names nothing rejects
134 // every source-address cert as an unsupported option.
135 //
136 // Naming it does NOT enforce it. CheckCert only decides whether an
137 // option is understood; the enforcement happens in x/crypto's
138 // serverAuthenticate, against the address the connection actually came
139 // from, read from the Permissions returned below. Listing exactly this
140 // one keeps the gate's promise intact: an option it cannot honour is
141 // still a promise it must not accept (TestGateRejectsUnknownCriticalOption).
142 SupportedCriticalOptions: []string{"source-address"},
128 } 143 }
129 // CheckCert consults IsRevoked during validation: a true result fails the 144 // CheckCert consults IsRevoked during validation: a true result fails the
130 // cert authentication outright, so a revoked cert cannot open the tunnel. 145 // cert authentication outright, so a revoked cert cannot open the tunnel.
@@ -181,13 +196,14 @@ func New(hostKey ssh.Signer, userCAs UserCALookup, resolve Resolver, authorize A
181 } 196 }
182 return &ssh.Permissions{ 197 return &ssh.Permissions{
183 // The cert's own restrictions, carried back so the server layer 198 // The cert's own restrictions, carried back so the server layer
184 // applies them. CheckCert above skips `source-address` on purpose; 199 // applies them. CheckCert above ACCEPTS `source-address` without
185 // x/crypto's serverAuthenticate is what checks it, against the 200 // checking it (see SupportedCriticalOptions where the checker is
186 // address the connection actually came from, and it reads it from 201 // built); x/crypto's serverAuthenticate is what checks it,
187 // the permissions returned here. The plane runs on the host 202 // against the address the connection actually came from, and it
188 // network, so that address is the client's, not a proxy's. Every 203 // reads it from the permissions returned here. The plane runs on
189 // other critical option is still refused by CheckCert, which has 204 // the host network, so that address is the client's, not a
190 // no SupportedCriticalOptions set. 205 // proxy's. Every other critical option is still refused by
206 // CheckCert, which supports only that one.
191 CriticalOptions: cert.CriticalOptions, 207 CriticalOptions: cert.CriticalOptions,
192 Extensions: map[string]string{ 208 Extensions: map[string]string{
193 // The tenant of the CA that signed this cert, resolved from the 209 // The tenant of the CA that signed this cert, resolved from the