diff --git a/cmd/server/service.go b/cmd/server/service.go index 92eed09..8953cbc 100644 --- a/cmd/server/service.go +++ b/cmd/server/service.go @@ -21,8 +21,8 @@ const defaultConfigTemplate = `# Nadir configuration - config.yaml server: secure_tls: true # trust_proxy: false - # tls_cert: /etc/nadir/tls/cert.pem - # tls_key: /etc/nadir/tls/key.pem + tls_cert: /var/lib/nadir/tls/cert.pem + tls_key: /var/lib/nadir/tls/key.pem hostname: 127.0.0.1 port: 9999 release_repo: https://tea.urania.dev/urania/nadir-agent @@ -30,9 +30,12 @@ server: roles: admin: "*": ["*"] + auditor: + "*": ["read"] assignments: %s: [admin] + dashboard: [auditor] ` // resolveConfigPath returns the config file path: CONFIG_PATH env (with ~ expanded) @@ -130,6 +133,20 @@ func installService() error { return fmt.Errorf("create data directory: %w", err) } + // Generate and save persistent self-signed TLS certificates + tlsDir := "/var/lib/nadir/tls" + if err := os.MkdirAll(tlsDir, 0700); err != nil { + return fmt.Errorf("create tls directory: %w", err) + } + certPath := filepath.Join(tlsDir, "cert.pem") + keyPath := filepath.Join(tlsDir, "key.pem") + if _, err := os.Stat(certPath); os.IsNotExist(err) { + if err := generateAndSaveCert(certPath, keyPath); err != nil { + return fmt.Errorf("generate certificates: %w", err) + } + fmt.Printf("generated persistent self-signed TLS certificate at %s\n", certPath) + } + cfgPath, err := resolveConfigPath() if err != nil { return err @@ -182,6 +199,42 @@ WantedBy=multi-user.target fmt.Printf("created logrotate configuration %s\n", logrotatePath) } + // Generate a token for the dashboard if it doesn't exist + var tokenStr string + store, err := auth.NewTokenStore(tokenDBPath) + if err == nil { + defer store.Close() + infos, err := store.List() + hasDashboard := false + if err == nil { + for _, t := range infos { + if t.Name == "dashboard" { + hasDashboard = true + break + } + } + } + if !hasDashboard { + tokenStr, _ = store.Create("dashboard") + } else { + tokenStr = "(already created; run 'nadir token add dashboard' to replace/generate a new one if lost)" + } + } else { + fmt.Printf("warning: failed to open token store: %v\n", err) + } + + // Output credentials to copy to the frontend + certBytes, err := os.ReadFile(filepath.Join("/var/lib/nadir/tls", "cert.pem")) + if err == nil { + fmt.Println("\n======================================================================") + fmt.Println(" NADIR CLIENT CREDENTIALS (COPY THESE TO SVELTEKIT FRONTEND)") + fmt.Println("======================================================================") + fmt.Printf("Token for \"dashboard\" (Bearer):\n %s\n\n", tokenStr) + fmt.Println("CA Certificate (Trust Store):") + fmt.Println(string(certBytes)) + fmt.Println("======================================================================") + } + fmt.Printf("installed and started %s; follow logs with: %s logs\n", serviceName, filepath.Base(exe)) return nil } diff --git a/cmd/server/tls.go b/cmd/server/tls.go index 628572a..aeb69f1 100644 --- a/cmd/server/tls.go +++ b/cmd/server/tls.go @@ -7,9 +7,11 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/pem" "log" "math/big" "net" + "os" "time" ) @@ -55,3 +57,69 @@ func generateSelfSignedCert() (tls.Certificate, error) { } return tls.Certificate{Certificate: [][]byte{derBytes}, PrivateKey: priv}, nil } + +// generateAndSaveCert generates a self-signed certificate and private key, +// and saves them as PEM files. +func generateAndSaveCert(certPath, keyPath string) error { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return err + } + + template := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{Organization: []string{"nadir-agent-tls"}}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(3650 * 24 * time.Hour), // 10 years + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + } + + // Automatically add the machine's external IP addresses to SANs so it can be verified + // correctly over the local network (e.g. Tailscale or Netbird). + if addrs, err := net.InterfaceAddrs(); err == nil { + for _, a := range addrs { + if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + template.IPAddresses = append(template.IPAddresses, ipnet.IP) + template.DNSNames = append(template.DNSNames, ipnet.IP.String()) + } + } + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return err + } + + certOut, err := os.Create(certPath) + if err != nil { + return err + } + defer certOut.Close() + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return err + } + + keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer keyOut.Close() + privBytes, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return err + } + if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}); err != nil { + return err + } + + return nil +}