Serve print-cert on the info API, with -address and preferring our v2 certificate

A program that verifies certificates against its own CAs, as
nebula-auth-request does, needs the certificate itself rather than the
parsed identity, and it asked the ssh debug server for it. The info API
now serves print-cert, so it can ask there instead.

- print-cert declares Identity: it reads a certificate from the hostmap or our own, and nothing operational
- -address names the vpn addr, with a port accepted and stripped and 4in6 unmapped, as identity takes it; the operand stays, and giving both is refused
- an address of our own answers with our own certificate, and a tunnel without one is not found, a 404 on the API
- with no address the certificate is the one whoami reports, the v2 when both versions are loaded, not the one handshakes start with
- GET /v1/print-cert?address=<addr>&raw=true answers {"pem": ...}; example config, CHANGELOG, surface test, goldens and an e2e check

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Matt RichardsonandClaude Fable 5.1 committed 2026-10-21 12:27:12 -04:00
1 parent e0dc742e17
commit 660a271466
7 files changed
+156 -17

No files matched your search

+30 -5
View File
@@ -64,11 +64,12 @@ func TestIdentityOverTheInfoAPI(t *testing.T) {
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
type identity struct {
Name string `json:"name"`
VpnAddrs []netip.Addr `json:"vpnAddrs"`
Networks []netip.Prefix `json:"networks"`
Groups []string `json:"groups"`
Issuer string `json:"issuer"`
Name string `json:"name"`
VpnAddrs []netip.Addr `json:"vpnAddrs"`
Networks []netip.Prefix `json:"networks"`
Groups []string `json:"groups"`
Issuer string `json:"issuer"`
Fingerprint string `json:"fingerprint"`
}
// A connection's RemoteAddr, port and all, resolves to the peer.
@@ -102,6 +103,30 @@ func TestIdentityOverTheInfoAPI(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, status)
assert.JSONEq(t, `{"error":"The provided vpn addr could not be parsed: "}`, body)
// The certificate itself, for a program that verifies it against its own CAs: the one
// nebula verified when the tunnel came up, as PEM, by the address a connection carries.
status, body = test.HTTPGet(t, path, "/v1/print-cert?address="+netip.AddrPortFrom(theirVpnIpNet[0].Addr(), 51234).String()+"&raw=true")
require.Equal(t, http.StatusOK, status, body)
var raw struct {
PEM string `json:"pem"`
}
require.NoError(t, json.Unmarshal([]byte(body), &raw))
theirs, _, err := cert.UnmarshalCertificateFromPEM([]byte(raw.PEM))
require.NoError(t, err)
assert.Equal(t, "them", theirs.Name())
fp, err := theirs.Fingerprint()
require.NoError(t, err)
assert.Equal(t, them.Fingerprint, fp, "the same certificate identity reports")
// Without raw it is the certificate's fields; without an address, our own.
status, body = test.HTTPGet(t, path, "/v1/print-cert")
require.Equal(t, http.StatusOK, status, body)
assert.Contains(t, body, `"name":"me"`)
status, body = test.HTTPGet(t, path, "/v1/print-cert?address=10.128.0.9")
assert.Equal(t, http.StatusNotFound, status)
assert.JSONEq(t, `{"error":"Could not find tunnel for vpn addr: 10.128.0.9"}`, body)
// Nothing else nebula serves is reachable here.
status, body = test.HTTPGet(t, path, "/v1/list-hostmap")
assert.Equal(t, http.StatusForbidden, status)