diff --git a/client/cmd/cert.go b/client/cmd/cert.go new file mode 100644 index 00000000000..e7fdcb82c1a --- /dev/null +++ b/client/cmd/cert.go @@ -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 +} diff --git a/client/cmd/root.go b/client/cmd/root.go index aa5b98dfd60..a5b945b5dd9 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -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) @@ -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. `+ diff --git a/client/internal/cert/manager.go b/client/internal/cert/manager.go new file mode 100644 index 00000000000..19a9a95f3b1 --- /dev/null +++ b/client/internal/cert/manager.go @@ -0,0 +1,245 @@ +package cert + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "sync" + "time" +) + +const ( + certFileName = "cert.pem" + keyFileName = "key.pem" + chainFileName = "chain.pem" + caFileName = "ca.pem" +) + +// Manager handles client-side certificate lifecycle: key generation, CSR creation, +// certificate storage, and renewal detection. +type Manager struct { + certDir string + mu sync.RWMutex +} + +// NewManager creates a new certificate manager that stores files in certDir. +func NewManager(certDir string) (*Manager, error) { + if err := os.MkdirAll(certDir, 0700); err != nil { + return nil, fmt.Errorf("create cert directory: %w", err) + } + return &Manager{certDir: certDir}, nil +} + +// GenerateKey creates a new ECDSA P-256 private key suitable for TLS certificates. +func (m *Manager) GenerateKey() (crypto.Signer, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate ECDSA key: %w", err) + } + return key, nil +} + +// CreateCSR creates a DER-encoded certificate signing request for the given FQDN. +// If wildcard is true, a wildcard SAN (*.fqdn) is included alongside the base FQDN. +func (m *Manager) CreateCSR(key crypto.Signer, fqdn string, wildcard bool) ([]byte, error) { + if fqdn == "" { + return nil, fmt.Errorf("FQDN is required for CSR creation") + } + + dnsNames := []string{fqdn} + if wildcard { + dnsNames = append(dnsNames, "*."+fqdn) + } + + template := &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: fqdn}, + DNSNames: dnsNames, + } + + csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, fmt.Errorf("create CSR: %w", err) + } + return csrDER, nil +} + +// StoreCert writes the leaf certificate, CA chain, and private key to disk. +// All three files are written to temp files first, then renamed atomically +// to avoid leaving inconsistent state on partial failure. +// The private key file is restricted to owner-only read/write (0600). +func (m *Manager) StoreCert(certPEM, chainPEM, keyPEM []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + + type pendingFile struct { + path string + data []byte + perm os.FileMode + } + files := []pendingFile{ + {filepath.Join(m.certDir, certFileName), certPEM, 0644}, + {filepath.Join(m.certDir, chainFileName), chainPEM, 0644}, + {filepath.Join(m.certDir, keyFileName), keyPEM, 0600}, + } + + // Phase 1: write all temp files + var tmpPaths []string + for _, f := range files { + tmp := f.path + ".tmp" + if err := os.WriteFile(tmp, f.data, f.perm); err != nil { + // Clean up any temp files written so far + for _, t := range tmpPaths { + _ = os.Remove(t) + } + return fmt.Errorf("write %s: %w", filepath.Base(f.path), err) + } + tmpPaths = append(tmpPaths, tmp) + } + + // Phase 2: rename all (atomic on most filesystems) + for i, f := range files { + if err := os.Rename(tmpPaths[i], f.path); err != nil { + // Clean up the failed and remaining temp files + for j := i; j < len(tmpPaths); j++ { + _ = os.Remove(tmpPaths[j]) + } + return fmt.Errorf("rename %s: %w", filepath.Base(f.path), err) + } + } + + return nil +} + +// StoreCA writes the active CA certificates to disk, concatenated into a single file. +func (m *Manager) StoreCA(caPEMs [][]byte) error { + m.mu.Lock() + defer m.mu.Unlock() + + var combined []byte + for _, p := range caPEMs { + combined = append(combined, p...) + if len(p) > 0 && p[len(p)-1] != '\n' { + combined = append(combined, '\n') + } + } + + return atomicWrite(filepath.Join(m.certDir, caFileName), combined, 0644) +} + +// LoadCert reads and parses the stored leaf certificate. +func (m *Manager) LoadCert() (*x509.Certificate, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.loadCert() +} + +// HasCert returns true if a certificate file exists on disk. +func (m *Manager) HasCert() bool { + m.mu.RLock() + defer m.mu.RUnlock() + + _, err := os.Stat(filepath.Join(m.certDir, certFileName)) + return err == nil +} + +// NeedsRenewal returns true if the stored certificate expires within the given threshold, +// or if the certificate cannot be loaded. +func (m *Manager) NeedsRenewal(threshold time.Duration) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + cert, err := m.loadCert() + if err != nil { + return true + } + return time.Until(cert.NotAfter) < threshold +} + +// IsExpired returns true if the stored certificate has expired. +func (m *Manager) IsExpired() bool { + m.mu.RLock() + defer m.mu.RUnlock() + + cert, err := m.loadCert() + if err != nil { + return false + } + return time.Now().After(cert.NotAfter) +} + +// FQDNChanged returns true if the stored certificate's primary DNS name +// differs from the given FQDN, indicating the peer was renamed. +func (m *Manager) FQDNChanged(currentFQDN string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + cert, err := m.loadCert() + if err != nil { + return false + } + if len(cert.DNSNames) == 0 { + return true + } + for _, name := range cert.DNSNames { + if name == currentFQDN { + return false + } + } + return true +} + +// loadCert is the internal unlocked version of LoadCert. Callers must hold mu. +func (m *Manager) loadCert() (*x509.Certificate, error) { + data, err := os.ReadFile(filepath.Join(m.certDir, certFileName)) + if err != nil { + return nil, fmt.Errorf("read cert: %w", err) + } + + block, _ := pem.Decode(data) + if block == nil { + return nil, fmt.Errorf("no PEM block found in cert file") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse cert: %w", err) + } + return cert, nil +} + +// CertPath returns the path to the leaf certificate file. +func (m *Manager) CertPath() string { + return filepath.Join(m.certDir, certFileName) +} + +// KeyPath returns the path to the private key file. +func (m *Manager) KeyPath() string { + return filepath.Join(m.certDir, keyFileName) +} + +// ChainPath returns the path to the CA chain file. +func (m *Manager) ChainPath() string { + return filepath.Join(m.certDir, chainFileName) +} + +// CAPath returns the path to the combined CA certificates file. +func (m *Manager) CAPath() string { + return filepath.Join(m.certDir, caFileName) +} + +// atomicWrite writes data to a temporary file and renames it to the target path. +func atomicWrite(path string, data []byte, perm os.FileMode) error { + tmp := path + ".tmp" + if err := os.WriteFile(tmp, data, perm); err != nil { + return err + } + return os.Rename(tmp, path) +} diff --git a/client/internal/cert/manager_test.go b/client/internal/cert/manager_test.go new file mode 100644 index 00000000000..857adb1102d --- /dev/null +++ b/client/internal/cert/manager_test.go @@ -0,0 +1,214 @@ +package cert + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateKey(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + key, err := mgr.GenerateKey() + require.NoError(t, err) + require.NotNil(t, key) + + ecKey, ok := key.(*ecdsa.PrivateKey) + require.True(t, ok, "expected ECDSA key") + assert.Equal(t, elliptic.P256(), ecKey.Curve) +} + +func TestCreateCSR(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + key, err := mgr.GenerateKey() + require.NoError(t, err) + + csrDER, err := mgr.CreateCSR(key, "peer1.netbird.example", false) + require.NoError(t, err) + + csr, err := x509.ParseCertificateRequest(csrDER) + require.NoError(t, err) + + assert.Equal(t, "peer1.netbird.example", csr.Subject.CommonName) + assert.Equal(t, []string{"peer1.netbird.example"}, csr.DNSNames) +} + +func TestCreateCSRWildcard(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + key, err := mgr.GenerateKey() + require.NoError(t, err) + + csrDER, err := mgr.CreateCSR(key, "peer1.netbird.example", true) + require.NoError(t, err) + + csr, err := x509.ParseCertificateRequest(csrDER) + require.NoError(t, err) + + assert.Equal(t, "peer1.netbird.example", csr.Subject.CommonName) + assert.Contains(t, csr.DNSNames, "peer1.netbird.example") + assert.Contains(t, csr.DNSNames, "*.peer1.netbird.example") + assert.Len(t, csr.DNSNames, 2) +} + +func TestStoreCertAndLoad(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + certPEM, keyPEM := generateTestCert(t, "test.netbird.example", time.Now().Add(365*24*time.Hour)) + + err = mgr.StoreCert(certPEM, []byte("chain"), keyPEM) + require.NoError(t, err) + + loaded, err := mgr.LoadCert() + require.NoError(t, err) + assert.Equal(t, "test.netbird.example", loaded.Subject.CommonName) + assert.Contains(t, loaded.DNSNames, "test.netbird.example") +} + +func TestStoreCA(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + ca1 := []byte("-----BEGIN CERTIFICATE-----\nCA1\n-----END CERTIFICATE-----\n") + ca2 := []byte("-----BEGIN CERTIFICATE-----\nCA2\n-----END CERTIFICATE-----\n") + + err = mgr.StoreCA([][]byte{ca1, ca2}) + require.NoError(t, err) + + data, err := os.ReadFile(mgr.CAPath()) + require.NoError(t, err) + assert.Contains(t, string(data), "CA1") + assert.Contains(t, string(data), "CA2") +} + +func TestHasCert(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + assert.False(t, mgr.HasCert()) + + certPEM, keyPEM := generateTestCert(t, "test.example", time.Now().Add(365*24*time.Hour)) + err = mgr.StoreCert(certPEM, []byte("chain"), keyPEM) + require.NoError(t, err) + + assert.True(t, mgr.HasCert()) +} + +func TestNeedsRenewal(t *testing.T) { + t.Run("expiring soon", func(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + certPEM, keyPEM := generateTestCert(t, "test.example", time.Now().Add(10*24*time.Hour)) + err = mgr.StoreCert(certPEM, []byte("chain"), keyPEM) + require.NoError(t, err) + + assert.True(t, mgr.NeedsRenewal(30*24*time.Hour)) + }) + + t.Run("not expiring soon", func(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + certPEM, keyPEM := generateTestCert(t, "test.example", time.Now().Add(365*24*time.Hour)) + err = mgr.StoreCert(certPEM, []byte("chain"), keyPEM) + require.NoError(t, err) + + assert.False(t, mgr.NeedsRenewal(30*24*time.Hour)) + }) + + t.Run("no cert returns true", func(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + assert.True(t, mgr.NeedsRenewal(30*24*time.Hour)) + }) +} + +func TestFQDNChanged(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + certPEM, keyPEM := generateTestCert(t, "old.netbird.example", time.Now().Add(365*24*time.Hour)) + err = mgr.StoreCert(certPEM, []byte("chain"), keyPEM) + require.NoError(t, err) + + assert.False(t, mgr.FQDNChanged("old.netbird.example")) + assert.True(t, mgr.FQDNChanged("new.netbird.example")) +} + +func TestFQDNChangedNoCert(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + assert.False(t, mgr.FQDNChanged("any.example")) +} + +func TestKeyFilePermissions(t *testing.T) { + mgr, err := NewManager(t.TempDir()) + require.NoError(t, err) + + certPEM, keyPEM := generateTestCert(t, "test.example", time.Now().Add(365*24*time.Hour)) + err = mgr.StoreCert(certPEM, []byte("chain"), keyPEM) + require.NoError(t, err) + + info, err := os.Stat(filepath.Join(mgr.certDir, keyFileName)) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0600), info.Mode().Perm()) +} + +func TestPaths(t *testing.T) { + dir := t.TempDir() + mgr, err := NewManager(dir) + require.NoError(t, err) + + assert.Equal(t, filepath.Join(dir, "cert.pem"), mgr.CertPath()) + assert.Equal(t, filepath.Join(dir, "key.pem"), mgr.KeyPath()) + assert.Equal(t, filepath.Join(dir, "chain.pem"), mgr.ChainPath()) + assert.Equal(t, filepath.Join(dir, "ca.pem"), mgr.CAPath()) +} + +// generateTestCert creates a self-signed certificate with the given FQDN and expiry. +func generateTestCert(t *testing.T, fqdn string, notAfter time.Time) (certPEM, keyPEM []byte) { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: fqdn}, + DNSNames: []string{fqdn}, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + keyDER, err := x509.MarshalECPrivateKey(key) + require.NoError(t, err) + keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + return certPEM, keyPEM +} diff --git a/client/internal/cert/trust_darwin.go b/client/internal/cert/trust_darwin.go new file mode 100644 index 00000000000..e75b575f9e9 --- /dev/null +++ b/client/internal/cert/trust_darwin.go @@ -0,0 +1,87 @@ +//go:build darwin + +package cert + +import ( + "crypto/sha1" //nolint:gosec // SHA-1 used for macOS Keychain fingerprint matching + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + "os/exec" + "strings" +) + +// InstallCA adds a CA certificate to the macOS System Keychain as a trusted root. +func InstallCA(caPEM []byte) error { + tmpFile, err := writeTempPEM(caPEM) + if err != nil { + return err + } + defer os.Remove(tmpFile) + + out, err := exec.Command("security", "add-trusted-cert", "-d", "-r", "trustRoot", + "-k", "/Library/Keychains/System.keychain", tmpFile).CombinedOutput() + if err != nil { + return fmt.Errorf("security add-trusted-cert: %s: %w", strings.TrimSpace(string(out)), err) + } + return nil +} + +// UninstallCA removes a CA certificate from the macOS System Keychain by its SHA-1 fingerprint. +func UninstallCA(caPEM []byte) error { + fp, err := sha1Fingerprint(caPEM) + if err != nil { + return err + } + + out, err := exec.Command("security", "delete-certificate", "-Z", fp, + "/Library/Keychains/System.keychain").CombinedOutput() + if err != nil { + return fmt.Errorf("security delete-certificate: %s: %w", strings.TrimSpace(string(out)), err) + } + return nil +} + +// IsCATrusted checks whether a CA certificate is trusted by the macOS system. +func IsCATrusted(caPEM []byte) bool { + tmpFile, err := writeTempPEM(caPEM) + if err != nil { + return false + } + defer os.Remove(tmpFile) + + err = exec.Command("security", "verify-cert", "-c", tmpFile).Run() + return err == nil +} + +func writeTempPEM(data []byte) (string, error) { + f, err := os.CreateTemp("", "netbird-ca-*.pem") + if err != nil { + return "", fmt.Errorf("create temp file: %w", err) + } + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(f.Name()) + return "", fmt.Errorf("write temp file: %w", err) + } + if err := f.Close(); err != nil { + os.Remove(f.Name()) + return "", fmt.Errorf("close temp file: %w", err) + } + return f.Name(), nil +} + +func sha1Fingerprint(caPEM []byte) (string, error) { + block, _ := pem.Decode(caPEM) + if block == nil { + return "", fmt.Errorf("no PEM block found") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", fmt.Errorf("parse certificate: %w", err) + } + sum := sha1.Sum(cert.Raw) //nolint:gosec + return strings.ToUpper(hex.EncodeToString(sum[:])), nil +} diff --git a/client/internal/cert/trust_linux.go b/client/internal/cert/trust_linux.go new file mode 100644 index 00000000000..89d08cbac72 --- /dev/null +++ b/client/internal/cert/trust_linux.go @@ -0,0 +1,146 @@ +//go:build linux + +package cert + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + "os/exec" + "strings" + "time" +) + +const ( + // Debian/Ubuntu paths + debianCertDir = "/usr/local/share/ca-certificates" + debianUpdateCmd = "update-ca-certificates" + + // RHEL/Fedora paths + rhelCertDir = "/etc/pki/ca-trust/source/anchors" + rhelUpdateCmd = "update-ca-trust" +) + +// InstallCA adds a CA certificate to the Linux system trust store. +// Supports Debian/Ubuntu and RHEL/Fedora families. +func InstallCA(caPEM []byte) error { + certDir, updateCmd, err := detectDistro() + if err != nil { + return err + } + + fp, err := certFingerprint(caPEM) + if err != nil { + return err + } + + certPath := fmt.Sprintf("%s/netbird-%s.crt", certDir, fp) + if err := os.WriteFile(certPath, caPEM, 0644); err != nil { + return fmt.Errorf("write CA cert: %w", err) + } + + if err := runWithTimeout(updateCmd, 30*time.Second); err != nil { + return err + } + return nil +} + +// UninstallCA removes a CA certificate from the Linux system trust store. +func UninstallCA(caPEM []byte) error { + certDir, updateCmd, err := detectDistro() + if err != nil { + return err + } + + fp, err := certFingerprint(caPEM) + if err != nil { + return err + } + + certPath := fmt.Sprintf("%s/netbird-%s.crt", certDir, fp) + if err := os.Remove(certPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove CA cert: %w", err) + } + + if err := runWithTimeout(updateCmd, 30*time.Second); err != nil { + return err + } + return nil +} + +// IsCATrusted checks whether a CA certificate file exists in the system trust store. +func IsCATrusted(caPEM []byte) bool { + certDir, _, err := detectDistro() + if err != nil { + return false + } + + fp, err := certFingerprint(caPEM) + if err != nil { + return false + } + + certPath := fmt.Sprintf("%s/netbird-%s.crt", certDir, fp) + _, err = os.Stat(certPath) + return err == nil +} + +// resolveCmd tries known absolute paths before falling back to LookPath, +// which may fail in restricted daemon/service environments where /usr/sbin +// is not in PATH. +func resolveCmd(name string) (string, error) { + candidates := []string{ + "/usr/sbin/" + name, + "/usr/bin/" + name, + "/sbin/" + name, + "/bin/" + name, + } + for _, p := range candidates { + if st, err := os.Stat(p); err == nil && st.Mode().Perm()&0111 != 0 { + return p, nil + } + } + return exec.LookPath(name) +} + +func detectDistro() (certDir, updateCmd string, err error) { + if _, err := os.Stat(debianCertDir); err == nil { + if cmd, err := resolveCmd(debianUpdateCmd); err == nil { + return debianCertDir, cmd, nil + } + } + if _, err := os.Stat(rhelCertDir); err == nil { + if cmd, err := resolveCmd(rhelUpdateCmd); err == nil { + return rhelCertDir, cmd, nil + } + } + return "", "", fmt.Errorf("unsupported Linux distribution: neither %s nor %s found", debianUpdateCmd, rhelUpdateCmd) +} + +func runWithTimeout(command string, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + out, err := exec.CommandContext(ctx, command).CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %s: %w", command, strings.TrimSpace(string(out)), err) + } + return nil +} + +func certFingerprint(caPEM []byte) (string, error) { + block, _ := pem.Decode(caPEM) + if block == nil { + return "", fmt.Errorf("no PEM block found") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", fmt.Errorf("parse certificate: %w", err) + } + sum := sha256.Sum256(cert.Raw) + return hex.EncodeToString(sum[:]), nil +} diff --git a/client/internal/cert/trust_windows.go b/client/internal/cert/trust_windows.go new file mode 100644 index 00000000000..9b0ff45a643 --- /dev/null +++ b/client/internal/cert/trust_windows.go @@ -0,0 +1,85 @@ +//go:build windows + +package cert + +import ( + "crypto/sha1" //nolint:gosec // SHA-1 used for Windows certutil fingerprint matching + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + "os/exec" + "strings" +) + +// InstallCA adds a CA certificate to the Windows Root certificate store. +func InstallCA(caPEM []byte) error { + tmpFile, err := writeTempPEM(caPEM) + if err != nil { + return err + } + defer os.Remove(tmpFile) + + out, err := exec.Command("certutil", "-addstore", "-f", "Root", tmpFile).CombinedOutput() + if err != nil { + return fmt.Errorf("certutil -addstore: %s: %w", strings.TrimSpace(string(out)), err) + } + return nil +} + +// UninstallCA removes a CA certificate from the Windows Root certificate store. +func UninstallCA(caPEM []byte) error { + fp, err := sha1Fingerprint(caPEM) + if err != nil { + return err + } + + out, err := exec.Command("certutil", "-delstore", "Root", fp).CombinedOutput() + if err != nil { + return fmt.Errorf("certutil -delstore: %s: %w", strings.TrimSpace(string(out)), err) + } + return nil +} + +// IsCATrusted checks whether a CA certificate is in the Windows Root store. +func IsCATrusted(caPEM []byte) bool { + tmpFile, err := writeTempPEM(caPEM) + if err != nil { + return false + } + defer os.Remove(tmpFile) + + err = exec.Command("certutil", "-verify", tmpFile).Run() + return err == nil +} + +func writeTempPEM(data []byte) (string, error) { + f, err := os.CreateTemp("", "netbird-ca-*.pem") + if err != nil { + return "", fmt.Errorf("create temp file: %w", err) + } + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(f.Name()) + return "", fmt.Errorf("write temp file: %w", err) + } + if err := f.Close(); err != nil { + os.Remove(f.Name()) + return "", fmt.Errorf("close temp file: %w", err) + } + return f.Name(), nil +} + +func sha1Fingerprint(caPEM []byte) (string, error) { + block, _ := pem.Decode(caPEM) + if block == nil { + return "", fmt.Errorf("no PEM block found") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", fmt.Errorf("parse certificate: %w", err) + } + sum := sha1.Sum(cert.Raw) //nolint:gosec + return strings.ToUpper(hex.EncodeToString(sum[:])), nil +} diff --git a/client/internal/connect.go b/client/internal/connect.go index 68a0cb8da90..fae4c1f2de9 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -21,6 +21,7 @@ import ( "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/netstack" + "github.com/netbirdio/netbird/client/internal/cert" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" @@ -53,6 +54,7 @@ type ConnectClient struct { engineMutex sync.Mutex persistSyncResponse bool + certManager *cert.Manager } func NewConnectClient( @@ -310,6 +312,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan c.engineMutex.Lock() engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, checks, stateManager) engine.SetSyncResponsePersistence(c.persistSyncResponse) + engine.SetCertManager(c.certManager) c.engine = engine c.engineMutex.Unlock() @@ -474,6 +477,18 @@ func (c *ConnectClient) SetSyncResponsePersistence(enabled bool) { } } +// SetCertManager sets the certificate manager for TLS certificate lifecycle. +func (c *ConnectClient) SetCertManager(m *cert.Manager) { + c.engineMutex.Lock() + c.certManager = m + engine := c.engine + c.engineMutex.Unlock() + + if engine != nil { + engine.SetCertManager(m) + } +} + // createEngineConfig converts configuration received from Management Service to EngineConfig func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConfig *mgmProto.PeerConfig, logPath string) (*EngineConfig, error) { nm := false diff --git a/client/internal/engine.go b/client/internal/engine.go index b0ae841f8c0..a6e48161cca 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -2,6 +2,9 @@ package internal import ( "context" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" "errors" "fmt" "math/rand" @@ -14,6 +17,7 @@ import ( "sort" "strings" "sync" + "sync/atomic" "time" "github.com/hashicorp/go-multierror" @@ -32,6 +36,7 @@ import ( nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/internal/acl" + "github.com/netbirdio/netbird/client/internal/cert" "github.com/netbirdio/netbird/client/internal/debug" "github.com/netbirdio/netbird/client/internal/dns" dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config" @@ -223,6 +228,8 @@ type Engine struct { jobExecutorWG sync.WaitGroup exposeManager *expose.Manager + certManager *cert.Manager + certRenewing atomic.Bool } // Peer is an instance of the Connection Peer @@ -556,6 +563,8 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) } }() + e.startCertRenewalLoop() + return nil } @@ -1027,8 +1036,26 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { e.statusRecorder.UpdateLocalPeerState(state) + // Store CA certificates from sync (empty list clears the local bundle) + if caCerts := conf.GetCaCertificatesPem(); e.certManager != nil { + if err := e.certManager.StoreCA(caCerts); err != nil { + log.Warnf("failed to store CA certificates: %v", err) + } + } + + // Detect FQDN change — re-issue cert if one exists and FQDN differs + if e.certManager != nil && e.certManager.HasCert() && e.certManager.FQDNChanged(conf.GetFqdn()) { + go e.renewCertificate("fqdn_change") + } + + // Renew certificate if it has expired (e.g. after peer re-authentication) + if e.certManager != nil && e.certManager.HasCert() && e.certManager.IsExpired() { + go e.renewCertificate("session_renewal") + } + return nil } + func (e *Engine) receiveJobEvents() { e.jobExecutorWG.Add(1) go func() { @@ -1827,6 +1854,11 @@ func (e *Engine) GetFirewallManager() firewallManager.Manager { return e.firewall } +// GetMgmClient returns the management service client. +func (e *Engine) GetMgmClient() mgm.Client { + return e.mgmClient +} + // GetExposeManager returns the expose session manager. func (e *Engine) GetExposeManager() *expose.Manager { e.syncMsgMux.Lock() @@ -2027,6 +2059,142 @@ func (e *Engine) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) { return sr, nil } +// SetCertManager sets the certificate manager for TLS certificate lifecycle. +func (e *Engine) SetCertManager(m *cert.Manager) { + e.certManager = m +} + +// startCertRenewalLoop runs a background goroutine that periodically checks +// whether the peer's TLS certificate needs renewal. +func (e *Engine) startCertRenewalLoop() { + if e.certManager == nil { + return + } + e.shutdownWg.Add(1) + go func() { + defer e.shutdownWg.Done() + ticker := time.NewTicker(1 * time.Hour) + defer ticker.Stop() + for { + select { + case <-e.ctx.Done(): + return + case <-ticker.C: + if e.certManager.IsExpired() { + log.Infof("certificate expired, starting renewal") + e.renewCertificate("session_renewal") + } else if c, err := e.certManager.LoadCert(); err == nil { + threshold := certRenewalThreshold(c.NotBefore, c.NotAfter) + if e.certManager.NeedsRenewal(threshold) { + log.Infof("certificate approaching expiry (threshold: %s), starting renewal", threshold) + e.renewCertificate("auto_renewal") + } + } + } + } + }() +} + +// certRenewalThreshold computes a renewal threshold proportional to the certificate's +// total validity. For short-lived certs (e.g. 24h), a fixed 30-day threshold would +// cause immediate renewal; instead we use 1/3 of the total lifetime, clamped between +// 1 hour and 30 days. +func certRenewalThreshold(notBefore, notAfter time.Time) time.Duration { + total := notAfter.Sub(notBefore) + threshold := total / 3 + + const minThreshold = 1 * time.Hour + const maxThreshold = 30 * 24 * time.Hour + if threshold < minThreshold { + threshold = minThreshold + } + if threshold > maxThreshold { + threshold = maxThreshold + } + return threshold +} + +// renewCertificate generates a new key and CSR, sends it to management for +// signing, and stores the resulting certificate locally. +func (e *Engine) renewCertificate(trigger string) { + if e.certManager == nil { + return + } + + if !e.certRenewing.CompareAndSwap(false, true) { + log.Debugf("cert renewal (%s): already in progress, skipping", trigger) + return + } + defer e.certRenewing.Store(false) + + fqdn := e.statusRecorder.GetLocalPeerState().FQDN + if fqdn == "" { + log.Warnf("cert renewal (%s): FQDN not available", trigger) + return + } + + key, err := e.certManager.GenerateKey() + if err != nil { + log.Warnf("cert renewal (%s): generate key: %v", trigger, err) + return + } + + // Detect if the existing cert is a wildcard so we preserve it on renewal + isWildcard := false + if existing, loadErr := e.certManager.LoadCert(); loadErr == nil { + for _, name := range existing.DNSNames { + if strings.HasPrefix(name, "*.") { + isWildcard = true + break + } + } + } + + csrDER, err := e.certManager.CreateCSR(key, fqdn, isWildcard) + if err != nil { + log.Warnf("cert renewal (%s): create CSR: %v", trigger, err) + return + } + + signCtx, cancel := context.WithTimeout(e.ctx, 30*time.Second) + defer cancel() + + signResp, err := e.mgmClient.SignCertificate(signCtx, csrDER, mgmProto.CertSigningType_CERT_SIGNING_INTERNAL, isWildcard) + if err != nil { + log.Warnf("cert renewal (%s): sign certificate: %v", trigger, err) + e.statusRecorder.PublishEvent(cProto.SystemEvent_WARNING, cProto.SystemEvent_SYSTEM, "Certificate renewal failed", err.Error(), nil) + return + } + + ecKey, ok := key.(*ecdsa.PrivateKey) + if !ok { + log.Warnf("cert renewal (%s): unexpected key type", trigger) + return + } + keyDER, err := x509.MarshalECPrivateKey(ecKey) + if err != nil { + log.Warnf("cert renewal (%s): marshal private key: %v", trigger, err) + return + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + certPEM := signResp.InternalCertPem + chainPEM := signResp.InternalChainPem + + if len(certPEM) == 0 { + log.Warnf("cert renewal (%s): server returned empty certificate", trigger) + return + } + + if err := e.certManager.StoreCert(certPEM, chainPEM, keyPEM); err != nil { + log.Warnf("cert renewal (%s): store certificate: %v", trigger, err) + return + } + + log.Infof("certificate renewed (%s) for %s", trigger, fqdn) + e.statusRecorder.PublishEvent(cProto.SystemEvent_INFO, cProto.SystemEvent_SYSTEM, "Certificate renewed", "", nil) +} + // GetWgAddr returns the wireguard address func (e *Engine) GetWgAddr() netip.Addr { if e.wgInterface == nil { diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 3879beba306..8331fbfb7ac 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v6.33.3 +// protoc v6.33.4 // source: daemon.proto package proto @@ -140,6 +140,52 @@ func (ExposeProtocol) EnumDescriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{1} } +type DaemonCertSigningType int32 + +const ( + DaemonCertSigningType_DAEMON_CERT_SIGNING_INTERNAL DaemonCertSigningType = 0 + DaemonCertSigningType_DAEMON_CERT_SIGNING_ACME DaemonCertSigningType = 1 +) + +// Enum value maps for DaemonCertSigningType. +var ( + DaemonCertSigningType_name = map[int32]string{ + 0: "DAEMON_CERT_SIGNING_INTERNAL", + 1: "DAEMON_CERT_SIGNING_ACME", + } + DaemonCertSigningType_value = map[string]int32{ + "DAEMON_CERT_SIGNING_INTERNAL": 0, + "DAEMON_CERT_SIGNING_ACME": 1, + } +) + +func (x DaemonCertSigningType) Enum() *DaemonCertSigningType { + p := new(DaemonCertSigningType) + *p = x + return p +} + +func (x DaemonCertSigningType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DaemonCertSigningType) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_proto_enumTypes[2].Descriptor() +} + +func (DaemonCertSigningType) Type() protoreflect.EnumType { + return &file_daemon_proto_enumTypes[2] +} + +func (x DaemonCertSigningType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DaemonCertSigningType.Descriptor instead. +func (DaemonCertSigningType) EnumDescriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{2} +} + // avoid collision with loglevel enum type OSLifecycleRequest_CycleType int32 @@ -174,11 +220,11 @@ func (x OSLifecycleRequest_CycleType) String() string { } func (OSLifecycleRequest_CycleType) Descriptor() protoreflect.EnumDescriptor { - return file_daemon_proto_enumTypes[2].Descriptor() + return file_daemon_proto_enumTypes[3].Descriptor() } func (OSLifecycleRequest_CycleType) Type() protoreflect.EnumType { - return &file_daemon_proto_enumTypes[2] + return &file_daemon_proto_enumTypes[3] } func (x OSLifecycleRequest_CycleType) Number() protoreflect.EnumNumber { @@ -226,11 +272,11 @@ func (x SystemEvent_Severity) String() string { } func (SystemEvent_Severity) Descriptor() protoreflect.EnumDescriptor { - return file_daemon_proto_enumTypes[3].Descriptor() + return file_daemon_proto_enumTypes[4].Descriptor() } func (SystemEvent_Severity) Type() protoreflect.EnumType { - return &file_daemon_proto_enumTypes[3] + return &file_daemon_proto_enumTypes[4] } func (x SystemEvent_Severity) Number() protoreflect.EnumNumber { @@ -281,11 +327,11 @@ func (x SystemEvent_Category) String() string { } func (SystemEvent_Category) Descriptor() protoreflect.EnumDescriptor { - return file_daemon_proto_enumTypes[4].Descriptor() + return file_daemon_proto_enumTypes[5].Descriptor() } func (SystemEvent_Category) Type() protoreflect.EnumType { - return &file_daemon_proto_enumTypes[4] + return &file_daemon_proto_enumTypes[5] } func (x SystemEvent_Category) Number() protoreflect.EnumNumber { @@ -5870,6 +5916,430 @@ func (x *ExposeServiceReady) GetDomain() string { return "" } +type CertificateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SigningType DaemonCertSigningType `protobuf:"varint,1,opt,name=signing_type,json=signingType,proto3,enum=daemon.DaemonCertSigningType" json:"signing_type,omitempty"` + Wildcard bool `protobuf:"varint,2,opt,name=wildcard,proto3" json:"wildcard,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CertificateRequest) Reset() { + *x = CertificateRequest{} + mi := &file_daemon_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CertificateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateRequest) ProtoMessage() {} + +func (x *CertificateRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[88] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateRequest.ProtoReflect.Descriptor instead. +func (*CertificateRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{88} +} + +func (x *CertificateRequest) GetSigningType() DaemonCertSigningType { + if x != nil { + return x.SigningType + } + return DaemonCertSigningType_DAEMON_CERT_SIGNING_INTERNAL +} + +func (x *CertificateRequest) GetWildcard() bool { + if x != nil { + return x.Wildcard + } + return false +} + +type CertificateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + CertPath string `protobuf:"bytes,1,opt,name=cert_path,json=certPath,proto3" json:"cert_path,omitempty"` + KeyPath string `protobuf:"bytes,2,opt,name=key_path,json=keyPath,proto3" json:"key_path,omitempty"` + DnsNames []string `protobuf:"bytes,3,rep,name=dns_names,json=dnsNames,proto3" json:"dns_names,omitempty"` + ExpiresAt int64 `protobuf:"varint,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CertificateResponse) Reset() { + *x = CertificateResponse{} + mi := &file_daemon_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CertificateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateResponse) ProtoMessage() {} + +func (x *CertificateResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[89] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateResponse.ProtoReflect.Descriptor instead. +func (*CertificateResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{89} +} + +func (x *CertificateResponse) GetCertPath() string { + if x != nil { + return x.CertPath + } + return "" +} + +func (x *CertificateResponse) GetKeyPath() string { + if x != nil { + return x.KeyPath + } + return "" +} + +func (x *CertificateResponse) GetDnsNames() []string { + if x != nil { + return x.DnsNames + } + return nil +} + +func (x *CertificateResponse) GetExpiresAt() int64 { + if x != nil { + return x.ExpiresAt + } + return 0 +} + +type CertificateStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CertificateStatusRequest) Reset() { + *x = CertificateStatusRequest{} + mi := &file_daemon_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CertificateStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateStatusRequest) ProtoMessage() {} + +func (x *CertificateStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[90] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateStatusRequest.ProtoReflect.Descriptor instead. +func (*CertificateStatusRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{90} +} + +type CertificateStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + HasCertificate bool `protobuf:"varint,1,opt,name=has_certificate,json=hasCertificate,proto3" json:"has_certificate,omitempty"` + DnsNames []string `protobuf:"bytes,2,rep,name=dns_names,json=dnsNames,proto3" json:"dns_names,omitempty"` + ExpiresAt int64 `protobuf:"varint,3,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + IssuedAt int64 `protobuf:"varint,4,opt,name=issued_at,json=issuedAt,proto3" json:"issued_at,omitempty"` + Issuer string `protobuf:"bytes,5,opt,name=issuer,proto3" json:"issuer,omitempty"` + CaTrusted bool `protobuf:"varint,6,opt,name=ca_trusted,json=caTrusted,proto3" json:"ca_trusted,omitempty"` + CertPath string `protobuf:"bytes,7,opt,name=cert_path,json=certPath,proto3" json:"cert_path,omitempty"` + KeyPath string `protobuf:"bytes,8,opt,name=key_path,json=keyPath,proto3" json:"key_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CertificateStatusResponse) Reset() { + *x = CertificateStatusResponse{} + mi := &file_daemon_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CertificateStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateStatusResponse) ProtoMessage() {} + +func (x *CertificateStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[91] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateStatusResponse.ProtoReflect.Descriptor instead. +func (*CertificateStatusResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{91} +} + +func (x *CertificateStatusResponse) GetHasCertificate() bool { + if x != nil { + return x.HasCertificate + } + return false +} + +func (x *CertificateStatusResponse) GetDnsNames() []string { + if x != nil { + return x.DnsNames + } + return nil +} + +func (x *CertificateStatusResponse) GetExpiresAt() int64 { + if x != nil { + return x.ExpiresAt + } + return 0 +} + +func (x *CertificateStatusResponse) GetIssuedAt() int64 { + if x != nil { + return x.IssuedAt + } + return 0 +} + +func (x *CertificateStatusResponse) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + +func (x *CertificateStatusResponse) GetCaTrusted() bool { + if x != nil { + return x.CaTrusted + } + return false +} + +func (x *CertificateStatusResponse) GetCertPath() string { + if x != nil { + return x.CertPath + } + return "" +} + +func (x *CertificateStatusResponse) GetKeyPath() string { + if x != nil { + return x.KeyPath + } + return "" +} + +type TrustCARequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TrustCARequest) Reset() { + *x = TrustCARequest{} + mi := &file_daemon_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TrustCARequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrustCARequest) ProtoMessage() {} + +func (x *TrustCARequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[92] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrustCARequest.ProtoReflect.Descriptor instead. +func (*TrustCARequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{92} +} + +type TrustCAResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + CaFingerprints []string `protobuf:"bytes,2,rep,name=ca_fingerprints,json=caFingerprints,proto3" json:"ca_fingerprints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TrustCAResponse) Reset() { + *x = TrustCAResponse{} + mi := &file_daemon_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TrustCAResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrustCAResponse) ProtoMessage() {} + +func (x *TrustCAResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[93] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrustCAResponse.ProtoReflect.Descriptor instead. +func (*TrustCAResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{93} +} + +func (x *TrustCAResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *TrustCAResponse) GetCaFingerprints() []string { + if x != nil { + return x.CaFingerprints + } + return nil +} + +type UntrustCARequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UntrustCARequest) Reset() { + *x = UntrustCARequest{} + mi := &file_daemon_proto_msgTypes[94] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UntrustCARequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UntrustCARequest) ProtoMessage() {} + +func (x *UntrustCARequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[94] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UntrustCARequest.ProtoReflect.Descriptor instead. +func (*UntrustCARequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{94} +} + +type UntrustCAResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UntrustCAResponse) Reset() { + *x = UntrustCAResponse{} + mi := &file_daemon_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UntrustCAResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UntrustCAResponse) ProtoMessage() {} + +func (x *UntrustCAResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[95] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UntrustCAResponse.ProtoReflect.Descriptor instead. +func (*UntrustCAResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{95} +} + +func (x *UntrustCAResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + type PortInfo_Range struct { state protoimpl.MessageState `protogen:"open.v1"` Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` @@ -5880,7 +6350,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5892,7 +6362,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6437,7 +6907,35 @@ const file_daemon_proto_rawDesc = "" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1f\n" + "\vservice_url\x18\x02 \x01(\tR\n" + "serviceUrl\x12\x16\n" + - "\x06domain\x18\x03 \x01(\tR\x06domain*b\n" + + "\x06domain\x18\x03 \x01(\tR\x06domain\"r\n" + + "\x12CertificateRequest\x12@\n" + + "\fsigning_type\x18\x01 \x01(\x0e2\x1d.daemon.DaemonCertSigningTypeR\vsigningType\x12\x1a\n" + + "\bwildcard\x18\x02 \x01(\bR\bwildcard\"\x89\x01\n" + + "\x13CertificateResponse\x12\x1b\n" + + "\tcert_path\x18\x01 \x01(\tR\bcertPath\x12\x19\n" + + "\bkey_path\x18\x02 \x01(\tR\akeyPath\x12\x1b\n" + + "\tdns_names\x18\x03 \x03(\tR\bdnsNames\x12\x1d\n" + + "\n" + + "expires_at\x18\x04 \x01(\x03R\texpiresAt\"\x1a\n" + + "\x18CertificateStatusRequest\"\x8c\x02\n" + + "\x19CertificateStatusResponse\x12'\n" + + "\x0fhas_certificate\x18\x01 \x01(\bR\x0ehasCertificate\x12\x1b\n" + + "\tdns_names\x18\x02 \x03(\tR\bdnsNames\x12\x1d\n" + + "\n" + + "expires_at\x18\x03 \x01(\x03R\texpiresAt\x12\x1b\n" + + "\tissued_at\x18\x04 \x01(\x03R\bissuedAt\x12\x16\n" + + "\x06issuer\x18\x05 \x01(\tR\x06issuer\x12\x1d\n" + + "\n" + + "ca_trusted\x18\x06 \x01(\bR\tcaTrusted\x12\x1b\n" + + "\tcert_path\x18\a \x01(\tR\bcertPath\x12\x19\n" + + "\bkey_path\x18\b \x01(\tR\akeyPath\"\x10\n" + + "\x0eTrustCARequest\"T\n" + + "\x0fTrustCAResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12'\n" + + "\x0fca_fingerprints\x18\x02 \x03(\tR\x0ecaFingerprints\"\x12\n" + + "\x10UntrustCARequest\"-\n" + + "\x11UntrustCAResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess*b\n" + "\bLogLevel\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05PANIC\x10\x01\x12\t\n" + @@ -6453,7 +6951,10 @@ const file_daemon_proto_rawDesc = "" + "\n" + "EXPOSE_TCP\x10\x02\x12\x0e\n" + "\n" + - "EXPOSE_UDP\x10\x032\xac\x15\n" + + "EXPOSE_UDP\x10\x03*W\n" + + "\x15DaemonCertSigningType\x12 \n" + + "\x1cDAEMON_CERT_SIGNING_INTERNAL\x10\x00\x12\x1c\n" + + "\x18DAEMON_CERT_SIGNING_ACME\x10\x012\xde\x17\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + @@ -6493,7 +6994,11 @@ const file_daemon_proto_rawDesc = "" + "\x0eStopCPUProfile\x12\x1d.daemon.StopCPUProfileRequest\x1a\x1e.daemon.StopCPUProfileResponse\"\x00\x12N\n" + "\x11NotifyOSLifecycle\x12\x1a.daemon.OSLifecycleRequest\x1a\x1b.daemon.OSLifecycleResponse\"\x00\x12W\n" + "\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00\x12M\n" + - "\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01B\bZ\x06/protob\x06proto3" + "\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01\x12O\n" + + "\x12RequestCertificate\x12\x1a.daemon.CertificateRequest\x1a\x1b.daemon.CertificateResponse\"\x00\x12]\n" + + "\x14GetCertificateStatus\x12 .daemon.CertificateStatusRequest\x1a!.daemon.CertificateStatusResponse\"\x00\x12<\n" + + "\aTrustCA\x12\x16.daemon.TrustCARequest\x1a\x17.daemon.TrustCAResponse\"\x00\x12B\n" + + "\tUntrustCA\x12\x18.daemon.UntrustCARequest\x1a\x19.daemon.UntrustCAResponse\"\x00B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once @@ -6507,222 +7012,240 @@ func file_daemon_proto_rawDescGZIP() []byte { return file_daemon_proto_rawDescData } -var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 91) +var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 6) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 99) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ExposeProtocol)(0), // 1: daemon.ExposeProtocol - (OSLifecycleRequest_CycleType)(0), // 2: daemon.OSLifecycleRequest.CycleType - (SystemEvent_Severity)(0), // 3: daemon.SystemEvent.Severity - (SystemEvent_Category)(0), // 4: daemon.SystemEvent.Category - (*EmptyRequest)(nil), // 5: daemon.EmptyRequest - (*OSLifecycleRequest)(nil), // 6: daemon.OSLifecycleRequest - (*OSLifecycleResponse)(nil), // 7: daemon.OSLifecycleResponse - (*LoginRequest)(nil), // 8: daemon.LoginRequest - (*LoginResponse)(nil), // 9: daemon.LoginResponse - (*WaitSSOLoginRequest)(nil), // 10: daemon.WaitSSOLoginRequest - (*WaitSSOLoginResponse)(nil), // 11: daemon.WaitSSOLoginResponse - (*UpRequest)(nil), // 12: daemon.UpRequest - (*UpResponse)(nil), // 13: daemon.UpResponse - (*StatusRequest)(nil), // 14: daemon.StatusRequest - (*StatusResponse)(nil), // 15: daemon.StatusResponse - (*DownRequest)(nil), // 16: daemon.DownRequest - (*DownResponse)(nil), // 17: daemon.DownResponse - (*GetConfigRequest)(nil), // 18: daemon.GetConfigRequest - (*GetConfigResponse)(nil), // 19: daemon.GetConfigResponse - (*PeerState)(nil), // 20: daemon.PeerState - (*LocalPeerState)(nil), // 21: daemon.LocalPeerState - (*SignalState)(nil), // 22: daemon.SignalState - (*ManagementState)(nil), // 23: daemon.ManagementState - (*RelayState)(nil), // 24: daemon.RelayState - (*NSGroupState)(nil), // 25: daemon.NSGroupState - (*SSHSessionInfo)(nil), // 26: daemon.SSHSessionInfo - (*SSHServerState)(nil), // 27: daemon.SSHServerState - (*FullStatus)(nil), // 28: daemon.FullStatus - (*ListNetworksRequest)(nil), // 29: daemon.ListNetworksRequest - (*ListNetworksResponse)(nil), // 30: daemon.ListNetworksResponse - (*SelectNetworksRequest)(nil), // 31: daemon.SelectNetworksRequest - (*SelectNetworksResponse)(nil), // 32: daemon.SelectNetworksResponse - (*IPList)(nil), // 33: daemon.IPList - (*Network)(nil), // 34: daemon.Network - (*PortInfo)(nil), // 35: daemon.PortInfo - (*ForwardingRule)(nil), // 36: daemon.ForwardingRule - (*ForwardingRulesResponse)(nil), // 37: daemon.ForwardingRulesResponse - (*DebugBundleRequest)(nil), // 38: daemon.DebugBundleRequest - (*DebugBundleResponse)(nil), // 39: daemon.DebugBundleResponse - (*GetLogLevelRequest)(nil), // 40: daemon.GetLogLevelRequest - (*GetLogLevelResponse)(nil), // 41: daemon.GetLogLevelResponse - (*SetLogLevelRequest)(nil), // 42: daemon.SetLogLevelRequest - (*SetLogLevelResponse)(nil), // 43: daemon.SetLogLevelResponse - (*State)(nil), // 44: daemon.State - (*ListStatesRequest)(nil), // 45: daemon.ListStatesRequest - (*ListStatesResponse)(nil), // 46: daemon.ListStatesResponse - (*CleanStateRequest)(nil), // 47: daemon.CleanStateRequest - (*CleanStateResponse)(nil), // 48: daemon.CleanStateResponse - (*DeleteStateRequest)(nil), // 49: daemon.DeleteStateRequest - (*DeleteStateResponse)(nil), // 50: daemon.DeleteStateResponse - (*SetSyncResponsePersistenceRequest)(nil), // 51: daemon.SetSyncResponsePersistenceRequest - (*SetSyncResponsePersistenceResponse)(nil), // 52: daemon.SetSyncResponsePersistenceResponse - (*TCPFlags)(nil), // 53: daemon.TCPFlags - (*TracePacketRequest)(nil), // 54: daemon.TracePacketRequest - (*TraceStage)(nil), // 55: daemon.TraceStage - (*TracePacketResponse)(nil), // 56: daemon.TracePacketResponse - (*SubscribeRequest)(nil), // 57: daemon.SubscribeRequest - (*SystemEvent)(nil), // 58: daemon.SystemEvent - (*GetEventsRequest)(nil), // 59: daemon.GetEventsRequest - (*GetEventsResponse)(nil), // 60: daemon.GetEventsResponse - (*SwitchProfileRequest)(nil), // 61: daemon.SwitchProfileRequest - (*SwitchProfileResponse)(nil), // 62: daemon.SwitchProfileResponse - (*SetConfigRequest)(nil), // 63: daemon.SetConfigRequest - (*SetConfigResponse)(nil), // 64: daemon.SetConfigResponse - (*AddProfileRequest)(nil), // 65: daemon.AddProfileRequest - (*AddProfileResponse)(nil), // 66: daemon.AddProfileResponse - (*RemoveProfileRequest)(nil), // 67: daemon.RemoveProfileRequest - (*RemoveProfileResponse)(nil), // 68: daemon.RemoveProfileResponse - (*ListProfilesRequest)(nil), // 69: daemon.ListProfilesRequest - (*ListProfilesResponse)(nil), // 70: daemon.ListProfilesResponse - (*Profile)(nil), // 71: daemon.Profile - (*GetActiveProfileRequest)(nil), // 72: daemon.GetActiveProfileRequest - (*GetActiveProfileResponse)(nil), // 73: daemon.GetActiveProfileResponse - (*LogoutRequest)(nil), // 74: daemon.LogoutRequest - (*LogoutResponse)(nil), // 75: daemon.LogoutResponse - (*GetFeaturesRequest)(nil), // 76: daemon.GetFeaturesRequest - (*GetFeaturesResponse)(nil), // 77: daemon.GetFeaturesResponse - (*GetPeerSSHHostKeyRequest)(nil), // 78: daemon.GetPeerSSHHostKeyRequest - (*GetPeerSSHHostKeyResponse)(nil), // 79: daemon.GetPeerSSHHostKeyResponse - (*RequestJWTAuthRequest)(nil), // 80: daemon.RequestJWTAuthRequest - (*RequestJWTAuthResponse)(nil), // 81: daemon.RequestJWTAuthResponse - (*WaitJWTTokenRequest)(nil), // 82: daemon.WaitJWTTokenRequest - (*WaitJWTTokenResponse)(nil), // 83: daemon.WaitJWTTokenResponse - (*StartCPUProfileRequest)(nil), // 84: daemon.StartCPUProfileRequest - (*StartCPUProfileResponse)(nil), // 85: daemon.StartCPUProfileResponse - (*StopCPUProfileRequest)(nil), // 86: daemon.StopCPUProfileRequest - (*StopCPUProfileResponse)(nil), // 87: daemon.StopCPUProfileResponse - (*InstallerResultRequest)(nil), // 88: daemon.InstallerResultRequest - (*InstallerResultResponse)(nil), // 89: daemon.InstallerResultResponse - (*ExposeServiceRequest)(nil), // 90: daemon.ExposeServiceRequest - (*ExposeServiceEvent)(nil), // 91: daemon.ExposeServiceEvent - (*ExposeServiceReady)(nil), // 92: daemon.ExposeServiceReady - nil, // 93: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 94: daemon.PortInfo.Range - nil, // 95: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 96: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 97: google.protobuf.Timestamp + (DaemonCertSigningType)(0), // 2: daemon.DaemonCertSigningType + (OSLifecycleRequest_CycleType)(0), // 3: daemon.OSLifecycleRequest.CycleType + (SystemEvent_Severity)(0), // 4: daemon.SystemEvent.Severity + (SystemEvent_Category)(0), // 5: daemon.SystemEvent.Category + (*EmptyRequest)(nil), // 6: daemon.EmptyRequest + (*OSLifecycleRequest)(nil), // 7: daemon.OSLifecycleRequest + (*OSLifecycleResponse)(nil), // 8: daemon.OSLifecycleResponse + (*LoginRequest)(nil), // 9: daemon.LoginRequest + (*LoginResponse)(nil), // 10: daemon.LoginResponse + (*WaitSSOLoginRequest)(nil), // 11: daemon.WaitSSOLoginRequest + (*WaitSSOLoginResponse)(nil), // 12: daemon.WaitSSOLoginResponse + (*UpRequest)(nil), // 13: daemon.UpRequest + (*UpResponse)(nil), // 14: daemon.UpResponse + (*StatusRequest)(nil), // 15: daemon.StatusRequest + (*StatusResponse)(nil), // 16: daemon.StatusResponse + (*DownRequest)(nil), // 17: daemon.DownRequest + (*DownResponse)(nil), // 18: daemon.DownResponse + (*GetConfigRequest)(nil), // 19: daemon.GetConfigRequest + (*GetConfigResponse)(nil), // 20: daemon.GetConfigResponse + (*PeerState)(nil), // 21: daemon.PeerState + (*LocalPeerState)(nil), // 22: daemon.LocalPeerState + (*SignalState)(nil), // 23: daemon.SignalState + (*ManagementState)(nil), // 24: daemon.ManagementState + (*RelayState)(nil), // 25: daemon.RelayState + (*NSGroupState)(nil), // 26: daemon.NSGroupState + (*SSHSessionInfo)(nil), // 27: daemon.SSHSessionInfo + (*SSHServerState)(nil), // 28: daemon.SSHServerState + (*FullStatus)(nil), // 29: daemon.FullStatus + (*ListNetworksRequest)(nil), // 30: daemon.ListNetworksRequest + (*ListNetworksResponse)(nil), // 31: daemon.ListNetworksResponse + (*SelectNetworksRequest)(nil), // 32: daemon.SelectNetworksRequest + (*SelectNetworksResponse)(nil), // 33: daemon.SelectNetworksResponse + (*IPList)(nil), // 34: daemon.IPList + (*Network)(nil), // 35: daemon.Network + (*PortInfo)(nil), // 36: daemon.PortInfo + (*ForwardingRule)(nil), // 37: daemon.ForwardingRule + (*ForwardingRulesResponse)(nil), // 38: daemon.ForwardingRulesResponse + (*DebugBundleRequest)(nil), // 39: daemon.DebugBundleRequest + (*DebugBundleResponse)(nil), // 40: daemon.DebugBundleResponse + (*GetLogLevelRequest)(nil), // 41: daemon.GetLogLevelRequest + (*GetLogLevelResponse)(nil), // 42: daemon.GetLogLevelResponse + (*SetLogLevelRequest)(nil), // 43: daemon.SetLogLevelRequest + (*SetLogLevelResponse)(nil), // 44: daemon.SetLogLevelResponse + (*State)(nil), // 45: daemon.State + (*ListStatesRequest)(nil), // 46: daemon.ListStatesRequest + (*ListStatesResponse)(nil), // 47: daemon.ListStatesResponse + (*CleanStateRequest)(nil), // 48: daemon.CleanStateRequest + (*CleanStateResponse)(nil), // 49: daemon.CleanStateResponse + (*DeleteStateRequest)(nil), // 50: daemon.DeleteStateRequest + (*DeleteStateResponse)(nil), // 51: daemon.DeleteStateResponse + (*SetSyncResponsePersistenceRequest)(nil), // 52: daemon.SetSyncResponsePersistenceRequest + (*SetSyncResponsePersistenceResponse)(nil), // 53: daemon.SetSyncResponsePersistenceResponse + (*TCPFlags)(nil), // 54: daemon.TCPFlags + (*TracePacketRequest)(nil), // 55: daemon.TracePacketRequest + (*TraceStage)(nil), // 56: daemon.TraceStage + (*TracePacketResponse)(nil), // 57: daemon.TracePacketResponse + (*SubscribeRequest)(nil), // 58: daemon.SubscribeRequest + (*SystemEvent)(nil), // 59: daemon.SystemEvent + (*GetEventsRequest)(nil), // 60: daemon.GetEventsRequest + (*GetEventsResponse)(nil), // 61: daemon.GetEventsResponse + (*SwitchProfileRequest)(nil), // 62: daemon.SwitchProfileRequest + (*SwitchProfileResponse)(nil), // 63: daemon.SwitchProfileResponse + (*SetConfigRequest)(nil), // 64: daemon.SetConfigRequest + (*SetConfigResponse)(nil), // 65: daemon.SetConfigResponse + (*AddProfileRequest)(nil), // 66: daemon.AddProfileRequest + (*AddProfileResponse)(nil), // 67: daemon.AddProfileResponse + (*RemoveProfileRequest)(nil), // 68: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 69: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 70: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 71: daemon.ListProfilesResponse + (*Profile)(nil), // 72: daemon.Profile + (*GetActiveProfileRequest)(nil), // 73: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 74: daemon.GetActiveProfileResponse + (*LogoutRequest)(nil), // 75: daemon.LogoutRequest + (*LogoutResponse)(nil), // 76: daemon.LogoutResponse + (*GetFeaturesRequest)(nil), // 77: daemon.GetFeaturesRequest + (*GetFeaturesResponse)(nil), // 78: daemon.GetFeaturesResponse + (*GetPeerSSHHostKeyRequest)(nil), // 79: daemon.GetPeerSSHHostKeyRequest + (*GetPeerSSHHostKeyResponse)(nil), // 80: daemon.GetPeerSSHHostKeyResponse + (*RequestJWTAuthRequest)(nil), // 81: daemon.RequestJWTAuthRequest + (*RequestJWTAuthResponse)(nil), // 82: daemon.RequestJWTAuthResponse + (*WaitJWTTokenRequest)(nil), // 83: daemon.WaitJWTTokenRequest + (*WaitJWTTokenResponse)(nil), // 84: daemon.WaitJWTTokenResponse + (*StartCPUProfileRequest)(nil), // 85: daemon.StartCPUProfileRequest + (*StartCPUProfileResponse)(nil), // 86: daemon.StartCPUProfileResponse + (*StopCPUProfileRequest)(nil), // 87: daemon.StopCPUProfileRequest + (*StopCPUProfileResponse)(nil), // 88: daemon.StopCPUProfileResponse + (*InstallerResultRequest)(nil), // 89: daemon.InstallerResultRequest + (*InstallerResultResponse)(nil), // 90: daemon.InstallerResultResponse + (*ExposeServiceRequest)(nil), // 91: daemon.ExposeServiceRequest + (*ExposeServiceEvent)(nil), // 92: daemon.ExposeServiceEvent + (*ExposeServiceReady)(nil), // 93: daemon.ExposeServiceReady + (*CertificateRequest)(nil), // 94: daemon.CertificateRequest + (*CertificateResponse)(nil), // 95: daemon.CertificateResponse + (*CertificateStatusRequest)(nil), // 96: daemon.CertificateStatusRequest + (*CertificateStatusResponse)(nil), // 97: daemon.CertificateStatusResponse + (*TrustCARequest)(nil), // 98: daemon.TrustCARequest + (*TrustCAResponse)(nil), // 99: daemon.TrustCAResponse + (*UntrustCARequest)(nil), // 100: daemon.UntrustCARequest + (*UntrustCAResponse)(nil), // 101: daemon.UntrustCAResponse + nil, // 102: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 103: daemon.PortInfo.Range + nil, // 104: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 105: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 106: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 2, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType - 96, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 28, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 97, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 97, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 96, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration - 26, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo - 23, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState - 22, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState - 21, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState - 20, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState - 24, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState - 25, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState - 58, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent - 27, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState - 34, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 93, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 94, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range - 35, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo - 35, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo - 36, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule - 0, // 21: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel - 0, // 22: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel - 44, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State - 53, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags - 55, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage - 3, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity - 4, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 97, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 95, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry - 58, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 96, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 71, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile - 1, // 33: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol - 92, // 34: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady - 33, // 35: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList - 8, // 36: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 10, // 37: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 12, // 38: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 14, // 39: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 16, // 40: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 18, // 41: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 29, // 42: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest - 31, // 43: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest - 31, // 44: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest - 5, // 45: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest - 38, // 46: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 40, // 47: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 42, // 48: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 45, // 49: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest - 47, // 50: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest - 49, // 51: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 51, // 52: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest - 54, // 53: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest - 57, // 54: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest - 59, // 55: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest - 61, // 56: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest - 63, // 57: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest - 65, // 58: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest - 67, // 59: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest - 69, // 60: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest - 72, // 61: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest - 74, // 62: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest - 76, // 63: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest - 78, // 64: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest - 80, // 65: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest - 82, // 66: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest - 84, // 67: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest - 86, // 68: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest - 6, // 69: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest - 88, // 70: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest - 90, // 71: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest - 9, // 72: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 11, // 73: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 13, // 74: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 15, // 75: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 17, // 76: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 19, // 77: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 30, // 78: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 32, // 79: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 32, // 80: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 37, // 81: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 39, // 82: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 41, // 83: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 43, // 84: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 46, // 85: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 48, // 86: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 50, // 87: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 52, // 88: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 56, // 89: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 58, // 90: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 60, // 91: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 62, // 92: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 64, // 93: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 66, // 94: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 68, // 95: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 70, // 96: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 73, // 97: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 75, // 98: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 77, // 99: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse - 79, // 100: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse - 81, // 101: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse - 83, // 102: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse - 85, // 103: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse - 87, // 104: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse - 7, // 105: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse - 89, // 106: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse - 91, // 107: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent - 72, // [72:108] is the sub-list for method output_type - 36, // [36:72] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 3, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType + 105, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 29, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus + 106, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 106, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 105, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 27, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo + 24, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState + 23, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState + 22, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState + 21, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState + 25, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState + 26, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState + 59, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent + 28, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState + 35, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network + 102, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 103, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 36, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo + 36, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo + 37, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule + 0, // 21: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel + 0, // 22: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel + 45, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State + 54, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags + 56, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage + 4, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity + 5, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category + 106, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 104, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 59, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent + 105, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 72, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile + 1, // 33: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol + 93, // 34: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady + 2, // 35: daemon.CertificateRequest.signing_type:type_name -> daemon.DaemonCertSigningType + 34, // 36: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList + 9, // 37: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 11, // 38: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 13, // 39: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 15, // 40: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 17, // 41: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 19, // 42: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 30, // 43: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest + 32, // 44: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest + 32, // 45: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest + 6, // 46: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest + 39, // 47: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 41, // 48: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 43, // 49: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 46, // 50: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 48, // 51: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 50, // 52: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 52, // 53: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest + 55, // 54: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest + 58, // 55: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest + 60, // 56: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest + 62, // 57: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest + 64, // 58: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest + 66, // 59: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest + 68, // 60: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest + 70, // 61: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest + 73, // 62: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest + 75, // 63: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest + 77, // 64: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest + 79, // 65: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest + 81, // 66: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest + 83, // 67: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest + 85, // 68: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest + 87, // 69: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest + 7, // 70: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest + 89, // 71: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest + 91, // 72: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest + 94, // 73: daemon.DaemonService.RequestCertificate:input_type -> daemon.CertificateRequest + 96, // 74: daemon.DaemonService.GetCertificateStatus:input_type -> daemon.CertificateStatusRequest + 98, // 75: daemon.DaemonService.TrustCA:input_type -> daemon.TrustCARequest + 100, // 76: daemon.DaemonService.UntrustCA:input_type -> daemon.UntrustCARequest + 10, // 77: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 12, // 78: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 14, // 79: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 16, // 80: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 18, // 81: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 20, // 82: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 31, // 83: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 33, // 84: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 33, // 85: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 38, // 86: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 40, // 87: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 42, // 88: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 44, // 89: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 47, // 90: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 49, // 91: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 51, // 92: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 53, // 93: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 57, // 94: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 59, // 95: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 61, // 96: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 63, // 97: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 65, // 98: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 67, // 99: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 69, // 100: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 71, // 101: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 74, // 102: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 76, // 103: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 78, // 104: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 80, // 105: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse + 82, // 106: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse + 84, // 107: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse + 86, // 108: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse + 88, // 109: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse + 8, // 110: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse + 90, // 111: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse + 92, // 112: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent + 95, // 113: daemon.DaemonService.RequestCertificate:output_type -> daemon.CertificateResponse + 97, // 114: daemon.DaemonService.GetCertificateStatus:output_type -> daemon.CertificateStatusResponse + 99, // 115: daemon.DaemonService.TrustCA:output_type -> daemon.TrustCAResponse + 101, // 116: daemon.DaemonService.UntrustCA:output_type -> daemon.UntrustCAResponse + 77, // [77:117] is the sub-list for method output_type + 37, // [37:77] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -6751,8 +7274,8 @@ func file_daemon_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), - NumEnums: 5, - NumMessages: 91, + NumEnums: 6, + NumMessages: 99, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 4dc41d401b6..ba282d82f36 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -106,6 +106,18 @@ service DaemonService { // ExposeService exposes a local port via the NetBird reverse proxy rpc ExposeService(ExposeServiceRequest) returns (stream ExposeServiceEvent) {} + + // RequestCertificate requests a TLS certificate for this peer + rpc RequestCertificate(CertificateRequest) returns (CertificateResponse) {} + + // GetCertificateStatus returns the current certificate status for this peer + rpc GetCertificateStatus(CertificateStatusRequest) returns (CertificateStatusResponse) {} + + // TrustCA installs the account CA into the OS trust store + rpc TrustCA(TrustCARequest) returns (TrustCAResponse) {} + + // UntrustCA removes the account CA from the OS trust store + rpc UntrustCA(UntrustCARequest) returns (UntrustCAResponse) {} } @@ -833,3 +845,44 @@ message ExposeServiceReady { string service_url = 2; string domain = 3; } + +enum DaemonCertSigningType { + DAEMON_CERT_SIGNING_INTERNAL = 0; + DAEMON_CERT_SIGNING_ACME = 1; +} + +message CertificateRequest { + DaemonCertSigningType signing_type = 1; + bool wildcard = 2; +} + +message CertificateResponse { + string cert_path = 1; + string key_path = 2; + repeated string dns_names = 3; + int64 expires_at = 4; +} + +message CertificateStatusRequest {} + +message CertificateStatusResponse { + bool has_certificate = 1; + repeated string dns_names = 2; + int64 expires_at = 3; + int64 issued_at = 4; + string issuer = 5; + bool ca_trusted = 6; + string cert_path = 7; + string key_path = 8; +} + +message TrustCARequest {} +message TrustCAResponse { + bool success = 1; + repeated string ca_fingerprints = 2; +} + +message UntrustCARequest {} +message UntrustCAResponse { + bool success = 1; +} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 4154dce592f..f57c8a2e22c 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -78,6 +78,14 @@ type DaemonServiceClient interface { GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error) // ExposeService exposes a local port via the NetBird reverse proxy ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error) + // RequestCertificate requests a TLS certificate for this peer + RequestCertificate(ctx context.Context, in *CertificateRequest, opts ...grpc.CallOption) (*CertificateResponse, error) + // GetCertificateStatus returns the current certificate status for this peer + GetCertificateStatus(ctx context.Context, in *CertificateStatusRequest, opts ...grpc.CallOption) (*CertificateStatusResponse, error) + // TrustCA installs the account CA into the OS trust store + TrustCA(ctx context.Context, in *TrustCARequest, opts ...grpc.CallOption) (*TrustCAResponse, error) + // UntrustCA removes the account CA from the OS trust store + UntrustCA(ctx context.Context, in *UntrustCARequest, opts ...grpc.CallOption) (*UntrustCAResponse, error) } type daemonServiceClient struct { @@ -458,6 +466,42 @@ func (x *daemonServiceExposeServiceClient) Recv() (*ExposeServiceEvent, error) { return m, nil } +func (c *daemonServiceClient) RequestCertificate(ctx context.Context, in *CertificateRequest, opts ...grpc.CallOption) (*CertificateResponse, error) { + out := new(CertificateResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/RequestCertificate", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) GetCertificateStatus(ctx context.Context, in *CertificateStatusRequest, opts ...grpc.CallOption) (*CertificateStatusResponse, error) { + out := new(CertificateStatusResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetCertificateStatus", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) TrustCA(ctx context.Context, in *TrustCARequest, opts ...grpc.CallOption) (*TrustCAResponse, error) { + out := new(TrustCAResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/TrustCA", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) UntrustCA(ctx context.Context, in *UntrustCARequest, opts ...grpc.CallOption) (*UntrustCAResponse, error) { + out := new(UntrustCAResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/UntrustCA", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -522,6 +566,14 @@ type DaemonServiceServer interface { GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) // ExposeService exposes a local port via the NetBird reverse proxy ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error + // RequestCertificate requests a TLS certificate for this peer + RequestCertificate(context.Context, *CertificateRequest) (*CertificateResponse, error) + // GetCertificateStatus returns the current certificate status for this peer + GetCertificateStatus(context.Context, *CertificateStatusRequest) (*CertificateStatusResponse, error) + // TrustCA installs the account CA into the OS trust store + TrustCA(context.Context, *TrustCARequest) (*TrustCAResponse, error) + // UntrustCA removes the account CA from the OS trust store + UntrustCA(context.Context, *UntrustCARequest) (*UntrustCAResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -637,6 +689,18 @@ func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *Ins func (UnimplementedDaemonServiceServer) ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error { return status.Errorf(codes.Unimplemented, "method ExposeService not implemented") } +func (UnimplementedDaemonServiceServer) RequestCertificate(context.Context, *CertificateRequest) (*CertificateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RequestCertificate not implemented") +} +func (UnimplementedDaemonServiceServer) GetCertificateStatus(context.Context, *CertificateStatusRequest) (*CertificateStatusResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCertificateStatus not implemented") +} +func (UnimplementedDaemonServiceServer) TrustCA(context.Context, *TrustCARequest) (*TrustCAResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TrustCA not implemented") +} +func (UnimplementedDaemonServiceServer) UntrustCA(context.Context, *UntrustCARequest) (*UntrustCAResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UntrustCA not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -1304,6 +1368,78 @@ func (x *daemonServiceExposeServiceServer) Send(m *ExposeServiceEvent) error { return x.ServerStream.SendMsg(m) } +func _DaemonService_RequestCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CertificateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).RequestCertificate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/RequestCertificate", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).RequestCertificate(ctx, req.(*CertificateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_GetCertificateStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CertificateStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).GetCertificateStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/GetCertificateStatus", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).GetCertificateStatus(ctx, req.(*CertificateStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_TrustCA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TrustCARequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).TrustCA(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/TrustCA", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).TrustCA(ctx, req.(*TrustCARequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_UntrustCA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UntrustCARequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).UntrustCA(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/UntrustCA", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).UntrustCA(ctx, req.(*UntrustCARequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1447,6 +1583,22 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetInstallerResult", Handler: _DaemonService_GetInstallerResult_Handler, }, + { + MethodName: "RequestCertificate", + Handler: _DaemonService_RequestCertificate_Handler, + }, + { + MethodName: "GetCertificateStatus", + Handler: _DaemonService_GetCertificateStatus_Handler, + }, + { + MethodName: "TrustCA", + Handler: _DaemonService_TrustCA_Handler, + }, + { + MethodName: "UntrustCA", + Handler: _DaemonService_UntrustCA_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/client/server/cert.go b/client/server/cert.go new file mode 100644 index 00000000000..5da87bc9ae4 --- /dev/null +++ b/client/server/cert.go @@ -0,0 +1,320 @@ +package server + +import ( + "context" + "crypto/ecdsa" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/cert" + "github.com/netbirdio/netbird/client/proto" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" +) + +// RequestCertificate handles a CLI request to generate a key, create a CSR, +// send it to management for signing, and store the resulting certificate. +func (s *Server) RequestCertificate(ctx context.Context, req *proto.CertificateRequest) (*proto.CertificateResponse, error) { + s.mutex.Lock() + if !s.clientRunning { + s.mutex.Unlock() + return nil, gstatus.Errorf(codes.FailedPrecondition, "client is not running, run 'netbird up' first") + } + connectClient := s.connectClient + s.mutex.Unlock() + + if connectClient == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "client not initialized") + } + + engine := connectClient.Engine() + if engine == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "engine not initialized") + } + + if s.certManager == nil { + return nil, gstatus.Errorf(codes.Internal, "certificate manager not available") + } + + fqdn := s.statusRecorder.GetLocalPeerState().FQDN + if fqdn == "" { + return nil, gstatus.Errorf(codes.FailedPrecondition, "peer FQDN not available, ensure peer is connected") + } + + key, err := s.certManager.GenerateKey() + if err != nil { + return nil, gstatus.Errorf(codes.Internal, "generate key: %v", err) + } + + csrDER, err := s.certManager.CreateCSR(key, fqdn, req.Wildcard) + if err != nil { + return nil, gstatus.Errorf(codes.Internal, "create CSR: %v", err) + } + + signingType := daemonSigningTypeToMgmt(req.SigningType) + + mgmClient := engine.GetMgmClient() + if mgmClient == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "management client not available") + } + + signCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + signResp, err := mgmClient.SignCertificate(signCtx, csrDER, signingType, req.Wildcard) + if err != nil { + return nil, gstatus.Errorf(codes.Internal, "sign certificate: %v", err) + } + + // Encode private key to PEM + ecKey, ok := key.(*ecdsa.PrivateKey) + if !ok { + return nil, gstatus.Errorf(codes.Internal, "unexpected key type") + } + keyDER, err := x509.MarshalECPrivateKey(ecKey) + if err != nil { + return nil, gstatus.Errorf(codes.Internal, "marshal private key: %v", err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + // Select cert and chain based on signing type + var certPEM, chainPEM []byte + switch signingType { + case mgmProto.CertSigningType_CERT_SIGNING_ACME: + certPEM = signResp.AcmeCertPem + chainPEM = signResp.AcmeChainPem + default: + certPEM = signResp.InternalCertPem + chainPEM = signResp.InternalChainPem + } + + if len(certPEM) == 0 { + return nil, gstatus.Errorf(codes.Internal, "sign certificate: empty certificate returned") + } + + if err := s.certManager.StoreCert(certPEM, chainPEM, keyPEM); err != nil { + return nil, gstatus.Errorf(codes.Internal, "store certificate: %v", err) + } + + // Parse cert to get DNS names for response + var dnsNames []string + var expiresAt int64 + if parsed, err := parsePEMCert(certPEM); err == nil { + dnsNames = parsed.DNSNames + expiresAt = parsed.NotAfter.Unix() + } else if signResp.ExpiresAt > 0 { + expiresAt = signResp.ExpiresAt + } + + log.Infof("certificate issued for %s, expires at %s", fqdn, time.Unix(expiresAt, 0).Format(time.RFC3339)) + + return &proto.CertificateResponse{ + CertPath: s.certManager.CertPath(), + KeyPath: s.certManager.KeyPath(), + DnsNames: dnsNames, + ExpiresAt: expiresAt, + }, nil +} + +// GetCertificateStatus returns the current certificate status for this peer. +func (s *Server) GetCertificateStatus(_ context.Context, _ *proto.CertificateStatusRequest) (*proto.CertificateStatusResponse, error) { + if s.certManager == nil { + return &proto.CertificateStatusResponse{HasCertificate: false}, nil + } + + if !s.certManager.HasCert() { + return &proto.CertificateStatusResponse{HasCertificate: false}, nil + } + + loaded, err := s.certManager.LoadCert() + if err != nil { + return nil, gstatus.Errorf(codes.Internal, "load certificate: %v", err) + } + + caTrusted := false + caPath := s.certManager.CAPath() + if caPEM, err := os.ReadFile(caPath); err == nil && len(caPEM) > 0 { + caTrusted = cert.IsCATrusted(caPEM) + } + + return &proto.CertificateStatusResponse{ + HasCertificate: true, + DnsNames: loaded.DNSNames, + ExpiresAt: loaded.NotAfter.Unix(), + IssuedAt: loaded.NotBefore.Unix(), + Issuer: loaded.Issuer.CommonName, + CaTrusted: caTrusted, + CertPath: s.certManager.CertPath(), + KeyPath: s.certManager.KeyPath(), + }, nil +} + +// TrustCA installs the account CA certificates into the OS trust store. +// If CA certificates are not yet synced locally, it fetches them from management. +func (s *Server) TrustCA(ctx context.Context, _ *proto.TrustCARequest) (*proto.TrustCAResponse, error) { + if s.certManager == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "certificate manager not available") + } + + caPath := s.certManager.CAPath() + caPEMData, err := os.ReadFile(caPath) + if err != nil { + // CA not synced yet — fetch from management + caPEMData, err = s.fetchAndStoreCA(ctx) + if err != nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "fetch CA certificates: %v", err) + } + } + + var fingerprints []string + rest := caPEMData + for { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + break + } + + singlePEM := pem.EncodeToMemory(block) + if err := cert.InstallCA(singlePEM); err != nil { + return nil, gstatus.Errorf(codes.Internal, "install CA: %v", err) + } + + fp := sha256.Sum256(block.Bytes) + fingerprints = append(fingerprints, hex.EncodeToString(fp[:])) + } + + if len(fingerprints) == 0 { + return nil, gstatus.Errorf(codes.FailedPrecondition, "no valid CA certificates found") + } + + log.Infof("installed %d CA certificate(s) into OS trust store", len(fingerprints)) + + return &proto.TrustCAResponse{ + Success: true, + CaFingerprints: fingerprints, + }, nil +} + +// UntrustCA removes the account CA certificates from the OS trust store. +func (s *Server) UntrustCA(_ context.Context, _ *proto.UntrustCARequest) (*proto.UntrustCAResponse, error) { + if s.certManager == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "certificate manager not available") + } + + caPath := s.certManager.CAPath() + caPEMData, err := os.ReadFile(caPath) + if err != nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "no CA certificates available") + } + + rest := caPEMData + var removed int + for { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + break + } + + singlePEM := pem.EncodeToMemory(block) + if err := cert.UninstallCA(singlePEM); err != nil { + log.Warnf("failed to remove CA from trust store: %v", err) + continue + } + removed++ + } + + log.Infof("removed %d CA certificate(s) from OS trust store", removed) + + return &proto.UntrustCAResponse{ + Success: true, + }, nil +} + +// fetchAndStoreCA fetches CA certificates from the management server and stores them locally. +func (s *Server) fetchAndStoreCA(ctx context.Context) ([]byte, error) { + s.mutex.Lock() + if !s.clientRunning { + s.mutex.Unlock() + return nil, fmt.Errorf("client is not running") + } + connectClient := s.connectClient + s.mutex.Unlock() + + if connectClient == nil { + return nil, fmt.Errorf("client not initialized") + } + + engine := connectClient.Engine() + if engine == nil { + return nil, fmt.Errorf("engine not initialized") + } + + mgmClient := engine.GetMgmClient() + if mgmClient == nil { + return nil, fmt.Errorf("management client not available") + } + + fetchCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + resp, err := mgmClient.GetCACertificates(fetchCtx) + if err != nil { + return nil, fmt.Errorf("get CA certificates: %w", err) + } + + if len(resp.Certificates) == 0 { + return nil, fmt.Errorf("no CA certificates configured on server") + } + + var caPEMs [][]byte + for _, c := range resp.Certificates { + if len(c.CertificatePem) > 0 { + caPEMs = append(caPEMs, c.CertificatePem) + } + } + + if len(caPEMs) == 0 { + return nil, fmt.Errorf("no valid CA certificates returned") + } + + if err := s.certManager.StoreCA(caPEMs); err != nil { + return nil, fmt.Errorf("store CA certificates: %w", err) + } + + // Read back the stored file + data, err := os.ReadFile(s.certManager.CAPath()) + if err != nil { + return nil, fmt.Errorf("read stored CA: %w", err) + } + + log.Infof("fetched and stored %d CA certificate(s) from management", len(caPEMs)) + return data, nil +} + +func daemonSigningTypeToMgmt(t proto.DaemonCertSigningType) mgmProto.CertSigningType { + switch t { + case proto.DaemonCertSigningType_DAEMON_CERT_SIGNING_ACME: + return mgmProto.CertSigningType_CERT_SIGNING_ACME + default: + return mgmProto.CertSigningType_CERT_SIGNING_INTERNAL + } +} + +func parsePEMCert(pemData []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(pemData) + if block == nil { + return nil, fmt.Errorf("no PEM block found") + } + return x509.ParseCertificate(block.Bytes) +} + diff --git a/client/server/server.go b/client/server/server.go index cab94238faa..5c1174fa721 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "runtime" "strconv" "sync" @@ -29,6 +30,7 @@ import ( "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/cert" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/version" @@ -89,7 +91,8 @@ type Server struct { sleepHandler *sleephandler.SleepHandler - jwtCache *jwtCache + jwtCache *jwtCache + certManager *cert.Manager } type oauthAuthFlow struct { @@ -114,6 +117,18 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable agent := &serverAgent{s} s.sleepHandler = sleephandler.New(agent) + certBaseDir := profilemanager.DefaultConfigPathDir + if configFile != "" { + certBaseDir = filepath.Dir(configFile) + } + certDir := filepath.Join(certBaseDir, "certs") + certMgr, err := cert.NewManager(certDir) + if err != nil { + log.Warnf("failed to initialize certificate manager: %v", err) + } else { + s.certManager = certMgr + } + return s } @@ -1615,6 +1630,7 @@ func (s *Server) connect(ctx context.Context, config *profilemanager.Config, sta log.Tracef("running client connection") s.connectClient = internal.NewConnectClient(ctx, config, statusRecorder, doInitialAutoUpdate) s.connectClient.SetSyncResponsePersistence(s.persistSyncResponse) + s.connectClient.SetCertManager(s.certManager) if err := s.connectClient.Run(runningChan, s.logFile); err != nil { return err } diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index 121c55ac5d2..475d5d48eb4 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -187,7 +187,11 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin account.InjectProxyPolicies(ctx) dnsCache := &cache.DNSConfigCache{} dnsDomain := c.GetDNSDomain(account.Settings) - peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain) + wildcardPeers, err := c.repo.GetPeersWithActiveWildcardCerts(ctx, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get wildcard peers: %v", err) + } + peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain, wildcardPeers) resourcePolicies := account.GetResourcePoliciesMap() routers := account.GetResourceRoutersMap() groupIDToUserIDs := account.GetActiveGroupUsers() @@ -258,7 +262,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin peerGroups := account.GetPeerGroups(p.ID) start = time.Now() - update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort) + update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort, nil) c.metrics.CountToSyncResponseDuration(time.Since(start)) c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{ @@ -343,7 +347,11 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe account.InjectProxyPolicies(ctx) dnsCache := &cache.DNSConfigCache{} dnsDomain := c.GetDNSDomain(account.Settings) - peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain) + wildcardPeers, err := c.repo.GetPeersWithActiveWildcardCerts(ctx, account.Id) + if err != nil { + log.WithContext(ctx).Errorf("failed to get wildcard peers: %v", err) + } + peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain, wildcardPeers) resourcePolicies := account.GetResourcePoliciesMap() routers := account.GetResourceRoutersMap() groupIDToUserIDs := account.GetActiveGroupUsers() @@ -390,7 +398,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe peerGroups := account.GetPeerGroups(peerId) dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion) - update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort) + update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort, nil) c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{ Update: update, MessageType: network_map.MessageTypeNetworkMap, @@ -480,7 +488,11 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr } dnsDomain := c.GetDNSDomain(account.Settings) - peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain) + wildcardPeers, err := c.repo.GetPeersWithActiveWildcardCerts(ctx, account.Id) + if err != nil { + log.WithContext(ctx).Errorf("failed to get wildcard peers: %v", err) + } + peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain, wildcardPeers) proxyNetworkMaps, err := c.proxyController.GetProxyNetworkMaps(ctx, account.Id, peer.ID, account.Peers) if err != nil { @@ -859,7 +871,11 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N } dnsDomain := c.GetDNSDomain(account.Settings) - peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain) + wildcardPeers, err := c.repo.GetPeersWithActiveWildcardCerts(ctx, account.Id) + if err != nil { + log.WithContext(ctx).Errorf("failed to get wildcard peers: %v", err) + } + peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain, wildcardPeers) proxyNetworkMaps, err := c.proxyController.GetProxyNetworkMaps(ctx, account.Id, peerID, account.Peers) if err != nil { diff --git a/management/internals/controllers/network_map/controller/repository.go b/management/internals/controllers/network_map/controller/repository.go index caef362cbdd..9aa84f38704 100644 --- a/management/internals/controllers/network_map/controller/repository.go +++ b/management/internals/controllers/network_map/controller/repository.go @@ -16,6 +16,7 @@ type Repository interface { GetPeersByIDs(ctx context.Context, accountID string, peerIDs []string) (map[string]*peer.Peer, error) GetPeerByID(ctx context.Context, accountID string, peerID string) (*peer.Peer, error) GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) + GetPeersWithActiveWildcardCerts(ctx context.Context, accountID string) (map[string]struct{}, error) } type repository struct { @@ -53,3 +54,7 @@ func (r *repository) GetPeerByID(ctx context.Context, accountID string, peerID s func (r *repository) GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) { return r.store.GetAccountZones(ctx, store.LockingStrengthNone, accountID) } + +func (r *repository) GetPeersWithActiveWildcardCerts(ctx context.Context, accountID string) (map[string]struct{}, error) { + return r.store.GetPeersWithActiveWildcardCerts(ctx, accountID) +} diff --git a/management/internals/server/boot.go b/management/internals/server/boot.go index 2049f0051aa..088f95e86e2 100644 --- a/management/internals/server/boot.go +++ b/management/internals/server/boot.go @@ -94,7 +94,7 @@ func (s *BaseServer) EventStore() activity.Store { func (s *BaseServer) APIHandler() http.Handler { return Create(s, func() http.Handler { - httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies) + httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies, s.CAManager()) if err != nil { log.Fatalf("failed to create API handler: %v", err) } @@ -157,6 +157,9 @@ func (s *BaseServer) GRPCServer() *grpc.Server { if serviceMgr != nil { serviceMgr.StartExposeReaper(context.Background()) } + + srv.SetCAManager(s.CAManager()) + mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv) mgmtProto.RegisterProxyServiceServer(gRPCAPIHandler, s.ReverseProxyGRPCServer()) diff --git a/management/internals/server/modules.go b/management/internals/server/modules.go index 2383019e2ae..967ab01afe5 100644 --- a/management/internals/server/modules.go +++ b/management/internals/server/modules.go @@ -26,6 +26,7 @@ import ( "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/routers" + "github.com/netbirdio/netbird/management/server/ca" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/users" @@ -208,6 +209,14 @@ func (s *BaseServer) ProxyManager() proxy.Manager { }) } +func (s *BaseServer) CAManager() *ca.Manager { + return Create(s, func() *ca.Manager { + mgr := ca.NewManager(s.AccountManager().GetStore()) + mgr.RegisterSigner(ca.NewACMEPersistSigner()) + return mgr + }) +} + func (s *BaseServer) ReverseProxyDomainManager() *manager.Manager { return Create(s, func() *manager.Manager { m := manager.NewManager(s.Store(), s.ProxyManager(), s.PermissionsManager()) diff --git a/management/internals/shared/grpc/cert_service.go b/management/internals/shared/grpc/cert_service.go new file mode 100644 index 00000000000..127ce015825 --- /dev/null +++ b/management/internals/shared/grpc/cert_service.go @@ -0,0 +1,230 @@ +package grpc + +import ( + "context" + "crypto/x509" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/ca" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/proto" +) + +// SetCAManager sets the CA manager on the server. +func (s *Server) SetCAManager(mgr *ca.Manager) { + s.caManager = mgr +} + +// SignCertificate handles a peer request to sign a certificate signing request. +func (s *Server) SignCertificate(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + signReq := &proto.SignCertificateRequest{} + peerKey, err := s.parseRequest(ctx, req, signReq) + if err != nil { + return nil, err + } + + accountID, peer, err := s.authenticateExposePeer(ctx, peerKey) + if err != nil { + return nil, err + } + + if s.caManager == nil { + return nil, status.Errorf(codes.Internal, "certificate authority not available") + } + + settings, err := s.accountManager.GetStore().GetAccountSettings(ctx, store.LockingStrengthNone, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get account settings: %v", err) + return nil, status.Errorf(codes.Internal, "failed to get account settings") + } + + if !settings.CertificateAuthorityEnabled { + return nil, status.Errorf(codes.FailedPrecondition, "certificate authority is not enabled for this account") + } + + wildcard := signReq.Wildcard + if wildcard && !settings.CertWildcardAllowed { + return nil, status.Errorf(codes.PermissionDenied, "wildcard certificates are not allowed for this account") + } + + csr, err := x509.ParseCertificateRequest(signReq.CsrDer) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid CSR: %v", err) + } + + // Validate that the CSR FQDN matches the requesting peer's actual FQDN. + // Without this check, a peer could request a cert for another peer's FQDN + // within the same account domain. + dnsDomain := s.networkMapController.GetDNSDomain(settings) + peerFQDN := peer.FQDN(dnsDomain) + if peerFQDN == "" { + return nil, status.Errorf(codes.FailedPrecondition, "peer has no FQDN configured") + } + + if err := validateCSRSANs(csr, peerFQDN, wildcard); err != nil { + return nil, err + } + + signingType := certSigningTypeToString(signReq.SigningType) + trigger := ca.TriggerManual + + // For peers with login expiration enabled, tie certificate validity to the session duration. + // Non-expiring peers get the default 90-day validity (certValidity=0). + var certValidity time.Duration + if peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled && settings.PeerLoginExpiration > 0 { + certValidity = settings.PeerLoginExpiration + } + + if err := s.caManager.CheckRateLimit(ctx, accountID, peer.ID, trigger, settings.CertRateLimitPerPeer); err != nil { + s.accountManager.StoreEvent(ctx, peer.ID, peer.ID, accountID, activity.CertificateRateLimited, peer.EventMeta(s.networkMapController.GetDNSDomain(settings))) + return nil, status.Errorf(codes.ResourceExhausted, "certificate rate limit exceeded") + } + + result, _, err := s.caManager.SignCertificate(ctx, ca.SignRequest{ + AccountID: accountID, + PeerID: peer.ID, + CSR: csr, + SigningType: signingType, + Wildcard: wildcard, + Trigger: trigger, + Validity: certValidity, + }) + if err != nil { + log.WithContext(ctx).Errorf("failed to sign certificate for peer %s: %v", peer.ID, err) + return nil, status.Errorf(codes.Internal, "failed to sign certificate") + } + + activityCode := activity.CertificateIssued + if wildcard { + activityCode = activity.CertificateWildcardIssued + } + s.accountManager.StoreEvent(ctx, peer.ID, peer.ID, accountID, activityCode, peer.EventMeta(s.networkMapController.GetDNSDomain(settings))) + + var expiresAt int64 + if notAfter, err := ca.NotAfterFromResult(result.CertPEM); err == nil { + expiresAt = notAfter.Unix() + } + + resp := &proto.SignCertificateResponse{ + ExpiresAt: expiresAt, + } + + switch signingType { + case ca.SigningTypeInternal: + resp.InternalCertPem = result.CertPEM + resp.InternalChainPem = result.ChainPEM + case ca.SigningTypeACME: + resp.AcmeCertPem = result.CertPEM + resp.AcmeChainPem = result.ChainPEM + } + + return s.encryptResponse(peerKey, resp) +} + +// GetCACertificates handles a peer request to get the active CA certificates. +func (s *Server) GetCACertificates(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + caReq := &proto.GetCACertificatesRequest{} + peerKey, err := s.parseRequest(ctx, req, caReq) + if err != nil { + return nil, err + } + + accountID, _, err := s.authenticateExposePeer(ctx, peerKey) + if err != nil { + return nil, err + } + + if s.caManager == nil { + return nil, status.Errorf(codes.Internal, "certificate authority not available") + } + + activeCAs, err := s.caManager.GetActiveCACertificates(ctx, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get CA certificates: %v", err) + return nil, status.Errorf(codes.Internal, "failed to get CA certificates") + } + + certs := make([]*proto.CACertificateInfo, 0, len(activeCAs)) + for _, c := range activeCAs { + certs = append(certs, &proto.CACertificateInfo{ + CertificatePem: []byte(c.CertificatePEM), + Fingerprint: c.Fingerprint, + IsActive: c.IsActive, + NotAfter: c.NotAfter.Unix(), + }) + } + + return s.encryptResponse(peerKey, &proto.GetCACertificatesResponse{ + Certificates: certs, + }) +} + +// getActiveCACertsPEM returns the PEM-encoded CA certificates for an account. +// Returns nil if the CA is not enabled or no active CAs exist. +func (s *Server) getActiveCACertsPEM(ctx context.Context, accountID string, settings *types.Settings) [][]byte { + if s.caManager == nil || settings == nil || !settings.CertificateAuthorityEnabled { + return nil + } + + activeCAs, err := s.caManager.GetActiveCACertificates(ctx, accountID) + if err != nil { + log.WithContext(ctx).Warnf("failed to get CA certificates for sync: %v", err) + return nil + } + + certs := make([][]byte, 0, len(activeCAs)) + for _, c := range activeCAs { + certs = append(certs, []byte(c.CertificatePEM)) + } + return certs +} + +// validateCSRSANs ensures the CSR contains only the expected peer FQDN (and wildcard if requested). +func validateCSRSANs(csr *x509.CertificateRequest, peerFQDN string, wildcard bool) error { + if len(csr.IPAddresses) > 0 { + return status.Errorf(codes.InvalidArgument, "CSR must not contain IP SANs") + } + if len(csr.EmailAddresses) > 0 { + return status.Errorf(codes.InvalidArgument, "CSR must not contain Email SANs") + } + if len(csr.URIs) > 0 { + return status.Errorf(codes.InvalidArgument, "CSR must not contain URI SANs") + } + + lowerFQDN := strings.ToLower(peerFQDN) + expected := map[string]struct{}{lowerFQDN: {}} + if wildcard { + expected["*."+lowerFQDN] = struct{}{} + } + if len(csr.DNSNames) != len(expected) { + return status.Errorf(codes.InvalidArgument, "CSR SAN set is invalid for peer FQDN %q", peerFQDN) + } + seen := make(map[string]struct{}, len(csr.DNSNames)) + for _, name := range csr.DNSNames { + lower := strings.ToLower(name) + if _, ok := expected[lower]; !ok { + return status.Errorf(codes.InvalidArgument, "CSR SAN %q is not allowed", name) + } + if _, dup := seen[lower]; dup { + return status.Errorf(codes.InvalidArgument, "CSR contains duplicate SAN %q", name) + } + seen[lower] = struct{}{} + } + return nil +} + +func certSigningTypeToString(t proto.CertSigningType) string { + switch t { + case proto.CertSigningType_CERT_SIGNING_ACME: + return ca.SigningTypeACME + default: + return ca.SigningTypeInternal + } +} diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index c74fa2660c7..2e1b39b862e 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -88,7 +88,7 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken return nbConfig } -func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool) *proto.PeerConfig { +func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, caCertsPEM [][]byte) *proto.PeerConfig { netmask, _ := network.Net.Mask.Size() fqdn := peer.FQDN(dnsName) @@ -109,17 +109,18 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, set AutoUpdate: &proto.AutoUpdateSettings{ Version: settings.AutoUpdateVersion, }, + CaCertificatesPem: caCertsPEM, } } -func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse { +func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64, caCertsPEM [][]byte) *proto.SyncResponse { response := &proto.SyncResponse{ - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH), + PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, caCertsPEM), NetworkMap: &proto.NetworkMap{ Serial: networkMap.Network.CurrentSerial(), Routes: toProtocolRoutes(networkMap.Routes), DNSConfig: toProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort), - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH), + PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, caCertsPEM), }, Checks: toProtocolChecks(ctx, checks), } diff --git a/management/internals/shared/grpc/server.go b/management/internals/shared/grpc/server.go index a07cafe90f8..877a0a16fa9 100644 --- a/management/internals/shared/grpc/server.go +++ b/management/internals/shared/grpc/server.go @@ -27,6 +27,7 @@ import ( "github.com/netbirdio/netbird/management/internals/controllers/network_map" rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" + "github.com/netbirdio/netbird/management/server/ca" nbconfig "github.com/netbirdio/netbird/management/internals/server/config" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/job" @@ -84,6 +85,8 @@ type Server struct { reverseProxyManager rpservice.Manager reverseProxyMu sync.RWMutex + + caManager *ca.Manager } // NewServer creates a new Management server @@ -824,7 +827,7 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil), - PeerConfig: toPeerConfig(peer, netMap.Network, s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, netMap.EnableSSH), + PeerConfig: toPeerConfig(peer, netMap.Network, s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, netMap.EnableSSH, s.getActiveCACertsPEM(ctx, peer.AccountID, settings)), Checks: toProtocolChecks(ctx, postureChecks), } @@ -891,7 +894,7 @@ func (s *Server) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, peer return status.Errorf(codes.Internal, "failed to get peer groups %s", err) } - plainResp := ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, s.networkMapController.GetDNSDomain(settings), postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort) + plainResp := ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, s.networkMapController.GetDNSDomain(settings), postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort, s.getActiveCACertsPEM(ctx, peer.AccountID, settings)) key, err := s.secretsManager.GetWGKey() if err != nil { diff --git a/management/server/account.go b/management/server/account.go index 01d0eebfa1e..1b332308c5d 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -350,6 +350,17 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco newSettings.Extra = oldSettings.Extra } + // Preserve internal-only CA settings not exposed in the API schema. + // These are managed by the CA handler endpoints, not the account update. + newSettings.CertificateAuthorityEnabled = oldSettings.CertificateAuthorityEnabled + newSettings.CertCAValidity = oldSettings.CertCAValidity + newSettings.CertCADisplayName = oldSettings.CertCADisplayName + newSettings.CertCAOrganization = oldSettings.CertCAOrganization + newSettings.CertDefaultValidity = oldSettings.CertDefaultValidity + newSettings.CertRateLimitPerPeer = oldSettings.CertRateLimitPerPeer + newSettings.CertACMEEnabled = oldSettings.CertACMEEnabled + newSettings.CertACMECAURL = oldSettings.CertACMECAURL + if err = transaction.SaveAccountSettings(ctx, accountID, newSettings); err != nil { return err } diff --git a/management/server/account_test.go b/management/server/account_test.go index a073d4fca7d..71803d6512c 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -405,7 +405,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { validatedPeers[p] = struct{}{} } - customZone := account.GetPeersCustomZone(context.Background(), "netbird.io") + customZone := account.GetPeersCustomZone(context.Background(), "netbird.io", nil) networkMap := account.GetPeerNetworkMap(context.Background(), testCase.peerID, customZone, nil, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil, account.GetActiveGroupUsers()) assert.Len(t, networkMap.Peers, len(testCase.expectedPeers)) assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers)) diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 53cf30d4c5d..870ddc05905 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -220,6 +220,27 @@ const ( // AccountPeerExposeDisabled indicates that a user disabled peer expose for the account AccountPeerExposeDisabled Activity = 115 + // CertificateAuthorityCreated indicates that a CA was created for the account + CertificateAuthorityCreated Activity = 116 + // CertificateAuthorityRotated indicates that the CA was rotated + CertificateAuthorityRotated Activity = 117 + // CertificateAuthorityDeactivated indicates that a CA was deactivated + CertificateAuthorityDeactivated Activity = 118 + // CertificateIssued indicates that a certificate was issued to a peer + CertificateIssued Activity = 119 + // CertificateRenewed indicates that a certificate was renewed for a peer + CertificateRenewed Activity = 120 + // CertificateRevoked indicates that a certificate was revoked + CertificateRevoked Activity = 121 + // CertificateExpiring indicates that a certificate is expiring soon + CertificateExpiring Activity = 122 + // CertificateExpired indicates that a certificate has expired + CertificateExpired Activity = 123 + // CertificateRateLimited indicates that a peer was rate-limited for certificate issuance + CertificateRateLimited Activity = 124 + // CertificateWildcardIssued indicates that a wildcard certificate was issued + CertificateWildcardIssued Activity = 125 + AccountDeleted Activity = 99999 ) @@ -364,6 +385,17 @@ var activityMap = map[Activity]Code{ AccountPeerExposeEnabled: {"Account peer expose enabled", "account.setting.peer.expose.enable"}, AccountPeerExposeDisabled: {"Account peer expose disabled", "account.setting.peer.expose.disable"}, + + CertificateAuthorityCreated: {"Certificate authority created", "certificate.authority.create"}, + CertificateAuthorityRotated: {"Certificate authority rotated", "certificate.authority.rotate"}, + CertificateAuthorityDeactivated: {"Certificate authority deactivated", "certificate.authority.deactivate"}, + CertificateIssued: {"Certificate issued", "certificate.issue"}, + CertificateRenewed: {"Certificate renewed", "certificate.renew"}, + CertificateRevoked: {"Certificate revoked", "certificate.revoke"}, + CertificateExpiring: {"Certificate expiring", "certificate.expiring"}, + CertificateExpired: {"Certificate expired", "certificate.expired"}, + CertificateRateLimited: {"Certificate rate limited", "certificate.rate.limit"}, + CertificateWildcardIssued: {"Wildcard certificate issued", "certificate.wildcard.issue"}, } // StringCode returns a string code of the activity diff --git a/management/server/ca/acme_stub.go b/management/server/ca/acme_stub.go new file mode 100644 index 00000000000..da3d29dd09f --- /dev/null +++ b/management/server/ca/acme_stub.go @@ -0,0 +1,31 @@ +package ca + +import ( + "context" + "crypto/x509" + "fmt" +) + +const ( + // SigningTypeACME is the identifier for the ACME DNS-PERSIST-01 signer. + SigningTypeACME = "acme" +) + +// ACMEPersistSigner is a stub implementation for the future ACME DNS-PERSIST-01 signing backend. +// It returns an error indicating the feature is not yet available. +type ACMEPersistSigner struct{} + +// NewACMEPersistSigner creates a new ACMEPersistSigner stub. +func NewACMEPersistSigner() *ACMEPersistSigner { + return &ACMEPersistSigner{} +} + +// Sign is not implemented and returns an error. +func (s *ACMEPersistSigner) Sign(_ context.Context, _ *x509.CertificateRequest, _ string, _ bool) (*SigningResult, error) { + return nil, fmt.Errorf("ACME DNS-PERSIST-01 signing is not yet available") +} + +// Type returns the signer type identifier. +func (s *ACMEPersistSigner) Type() string { + return SigningTypeACME +} diff --git a/management/server/ca/internal_ca.go b/management/server/ca/internal_ca.go new file mode 100644 index 00000000000..d61cc357f5f --- /dev/null +++ b/management/server/ca/internal_ca.go @@ -0,0 +1,346 @@ +package ca + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "fmt" + "math/big" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + // SigningTypeInternal is the identifier for the internal CA signer. + SigningTypeInternal = "internal" + + // DefaultCAValidity is the default validity period for a root CA certificate. + DefaultCAValidity = 10 * 365 * 24 * time.Hour + + // defaultCertValidity is the default validity for issued peer certificates. + defaultCertValidity = 90 * 24 * time.Hour + + // defaultCAOrganization is the fallback organization when none is provided. + defaultCAOrganization = "NetBird Self-Hosted" +) + +// CAOptions configures the generated CA certificate's subject and validity. +type CAOptions struct { + // DisplayName is used in the CA common name (e.g. "Zakhar" → "Zakhar Internal CA"). + // Falls back to the DNS domain if empty. + DisplayName string + + // Organization is the x509 Organization field. Falls back to "NetBird Self-Hosted". + Organization string + + // Validity overrides the CA certificate lifetime. Falls back to DefaultCAValidity (10 years). + Validity time.Duration +} + +// InternalCASigner signs CSRs using an internally managed root CA. +// The root CA is an ECDSA P-256 self-signed certificate with x509 NameConstraints +// limiting issuance to the account's DNS domain. +type InternalCASigner struct { + caCert *x509.Certificate + caKey *ecdsa.PrivateKey + caID string + validity time.Duration +} + +// NewInternalCASigner creates a signer from an existing CA certificate and private key in PEM format. +// validity controls how long issued peer certificates are valid. +func NewInternalCASigner(certPEM, keyPEM []byte, caID string, validity time.Duration) (*InternalCASigner, error) { + cert, err := parseCertificatePEM(certPEM) + if err != nil { + return nil, fmt.Errorf("parse CA certificate: %w", err) + } + + key, err := parseECPrivateKeyPEM(keyPEM) + if err != nil { + return nil, fmt.Errorf("parse CA private key: %w", err) + } + + if validity <= 0 { + validity = defaultCertValidity + } + + certPubKey, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("CA certificate does not contain an ECDSA public key") + } + if !certPubKey.Equal(&key.PublicKey) { + return nil, fmt.Errorf("CA certificate and private key do not match") + } + + return &InternalCASigner{ + caCert: cert, + caKey: key, + caID: caID, + validity: validity, + }, nil +} + +// Sign signs the given CSR and returns the issued certificate and CA chain in PEM format. +// It validates that the CSR's DNS names match the expected peer FQDN and optionally adds +// a wildcard SAN. +func (s *InternalCASigner) Sign(ctx context.Context, csr *x509.CertificateRequest, peerFQDN string, wildcard bool) (*SigningResult, error) { + if csr == nil { + return nil, fmt.Errorf("CSR is nil") + } + + if err := csr.CheckSignature(); err != nil { + return nil, fmt.Errorf("invalid CSR signature: %w", err) + } + + if err := s.validateCSRNames(csr, peerFQDN, wildcard); err != nil { + return nil, err + } + + dnsNames := []string{peerFQDN} + if wildcard { + dnsNames = append(dnsNames, "*."+peerFQDN) + } + + serialNumber, err := generateSerialNumber() + if err != nil { + return nil, fmt.Errorf("generate serial number: %w", err) + } + + now := time.Now().UTC() + notAfter := now.Add(s.validity) + if notAfter.After(s.caCert.NotAfter) { + notAfter = s.caCert.NotAfter + } + if !notAfter.After(now) { + return nil, fmt.Errorf("CA certificate is expired or has insufficient remaining validity") + } + template := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: peerFQDN, + }, + DNSNames: dnsNames, + NotBefore: now, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, s.caCert, csr.PublicKey, s.caKey) + if err != nil { + return nil, fmt.Errorf("create certificate: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + chainPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: s.caCert.Raw}) + + log.WithContext(ctx).Debugf("signed certificate for %s (serial: %s, wildcard: %v)", peerFQDN, serialNumber.Text(16), wildcard) + + return &SigningResult{ + CertPEM: certPEM, + ChainPEM: chainPEM, + }, nil +} + +// Type returns the signer type identifier. +func (s *InternalCASigner) Type() string { + return SigningTypeInternal +} + +// CAID returns the CA certificate ID used by this signer. +func (s *InternalCASigner) CAID() string { + return s.caID +} + +// SerialNumberFromResult parses the serial number from a signed certificate PEM. +func SerialNumberFromResult(certPEM []byte) (string, error) { + cert, err := parseCertificatePEM(certPEM) + if err != nil { + return "", fmt.Errorf("parse issued certificate: %w", err) + } + return cert.SerialNumber.Text(16), nil +} + +// NotAfterFromResult parses the NotAfter timestamp from a signed certificate PEM. +func NotAfterFromResult(certPEM []byte) (time.Time, error) { + cert, err := parseCertificatePEM(certPEM) + if err != nil { + return time.Time{}, fmt.Errorf("parse issued certificate: %w", err) + } + return cert.NotAfter, nil +} + +// GenerateCAResult holds the output of GenerateCA including the resolved subject fields. +type GenerateCAResult struct { + CertPEM []byte + KeyPEM []byte + Fingerprint string + DisplayName string // the resolved CN (after applying defaults) + Organization string // the resolved O (after applying defaults) +} + +// GenerateCA creates a new ECDSA P-256 self-signed root CA certificate with +// NameConstraints limiting issuance to the given DNS domain. +// Returns the certificate and key in PEM format, the SHA-256 fingerprint, and the resolved subject fields. +func GenerateCA(dnsDomain string, opts CAOptions) (*GenerateCAResult, error) { + if dnsDomain == "" { + return nil, fmt.Errorf("dnsDomain is required for CA generation") + } + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate CA key: %w", err) + } + + serialNumber, err := generateSerialNumber() + if err != nil { + return nil, fmt.Errorf("generate serial number: %w", err) + } + + // Generate a short unique suffix from the serial number for default names. + // This helps distinguish multiple CA instances on the same domain. + serialBytes := serialNumber.FillBytes(make([]byte, 16)) + suffix := fmt.Sprintf("%06x", serialBytes[:3]) + + var cn string + if opts.DisplayName != "" { + cn = opts.DisplayName + " Internal CA" + } else { + cn = fmt.Sprintf("%s Internal CA (%s)", dnsDomain, suffix) + } + + org := defaultCAOrganization + if opts.Organization != "" { + org = opts.Organization + } + + validity := DefaultCAValidity + if opts.Validity > 0 { + validity = opts.Validity + } + + now := time.Now().UTC() + template := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: cn, + Organization: []string{org}, + }, + NotBefore: now, + NotAfter: now.Add(validity), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 0, + MaxPathLenZero: true, + PermittedDNSDomainsCritical: true, + PermittedDNSDomains: []string{"." + dnsDomain, dnsDomain}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return nil, fmt.Errorf("create CA certificate: %w", err) + } + + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + return nil, fmt.Errorf("marshal CA private key: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + hash := sha256.Sum256(certDER) + fingerprint := hex.EncodeToString(hash[:]) + + return &GenerateCAResult{ + CertPEM: certPEM, + KeyPEM: keyPEM, + Fingerprint: fingerprint, + DisplayName: cn, + Organization: org, + }, nil +} + +// Fingerprint computes the SHA-256 fingerprint of a PEM-encoded certificate. +func Fingerprint(certPEM []byte) (string, error) { + cert, err := parseCertificatePEM(certPEM) + if err != nil { + return "", fmt.Errorf("parse certificate for fingerprint: %w", err) + } + hash := sha256.Sum256(cert.Raw) + return hex.EncodeToString(hash[:]), nil +} + +// validateCSRNames checks that the CSR DNS names match the expected FQDN. +func (s *InternalCASigner) validateCSRNames(csr *x509.CertificateRequest, peerFQDN string, wildcard bool) error { + if len(csr.DNSNames) == 0 { + return fmt.Errorf("CSR must contain at least one DNS name") + } + + expectedNames := map[string]bool{strings.ToLower(peerFQDN): true} + if wildcard { + expectedNames["*."+strings.ToLower(peerFQDN)] = true + } + + for _, name := range csr.DNSNames { + if !expectedNames[strings.ToLower(name)] { + return fmt.Errorf("CSR contains unexpected DNS name %q, expected %v", name, peerFQDN) + } + } + + if !containsName(csr.DNSNames, peerFQDN) { + return fmt.Errorf("CSR must contain the peer FQDN %q", peerFQDN) + } + + return nil +} + +func containsName(names []string, target string) bool { + for _, n := range names { + if strings.EqualFold(n, target) { + return true + } + } + return false +} + +func generateSerialNumber() (*big.Int, error) { + max := new(big.Int).Lsh(big.NewInt(1), 128) + for { + serial, err := rand.Int(rand.Reader, max) + if err != nil { + return nil, fmt.Errorf("generate random serial: %w", err) + } + if serial.Sign() > 0 { + return serial, nil + } + } +} + +func parseCertificatePEM(data []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(data) + if block == nil || block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("failed to decode PEM certificate block") + } + return x509.ParseCertificate(block.Bytes) +} + +func parseECPrivateKeyPEM(data []byte) (*ecdsa.PrivateKey, error) { + block, _ := pem.Decode(data) + if block == nil || block.Type != "EC PRIVATE KEY" { + return nil, fmt.Errorf("failed to decode PEM EC private key block") + } + return x509.ParseECPrivateKey(block.Bytes) +} diff --git a/management/server/ca/internal_ca_test.go b/management/server/ca/internal_ca_test.go new file mode 100644 index 00000000000..e100a3c983b --- /dev/null +++ b/management/server/ca/internal_ca_test.go @@ -0,0 +1,323 @@ +package ca + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateCA(t *testing.T) { + result, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + require.NotEmpty(t, result.CertPEM) + require.NotEmpty(t, result.KeyPEM) + require.NotEmpty(t, result.Fingerprint) + + cert, err := parseCertificatePEM(result.CertPEM) + require.NoError(t, err) + + assert.True(t, cert.IsCA) + assert.Contains(t, cert.Subject.CommonName, "netbird.example Internal CA (") + assert.Equal(t, []string{"NetBird Self-Hosted"}, cert.Subject.Organization) + assert.True(t, cert.BasicConstraintsValid) + assert.Equal(t, 0, cert.MaxPathLen) + assert.True(t, cert.MaxPathLenZero) + assert.Contains(t, cert.PermittedDNSDomains, ".netbird.example") + assert.Contains(t, cert.PermittedDNSDomains, "netbird.example") + + key, err := parseECPrivateKeyPEM(result.KeyPEM) + require.NoError(t, err) + assert.Equal(t, elliptic.P256(), key.Curve) +} + +func TestGenerateCA_Fingerprint(t *testing.T) { + result, err := GenerateCA("test.example", CAOptions{}) + require.NoError(t, err) + + fingerprint2, err := Fingerprint(result.CertPEM) + require.NoError(t, err) + + assert.Equal(t, result.Fingerprint, fingerprint2) + assert.Len(t, result.Fingerprint, 64) // SHA-256 hex = 64 chars +} + +func TestGenerateCA_CustomOptions(t *testing.T) { + result, err := GenerateCA("zakhar.internal", CAOptions{ + DisplayName: "Zakhar", + Organization: "Zakhar Corp", + Validity: 5 * 365 * 24 * time.Hour, + }) + require.NoError(t, err) + + cert, err := parseCertificatePEM(result.CertPEM) + require.NoError(t, err) + + assert.Equal(t, "Zakhar Internal CA", cert.Subject.CommonName) + assert.Equal(t, []string{"Zakhar Corp"}, cert.Subject.Organization) + + // 5 years validity ≈ 1825 days, allow a small delta + validity := cert.NotAfter.Sub(cert.NotBefore) + assert.InDelta(t, (5 * 365 * 24 * time.Hour).Hours(), validity.Hours(), 24) +} + +func TestGenerateCA_EmptyDomainReturnsError(t *testing.T) { + _, err := GenerateCA("", CAOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "dnsDomain is required") +} + +func TestGenerateCA_DomainOnlyFallback(t *testing.T) { + // No display name or org → CN falls back to domain + unique suffix, org to default + result, err := GenerateCA("mynetwork.selfhosted", CAOptions{}) + require.NoError(t, err) + + cert, err := parseCertificatePEM(result.CertPEM) + require.NoError(t, err) + + assert.Contains(t, cert.Subject.CommonName, "mynetwork.selfhosted Internal CA (") + assert.Len(t, cert.Subject.CommonName, len("mynetwork.selfhosted Internal CA (")+len("abcdef)")) + assert.Equal(t, []string{"NetBird Self-Hosted"}, cert.Subject.Organization) +} + +func TestGenerateCA_CustomNameNoSuffix(t *testing.T) { + // When DisplayName is provided, no suffix is added + result, err := GenerateCA("zakhar.internal", CAOptions{DisplayName: "Zakhar"}) + require.NoError(t, err) + + cert, err := parseCertificatePEM(result.CertPEM) + require.NoError(t, err) + + assert.Equal(t, "Zakhar Internal CA", cert.Subject.CommonName) + assert.NotContains(t, cert.Subject.CommonName, "(") +} + +func TestInternalCASigner_Sign(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "peer1.netbird.example"}, + DNSNames: []string{"peer1.netbird.example"}, + }, csrKey) + require.NoError(t, err) + + parsedCSR, err := x509.ParseCertificateRequest(csr) + require.NoError(t, err) + + result, err := signer.Sign(context.Background(), parsedCSR, "peer1.netbird.example", false) + require.NoError(t, err) + require.NotNil(t, result) + require.NotEmpty(t, result.CertPEM) + require.NotEmpty(t, result.ChainPEM) + + issuedCert, err := parseCertificatePEM(result.CertPEM) + require.NoError(t, err) + + assert.Equal(t, "peer1.netbird.example", issuedCert.Subject.CommonName) + assert.Equal(t, []string{"peer1.netbird.example"}, issuedCert.DNSNames) + assert.Contains(t, issuedCert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + assert.Contains(t, issuedCert.ExtKeyUsage, x509.ExtKeyUsageClientAuth) +} + +func TestInternalCASigner_SignWildcard(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "peer1.netbird.example"}, + DNSNames: []string{"peer1.netbird.example", "*.peer1.netbird.example"}, + }, csrKey) + require.NoError(t, err) + + parsedCSR, err := x509.ParseCertificateRequest(csr) + require.NoError(t, err) + + result, err := signer.Sign(context.Background(), parsedCSR, "peer1.netbird.example", true) + require.NoError(t, err) + + issuedCert, err := parseCertificatePEM(result.CertPEM) + require.NoError(t, err) + + assert.Contains(t, issuedCert.DNSNames, "peer1.netbird.example") + assert.Contains(t, issuedCert.DNSNames, "*.peer1.netbird.example") +} + +func TestInternalCASigner_SignRejectsMismatchedFQDN(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "attacker.evil.com"}, + DNSNames: []string{"attacker.evil.com"}, + }, csrKey) + require.NoError(t, err) + + parsedCSR, err := x509.ParseCertificateRequest(csr) + require.NoError(t, err) + + _, err = signer.Sign(context.Background(), parsedCSR, "peer1.netbird.example", false) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unexpected DNS name") +} + +func TestInternalCASigner_SignRejectsWildcardWhenNotRequested(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "peer1.netbird.example"}, + DNSNames: []string{"peer1.netbird.example", "*.peer1.netbird.example"}, + }, csrKey) + require.NoError(t, err) + + parsedCSR, err := x509.ParseCertificateRequest(csr) + require.NoError(t, err) + + // wildcard=false but CSR contains wildcard + _, err = signer.Sign(context.Background(), parsedCSR, "peer1.netbird.example", false) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unexpected DNS name") +} + +func TestInternalCASigner_SignRejectsEmptyDNSNames(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "peer1.netbird.example"}, + }, csrKey) + require.NoError(t, err) + + parsedCSR, err := x509.ParseCertificateRequest(csr) + require.NoError(t, err) + + _, err = signer.Sign(context.Background(), parsedCSR, "peer1.netbird.example", false) + assert.Error(t, err) + assert.Contains(t, err.Error(), "at least one DNS name") +} + +func TestInternalCASigner_Type(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + assert.Equal(t, SigningTypeInternal, signer.Type()) +} + +func TestInternalCASigner_CertificateChainVerifies(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "peer1.netbird.example"}, + DNSNames: []string{"peer1.netbird.example"}, + }, csrKey) + require.NoError(t, err) + + parsedCSR, err := x509.ParseCertificateRequest(csr) + require.NoError(t, err) + + result, err := signer.Sign(context.Background(), parsedCSR, "peer1.netbird.example", false) + require.NoError(t, err) + + // Parse the CA cert and issued cert + caCert, err := parseCertificatePEM(result.ChainPEM) + require.NoError(t, err) + + issuedCert, err := parseCertificatePEM(result.CertPEM) + require.NoError(t, err) + + // Build verification pool + roots := x509.NewCertPool() + roots.AddCert(caCert) + + opts := x509.VerifyOptions{ + Roots: roots, + DNSName: "peer1.netbird.example", + } + + chains, err := issuedCert.Verify(opts) + require.NoError(t, err) + assert.NotEmpty(t, chains) +} + +func TestSerialNumberFromResult(t *testing.T) { + caResult, err := GenerateCA("netbird.example", CAOptions{}) + require.NoError(t, err) + certPEM, keyPEM := caResult.CertPEM, caResult.KeyPEM + + signer, err := NewInternalCASigner(certPEM, keyPEM, "test-ca-id", 0) + require.NoError(t, err) + + csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "peer1.netbird.example"}, + DNSNames: []string{"peer1.netbird.example"}, + }, csrKey) + require.NoError(t, err) + + parsedCSR, err := x509.ParseCertificateRequest(csr) + require.NoError(t, err) + + result, err := signer.Sign(context.Background(), parsedCSR, "peer1.netbird.example", false) + require.NoError(t, err) + + serial, err := SerialNumberFromResult(result.CertPEM) + require.NoError(t, err) + assert.NotEmpty(t, serial) +} diff --git a/management/server/ca/manager.go b/management/server/ca/manager.go new file mode 100644 index 00000000000..73086dc77ac --- /dev/null +++ b/management/server/ca/manager.go @@ -0,0 +1,253 @@ +package ca + +import ( + "context" + "crypto/x509" + "fmt" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + // DefaultRateLimitPerPeer is the default maximum certificate issuances per peer per day. + DefaultRateLimitPerPeer = 10 + + // TriggerManual indicates a manual certificate request by the user. + TriggerManual = "manual" + // TriggerRenewal indicates an automatic renewal before expiry. + TriggerRenewal = "renewal" + // TriggerDomainChange indicates re-issuance due to a peer FQDN change. + TriggerDomainChange = "domain_change" + // TriggerSessionRenewal indicates re-issuance after peer re-authentication. + TriggerSessionRenewal = "session_renewal" +) + +// CAStore defines the storage operations needed by the CA Manager. +type CAStore interface { + CreateCACertificate(ctx context.Context, ca *CACertificate) error + GetCACertificateByID(ctx context.Context, accountID, caID string) (*CACertificate, error) + GetActiveCACertificates(ctx context.Context, accountID string) ([]*CACertificate, error) + DeactivateCACertificate(ctx context.Context, accountID, caID string) error + + CreateIssuedCertificate(ctx context.Context, cert *IssuedCertificate) error + GetIssuedCertificates(ctx context.Context, accountID string) ([]*IssuedCertificate, error) + GetIssuedCertificatesByPeer(ctx context.Context, accountID, peerID string) ([]*IssuedCertificate, error) + GetIssuedCertificateBySerial(ctx context.Context, accountID, serialNumber string) (*IssuedCertificate, error) + RevokeCertificate(ctx context.Context, accountID, serialNumber string) error + GetExpiringCertificates(ctx context.Context, accountID string, expiringBefore time.Time) ([]*IssuedCertificate, error) + + GetPeersWithActiveWildcardCerts(ctx context.Context, accountID string) (map[string]struct{}, error) + + CreateCertIssuanceLog(ctx context.Context, entry *CertIssuanceLog) error + CountCertIssuancesInWindow(ctx context.Context, accountID, peerID string, since time.Time) (int64, error) +} + +// Manager orchestrates certificate signing across multiple backends. +type Manager struct { + store CAStore + signers map[string]CertSigner +} + +// NewManager creates a new CA Manager. +func NewManager(store CAStore) *Manager { + return &Manager{ + store: store, + signers: make(map[string]CertSigner), + } +} + +// RegisterSigner adds a signing backend to the manager. +func (m *Manager) RegisterSigner(signer CertSigner) { + m.signers[signer.Type()] = signer +} + +// InitForAccount generates a new internal root CA for the given account and stores it. +// The CA is constrained to the account's DNS domain via x509 NameConstraints. +// Encryption of sensitive fields is handled transparently by the store layer. +func (m *Manager) InitForAccount(ctx context.Context, accountID, dnsDomain string, opts CAOptions) (*CACertificate, error) { + result, err := GenerateCA(dnsDomain, opts) + if err != nil { + return nil, fmt.Errorf("generate CA: %w", err) + } + + cert, err := parseCertificatePEM(result.CertPEM) + if err != nil { + return nil, fmt.Errorf("parse generated CA cert: %w", err) + } + + caCert := NewCACertificate(accountID, string(result.CertPEM), string(result.KeyPEM), result.Fingerprint, CAOptions{DisplayName: result.DisplayName, Organization: result.Organization}, cert.NotBefore, cert.NotAfter) + + if err := m.store.CreateCACertificate(ctx, caCert); err != nil { + return nil, fmt.Errorf("store CA certificate: %w", err) + } + + log.WithContext(ctx).Infof("initialized internal CA for account %s (fingerprint: %s)", accountID, result.Fingerprint) + + return caCert, nil +} + +// SignRequest holds the parameters for signing a certificate. +type SignRequest struct { + AccountID string + PeerID string + CSR *x509.CertificateRequest + SigningType string + Wildcard bool + Trigger string + Validity time.Duration +} + +// SignCertificate signs a CSR using the specified backend and records the issuance. +// Decryption of CA private keys is handled transparently by the store layer. +func (m *Manager) SignCertificate(ctx context.Context, req SignRequest) (*SigningResult, *IssuedCertificate, error) { + accountID, peerID, csr := req.AccountID, req.PeerID, req.CSR + signingType, wildcard, trigger, validity := req.SigningType, req.Wildcard, req.Trigger, req.Validity + if csr == nil { + return nil, nil, fmt.Errorf("csr is required") + } + + if signingType == SigningTypeACME { + signer, ok := m.signers[SigningTypeACME] + if !ok { + return nil, nil, fmt.Errorf("ACME signer not registered") + } + // ACME stub will return an error + result, err := signer.Sign(ctx, csr, "", wildcard) + return result, nil, err + } + + if len(csr.DNSNames) == 0 { + return nil, nil, fmt.Errorf("csr must include at least one DNS SAN") + } + peerFQDN := csr.DNSNames[0] + + activeCAs, err := m.store.GetActiveCACertificates(ctx, accountID) + if err != nil { + return nil, nil, fmt.Errorf("get active CAs: %w", err) + } + + if len(activeCAs) == 0 { + return nil, nil, fmt.Errorf("no active CA found for account %s", accountID) + } + + // Use the most recently created active CA + ca := activeCAs[0] + + certValidity := defaultCertValidity + if validity > 0 { + certValidity = validity + } + + signer, err := NewInternalCASigner([]byte(ca.CertificatePEM), []byte(ca.PrivateKeyPEM), ca.ID, certValidity) + if err != nil { + return nil, nil, fmt.Errorf("create internal signer: %w", err) + } + + result, err := signer.Sign(ctx, csr, peerFQDN, wildcard) + if err != nil { + return nil, nil, fmt.Errorf("sign certificate: %w", err) + } + + serialNumber, err := SerialNumberFromResult(result.CertPEM) + if err != nil { + return nil, nil, fmt.Errorf("extract serial number: %w", err) + } + + notAfter, err := NotAfterFromResult(result.CertPEM) + if err != nil { + return nil, nil, fmt.Errorf("extract not after: %w", err) + } + + dnsNames := csr.DNSNames + if wildcard && !containsName(dnsNames, "*."+peerFQDN) { + dnsNames = append(dnsNames, "*."+peerFQDN) + } + + issued := NewIssuedCertificate(IssuedCertParams{ + AccountID: accountID, + PeerID: peerID, + SerialNumber: serialNumber, + DNSNames: dnsNames, + HasWildcard: wildcard, + NotBefore: time.Now().UTC(), + NotAfter: notAfter, + SigningType: SigningTypeInternal, + SignedByCAID: ca.ID, + }) + + if err := m.store.CreateIssuedCertificate(ctx, issued); err != nil { + return nil, nil, fmt.Errorf("store issued certificate: %w", err) + } + + logEntry := NewCertIssuanceLog(accountID, peerID, trigger) + if err := m.store.CreateCertIssuanceLog(ctx, logEntry); err != nil { + log.WithContext(ctx).Warnf("failed to log certificate issuance: %v", err) + } + + return result, issued, nil +} + +// GetActiveCACertificates returns all active CA certificates for the given account. +func (m *Manager) GetActiveCACertificates(ctx context.Context, accountID string) ([]*CACertificate, error) { + return m.store.GetActiveCACertificates(ctx, accountID) +} + +// GetCACertificate returns a specific CA certificate by ID. +func (m *Manager) GetCACertificate(ctx context.Context, accountID, caID string) (*CACertificate, error) { + return m.store.GetCACertificateByID(ctx, accountID, caID) +} + +// GetIssuedCertificates returns all issued certificates for the given account. +func (m *Manager) GetIssuedCertificates(ctx context.Context, accountID string) ([]*IssuedCertificate, error) { + return m.store.GetIssuedCertificates(ctx, accountID) +} + +// RotateCA creates a new CA while keeping existing CAs active for trust continuity. +func (m *Manager) RotateCA(ctx context.Context, accountID, dnsDomain string, opts CAOptions) (*CACertificate, error) { + return m.InitForAccount(ctx, accountID, dnsDomain, opts) +} + +// DeactivateCA deactivates a specific CA certificate. +func (m *Manager) DeactivateCA(ctx context.Context, accountID, caID string) error { + return m.store.DeactivateCACertificate(ctx, accountID, caID) +} + +// CheckRateLimit checks if the peer has exceeded the rate limit for certificate issuance. +// domain_change and session_renewal triggers are exempt from rate limiting. +func (m *Manager) CheckRateLimit(ctx context.Context, accountID, peerID, trigger string, limit int) error { + if trigger == TriggerDomainChange || trigger == TriggerSessionRenewal { + return nil + } + + if limit <= 0 { + limit = DefaultRateLimitPerPeer + } + + since := time.Now().UTC().Add(-24 * time.Hour) + count, err := m.store.CountCertIssuancesInWindow(ctx, accountID, peerID, since) + if err != nil { + return fmt.Errorf("count issuances: %w", err) + } + + if count >= int64(limit) { + return fmt.Errorf("peer %s exceeded certificate rate limit (%d/%d in 24h)", peerID, count, limit) + } + + return nil +} + +// GetIssuedCertificatesByPeer returns all issued certificates for a peer. +func (m *Manager) GetIssuedCertificatesByPeer(ctx context.Context, accountID, peerID string) ([]*IssuedCertificate, error) { + return m.store.GetIssuedCertificatesByPeer(ctx, accountID, peerID) +} + +// RevokeCertificate revokes a certificate by its serial number. +func (m *Manager) RevokeCertificate(ctx context.Context, accountID, serialNumber string) error { + return m.store.RevokeCertificate(ctx, accountID, serialNumber) +} + +// GetExpiringCertificates returns certificates that expire before the given time. +func (m *Manager) GetExpiringCertificates(ctx context.Context, accountID string, expiringBefore time.Time) ([]*IssuedCertificate, error) { + return m.store.GetExpiringCertificates(ctx, accountID, expiringBefore) +} diff --git a/management/server/ca/manager_test.go b/management/server/ca/manager_test.go new file mode 100644 index 00000000000..50f3295d1af --- /dev/null +++ b/management/server/ca/manager_test.go @@ -0,0 +1,456 @@ +package ca + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockCAStore struct { + caCerts []*CACertificate + issuedCerts []*IssuedCertificate + issuanceLogs []*CertIssuanceLog +} + +func newMockCAStore() *mockCAStore { + return &mockCAStore{} +} + +func (m *mockCAStore) CreateCACertificate(_ context.Context, ca *CACertificate) error { + m.caCerts = append(m.caCerts, ca) + return nil +} + +func (m *mockCAStore) GetCACertificateByID(_ context.Context, accountID, caID string) (*CACertificate, error) { + for _, c := range m.caCerts { + if c.AccountID == accountID && c.ID == caID { + return c, nil + } + } + return nil, fmt.Errorf("CA not found") +} + +func (m *mockCAStore) GetActiveCACertificates(_ context.Context, accountID string) ([]*CACertificate, error) { + var active []*CACertificate + for _, c := range m.caCerts { + if c.AccountID == accountID && c.IsActive { + active = append(active, c) + } + } + return active, nil +} + +func (m *mockCAStore) DeactivateCACertificate(_ context.Context, accountID, caID string) error { + for _, c := range m.caCerts { + if c.AccountID == accountID && c.ID == caID { + c.IsActive = false + return nil + } + } + return fmt.Errorf("CA not found") +} + +func (m *mockCAStore) CreateIssuedCertificate(_ context.Context, cert *IssuedCertificate) error { + m.issuedCerts = append(m.issuedCerts, cert) + return nil +} + +func (m *mockCAStore) GetIssuedCertificates(_ context.Context, accountID string) ([]*IssuedCertificate, error) { + var certs []*IssuedCertificate + for _, c := range m.issuedCerts { + if c.AccountID == accountID { + certs = append(certs, c) + } + } + return certs, nil +} + +func (m *mockCAStore) GetIssuedCertificatesByPeer(_ context.Context, accountID, peerID string) ([]*IssuedCertificate, error) { + var certs []*IssuedCertificate + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.PeerID == peerID { + certs = append(certs, c) + } + } + return certs, nil +} + +func (m *mockCAStore) GetIssuedCertificateBySerial(_ context.Context, accountID, serialNumber string) (*IssuedCertificate, error) { + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.SerialNumber == serialNumber { + return c, nil + } + } + return nil, fmt.Errorf("issued cert not found") +} + +func (m *mockCAStore) RevokeCertificate(_ context.Context, accountID, serialNumber string) error { + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.SerialNumber == serialNumber { + c.Revoked = true + return nil + } + } + return fmt.Errorf("issued cert not found") +} + +func (m *mockCAStore) GetExpiringCertificates(_ context.Context, accountID string, expiringBefore time.Time) ([]*IssuedCertificate, error) { + var certs []*IssuedCertificate + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.NotAfter.Before(expiringBefore) && !c.Revoked { + certs = append(certs, c) + } + } + return certs, nil +} + +func (m *mockCAStore) CreateCertIssuanceLog(_ context.Context, entry *CertIssuanceLog) error { + m.issuanceLogs = append(m.issuanceLogs, entry) + return nil +} + +func (m *mockCAStore) CountCertIssuancesInWindow(_ context.Context, accountID, peerID string, since time.Time) (int64, error) { + var count int64 + for _, l := range m.issuanceLogs { + if l.AccountID == accountID && l.PeerID == peerID && !l.IssuedAt.Before(since) { + count++ + } + } + return count, nil +} + +func (m *mockCAStore) GetPeersWithActiveWildcardCerts(_ context.Context, accountID string) (map[string]struct{}, error) { + result := make(map[string]struct{}) + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.HasWildcard && !c.Revoked && c.NotAfter.After(time.Now()) { + result[c.PeerID] = struct{}{} + } + } + return result, nil +} + +func createTestCSR(t *testing.T, fqdn string, wildcard bool) *x509.CertificateRequest { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + dnsNames := []string{fqdn} + if wildcard { + dnsNames = append(dnsNames, "*."+fqdn) + } + + csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: fqdn}, + DNSNames: dnsNames, + }, key) + require.NoError(t, err) + + csr, err := x509.ParseCertificateRequest(csrDER) + require.NoError(t, err) + + return csr +} + +func setupTestManager(t *testing.T) (*Manager, *mockCAStore) { + t.Helper() + + store := newMockCAStore() + mgr := NewManager(store) + mgr.RegisterSigner(NewACMEPersistSigner()) + + return mgr, store +} + +func TestManager_InitForAccount(t *testing.T) { + mgr, store := setupTestManager(t) + + caCert, err := mgr.InitForAccount(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + require.NotNil(t, caCert) + + assert.Equal(t, "account1", caCert.AccountID) + assert.True(t, caCert.IsActive) + assert.NotEmpty(t, caCert.Fingerprint) + assert.NotEmpty(t, caCert.CertificatePEM) + assert.NotEmpty(t, caCert.PrivateKeyPEM) + assert.Len(t, store.caCerts, 1) +} + +func TestManager_SignCertificate(t *testing.T) { + mgr, store := setupTestManager(t) + + _, err := mgr.InitForAccount(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + + csr := createTestCSR(t, "peer1.netbird.example", false) + + result, issued, err := mgr.SignCertificate( + context.Background(), SignRequest{ + AccountID: "account1", PeerID: "peer1", CSR: csr, + SigningType: SigningTypeInternal, Trigger: TriggerManual, + }, + ) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, issued) + + assert.NotEmpty(t, result.CertPEM) + assert.NotEmpty(t, result.ChainPEM) + assert.Equal(t, "account1", issued.AccountID) + assert.Equal(t, "peer1", issued.PeerID) + assert.Equal(t, SigningTypeInternal, issued.SigningType) + assert.False(t, issued.HasWildcard) + assert.Contains(t, issued.DNSNames, "peer1.netbird.example") + + assert.Len(t, store.issuedCerts, 1) + assert.Len(t, store.issuanceLogs, 1) + assert.Equal(t, TriggerManual, store.issuanceLogs[0].Trigger) +} + +func TestManager_SignCertificate_Wildcard(t *testing.T) { + mgr, _ := setupTestManager(t) + + _, err := mgr.InitForAccount(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + + csr := createTestCSR(t, "peer1.netbird.example", true) + + result, issued, err := mgr.SignCertificate( + context.Background(), SignRequest{ + AccountID: "account1", PeerID: "peer1", CSR: csr, + SigningType: SigningTypeInternal, Wildcard: true, Trigger: TriggerManual, + }, + ) + require.NoError(t, err) + require.NotNil(t, result) + + assert.True(t, issued.HasWildcard) + assert.Contains(t, issued.DNSNames, "*.peer1.netbird.example") +} + +func TestManager_SignCertificate_NoActiveCA(t *testing.T) { + mgr, _ := setupTestManager(t) + + csr := createTestCSR(t, "peer1.netbird.example", false) + + _, _, err := mgr.SignCertificate( + context.Background(), SignRequest{ + AccountID: "account1", PeerID: "peer1", CSR: csr, + SigningType: SigningTypeInternal, Trigger: TriggerManual, + }, + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no active CA") +} + +func TestManager_SignCertificate_ACMEStub(t *testing.T) { + mgr, _ := setupTestManager(t) + + csr := createTestCSR(t, "peer1.netbird.example", false) + + _, _, err := mgr.SignCertificate( + context.Background(), SignRequest{ + AccountID: "account1", PeerID: "peer1", CSR: csr, + SigningType: SigningTypeACME, Trigger: TriggerManual, + }, + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not yet available") +} + +func TestManager_CheckRateLimit(t *testing.T) { + tests := []struct { + name string + trigger string + logCount int + limit int + expectError bool + }{ + { + name: "under limit", + trigger: TriggerManual, + logCount: 5, + limit: 10, + expectError: false, + }, + { + name: "at limit", + trigger: TriggerManual, + logCount: 10, + limit: 10, + expectError: true, + }, + { + name: "over limit", + trigger: TriggerManual, + logCount: 15, + limit: 10, + expectError: true, + }, + { + name: "domain change exempt", + trigger: TriggerDomainChange, + logCount: 100, + limit: 10, + expectError: false, + }, + { + name: "renewal at limit", + trigger: TriggerRenewal, + logCount: 10, + limit: 10, + expectError: true, + }, + { + name: "zero limit uses default", + trigger: TriggerManual, + logCount: DefaultRateLimitPerPeer, + limit: 0, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store := newMockCAStore() + mgr := NewManager(store) + + for i := 0; i < tt.logCount; i++ { + store.issuanceLogs = append(store.issuanceLogs, &CertIssuanceLog{ + AccountID: "account1", + PeerID: "peer1", + IssuedAt: time.Now().UTC(), + Trigger: TriggerManual, + }) + } + + err := mgr.CheckRateLimit(context.Background(), "account1", "peer1", tt.trigger, tt.limit) + if tt.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), "rate limit") + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestManager_RotateCA(t *testing.T) { + mgr, store := setupTestManager(t) + + ca1, err := mgr.InitForAccount(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + + ca2, err := mgr.RotateCA(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + + assert.NotEqual(t, ca1.ID, ca2.ID) + assert.NotEqual(t, ca1.Fingerprint, ca2.Fingerprint) + assert.Len(t, store.caCerts, 2) + + // Both should be active + active, err := mgr.GetActiveCACertificates(context.Background(), "account1") + require.NoError(t, err) + assert.Len(t, active, 2) +} + +func TestManager_DeactivateCA(t *testing.T) { + mgr, _ := setupTestManager(t) + + caCert, err := mgr.InitForAccount(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + + err = mgr.DeactivateCA(context.Background(), "account1", caCert.ID) + require.NoError(t, err) + + active, err := mgr.GetActiveCACertificates(context.Background(), "account1") + require.NoError(t, err) + assert.Len(t, active, 0) +} + +func TestManager_RevokeCertificate(t *testing.T) { + mgr, store := setupTestManager(t) + + _, err := mgr.InitForAccount(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + + csr := createTestCSR(t, "peer1.netbird.example", false) + + _, issued, err := mgr.SignCertificate( + context.Background(), SignRequest{ + AccountID: "account1", PeerID: "peer1", CSR: csr, + SigningType: SigningTypeInternal, Trigger: TriggerManual, + }, + ) + require.NoError(t, err) + + err = mgr.RevokeCertificate(context.Background(), "account1", issued.SerialNumber) + require.NoError(t, err) + + assert.True(t, store.issuedCerts[0].Revoked) +} + +func TestManager_SignCertificate_CustomValidity(t *testing.T) { + mgr, _ := setupTestManager(t) + + _, err := mgr.InitForAccount(context.Background(), "account1", "netbird.example", CAOptions{}) + require.NoError(t, err) + + csr := createTestCSR(t, "peer1.netbird.example", false) + + customValidity := 24 * time.Hour + result, issued, err := mgr.SignCertificate( + context.Background(), SignRequest{ + AccountID: "account1", PeerID: "peer1", CSR: csr, + SigningType: SigningTypeInternal, Trigger: TriggerManual, Validity: customValidity, + }, + ) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, issued) + + // Verify the cert has approximately the custom validity (within 1 minute tolerance) + notAfter, err := NotAfterFromResult(result.CertPEM) + require.NoError(t, err) + + expectedExpiry := time.Now().Add(customValidity) + assert.WithinDuration(t, expectedExpiry, notAfter, 1*time.Minute) +} + +func TestManager_CheckRateLimit_SessionRenewalExempt(t *testing.T) { + store := newMockCAStore() + mgr := NewManager(store) + + // Fill up rate limit logs well beyond the limit + for i := 0; i < 100; i++ { + store.issuanceLogs = append(store.issuanceLogs, &CertIssuanceLog{ + AccountID: "account1", + PeerID: "peer1", + IssuedAt: time.Now().UTC(), + Trigger: TriggerManual, + }) + } + + // Session renewal should be exempt from rate limiting + err := mgr.CheckRateLimit(context.Background(), "account1", "peer1", TriggerSessionRenewal, 10) + assert.NoError(t, err) +} + +func TestACMEPersistSigner_ReturnsError(t *testing.T) { + signer := NewACMEPersistSigner() + assert.Equal(t, SigningTypeACME, signer.Type()) + + _, err := signer.Sign(context.Background(), nil, "", false) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not yet available") +} diff --git a/management/server/ca/signer.go b/management/server/ca/signer.go new file mode 100644 index 00000000000..52e67ed23c2 --- /dev/null +++ b/management/server/ca/signer.go @@ -0,0 +1,25 @@ +package ca + +import ( + "context" + "crypto/x509" +) + +// SigningResult holds the output of a certificate signing operation. +type SigningResult struct { + CertPEM []byte + ChainPEM []byte +} + +// CertSigner is a backend-agnostic interface for signing certificate requests. +// Implementations include InternalCASigner (self-signed root CA) and +// ACMEPersistSigner (stub for future ACME DNS-PERSIST-01). +type CertSigner interface { + // Sign signs the given CSR and returns the issued certificate and chain in PEM format. + // peerFQDN is the expected FQDN that must match the CSR's DNSNames. + // If wildcard is true, a wildcard SAN (*.peerFQDN) is added. + Sign(ctx context.Context, csr *x509.CertificateRequest, peerFQDN string, wildcard bool) (*SigningResult, error) + + // Type returns the signer type identifier (e.g., "internal", "acme"). + Type() string +} diff --git a/management/server/ca/types.go b/management/server/ca/types.go new file mode 100644 index 00000000000..63a95e5b3c1 --- /dev/null +++ b/management/server/ca/types.go @@ -0,0 +1,143 @@ +package ca + +import ( + "fmt" + "time" + + "github.com/rs/xid" + + "github.com/netbirdio/netbird/util/crypt" +) + +// CACertificate represents a Certificate Authority certificate stored in the database. +type CACertificate struct { + ID string `gorm:"primaryKey"` + AccountID string `gorm:"index"` + CertificatePEM string `gorm:"type:text"` + PrivateKeyPEM string `gorm:"type:text"` // encrypted at rest via FieldEncrypt + Fingerprint string `gorm:"index"` + DisplayName string // CN used when generating the CA + Organization string // O used when generating the CA + NotBefore time.Time + NotAfter time.Time + IsActive bool `gorm:"index"` + CreatedAt time.Time +} + +// NewCACertificate creates a new CACertificate with a generated ID and creation timestamp. +func NewCACertificate(accountID string, certPEM, keyPEM, fingerprint string, opts CAOptions, notBefore, notAfter time.Time) *CACertificate { + return &CACertificate{ + ID: xid.New().String(), + AccountID: accountID, + CertificatePEM: certPEM, + PrivateKeyPEM: keyPEM, + Fingerprint: fingerprint, + DisplayName: opts.DisplayName, + Organization: opts.Organization, + NotBefore: notBefore, + NotAfter: notAfter, + IsActive: true, + CreatedAt: time.Now().UTC(), + } +} + +// EncryptSensitiveData encrypts the CA private key in place. +func (c *CACertificate) EncryptSensitiveData(enc *crypt.FieldEncrypt) error { + if enc == nil { + return nil + } + + if c.PrivateKeyPEM != "" { + encrypted, err := enc.Encrypt(c.PrivateKeyPEM) + if err != nil { + return fmt.Errorf("encrypt ca private key: %w", err) + } + c.PrivateKeyPEM = encrypted + } + + return nil +} + +// DecryptSensitiveData decrypts the CA private key in place. +func (c *CACertificate) DecryptSensitiveData(enc *crypt.FieldEncrypt) error { + if enc == nil { + return nil + } + + if c.PrivateKeyPEM != "" { + decrypted, err := enc.Decrypt(c.PrivateKeyPEM) + if err != nil { + return fmt.Errorf("decrypt ca private key: %w", err) + } + c.PrivateKeyPEM = decrypted + } + + return nil +} + +// IssuedCertificate represents a certificate issued to a peer. +type IssuedCertificate struct { + ID string `gorm:"primaryKey"` + AccountID string `gorm:"index"` + PeerID string `gorm:"index"` + SerialNumber string `gorm:"uniqueIndex"` + DNSNames []string `gorm:"serializer:json"` + HasWildcard bool + NotBefore time.Time + NotAfter time.Time + SigningType string // "internal" or "acme" + SignedByCAID string `gorm:"index"` + Revoked bool + CreatedAt time.Time +} + +// IssuedCertParams holds the parameters for creating an IssuedCertificate. +type IssuedCertParams struct { + AccountID string + PeerID string + SerialNumber string + DNSNames []string + HasWildcard bool + NotBefore time.Time + NotAfter time.Time + SigningType string + SignedByCAID string +} + +// NewIssuedCertificate creates a new IssuedCertificate with a generated ID and creation timestamp. +func NewIssuedCertificate(p IssuedCertParams) *IssuedCertificate { + return &IssuedCertificate{ + ID: xid.New().String(), + AccountID: p.AccountID, + PeerID: p.PeerID, + SerialNumber: p.SerialNumber, + DNSNames: p.DNSNames, + HasWildcard: p.HasWildcard, + NotBefore: p.NotBefore, + NotAfter: p.NotAfter, + SigningType: p.SigningType, + SignedByCAID: p.SignedByCAID, + Revoked: false, + CreatedAt: time.Now().UTC(), + } +} + +// CertIssuanceLog records each certificate issuance event for rate limiting. +type CertIssuanceLog struct { + ID string `gorm:"primaryKey"` + AccountID string `gorm:"index"` + PeerID string `gorm:"index"` + IssuedAt time.Time `gorm:"index"` + Trigger string // "manual", "renewal", "domain_change" +} + +// NewCertIssuanceLog creates a new CertIssuanceLog with a generated ID. +func NewCertIssuanceLog(accountID, peerID, trigger string) *CertIssuanceLog { + return &CertIssuanceLog{ + ID: xid.New().String(), + AccountID: accountID, + PeerID: peerID, + IssuedAt: time.Now().UTC(), + Trigger: trigger, + } +} diff --git a/management/server/http/handler.go b/management/server/http/handler.go index ddeda6d7fe1..084c8de4ebd 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -15,6 +15,7 @@ import ( "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain/manager" + "github.com/netbirdio/netbird/management/server/ca" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs" @@ -43,6 +44,8 @@ import ( "github.com/netbirdio/netbird/management/server/auth" "github.com/netbirdio/netbird/management/server/geolocation" nbgroups "github.com/netbirdio/netbird/management/server/groups" + cahandler "github.com/netbirdio/netbird/management/server/http/handlers/ca" + "github.com/netbirdio/netbird/management/server/http/handlers/accounts" "github.com/netbirdio/netbird/management/server/http/handlers/dns" "github.com/netbirdio/netbird/management/server/http/handlers/events" @@ -73,7 +76,7 @@ const ( ) // NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints. -func NewAPIHandler(ctx context.Context, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, integratedValidator integrated_validator.IntegratedValidator, proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix) (http.Handler, error) { +func NewAPIHandler(ctx context.Context, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, integratedValidator integrated_validator.IntegratedValidator, proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix, caManager *ca.Manager) (http.Handler, error) { // Register bypass paths for unauthenticated endpoints if err := bypass.AddBypassPath("/api/instance"); err != nil { @@ -176,6 +179,9 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks if serviceManager != nil && reverseProxyDomainManager != nil { reverseproxymanager.RegisterEndpoints(serviceManager, *reverseProxyDomainManager, reverseProxyAccessLogsManager, router) } + if caManager != nil { + cahandler.AddEndpoints(caManager, accountManager, permissionsManager, router) + } // Register OAuth callback handler for proxy authentication if proxyGRPCServer != nil { diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index 27a57c43425..d768e2df13e 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -215,6 +215,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS if req.Settings.LazyConnectionEnabled != nil { returnSettings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled } + if req.Settings.CertWildcardAllowed != nil { + returnSettings.CertWildcardAllowed = *req.Settings.CertWildcardAllowed + } if req.Settings.AutoUpdateVersion != nil { _, err := goversion.NewSemver(*req.Settings.AutoUpdateVersion) if *req.Settings.AutoUpdateVersion == autoUpdateLatestVersion || @@ -345,6 +348,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled, PeerExposeEnabled: settings.PeerExposeEnabled, PeerExposeGroups: settings.PeerExposeGroups, + CertWildcardAllowed: &settings.CertWildcardAllowed, LazyConnectionEnabled: &settings.LazyConnectionEnabled, DnsDomain: &settings.DNSDomain, AutoUpdateVersion: &settings.AutoUpdateVersion, diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index 6cbd5908d92..cf1e5d39c68 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -124,6 +124,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateVersion: sr(""), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), + CertWildcardAllowed: br(false), }, expectedArray: true, expectedID: accountID, @@ -149,6 +150,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateVersion: sr(""), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), + CertWildcardAllowed: br(false), }, expectedArray: false, expectedID: accountID, @@ -174,6 +176,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateVersion: sr("latest"), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), + CertWildcardAllowed: br(false), }, expectedArray: false, expectedID: accountID, @@ -199,6 +202,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateVersion: sr(""), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), + CertWildcardAllowed: br(false), }, expectedArray: false, expectedID: accountID, @@ -224,6 +228,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateVersion: sr(""), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), + CertWildcardAllowed: br(false), }, expectedArray: false, expectedID: accountID, @@ -249,6 +254,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateVersion: sr(""), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), + CertWildcardAllowed: br(false), }, expectedArray: false, expectedID: accountID, diff --git a/management/server/http/handlers/ca/ca_handler.go b/management/server/http/handlers/ca/ca_handler.go new file mode 100644 index 00000000000..fcb62463965 --- /dev/null +++ b/management/server/http/handlers/ca/ca_handler.go @@ -0,0 +1,339 @@ +package ca + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "time" + + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/management/server/account" + "github.com/netbirdio/netbird/management/server/activity" + nbca "github.com/netbirdio/netbird/management/server/ca" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/permissions" + "github.com/netbirdio/netbird/management/server/permissions/modules" + "github.com/netbirdio/netbird/management/server/permissions/operations" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" +) + +type handler struct { + caManager *nbca.Manager + accountManager account.Manager + permissionsManager permissions.Manager +} + +// AddEndpoints registers the CA management REST endpoints. +func AddEndpoints(caManager *nbca.Manager, accountManager account.Manager, permissionsManager permissions.Manager, router *mux.Router) { + h := newHandler(caManager, accountManager, permissionsManager) + router.HandleFunc("/ca", h.listCAs).Methods("GET", "OPTIONS") + router.HandleFunc("/ca", h.initCA).Methods("POST", "OPTIONS") + router.HandleFunc("/ca/rotate", h.rotateCA).Methods("POST", "OPTIONS") + router.HandleFunc("/ca/certificates", h.listIssuedCerts).Methods("GET", "OPTIONS") + router.HandleFunc("/ca/certificates/{serialNumber}/revoke", h.revokeCert).Methods("POST", "OPTIONS") + router.HandleFunc("/ca/{caId}", h.getCA).Methods("GET", "OPTIONS") + router.HandleFunc("/ca/{caId}", h.deactivateCA).Methods("DELETE", "OPTIONS") +} + +func newHandler(caManager *nbca.Manager, accountManager account.Manager, permissionsManager permissions.Manager) *handler { + return &handler{ + caManager: caManager, + accountManager: accountManager, + permissionsManager: permissionsManager, + } +} + +func (h *handler) listCAs(w http.ResponseWriter, r *http.Request) { + accountID, _, err := h.checkPermission(r, operations.Read) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + cas, err := h.caManager.GetActiveCACertificates(r.Context(), accountID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + resp := make([]api.CACertificateResponse, 0, len(cas)) + for _, c := range cas { + resp = append(resp, toCACertificateResponse(c, false)) + } + + util.WriteJSONObject(r.Context(), w, resp) +} + +func (h *handler) initCA(w http.ResponseWriter, r *http.Request) { + accountID, userID, err := h.checkPermission(r, operations.Create) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + dnsDomain, err := h.getAccountDNSDomain(r, accountID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + opts, err := parseCAOptions(r) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + caCert, err := h.caManager.InitForAccount(r.Context(), accountID, dnsDomain, opts) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + // Enable certificate authority in account settings + settings, err := h.accountManager.GetStore().GetAccountSettings(r.Context(), store.LockingStrengthNone, accountID) + if err != nil { + log.WithContext(r.Context()).Errorf("failed to get account settings for CA enable: %v", err) + util.WriteError(r.Context(), err, w) + return + } + if !settings.CertificateAuthorityEnabled { + settings.CertificateAuthorityEnabled = true + if saveErr := h.accountManager.GetStore().SaveAccountSettings(r.Context(), accountID, settings); saveErr != nil { + log.WithContext(r.Context()).Errorf("failed to enable certificate authority in account settings: %v", saveErr) + util.WriteError(r.Context(), saveErr, w) + return + } + } + + h.accountManager.StoreEvent(r.Context(), userID, caCert.ID, accountID, activity.CertificateAuthorityCreated, nil) + + util.WriteJSONObject(r.Context(), w, toCACertificateResponse(caCert, false)) +} + +func (h *handler) getCA(w http.ResponseWriter, r *http.Request) { + accountID, _, err := h.checkPermission(r, operations.Read) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + vars := mux.Vars(r) + caID := vars["caId"] + if caID == "" { + util.WriteErrorResponse("CA ID is required", http.StatusBadRequest, w) + return + } + + caCert, err := h.caManager.GetCACertificate(r.Context(), accountID, caID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + util.WriteJSONObject(r.Context(), w, toCACertificateResponse(caCert, true)) +} + +func (h *handler) deactivateCA(w http.ResponseWriter, r *http.Request) { + accountID, userID, err := h.checkPermission(r, operations.Delete) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + vars := mux.Vars(r) + caID := vars["caId"] + if caID == "" { + util.WriteErrorResponse("CA ID is required", http.StatusBadRequest, w) + return + } + + if err := h.caManager.DeactivateCA(r.Context(), accountID, caID); err != nil { + util.WriteError(r.Context(), err, w) + return + } + + h.accountManager.StoreEvent(r.Context(), userID, caID, accountID, activity.CertificateAuthorityDeactivated, nil) + + util.WriteJSONObject(r.Context(), w, util.EmptyObject{}) +} + +func (h *handler) rotateCA(w http.ResponseWriter, r *http.Request) { + accountID, userID, err := h.checkPermission(r, operations.Create) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + dnsDomain, err := h.getAccountDNSDomain(r, accountID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + opts, err := parseCAOptions(r) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + caCert, err := h.caManager.RotateCA(r.Context(), accountID, dnsDomain, opts) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + h.accountManager.StoreEvent(r.Context(), userID, caCert.ID, accountID, activity.CertificateAuthorityRotated, nil) + + util.WriteJSONObject(r.Context(), w, toCACertificateResponse(caCert, false)) +} + +func (h *handler) listIssuedCerts(w http.ResponseWriter, r *http.Request) { + accountID, _, err := h.checkPermission(r, operations.Read) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + peerID := r.URL.Query().Get("peer_id") + + var certs []*nbca.IssuedCertificate + if peerID != "" { + certs, err = h.caManager.GetIssuedCertificatesByPeer(r.Context(), accountID, peerID) + } else { + certs, err = h.caManager.GetIssuedCertificates(r.Context(), accountID) + } + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + resp := make([]api.IssuedCertificateResponse, 0, len(certs)) + for _, c := range certs { + resp = append(resp, toIssuedCertificateResponse(c)) + } + + util.WriteJSONObject(r.Context(), w, resp) +} + +func (h *handler) revokeCert(w http.ResponseWriter, r *http.Request) { + accountID, userID, err := h.checkPermission(r, operations.Delete) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + vars := mux.Vars(r) + serialNumber := vars["serialNumber"] + if serialNumber == "" { + util.WriteErrorResponse("serial number is required", http.StatusBadRequest, w) + return + } + + if err := h.caManager.RevokeCertificate(r.Context(), accountID, serialNumber); err != nil { + util.WriteError(r.Context(), err, w) + return + } + + h.accountManager.StoreEvent(r.Context(), userID, serialNumber, accountID, activity.CertificateRevoked, nil) + + util.WriteJSONObject(r.Context(), w, util.EmptyObject{}) +} + +// checkPermission extracts user auth and validates permissions. +// Returns accountID and userID on success. +func (h *handler) checkPermission(r *http.Request, op operations.Operation) (string, string, error) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + return "", "", err + } + + accountID, userID := userAuth.AccountId, userAuth.UserId + + allowed, err := h.permissionsManager.ValidateUserPermissions(r.Context(), accountID, userID, modules.CertificateAuthority, op) + if err != nil { + return "", "", status.NewPermissionValidationError(err) + } + + if !allowed { + return "", "", status.NewPermissionDeniedError() + } + + return accountID, userID, nil +} + +// getAccountDNSDomain retrieves the DNS domain from account settings. +func (h *handler) getAccountDNSDomain(r *http.Request, accountID string) (string, error) { + settings, err := h.accountManager.GetStore().GetAccountSettings(r.Context(), store.LockingStrengthNone, accountID) + if err != nil { + return "", err + } + + if settings.DNSDomain == "" { + return "", status.Errorf(status.PreconditionFailed, "account DNS domain is not configured") + } + + return settings.DNSDomain, nil +} + +// parseCAOptions extracts optional CA configuration from the request body. +// An empty or missing body is valid — all fields fall back to defaults. +func parseCAOptions(r *http.Request) (nbca.CAOptions, error) { + var req api.CAInitRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) { + return nbca.CAOptions{}, status.Errorf(status.InvalidArgument, "invalid request body: %v", err) + } + + opts := nbca.CAOptions{} + if req.DisplayName != nil { + opts.DisplayName = *req.DisplayName + } + if req.Organization != nil { + opts.Organization = *req.Organization + } + if req.ValidityDays != nil { + opts.Validity = time.Duration(*req.ValidityDays) * 24 * time.Hour + } + return opts, nil +} + +func toCACertificateResponse(c *nbca.CACertificate, includePEM bool) api.CACertificateResponse { + resp := api.CACertificateResponse{ + Id: c.ID, + Fingerprint: c.Fingerprint, + DisplayName: &c.DisplayName, + Organization: &c.Organization, + NotBefore: c.NotBefore.UTC(), + NotAfter: c.NotAfter.UTC(), + IsActive: c.IsActive, + CreatedAt: c.CreatedAt.UTC(), + } + if includePEM { + resp.CertificatePem = &c.CertificatePEM + } + return resp +} + +func toIssuedCertificateResponse(c *nbca.IssuedCertificate) api.IssuedCertificateResponse { + dnsNames := c.DNSNames + if dnsNames == nil { + dnsNames = []string{} + } + return api.IssuedCertificateResponse{ + Id: c.ID, + PeerId: c.PeerID, + SerialNumber: c.SerialNumber, + DnsNames: dnsNames, + HasWildcard: c.HasWildcard, + NotBefore: c.NotBefore.UTC(), + NotAfter: c.NotAfter.UTC(), + SigningType: c.SigningType, + Revoked: c.Revoked, + CreatedAt: c.CreatedAt.UTC(), + } +} diff --git a/management/server/http/handlers/ca/ca_handler_test.go b/management/server/http/handlers/ca/ca_handler_test.go new file mode 100644 index 00000000000..ac67368bc9e --- /dev/null +++ b/management/server/http/handlers/ca/ca_handler_test.go @@ -0,0 +1,477 @@ +package ca + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + nbca "github.com/netbirdio/netbird/management/server/ca" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/management/server/permissions" + "github.com/netbirdio/netbird/management/server/permissions/modules" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/auth" + "github.com/netbirdio/netbird/shared/management/http/api" +) + +const ( + testAccountID = "test-account" + testUserID = "test-user" + testDNSDomain = "netbird.example" +) + +// mockCAStore implements nbca.CAStore for testing. +type mockCAStore struct { + caCerts []*nbca.CACertificate + issuedCerts []*nbca.IssuedCertificate + issuanceLogs []*nbca.CertIssuanceLog +} + +func newMockCAStore() *mockCAStore { + return &mockCAStore{} +} + +func (m *mockCAStore) CreateCACertificate(_ context.Context, ca *nbca.CACertificate) error { + m.caCerts = append(m.caCerts, ca) + return nil +} + +func (m *mockCAStore) GetCACertificateByID(_ context.Context, accountID, caID string) (*nbca.CACertificate, error) { + for _, c := range m.caCerts { + if c.AccountID == accountID && c.ID == caID { + return c, nil + } + } + return nil, fmt.Errorf("CA not found") +} + +func (m *mockCAStore) GetActiveCACertificates(_ context.Context, accountID string) ([]*nbca.CACertificate, error) { + var active []*nbca.CACertificate + for _, c := range m.caCerts { + if c.AccountID == accountID && c.IsActive { + active = append(active, c) + } + } + return active, nil +} + +func (m *mockCAStore) DeactivateCACertificate(_ context.Context, accountID, caID string) error { + for _, c := range m.caCerts { + if c.AccountID == accountID && c.ID == caID { + c.IsActive = false + return nil + } + } + return fmt.Errorf("CA not found") +} + +func (m *mockCAStore) CreateIssuedCertificate(_ context.Context, cert *nbca.IssuedCertificate) error { + m.issuedCerts = append(m.issuedCerts, cert) + return nil +} + +func (m *mockCAStore) GetIssuedCertificates(_ context.Context, accountID string) ([]*nbca.IssuedCertificate, error) { + var certs []*nbca.IssuedCertificate + for _, c := range m.issuedCerts { + if c.AccountID == accountID { + certs = append(certs, c) + } + } + return certs, nil +} + +func (m *mockCAStore) GetIssuedCertificatesByPeer(_ context.Context, accountID, peerID string) ([]*nbca.IssuedCertificate, error) { + var certs []*nbca.IssuedCertificate + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.PeerID == peerID { + certs = append(certs, c) + } + } + return certs, nil +} + +func (m *mockCAStore) GetIssuedCertificateBySerial(_ context.Context, accountID, serialNumber string) (*nbca.IssuedCertificate, error) { + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.SerialNumber == serialNumber { + return c, nil + } + } + return nil, fmt.Errorf("cert not found") +} + +func (m *mockCAStore) RevokeCertificate(_ context.Context, accountID, serialNumber string) error { + for _, c := range m.issuedCerts { + if c.AccountID == accountID && c.SerialNumber == serialNumber { + c.Revoked = true + return nil + } + } + return fmt.Errorf("cert not found") +} + +func (m *mockCAStore) GetExpiringCertificates(_ context.Context, _ string, _ time.Time) ([]*nbca.IssuedCertificate, error) { + return nil, nil +} + +func (m *mockCAStore) CreateCertIssuanceLog(_ context.Context, entry *nbca.CertIssuanceLog) error { + m.issuanceLogs = append(m.issuanceLogs, entry) + return nil +} + +func (m *mockCAStore) CountCertIssuancesInWindow(_ context.Context, _, _ string, _ time.Time) (int64, error) { + return 0, nil +} + +func (m *mockCAStore) GetPeersWithActiveWildcardCerts(_ context.Context, _ string) (map[string]struct{}, error) { + return nil, nil +} + +// mockStoreForSettings implements only GetAccountSettings from store.Store. +type mockStoreForSettings struct { + store.Store + settings *types.Settings +} + +func (m *mockStoreForSettings) GetAccountSettings(_ context.Context, _ store.LockingStrength, _ string) (*types.Settings, error) { + return m.settings, nil +} + +func (m *mockStoreForSettings) SaveAccountSettings(_ context.Context, _ string, settings *types.Settings) error { + m.settings = settings + return nil +} + +func setupTestHandler(t *testing.T, allowPermission bool) (*handler, *mockCAStore) { + t.Helper() + + caStore := newMockCAStore() + caManager := nbca.NewManager(caStore) + caManager.RegisterSigner(nbca.NewACMEPersistSigner()) + + ctrl := gomock.NewController(t) + permissionsManagerMock := permissions.NewMockManager(ctrl) + + if allowPermission { + permissionsManagerMock.EXPECT(). + ValidateUserPermissions(gomock.Any(), testAccountID, testUserID, modules.CertificateAuthority, gomock.Any()). + Return(true, nil). + AnyTimes() + } else { + permissionsManagerMock.EXPECT(). + ValidateUserPermissions(gomock.Any(), testAccountID, testUserID, modules.CertificateAuthority, gomock.Any()). + Return(false, nil). + AnyTimes() + } + + mockStore := &mockStoreForSettings{ + settings: &types.Settings{ + DNSDomain: testDNSDomain, + }, + } + + accountManager := &mock_server.MockAccountManager{ + GetStoreFunc: func() store.Store { + return mockStore + }, + } + + h := newHandler(caManager, accountManager, permissionsManagerMock) + return h, caStore +} + +func newAuthenticatedRequest(t *testing.T, method, path string) *http.Request { + t.Helper() + req := httptest.NewRequest(method, path, nil) + return nbcontext.SetUserAuthInRequest(req, auth.UserAuth{ + UserId: testUserID, + AccountId: testAccountID, + }) +} + +func TestListCAs(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + // Seed a CA + caStore.caCerts = append(caStore.caCerts, &nbca.CACertificate{ + ID: "ca-1", + AccountID: testAccountID, + CertificatePEM: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----", + Fingerprint: "abc123", + NotBefore: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC), + IsActive: true, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodGet, "/api/ca") + + router := mux.NewRouter() + router.HandleFunc("/api/ca", h.listCAs).Methods("GET") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var resp []api.CACertificateResponse + err := json.NewDecoder(recorder.Body).Decode(&resp) + require.NoError(t, err) + require.Len(t, resp, 1) + + assert.Equal(t, "ca-1", resp[0].Id) + assert.Equal(t, "abc123", resp[0].Fingerprint) + assert.True(t, resp[0].IsActive) + // Should NOT include PEM in list response + assert.Nil(t, resp[0].CertificatePem) +} + +func TestGetCA(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + caStore.caCerts = append(caStore.caCerts, &nbca.CACertificate{ + ID: "ca-1", + AccountID: testAccountID, + CertificatePEM: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----", + Fingerprint: "abc123", + NotBefore: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC), + IsActive: true, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodGet, "/api/ca/ca-1") + + router := mux.NewRouter() + router.HandleFunc("/api/ca/{caId}", h.getCA).Methods("GET") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var resp api.CACertificateResponse + err := json.NewDecoder(recorder.Body).Decode(&resp) + require.NoError(t, err) + + assert.Equal(t, "ca-1", resp.Id) + // Should include PEM in detail response + assert.NotNil(t, resp.CertificatePem) + assert.NotEmpty(t, *resp.CertificatePem) +} + +func TestInitCA(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodPost, "/api/ca") + + router := mux.NewRouter() + router.HandleFunc("/api/ca", h.initCA).Methods("POST") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var resp api.CACertificateResponse + err := json.NewDecoder(recorder.Body).Decode(&resp) + require.NoError(t, err) + + assert.NotEmpty(t, resp.Id) + assert.NotEmpty(t, resp.Fingerprint) + assert.True(t, resp.IsActive) + assert.Len(t, caStore.caCerts, 1) +} + +func TestDeactivateCA(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + caStore.caCerts = append(caStore.caCerts, &nbca.CACertificate{ + ID: "ca-1", + AccountID: testAccountID, + IsActive: true, + }) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodDelete, "/api/ca/ca-1") + + router := mux.NewRouter() + router.HandleFunc("/api/ca/{caId}", h.deactivateCA).Methods("DELETE") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.False(t, caStore.caCerts[0].IsActive) +} + +func TestRotateCA(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + // Init first CA + caStore.caCerts = append(caStore.caCerts, &nbca.CACertificate{ + ID: "ca-1", + AccountID: testAccountID, + IsActive: true, + }) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodPost, "/api/ca/rotate") + + router := mux.NewRouter() + router.HandleFunc("/api/ca/rotate", h.rotateCA).Methods("POST") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var resp api.CACertificateResponse + err := json.NewDecoder(recorder.Body).Decode(&resp) + require.NoError(t, err) + + assert.NotEmpty(t, resp.Id) + // Should have 2 CAs now (original + rotated) + assert.Len(t, caStore.caCerts, 2) +} + +func TestListIssuedCerts(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + caStore.issuedCerts = append(caStore.issuedCerts, + &nbca.IssuedCertificate{ + ID: "cert-1", + AccountID: testAccountID, + PeerID: "peer-1", + SerialNumber: "1234", + DNSNames: []string{"peer1.netbird.example"}, + SigningType: "internal", + CreatedAt: time.Now().UTC(), + }, + &nbca.IssuedCertificate{ + ID: "cert-2", + AccountID: testAccountID, + PeerID: "peer-2", + SerialNumber: "5678", + DNSNames: []string{"peer2.netbird.example"}, + SigningType: "internal", + CreatedAt: time.Now().UTC(), + }, + ) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodGet, "/api/ca/certificates") + + router := mux.NewRouter() + router.HandleFunc("/api/ca/certificates", h.listIssuedCerts).Methods("GET") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var resp []api.IssuedCertificateResponse + err := json.NewDecoder(recorder.Body).Decode(&resp) + require.NoError(t, err) + assert.Len(t, resp, 2) +} + +func TestListIssuedCerts_FilterByPeer(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + caStore.issuedCerts = append(caStore.issuedCerts, + &nbca.IssuedCertificate{ + ID: "cert-1", + AccountID: testAccountID, + PeerID: "peer-1", + SerialNumber: "1234", + DNSNames: []string{"peer1.netbird.example"}, + SigningType: "internal", + CreatedAt: time.Now().UTC(), + }, + &nbca.IssuedCertificate{ + ID: "cert-2", + AccountID: testAccountID, + PeerID: "peer-2", + SerialNumber: "5678", + DNSNames: []string{"peer2.netbird.example"}, + SigningType: "internal", + CreatedAt: time.Now().UTC(), + }, + ) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodGet, "/api/ca/certificates?peer_id=peer-1") + + router := mux.NewRouter() + router.HandleFunc("/api/ca/certificates", h.listIssuedCerts).Methods("GET") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var resp []api.IssuedCertificateResponse + err := json.NewDecoder(recorder.Body).Decode(&resp) + require.NoError(t, err) + assert.Len(t, resp, 1) + assert.Equal(t, "peer-1", resp[0].PeerId) +} + +func TestRevokeCert(t *testing.T) { + h, caStore := setupTestHandler(t, true) + + caStore.issuedCerts = append(caStore.issuedCerts, &nbca.IssuedCertificate{ + ID: "cert-1", + AccountID: testAccountID, + PeerID: "peer-1", + SerialNumber: "1234", + CreatedAt: time.Now().UTC(), + }) + + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, http.MethodPost, "/api/ca/certificates/1234/revoke") + + router := mux.NewRouter() + router.HandleFunc("/api/ca/certificates/{serialNumber}/revoke", h.revokeCert).Methods("POST") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.True(t, caStore.issuedCerts[0].Revoked) +} + +func TestPermissionDenied(t *testing.T) { + h, _ := setupTestHandler(t, false) + + endpoints := []struct { + method string + path string + route string + handle http.HandlerFunc + }{ + {"GET", "/api/ca", "/api/ca", h.listCAs}, + {"POST", "/api/ca", "/api/ca", h.initCA}, + {"GET", "/api/ca/ca-1", "/api/ca/{caId}", h.getCA}, + {"DELETE", "/api/ca/ca-1", "/api/ca/{caId}", h.deactivateCA}, + {"POST", "/api/ca/rotate", "/api/ca/rotate", h.rotateCA}, + {"GET", "/api/ca/certificates", "/api/ca/certificates", h.listIssuedCerts}, + {"POST", "/api/ca/certificates/1234/revoke", "/api/ca/certificates/{serialNumber}/revoke", h.revokeCert}, + } + + for _, ep := range endpoints { + t.Run(ep.method+" "+ep.path, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := newAuthenticatedRequest(t, ep.method, ep.path) + + router := mux.NewRouter() + router.HandleFunc(ep.route, ep.handle).Methods(ep.method) + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + assert.Equal(t, http.StatusForbidden, recorder.Code, "expected 403 for %s %s, got %d: %s", ep.method, ep.path, recorder.Code, string(body)) + }) + } +} diff --git a/management/server/http/testing/testing_tools/channel/channel.go b/management/server/http/testing/testing_tools/channel/channel.go index 1d74f88d590..aa95b5067a4 100644 --- a/management/server/http/testing/testing_tools/channel/channel.go +++ b/management/server/http/testing/testing_tools/channel/channel.go @@ -33,6 +33,7 @@ import ( "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" + nbca "github.com/netbirdio/netbird/management/server/ca" serverauth "github.com/netbirdio/netbird/management/server/auth" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/groups" @@ -128,8 +129,10 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee groupsManagerMock := groups.NewManagerMock() customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "") zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager) + caManager := nbca.NewManager(store) + caManager.RegisterSigner(nbca.NewACMEPersistSigner()) - apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil) + apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, caManager) if err != nil { t.Fatalf("Failed to create API handler: %v", err) } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index b17757ffda4..192a0fe6511 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1180,7 +1180,7 @@ func TestToSyncResponse(t *testing.T) { } dnsCache := &cache.DNSConfigCache{} accountSettings := &types.Settings{RoutingPeerDNSResolutionEnabled: true} - response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, accountSettings, nil, []string{}, int64(dnsForwarderPort)) + response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, accountSettings, nil, []string{}, int64(dnsForwarderPort), nil) assert.NotNil(t, response) // assert peer config diff --git a/management/server/permissions/modules/module.go b/management/server/permissions/modules/module.go index 93007d4c1a3..8acf1365ecc 100644 --- a/management/server/permissions/modules/module.go +++ b/management/server/permissions/modules/module.go @@ -17,8 +17,9 @@ const ( Users Module = "users" SetupKeys Module = "setup_keys" Pats Module = "pats" - IdentityProviders Module = "identity_providers" - Services Module = "services" + IdentityProviders Module = "identity_providers" + Services Module = "services" + CertificateAuthority Module = "certificate_authority" ) var All = map[Module]struct{}{ @@ -36,6 +37,7 @@ var All = map[Module]struct{}{ Users: {}, SetupKeys: {}, Pats: {}, - IdentityProviders: {}, - Services: {}, + IdentityProviders: {}, + Services: {}, + CertificateAuthority: {}, } diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 8f147d915a4..ac19aed0525 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -28,6 +28,7 @@ import ( "gorm.io/gorm/logger" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/ca" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy" @@ -134,6 +135,7 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met &networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{}, &types.Job{}, &zones.Zone{}, &records.Record{}, &types.UserInviteRecord{}, &rpservice.Service{}, &rpservice.Target{}, &domain.Domain{}, &accesslogs.AccessLogEntry{}, &proxy.Proxy{}, + &ca.CACertificate{}, &ca.IssuedCertificate{}, &ca.CertIssuanceLog{}, ) if err != nil { return nil, fmt.Errorf("auto migratePreAuto: %w", err) @@ -5375,6 +5377,26 @@ func (s *SqlStore) SaveProxy(ctx context.Context, p *proxy.Proxy) error { return nil } +// CreateCACertificate persists a new CA certificate in the database. +// A copy is made before encryption to avoid mutating the caller's struct. +func (s *SqlStore) CreateCACertificate(ctx context.Context, caCert *ca.CACertificate) error { + if caCert == nil { + return status.Errorf(status.InvalidArgument, "CA certificate is nil") + } + + caCopy := *caCert + if err := caCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil { + return fmt.Errorf("encrypt CA certificate: %w", err) + } + + result := s.db.Create(&caCopy) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to create CA certificate in store: %v", result.Error) + return status.Errorf(status.Internal, "failed to create CA certificate in store") + } + return nil +} + // UpdateProxyHeartbeat updates the last_seen timestamp for a proxy func (s *SqlStore) UpdateProxyHeartbeat(ctx context.Context, proxyID string) error { result := s.db.WithContext(ctx). @@ -5389,6 +5411,60 @@ func (s *SqlStore) UpdateProxyHeartbeat(ctx context.Context, proxyID string) err return nil } +// GetCACertificateByID returns a CA certificate by its ID for the given account. +func (s *SqlStore) GetCACertificateByID(ctx context.Context, accountID, caID string) (*ca.CACertificate, error) { + var caCert ca.CACertificate + result := s.db.Take(&caCert, accountAndIDQueryCondition, accountID, caID) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.Errorf(status.NotFound, "CA certificate with ID %s not found", caID) + } + log.WithContext(ctx).Errorf("failed to get CA certificate from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get CA certificate from store") + } + + if err := caCert.DecryptSensitiveData(s.fieldEncrypt); err != nil { + return nil, fmt.Errorf("decrypt CA certificate: %w", err) + } + + return &caCert, nil +} + +// GetActiveCACertificates returns all active CA certificates for the given account, ordered by creation time descending. +func (s *SqlStore) GetActiveCACertificates(ctx context.Context, accountID string) ([]*ca.CACertificate, error) { + var caCerts []*ca.CACertificate + result := s.db.Where("account_id = ? AND is_active = ?", accountID, true). + Order("created_at DESC"). + Find(&caCerts) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to get active CA certificates from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get active CA certificates from store") + } + + for _, c := range caCerts { + if err := c.DecryptSensitiveData(s.fieldEncrypt); err != nil { + return nil, fmt.Errorf("decrypt CA certificate: %w", err) + } + } + + return caCerts, nil +} + +// DeactivateCACertificate marks a CA certificate as inactive. +func (s *SqlStore) DeactivateCACertificate(ctx context.Context, accountID, caID string) error { + result := s.db.Model(&ca.CACertificate{}). + Where(accountAndIDQueryCondition, accountID, caID). + Update("is_active", false) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to deactivate CA certificate in store: %v", result.Error) + return status.Errorf(status.Internal, "failed to deactivate CA certificate in store") + } + if result.RowsAffected == 0 { + return status.Errorf(status.NotFound, "CA certificate with ID %s not found", caID) + } + return nil +} + // GetActiveProxyClusterAddresses returns all unique cluster addresses for active proxies func (s *SqlStore) GetActiveProxyClusterAddresses(ctx context.Context) ([]string, error) { var addresses []string @@ -5426,3 +5502,123 @@ func (s *SqlStore) CleanupStaleProxies(ctx context.Context, inactivityDuration t return nil } + +// CreateIssuedCertificate persists a new issued certificate record. +func (s *SqlStore) CreateIssuedCertificate(ctx context.Context, cert *ca.IssuedCertificate) error { + if cert == nil { + return status.Errorf(status.InvalidArgument, "issued certificate is nil") + } + + result := s.db.Create(cert) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to create issued certificate in store: %v", result.Error) + return status.Errorf(status.Internal, "failed to create issued certificate in store") + } + return nil +} + +// GetIssuedCertificates returns all issued certificates for an account, ordered by creation time descending. +func (s *SqlStore) GetIssuedCertificates(ctx context.Context, accountID string) ([]*ca.IssuedCertificate, error) { + var certs []*ca.IssuedCertificate + result := s.db.Where("account_id = ?", accountID).Order("created_at DESC").Find(&certs) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to get issued certificates from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get issued certificates from store") + } + return certs, nil +} + +// GetIssuedCertificatesByPeer returns all issued certificates for a peer, ordered by creation time descending. +func (s *SqlStore) GetIssuedCertificatesByPeer(ctx context.Context, accountID, peerID string) ([]*ca.IssuedCertificate, error) { + var certs []*ca.IssuedCertificate + result := s.db.Where(accountAndPeerIDQueryCondition, accountID, peerID). + Order("created_at DESC"). + Find(&certs) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to get issued certificates from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get issued certificates from store") + } + return certs, nil +} + +// GetIssuedCertificateBySerial returns an issued certificate by its serial number. +func (s *SqlStore) GetIssuedCertificateBySerial(ctx context.Context, accountID, serialNumber string) (*ca.IssuedCertificate, error) { + var cert ca.IssuedCertificate + result := s.db.Take(&cert, "account_id = ? AND serial_number = ?", accountID, serialNumber) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.Errorf(status.NotFound, "issued certificate with serial %s not found", serialNumber) + } + log.WithContext(ctx).Errorf("failed to get issued certificate from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get issued certificate from store") + } + return &cert, nil +} + +// RevokeCertificate marks an issued certificate as revoked. +func (s *SqlStore) RevokeCertificate(ctx context.Context, accountID, serialNumber string) error { + result := s.db.Model(&ca.IssuedCertificate{}). + Where("account_id = ? AND serial_number = ?", accountID, serialNumber). + Update("revoked", true) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to revoke certificate in store: %v", result.Error) + return status.Errorf(status.Internal, "failed to revoke certificate in store") + } + if result.RowsAffected == 0 { + return status.Errorf(status.NotFound, "issued certificate with serial %s not found", serialNumber) + } + return nil +} + +// GetExpiringCertificates returns non-revoked issued certificates that expire before the given time. +func (s *SqlStore) GetExpiringCertificates(ctx context.Context, accountID string, expiringBefore time.Time) ([]*ca.IssuedCertificate, error) { + var certs []*ca.IssuedCertificate + result := s.db.Where("account_id = ? AND not_after < ? AND revoked = ?", accountID, expiringBefore, false). + Find(&certs) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to get expiring certificates from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get expiring certificates from store") + } + return certs, nil +} + +// GetPeersWithActiveWildcardCerts returns the set of peer IDs that have at least one +// active (non-revoked, non-expired) issued certificate with HasWildcard=true. +func (s *SqlStore) GetPeersWithActiveWildcardCerts(ctx context.Context, accountID string) (map[string]struct{}, error) { + var peerIDs []string + result := s.db.Model(&ca.IssuedCertificate{}). + Where("account_id = ? AND has_wildcard = ? AND revoked = ? AND not_after > ?", accountID, true, false, time.Now()). + Distinct().Pluck("peer_id", &peerIDs) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to get peers with wildcard certs from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get peers with wildcard certs from store") + } + m := make(map[string]struct{}, len(peerIDs)) + for _, id := range peerIDs { + m[id] = struct{}{} + } + return m, nil +} + +// CreateCertIssuanceLog records a certificate issuance event. +func (s *SqlStore) CreateCertIssuanceLog(ctx context.Context, entry *ca.CertIssuanceLog) error { + result := s.db.Create(entry) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to create cert issuance log in store: %v", result.Error) + return status.Errorf(status.Internal, "failed to create cert issuance log in store") + } + return nil +} + +// CountCertIssuancesInWindow counts certificate issuances for a peer within the given time window. +func (s *SqlStore) CountCertIssuancesInWindow(ctx context.Context, accountID, peerID string, since time.Time) (int64, error) { + var count int64 + result := s.db.Model(&ca.CertIssuanceLog{}). + Where("account_id = ? AND peer_id = ? AND issued_at >= ?", accountID, peerID, since). + Count(&count) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to count cert issuances in store: %v", result.Error) + return 0, status.Errorf(status.Internal, "failed to count cert issuances in store") + } + return count, nil +} diff --git a/management/server/store/store.go b/management/server/store/store.go index 5123cde7219..ffadfd2746c 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -25,6 +25,7 @@ import ( "gorm.io/gorm" "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/ca" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy" @@ -287,6 +288,23 @@ type Store interface { CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error) + + CreateCACertificate(ctx context.Context, ca *ca.CACertificate) error + GetCACertificateByID(ctx context.Context, accountID, caID string) (*ca.CACertificate, error) + GetActiveCACertificates(ctx context.Context, accountID string) ([]*ca.CACertificate, error) + DeactivateCACertificate(ctx context.Context, accountID, caID string) error + + CreateIssuedCertificate(ctx context.Context, cert *ca.IssuedCertificate) error + GetIssuedCertificates(ctx context.Context, accountID string) ([]*ca.IssuedCertificate, error) + GetIssuedCertificatesByPeer(ctx context.Context, accountID, peerID string) ([]*ca.IssuedCertificate, error) + GetIssuedCertificateBySerial(ctx context.Context, accountID, serialNumber string) (*ca.IssuedCertificate, error) + RevokeCertificate(ctx context.Context, accountID, serialNumber string) error + GetExpiringCertificates(ctx context.Context, accountID string, expiringBefore time.Time) ([]*ca.IssuedCertificate, error) + + GetPeersWithActiveWildcardCerts(ctx context.Context, accountID string) (map[string]struct{}, error) + + CreateCertIssuanceLog(ctx context.Context, entry *ca.CertIssuanceLog) error + CountCertIssuancesInWindow(ctx context.Context, accountID, peerID string, since time.Time) (int64, error) } const ( diff --git a/management/server/types/account.go b/management/server/types/account.go index 6145ceeb2fa..dec32651971 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -497,7 +497,7 @@ func getUniqueHostLabel(name string, peerLabels LookupMap) string { return "" } -func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdns.CustomZone { +func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string, wildcardPeers map[string]struct{}) nbdns.CustomZone { var merr *multierror.Error if dnsDomain == "" { @@ -532,6 +532,22 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn }) sb.Reset() + if _, ok := wildcardPeers[peer.ID]; ok { + sb.Grow(2 + len(peer.DNSLabel) + len(domainSuffix)) + sb.WriteString("*.") + sb.WriteString(peer.DNSLabel) + sb.WriteString(domainSuffix) + + customZone.Records = append(customZone.Records, nbdns.SimpleRecord{ + Name: sb.String(), + Type: int(dns.TypeA), + Class: nbdns.DefaultClass, + TTL: defaultTTL, + RData: peer.IP.String(), + }) + sb.Reset() + } + for _, extraLabel := range peer.ExtraDNSLabels { sb.Grow(len(extraLabel) + len(domainSuffix)) sb.WriteString(extraLabel) diff --git a/management/server/types/settings.go b/management/server/types/settings.go index e165968fc70..27f8a32d130 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -52,6 +52,33 @@ type Settings struct { // PeerExposeGroups list of peer group IDs allowed to expose services PeerExposeGroups []string `gorm:"serializer:json"` + // CertificateAuthorityEnabled globally enables or disables the internal certificate authority + CertificateAuthorityEnabled bool + + // CertWildcardAllowed allows peers to request wildcard certificates + CertWildcardAllowed bool + + // CertCAValidity is the validity duration for CA certificates (default: 10 years) + CertCAValidity time.Duration + + // CertCADisplayName is used in the CA certificate CommonName (e.g. "Zakhar" → "Zakhar Internal CA") + CertCADisplayName string + + // CertCAOrganization is the x509 Organization field in the CA certificate + CertCAOrganization string + + // CertDefaultValidity is the default validity duration for issued peer certificates + CertDefaultValidity time.Duration + + // CertRateLimitPerPeer is the maximum number of certificate issuances per peer per day + CertRateLimitPerPeer int + + // CertACMEEnabled indicates if the ACME DNS-PERSIST-01 backend is enabled (stub for now) + CertACMEEnabled bool + + // CertACMECAURL is the ACME CA directory URL (stub for now) + CertACMECAURL string + // Extra is a dictionary of Account settings Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` @@ -87,6 +114,15 @@ func (s *Settings) Copy() *Settings { RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled, PeerExposeEnabled: s.PeerExposeEnabled, PeerExposeGroups: slices.Clone(s.PeerExposeGroups), + CertificateAuthorityEnabled: s.CertificateAuthorityEnabled, + CertWildcardAllowed: s.CertWildcardAllowed, + CertCAValidity: s.CertCAValidity, + CertCADisplayName: s.CertCADisplayName, + CertCAOrganization: s.CertCAOrganization, + CertDefaultValidity: s.CertDefaultValidity, + CertRateLimitPerPeer: s.CertRateLimitPerPeer, + CertACMEEnabled: s.CertACMEEnabled, + CertACMECAURL: s.CertACMECAURL, LazyConnectionEnabled: s.LazyConnectionEnabled, DNSDomain: s.DNSDomain, NetworkRange: s.NetworkRange, diff --git a/management/server/types/testdata/networkmap_golden_new_with_onpeeradded_router.json b/management/server/types/testdata/networkmap_golden_new_with_onpeeradded_router.json new file mode 100644 index 00000000000..8e322a5b2e6 --- /dev/null +++ b/management/server/types/testdata/networkmap_golden_new_with_onpeeradded_router.json @@ -0,0 +1,11093 @@ +{ + "Peers": [ + { + "ID": "peer-0", + "Key": "key-peer-0", + "IP": "100.64.0.1", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer1", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-1", + "Key": "key-peer-1", + "IP": "100.64.0.2", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer2", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-10", + "Key": "key-peer-10", + "IP": "100.64.0.11", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer11", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-11", + "Key": "key-peer-11", + "IP": "100.64.0.12", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer12", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-12", + "Key": "key-peer-12", + "IP": "100.64.0.13", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer13", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-13", + "Key": "key-peer-13", + "IP": "100.64.0.14", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer14", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-14", + "Key": "key-peer-14", + "IP": "100.64.0.15", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer15", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-15", + "Key": "key-peer-15", + "IP": "100.64.0.16", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer16", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-16", + "Key": "key-peer-16", + "IP": "100.64.0.17", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer17", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-17", + "Key": "key-peer-17", + "IP": "100.64.0.18", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer18", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-18", + "Key": "key-peer-18", + "IP": "100.64.0.19", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer19", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-19", + "Key": "key-peer-19", + "IP": "100.64.0.20", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer20", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-2", + "Key": "key-peer-2", + "IP": "100.64.0.3", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer3", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-20", + "Key": "key-peer-20", + "IP": "100.64.0.21", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer21", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-21", + "Key": "key-peer-21", + "IP": "100.64.0.22", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer22", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-22", + "Key": "key-peer-22", + "IP": "100.64.0.23", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer23", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-23", + "Key": "key-peer-23", + "IP": "100.64.0.24", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer24", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-24", + "Key": "key-peer-24", + "IP": "100.64.0.25", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer25", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-25", + "Key": "key-peer-25", + "IP": "100.64.0.26", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer26", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-26", + "Key": "key-peer-26", + "IP": "100.64.0.27", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer27", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-27", + "Key": "key-peer-27", + "IP": "100.64.0.28", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer28", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-28", + "Key": "key-peer-28", + "IP": "100.64.0.29", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer29", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-29", + "Key": "key-peer-29", + "IP": "100.64.0.30", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer30", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-3", + "Key": "key-peer-3", + "IP": "100.64.0.4", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer4", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-30", + "Key": "key-peer-30", + "IP": "100.64.0.31", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer31", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-31", + "Key": "key-peer-31", + "IP": "100.64.0.32", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer32", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-32", + "Key": "key-peer-32", + "IP": "100.64.0.33", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer33", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-33", + "Key": "key-peer-33", + "IP": "100.64.0.34", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer34", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-34", + "Key": "key-peer-34", + "IP": "100.64.0.35", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer35", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-35", + "Key": "key-peer-35", + "IP": "100.64.0.36", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer36", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-36", + "Key": "key-peer-36", + "IP": "100.64.0.37", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer37", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-37", + "Key": "key-peer-37", + "IP": "100.64.0.38", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer38", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-38", + "Key": "key-peer-38", + "IP": "100.64.0.39", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer39", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-39", + "Key": "key-peer-39", + "IP": "100.64.0.40", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer40", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-4", + "Key": "key-peer-4", + "IP": "100.64.0.5", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer5", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-40", + "Key": "key-peer-40", + "IP": "100.64.0.41", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer41", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-41", + "Key": "key-peer-41", + "IP": "100.64.0.42", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer42", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-42", + "Key": "key-peer-42", + "IP": "100.64.0.43", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer43", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-43", + "Key": "key-peer-43", + "IP": "100.64.0.44", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer44", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-44", + "Key": "key-peer-44", + "IP": "100.64.0.45", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer45", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-45", + "Key": "key-peer-45", + "IP": "100.64.0.46", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer46", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-46", + "Key": "key-peer-46", + "IP": "100.64.0.47", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer47", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-47", + "Key": "key-peer-47", + "IP": "100.64.0.48", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer48", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-48", + "Key": "key-peer-48", + "IP": "100.64.0.49", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer49", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-49", + "Key": "key-peer-49", + "IP": "100.64.0.50", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer50", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-5", + "Key": "key-peer-5", + "IP": "100.64.0.6", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer6", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-50", + "Key": "key-peer-50", + "IP": "100.64.0.51", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer51", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-51", + "Key": "key-peer-51", + "IP": "100.64.0.52", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer52", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-52", + "Key": "key-peer-52", + "IP": "100.64.0.53", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer53", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-53", + "Key": "key-peer-53", + "IP": "100.64.0.54", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer54", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-54", + "Key": "key-peer-54", + "IP": "100.64.0.55", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer55", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-55", + "Key": "key-peer-55", + "IP": "100.64.0.56", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer56", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-56", + "Key": "key-peer-56", + "IP": "100.64.0.57", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer57", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-57", + "Key": "key-peer-57", + "IP": "100.64.0.58", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer58", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-58", + "Key": "key-peer-58", + "IP": "100.64.0.59", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer59", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-59", + "Key": "key-peer-59", + "IP": "100.64.0.60", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer60", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-6", + "Key": "key-peer-6", + "IP": "100.64.0.7", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer7", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-61", + "Key": "key-peer-61", + "IP": "100.64.0.62", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer62", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-62", + "Key": "key-peer-62", + "IP": "100.64.0.63", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer63", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-63", + "Key": "key-peer-63", + "IP": "100.64.0.64", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer64", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-64", + "Key": "key-peer-64", + "IP": "100.64.0.65", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer65", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-65", + "Key": "key-peer-65", + "IP": "100.64.0.66", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer66", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-66", + "Key": "key-peer-66", + "IP": "100.64.0.67", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer67", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-67", + "Key": "key-peer-67", + "IP": "100.64.0.68", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer68", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-68", + "Key": "key-peer-68", + "IP": "100.64.0.69", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer69", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-69", + "Key": "key-peer-69", + "IP": "100.64.0.70", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer70", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-7", + "Key": "key-peer-7", + "IP": "100.64.0.8", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer8", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-70", + "Key": "key-peer-70", + "IP": "100.64.0.71", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer71", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-71", + "Key": "key-peer-71", + "IP": "100.64.0.72", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer72", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-72", + "Key": "key-peer-72", + "IP": "100.64.0.73", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer73", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-73", + "Key": "key-peer-73", + "IP": "100.64.0.74", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer74", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-74", + "Key": "key-peer-74", + "IP": "100.64.0.75", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer75", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-75", + "Key": "key-peer-75", + "IP": "100.64.0.76", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer76", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-76", + "Key": "key-peer-76", + "IP": "100.64.0.77", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer77", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-77", + "Key": "key-peer-77", + "IP": "100.64.0.78", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer78", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-78", + "Key": "key-peer-78", + "IP": "100.64.0.79", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer79", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-79", + "Key": "key-peer-79", + "IP": "100.64.0.80", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer80", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-8", + "Key": "key-peer-8", + "IP": "100.64.0.9", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer9", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-80", + "Key": "key-peer-80", + "IP": "100.64.0.81", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer81", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-81", + "Key": "key-peer-81", + "IP": "100.64.0.82", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer82", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-82", + "Key": "key-peer-82", + "IP": "100.64.0.83", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer83", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-83", + "Key": "key-peer-83", + "IP": "100.64.0.84", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer84", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-84", + "Key": "key-peer-84", + "IP": "100.64.0.85", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer85", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-85", + "Key": "key-peer-85", + "IP": "100.64.0.86", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer86", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-86", + "Key": "key-peer-86", + "IP": "100.64.0.87", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer87", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-87", + "Key": "key-peer-87", + "IP": "100.64.0.88", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer88", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-88", + "Key": "key-peer-88", + "IP": "100.64.0.89", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer89", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-89", + "Key": "key-peer-89", + "IP": "100.64.0.90", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer90", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-9", + "Key": "key-peer-9", + "IP": "100.64.0.10", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer10", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-90", + "Key": "key-peer-90", + "IP": "100.64.0.91", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer91", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-91", + "Key": "key-peer-91", + "IP": "100.64.0.92", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer92", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-92", + "Key": "key-peer-92", + "IP": "100.64.0.93", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer93", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-93", + "Key": "key-peer-93", + "IP": "100.64.0.94", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer94", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-94", + "Key": "key-peer-94", + "IP": "100.64.0.95", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer95", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-95", + "Key": "key-peer-95", + "IP": "100.64.0.96", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer96", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-96", + "Key": "key-peer-96", + "IP": "100.64.0.97", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer97", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-97", + "Key": "key-peer-97", + "IP": "100.64.0.98", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.25.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer98", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + }, + { + "ID": "peer-new-router-102", + "Key": "key-peer-new-router-102", + "IP": "100.64.1.2", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.26.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "newrouter102", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": false, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + } + ], + "Network": { + "id": "net-golden-test", + "Net": { + "IP": "100.64.0.0", + "Mask": "//8AAA==" + }, + "Dns": "", + "Serial": 2 + }, + "Routes": [ + { + "ID": "res-database:peer-95", + "AccountID": "account-golden-test", + "Network": "", + "Domains": null, + "KeepRoute": true, + "NetID": "", + "Description": "", + "Peer": "key-peer-95", + "PeerID": "peer-95", + "PeerGroups": null, + "NetworkType": 0, + "Masquerade": false, + "Metric": 0, + "Enabled": true, + "Groups": null, + "AccessControlGroups": null, + "SkipAutoApply": false + }, + { + "ID": "route-ha-1:peer-60", + "AccountID": "account-golden-test", + "Network": "10.10.0.0/16", + "Domains": null, + "KeepRoute": false, + "NetID": "", + "Description": "HA Route 1", + "Peer": "key-peer-60", + "PeerID": "peer-80", + "PeerGroups": null, + "NetworkType": 0, + "Masquerade": false, + "Metric": 1000, + "Enabled": true, + "Groups": [ + "group-all" + ], + "AccessControlGroups": [ + "group-all" + ], + "SkipAutoApply": false + }, + { + "ID": "route-ha-2:peer-60", + "AccountID": "account-golden-test", + "Network": "10.10.0.0/16", + "Domains": null, + "KeepRoute": false, + "NetID": "", + "Description": "HA Route 2", + "Peer": "key-peer-60", + "PeerID": "peer-90", + "PeerGroups": null, + "NetworkType": 0, + "Masquerade": false, + "Metric": 900, + "Enabled": true, + "Groups": [ + "group-dev", + "group-ops" + ], + "AccessControlGroups": [ + "group-all" + ], + "SkipAutoApply": false + }, + { + "ID": "route-main:peer-60", + "AccountID": "account-golden-test", + "Network": "192.168.10.0/24", + "Domains": null, + "KeepRoute": false, + "NetID": "", + "Description": "Route to internal resource", + "Peer": "key-peer-60", + "PeerID": "peer-75", + "PeerGroups": null, + "NetworkType": 0, + "Masquerade": false, + "Metric": 0, + "Enabled": true, + "Groups": [ + "group-dev", + "group-ops" + ], + "AccessControlGroups": [ + "group-dev" + ], + "SkipAutoApply": false + }, + { + "ID": "route-new-router:peer-60", + "AccountID": "account-golden-test", + "Network": "172.16.0.0/24", + "Domains": null, + "KeepRoute": false, + "NetID": "", + "Description": "Route from new router", + "Peer": "key-peer-60", + "PeerID": "peer-new-router-102", + "PeerGroups": null, + "NetworkType": 0, + "Masquerade": false, + "Metric": 0, + "Enabled": true, + "Groups": [ + "group-dev", + "group-ops" + ], + "AccessControlGroups": [ + "group-dev" + ], + "SkipAutoApply": false + } + ], + "DNSConfig": { + "ServiceEnable": false, + "NameServerGroups": null, + "CustomZones": null, + "ForwarderPort": 0 + }, + "OfflinePeers": [ + { + "ID": "peer-98", + "Key": "key-peer-98", + "IP": "100.64.0.99", + "Meta": { + "Hostname": "", + "GoOS": "linux", + "Kernel": "", + "Core": "", + "Platform": "", + "OS": "", + "OSVersion": "", + "WtVersion": "0.40.0", + "UIVersion": "", + "KernelVersion": "", + "NetworkAddresses": null, + "SystemSerialNumber": "", + "SystemProductName": "", + "SystemManufacturer": "", + "Environment": { + "Cloud": "", + "Platform": "" + }, + "Flags": { + "RosenpassEnabled": false, + "RosenpassPermissive": false, + "ServerSSHAllowed": false, + "DisableClientRoutes": false, + "DisableServerRoutes": false, + "DisableDNS": false, + "DisableFirewall": false, + "BlockLANAccess": false, + "BlockInbound": false, + "LazyConnectionEnabled": false + }, + "Files": null + }, + "ProxyMeta": { + "Embedded": false, + "Cluster": "" + }, + "Name": "", + "DNSLabel": "peer99", + "Status": { + "LastSeen": "0001-01-01T00:00:00Z", + "Connected": true, + "LoginExpired": false, + "RequiresApproval": false + }, + "UserID": "user-admin", + "SSHKey": "", + "SSHEnabled": false, + "LoginExpirationEnabled": true, + "InactivityExpirationEnabled": false, + "LastLogin": "0001-01-01T00:00:00Z", + "CreatedAt": "0001-01-01T00:00:00Z", + "Ephemeral": false, + "Location": { + "ConnectionIP": "", + "CountryCode": "", + "CityName": "", + "GeoNameID": 0 + }, + "ExtraDNSLabels": null, + "AllowExtraDNSLabels": false + } + ], + "FirewallRules": [ + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.1", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.1", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.1", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.1", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.1", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.10", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.10", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.10", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.10", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.10", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.11", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.11", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.11", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.11", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.11", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.12", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.12", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.12", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.12", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.12", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.13", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.13", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.13", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.13", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.13", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.14", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.14", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.14", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.14", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.14", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.15", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.15", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.15", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.15", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.15", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.16", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.16", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.16", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.16", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.16", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.17", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.17", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.17", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.17", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.17", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.18", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.18", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.18", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.18", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.18", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.19", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.19", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.19", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.19", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.19", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.2", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.2", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.2", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.2", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.2", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.20", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.20", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.20", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.20", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.20", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.21", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.21", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.21", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.21", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.21", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.22", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.22", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.22", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.22", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.22", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.23", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.23", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.23", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.23", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.23", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.24", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.24", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.24", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.24", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.24", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.25", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.25", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.25", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.25", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.25", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.26", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.26", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.26", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.26", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.26", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.27", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.27", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.27", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.27", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.27", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.28", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.28", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.28", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.28", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.28", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.29", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.29", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.29", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.29", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.29", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.3", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.3", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.3", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.3", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.3", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.30", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.30", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.30", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.30", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.30", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.31", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.31", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.31", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.31", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.31", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.32", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.32", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.32", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.32", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.32", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.33", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.33", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.33", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.33", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.33", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.34", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.34", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.34", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.34", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.34", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.35", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.35", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.35", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.35", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.35", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.36", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.36", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.36", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.36", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.36", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.37", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.37", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.37", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.37", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.37", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.38", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.38", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.38", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.38", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.38", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.39", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.39", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.39", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.39", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.39", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.4", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.4", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.4", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.4", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.4", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.40", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.40", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.40", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.40", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.40", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.41", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.41", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.41", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.41", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.41", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.42", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.42", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.42", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.42", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.42", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.43", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.43", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.43", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.43", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.43", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.44", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.44", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.44", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.44", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.44", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.45", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.45", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.45", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.45", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.45", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.46", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.46", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.46", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.46", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.46", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.47", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.47", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.47", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.47", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.47", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.48", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.48", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.48", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.48", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.48", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.49", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.49", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.49", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.49", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.49", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.5", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.5", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.5", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.5", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.5", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.50", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.50", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.50", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.50", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.50", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.51", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.51", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.52", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.52", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.53", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.53", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.54", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.54", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.55", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.55", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.56", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.56", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.57", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.57", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.58", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.58", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.59", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.59", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.6", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.6", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.6", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.6", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.6", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.60", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.60", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.62", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.62", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.63", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.63", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.64", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.64", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.65", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.65", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.66", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.66", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.67", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.67", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.68", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.68", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.69", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.69", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.7", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.7", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.7", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.7", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.7", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.70", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.70", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.71", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.71", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.72", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.72", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.73", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.73", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.74", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.74", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.75", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.75", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.76", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.76", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.77", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.77", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.78", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.78", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.79", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.79", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.8", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.8", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.8", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.8", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.8", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.80", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.80", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.81", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.81", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.82", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.82", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.83", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.83", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.84", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.84", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.85", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.85", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.86", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.86", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.87", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.87", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.88", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.88", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.89", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.89", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.9", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.9", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-ssh", + "PeerIP": "100.64.0.9", + "Direction": 0, + "Action": "accept", + "Protocol": "tcp", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.9", + "Direction": 0, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-drop", + "PeerIP": "100.64.0.9", + "Direction": 1, + "Action": "drop", + "Protocol": "tcp", + "Port": "5432", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.90", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.90", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.91", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.91", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.92", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.92", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.93", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.93", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.94", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.94", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.95", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.95", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.96", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.96", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.97", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.97", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.98", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.98", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.99", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.0.99", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.1.2", + "Direction": 0, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + }, + { + "PolicyID": "policy-all", + "PeerIP": "100.64.1.2", + "Direction": 1, + "Action": "accept", + "Protocol": "all", + "Port": "", + "PortRange": { + "Start": 0, + "End": 0 + } + } + ], + "RoutesFirewallRules": [ + { + "PolicyID": "", + "RouteID": "route-ha-1:peer-60", + "SourceRanges": [ + "100.64.0.1/32", + "100.64.0.10/32", + "100.64.0.11/32", + "100.64.0.12/32", + "100.64.0.13/32", + "100.64.0.14/32", + "100.64.0.15/32", + "100.64.0.16/32", + "100.64.0.17/32", + "100.64.0.18/32", + "100.64.0.19/32", + "100.64.0.2/32", + "100.64.0.20/32", + "100.64.0.21/32", + "100.64.0.22/32", + "100.64.0.23/32", + "100.64.0.24/32", + "100.64.0.25/32", + "100.64.0.26/32", + "100.64.0.27/32", + "100.64.0.28/32", + "100.64.0.29/32", + "100.64.0.3/32", + "100.64.0.30/32", + "100.64.0.31/32", + "100.64.0.32/32", + "100.64.0.33/32", + "100.64.0.34/32", + "100.64.0.35/32", + "100.64.0.36/32", + "100.64.0.37/32", + "100.64.0.38/32", + "100.64.0.39/32", + "100.64.0.4/32", + "100.64.0.40/32", + "100.64.0.41/32", + "100.64.0.42/32", + "100.64.0.43/32", + "100.64.0.44/32", + "100.64.0.45/32", + "100.64.0.46/32", + "100.64.0.47/32", + "100.64.0.48/32", + "100.64.0.49/32", + "100.64.0.5/32", + "100.64.0.50/32", + "100.64.0.51/32", + "100.64.0.52/32", + "100.64.0.53/32", + "100.64.0.54/32", + "100.64.0.55/32", + "100.64.0.56/32", + "100.64.0.57/32", + "100.64.0.58/32", + "100.64.0.59/32", + "100.64.0.6/32", + "100.64.0.60/32", + "100.64.0.62/32", + "100.64.0.63/32", + "100.64.0.64/32", + "100.64.0.65/32", + "100.64.0.66/32", + "100.64.0.67/32", + "100.64.0.68/32", + "100.64.0.69/32", + "100.64.0.7/32", + "100.64.0.70/32", + "100.64.0.71/32", + "100.64.0.72/32", + "100.64.0.73/32", + "100.64.0.74/32", + "100.64.0.75/32", + "100.64.0.76/32", + "100.64.0.77/32", + "100.64.0.78/32", + "100.64.0.79/32", + "100.64.0.8/32", + "100.64.0.80/32", + "100.64.0.81/32", + "100.64.0.82/32", + "100.64.0.83/32", + "100.64.0.84/32", + "100.64.0.85/32", + "100.64.0.86/32", + "100.64.0.87/32", + "100.64.0.88/32", + "100.64.0.89/32", + "100.64.0.9/32", + "100.64.0.90/32", + "100.64.0.91/32", + "100.64.0.92/32", + "100.64.0.93/32", + "100.64.0.94/32", + "100.64.0.95/32", + "100.64.0.96/32", + "100.64.0.97/32", + "100.64.0.98/32", + "100.64.0.99/32", + "100.64.1.2/32" + ], + "Action": "accept", + "Destination": "10.10.0.0/16", + "Protocol": "all", + "Port": 0, + "PortRange": { + "Start": 0, + "End": 0 + }, + "Domains": null, + "IsDynamic": false + }, + { + "PolicyID": "", + "RouteID": "route-ha-2:peer-60", + "SourceRanges": [ + "100.64.0.1/32", + "100.64.0.10/32", + "100.64.0.11/32", + "100.64.0.12/32", + "100.64.0.13/32", + "100.64.0.14/32", + "100.64.0.15/32", + "100.64.0.16/32", + "100.64.0.17/32", + "100.64.0.18/32", + "100.64.0.19/32", + "100.64.0.2/32", + "100.64.0.20/32", + "100.64.0.21/32", + "100.64.0.22/32", + "100.64.0.23/32", + "100.64.0.24/32", + "100.64.0.25/32", + "100.64.0.26/32", + "100.64.0.27/32", + "100.64.0.28/32", + "100.64.0.29/32", + "100.64.0.3/32", + "100.64.0.30/32", + "100.64.0.31/32", + "100.64.0.32/32", + "100.64.0.33/32", + "100.64.0.34/32", + "100.64.0.35/32", + "100.64.0.36/32", + "100.64.0.37/32", + "100.64.0.38/32", + "100.64.0.39/32", + "100.64.0.4/32", + "100.64.0.40/32", + "100.64.0.41/32", + "100.64.0.42/32", + "100.64.0.43/32", + "100.64.0.44/32", + "100.64.0.45/32", + "100.64.0.46/32", + "100.64.0.47/32", + "100.64.0.48/32", + "100.64.0.49/32", + "100.64.0.5/32", + "100.64.0.50/32", + "100.64.0.51/32", + "100.64.0.52/32", + "100.64.0.53/32", + "100.64.0.54/32", + "100.64.0.55/32", + "100.64.0.56/32", + "100.64.0.57/32", + "100.64.0.58/32", + "100.64.0.59/32", + "100.64.0.6/32", + "100.64.0.60/32", + "100.64.0.62/32", + "100.64.0.63/32", + "100.64.0.64/32", + "100.64.0.65/32", + "100.64.0.66/32", + "100.64.0.67/32", + "100.64.0.68/32", + "100.64.0.69/32", + "100.64.0.7/32", + "100.64.0.70/32", + "100.64.0.71/32", + "100.64.0.72/32", + "100.64.0.73/32", + "100.64.0.74/32", + "100.64.0.75/32", + "100.64.0.76/32", + "100.64.0.77/32", + "100.64.0.78/32", + "100.64.0.79/32", + "100.64.0.8/32", + "100.64.0.80/32", + "100.64.0.81/32", + "100.64.0.82/32", + "100.64.0.83/32", + "100.64.0.84/32", + "100.64.0.85/32", + "100.64.0.86/32", + "100.64.0.87/32", + "100.64.0.88/32", + "100.64.0.89/32", + "100.64.0.9/32", + "100.64.0.90/32", + "100.64.0.91/32", + "100.64.0.92/32", + "100.64.0.93/32", + "100.64.0.94/32", + "100.64.0.95/32", + "100.64.0.96/32", + "100.64.0.97/32", + "100.64.0.98/32", + "100.64.0.99/32", + "100.64.1.2/32" + ], + "Action": "accept", + "Destination": "10.10.0.0/16", + "Protocol": "all", + "Port": 0, + "PortRange": { + "Start": 0, + "End": 0 + }, + "Domains": null, + "IsDynamic": false + } + ], + "ForwardingRules": null, + "AuthorizedUsers": { + "admin": { + "user-dev": {}, + "user-ops": {} + }, + "root": { + "user-dev": {}, + "user-ops": {} + } + }, + "EnableSSH": true +} \ No newline at end of file diff --git a/shared/management/client/cert.go b/shared/management/client/cert.go new file mode 100644 index 00000000000..49a437cdab1 --- /dev/null +++ b/shared/management/client/cert.go @@ -0,0 +1,79 @@ +package client + +import ( + "context" + "fmt" + + "github.com/netbirdio/netbird/encryption" + "github.com/netbirdio/netbird/shared/management/proto" +) + +// SignCertificate sends a CSR to the management server for signing. +func (c *GrpcClient) SignCertificate(ctx context.Context, csrDER []byte, signingType proto.CertSigningType, wildcard bool) (*proto.SignCertificateResponse, error) { + serverPubKey, err := c.GetServerPublicKey() + if err != nil { + return nil, err + } + + req := &proto.SignCertificateRequest{ + CsrDer: csrDER, + SigningType: signingType, + Wildcard: wildcard, + } + + encReq, err := encryption.EncryptMessage(*serverPubKey, c.key, req) + if err != nil { + return nil, fmt.Errorf("encrypt sign certificate request: %w", err) + } + + mgmCtx, cancel := context.WithTimeout(ctx, ConnectTimeout) + defer cancel() + + resp, err := c.realClient.SignCertificate(mgmCtx, &proto.EncryptedMessage{ + WgPubKey: c.key.PublicKey().String(), + Body: encReq, + }) + if err != nil { + return nil, err + } + + signResp := &proto.SignCertificateResponse{} + if err := encryption.DecryptMessage(*serverPubKey, c.key, resp.Body, signResp); err != nil { + return nil, fmt.Errorf("decrypt sign certificate response: %w", err) + } + + return signResp, nil +} + +// GetCACertificates fetches the active CA certificates from the management server. +func (c *GrpcClient) GetCACertificates(ctx context.Context) (*proto.GetCACertificatesResponse, error) { + serverPubKey, err := c.GetServerPublicKey() + if err != nil { + return nil, err + } + + req := &proto.GetCACertificatesRequest{} + + encReq, err := encryption.EncryptMessage(*serverPubKey, c.key, req) + if err != nil { + return nil, fmt.Errorf("encrypt get CA certificates request: %w", err) + } + + mgmCtx, cancel := context.WithTimeout(ctx, ConnectTimeout) + defer cancel() + + resp, err := c.realClient.GetCACertificates(mgmCtx, &proto.EncryptedMessage{ + WgPubKey: c.key.PublicKey().String(), + Body: encReq, + }) + if err != nil { + return nil, err + } + + caResp := &proto.GetCACertificatesResponse{} + if err := encryption.DecryptMessage(*serverPubKey, c.key, resp.Body, caResp); err != nil { + return nil, fmt.Errorf("decrypt get CA certificates response: %w", err) + } + + return caResp, nil +} diff --git a/shared/management/client/client.go b/shared/management/client/client.go index ba525602e53..60a439875f4 100644 --- a/shared/management/client/client.go +++ b/shared/management/client/client.go @@ -28,4 +28,6 @@ type Client interface { CreateExpose(ctx context.Context, req ExposeRequest) (*ExposeResponse, error) RenewExpose(ctx context.Context, domain string) error StopExpose(ctx context.Context, domain string) error + SignCertificate(ctx context.Context, csrDER []byte, signingType proto.CertSigningType, wildcard bool) (*proto.SignCertificateResponse, error) + GetCACertificates(ctx context.Context) (*proto.GetCACertificatesResponse, error) } diff --git a/shared/management/client/mock.go b/shared/management/client/mock.go index 57256d6d47d..1590d2ce214 100644 --- a/shared/management/client/mock.go +++ b/shared/management/client/mock.go @@ -25,6 +25,8 @@ type MockClient struct { CreateExposeFunc func(ctx context.Context, req ExposeRequest) (*ExposeResponse, error) RenewExposeFunc func(ctx context.Context, domain string) error StopExposeFunc func(ctx context.Context, domain string) error + SignCertificateFunc func(ctx context.Context, csrDER []byte, signingType proto.CertSigningType, wildcard bool) (*proto.SignCertificateResponse, error) + GetCACertificatesFunc func(ctx context.Context) (*proto.GetCACertificatesResponse, error) } func (m *MockClient) IsHealthy() bool { @@ -126,3 +128,17 @@ func (m *MockClient) StopExpose(ctx context.Context, domain string) error { } return m.StopExposeFunc(ctx, domain) } + +func (m *MockClient) SignCertificate(ctx context.Context, csrDER []byte, signingType proto.CertSigningType, wildcard bool) (*proto.SignCertificateResponse, error) { + if m.SignCertificateFunc == nil { + return nil, nil + } + return m.SignCertificateFunc(ctx, csrDER, signingType, wildcard) +} + +func (m *MockClient) GetCACertificates(ctx context.Context) (*proto.GetCACertificatesResponse, error) { + if m.GetCACertificatesFunc == nil { + return nil, nil + } + return m.GetCACertificatesFunc(ctx) +} diff --git a/shared/management/client/rest/ca.go b/shared/management/client/rest/ca.go new file mode 100644 index 00000000000..594acee546b --- /dev/null +++ b/shared/management/client/rest/ca.go @@ -0,0 +1,116 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// CertificateAuthorityAPI APIs for Certificate Authority Management, do not use directly +type CertificateAuthorityAPI struct { + c *Client +} + +// ListCAs list all active CA certificates +func (a *CertificateAuthorityAPI) ListCAs(ctx context.Context) ([]api.CACertificateResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/ca", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.CACertificateResponse](resp) + return ret, err +} + +// InitCA initialize a new certificate authority +func (a *CertificateAuthorityAPI) InitCA(ctx context.Context) (*api.CACertificateResponse, error) { + resp, err := a.c.NewRequest(ctx, "POST", "/api/ca", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.CACertificateResponse](resp) + if err != nil { + return nil, err + } + return &ret, nil +} + +// GetCA get CA certificate detail +func (a *CertificateAuthorityAPI) GetCA(ctx context.Context, caID string) (*api.CACertificateResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/ca/"+caID, nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.CACertificateResponse](resp) + if err != nil { + return nil, err + } + return &ret, nil +} + +// DeactivateCA deactivate a CA certificate +func (a *CertificateAuthorityAPI) DeactivateCA(ctx context.Context, caID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/ca/"+caID, nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} + +// RotateCA rotate the certificate authority +func (a *CertificateAuthorityAPI) RotateCA(ctx context.Context) (*api.CACertificateResponse, error) { + resp, err := a.c.NewRequest(ctx, "POST", "/api/ca/rotate", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.CACertificateResponse](resp) + if err != nil { + return nil, err + } + return &ret, nil +} + +// ListIssuedCertificates list issued certificates, optionally filtered by peer ID +func (a *CertificateAuthorityAPI) ListIssuedCertificates(ctx context.Context, peerID string) ([]api.IssuedCertificateResponse, error) { + var query map[string]string + if peerID != "" { + query = map[string]string{"peer_id": peerID} + } + resp, err := a.c.NewRequest(ctx, "GET", "/api/ca/certificates", nil, query) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.IssuedCertificateResponse](resp) + return ret, err +} + +// RevokeCertificate revoke an issued certificate by serial number +func (a *CertificateAuthorityAPI) RevokeCertificate(ctx context.Context, serialNumber string) error { + resp, err := a.c.NewRequest(ctx, "POST", "/api/ca/certificates/"+serialNumber+"/revoke", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} diff --git a/shared/management/client/rest/ca_test.go b/shared/management/client/rest/ca_test.go new file mode 100644 index 00000000000..e48730a0fdf --- /dev/null +++ b/shared/management/client/rest/ca_test.go @@ -0,0 +1,339 @@ +//go:build integration +// +build integration + +package rest_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var ( + testCACert = api.CACertificateResponse{ + Id: "ca-1", + Fingerprint: "abc123", + NotBefore: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC), + IsActive: true, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + } + + testIssuedCert = api.IssuedCertificateResponse{ + Id: "cert-1", + PeerId: "peer-1", + SerialNumber: "1234", + DnsNames: []string{"peer1.example.com"}, + HasWildcard: false, + NotBefore: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + SigningType: "acme", + Revoked: false, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + } +) + +func TestCA_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.CACertificateResponse{testCACert}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.ListCAs(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testCACert, ret[0]) + }) +} + +func TestCA_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.ListCAs(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestCA_Init_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testCACert) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.InitCA(context.Background()) + require.NoError(t, err) + assert.Equal(t, testCACert, *ret) + }) +} + +func TestCA_Init_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Failed", Code: 500}) + w.WriteHeader(500) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.InitCA(context.Background()) + assert.Error(t, err) + assert.Equal(t, "Failed", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestCA_Get_200(t *testing.T) { + pem := "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----" + certWithPem := testCACert + certWithPem.CertificatePem = &pem + + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/ca-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(certWithPem) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.GetCA(context.Background(), "ca-1") + require.NoError(t, err) + assert.Equal(t, certWithPem, *ret) + assert.NotNil(t, ret.CertificatePem) + }) +} + +func TestCA_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/ca-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.GetCA(context.Background(), "ca-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestCA_Deactivate_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/ca-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.CertificateAuthority.DeactivateCA(context.Background(), "ca-1") + require.NoError(t, err) + }) +} + +func TestCA_Deactivate_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/ca-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.CertificateAuthority.DeactivateCA(context.Background(), "ca-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestCA_Rotate_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/rotate", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testCACert) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.RotateCA(context.Background()) + require.NoError(t, err) + assert.Equal(t, testCACert, *ret) + }) +} + +func TestCA_Rotate_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/rotate", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No active CA", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.RotateCA(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No active CA", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestCA_ListIssuedCerts_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/certificates", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.IssuedCertificateResponse{testIssuedCert}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.ListIssuedCertificates(context.Background(), "") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testIssuedCert, ret[0]) + }) +} + +func TestCA_ListIssuedCerts_WithPeerFilter(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/certificates", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "peer-1", r.URL.Query().Get("peer_id")) + retBytes, _ := json.Marshal([]api.IssuedCertificateResponse{testIssuedCert}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.ListIssuedCertificates(context.Background(), "peer-1") + require.NoError(t, err) + assert.Len(t, ret, 1) + }) +} + +func TestCA_ListIssuedCerts_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/certificates", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Forbidden", Code: 403}) + w.WriteHeader(403) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.CertificateAuthority.ListIssuedCertificates(context.Background(), "") + assert.Error(t, err) + assert.Equal(t, "Forbidden", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestCA_Revoke_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/certificates/1234/revoke", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + w.WriteHeader(200) + }) + err := c.CertificateAuthority.RevokeCertificate(context.Background(), "1234") + require.NoError(t, err) + }) +} + +func TestCA_Revoke_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ca/certificates/1234/revoke", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.CertificateAuthority.RevokeCertificate(context.Background(), "1234") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestCA_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *rest.Client) { + ctx := context.Background() + + // Step 1: Set DNS domain on the account (required for CA init/rotate) + accounts, err := c.Accounts.List(ctx) + require.NoError(t, err) + require.NotEmpty(t, accounts) + accountID := accounts[0].Id + + _, err = c.Accounts.Update(ctx, accountID, api.AccountRequest{ + Settings: api.AccountSettings{ + DnsDomain: ptr("test.netbird.io"), + PeerLoginExpiration: accounts[0].Settings.PeerLoginExpiration, + PeerLoginExpirationEnabled: accounts[0].Settings.PeerLoginExpirationEnabled, + }, + }) + require.NoError(t, err) + + // Step 2: List CAs (should be empty initially) + cas, err := c.CertificateAuthority.ListCAs(ctx) + require.NoError(t, err) + assert.Len(t, cas, 0) + + // Step 3: Init CA + initCA, err := c.CertificateAuthority.InitCA(ctx) + require.NoError(t, err) + assert.NotEmpty(t, initCA.Id) + assert.NotEmpty(t, initCA.Fingerprint) + assert.True(t, initCA.IsActive) + assert.Nil(t, initCA.CertificatePem) // list/init responses don't include PEM + + // Step 4: List CAs (should have 1) + cas, err = c.CertificateAuthority.ListCAs(ctx) + require.NoError(t, err) + assert.Len(t, cas, 1) + assert.Equal(t, initCA.Id, cas[0].Id) + + // Step 5: Get CA detail (includes PEM) + detail, err := c.CertificateAuthority.GetCA(ctx, initCA.Id) + require.NoError(t, err) + assert.Equal(t, initCA.Id, detail.Id) + assert.NotNil(t, detail.CertificatePem) + assert.Contains(t, *detail.CertificatePem, "BEGIN CERTIFICATE") + + // Step 6: Rotate CA (creates new CA, old remains active for trust continuity) + rotatedCA, err := c.CertificateAuthority.RotateCA(ctx) + require.NoError(t, err) + assert.NotEmpty(t, rotatedCA.Id) + assert.NotEqual(t, initCA.Id, rotatedCA.Id) + assert.True(t, rotatedCA.IsActive) + + // Step 7: List CAs after rotation (both old and new are active) + cas, err = c.CertificateAuthority.ListCAs(ctx) + require.NoError(t, err) + assert.Len(t, cas, 2) + + // Step 8: Deactivate both CAs + err = c.CertificateAuthority.DeactivateCA(ctx, initCA.Id) + require.NoError(t, err) + err = c.CertificateAuthority.DeactivateCA(ctx, rotatedCA.Id) + require.NoError(t, err) + + // Step 9: List CAs (should be empty now) + cas, err = c.CertificateAuthority.ListCAs(ctx) + require.NoError(t, err) + assert.Len(t, cas, 0) + + // Step 10: List issued certificates (empty, no certs issued via REST) + certs, err := c.CertificateAuthority.ListIssuedCertificates(ctx, "") + require.NoError(t, err) + assert.Len(t, certs, 0) + + // Step 11: Revoke non-existent cert returns error + err = c.CertificateAuthority.RevokeCertificate(ctx, "nonexistent-serial") + assert.Error(t, err) + }) +} diff --git a/shared/management/client/rest/client.go b/shared/management/client/rest/client.go index f308761fb76..87770b221b8 100644 --- a/shared/management/client/rest/client.go +++ b/shared/management/client/rest/client.go @@ -134,6 +134,9 @@ type Client struct { // ReverseProxyDomains NetBird reverse proxy domains APIs ReverseProxyDomains *ReverseProxyDomainsAPI + + // CertificateAuthority NetBird certificate authority APIs + CertificateAuthority *CertificateAuthorityAPI } // New initialize new Client instance using PAT token @@ -192,6 +195,7 @@ func (c *Client) initialize() { c.ReverseProxyServices = &ReverseProxyServicesAPI{c} c.ReverseProxyClusters = &ReverseProxyClustersAPI{c} c.ReverseProxyDomains = &ReverseProxyDomainsAPI{c} + c.CertificateAuthority = &CertificateAuthorityAPI{c} } // NewRequest creates and executes new management API request diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 7f03d698606..f98b156b6ea 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -40,6 +40,8 @@ tags: description: Interact with and view information about reverse proxy services. - name: Instance description: Instance setup and status endpoints for initial configuration. + - name: Certificate Authority + description: Manage internal certificate authority and issued certificates. - name: Jobs description: Interact with and view information about remote jobs. x-experimental: true @@ -336,6 +338,9 @@ components: items: type: string example: ch8i4ug6lnn4g9hqv7m0 + cert_wildcard_allowed: + description: Allows peers to request wildcard subdomain certificates + type: boolean extra: $ref: '#/components/schemas/AccountExtraSettings' lazy_connection_enabled: @@ -2303,7 +2308,11 @@ components: "peer.job.create", "user.password.change", "user.invite.link.create", "user.invite.link.accept", "user.invite.link.regenerate", "user.invite.link.delete", - "service.create", "service.update", "service.delete" + "service.create", "service.update", "service.delete", + "certificate.authority.create", "certificate.authority.rotate", "certificate.authority.deactivate", + "certificate.issue", "certificate.renew", "certificate.revoke", + "certificate.expiring", "certificate.expired", "certificate.rate.limit", + "certificate.wildcard.issue" ] example: route.add initiator_id: @@ -4231,6 +4240,130 @@ components: type: string description: The ID of the bypassed peer. example: "chacbco6lnnbn6cg5s91" + CAInitRequest: + type: object + description: Optional parameters for initializing a CA certificate + properties: + display_name: + description: Display name for the CA certificate CommonName. Defaults to "{domain} Internal CA ({suffix})" + type: string + maxLength: 64 + organization: + description: Organization name for the CA certificate. Defaults to "NetBird Self-Hosted" + type: string + maxLength: 64 + validity_days: + description: CA certificate validity in days. Defaults to 3650 (~10 years) + type: integer + minimum: 1 + maximum: 36500 + CACertificateResponse: + type: object + description: A certificate authority certificate + properties: + id: + description: CA certificate ID + type: string + example: ch8i4ug6lnn4g9hqv7m0 + fingerprint: + description: SHA-256 fingerprint of the CA certificate + type: string + example: "AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89" + display_name: + description: Display name (CommonName) of the CA certificate + type: string + organization: + description: Organization of the CA certificate + type: string + certificate_pem: + description: PEM-encoded CA certificate (only included in single CA detail responses) + type: string + not_before: + description: Certificate validity start time (UTC) + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + not_after: + description: Certificate validity end time (UTC) + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + is_active: + description: Whether this CA certificate is currently active + type: boolean + example: true + created_at: + description: CA certificate creation time (UTC) + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + required: + - id + - fingerprint + - not_before + - not_after + - is_active + - created_at + IssuedCertificateResponse: + type: object + description: An issued certificate + properties: + id: + description: Issued certificate ID + type: string + example: ch8i4ug6lnn4g9hqv7m0 + peer_id: + description: ID of the peer this certificate was issued for + type: string + example: chacbco6lnnbn6cg5s90 + serial_number: + description: Certificate serial number + type: string + example: "1234567890" + dns_names: + description: DNS names included in the certificate + type: array + items: + type: string + example: ["peer1.example.com"] + has_wildcard: + description: Whether the certificate includes a wildcard DNS name + type: boolean + example: false + not_before: + description: Certificate validity start time (UTC) + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + not_after: + description: Certificate validity end time (UTC) + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + signing_type: + description: How the certificate was signed + type: string + example: "acme" + revoked: + description: Whether the certificate has been revoked + type: boolean + example: false + created_at: + description: Certificate issuance time (UTC) + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + required: + - id + - peer_id + - serial_number + - dns_names + - has_wildcard + - not_before + - not_after + - signing_type + - revoked + - created_at ErrorResponse: type: object description: "Standard error response. Note: The exact structure of this error response is inferred from `util.WriteErrorResponse` and `util.WriteError` usage in the provided Go code, as a specific Go struct for errors was not provided." @@ -9888,3 +10021,204 @@ paths: "$ref": "#/components/responses/not_found" '500': "$ref": "#/components/responses/internal_error" + /api/ca: + get: + summary: List Active CAs + description: Returns a list of all active certificate authority certificates + tags: [ Certificate Authority ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: A JSON Array of CA Certificates + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CACertificateResponse' + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + post: + summary: Initialize CA + description: Initialize a new certificate authority for the account + tags: [ Certificate Authority ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + requestBody: + description: Optional CA configuration parameters + content: + application/json: + schema: + $ref: '#/components/schemas/CAInitRequest' + responses: + '200': + description: A JSON Object of the created CA Certificate + content: + application/json: + schema: + $ref: '#/components/schemas/CACertificateResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/ca/{caId}: + get: + summary: Retrieve a CA Certificate + description: Returns detailed information about a specific CA certificate, including PEM + tags: [ Certificate Authority ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: caId + required: true + schema: + type: string + description: The unique identifier of a CA certificate + example: chacbco6lnnbn6cg5s91 + responses: + '200': + description: A JSON Object of the CA Certificate + content: + application/json: + schema: + $ref: '#/components/schemas/CACertificateResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Deactivate a CA Certificate + description: Deactivates a certificate authority certificate + tags: [ Certificate Authority ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: caId + required: true + schema: + type: string + description: The unique identifier of a CA certificate + example: chacbco6lnnbn6cg5s91 + responses: + '200': + description: CA Certificate deactivated + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" + /api/ca/rotate: + post: + summary: Rotate CA + description: Rotate the certificate authority, deactivating the current one and creating a new one + tags: [ Certificate Authority ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + requestBody: + description: Optional CA configuration parameters for the new CA + content: + application/json: + schema: + $ref: '#/components/schemas/CAInitRequest' + responses: + '200': + description: A JSON Object of the new CA Certificate + content: + application/json: + schema: + $ref: '#/components/schemas/CACertificateResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/ca/certificates: + get: + summary: List Issued Certificates + description: Returns a list of all issued certificates, optionally filtered by peer ID + tags: [ Certificate Authority ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: query + name: peer_id + required: false + schema: + type: string + description: Filter certificates by peer ID + example: chacbco6lnnbn6cg5s90 + responses: + '200': + description: A JSON Array of Issued Certificates + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/IssuedCertificateResponse' + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/ca/certificates/{serialNumber}/revoke: + post: + summary: Revoke a Certificate + description: Revokes an issued certificate by serial number + tags: [ Certificate Authority ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: serialNumber + required: true + schema: + type: string + description: The serial number of the certificate to revoke + example: "1234567890" + responses: + '200': + description: Certificate revoked + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index d4a07f80622..7228e49912e 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -1,6 +1,6 @@ // Package api provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT. package api import ( @@ -24,6 +24,22 @@ const ( CreateIntegrationRequestPlatformS3 CreateIntegrationRequestPlatform = "s3" ) +// Valid indicates whether the value is a known member of the CreateIntegrationRequestPlatform enum. +func (e CreateIntegrationRequestPlatform) Valid() bool { + switch e { + case CreateIntegrationRequestPlatformDatadog: + return true + case CreateIntegrationRequestPlatformFirehose: + return true + case CreateIntegrationRequestPlatformGenericHttp: + return true + case CreateIntegrationRequestPlatformS3: + return true + default: + return false + } +} + // Defines values for DNSRecordType. const ( DNSRecordTypeA DNSRecordType = "A" @@ -31,6 +47,20 @@ const ( DNSRecordTypeCNAME DNSRecordType = "CNAME" ) +// Valid indicates whether the value is a known member of the DNSRecordType enum. +func (e DNSRecordType) Valid() bool { + switch e { + case DNSRecordTypeA: + return true + case DNSRecordTypeAAAA: + return true + case DNSRecordTypeCNAME: + return true + default: + return false + } +} + // Defines values for EventActivityCode. const ( EventActivityCodeAccountCreate EventActivityCode = "account.create" @@ -52,6 +82,16 @@ const ( EventActivityCodeAccountSettingRoutingPeerDnsResolutionDisable EventActivityCode = "account.setting.routing.peer.dns.resolution.disable" EventActivityCodeAccountSettingRoutingPeerDnsResolutionEnable EventActivityCode = "account.setting.routing.peer.dns.resolution.enable" EventActivityCodeAccountSettingsAutoVersionUpdate EventActivityCode = "account.settings.auto.version.update" + EventActivityCodeCertificateAuthorityCreate EventActivityCode = "certificate.authority.create" + EventActivityCodeCertificateAuthorityDeactivate EventActivityCode = "certificate.authority.deactivate" + EventActivityCodeCertificateAuthorityRotate EventActivityCode = "certificate.authority.rotate" + EventActivityCodeCertificateExpired EventActivityCode = "certificate.expired" + EventActivityCodeCertificateExpiring EventActivityCode = "certificate.expiring" + EventActivityCodeCertificateIssue EventActivityCode = "certificate.issue" + EventActivityCodeCertificateRateLimit EventActivityCode = "certificate.rate.limit" + EventActivityCodeCertificateRenew EventActivityCode = "certificate.renew" + EventActivityCodeCertificateRevoke EventActivityCode = "certificate.revoke" + EventActivityCodeCertificateWildcardIssue EventActivityCode = "certificate.wildcard.issue" EventActivityCodeDashboardLogin EventActivityCode = "dashboard.login" EventActivityCodeDnsSettingDisabledManagementGroupAdd EventActivityCode = "dns.setting.disabled.management.group.add" EventActivityCodeDnsSettingDisabledManagementGroupDelete EventActivityCode = "dns.setting.disabled.management.group.delete" @@ -147,12 +187,276 @@ const ( EventActivityCodeUserUnblock EventActivityCode = "user.unblock" ) +// Valid indicates whether the value is a known member of the EventActivityCode enum. +func (e EventActivityCode) Valid() bool { + switch e { + case EventActivityCodeAccountCreate: + return true + case EventActivityCodeAccountDelete: + return true + case EventActivityCodeAccountDnsDomainUpdate: + return true + case EventActivityCodeAccountNetworkRangeUpdate: + return true + case EventActivityCodeAccountPeerInactivityExpirationDisable: + return true + case EventActivityCodeAccountPeerInactivityExpirationEnable: + return true + case EventActivityCodeAccountPeerInactivityExpirationUpdate: + return true + case EventActivityCodeAccountSettingGroupPropagationDisable: + return true + case EventActivityCodeAccountSettingGroupPropagationEnable: + return true + case EventActivityCodeAccountSettingLazyConnectionDisable: + return true + case EventActivityCodeAccountSettingLazyConnectionEnable: + return true + case EventActivityCodeAccountSettingPeerApprovalDisable: + return true + case EventActivityCodeAccountSettingPeerApprovalEnable: + return true + case EventActivityCodeAccountSettingPeerLoginExpirationDisable: + return true + case EventActivityCodeAccountSettingPeerLoginExpirationEnable: + return true + case EventActivityCodeAccountSettingPeerLoginExpirationUpdate: + return true + case EventActivityCodeAccountSettingRoutingPeerDnsResolutionDisable: + return true + case EventActivityCodeAccountSettingRoutingPeerDnsResolutionEnable: + return true + case EventActivityCodeAccountSettingsAutoVersionUpdate: + return true + case EventActivityCodeCertificateAuthorityCreate: + return true + case EventActivityCodeCertificateAuthorityDeactivate: + return true + case EventActivityCodeCertificateAuthorityRotate: + return true + case EventActivityCodeCertificateExpired: + return true + case EventActivityCodeCertificateExpiring: + return true + case EventActivityCodeCertificateIssue: + return true + case EventActivityCodeCertificateRateLimit: + return true + case EventActivityCodeCertificateRenew: + return true + case EventActivityCodeCertificateRevoke: + return true + case EventActivityCodeCertificateWildcardIssue: + return true + case EventActivityCodeDashboardLogin: + return true + case EventActivityCodeDnsSettingDisabledManagementGroupAdd: + return true + case EventActivityCodeDnsSettingDisabledManagementGroupDelete: + return true + case EventActivityCodeDnsZoneCreate: + return true + case EventActivityCodeDnsZoneDelete: + return true + case EventActivityCodeDnsZoneRecordCreate: + return true + case EventActivityCodeDnsZoneRecordDelete: + return true + case EventActivityCodeDnsZoneRecordUpdate: + return true + case EventActivityCodeDnsZoneUpdate: + return true + case EventActivityCodeGroupAdd: + return true + case EventActivityCodeGroupDelete: + return true + case EventActivityCodeGroupUpdate: + return true + case EventActivityCodeIdentityproviderCreate: + return true + case EventActivityCodeIdentityproviderDelete: + return true + case EventActivityCodeIdentityproviderUpdate: + return true + case EventActivityCodeIntegrationCreate: + return true + case EventActivityCodeIntegrationDelete: + return true + case EventActivityCodeIntegrationUpdate: + return true + case EventActivityCodeNameserverGroupAdd: + return true + case EventActivityCodeNameserverGroupDelete: + return true + case EventActivityCodeNameserverGroupUpdate: + return true + case EventActivityCodeNetworkCreate: + return true + case EventActivityCodeNetworkDelete: + return true + case EventActivityCodeNetworkResourceCreate: + return true + case EventActivityCodeNetworkResourceDelete: + return true + case EventActivityCodeNetworkResourceUpdate: + return true + case EventActivityCodeNetworkRouterCreate: + return true + case EventActivityCodeNetworkRouterDelete: + return true + case EventActivityCodeNetworkRouterUpdate: + return true + case EventActivityCodeNetworkUpdate: + return true + case EventActivityCodePeerApprovalRevoke: + return true + case EventActivityCodePeerApprove: + return true + case EventActivityCodePeerGroupAdd: + return true + case EventActivityCodePeerGroupDelete: + return true + case EventActivityCodePeerInactivityExpirationDisable: + return true + case EventActivityCodePeerInactivityExpirationEnable: + return true + case EventActivityCodePeerIpUpdate: + return true + case EventActivityCodePeerJobCreate: + return true + case EventActivityCodePeerLoginExpirationDisable: + return true + case EventActivityCodePeerLoginExpirationEnable: + return true + case EventActivityCodePeerLoginExpire: + return true + case EventActivityCodePeerRename: + return true + case EventActivityCodePeerSetupkeyAdd: + return true + case EventActivityCodePeerSshDisable: + return true + case EventActivityCodePeerSshEnable: + return true + case EventActivityCodePeerUserAdd: + return true + case EventActivityCodePersonalAccessTokenCreate: + return true + case EventActivityCodePersonalAccessTokenDelete: + return true + case EventActivityCodePolicyAdd: + return true + case EventActivityCodePolicyDelete: + return true + case EventActivityCodePolicyUpdate: + return true + case EventActivityCodePostureCheckCreate: + return true + case EventActivityCodePostureCheckDelete: + return true + case EventActivityCodePostureCheckUpdate: + return true + case EventActivityCodeResourceGroupAdd: + return true + case EventActivityCodeResourceGroupDelete: + return true + case EventActivityCodeRouteAdd: + return true + case EventActivityCodeRouteDelete: + return true + case EventActivityCodeRouteUpdate: + return true + case EventActivityCodeRuleAdd: + return true + case EventActivityCodeRuleDelete: + return true + case EventActivityCodeRuleUpdate: + return true + case EventActivityCodeServiceCreate: + return true + case EventActivityCodeServiceDelete: + return true + case EventActivityCodeServiceUpdate: + return true + case EventActivityCodeServiceUserCreate: + return true + case EventActivityCodeServiceUserDelete: + return true + case EventActivityCodeSetupkeyAdd: + return true + case EventActivityCodeSetupkeyDelete: + return true + case EventActivityCodeSetupkeyGroupAdd: + return true + case EventActivityCodeSetupkeyGroupDelete: + return true + case EventActivityCodeSetupkeyOveruse: + return true + case EventActivityCodeSetupkeyRevoke: + return true + case EventActivityCodeSetupkeyUpdate: + return true + case EventActivityCodeTransferredOwnerRole: + return true + case EventActivityCodeUserApprove: + return true + case EventActivityCodeUserBlock: + return true + case EventActivityCodeUserCreate: + return true + case EventActivityCodeUserDelete: + return true + case EventActivityCodeUserGroupAdd: + return true + case EventActivityCodeUserGroupDelete: + return true + case EventActivityCodeUserInvite: + return true + case EventActivityCodeUserInviteLinkAccept: + return true + case EventActivityCodeUserInviteLinkCreate: + return true + case EventActivityCodeUserInviteLinkDelete: + return true + case EventActivityCodeUserInviteLinkRegenerate: + return true + case EventActivityCodeUserJoin: + return true + case EventActivityCodeUserPasswordChange: + return true + case EventActivityCodeUserPeerDelete: + return true + case EventActivityCodeUserPeerLogin: + return true + case EventActivityCodeUserReject: + return true + case EventActivityCodeUserRoleUpdate: + return true + case EventActivityCodeUserUnblock: + return true + default: + return false + } +} + // Defines values for GeoLocationCheckAction. const ( GeoLocationCheckActionAllow GeoLocationCheckAction = "allow" GeoLocationCheckActionDeny GeoLocationCheckAction = "deny" ) +// Valid indicates whether the value is a known member of the GeoLocationCheckAction enum. +func (e GeoLocationCheckAction) Valid() bool { + switch e { + case GeoLocationCheckActionAllow: + return true + case GeoLocationCheckActionDeny: + return true + default: + return false + } +} + // Defines values for GroupIssued. const ( GroupIssuedApi GroupIssued = "api" @@ -160,6 +464,20 @@ const ( GroupIssuedJwt GroupIssued = "jwt" ) +// Valid indicates whether the value is a known member of the GroupIssued enum. +func (e GroupIssued) Valid() bool { + switch e { + case GroupIssuedApi: + return true + case GroupIssuedIntegration: + return true + case GroupIssuedJwt: + return true + default: + return false + } +} + // Defines values for GroupMinimumIssued. const ( GroupMinimumIssuedApi GroupMinimumIssued = "api" @@ -167,6 +485,20 @@ const ( GroupMinimumIssuedJwt GroupMinimumIssued = "jwt" ) +// Valid indicates whether the value is a known member of the GroupMinimumIssued enum. +func (e GroupMinimumIssued) Valid() bool { + switch e { + case GroupMinimumIssuedApi: + return true + case GroupMinimumIssuedIntegration: + return true + case GroupMinimumIssuedJwt: + return true + default: + return false + } +} + // Defines values for IdentityProviderType. const ( IdentityProviderTypeEntra IdentityProviderType = "entra" @@ -178,6 +510,28 @@ const ( IdentityProviderTypeZitadel IdentityProviderType = "zitadel" ) +// Valid indicates whether the value is a known member of the IdentityProviderType enum. +func (e IdentityProviderType) Valid() bool { + switch e { + case IdentityProviderTypeEntra: + return true + case IdentityProviderTypeGoogle: + return true + case IdentityProviderTypeMicrosoft: + return true + case IdentityProviderTypeOidc: + return true + case IdentityProviderTypeOkta: + return true + case IdentityProviderTypePocketid: + return true + case IdentityProviderTypeZitadel: + return true + default: + return false + } +} + // Defines values for IngressPortAllocationPortMappingProtocol. const ( IngressPortAllocationPortMappingProtocolTcp IngressPortAllocationPortMappingProtocol = "tcp" @@ -185,6 +539,20 @@ const ( IngressPortAllocationPortMappingProtocolUdp IngressPortAllocationPortMappingProtocol = "udp" ) +// Valid indicates whether the value is a known member of the IngressPortAllocationPortMappingProtocol enum. +func (e IngressPortAllocationPortMappingProtocol) Valid() bool { + switch e { + case IngressPortAllocationPortMappingProtocolTcp: + return true + case IngressPortAllocationPortMappingProtocolTcpudp: + return true + case IngressPortAllocationPortMappingProtocolUdp: + return true + default: + return false + } +} + // Defines values for IngressPortAllocationRequestDirectPortProtocol. const ( IngressPortAllocationRequestDirectPortProtocolTcp IngressPortAllocationRequestDirectPortProtocol = "tcp" @@ -192,6 +560,20 @@ const ( IngressPortAllocationRequestDirectPortProtocolUdp IngressPortAllocationRequestDirectPortProtocol = "udp" ) +// Valid indicates whether the value is a known member of the IngressPortAllocationRequestDirectPortProtocol enum. +func (e IngressPortAllocationRequestDirectPortProtocol) Valid() bool { + switch e { + case IngressPortAllocationRequestDirectPortProtocolTcp: + return true + case IngressPortAllocationRequestDirectPortProtocolTcpudp: + return true + case IngressPortAllocationRequestDirectPortProtocolUdp: + return true + default: + return false + } +} + // Defines values for IngressPortAllocationRequestPortRangeProtocol. const ( IngressPortAllocationRequestPortRangeProtocolTcp IngressPortAllocationRequestPortRangeProtocol = "tcp" @@ -199,6 +581,20 @@ const ( IngressPortAllocationRequestPortRangeProtocolUdp IngressPortAllocationRequestPortRangeProtocol = "udp" ) +// Valid indicates whether the value is a known member of the IngressPortAllocationRequestPortRangeProtocol enum. +func (e IngressPortAllocationRequestPortRangeProtocol) Valid() bool { + switch e { + case IngressPortAllocationRequestPortRangeProtocolTcp: + return true + case IngressPortAllocationRequestPortRangeProtocolTcpudp: + return true + case IngressPortAllocationRequestPortRangeProtocolUdp: + return true + default: + return false + } +} + // Defines values for IntegrationResponsePlatform. const ( IntegrationResponsePlatformDatadog IntegrationResponsePlatform = "datadog" @@ -207,12 +603,40 @@ const ( IntegrationResponsePlatformS3 IntegrationResponsePlatform = "s3" ) +// Valid indicates whether the value is a known member of the IntegrationResponsePlatform enum. +func (e IntegrationResponsePlatform) Valid() bool { + switch e { + case IntegrationResponsePlatformDatadog: + return true + case IntegrationResponsePlatformFirehose: + return true + case IntegrationResponsePlatformGenericHttp: + return true + case IntegrationResponsePlatformS3: + return true + default: + return false + } +} + // Defines values for InvoiceResponseType. const ( InvoiceResponseTypeAccount InvoiceResponseType = "account" InvoiceResponseTypeTenants InvoiceResponseType = "tenants" ) +// Valid indicates whether the value is a known member of the InvoiceResponseType enum. +func (e InvoiceResponseType) Valid() bool { + switch e { + case InvoiceResponseTypeAccount: + return true + case InvoiceResponseTypeTenants: + return true + default: + return false + } +} + // Defines values for JobResponseStatus. const ( JobResponseStatusFailed JobResponseStatus = "failed" @@ -220,11 +644,35 @@ const ( JobResponseStatusSucceeded JobResponseStatus = "succeeded" ) +// Valid indicates whether the value is a known member of the JobResponseStatus enum. +func (e JobResponseStatus) Valid() bool { + switch e { + case JobResponseStatusFailed: + return true + case JobResponseStatusPending: + return true + case JobResponseStatusSucceeded: + return true + default: + return false + } +} + // Defines values for NameserverNsType. const ( NameserverNsTypeUdp NameserverNsType = "udp" ) +// Valid indicates whether the value is a known member of the NameserverNsType enum. +func (e NameserverNsType) Valid() bool { + switch e { + case NameserverNsTypeUdp: + return true + default: + return false + } +} + // Defines values for NetworkResourceType. const ( NetworkResourceTypeDomain NetworkResourceType = "domain" @@ -232,18 +680,56 @@ const ( NetworkResourceTypeSubnet NetworkResourceType = "subnet" ) +// Valid indicates whether the value is a known member of the NetworkResourceType enum. +func (e NetworkResourceType) Valid() bool { + switch e { + case NetworkResourceTypeDomain: + return true + case NetworkResourceTypeHost: + return true + case NetworkResourceTypeSubnet: + return true + default: + return false + } +} + // Defines values for PeerNetworkRangeCheckAction. const ( PeerNetworkRangeCheckActionAllow PeerNetworkRangeCheckAction = "allow" PeerNetworkRangeCheckActionDeny PeerNetworkRangeCheckAction = "deny" ) +// Valid indicates whether the value is a known member of the PeerNetworkRangeCheckAction enum. +func (e PeerNetworkRangeCheckAction) Valid() bool { + switch e { + case PeerNetworkRangeCheckActionAllow: + return true + case PeerNetworkRangeCheckActionDeny: + return true + default: + return false + } +} + // Defines values for PolicyRuleAction. const ( PolicyRuleActionAccept PolicyRuleAction = "accept" PolicyRuleActionDrop PolicyRuleAction = "drop" ) +// Valid indicates whether the value is a known member of the PolicyRuleAction enum. +func (e PolicyRuleAction) Valid() bool { + switch e { + case PolicyRuleActionAccept: + return true + case PolicyRuleActionDrop: + return true + default: + return false + } +} + // Defines values for PolicyRuleProtocol. const ( PolicyRuleProtocolAll PolicyRuleProtocol = "all" @@ -253,12 +739,42 @@ const ( PolicyRuleProtocolUdp PolicyRuleProtocol = "udp" ) +// Valid indicates whether the value is a known member of the PolicyRuleProtocol enum. +func (e PolicyRuleProtocol) Valid() bool { + switch e { + case PolicyRuleProtocolAll: + return true + case PolicyRuleProtocolIcmp: + return true + case PolicyRuleProtocolNetbirdSsh: + return true + case PolicyRuleProtocolTcp: + return true + case PolicyRuleProtocolUdp: + return true + default: + return false + } +} + // Defines values for PolicyRuleMinimumAction. const ( PolicyRuleMinimumActionAccept PolicyRuleMinimumAction = "accept" PolicyRuleMinimumActionDrop PolicyRuleMinimumAction = "drop" ) +// Valid indicates whether the value is a known member of the PolicyRuleMinimumAction enum. +func (e PolicyRuleMinimumAction) Valid() bool { + switch e { + case PolicyRuleMinimumActionAccept: + return true + case PolicyRuleMinimumActionDrop: + return true + default: + return false + } +} + // Defines values for PolicyRuleMinimumProtocol. const ( PolicyRuleMinimumProtocolAll PolicyRuleMinimumProtocol = "all" @@ -268,12 +784,42 @@ const ( PolicyRuleMinimumProtocolUdp PolicyRuleMinimumProtocol = "udp" ) +// Valid indicates whether the value is a known member of the PolicyRuleMinimumProtocol enum. +func (e PolicyRuleMinimumProtocol) Valid() bool { + switch e { + case PolicyRuleMinimumProtocolAll: + return true + case PolicyRuleMinimumProtocolIcmp: + return true + case PolicyRuleMinimumProtocolNetbirdSsh: + return true + case PolicyRuleMinimumProtocolTcp: + return true + case PolicyRuleMinimumProtocolUdp: + return true + default: + return false + } +} + // Defines values for PolicyRuleUpdateAction. const ( PolicyRuleUpdateActionAccept PolicyRuleUpdateAction = "accept" PolicyRuleUpdateActionDrop PolicyRuleUpdateAction = "drop" ) +// Valid indicates whether the value is a known member of the PolicyRuleUpdateAction enum. +func (e PolicyRuleUpdateAction) Valid() bool { + switch e { + case PolicyRuleUpdateActionAccept: + return true + case PolicyRuleUpdateActionDrop: + return true + default: + return false + } +} + // Defines values for PolicyRuleUpdateProtocol. const ( PolicyRuleUpdateProtocolAll PolicyRuleUpdateProtocol = "all" @@ -283,6 +829,24 @@ const ( PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp" ) +// Valid indicates whether the value is a known member of the PolicyRuleUpdateProtocol enum. +func (e PolicyRuleUpdateProtocol) Valid() bool { + switch e { + case PolicyRuleUpdateProtocolAll: + return true + case PolicyRuleUpdateProtocolIcmp: + return true + case PolicyRuleUpdateProtocolNetbirdSsh: + return true + case PolicyRuleUpdateProtocolTcp: + return true + case PolicyRuleUpdateProtocolUdp: + return true + default: + return false + } +} + // Defines values for ResourceType. const ( ResourceTypeDomain ResourceType = "domain" @@ -291,12 +855,40 @@ const ( ResourceTypeSubnet ResourceType = "subnet" ) +// Valid indicates whether the value is a known member of the ResourceType enum. +func (e ResourceType) Valid() bool { + switch e { + case ResourceTypeDomain: + return true + case ResourceTypeHost: + return true + case ResourceTypePeer: + return true + case ResourceTypeSubnet: + return true + default: + return false + } +} + // Defines values for ReverseProxyDomainType. const ( ReverseProxyDomainTypeCustom ReverseProxyDomainType = "custom" ReverseProxyDomainTypeFree ReverseProxyDomainType = "free" ) +// Valid indicates whether the value is a known member of the ReverseProxyDomainType enum. +func (e ReverseProxyDomainType) Valid() bool { + switch e { + case ReverseProxyDomainTypeCustom: + return true + case ReverseProxyDomainTypeFree: + return true + default: + return false + } +} + // Defines values for SentinelOneMatchAttributesNetworkStatus. const ( SentinelOneMatchAttributesNetworkStatusConnected SentinelOneMatchAttributesNetworkStatus = "connected" @@ -304,6 +896,20 @@ const ( SentinelOneMatchAttributesNetworkStatusQuarantined SentinelOneMatchAttributesNetworkStatus = "quarantined" ) +// Valid indicates whether the value is a known member of the SentinelOneMatchAttributesNetworkStatus enum. +func (e SentinelOneMatchAttributesNetworkStatus) Valid() bool { + switch e { + case SentinelOneMatchAttributesNetworkStatusConnected: + return true + case SentinelOneMatchAttributesNetworkStatusDisconnected: + return true + case SentinelOneMatchAttributesNetworkStatusQuarantined: + return true + default: + return false + } +} + // Defines values for ServiceMetaStatus. const ( ServiceMetaStatusActive ServiceMetaStatus = "active" @@ -314,12 +920,44 @@ const ( ServiceMetaStatusTunnelNotCreated ServiceMetaStatus = "tunnel_not_created" ) +// Valid indicates whether the value is a known member of the ServiceMetaStatus enum. +func (e ServiceMetaStatus) Valid() bool { + switch e { + case ServiceMetaStatusActive: + return true + case ServiceMetaStatusCertificateFailed: + return true + case ServiceMetaStatusCertificatePending: + return true + case ServiceMetaStatusError: + return true + case ServiceMetaStatusPending: + return true + case ServiceMetaStatusTunnelNotCreated: + return true + default: + return false + } +} + // Defines values for ServiceTargetProtocol. const ( ServiceTargetProtocolHttp ServiceTargetProtocol = "http" ServiceTargetProtocolHttps ServiceTargetProtocol = "https" ) +// Valid indicates whether the value is a known member of the ServiceTargetProtocol enum. +func (e ServiceTargetProtocol) Valid() bool { + switch e { + case ServiceTargetProtocolHttp: + return true + case ServiceTargetProtocolHttps: + return true + default: + return false + } +} + // Defines values for ServiceTargetTargetType. const ( ServiceTargetTargetTypePeer ServiceTargetTargetType = "peer" @@ -331,6 +969,18 @@ const ( ServiceTargetOptionsPathRewritePreserve ServiceTargetOptionsPathRewrite = "preserve" ) +// Valid indicates whether the value is a known member of the ServiceTargetTargetType enum. +func (e ServiceTargetTargetType) Valid() bool { + switch e { + case ServiceTargetTargetTypePeer: + return true + case ServiceTargetTargetTypeResource: + return true + default: + return false + } +} + // Defines values for TenantResponseStatus. const ( TenantResponseStatusActive TenantResponseStatus = "active" @@ -339,6 +989,22 @@ const ( TenantResponseStatusPending TenantResponseStatus = "pending" ) +// Valid indicates whether the value is a known member of the TenantResponseStatus enum. +func (e TenantResponseStatus) Valid() bool { + switch e { + case TenantResponseStatusActive: + return true + case TenantResponseStatusExisting: + return true + case TenantResponseStatusInvited: + return true + case TenantResponseStatusPending: + return true + default: + return false + } +} + // Defines values for UserStatus. const ( UserStatusActive UserStatus = "active" @@ -346,11 +1012,35 @@ const ( UserStatusInvited UserStatus = "invited" ) +// Valid indicates whether the value is a known member of the UserStatus enum. +func (e UserStatus) Valid() bool { + switch e { + case UserStatusActive: + return true + case UserStatusBlocked: + return true + case UserStatusInvited: + return true + default: + return false + } +} + // Defines values for WorkloadType. const ( WorkloadTypeBundle WorkloadType = "bundle" ) +// Valid indicates whether the value is a known member of the WorkloadType enum. +func (e WorkloadType) Valid() bool { + switch e { + case WorkloadTypeBundle: + return true + default: + return false + } +} + // Defines values for GetApiEventsNetworkTrafficParamsType. const ( GetApiEventsNetworkTrafficParamsTypeTYPEDROP GetApiEventsNetworkTrafficParamsType = "TYPE_DROP" @@ -359,12 +1049,40 @@ const ( GetApiEventsNetworkTrafficParamsTypeTYPEUNKNOWN GetApiEventsNetworkTrafficParamsType = "TYPE_UNKNOWN" ) +// Valid indicates whether the value is a known member of the GetApiEventsNetworkTrafficParamsType enum. +func (e GetApiEventsNetworkTrafficParamsType) Valid() bool { + switch e { + case GetApiEventsNetworkTrafficParamsTypeTYPEDROP: + return true + case GetApiEventsNetworkTrafficParamsTypeTYPEEND: + return true + case GetApiEventsNetworkTrafficParamsTypeTYPESTART: + return true + case GetApiEventsNetworkTrafficParamsTypeTYPEUNKNOWN: + return true + default: + return false + } +} + // Defines values for GetApiEventsNetworkTrafficParamsConnectionType. const ( GetApiEventsNetworkTrafficParamsConnectionTypeP2P GetApiEventsNetworkTrafficParamsConnectionType = "P2P" GetApiEventsNetworkTrafficParamsConnectionTypeROUTED GetApiEventsNetworkTrafficParamsConnectionType = "ROUTED" ) +// Valid indicates whether the value is a known member of the GetApiEventsNetworkTrafficParamsConnectionType enum. +func (e GetApiEventsNetworkTrafficParamsConnectionType) Valid() bool { + switch e { + case GetApiEventsNetworkTrafficParamsConnectionTypeP2P: + return true + case GetApiEventsNetworkTrafficParamsConnectionTypeROUTED: + return true + default: + return false + } +} + // Defines values for GetApiEventsNetworkTrafficParamsDirection. const ( GetApiEventsNetworkTrafficParamsDirectionDIRECTIONUNKNOWN GetApiEventsNetworkTrafficParamsDirection = "DIRECTION_UNKNOWN" @@ -372,6 +1090,19 @@ const ( GetApiEventsNetworkTrafficParamsDirectionINGRESS GetApiEventsNetworkTrafficParamsDirection = "INGRESS" ) +// Valid indicates whether the value is a known member of the GetApiEventsNetworkTrafficParamsDirection enum. +func (e GetApiEventsNetworkTrafficParamsDirection) Valid() bool { + switch e { + case GetApiEventsNetworkTrafficParamsDirectionDIRECTIONUNKNOWN: + return true + case GetApiEventsNetworkTrafficParamsDirectionEGRESS: + return true + case GetApiEventsNetworkTrafficParamsDirectionINGRESS: + return true + default: + return false + } +} // Defines values for GetApiEventsProxyParamsSortBy. const ( GetApiEventsProxyParamsSortByAuthMethod GetApiEventsProxyParamsSortBy = "auth_method" @@ -387,12 +1118,52 @@ const ( GetApiEventsProxyParamsSortByUserId GetApiEventsProxyParamsSortBy = "user_id" ) +// Valid indicates whether the value is a known member of the GetApiEventsProxyParamsSortBy enum. +func (e GetApiEventsProxyParamsSortBy) Valid() bool { + switch e { + case GetApiEventsProxyParamsSortByAuthMethod: + return true + case GetApiEventsProxyParamsSortByDuration: + return true + case GetApiEventsProxyParamsSortByHost: + return true + case GetApiEventsProxyParamsSortByMethod: + return true + case GetApiEventsProxyParamsSortByPath: + return true + case GetApiEventsProxyParamsSortByReason: + return true + case GetApiEventsProxyParamsSortBySourceIp: + return true + case GetApiEventsProxyParamsSortByStatusCode: + return true + case GetApiEventsProxyParamsSortByTimestamp: + return true + case GetApiEventsProxyParamsSortByUrl: + return true + case GetApiEventsProxyParamsSortByUserId: + return true + default: + return false + } +} // Defines values for GetApiEventsProxyParamsSortOrder. const ( GetApiEventsProxyParamsSortOrderAsc GetApiEventsProxyParamsSortOrder = "asc" GetApiEventsProxyParamsSortOrderDesc GetApiEventsProxyParamsSortOrder = "desc" ) +// Valid indicates whether the value is a known member of the GetApiEventsProxyParamsSortOrder enum. +func (e GetApiEventsProxyParamsSortOrder) Valid() bool { + switch e { + case GetApiEventsProxyParamsSortOrderAsc: + return true + case GetApiEventsProxyParamsSortOrderDesc: + return true + default: + return false + } +} // Defines values for GetApiEventsProxyParamsMethod. const ( GetApiEventsProxyParamsMethodDELETE GetApiEventsProxyParamsMethod = "DELETE" @@ -404,18 +1175,64 @@ const ( GetApiEventsProxyParamsMethodPUT GetApiEventsProxyParamsMethod = "PUT" ) +// Valid indicates whether the value is a known member of the GetApiEventsProxyParamsMethod enum. +func (e GetApiEventsProxyParamsMethod) Valid() bool { + switch e { + case GetApiEventsProxyParamsMethodDELETE: + return true + case GetApiEventsProxyParamsMethodGET: + return true + case GetApiEventsProxyParamsMethodHEAD: + return true + case GetApiEventsProxyParamsMethodOPTIONS: + return true + case GetApiEventsProxyParamsMethodPATCH: + return true + case GetApiEventsProxyParamsMethodPOST: + return true + case GetApiEventsProxyParamsMethodPUT: + return true + default: + return false + } +} + // Defines values for GetApiEventsProxyParamsStatus. const ( GetApiEventsProxyParamsStatusFailed GetApiEventsProxyParamsStatus = "failed" GetApiEventsProxyParamsStatusSuccess GetApiEventsProxyParamsStatus = "success" ) +// Valid indicates whether the value is a known member of the GetApiEventsProxyParamsStatus enum. +func (e GetApiEventsProxyParamsStatus) Valid() bool { + switch e { + case GetApiEventsProxyParamsStatusFailed: + return true + case GetApiEventsProxyParamsStatusSuccess: + return true + default: + return false + } +} + // Defines values for PutApiIntegrationsMspTenantsIdInviteJSONBodyValue. const ( PutApiIntegrationsMspTenantsIdInviteJSONBodyValueAccept PutApiIntegrationsMspTenantsIdInviteJSONBodyValue = "accept" PutApiIntegrationsMspTenantsIdInviteJSONBodyValueDecline PutApiIntegrationsMspTenantsIdInviteJSONBodyValue = "decline" ) +// Valid indicates whether the value is a known member of the PutApiIntegrationsMspTenantsIdInviteJSONBodyValue enum. +func (e PutApiIntegrationsMspTenantsIdInviteJSONBodyValue) Valid() bool { + switch e { + case PutApiIntegrationsMspTenantsIdInviteJSONBodyValueAccept: + return true + case PutApiIntegrationsMspTenantsIdInviteJSONBodyValueDecline: + return true + default: + return false + } +} + // AccessiblePeer defines model for AccessiblePeer. type AccessiblePeer struct { // CityName Commonly used English name of the city @@ -510,6 +1327,9 @@ type AccountSettings struct { // AutoUpdateVersion Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1") AutoUpdateVersion *string `json:"auto_update_version,omitempty"` + // CertWildcardAllowed Allows peers to request wildcard subdomain certificates + CertWildcardAllowed *bool `json:"cert_wildcard_allowed,omitempty"` + // DnsDomain Allows to define a custom dns domain for the account DnsDomain *string `json:"dns_domain,omitempty"` @@ -598,7 +1418,7 @@ type BundleParameters struct { // BundleResult defines model for BundleResult. type BundleResult struct { - UploadKey *string `json:"upload_key"` + UploadKey *string `json:"upload_key,omitempty"` } // BundleWorkloadRequest defines model for BundleWorkloadRequest. @@ -628,6 +1448,48 @@ type BypassResponse struct { PeerId string `json:"peer_id"` } +// CACertificateResponse A certificate authority certificate +type CACertificateResponse struct { + // CertificatePem PEM-encoded CA certificate (only included in single CA detail responses) + CertificatePem *string `json:"certificate_pem,omitempty"` + + // CreatedAt CA certificate creation time (UTC) + CreatedAt time.Time `json:"created_at"` + + // DisplayName Display name (CommonName) of the CA certificate + DisplayName *string `json:"display_name,omitempty"` + + // Fingerprint SHA-256 fingerprint of the CA certificate + Fingerprint string `json:"fingerprint"` + + // Id CA certificate ID + Id string `json:"id"` + + // IsActive Whether this CA certificate is currently active + IsActive bool `json:"is_active"` + + // NotAfter Certificate validity end time (UTC) + NotAfter time.Time `json:"not_after"` + + // NotBefore Certificate validity start time (UTC) + NotBefore time.Time `json:"not_before"` + + // Organization Organization of the CA certificate + Organization *string `json:"organization,omitempty"` +} + +// CAInitRequest Optional parameters for initializing a CA certificate +type CAInitRequest struct { + // DisplayName Display name for the CA certificate CommonName. Defaults to "{domain} Internal CA ({suffix})" + DisplayName *string `json:"display_name,omitempty"` + + // Organization Organization name for the CA certificate. Defaults to "NetBird Self-Hosted" + Organization *string `json:"organization,omitempty"` + + // ValidityDays CA certificate validity in days. Defaults to 3650 (~10 years) + ValidityDays *int `json:"validity_days,omitempty"` +} + // CheckoutResponse defines model for CheckoutResponse. type CheckoutResponse struct { // SessionId The unique identifier for the checkout session. @@ -1399,6 +2261,39 @@ type InvoiceResponse struct { // InvoiceResponseType The invoice type type InvoiceResponseType string +// IssuedCertificateResponse An issued certificate +type IssuedCertificateResponse struct { + // CreatedAt Certificate issuance time (UTC) + CreatedAt time.Time `json:"created_at"` + + // DnsNames DNS names included in the certificate + DnsNames []string `json:"dns_names"` + + // HasWildcard Whether the certificate includes a wildcard DNS name + HasWildcard bool `json:"has_wildcard"` + + // Id Issued certificate ID + Id string `json:"id"` + + // NotAfter Certificate validity end time (UTC) + NotAfter time.Time `json:"not_after"` + + // NotBefore Certificate validity start time (UTC) + NotBefore time.Time `json:"not_before"` + + // PeerId ID of the peer this certificate was issued for + PeerId string `json:"peer_id"` + + // Revoked Whether the certificate has been revoked + Revoked bool `json:"revoked"` + + // SerialNumber Certificate serial number + SerialNumber string `json:"serial_number"` + + // SigningType How the certificate was signed + SigningType string `json:"signing_type"` +} + // JobRequest defines model for JobRequest. type JobRequest struct { Workload WorkloadRequest `json:"workload"` @@ -1406,9 +2301,9 @@ type JobRequest struct { // JobResponse defines model for JobResponse. type JobResponse struct { - CompletedAt *time.Time `json:"completed_at"` + CompletedAt *time.Time `json:"completed_at,omitempty"` CreatedAt time.Time `json:"created_at"` - FailedReason *string `json:"failed_reason"` + FailedReason *string `json:"failed_reason,omitempty"` Id string `json:"id"` Status JobResponseStatus `json:"status"` TriggeredBy string `json:"triggered_by"` @@ -3327,6 +4222,12 @@ type ZoneRequest struct { Name string `json:"name"` } +// GetApiCaCertificatesParams defines parameters for GetApiCaCertificates. +type GetApiCaCertificatesParams struct { + // PeerId Filter certificates by peer ID + PeerId *string `form:"peer_id,omitempty" json:"peer_id,omitempty"` +} + // GetApiEventsNetworkTrafficParams defines parameters for GetApiEventsNetworkTraffic. type GetApiEventsNetworkTrafficParams struct { // Page Page number @@ -3525,6 +4426,12 @@ type GetApiUsersParams struct { // PutApiAccountsAccountIdJSONRequestBody defines body for PutApiAccountsAccountId for application/json ContentType. type PutApiAccountsAccountIdJSONRequestBody = AccountRequest +// PostApiCaJSONRequestBody defines body for PostApiCa for application/json ContentType. +type PostApiCaJSONRequestBody = CAInitRequest + +// PostApiCaRotateJSONRequestBody defines body for PostApiCaRotate for application/json ContentType. +type PostApiCaRotateJSONRequestBody = CAInitRequest + // PostApiDnsNameserversJSONRequestBody defines body for PostApiDnsNameservers for application/json ContentType. type PostApiDnsNameserversJSONRequestBody = NameserverGroupRequest diff --git a/shared/management/proto/management.pb.go b/shared/management/proto/management.pb.go index 97a2a4d187c..227aad0f221 100644 --- a/shared/management/proto/management.pb.go +++ b/shared/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v6.33.3 +// protoc v6.33.4 // source: management.proto package proto @@ -221,6 +221,52 @@ func (RuleAction) EnumDescriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{3} } +type CertSigningType int32 + +const ( + CertSigningType_CERT_SIGNING_INTERNAL CertSigningType = 0 + CertSigningType_CERT_SIGNING_ACME CertSigningType = 1 +) + +// Enum value maps for CertSigningType. +var ( + CertSigningType_name = map[int32]string{ + 0: "CERT_SIGNING_INTERNAL", + 1: "CERT_SIGNING_ACME", + } + CertSigningType_value = map[string]int32{ + "CERT_SIGNING_INTERNAL": 0, + "CERT_SIGNING_ACME": 1, + } +) + +func (x CertSigningType) Enum() *CertSigningType { + p := new(CertSigningType) + *p = x + return p +} + +func (x CertSigningType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CertSigningType) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[4].Descriptor() +} + +func (CertSigningType) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[4] +} + +func (x CertSigningType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CertSigningType.Descriptor instead. +func (CertSigningType) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{4} +} + type ExposeProtocol int32 const ( @@ -257,11 +303,11 @@ func (x ExposeProtocol) String() string { } func (ExposeProtocol) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[4].Descriptor() + return file_management_proto_enumTypes[5].Descriptor() } func (ExposeProtocol) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[4] + return &file_management_proto_enumTypes[5] } func (x ExposeProtocol) Number() protoreflect.EnumNumber { @@ -270,7 +316,7 @@ func (x ExposeProtocol) Number() protoreflect.EnumNumber { // Deprecated: Use ExposeProtocol.Descriptor instead. func (ExposeProtocol) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{4} + return file_management_proto_rawDescGZIP(), []int{5} } type HostConfig_Protocol int32 @@ -312,11 +358,11 @@ func (x HostConfig_Protocol) String() string { } func (HostConfig_Protocol) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[5].Descriptor() + return file_management_proto_enumTypes[6].Descriptor() } func (HostConfig_Protocol) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[5] + return &file_management_proto_enumTypes[6] } func (x HostConfig_Protocol) Number() protoreflect.EnumNumber { @@ -355,11 +401,11 @@ func (x DeviceAuthorizationFlowProvider) String() string { } func (DeviceAuthorizationFlowProvider) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[6].Descriptor() + return file_management_proto_enumTypes[7].Descriptor() } func (DeviceAuthorizationFlowProvider) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[6] + return &file_management_proto_enumTypes[7] } func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { @@ -2160,6 +2206,8 @@ type PeerConfig struct { Mtu int32 `protobuf:"varint,7,opt,name=mtu,proto3" json:"mtu,omitempty"` // Auto-update config AutoUpdate *AutoUpdateSettings `protobuf:"bytes,8,opt,name=autoUpdate,proto3" json:"autoUpdate,omitempty"` + // CA certificates for the account's internal certificate authority + CaCertificatesPem [][]byte `protobuf:"bytes,9,rep,name=ca_certificates_pem,json=caCertificatesPem,proto3" json:"ca_certificates_pem,omitempty"` } func (x *PeerConfig) Reset() { @@ -2250,6 +2298,13 @@ func (x *PeerConfig) GetAutoUpdate() *AutoUpdateSettings { return nil } +func (x *PeerConfig) GetCaCertificatesPem() [][]byte { + if x != nil { + return x.CaCertificatesPem + } + return nil +} + type AutoUpdateSettings struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -4363,6 +4418,304 @@ func (*StopExposeResponse) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{52} } +type SignCertificateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CsrDer []byte `protobuf:"bytes,1,opt,name=csr_der,json=csrDer,proto3" json:"csr_der,omitempty"` + SigningType CertSigningType `protobuf:"varint,2,opt,name=signing_type,json=signingType,proto3,enum=management.CertSigningType" json:"signing_type,omitempty"` + Wildcard bool `protobuf:"varint,3,opt,name=wildcard,proto3" json:"wildcard,omitempty"` +} + +func (x *SignCertificateRequest) Reset() { + *x = SignCertificateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignCertificateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignCertificateRequest) ProtoMessage() {} + +func (x *SignCertificateRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[53] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignCertificateRequest.ProtoReflect.Descriptor instead. +func (*SignCertificateRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{53} +} + +func (x *SignCertificateRequest) GetCsrDer() []byte { + if x != nil { + return x.CsrDer + } + return nil +} + +func (x *SignCertificateRequest) GetSigningType() CertSigningType { + if x != nil { + return x.SigningType + } + return CertSigningType_CERT_SIGNING_INTERNAL +} + +func (x *SignCertificateRequest) GetWildcard() bool { + if x != nil { + return x.Wildcard + } + return false +} + +type SignCertificateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InternalCertPem []byte `protobuf:"bytes,1,opt,name=internal_cert_pem,json=internalCertPem,proto3" json:"internal_cert_pem,omitempty"` + InternalChainPem []byte `protobuf:"bytes,2,opt,name=internal_chain_pem,json=internalChainPem,proto3" json:"internal_chain_pem,omitempty"` + AcmeCertPem []byte `protobuf:"bytes,3,opt,name=acme_cert_pem,json=acmeCertPem,proto3" json:"acme_cert_pem,omitempty"` + AcmeChainPem []byte `protobuf:"bytes,4,opt,name=acme_chain_pem,json=acmeChainPem,proto3" json:"acme_chain_pem,omitempty"` + ExpiresAt int64 `protobuf:"varint,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` +} + +func (x *SignCertificateResponse) Reset() { + *x = SignCertificateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignCertificateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignCertificateResponse) ProtoMessage() {} + +func (x *SignCertificateResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[54] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignCertificateResponse.ProtoReflect.Descriptor instead. +func (*SignCertificateResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{54} +} + +func (x *SignCertificateResponse) GetInternalCertPem() []byte { + if x != nil { + return x.InternalCertPem + } + return nil +} + +func (x *SignCertificateResponse) GetInternalChainPem() []byte { + if x != nil { + return x.InternalChainPem + } + return nil +} + +func (x *SignCertificateResponse) GetAcmeCertPem() []byte { + if x != nil { + return x.AcmeCertPem + } + return nil +} + +func (x *SignCertificateResponse) GetAcmeChainPem() []byte { + if x != nil { + return x.AcmeChainPem + } + return nil +} + +func (x *SignCertificateResponse) GetExpiresAt() int64 { + if x != nil { + return x.ExpiresAt + } + return 0 +} + +type GetCACertificatesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetCACertificatesRequest) Reset() { + *x = GetCACertificatesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetCACertificatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCACertificatesRequest) ProtoMessage() {} + +func (x *GetCACertificatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[55] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCACertificatesRequest.ProtoReflect.Descriptor instead. +func (*GetCACertificatesRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{55} +} + +type GetCACertificatesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Certificates []*CACertificateInfo `protobuf:"bytes,1,rep,name=certificates,proto3" json:"certificates,omitempty"` +} + +func (x *GetCACertificatesResponse) Reset() { + *x = GetCACertificatesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetCACertificatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCACertificatesResponse) ProtoMessage() {} + +func (x *GetCACertificatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[56] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCACertificatesResponse.ProtoReflect.Descriptor instead. +func (*GetCACertificatesResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{56} +} + +func (x *GetCACertificatesResponse) GetCertificates() []*CACertificateInfo { + if x != nil { + return x.Certificates + } + return nil +} + +type CACertificateInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CertificatePem []byte `protobuf:"bytes,1,opt,name=certificate_pem,json=certificatePem,proto3" json:"certificate_pem,omitempty"` + Fingerprint string `protobuf:"bytes,2,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + IsActive bool `protobuf:"varint,3,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + NotAfter int64 `protobuf:"varint,4,opt,name=not_after,json=notAfter,proto3" json:"not_after,omitempty"` +} + +func (x *CACertificateInfo) Reset() { + *x = CACertificateInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CACertificateInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CACertificateInfo) ProtoMessage() {} + +func (x *CACertificateInfo) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[57] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CACertificateInfo.ProtoReflect.Descriptor instead. +func (*CACertificateInfo) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{57} +} + +func (x *CACertificateInfo) GetCertificatePem() []byte { + if x != nil { + return x.CertificatePem + } + return nil +} + +func (x *CACertificateInfo) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *CACertificateInfo) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *CACertificateInfo) GetNotAfter() int64 { + if x != nil { + return x.NotAfter + } + return 0 +} + type PortInfo_Range struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -4375,7 +4728,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[54] + mi := &file_management_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4388,7 +4741,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[54] + mi := &file_management_proto_msgTypes[59] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4693,7 +5046,7 @@ var file_management_proto_rawDesc = []byte{ 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xd3, 0x02, 0x0a, 0x0a, 0x50, 0x65, + 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x83, 0x03, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, @@ -4714,7 +5067,10 @@ var file_management_proto_rawDesc = []byte{ 0x3e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, + 0x6e, 0x67, 0x73, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x2e, 0x0a, 0x13, 0x63, 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x73, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x61, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x50, 0x65, 0x6d, 0x22, 0x52, 0x0a, 0x12, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, @@ -5026,83 +5382,138 @@ var file_management_proto_rawDesc = []byte{ 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, - 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x3a, 0x0a, 0x09, 0x4a, - 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x75, 0x6e, 0x6b, 0x6e, - 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x66, - 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, - 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, - 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, - 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, - 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, - 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, - 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x2a, 0x53, 0x0a, 0x0e, 0x45, - 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0f, 0x0a, - 0x0b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x10, - 0x0a, 0x0c, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x01, - 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x02, - 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, 0x44, 0x50, 0x10, 0x03, - 0x32, 0xfd, 0x06, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, - 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, - 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8d, 0x01, 0x0a, 0x16, + 0x53, 0x69, 0x67, 0x6e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x73, 0x72, 0x5f, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x73, 0x72, 0x44, 0x65, 0x72, 0x12, + 0x3e, 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x77, 0x69, 0x6c, 0x64, 0x63, 0x61, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x77, 0x69, 0x6c, 0x64, 0x63, 0x61, 0x72, 0x64, 0x22, 0xdc, 0x01, 0x0a, 0x17, + 0x53, 0x69, 0x67, 0x6e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x43, 0x65, 0x72, 0x74, + 0x50, 0x65, 0x6d, 0x12, 0x2c, 0x0a, 0x12, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, + 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x50, 0x65, + 0x6d, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x6d, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x70, + 0x65, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x61, 0x63, 0x6d, 0x65, 0x43, 0x65, + 0x72, 0x74, 0x50, 0x65, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x63, 0x6d, 0x65, 0x5f, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x61, + 0x63, 0x6d, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x50, 0x65, 0x6d, 0x12, 0x1d, 0x0a, 0x0a, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x47, 0x65, + 0x74, 0x43, 0x41, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5e, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x43, 0x41, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x41, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x22, 0x98, 0x01, 0x0a, 0x11, 0x43, 0x41, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x0f, + 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6d, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x50, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x41, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, + 0x72, 0x2a, 0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, + 0x0a, 0x0e, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, + 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, + 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, + 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, + 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, + 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, + 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, + 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, + 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, + 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, + 0x01, 0x2a, 0x43, 0x0a, 0x0f, 0x43, 0x65, 0x72, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x53, 0x49, 0x47, + 0x4e, 0x49, 0x4e, 0x47, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x00, 0x12, + 0x15, 0x0a, 0x11, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x53, 0x49, 0x47, 0x4e, 0x49, 0x4e, 0x47, 0x5f, + 0x41, 0x43, 0x4d, 0x45, 0x10, 0x01, 0x2a, 0x53, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x45, 0x58, 0x50, 0x4f, + 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, 0x50, + 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45, + 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x45, + 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, 0x44, 0x50, 0x10, 0x03, 0x32, 0xa1, 0x08, 0x0a, 0x11, + 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, - 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, - 0x12, 0x47, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, + 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, + 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x0c, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x6e, 0x65, 0x77, - 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, - 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, + 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, + 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, + 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x03, 0x4a, + 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, - 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x28, 0x01, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, + 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, + 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, + 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x0f, 0x53, + 0x69, 0x67, 0x6e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x11, + 0x47, 0x65, 0x74, 0x43, 0x41, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x73, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, + 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -5117,166 +5528,178 @@ func file_management_proto_rawDescGZIP() []byte { return file_management_proto_rawDescData } -var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 55) +var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 8) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 60) var file_management_proto_goTypes = []interface{}{ (JobStatus)(0), // 0: management.JobStatus (RuleProtocol)(0), // 1: management.RuleProtocol (RuleDirection)(0), // 2: management.RuleDirection (RuleAction)(0), // 3: management.RuleAction - (ExposeProtocol)(0), // 4: management.ExposeProtocol - (HostConfig_Protocol)(0), // 5: management.HostConfig.Protocol - (DeviceAuthorizationFlowProvider)(0), // 6: management.DeviceAuthorizationFlow.provider - (*EncryptedMessage)(nil), // 7: management.EncryptedMessage - (*JobRequest)(nil), // 8: management.JobRequest - (*JobResponse)(nil), // 9: management.JobResponse - (*BundleParameters)(nil), // 10: management.BundleParameters - (*BundleResult)(nil), // 11: management.BundleResult - (*SyncRequest)(nil), // 12: management.SyncRequest - (*SyncResponse)(nil), // 13: management.SyncResponse - (*SyncMetaRequest)(nil), // 14: management.SyncMetaRequest - (*LoginRequest)(nil), // 15: management.LoginRequest - (*PeerKeys)(nil), // 16: management.PeerKeys - (*Environment)(nil), // 17: management.Environment - (*File)(nil), // 18: management.File - (*Flags)(nil), // 19: management.Flags - (*PeerSystemMeta)(nil), // 20: management.PeerSystemMeta - (*LoginResponse)(nil), // 21: management.LoginResponse - (*ServerKeyResponse)(nil), // 22: management.ServerKeyResponse - (*Empty)(nil), // 23: management.Empty - (*NetbirdConfig)(nil), // 24: management.NetbirdConfig - (*HostConfig)(nil), // 25: management.HostConfig - (*RelayConfig)(nil), // 26: management.RelayConfig - (*FlowConfig)(nil), // 27: management.FlowConfig - (*JWTConfig)(nil), // 28: management.JWTConfig - (*ProtectedHostConfig)(nil), // 29: management.ProtectedHostConfig - (*PeerConfig)(nil), // 30: management.PeerConfig - (*AutoUpdateSettings)(nil), // 31: management.AutoUpdateSettings - (*NetworkMap)(nil), // 32: management.NetworkMap - (*SSHAuth)(nil), // 33: management.SSHAuth - (*MachineUserIndexes)(nil), // 34: management.MachineUserIndexes - (*RemotePeerConfig)(nil), // 35: management.RemotePeerConfig - (*SSHConfig)(nil), // 36: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 37: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 38: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 39: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 40: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 41: management.ProviderConfig - (*Route)(nil), // 42: management.Route - (*DNSConfig)(nil), // 43: management.DNSConfig - (*CustomZone)(nil), // 44: management.CustomZone - (*SimpleRecord)(nil), // 45: management.SimpleRecord - (*NameServerGroup)(nil), // 46: management.NameServerGroup - (*NameServer)(nil), // 47: management.NameServer - (*FirewallRule)(nil), // 48: management.FirewallRule - (*NetworkAddress)(nil), // 49: management.NetworkAddress - (*Checks)(nil), // 50: management.Checks - (*PortInfo)(nil), // 51: management.PortInfo - (*RouteFirewallRule)(nil), // 52: management.RouteFirewallRule - (*ForwardingRule)(nil), // 53: management.ForwardingRule - (*ExposeServiceRequest)(nil), // 54: management.ExposeServiceRequest - (*ExposeServiceResponse)(nil), // 55: management.ExposeServiceResponse - (*RenewExposeRequest)(nil), // 56: management.RenewExposeRequest - (*RenewExposeResponse)(nil), // 57: management.RenewExposeResponse - (*StopExposeRequest)(nil), // 58: management.StopExposeRequest - (*StopExposeResponse)(nil), // 59: management.StopExposeResponse - nil, // 60: management.SSHAuth.MachineUsersEntry - (*PortInfo_Range)(nil), // 61: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 62: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 63: google.protobuf.Duration + (CertSigningType)(0), // 4: management.CertSigningType + (ExposeProtocol)(0), // 5: management.ExposeProtocol + (HostConfig_Protocol)(0), // 6: management.HostConfig.Protocol + (DeviceAuthorizationFlowProvider)(0), // 7: management.DeviceAuthorizationFlow.provider + (*EncryptedMessage)(nil), // 8: management.EncryptedMessage + (*JobRequest)(nil), // 9: management.JobRequest + (*JobResponse)(nil), // 10: management.JobResponse + (*BundleParameters)(nil), // 11: management.BundleParameters + (*BundleResult)(nil), // 12: management.BundleResult + (*SyncRequest)(nil), // 13: management.SyncRequest + (*SyncResponse)(nil), // 14: management.SyncResponse + (*SyncMetaRequest)(nil), // 15: management.SyncMetaRequest + (*LoginRequest)(nil), // 16: management.LoginRequest + (*PeerKeys)(nil), // 17: management.PeerKeys + (*Environment)(nil), // 18: management.Environment + (*File)(nil), // 19: management.File + (*Flags)(nil), // 20: management.Flags + (*PeerSystemMeta)(nil), // 21: management.PeerSystemMeta + (*LoginResponse)(nil), // 22: management.LoginResponse + (*ServerKeyResponse)(nil), // 23: management.ServerKeyResponse + (*Empty)(nil), // 24: management.Empty + (*NetbirdConfig)(nil), // 25: management.NetbirdConfig + (*HostConfig)(nil), // 26: management.HostConfig + (*RelayConfig)(nil), // 27: management.RelayConfig + (*FlowConfig)(nil), // 28: management.FlowConfig + (*JWTConfig)(nil), // 29: management.JWTConfig + (*ProtectedHostConfig)(nil), // 30: management.ProtectedHostConfig + (*PeerConfig)(nil), // 31: management.PeerConfig + (*AutoUpdateSettings)(nil), // 32: management.AutoUpdateSettings + (*NetworkMap)(nil), // 33: management.NetworkMap + (*SSHAuth)(nil), // 34: management.SSHAuth + (*MachineUserIndexes)(nil), // 35: management.MachineUserIndexes + (*RemotePeerConfig)(nil), // 36: management.RemotePeerConfig + (*SSHConfig)(nil), // 37: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 38: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 39: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 40: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 41: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 42: management.ProviderConfig + (*Route)(nil), // 43: management.Route + (*DNSConfig)(nil), // 44: management.DNSConfig + (*CustomZone)(nil), // 45: management.CustomZone + (*SimpleRecord)(nil), // 46: management.SimpleRecord + (*NameServerGroup)(nil), // 47: management.NameServerGroup + (*NameServer)(nil), // 48: management.NameServer + (*FirewallRule)(nil), // 49: management.FirewallRule + (*NetworkAddress)(nil), // 50: management.NetworkAddress + (*Checks)(nil), // 51: management.Checks + (*PortInfo)(nil), // 52: management.PortInfo + (*RouteFirewallRule)(nil), // 53: management.RouteFirewallRule + (*ForwardingRule)(nil), // 54: management.ForwardingRule + (*ExposeServiceRequest)(nil), // 55: management.ExposeServiceRequest + (*ExposeServiceResponse)(nil), // 56: management.ExposeServiceResponse + (*RenewExposeRequest)(nil), // 57: management.RenewExposeRequest + (*RenewExposeResponse)(nil), // 58: management.RenewExposeResponse + (*StopExposeRequest)(nil), // 59: management.StopExposeRequest + (*StopExposeResponse)(nil), // 60: management.StopExposeResponse + (*SignCertificateRequest)(nil), // 61: management.SignCertificateRequest + (*SignCertificateResponse)(nil), // 62: management.SignCertificateResponse + (*GetCACertificatesRequest)(nil), // 63: management.GetCACertificatesRequest + (*GetCACertificatesResponse)(nil), // 64: management.GetCACertificatesResponse + (*CACertificateInfo)(nil), // 65: management.CACertificateInfo + nil, // 66: management.SSHAuth.MachineUsersEntry + (*PortInfo_Range)(nil), // 67: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 68: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 69: google.protobuf.Duration } var file_management_proto_depIdxs = []int32{ - 10, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters + 11, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters 0, // 1: management.JobResponse.status:type_name -> management.JobStatus - 11, // 2: management.JobResponse.bundle:type_name -> management.BundleResult - 20, // 3: management.SyncRequest.meta:type_name -> management.PeerSystemMeta - 24, // 4: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig - 30, // 5: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 35, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 32, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 50, // 8: management.SyncResponse.Checks:type_name -> management.Checks - 20, // 9: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta - 20, // 10: management.LoginRequest.meta:type_name -> management.PeerSystemMeta - 16, // 11: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 49, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress - 17, // 13: management.PeerSystemMeta.environment:type_name -> management.Environment - 18, // 14: management.PeerSystemMeta.files:type_name -> management.File - 19, // 15: management.PeerSystemMeta.flags:type_name -> management.Flags - 24, // 16: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig - 30, // 17: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 50, // 18: management.LoginResponse.Checks:type_name -> management.Checks - 62, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 25, // 20: management.NetbirdConfig.stuns:type_name -> management.HostConfig - 29, // 21: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig - 25, // 22: management.NetbirdConfig.signal:type_name -> management.HostConfig - 26, // 23: management.NetbirdConfig.relay:type_name -> management.RelayConfig - 27, // 24: management.NetbirdConfig.flow:type_name -> management.FlowConfig - 5, // 25: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 63, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration - 25, // 27: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 36, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 31, // 29: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings - 30, // 30: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 35, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 42, // 32: management.NetworkMap.Routes:type_name -> management.Route - 43, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 35, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 48, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 52, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 53, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule - 33, // 38: management.NetworkMap.sshAuth:type_name -> management.SSHAuth - 60, // 39: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry - 36, // 40: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 28, // 41: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig - 6, // 42: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 41, // 43: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 41, // 44: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 46, // 45: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 44, // 46: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 45, // 47: management.CustomZone.Records:type_name -> management.SimpleRecord - 47, // 48: management.NameServerGroup.NameServers:type_name -> management.NameServer + 12, // 2: management.JobResponse.bundle:type_name -> management.BundleResult + 21, // 3: management.SyncRequest.meta:type_name -> management.PeerSystemMeta + 25, // 4: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig + 31, // 5: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 36, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 33, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 51, // 8: management.SyncResponse.Checks:type_name -> management.Checks + 21, // 9: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta + 21, // 10: management.LoginRequest.meta:type_name -> management.PeerSystemMeta + 17, // 11: management.LoginRequest.peerKeys:type_name -> management.PeerKeys + 50, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 18, // 13: management.PeerSystemMeta.environment:type_name -> management.Environment + 19, // 14: management.PeerSystemMeta.files:type_name -> management.File + 20, // 15: management.PeerSystemMeta.flags:type_name -> management.Flags + 25, // 16: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig + 31, // 17: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 51, // 18: management.LoginResponse.Checks:type_name -> management.Checks + 68, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 26, // 20: management.NetbirdConfig.stuns:type_name -> management.HostConfig + 30, // 21: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig + 26, // 22: management.NetbirdConfig.signal:type_name -> management.HostConfig + 27, // 23: management.NetbirdConfig.relay:type_name -> management.RelayConfig + 28, // 24: management.NetbirdConfig.flow:type_name -> management.FlowConfig + 6, // 25: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 69, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration + 26, // 27: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 37, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 32, // 29: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings + 31, // 30: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 36, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 43, // 32: management.NetworkMap.Routes:type_name -> management.Route + 44, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 36, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 49, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 53, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 54, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule + 34, // 38: management.NetworkMap.sshAuth:type_name -> management.SSHAuth + 66, // 39: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry + 37, // 40: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 29, // 41: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig + 7, // 42: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 42, // 43: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 42, // 44: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 47, // 45: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 45, // 46: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 46, // 47: management.CustomZone.Records:type_name -> management.SimpleRecord + 48, // 48: management.NameServerGroup.NameServers:type_name -> management.NameServer 2, // 49: management.FirewallRule.Direction:type_name -> management.RuleDirection 3, // 50: management.FirewallRule.Action:type_name -> management.RuleAction 1, // 51: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 51, // 52: management.FirewallRule.PortInfo:type_name -> management.PortInfo - 61, // 53: management.PortInfo.range:type_name -> management.PortInfo.Range + 52, // 52: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 67, // 53: management.PortInfo.range:type_name -> management.PortInfo.Range 3, // 54: management.RouteFirewallRule.action:type_name -> management.RuleAction 1, // 55: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 51, // 56: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 52, // 56: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo 1, // 57: management.ForwardingRule.protocol:type_name -> management.RuleProtocol - 51, // 58: management.ForwardingRule.destinationPort:type_name -> management.PortInfo - 51, // 59: management.ForwardingRule.translatedPort:type_name -> management.PortInfo - 4, // 60: management.ExposeServiceRequest.protocol:type_name -> management.ExposeProtocol - 34, // 61: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes - 7, // 62: management.ManagementService.Login:input_type -> management.EncryptedMessage - 7, // 63: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 23, // 64: management.ManagementService.GetServerKey:input_type -> management.Empty - 23, // 65: management.ManagementService.isHealthy:input_type -> management.Empty - 7, // 66: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 7, // 67: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 7, // 68: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 7, // 69: management.ManagementService.Logout:input_type -> management.EncryptedMessage - 7, // 70: management.ManagementService.Job:input_type -> management.EncryptedMessage - 7, // 71: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage - 7, // 72: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage - 7, // 73: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage - 7, // 74: management.ManagementService.Login:output_type -> management.EncryptedMessage - 7, // 75: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 22, // 76: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 23, // 77: management.ManagementService.isHealthy:output_type -> management.Empty - 7, // 78: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 7, // 79: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 23, // 80: management.ManagementService.SyncMeta:output_type -> management.Empty - 23, // 81: management.ManagementService.Logout:output_type -> management.Empty - 7, // 82: management.ManagementService.Job:output_type -> management.EncryptedMessage - 7, // 83: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage - 7, // 84: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage - 7, // 85: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage - 74, // [74:86] is the sub-list for method output_type - 62, // [62:74] is the sub-list for method input_type - 62, // [62:62] is the sub-list for extension type_name - 62, // [62:62] is the sub-list for extension extendee - 0, // [0:62] is the sub-list for field type_name + 52, // 58: management.ForwardingRule.destinationPort:type_name -> management.PortInfo + 52, // 59: management.ForwardingRule.translatedPort:type_name -> management.PortInfo + 5, // 60: management.ExposeServiceRequest.protocol:type_name -> management.ExposeProtocol + 4, // 61: management.SignCertificateRequest.signing_type:type_name -> management.CertSigningType + 65, // 62: management.GetCACertificatesResponse.certificates:type_name -> management.CACertificateInfo + 35, // 63: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes + 8, // 64: management.ManagementService.Login:input_type -> management.EncryptedMessage + 8, // 65: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 24, // 66: management.ManagementService.GetServerKey:input_type -> management.Empty + 24, // 67: management.ManagementService.isHealthy:input_type -> management.Empty + 8, // 68: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 8, // 69: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 8, // 70: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 8, // 71: management.ManagementService.Logout:input_type -> management.EncryptedMessage + 8, // 72: management.ManagementService.Job:input_type -> management.EncryptedMessage + 8, // 73: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage + 8, // 74: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage + 8, // 75: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage + 8, // 76: management.ManagementService.SignCertificate:input_type -> management.EncryptedMessage + 8, // 77: management.ManagementService.GetCACertificates:input_type -> management.EncryptedMessage + 8, // 78: management.ManagementService.Login:output_type -> management.EncryptedMessage + 8, // 79: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 23, // 80: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 24, // 81: management.ManagementService.isHealthy:output_type -> management.Empty + 8, // 82: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 8, // 83: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 24, // 84: management.ManagementService.SyncMeta:output_type -> management.Empty + 24, // 85: management.ManagementService.Logout:output_type -> management.Empty + 8, // 86: management.ManagementService.Job:output_type -> management.EncryptedMessage + 8, // 87: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage + 8, // 88: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage + 8, // 89: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage + 8, // 90: management.ManagementService.SignCertificate:output_type -> management.EncryptedMessage + 8, // 91: management.ManagementService.GetCACertificates:output_type -> management.EncryptedMessage + 78, // [78:92] is the sub-list for method output_type + 64, // [64:78] is the sub-list for method input_type + 64, // [64:64] is the sub-list for extension type_name + 64, // [64:64] is the sub-list for extension extendee + 0, // [0:64] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -5921,7 +6344,67 @@ func file_management_proto_init() { return nil } } + file_management_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignCertificateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } file_management_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignCertificateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[55].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCACertificatesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[56].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCACertificatesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[57].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CACertificateInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[59].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PortInfo_Range); i { case 0: return &v.state @@ -5949,8 +6432,8 @@ func file_management_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, - NumEnums: 7, - NumMessages: 55, + NumEnums: 8, + NumMessages: 60, NumExtensions: 0, NumServices: 1, }, diff --git a/shared/management/proto/management.proto b/shared/management/proto/management.proto index 3667ae27f51..771c75875b8 100644 --- a/shared/management/proto/management.proto +++ b/shared/management/proto/management.proto @@ -60,6 +60,12 @@ service ManagementService { // StopExpose terminates an active expose session rpc StopExpose(EncryptedMessage) returns (EncryptedMessage) {} + + // SignCertificate signs a CSR and returns the issued TLS certificate + rpc SignCertificate(EncryptedMessage) returns (EncryptedMessage) {} + + // GetCACertificates returns the active CA certificates for the account + rpc GetCACertificates(EncryptedMessage) returns (EncryptedMessage) {} } message EncryptedMessage { @@ -335,6 +341,9 @@ message PeerConfig { // Auto-update config AutoUpdateSettings autoUpdate = 8; + + // CA certificates for the account's internal certificate authority + repeated bytes ca_certificates_pem = 9; } message AutoUpdateSettings { @@ -647,6 +656,11 @@ message ForwardingRule { PortInfo translatedPort = 4; } +enum CertSigningType { + CERT_SIGNING_INTERNAL = 0; + CERT_SIGNING_ACME = 1; +} + enum ExposeProtocol { EXPOSE_HTTP = 0; EXPOSE_HTTPS = 1; @@ -681,3 +695,30 @@ message StopExposeRequest { } message StopExposeResponse {} + +message SignCertificateRequest { + bytes csr_der = 1; + CertSigningType signing_type = 2; + bool wildcard = 3; +} + +message SignCertificateResponse { + bytes internal_cert_pem = 1; + bytes internal_chain_pem = 2; + bytes acme_cert_pem = 3; + bytes acme_chain_pem = 4; + int64 expires_at = 5; +} + +message GetCACertificatesRequest {} + +message GetCACertificatesResponse { + repeated CACertificateInfo certificates = 1; +} + +message CACertificateInfo { + bytes certificate_pem = 1; + string fingerprint = 2; + bool is_active = 3; + int64 not_after = 4; +} diff --git a/shared/management/proto/management_grpc.pb.go b/shared/management/proto/management_grpc.pb.go index 39a34204115..9d5fefccb06 100644 --- a/shared/management/proto/management_grpc.pb.go +++ b/shared/management/proto/management_grpc.pb.go @@ -58,6 +58,10 @@ type ManagementServiceClient interface { RenewExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) // StopExpose terminates an active expose session StopExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) + // SignCertificate signs a CSR and returns the issued TLS certificate + SignCertificate(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) + // GetCACertificates returns the active CA certificates for the account + GetCACertificates(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) } type managementServiceClient struct { @@ -221,6 +225,24 @@ func (c *managementServiceClient) StopExpose(ctx context.Context, in *EncryptedM return out, nil } +func (c *managementServiceClient) SignCertificate(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + out := new(EncryptedMessage) + err := c.cc.Invoke(ctx, "/management.ManagementService/SignCertificate", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *managementServiceClient) GetCACertificates(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + out := new(EncryptedMessage) + err := c.cc.Invoke(ctx, "/management.ManagementService/GetCACertificates", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ManagementServiceServer is the server API for ManagementService service. // All implementations must embed UnimplementedManagementServiceServer // for forward compatibility @@ -265,6 +287,10 @@ type ManagementServiceServer interface { RenewExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) // StopExpose terminates an active expose session StopExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) + // SignCertificate signs a CSR and returns the issued TLS certificate + SignCertificate(context.Context, *EncryptedMessage) (*EncryptedMessage, error) + // GetCACertificates returns the active CA certificates for the account + GetCACertificates(context.Context, *EncryptedMessage) (*EncryptedMessage, error) mustEmbedUnimplementedManagementServiceServer() } @@ -308,6 +334,12 @@ func (UnimplementedManagementServiceServer) RenewExpose(context.Context, *Encryp func (UnimplementedManagementServiceServer) StopExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { return nil, status.Errorf(codes.Unimplemented, "method StopExpose not implemented") } +func (UnimplementedManagementServiceServer) SignCertificate(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignCertificate not implemented") +} +func (UnimplementedManagementServiceServer) GetCACertificates(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCACertificates not implemented") +} func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {} // UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service. @@ -548,6 +580,42 @@ func _ManagementService_StopExpose_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _ManagementService_SignCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).SignCertificate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/SignCertificate", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).SignCertificate(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _ManagementService_GetCACertificates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).GetCACertificates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/GetCACertificates", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).GetCACertificates(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + // ManagementService_ServiceDesc is the grpc.ServiceDesc for ManagementService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -595,6 +663,14 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{ MethodName: "StopExpose", Handler: _ManagementService_StopExpose_Handler, }, + { + MethodName: "SignCertificate", + Handler: _ManagementService_SignCertificate_Handler, + }, + { + MethodName: "GetCACertificates", + Handler: _ManagementService_GetCACertificates_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/shared/management/proto/proxy_service.pb.go b/shared/management/proto/proxy_service.pb.go index 77c8ea4f47d..abc46013507 100644 --- a/shared/management/proto/proxy_service.pb.go +++ b/shared/management/proto/proxy_service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v6.33.3 +// protoc v6.33.4 // source: proxy_service.proto package proto