From 660a271466032a84a45dcdb0b11e5f24131bd08b Mon Sep 17 00:00:00 2001 From: Matt Richardson Date: Tue, 20 Oct 2026 14:19:55 -0400 Subject: [PATCH] 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=&raw=true answers {"pem": ...}; example config, CHANGELOG, surface test, goldens and an e2e check Co-Authored-By: Claude Fable 5.1 --- commands.go | 49 ++++++++++++++++++---- commands_test.go | 63 +++++++++++++++++++++++++++++ e2e/info_api_test.go | 35 +++++++++++++--- e2e/testdata/commands_output.golden | 17 +++++++- examples/config.yml | 3 ++ ssh_commands_test.go | 2 +- testdata/ssh_commands.golden | 4 +- 7 files changed, 156 insertions(+), 17 deletions(-) diff --git a/commands.go b/commands.go index fc5fde5a..5360bb25 100644 --- a/commands.go +++ b/commands.go @@ -28,6 +28,17 @@ type listHostMapFlags struct { type printCertFlags struct { Raw bool + + // Address is the -address form of the vpn addr, which the info API needs since a + // query string has no operands; see identityFlags for why presence is recorded. + Address string + addressSet bool +} + +func (f *printCertFlags) setAddress(v string) error { + f.Address = v + f.addressSet = true + return nil } type changeRemoteFlags struct { @@ -176,9 +187,12 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte reg.Register(commands.Flagged(&commands.Command{ Name: commands.PrintCert, ShortDescription: "Prints the current certificate being used or the certificate for the provided vpn addr", + Help: "The vpn addr is an operand or -address, which may carry a port so a connection's remote address can be passed through as is. For a peer it is the certificate nebula verified when the tunnel came up; with no address it is this node's own, the v2 certificate when both versions are loaded. -raw prints the PEM.", + Identity: true, Text: textPrintCert, }, func(fl *flag.FlagSet, s *printCertFlags) { fl.BoolVar(&s.Raw, "raw", false, "prints the PEM encoded certificate; with -json, an object holding it") + fl.Func("address", "The vpn addr to look up, optionally with a port", s.setAddress) }, func(s *printCertFlags, a []string) (commands.Result, error) { return runPrintCert(f, s, a) })) @@ -513,20 +527,39 @@ func textLogFormat(fi logFormatInfo, w io.Writer) error { return err } +// runPrintCert prints a certificate: this node's own, or the one the host owning a vpn +// addr presented. The addr is an operand, as it always was over ssh, or -address, which +// is how the info API names it, and an address of our own answers with our own +// certificate as identity does. A tunnel still handshaking, or one being torn down, has +// no certificate and is reported as not found. func runPrintCert(ifce *Interface, args *printCertFlags, a []string) (commands.Result, error) { + if len(a) > 0 && args.addressSet { + return commands.Failed("print-cert takes one vpn addr, as -address or as an operand, not both"), nil + } - c := ifce.pki.getCertState().GetDefaultCertificate() - if len(a) > 0 { - hostInfo, res, ok := tunnelOperand(ifce, a, "vpn addr") + cs := ifce.pki.getCertState() + var c cert.Certificate + switch { + case len(a) > 0: + vpnAddr, res, ok := vpnAddrOperand(a, "vpn addr") if !ok { return res, nil } - // A tunnel still handshaking, or one being torn down, has no certificate. - cc := hostInfo.GetCert() - if cc == nil { - return commands.Failed("Could not find tunnel for vpn addr: %v", a[0]), nil + if c, _ = findCertificateForVpnAddr(cs, ifce.hostMap, vpnAddr); c == nil { + return commands.NotFound("Could not find tunnel for vpn addr: %v", a[0]), nil + } + case args.addressSet: + vpnAddr, err := parseVpnAddrWithOptionalPort(args.Address) + if err != nil { + return commands.Failed("The provided vpn addr could not be parsed: %s", args.Address), nil + } + if c, _ = findCertificateForVpnAddr(cs, ifce.hostMap, vpnAddr); c == nil { + return commands.NotFound("Could not find tunnel for vpn addr: %v", args.Address), nil + } + default: + if c = ownCertificate(cs); c == nil { + return commands.Failed("No certificate is loaded"), nil } - c = cc.Certificate } if args.Raw { diff --git a/commands_test.go b/commands_test.go index e0e544c1..54c0948f 100644 --- a/commands_test.go +++ b/commands_test.go @@ -6,7 +6,10 @@ import ( "io" "net/netip" "testing" + "time" + "github.com/slackhq/nebula/cert" + "github.com/slackhq/nebula/cert_test" "github.com/slackhq/nebula/commands" "github.com/slackhq/nebula/logging" "github.com/slackhq/nebula/test" @@ -80,3 +83,63 @@ func TestPrintCertOfATunnelWithoutACertificate(t *testing.T) { require.NoError(t, err) assert.True(t, r.OK, "a tunnel with a certificate is printed") } + +// TestPrintCertByAddressFlag: -address is the info API's spelling of the operand, takes a +// port and a 4in6 form the way identity does, answers our own address with our own +// certificate, and is refused together with an operand. +func TestPrintCertByAddressFlag(t *testing.T) { + f, peerCert := identityFixture(t) + + for _, addr := range []string{"10.0.0.2", "10.0.0.2:4242", "[::ffff:10.0.0.2]:4242"} { + fl := &printCertFlags{} + require.NoError(t, fl.setAddress(addr)) + r, err := runPrintCert(f, fl, nil) + require.NoError(t, err) + require.True(t, r.OK, addr) + assert.Equal(t, peerCert, r.Value, addr) + } + + fl := &printCertFlags{} + require.NoError(t, fl.setAddress("10.0.0.1")) + r, err := runPrintCert(f, fl, nil) + require.NoError(t, err) + require.True(t, r.OK) + assert.Equal(t, ownCertificate(f.pki.getCertState()), r.Value, "our own address is our own certificate") + + fl = &printCertFlags{} + require.NoError(t, fl.setAddress("10.0.0.3")) + r, err = runPrintCert(f, fl, nil) + require.NoError(t, err) + assert.False(t, r.OK) + assert.True(t, r.NotFound) + + fl = &printCertFlags{} + require.NoError(t, fl.setAddress("not-an-address")) + r, err = runPrintCert(f, fl, nil) + require.NoError(t, err) + assert.Equal(t, "The provided vpn addr could not be parsed: not-an-address", r.Reason) + + fl = &printCertFlags{} + require.NoError(t, fl.setAddress("10.0.0.2")) + r, err = runPrintCert(f, fl, []string{"10.0.0.2"}) + require.NoError(t, err) + assert.Equal(t, "print-cert takes one vpn addr, as -address or as an operand, not both", r.Reason) +} + +// TestPrintCertPrefersOwnV2: with no address print-cert reports the certificate whoami +// does, the v2 when both versions are loaded, rather than the one handshakes start with. +func TestPrintCertPrefersOwnV2(t *testing.T) { + v4 := netip.MustParsePrefix("10.0.0.1/24") + ca1, _, caKey1, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil) + v1, _, _, _ := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca1, caKey1, "me", time.Time{}, time.Time{}, []netip.Prefix{v4}, nil, nil) + ca2, _, caKey2, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil) + v2, _, _, _ := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca2, caKey2, "me", time.Time{}, time.Time{}, []netip.Prefix{v4}, nil, nil) + + f := &Interface{pki: &PKI{}, hostMap: newHostMap(test.NewLogger())} + f.pki.cs.Store(&CertState{v1Cert: v1, v2Cert: v2, initiatingVersion: cert.Version1}) + + r, err := runPrintCert(f, &printCertFlags{}, nil) + require.NoError(t, err) + require.True(t, r.OK) + assert.Equal(t, v2, r.Value) +} diff --git a/e2e/info_api_test.go b/e2e/info_api_test.go index f1136bb0..40b62889 100644 --- a/e2e/info_api_test.go +++ b/e2e/info_api_test.go @@ -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) diff --git a/e2e/testdata/commands_output.golden b/e2e/testdata/commands_output.golden index 3899f7d0..8fedccff 100644 --- a/e2e/testdata/commands_output.golden +++ b/e2e/testdata/commands_output.golden @@ -779,7 +779,7 @@ whoami - Prints the certificate identity of this node $ help -json -[{"name":"change-remote","short":"Changes the remote address used in the tunnel for the provided vpn addr","flags":[{"name":"address","usage":"The new remote address, ip:port"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"close-tunnel","short":"Closes a tunnel for the provided vpn addr","flags":[{"name":"local-only","bool":true,"default":"false","usage":"Disables notifying the remote that the tunnel is shutting down"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"create-tunnel","short":"Creates a tunnel for the provided vpn address","help":"The lighthouses will be queried for real addresses but you can provide one as well.","flags":[{"name":"address","usage":"Optionally provide a real remote address, ip:port "},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"device-info","short":"Prints information about the network device.","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"help","short":"prints available commands or help \u003ccommand\u003e for specific usage info","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"identity","short":"Prints the certificate identity of the host owning the provided vpn addr","help":"Identity is what the certificate says: name, vpn addrs, networks, groups, fingerprint, issuer and validity. The address may carry a port, so a connection's remote address can be passed through as is. For this node's own identity use whoami.","flags":[{"name":"address","usage":"The vpn addr to look up, optionally with a port"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"list-hostmap","short":"List all known previously connected hosts","flags":[{"name":"by-index","bool":true,"default":"false","usage":"gets all hosts in the hostmap from the index table"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"list-lighthouse-addrmap","short":"List all lighthouse map entries","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"list-pending-hostmap","short":"List all handshaking hosts","flags":[{"name":"by-index","bool":true,"default":"false","usage":"gets all hosts in the hostmap from the index table"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"log-format","short":"Gets or sets the current log format","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"log-level","short":"Gets or sets the current log level","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"logout","short":"Ends the current session","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"print-cert","short":"Prints the current certificate being used or the certificate for the provided vpn addr","flags":[{"name":"raw","bool":true,"default":"false","usage":"prints the PEM encoded certificate; with -json, an object holding it"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"print-relays","short":"Prints json details about all relay info","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"print-tunnel","short":"Prints json details about a tunnel for the provided vpn addr","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"query-lighthouse","short":"Query the lighthouses for the provided vpn address","help":"This command is asynchronous. Only currently known udp addresses will be printed.","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"reload","short":"Reloads configuration from disk, same as sending HUP to the process","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"version","short":"Prints the currently running version of nebula","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"whoami","short":"Prints the certificate identity of this node","help":"The fields identity prints, read from this node's own certificate.","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]}] +[{"name":"change-remote","short":"Changes the remote address used in the tunnel for the provided vpn addr","flags":[{"name":"address","usage":"The new remote address, ip:port"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"close-tunnel","short":"Closes a tunnel for the provided vpn addr","flags":[{"name":"local-only","bool":true,"default":"false","usage":"Disables notifying the remote that the tunnel is shutting down"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"create-tunnel","short":"Creates a tunnel for the provided vpn address","help":"The lighthouses will be queried for real addresses but you can provide one as well.","flags":[{"name":"address","usage":"Optionally provide a real remote address, ip:port "},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"device-info","short":"Prints information about the network device.","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"help","short":"prints available commands or help \u003ccommand\u003e for specific usage info","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"identity","short":"Prints the certificate identity of the host owning the provided vpn addr","help":"Identity is what the certificate says: name, vpn addrs, networks, groups, fingerprint, issuer and validity. The address may carry a port, so a connection's remote address can be passed through as is. For this node's own identity use whoami.","flags":[{"name":"address","usage":"The vpn addr to look up, optionally with a port"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"list-hostmap","short":"List all known previously connected hosts","flags":[{"name":"by-index","bool":true,"default":"false","usage":"gets all hosts in the hostmap from the index table"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"list-lighthouse-addrmap","short":"List all lighthouse map entries","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"list-pending-hostmap","short":"List all handshaking hosts","flags":[{"name":"by-index","bool":true,"default":"false","usage":"gets all hosts in the hostmap from the index table"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"log-format","short":"Gets or sets the current log format","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"log-level","short":"Gets or sets the current log level","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"logout","short":"Ends the current session","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"print-cert","short":"Prints the current certificate being used or the certificate for the provided vpn addr","help":"The vpn addr is an operand or -address, which may carry a port so a connection's remote address can be passed through as is. For a peer it is the certificate nebula verified when the tunnel came up; with no address it is this node's own, the v2 certificate when both versions are loaded. -raw prints the PEM.","flags":[{"name":"address","usage":"The vpn addr to look up, optionally with a port"},{"name":"raw","bool":true,"default":"false","usage":"prints the PEM encoded certificate; with -json, an object holding it"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"print-relays","short":"Prints json details about all relay info","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"print-tunnel","short":"Prints json details about a tunnel for the provided vpn addr","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"query-lighthouse","short":"Query the lighthouses for the provided vpn address","help":"This command is asynchronous. Only currently known udp addresses will be printed.","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"reload","short":"Reloads configuration from disk, same as sending HUP to the process","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"version","short":"Prints the currently running version of nebula","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]},{"name":"whoami","short":"Prints the certificate identity of this node","help":"The fields identity prints, read from this node's own certificate.","flags":[{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]}] $ help -pretty [ @@ -1034,7 +1034,12 @@ $ help -pretty { "name": "print-cert", "short": "Prints the current certificate being used or the certificate for the provided vpn addr", + "help": "The vpn addr is an operand or -address, which may carry a port so a connection's remote address can be passed through as is. For a peer it is the certificate nebula verified when the tunnel came up; with no address it is this node's own, the v2 certificate when both versions are loaded. -raw prints the PEM.", "flags": [ + { + "name": "address", + "usage": "The vpn addr to look up, optionally with a port" + }, { "name": "raw", "bool": true, @@ -1169,6 +1174,9 @@ $ help -pretty $ help print-cert print-cert - Prints the current certificate being used or the certificate for the provided vpn addr + The vpn addr is an operand or -address, which may carry a port so a connection's remote address can be passed through as is. For a peer it is the certificate nebula verified when the tunnel came up; with no address it is this node's own, the v2 certificate when both versions are loaded. -raw prints the PEM. + -address string + The vpn addr to look up, optionally with a port -raw prints the PEM encoded certificate; with -json, an object holding it -json @@ -1177,13 +1185,18 @@ print-cert - Prints the current certificate being used or the certificate for th pretty prints json, assumes -json $ help -json print-cert -{"name":"print-cert","short":"Prints the current certificate being used or the certificate for the provided vpn addr","flags":[{"name":"raw","bool":true,"default":"false","usage":"prints the PEM encoded certificate; with -json, an object holding it"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]} +{"name":"print-cert","short":"Prints the current certificate being used or the certificate for the provided vpn addr","help":"The vpn addr is an operand or -address, which may carry a port so a connection's remote address can be passed through as is. For a peer it is the certificate nebula verified when the tunnel came up; with no address it is this node's own, the v2 certificate when both versions are loaded. -raw prints the PEM.","flags":[{"name":"address","usage":"The vpn addr to look up, optionally with a port"},{"name":"raw","bool":true,"default":"false","usage":"prints the PEM encoded certificate; with -json, an object holding it"},{"name":"json","bool":true,"default":"false","usage":"outputs as json"},{"name":"pretty","bool":true,"default":"false","usage":"pretty prints json, assumes -json"}]} $ help -pretty print-cert { "name": "print-cert", "short": "Prints the current certificate being used or the certificate for the provided vpn addr", + "help": "The vpn addr is an operand or -address, which may carry a port so a connection's remote address can be passed through as is. For a peer it is the certificate nebula verified when the tunnel came up; with no address it is this node's own, the v2 certificate when both versions are loaded. -raw prints the PEM.", "flags": [ + { + "name": "address", + "usage": "The vpn addr to look up, optionally with a port" + }, { "name": "raw", "bool": true, diff --git a/examples/config.yml b/examples/config.yml index f53b4cdf..008b0651 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -262,6 +262,9 @@ punchy: # is ignored. 404 when no tunnel is # up, 400 when no address is given. # GET /v1/whoami this node's identity + # GET /v1/print-cert?address=[:port]&raw=true + # the certificate for the specified + # address, as {"pem": ...}. # GET /v1 describes what is served # # NB: unless a connection arrived on the nebula interface, there's no guarantee diff --git a/ssh_commands_test.go b/ssh_commands_test.go index a37a811d..92ed342f 100644 --- a/ssh_commands_test.go +++ b/ssh_commands_test.go @@ -115,5 +115,5 @@ func TestInfoAPISurface(t *testing.T) { } } - assert.ElementsMatch(t, []commands.Name{commands.Identity, commands.Whoami}, served) + assert.ElementsMatch(t, []commands.Name{commands.PrintCert, commands.Identity, commands.Whoami}, served) } diff --git a/testdata/ssh_commands.golden b/testdata/ssh_commands.golden index 30911591..a16c3b1d 100644 --- a/testdata/ssh_commands.golden +++ b/testdata/ssh_commands.golden @@ -100,8 +100,10 @@ log-level pretty prints json, assumes -json print-cert short: Prints the current certificate being used or the certificate for the provided vpn addr - help: + help: The vpn addr is an operand or -address, which may carry a port so a connection's remote address can be passed through as is. For a peer it is the certificate nebula verified when the tunnel came up; with no address it is this node's own, the v2 certificate when both versions are loaded. -raw prints the PEM. flags: + -address string + The vpn addr to look up, optionally with a port -raw prints the PEM encoded certificate; with -json, an object holding it -json