|
|
|
@@ -1,6 +1,7 @@
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
@@ -13,26 +14,29 @@ import (
|
|
|
|
|
"nadir/internal/config"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const defaultConfigTemplate = `# Nadir configuration - config.yaml
|
|
|
|
|
const configTemplateBase = `# Nadir configuration - config.yaml
|
|
|
|
|
#
|
|
|
|
|
# Single source of truth for runtime settings.
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
server:
|
|
|
|
|
secure_tls: true
|
|
|
|
|
# trust_proxy: false
|
|
|
|
|
# tls_cert: /etc/nadir/tls/cert.pem
|
|
|
|
|
# tls_key: /etc/nadir/tls/key.pem
|
|
|
|
|
hostname: 127.0.0.1
|
|
|
|
|
port: 9999
|
|
|
|
|
secure_tls: %s
|
|
|
|
|
%s
|
|
|
|
|
%s
|
|
|
|
|
%s
|
|
|
|
|
hostname: %s
|
|
|
|
|
port: %d
|
|
|
|
|
release_repo: https://tea.urania.dev/urania/nadir-agent
|
|
|
|
|
|
|
|
|
|
roles:
|
|
|
|
|
admin:
|
|
|
|
|
"*": ["*"]
|
|
|
|
|
auditor:
|
|
|
|
|
"*": ["read"]
|
|
|
|
|
|
|
|
|
|
assignments:
|
|
|
|
|
%s: [admin]
|
|
|
|
|
dashboard: [auditor]
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
// resolveConfigPath returns the config file path: CONFIG_PATH env (with ~ expanded)
|
|
|
|
@@ -101,7 +105,38 @@ const (
|
|
|
|
|
// installService writes the systemd unit, enables it on boot, and starts it.
|
|
|
|
|
// The unit pins the absolute executable and config paths captured now, so the
|
|
|
|
|
// service doesn't depend on the working directory at boot.
|
|
|
|
|
func installService() error {
|
|
|
|
|
func installService(args []string) error {
|
|
|
|
|
// Parse options
|
|
|
|
|
fs := flag.NewFlagSet("install", flag.ContinueOnError)
|
|
|
|
|
tlsOpt := fs.Bool("tls", false, "Generate persistent self-signed TLS cert/key and enable HTTPS")
|
|
|
|
|
unsecureOpt := fs.Bool("unsecure", false, "Serve plaintext HTTP directly")
|
|
|
|
|
trustProxyOpt := fs.Bool("trust-proxy", false, "Serve plaintext HTTP behind a trusted TLS-terminating reverse proxy")
|
|
|
|
|
hostnameOpt := fs.String("hostname", "127.0.0.1", "Hostname to bind to")
|
|
|
|
|
portOpt := fs.Int("port", 9999, "Port to bind to")
|
|
|
|
|
if err := fs.Parse(args); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
optCount := 0
|
|
|
|
|
if *tlsOpt {
|
|
|
|
|
optCount++
|
|
|
|
|
}
|
|
|
|
|
if *unsecureOpt {
|
|
|
|
|
optCount++
|
|
|
|
|
}
|
|
|
|
|
if *trustProxyOpt {
|
|
|
|
|
optCount++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if optCount > 1 {
|
|
|
|
|
return fmt.Errorf("options --tls, --unsecure, and --trust-proxy are mutually exclusive")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default to tls if nothing is specified
|
|
|
|
|
isTLS := *tlsOpt || optCount == 0
|
|
|
|
|
isUnsecure := *unsecureOpt
|
|
|
|
|
isTrustProxy := *trustProxyOpt
|
|
|
|
|
|
|
|
|
|
// Provision the PAM service the server authenticates against, so it exists
|
|
|
|
|
// before the unit starts rather than appearing on first login. Idempotent:
|
|
|
|
|
// EnsurePAMService leaves an existing /etc/pam.d/nadir untouched. runServer
|
|
|
|
@@ -130,14 +165,51 @@ func installService() error {
|
|
|
|
|
return fmt.Errorf("create data directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate and save persistent self-signed TLS certificates if TLS mode
|
|
|
|
|
certPath := filepath.Join("/var/lib/nadir/tls", "cert.pem")
|
|
|
|
|
if isTLS {
|
|
|
|
|
tlsDir := "/var/lib/nadir/tls"
|
|
|
|
|
if err := os.MkdirAll(tlsDir, 0700); err != nil {
|
|
|
|
|
return fmt.Errorf("create tls directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
keyPath := filepath.Join(tlsDir, "key.pem")
|
|
|
|
|
if _, err := os.Stat(certPath); os.IsNotExist(err) {
|
|
|
|
|
if err := generateAndSaveCert(certPath, keyPath, *hostnameOpt); 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construct configuration template content based on installation options
|
|
|
|
|
secureTLSVal := "true"
|
|
|
|
|
trustProxyLine := "# trust_proxy: false"
|
|
|
|
|
certLine := "tls_cert: /var/lib/nadir/tls/cert.pem"
|
|
|
|
|
keyLine := "tls_key: /var/lib/nadir/tls/key.pem"
|
|
|
|
|
|
|
|
|
|
if isUnsecure {
|
|
|
|
|
secureTLSVal = "false"
|
|
|
|
|
trustProxyLine = "# trust_proxy: false"
|
|
|
|
|
certLine = "# tls_cert: /var/lib/nadir/tls/cert.pem"
|
|
|
|
|
keyLine = "# tls_key: /var/lib/nadir/tls/key.pem"
|
|
|
|
|
} else if isTrustProxy {
|
|
|
|
|
secureTLSVal = "false"
|
|
|
|
|
trustProxyLine = "trust_proxy: true"
|
|
|
|
|
certLine = "# tls_cert: /var/lib/nadir/tls/cert.pem"
|
|
|
|
|
keyLine = "# tls_key: /var/lib/nadir/tls/key.pem"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
username := getUsername()
|
|
|
|
|
configContent := fmt.Sprintf(configTemplateBase, secureTLSVal, trustProxyLine, certLine, keyLine, *hostnameOpt, *portOpt, username)
|
|
|
|
|
|
|
|
|
|
// Ensure default config file exists
|
|
|
|
|
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
|
|
|
|
if err := saveDefaultConfig(cfgPath); err != nil {
|
|
|
|
|
if err := saveDefaultConfig(cfgPath, configContent); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -182,6 +254,44 @@ 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
|
|
|
|
|
fmt.Println("\n======================================================================")
|
|
|
|
|
fmt.Println(" NADIR CLIENT CREDENTIALS (COPY THESE TO SVELTEKIT FRONTEND)")
|
|
|
|
|
fmt.Println("======================================================================")
|
|
|
|
|
fmt.Printf("Token for \"dashboard\" (Bearer):\n %s\n", tokenStr)
|
|
|
|
|
if isTLS {
|
|
|
|
|
certBytes, err := os.ReadFile(certPath)
|
|
|
|
|
if err == nil {
|
|
|
|
|
fmt.Println("\nCA 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
|
|
|
|
|
}
|
|
|
|
@@ -393,21 +503,24 @@ func fatalIf(err error) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DefaultConfigContent returns the default configuration template filled with the username.
|
|
|
|
|
func DefaultConfigContent(username string) string {
|
|
|
|
|
return fmt.Sprintf(configTemplateBase, "true", "# trust_proxy: false", "# tls_cert: /var/lib/nadir/tls/cert.pem", "# tls_key: /var/lib/nadir/tls/key.pem", "127.0.0.1", 9999, username)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// saveDefaultConfig writes the default configuration template to cfgPath.
|
|
|
|
|
func saveDefaultConfig(cfgPath string) error {
|
|
|
|
|
func saveDefaultConfig(cfgPath, content string) error {
|
|
|
|
|
dir := filepath.Dir(cfgPath)
|
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
|
return fmt.Errorf("create config directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
chownToSudoUser(dir)
|
|
|
|
|
|
|
|
|
|
username := getUsername()
|
|
|
|
|
configContent := fmt.Sprintf(defaultConfigTemplate, username)
|
|
|
|
|
if err := os.WriteFile(cfgPath, []byte(configContent), 0600); err != nil {
|
|
|
|
|
if err := os.WriteFile(cfgPath, []byte(content), 0600); err != nil {
|
|
|
|
|
return fmt.Errorf("write default config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
chownToSudoUser(cfgPath)
|
|
|
|
|
fmt.Printf("created default configuration file at %s (assigned admin role to user %q)\n", cfgPath, username)
|
|
|
|
|
fmt.Printf("created default configuration file at %s\n", cfgPath)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -415,19 +528,22 @@ func usage(w io.Writer) {
|
|
|
|
|
fmt.Fprint(w, `nadir - Linux system administration backend
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
nadir [run] [-d] [-f <path>] Start the server (-d / --detach: run in background)
|
|
|
|
|
nadir --save-config [-f <path>] Save default configuration to path and exit
|
|
|
|
|
nadir install [-f <path>] Install + enable the systemd service (starts on boot)
|
|
|
|
|
nadir uninstall [--complete] Remove the service (keeps data/config; --complete wipes all)
|
|
|
|
|
nadir start|stop|restart|status Control the running service
|
|
|
|
|
nadir enable|disable Toggle start-on-boot without removing the unit
|
|
|
|
|
nadir logs [clear] Follow (default) or clear server logs
|
|
|
|
|
nadir token add <name> Mint a machine credential (Bearer token), shown once
|
|
|
|
|
nadir token rm <name> Revoke a token (effective immediately, no restart)
|
|
|
|
|
nadir token ls List token names and when they were created
|
|
|
|
|
nadir update [--check|--force] Fetch the latest release from server.release_repo and restart
|
|
|
|
|
(--check: report only; --force: re-download when already current)
|
|
|
|
|
nadir help Show this help
|
|
|
|
|
nadir [run] [-d] [-f <path>] Start the server (-d / --detach: run in background)
|
|
|
|
|
nadir --save-config [-f <path>] Save default configuration to path and exit
|
|
|
|
|
nadir install [--tls|--unsecure|--trust-proxy] Install + enable the systemd service (starts on boot)
|
|
|
|
|
(--tls: enable HTTPS with self-signed certificate, default)
|
|
|
|
|
(--unsecure: serve HTTP directly)
|
|
|
|
|
(--trust-proxy: serve HTTP behind a reverse proxy)
|
|
|
|
|
nadir uninstall [--complete] Remove the service (keeps data/config; --complete wipes all)
|
|
|
|
|
nadir start|stop|restart|status Control the running service
|
|
|
|
|
nadir enable|disable Toggle start-on-boot without removing the unit
|
|
|
|
|
nadir logs [clear] Follow (default) or clear server logs
|
|
|
|
|
nadir token add <name> Mint a machine credential (Bearer token), shown once
|
|
|
|
|
nadir token rm <name> Revoke a token (effective immediately, no restart)
|
|
|
|
|
nadir token ls List token names and when they were created
|
|
|
|
|
nadir update [--check|--force] Fetch the latest release from server.release_repo and restart
|
|
|
|
|
(--check: report only; --force: re-download when already current)
|
|
|
|
|
nadir help Show this help
|
|
|
|
|
|
|
|
|
|
Most commands need root. Config path is specified via -f/--config or CONFIG_PATH (default ~/.config/config.yaml).
|
|
|
|
|
`)
|
|
|
|
|