Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
10a1c38
[management] Add internal CA module with signing and storage
zgv163 Mar 2, 2026
8b9fed2
[management] Add proto definitions for certificate signing
zgv163 Mar 2, 2026
fa6f499
[management] Implement certificate signing gRPC handlers
zgv163 Mar 2, 2026
f3c701d
[client] Add proto definitions for certificate daemon RPCs
zgv163 Mar 2, 2026
6e07970
[client] Add certificate manager and platform trust store
zgv163 Mar 2, 2026
ca8e49f
[client] Add daemon handlers and CLI for certificate management
zgv163 Mar 2, 2026
026b7a2
[client] Add CA distribution via sync and certificate auto-renewal
zgv163 Mar 2, 2026
a25ee9c
[management] Add CA REST API and permission module
zgv163 Mar 2, 2026
d247733
[management] Add CA OpenAPI spec, REST client, and use generated types
zgv163 Mar 2, 2026
e211cfd
[management] Wire CA into black-box test server and add integration test
zgv163 Mar 2, 2026
d4f7860
[management] Add wildcard DNS record support for peers with wildcard …
zgv163 Mar 2, 2026
e1453b4
[management] Enable certificate authority setting on CA initialization
zgv163 Mar 3, 2026
13036c6
[management,client] Tie certificate validity to peer login expiration
zgv163 Mar 3, 2026
a6cc322
[client] Fetch CA certificates from management on trust-ca if not synced
zgv163 Mar 3, 2026
019203c
[management] Add CA customization options and wildcard certificate se…
zgv163 Mar 3, 2026
a771343
[management,client] Address review feedback
zgv163 Mar 5, 2026
5b087ad
[management,client] Address review feedback (round 2)
zgv163 Mar 5, 2026
4f9be46
[management,client] Fix rebase conflicts and address remaining review…
zgv163 Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions client/cmd/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package cmd

import (
"fmt"
"strings"
"time"

"github.com/spf13/cobra"
"google.golang.org/grpc/status"

"github.com/netbirdio/netbird/client/proto"
)

var (
certSigningType string
certWildcard bool
)

var certCmd = &cobra.Command{
Use: "cert",
Short: "Manage TLS certificates",
Long: "Commands to request, inspect, and manage TLS certificates for this peer.",
}

var certRequestCmd = &cobra.Command{
Use: "request",
Short: "Request a TLS certificate for this peer",
Example: " netbird cert request\n netbird cert request --type acme --wildcard",
RunE: certRequestFn,
}

var certStatusCmd = &cobra.Command{
Use: "status",
Short: "Show current certificate status",
RunE: certStatusFn,
}

var certTrustCACmd = &cobra.Command{
Use: "trust-ca",
Short: "Install account CA into the OS trust store",
RunE: certTrustCAFn,
}

var certUntrustCACmd = &cobra.Command{
Use: "untrust-ca",
Short: "Remove account CA from the OS trust store",
RunE: certUntrustCAFn,
}

func init() {
certRequestCmd.Flags().StringVar(&certSigningType, "type", "internal", "Signing type: internal or acme")
certRequestCmd.Flags().BoolVar(&certWildcard, "wildcard", false, "Include wildcard SAN (*.fqdn)")
}

func certRequestFn(cmd *cobra.Command, _ []string) error {
conn, err := getClient(cmd)
if err != nil {
return err
}
defer conn.Close()

signingType := proto.DaemonCertSigningType_DAEMON_CERT_SIGNING_INTERNAL
switch strings.ToLower(certSigningType) {
case "acme":
signingType = proto.DaemonCertSigningType_DAEMON_CERT_SIGNING_ACME
case "internal":
signingType = proto.DaemonCertSigningType_DAEMON_CERT_SIGNING_INTERNAL
default:
return fmt.Errorf("invalid signing type %q: must be 'internal' or 'acme'", certSigningType)
}

client := proto.NewDaemonServiceClient(conn)
resp, err := client.RequestCertificate(cmd.Context(), &proto.CertificateRequest{
SigningType: signingType,
Wildcard: certWildcard,
})
if err != nil {
return fmt.Errorf("request certificate: %v", status.Convert(err).Message())
}

cmd.Println("Certificate issued successfully")
cmd.Printf(" Certificate: %s\n", resp.CertPath)
cmd.Printf(" Private key: %s\n", resp.KeyPath)
if len(resp.DnsNames) > 0 {
cmd.Printf(" DNS names: %s\n", strings.Join(resp.DnsNames, ", "))
}
if resp.ExpiresAt > 0 {
cmd.Printf(" Expires: %s\n", time.Unix(resp.ExpiresAt, 0).Format(time.RFC3339))
}

return nil
}

func certStatusFn(cmd *cobra.Command, _ []string) error {
conn, err := getClient(cmd)
if err != nil {
return err
}
defer conn.Close()

client := proto.NewDaemonServiceClient(conn)
resp, err := client.GetCertificateStatus(cmd.Context(), &proto.CertificateStatusRequest{})
if err != nil {
return fmt.Errorf("get certificate status: %v", status.Convert(err).Message())
}

if !resp.HasCertificate {
cmd.Println("No certificate found. Run 'netbird cert request' to obtain one.")
return nil
}

cmd.Println("Certificate status:")
cmd.Printf(" DNS names: %s\n", strings.Join(resp.DnsNames, ", "))
cmd.Printf(" Issuer: %s\n", resp.Issuer)
if resp.IssuedAt > 0 {
cmd.Printf(" Issued: %s\n", time.Unix(resp.IssuedAt, 0).Format(time.RFC3339))
}
if resp.ExpiresAt > 0 {
cmd.Printf(" Expires: %s\n", time.Unix(resp.ExpiresAt, 0).Format(time.RFC3339))
}
cmd.Printf(" CA trusted: %v\n", resp.CaTrusted)
cmd.Printf(" Certificate: %s\n", resp.CertPath)
cmd.Printf(" Private key: %s\n", resp.KeyPath)

return nil
}

func certTrustCAFn(cmd *cobra.Command, _ []string) error {
conn, err := getClient(cmd)
if err != nil {
return err
}
defer conn.Close()

client := proto.NewDaemonServiceClient(conn)
resp, err := client.TrustCA(cmd.Context(), &proto.TrustCARequest{})
if err != nil {
return fmt.Errorf("trust CA: %v", status.Convert(err).Message())
}

if !resp.Success {
return fmt.Errorf("trust CA failed")
}

cmd.Printf("CA certificate(s) installed into OS trust store\n")
for _, fp := range resp.CaFingerprints {
cmd.Printf(" Fingerprint: %s\n", fp)
}

return nil
}

func certUntrustCAFn(cmd *cobra.Command, _ []string) error {
conn, err := getClient(cmd)
if err != nil {
return err
}
defer conn.Close()

client := proto.NewDaemonServiceClient(conn)
resp, err := client.UntrustCA(cmd.Context(), &proto.UntrustCARequest{})
if err != nil {
return fmt.Errorf("untrust CA: %v", status.Convert(err).Message())
}

if !resp.Success {
return fmt.Errorf("untrust CA failed")
}

cmd.Println("CA certificate(s) removed from OS trust store")
return nil
}
7 changes: 7 additions & 0 deletions client/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ func init() {
rootCmd.AddCommand(debugCmd)
rootCmd.AddCommand(profileCmd)
rootCmd.AddCommand(exposeCmd)
rootCmd.AddCommand(certCmd)

networksCMD.AddCommand(routesListCmd)
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
Expand All @@ -173,6 +174,12 @@ func init() {
profileCmd.AddCommand(profileRemoveCmd)
profileCmd.AddCommand(profileSelectCmd)

// cert commands
certCmd.AddCommand(certRequestCmd)
certCmd.AddCommand(certStatusCmd)
certCmd.AddCommand(certTrustCACmd)
certCmd.AddCommand(certUntrustCACmd)

upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
`Sets external IPs maps between local addresses and interfaces.`+
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
Expand Down
Loading