+136
-8
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -8,8 +9,11 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
|
||||
"nadir/internal/auth"
|
||||
"nadir/internal/config"
|
||||
)
|
||||
@@ -137,6 +141,125 @@ func installService(args []string) error {
|
||||
isUnsecure := *unsecureOpt || optCount == 0
|
||||
isTrustProxy := *trustProxyOpt
|
||||
|
||||
cfgPath, err := resolveConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shouldWriteConfig := false
|
||||
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
||||
shouldWriteConfig = true
|
||||
}
|
||||
|
||||
username := getUsername()
|
||||
var logFiles map[string][]string
|
||||
|
||||
if fs.NFlag() == 0 && (isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
if !shouldWriteConfig {
|
||||
fmt.Printf("Configuration file already exists at %s. Overwrite? [y/N] (default n): ", cfgPath)
|
||||
overwriteInput, _ := reader.ReadString('\n')
|
||||
overwriteInput = strings.ToLower(strings.TrimSpace(overwriteInput))
|
||||
if overwriteInput != "y" && overwriteInput != "yes" {
|
||||
fmt.Println("Keeping existing configuration. Proceeding with installation...")
|
||||
if existingCfg, loadErr := config.Load(cfgPath); loadErr == nil {
|
||||
*hostnameOpt = existingCfg.Server.Hostname
|
||||
if p, err := strconv.Atoi(existingCfg.Server.Port); err == nil {
|
||||
*portOpt = p
|
||||
}
|
||||
isTLS = existingCfg.Server.TLSCert != "" && existingCfg.Server.TLSKey != ""
|
||||
isTrustProxy = existingCfg.Server.TrustProxy
|
||||
isUnsecure = !isTLS && !isTrustProxy
|
||||
}
|
||||
goto skipConfigPrompt
|
||||
}
|
||||
shouldWriteConfig = true
|
||||
}
|
||||
|
||||
fmt.Println("Configuring Nadir installation:")
|
||||
fmt.Println(" 1) Serve plaintext HTTP directly (unsecure) [default]")
|
||||
fmt.Println(" 2) Generate persistent self-signed TLS cert/key and enable HTTPS (tls)")
|
||||
fmt.Println(" 3) Serve plaintext HTTP behind a trusted TLS-terminating reverse proxy (trust-proxy)")
|
||||
fmt.Print("Enter choice [1-3] (default 1): ")
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
if choice == "" || choice == "1" {
|
||||
isUnsecure = true
|
||||
isTLS = false
|
||||
isTrustProxy = false
|
||||
} else if choice == "2" {
|
||||
isTLS = true
|
||||
isUnsecure = false
|
||||
isTrustProxy = false
|
||||
} else if choice == "3" {
|
||||
isTrustProxy = true
|
||||
isTLS = false
|
||||
isUnsecure = false
|
||||
} else {
|
||||
return fmt.Errorf("invalid choice: %q", choice)
|
||||
}
|
||||
|
||||
fmt.Printf("Enter hostname to bind to (default %s): ", *hostnameOpt)
|
||||
hostChoice, _ := reader.ReadString('\n')
|
||||
hostChoice = strings.TrimSpace(hostChoice)
|
||||
if hostChoice != "" {
|
||||
*hostnameOpt = hostChoice
|
||||
}
|
||||
|
||||
fmt.Printf("Enter port to bind to (default %d): ", *portOpt)
|
||||
portChoice, _ := reader.ReadString('\n')
|
||||
portChoice = strings.TrimSpace(portChoice)
|
||||
if portChoice != "" {
|
||||
p, err := strconv.Atoi(portChoice)
|
||||
if err != nil || p <= 0 || p > 65535 {
|
||||
return fmt.Errorf("invalid port: %q", portChoice)
|
||||
}
|
||||
*portOpt = p
|
||||
}
|
||||
|
||||
fmt.Printf("Enter main admin username (default %s): ", username)
|
||||
userChoice, _ := reader.ReadString('\n')
|
||||
userChoice = strings.TrimSpace(userChoice)
|
||||
if userChoice != "" {
|
||||
username = userChoice
|
||||
}
|
||||
|
||||
fmt.Print("Would you like to expose any log files to the Nadir UI? [y/N] (default n): ")
|
||||
logInput, _ := reader.ReadString('\n')
|
||||
logInput = strings.ToLower(strings.TrimSpace(logInput))
|
||||
if logInput == "y" || logInput == "yes" {
|
||||
logFiles = make(map[string][]string)
|
||||
for {
|
||||
fmt.Print(" Enter service/unit name (e.g. nginx): ")
|
||||
unit, _ := reader.ReadString('\n')
|
||||
unit = strings.TrimSpace(unit)
|
||||
if unit == "" {
|
||||
fmt.Println(" Service name cannot be empty. Skipping.")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" Enter absolute path to log file for %s: ", unit)
|
||||
path, _ := reader.ReadString('\n')
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
fmt.Println(" Path cannot be empty. Skipping.")
|
||||
continue
|
||||
}
|
||||
|
||||
logFiles[unit] = append(logFiles[unit], path)
|
||||
|
||||
fmt.Print(" Add another log file? [y/N] (default n): ")
|
||||
another, _ := reader.ReadString('\n')
|
||||
another = strings.ToLower(strings.TrimSpace(another))
|
||||
if another != "y" && another != "yes" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
skipConfigPrompt:
|
||||
// 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
|
||||
@@ -181,11 +304,6 @@ func installService(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
cfgPath, err := resolveConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Construct configuration template content based on installation options
|
||||
secureTLSVal := "true"
|
||||
trustProxyLine := "# trust_proxy: false"
|
||||
@@ -204,11 +322,21 @@ func installService(args []string) error {
|
||||
keyLine = "# tls_key: /var/lib/nadir/tls/key.pem"
|
||||
}
|
||||
|
||||
username := getUsername()
|
||||
configContent := fmt.Sprintf(configTemplateBase, secureTLSVal, trustProxyLine, certLine, keyLine, *hostnameOpt, *portOpt, username)
|
||||
if len(logFiles) > 0 {
|
||||
var logFilesSection strings.Builder
|
||||
logFilesSection.WriteString("\nlog_files:\n")
|
||||
for unit, paths := range logFiles {
|
||||
logFilesSection.WriteString(fmt.Sprintf(" %s:\n", unit))
|
||||
for _, path := range paths {
|
||||
logFilesSection.WriteString(fmt.Sprintf(" - %s\n", path))
|
||||
}
|
||||
}
|
||||
configContent += logFilesSection.String()
|
||||
}
|
||||
|
||||
// Ensure default config file exists
|
||||
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
||||
// Ensure default config file exists or we explicitly overwrote it
|
||||
if shouldWriteConfig {
|
||||
if err := saveDefaultConfig(cfgPath, configContent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20260527172527-a09352b57a22
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
|
||||
-223
@@ -1,223 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Nadir interactive setup — generates config.yaml
|
||||
#
|
||||
# Run this script on a fresh box after placing the nadir binary, before
|
||||
# `nadir install`. It asks about server settings, TLS mode, and log-file
|
||||
# tracking, then writes the configuration file and prints the next steps.
|
||||
#
|
||||
# Everything in here is a question with a default answer in brackets.
|
||||
# Press Enter to accept the default.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
set -e
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
prompt() {
|
||||
printf "%s [%s]: " "$1" "$2"
|
||||
read -r val
|
||||
echo "${val:-$2}"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
while :; do
|
||||
printf "%s (y/n) [%s]: " "$1" "$2"
|
||||
read -r val
|
||||
val=$(printf "%s" "${val:-$2}" | tr '[:upper:]' '[:lower:]')
|
||||
case "$val" in
|
||||
y|yes) return 0 ;;
|
||||
n|no) return 1 ;;
|
||||
*) printf " please answer y or n.\n" >&2 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
prompt_multi() {
|
||||
desc="$1"
|
||||
label="$2"
|
||||
result=""
|
||||
printf "%s\n" "$desc"
|
||||
printf " Enter one %s at a time, then press Enter on an empty line when done.\n" "$label"
|
||||
while :; do
|
||||
printf " > "
|
||||
read -r val
|
||||
[ -z "$val" ] && break
|
||||
result="${result} - ${val}
|
||||
"
|
||||
done
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# ── preamble ─────────────────────────────────────────────────────────
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This script should usually be run as root so the config file ends"
|
||||
echo "up owned by root (the server runs as root). Continue anyway? (y/n) [y]"
|
||||
if ! prompt_yn "Continue without root?" "y"; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Nadir — interactive setup ║"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo
|
||||
|
||||
# ── 1. admin username ───────────────────────────────────────────────
|
||||
|
||||
SUGGESTED_USER="${SUDO_USER:-$USER}"
|
||||
if [ -z "$SUGGESTED_USER" ] || [ "$SUGGESTED_USER" = "root" ]; then
|
||||
SUGGESTED_USER="admin"
|
||||
fi
|
||||
ADMIN_USER=$(prompt "Admin username (system user assigned the admin role)" "$SUGGESTED_USER")
|
||||
|
||||
# ── 2. config path ───────────────────────────────────────────────────
|
||||
|
||||
DEFAULT_CONFIG="${CONFIG_PATH:-$HOME/.config/nadir/config.yaml}"
|
||||
CONFIG_PATH=$(prompt "Config file path" "$DEFAULT_CONFIG")
|
||||
|
||||
# ── 3. hostname ──────────────────────────────────────────────────────
|
||||
|
||||
HOSTNAME=$(prompt "Bind address (use 127.0.0.1 for local-only, or a Tailscale/Netbird IP)" "127.0.0.1")
|
||||
|
||||
# ── 4. port ──────────────────────────────────────────────────────────
|
||||
|
||||
PORT=$(prompt "Port" "9999")
|
||||
|
||||
# ── 5. TLS mode ──────────────────────────────────────────────────────
|
||||
|
||||
echo
|
||||
echo "How should clients connect to nadir?"
|
||||
echo " 1) Plain HTTP (unsecure — local-only or dev)"
|
||||
echo " 2) HTTPS with a self-signed certificate (nadir generates cert + key)"
|
||||
echo " 3) Behind a TLS-terminating reverse proxy (trust_proxy mode)"
|
||||
printf "Choose [1-3] [1]: "
|
||||
read -r tls_mode
|
||||
tls_mode="${tls_mode:-1}"
|
||||
|
||||
SECURE_TLS="false"
|
||||
TRUST_PROXY_LINE="# trust_proxy: false"
|
||||
CERT_LINE="# tls_cert: /var/lib/nadir/tls/cert.pem"
|
||||
KEY_LINE="# tls_key: /var/lib/nadir/tls/key.pem"
|
||||
|
||||
case "$tls_mode" in
|
||||
2)
|
||||
echo " → HTTPS with self-signed certificate"
|
||||
SECURE_TLS="true"
|
||||
CERT_LINE="tls_cert: /var/lib/nadir/tls/cert.pem"
|
||||
KEY_LINE="tls_key: /var/lib/nadir/tls/key.pem"
|
||||
;;
|
||||
3)
|
||||
echo " → Behind reverse proxy (trust_proxy)"
|
||||
SECURE_TLS="true"
|
||||
TRUST_PROXY_LINE="trust_proxy: true"
|
||||
;;
|
||||
*)
|
||||
echo " → Plain HTTP"
|
||||
SECURE_TLS="false"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── 6. release repo ──────────────────────────────────────────────────
|
||||
|
||||
RELEASE_REPO=$(prompt "Gitea release repo (for auto-updates, or leave blank to disable)" "")
|
||||
|
||||
# ── 7. log files ─────────────────────────────────────────────────────
|
||||
|
||||
echo
|
||||
if prompt_yn "Track log files (add entries to log_files allowlist)?" "n"; then
|
||||
echo
|
||||
echo "For each service (e.g. nginx, ssh, auth), you'll list the paths"
|
||||
echo "nadir is allowed to serve. Add as many services as you like."
|
||||
echo
|
||||
LOG_FILES_SECTION="log_files:"
|
||||
LOG_FILES_BODY=""
|
||||
|
||||
while :; do
|
||||
service=$(prompt " Service name (e.g. nginx, sshd) — empty line to stop" "")
|
||||
[ -z "$service" ] && break
|
||||
paths=$(prompt_multi "" "log path for $service")
|
||||
LOG_FILES_BODY="${LOG_FILES_BODY}
|
||||
${service}:"
|
||||
# indentation inside prompt_multi already includes leading spaces
|
||||
IFS='
|
||||
'
|
||||
for line in $paths; do
|
||||
LOG_FILES_BODY="${LOG_FILES_BODY}
|
||||
${line}"
|
||||
done
|
||||
echo
|
||||
done
|
||||
|
||||
if [ -n "$LOG_FILES_BODY" ]; then
|
||||
LOG_FILES_SECTION="${LOG_FILES_SECTION}${LOG_FILES_BODY}"
|
||||
else
|
||||
LOG_FILES_SECTION="# log_files:"
|
||||
fi
|
||||
else
|
||||
LOG_FILES_SECTION="# log_files:"
|
||||
fi
|
||||
|
||||
# ── write config ─────────────────────────────────────────────────────
|
||||
|
||||
CONFIG_DIR=$(dirname "$CONFIG_PATH")
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
cat > "$CONFIG_PATH" <<CONFIGEOF
|
||||
# Nadir configuration - config.yaml
|
||||
# Generated by install.sh
|
||||
|
||||
server:
|
||||
secure_tls: ${SECURE_TLS}
|
||||
${TRUST_PROXY_LINE}
|
||||
${CERT_LINE}
|
||||
${KEY_LINE}
|
||||
hostname: ${HOSTNAME}
|
||||
port: ${PORT}
|
||||
release_repo: ${RELEASE_REPO}
|
||||
|
||||
roles:
|
||||
admin:
|
||||
"*": ["*"]
|
||||
auditor:
|
||||
"*": ["read"]
|
||||
|
||||
assignments:
|
||||
${ADMIN_USER}: [admin]
|
||||
dashboard: [auditor]
|
||||
|
||||
${LOG_FILES_SECTION}
|
||||
CONFIGEOF
|
||||
|
||||
chmod 600 "$CONFIG_PATH"
|
||||
echo
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Configuration written to: ║"
|
||||
echo "║ $CONFIG_PATH"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo
|
||||
echo " 1. Make sure the nadir binary is installed:"
|
||||
echo " sudo cp ./nadir /usr/local/bin/nadir"
|
||||
echo " sudo chmod +x /usr/local/bin/nadir"
|
||||
echo
|
||||
echo " 2. Run the service installer:"
|
||||
echo " sudo nadir install"
|
||||
echo
|
||||
echo " 3. If you chose TLS mode, nadir will generate a self-signed"
|
||||
echo " certificate automatically during install."
|
||||
echo
|
||||
echo " 4. Check status:"
|
||||
echo " nadir status"
|
||||
echo
|
||||
|
||||
# If a SUDO_USER exists, chown the config directory so the user can
|
||||
# read it without root.
|
||||
if [ -n "${SUDO_USER}" ] && [ "${SUDO_USER}" != "root" ]; then
|
||||
chown "${SUDO_USER}:${SUDO_USER}" "$CONFIG_DIR" 2>/dev/null || true
|
||||
chown "${SUDO_USER}:${SUDO_USER}" "$CONFIG_PATH" 2>/dev/null || true
|
||||
fi
|
||||
+5
-1
@@ -99,7 +99,11 @@ do_install() {
|
||||
|
||||
echo "binary installed at /usr/local/bin/nadir"
|
||||
echo "installing as a systemd service ..."
|
||||
/usr/local/bin/nadir install
|
||||
if [ -c /dev/tty ]; then
|
||||
/usr/local/bin/nadir install < /dev/tty
|
||||
else
|
||||
/usr/local/bin/nadir install
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "done. check status with: nadir status"
|
||||
|
||||
Reference in New Issue
Block a user