feat: privilege separation (#114)

This commit is contained in:
Julian Sommer
2026-04-07 16:22:19 +10:00
committed by GitHub
parent 99f9949bdd
commit a56a2baf1f
21 changed files with 540 additions and 131 deletions
+12 -7
View File
@@ -180,9 +180,13 @@ jobs:
sparse-checkout: scripts/tofu-cluster
path: mulga
- name: Clone and build
- name: Build Install Artifacts
working-directory: mulga/scripts/tofu-cluster
run: ./build-artifacts.sh "${GIT_BRANCH}" "${RUNNER_TEMP}/spinifex-artifacts.tar"
run: |
./build-install-artifacts.sh "${GIT_BRANCH}" \
"${RUNNER_TEMP}/spinifex-distro.tar.gz" \
"${RUNNER_TEMP}/setup.sh" \
"${RUNNER_TEMP}/spinifex-tests.tar"
- name: Provision VMs
working-directory: mulga/scripts/tofu-cluster
@@ -194,13 +198,14 @@ jobs:
./scrub-env.sh env2 >> "$TOFU_LOG" 2>&1 || true
tofu apply -var-file=envs/env2.tfvars -auto-approve -input=false >> "$TOFU_LOG" 2>&1
- name: Bootstrap Cluster
- name: Bootstrap via Binary Install
working-directory: mulga/scripts/tofu-cluster
run: |
SPX_PREBUILT=true \
SPX_ARTIFACT_TAR="${RUNNER_TEMP}/spinifex-artifacts.tar" \
SPX_EXTERNAL_POOL="192.168.1.240-192.168.1.250" \
./bootstrap.sh env2 >> "$TOFU_LOG" 2>&1
./bootstrap-install.sh env2 \
"${RUNNER_TEMP}/setup.sh" \
"${RUNNER_TEMP}/spinifex-distro.tar.gz" \
--test-tar "${RUNNER_TEMP}/spinifex-tests.tar"
- name: Run E2E Tests
working-directory: mulga/scripts/tofu-cluster
@@ -218,7 +223,7 @@ jobs:
done
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" tf-user@${NODE1_IP} \
"cd ~/Development/mulga/spinifex && ./tests/e2e/run-multinode-e2e.sh ${NODE_IPS} && ./tests/e2e/run-cert-e2e.sh --multinode ${NODE_IPS}"
"cd ~/e2e-tests && ./run-multinode-e2e.sh ${NODE_IPS} && ./run-cert-e2e.sh --multinode ${NODE_IPS}"
- name: Teardown
if: always()
+13 -1
View File
@@ -116,6 +116,17 @@ run:
$(MAKE) go_build
$(MAKE) go_run
# Fast iteration: build + install binary + restart all services
deploy: build
sudo install -m 755 bin/spx /usr/local/bin/spx
sudo systemctl daemon-reload
sudo systemctl restart spinifex.target
# Re-run setup.sh after changing systemd units, helper scripts, or logrotate config.
# Not needed for code-only changes — use deploy for those.
reinstall:
scripts/dev-install.sh
clean:
rm -f ./bin/$(GO_PROJECT_NAME)
rm -rf spinifex/services/spinifexui/frontend/dist
@@ -207,7 +218,8 @@ distro-arm64:
distro-clean:
rm -rf dist/
.PHONY: build build-ui build-alb-agent build-system-image build-alb-image go_build go_run preflight test test-cover test-race diff-coverage bench run clean \
.PHONY: build build-ui build-alb-agent build-system-image build-alb-image go_build go_run preflight test test-cover test-race diff-coverage bench run \
deploy reinstall clean \
install-system install-go install-aws quickinstall \
lint fix govulncheck \
distro distro-amd64 distro-arm64 distro-clean
+23 -1
View File
@@ -6,7 +6,7 @@ Wants=spinifex-nats.service
[Service]
Type=simple
User=spinifex
User=spinifex-gw
Group=spinifex
ExecStartPre=/var/lib/spinifex/wait-for-nats.sh
ExecStart=/usr/local/bin/spx service awsgw start
@@ -18,5 +18,27 @@ Environment=SPINIFEX_BASE_DIR=/var/lib/spinifex/
Environment=SPINIFEX_AWSGW_TLS_CERT=/etc/spinifex/server.pem
Environment=SPINIFEX_AWSGW_TLS_KEY=/etc/spinifex/server.key
# Filesystem access
NoNewPrivileges=yes
ProtectSystem=strict
PrivateDevices=yes
ReadOnlyPaths=/etc/spinifex/spinifex.toml /etc/spinifex/master.key /etc/spinifex/server.pem /etc/spinifex/server.key
ReadWritePaths=/var/lib/spinifex /var/log/spinifex /etc/spinifex/bootstrap.json /tmp
# Hardening
ProtectHome=yes
PrivateTmp=no
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
SystemCallArchitectures=native
[Install]
WantedBy=spinifex.target
+25 -1
View File
@@ -6,7 +6,7 @@ Wants=spinifex-nats.service
[Service]
Type=simple
User=spinifex
User=spinifex-daemon
Group=spinifex
ExecStartPre=/var/lib/spinifex/wait-for-nats.sh
ExecStart=/usr/local/bin/spx service spinifex start
@@ -17,5 +17,29 @@ Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/spinifex.toml
Environment=SPINIFEX_BASE_DIR=/var/lib/spinifex/spinifex/
Environment=SPINIFEX_WAL_DIR=/var/lib/spinifex/spinifex/
# Filesystem access — NoNewPrivileges omitted (daemon uses sudo for ip, ovs-vsctl, dhcpcd)
ProtectSystem=full
PrivateDevices=no
DevicePolicy=closed
DeviceAllow=/dev/kvm rw
DeviceAllow=/dev/net/tun rw
ReadOnlyPaths=/etc/spinifex/spinifex.toml /etc/spinifex/server.pem /etc/spinifex/server.key
ReadWritePaths=/var/lib/spinifex/spinifex /var/log/spinifex /run/spinifex
# Hardening
ProtectHome=yes
PrivateTmp=no
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
SystemCallArchitectures=native
[Install]
WantedBy=spinifex.target
+23 -1
View File
@@ -5,7 +5,7 @@ Before=spinifex-predastore.service spinifex-daemon.service spinifex-awsgw.servic
[Service]
Type=simple
User=spinifex
User=spinifex-nats
Group=spinifex
ExecStart=/usr/local/bin/spx service nats start
Restart=on-failure
@@ -13,5 +13,27 @@ RestartSec=5
OOMScoreAdjust=-500
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/nats/nats.conf
# Filesystem access
NoNewPrivileges=yes
ProtectSystem=strict
PrivateDevices=yes
ReadOnlyPaths=/etc/spinifex/nats
ReadWritePaths=/var/lib/spinifex/nats /var/log/spinifex /tmp
# Hardening
ProtectHome=yes
PrivateTmp=no
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
SystemCallArchitectures=native
[Install]
WantedBy=spinifex.target
+23 -1
View File
@@ -6,7 +6,7 @@ Wants=spinifex-nats.service
[Service]
Type=simple
User=spinifex
User=spinifex-storage
Group=spinifex
ExecStartPre=/var/lib/spinifex/wait-for-nats.sh
ExecStart=/var/lib/spinifex/predastore-start.sh
@@ -18,5 +18,27 @@ Environment=SPINIFEX_PREDASTORE_BASE_PATH=/var/lib/spinifex/predastore/
Environment=SPINIFEX_PREDASTORE_TLS_CERT=/etc/spinifex/server.pem
Environment=SPINIFEX_PREDASTORE_TLS_KEY=/etc/spinifex/server.key
# Filesystem access
NoNewPrivileges=yes
ProtectSystem=strict
PrivateDevices=yes
ReadOnlyPaths=/etc/spinifex/predastore /etc/spinifex/spinifex.toml /etc/spinifex/master.key /etc/spinifex/server.pem /etc/spinifex/server.key
ReadWritePaths=/var/lib/spinifex/predastore /var/log/spinifex /tmp
# Hardening
ProtectHome=yes
PrivateTmp=no
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
SystemCallArchitectures=native
[Install]
WantedBy=spinifex.target
+23 -1
View File
@@ -5,7 +5,7 @@ After=spinifex-awsgw.service
[Service]
Type=simple
User=spinifex
User=spinifex-ui
Group=spinifex
ExecStart=/usr/local/bin/spx service spinifex-ui start
Restart=on-failure
@@ -14,5 +14,27 @@ OOMScoreAdjust=-500
Environment=SPINIFEX_UI_TLS_CERT=/etc/spinifex/server.pem
Environment=SPINIFEX_UI_TLS_KEY=/etc/spinifex/server.key
# Filesystem access
NoNewPrivileges=yes
ProtectSystem=strict
PrivateDevices=yes
ReadOnlyPaths=/etc/spinifex/server.pem /etc/spinifex/server.key
ReadWritePaths=/var/log/spinifex /tmp
# Hardening
ProtectHome=yes
PrivateTmp=no
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
SystemCallArchitectures=native
[Install]
WantedBy=spinifex.target
+23 -1
View File
@@ -6,7 +6,7 @@ Wants=spinifex-nats.service
[Service]
Type=simple
User=spinifex
User=spinifex-viperblock
Group=spinifex
ExecStartPre=/var/lib/spinifex/wait-for-nats.sh
ExecStart=/usr/local/bin/spx service viperblock start
@@ -17,5 +17,27 @@ EnvironmentFile=/etc/spinifex/systemd.env
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/spinifex.toml
Environment=SPINIFEX_BASE_DIR=/var/lib/spinifex/viperblock/
# Filesystem access
NoNewPrivileges=yes
ProtectSystem=strict
PrivateDevices=yes
ReadOnlyPaths=/etc/spinifex/spinifex.toml /etc/spinifex/systemd.env
ReadWritePaths=/var/lib/spinifex/viperblock /var/log/spinifex /tmp
# Hardening
ProtectHome=yes
PrivateTmp=no
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
SystemCallArchitectures=native
[Install]
WantedBy=spinifex.target
+23 -1
View File
@@ -6,7 +6,7 @@ Wants=spinifex-nats.service
[Service]
Type=simple
User=spinifex
User=spinifex-vpcd
Group=spinifex
ExecStartPre=/var/lib/spinifex/wait-for-nats.sh
ExecStart=/usr/local/bin/spx service vpcd start
@@ -14,6 +14,28 @@ Restart=on-failure
RestartSec=5
OOMScoreAdjust=-500
Environment=SPINIFEX_CONFIG_PATH=/etc/spinifex/spinifex.toml
Environment=SPINIFEX_BASE_DIR=/var/lib/spinifex/vpcd/
# Filesystem access — NoNewPrivileges omitted (vpcd uses sudo for ovs-vsctl, ovn-nbctl, systemctl)
ProtectSystem=full
PrivateDevices=yes
ReadOnlyPaths=/etc/spinifex/spinifex.toml
ReadWritePaths=/var/lib/spinifex/vpcd /var/log/spinifex
# Hardening
ProtectHome=yes
PrivateTmp=no
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
SystemCallArchitectures=native
[Install]
WantedBy=spinifex.target
+12 -21
View File
@@ -933,6 +933,7 @@ func runAdminInit(cmd *cobra.Command, args []string) {
Region: region,
NatsToken: natsToken,
DataDir: spxRoot,
LogDir: LogDirFor(spxRoot),
ConfigDir: configDir,
Node: node,
@@ -1014,14 +1015,10 @@ func runAdminInit(cmd *cobra.Command, args []string) {
admin.CreateServiceDirectories(spxRoot)
// In production layout (running as root), fix ownership so the service user
// can read config and write data. SUDO_USER identifies the operator account.
// In production layout (running as root), set per-service ownership on
// directories and shared config files (root:spinifex with correct modes).
if os.Getuid() == 0 {
sudoUser := os.Getenv("SUDO_USER")
if sudoUser != "" {
admin.ChownRecursive(configDir, sudoUser)
admin.ChownRecursive(spxRoot, sudoUser)
}
admin.SetServiceOwnership()
}
// Print success message
@@ -1171,6 +1168,7 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
Region: region,
NatsToken: natsToken,
DataDir: spxRoot,
LogDir: LogDirFor(spxRoot),
ConfigDir: configDir,
Node: node,
@@ -1246,14 +1244,10 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
admin.CreateServiceDirectories(spxRoot)
// In production layout (running as root), fix ownership so the service user
// can read config and write data. SUDO_USER identifies the operator account.
// In production layout (running as root), set per-service ownership on
// directories and shared config files (root:spinifex with correct modes).
if os.Getuid() == 0 {
sudoUser := os.Getenv("SUDO_USER")
if sudoUser != "" {
admin.ChownRecursive(configDir, sudoUser)
admin.ChownRecursive(spxRoot, sudoUser)
}
admin.SetServiceOwnership()
}
// Keep formation server running briefly so joining nodes can fetch complete status
@@ -1569,6 +1563,7 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
Region: creds.Region,
NatsToken: creds.NatsToken,
DataDir: dataDir,
LogDir: LogDirFor(dataDir),
ConfigDir: configDir,
Node: node,
@@ -1648,14 +1643,10 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
admin.CreateServiceDirectories(dataDir)
// In production layout (running as root), fix ownership so the service user
// can read config and write data. SUDO_USER identifies the operator account.
// In production layout (running as root), set per-service ownership on
// directories and shared config files (root:spinifex with correct modes).
if os.Getuid() == 0 {
sudoUser := os.Getenv("SUDO_USER")
if sudoUser != "" {
admin.ChownRecursive(configDir, sudoUser)
admin.ChownRecursive(dataDir, sudoUser)
}
admin.SetServiceOwnership()
}
// Print cluster summary
+10
View File
@@ -26,6 +26,16 @@ func DefaultDataDir() string {
return filepath.Join(realUserHomeDir(), "spinifex")
}
// LogDirFor returns the log directory for a given data directory.
// Production: /var/log/spinifex (matches systemd ReadWritePaths)
// Development: <dataDir>/logs (supports custom per-node data dirs)
func LogDirFor(dataDir string) string {
if isProductionLayout() {
return "/var/log/spinifex"
}
return filepath.Join(dataDir, "logs")
}
// realUserHomeDir returns the home directory of the real (non-sudo) user.
// When running under sudo, SUDO_USER is set to the invoking user — resolve
// their home directory so config/data land in the right place.
+6
View File
@@ -733,6 +733,12 @@ var vpcdStartCmd = &cobra.Command{
}
}
baseDir := viper.GetString("base-dir")
if baseDir != "" {
fmt.Println("Overwriting vpcd base-dir to:", baseDir)
nodeConfig.BaseDir = baseDir
}
svc, err := service.New("vpcd", &vpcd.Config{
NatsHost: nodeConfig.NATS.Host,
NatsToken: nodeConfig.NATS.ACL.Token,
+1 -1
View File
@@ -40,7 +40,7 @@ jetstream {
debug: false
trace: false
logtime: true
log_file: "{{.DataDir}}/logs/nats.log"
log_file: "{{.LogDir}}/nats.log"
# Limits
max_connections: 1000
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# dev-install.sh — Full local development setup via production installer.
# Builds from source, assembles a tarball, runs setup.sh for scaffolding
# (user creation, directories, systemd units), initializes the cluster,
# and starts services via systemd.
#
# Usage: ./scripts/dev-install.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MULGA_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
echo "=== Building ==="
cd "$PROJECT_ROOT" && make build
cd "$MULGA_ROOT/viperblock" && make go_build_nbd
echo "=== Assembling tarball for setup.sh ==="
STAGING=$(mktemp -d)
trap 'rm -rf "$STAGING"' EXIT
cp "$PROJECT_ROOT/bin/spx" "$STAGING/spx"
cp "$MULGA_ROOT/viperblock/lib/nbdkit-viperblock-plugin.so" "$STAGING/"
cp "$PROJECT_ROOT/scripts/setup-ovn.sh" "$STAGING/"
mkdir -p "$STAGING/systemd"
cp "$PROJECT_ROOT/build/systemd/"* "$STAGING/systemd/"
mkdir -p "$STAGING/scripts"
cp "$PROJECT_ROOT/build/scripts/"* "$STAGING/scripts/"
cp "$PROJECT_ROOT/build/logrotate/spinifex" "$STAGING/logrotate-spinifex"
tar czf /tmp/spinifex-local.tar.gz -C "$STAGING" .
echo "=== Cleaning stale state from previous installs ==="
# Stop any running services before modifying files
sudo systemctl stop spinifex.target 2>/dev/null || true
sudo systemctl reset-failed 2>/dev/null || true
# Remove stale files owned by the dev user in production paths.
# Previous dev-mode installs (admin init without sudo, start-dev.sh) leave
# files owned by tf-user that service users (spinifex-nats, etc.) can't read
# under systemd's ProtectSystem=strict sandboxing.
for dir in /var/lib/spinifex /var/log/spinifex /etc/spinifex; do
if [ -d "$dir" ]; then
# Remove PID files, stale logs, and the legacy ~/spinifex/config symlink
sudo find "$dir" -name '*.pid' -delete 2>/dev/null || true
sudo find "$dir" -name '*.log' -delete 2>/dev/null || true
sudo find "$dir" -name '*.log.*' -delete 2>/dev/null || true
fi
done
# Remove legacy data dir contents that conflict with production layout
if [ -d /var/lib/spinifex/config ]; then
sudo rm -rf /var/lib/spinifex/config
fi
echo "=== Running setup.sh (creates users, dirs, systemd units) ==="
sudo INSTALL_SPINIFEX_TARBALL=/tmp/spinifex-local.tar.gz bash "$PROJECT_ROOT/scripts/setup.sh"
rm -f /tmp/spinifex-local.tar.gz
echo "=== Setting up OVN ==="
sudo /usr/local/share/spinifex/setup-ovn.sh --management
echo "=== Initializing ==="
sudo spx admin init --force --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1
echo "=== Installing CA certificate ==="
sudo cp /etc/spinifex/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt
sudo update-ca-certificates
echo "=== Starting services ==="
sudo systemctl start spinifex.target
echo "=== Done ==="
echo "Services: sudo systemctl status spinifex.target"
echo "Logs: journalctl -u 'spinifex-*' -f"
echo "Test: AWS_PROFILE=spinifex aws ec2 describe-instances"
echo "Iterate: make deploy (rebuild + restart all)"
# Check if user needs to re-login for spinifex group membership
if ! id -Gn 2>/dev/null | grep -qw spinifex; then
echo ""
echo "NOTE: You were added to the 'spinifex' group but your current"
echo "session doesn't reflect it yet. Log out and back in, or run:"
echo " newgrp spinifex"
fi
+10 -18
View File
@@ -165,24 +165,16 @@ else
fi
echo ""
echo "🎉 Development environment setup complete!"
echo "Development environment setup complete!"
echo ""
echo "📋 Next steps:"
echo "Recommended: Use scripts/dev-install.sh for a production-parity setup"
echo " (systemd, privilege separation, production paths)."
echo ""
echo " 1. Setup OVN networking:"
echo " ./scripts/setup-ovn.sh --management"
echo " scripts/dev-install.sh # One-time: build, install, init, start via systemd"
echo " make deploy # Daily: rebuild + copy binary + restart all"
echo " make reinstall # Changed a .service file? Re-run full setup"
echo ""
echo " 2. Initialize Spinifex (creates ~/spinifex/ directories, config, certs, and installs CA):"
echo " sudo ./bin/spx admin init --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1"
echo ""
echo " 3. Start services:"
echo " ./scripts/start-dev.sh"
echo ""
echo "🔧 Development endpoints:"
echo " - Spinifex Gateway: https://localhost:$SPINIFEX_GATEWAY_PORT"
echo " - Predastore S3: https://localhost:$PREDASTORE_PORT"
echo " - NATS: nats://localhost:$NATS_PORT"
echo ""
echo "🧪 Test with AWS CLI:"
echo " export AWS_PROFILE=spinifex"
echo " aws ec2 describe-instances"
echo "Legacy workflow (deprecated, retained for pseudo-multinode CI only):"
echo " ./scripts/setup-ovn.sh --management"
echo " sudo ./bin/spx admin init --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1"
echo " ./scripts/start-dev.sh"
+5 -13
View File
@@ -565,21 +565,13 @@ else
echo " systemd override: already exists"
fi
# Create sudoers rule for network commands that always need root
# (ip tuntap, ip link set — NET_ADMIN operations)
# Sudoers rules for spinifex-daemon and spinifex-vpcd are managed by setup.sh
# (install_sudoers). Skip writing here to avoid conflicts.
SUDOERS_FILE="/etc/sudoers.d/spinifex-network"
if [ ! -f "$SUDOERS_FILE" ]; then
CURRENT_USER=$(whoami)
sudo tee "$SUDOERS_FILE" >/dev/null <<EOF
# Spinifex VPC networking: allow non-root daemon to manage tap devices and OVS
$CURRENT_USER ALL=(root) NOPASSWD: /sbin/ip, /usr/sbin/ip
$CURRENT_USER ALL=(root) NOPASSWD: /usr/bin/ovs-vsctl, /usr/bin/ovs-appctl
$CURRENT_USER ALL=(root) NOPASSWD: /usr/bin/ovn-nbctl, /usr/bin/ovn-sbctl
EOF
sudo chmod 0440 "$SUDOERS_FILE"
echo " sudoers rule: created ($SUDOERS_FILE)"
if [ -f "$SUDOERS_FILE" ]; then
echo " sudoers rule: already exists ($SUDOERS_FILE, managed by setup.sh)"
else
echo " sudoers rule: already exists"
echo " sudoers rule: not found — run setup.sh first, or install manually"
fi
# --- Step 9: Configure OVN log rotation ---
+102 -31
View File
@@ -89,27 +89,66 @@ detect_arch() {
info "Detected architecture: $MACHINE ($ARCH)"
}
# --- Detect service user ---
detect_service_user() {
# Use the real user — either the current user, or SUDO_USER if running via sudo
SPINIFEX_USER="${SUDO_USER:-$(whoami)}"
SPINIFEX_GROUP="$(id -gn "$SPINIFEX_USER")"
# --- Create per-service system users ---
create_service_users() {
SPINIFEX_GROUP="spinifex"
if [ "$SPINIFEX_USER" = "root" ]; then
fatal "Cannot determine service user. Do not run as root directly — use: curl ... | bash (as a normal user)"
# Create shared group
if ! getent group "$SPINIFEX_GROUP" > /dev/null 2>&1; then
$SUDO groupadd --system "$SPINIFEX_GROUP"
fi
info "Services will run as: $SPINIFEX_USER:$SPINIFEX_GROUP"
# Create per-service users with correct home directories
declare -A SERVICE_HOMES=(
[nats]="/var/lib/spinifex/nats"
[gw]="/var/lib/spinifex"
[daemon]="/var/lib/spinifex/spinifex"
[storage]="/var/lib/spinifex/predastore"
[viperblock]="/var/lib/spinifex/viperblock"
[vpcd]="/var/lib/spinifex"
[ui]="/var/lib/spinifex"
)
for svc in nats gw daemon storage viperblock vpcd ui; do
local user="spinifex-${svc}"
if ! id "$user" > /dev/null 2>&1; then
$SUDO useradd --system --no-create-home \
--home-dir "${SERVICE_HOMES[$svc]}" \
--gid "$SPINIFEX_GROUP" \
--shell /usr/sbin/nologin \
"$user"
fi
done
# Allow the service user to manage OVN/OVS and tap devices
$SUDO tee /etc/sudoers.d/spinifex-network > /dev/null << SUDOERS
# Spinifex VPC networking: allow service user to manage tap devices, OVS, and OVN
${SPINIFEX_USER} ALL=(root) NOPASSWD: /sbin/ip, /usr/sbin/ip
${SPINIFEX_USER} ALL=(root) NOPASSWD: /usr/bin/ovs-vsctl, /usr/bin/ovs-appctl
${SPINIFEX_USER} ALL=(root) NOPASSWD: /usr/bin/ovn-nbctl, /usr/bin/ovn-sbctl
# Add invoking user to spinifex group for admin CLI access
ADMIN_USER="${SUDO_USER:-$(whoami)}"
if [ "$ADMIN_USER" != "root" ]; then
$SUDO usermod -aG "$SPINIFEX_GROUP" "$ADMIN_USER"
fi
# KVM access for daemon
if getent group kvm > /dev/null 2>&1; then
$SUDO usermod -aG kvm spinifex-daemon
fi
info "Service users created (spinifex-{nats,gw,daemon,storage,viperblock,vpcd,ui})"
}
# --- Install scoped sudoers rules ---
install_sudoers() {
$SUDO tee /etc/sudoers.d/spinifex-network > /dev/null << 'SUDOERS'
# Spinifex daemon: tap devices, OVS bridge management, and DHCP for external IPs
spinifex-daemon ALL=(root) NOPASSWD: /sbin/ip, /usr/sbin/ip
spinifex-daemon ALL=(root) NOPASSWD: /usr/bin/ovs-vsctl, /usr/bin/ovs-appctl
spinifex-daemon ALL=(root) NOPASSWD: /usr/sbin/dhcpcd
# Spinifex VPC daemon: OVN and OVS read/write, OVN controller status check
spinifex-vpcd ALL=(root) NOPASSWD: /usr/bin/ovs-vsctl, /usr/bin/ovs-appctl
spinifex-vpcd ALL=(root) NOPASSWD: /usr/bin/ovn-nbctl, /usr/bin/ovn-sbctl
spinifex-vpcd ALL=(root) NOPASSWD: /usr/bin/systemctl is-active --quiet ovn-controller
SUDOERS
$SUDO chmod 0440 /etc/sudoers.d/spinifex-network
info "Sudoers rules installed for $SPINIFEX_USER"
$SUDO visudo -cf /etc/sudoers.d/spinifex-network || fatal "Invalid sudoers syntax in spinifex-network"
info "Scoped sudoers rules installed for spinifex-daemon and spinifex-vpcd"
}
# --- Install apt dependencies ---
@@ -241,12 +280,15 @@ install_files() {
create_directories() {
info "Creating directories..."
# Top-level directories (root-owned, group-readable by spinifex)
$SUDO mkdir -p /etc/spinifex
$SUDO chmod 0700 /etc/spinifex
$SUDO chown "$SPINIFEX_USER:$SPINIFEX_GROUP" /etc/spinifex
$SUDO chmod 0750 /etc/spinifex
$SUDO chown "root:$SPINIFEX_GROUP" /etc/spinifex
$SUDO mkdir -p /var/lib/spinifex
$SUDO chown "$SPINIFEX_USER:$SPINIFEX_GROUP" /var/lib/spinifex
# TODO: tighten to 0750 once awsgw gets its own data dir (currently writes awsgw.pid here)
$SUDO chmod 0770 /var/lib/spinifex
$SUDO chown "root:$SPINIFEX_GROUP" /var/lib/spinifex
# Symlink so services that expect BaseDir/config/ can find /etc/spinifex/
if [ ! -e /var/lib/spinifex/config ]; then
@@ -259,26 +301,57 @@ create_directories() {
fi
$SUDO mkdir -p /var/log/spinifex
$SUDO chown "$SPINIFEX_USER:$SPINIFEX_GROUP" /var/log/spinifex
$SUDO chmod 0775 /var/log/spinifex
$SUDO chown "root:$SPINIFEX_GROUP" /var/log/spinifex
$SUDO mkdir -p /run/spinifex
$SUDO chown "$SPINIFEX_USER:$SPINIFEX_GROUP" /run/spinifex
$SUDO chmod 0775 /run/spinifex
$SUDO chown "root:$SPINIFEX_GROUP" /run/spinifex
# Per-service config directories
$SUDO mkdir -p /etc/spinifex/nats
$SUDO chown "spinifex-nats:$SPINIFEX_GROUP" /etc/spinifex/nats
$SUDO chmod 0750 /etc/spinifex/nats
$SUDO mkdir -p /etc/spinifex/predastore
$SUDO chown "spinifex-storage:$SPINIFEX_GROUP" /etc/spinifex/predastore
$SUDO chmod 0750 /etc/spinifex/predastore
# Per-service data directories
$SUDO mkdir -p /var/lib/spinifex/nats
$SUDO chown "spinifex-nats:$SPINIFEX_GROUP" /var/lib/spinifex/nats
$SUDO chmod 0700 /var/lib/spinifex/nats
$SUDO mkdir -p /var/lib/spinifex/spinifex
$SUDO chown "spinifex-daemon:$SPINIFEX_GROUP" /var/lib/spinifex/spinifex
$SUDO chmod 0700 /var/lib/spinifex/spinifex
$SUDO mkdir -p /var/lib/spinifex/predastore
$SUDO chown "spinifex-storage:$SPINIFEX_GROUP" /var/lib/spinifex/predastore
$SUDO chmod 0700 /var/lib/spinifex/predastore
$SUDO mkdir -p /var/lib/spinifex/viperblock
$SUDO chown "$SPINIFEX_USER:$SPINIFEX_GROUP" /var/lib/spinifex/viperblock
$SUDO chown "spinifex-viperblock:$SPINIFEX_GROUP" /var/lib/spinifex/viperblock
$SUDO chmod 0700 /var/lib/spinifex/viperblock
$SUDO mkdir -p /var/lib/spinifex/vpcd
$SUDO chown "spinifex-vpcd:$SPINIFEX_GROUP" /var/lib/spinifex/vpcd
$SUDO chmod 0700 /var/lib/spinifex/vpcd
# Top-level config files (root-owned, group-readable)
# Generate environment file with install-specific values (e.g. arch-dependent paths)
$SUDO tee /etc/spinifex/systemd.env > /dev/null << EOF
# Generated by setup.sh — install-specific environment variables
SPINIFEX_VIPERBLOCK_PLUGIN_PATH=${PLUGINDIR}/nbdkit-viperblock-plugin.so
EOF
$SUDO chown "$SPINIFEX_USER:$SPINIFEX_GROUP" /etc/spinifex/systemd.env
$SUDO chown "spinifex-viperblock:$SPINIFEX_GROUP" /etc/spinifex/systemd.env
$SUDO chmod 0640 /etc/spinifex/systemd.env
info "Generated /etc/spinifex/systemd.env"
# Service helper scripts
# Service helper scripts (root-owned, group-executable by all service users)
if [ -d "$EXTRACT_DIR/scripts" ]; then
for script in "$EXTRACT_DIR"/scripts/*.sh; do
$SUDO install -o "$SPINIFEX_USER" -g "$SPINIFEX_GROUP" -m 0755 \
$SUDO install -o root -g "$SPINIFEX_GROUP" -m 0755 \
"$script" "/var/lib/spinifex/$(basename "$script")"
info " /var/lib/spinifex/$(basename "$script")"
done
@@ -294,15 +367,12 @@ install_systemd() {
fi
for unit in "$EXTRACT_DIR"/systemd/*; do
# Substitute User= and Group= with the detected service user
sed "s/^User=spinifex$/User=$SPINIFEX_USER/;s/^Group=spinifex$/Group=$SPINIFEX_GROUP/" \
"$unit" | $SUDO tee "/etc/systemd/system/$(basename "$unit")" > /dev/null
$SUDO chmod 0644 "/etc/systemd/system/$(basename "$unit")"
$SUDO install -m 0644 "$unit" "/etc/systemd/system/$(basename "$unit")"
info " /etc/systemd/system/$(basename "$unit")"
done
$SUDO systemctl daemon-reload
info "Systemd units installed (running as $SPINIFEX_USER:$SPINIFEX_GROUP)"
info "Systemd units installed (per-service users)"
}
# --- Install logrotate ---
@@ -343,7 +413,7 @@ print_summary() {
echo ""
echo " Version: $INSTALLED_VERSION"
echo " Architecture: $ARCH"
echo " Service user: $SPINIFEX_USER"
echo " Service users: spinifex-{nats,gw,daemon,storage,viperblock,vpcd,ui}"
echo " Binary: /usr/local/bin/spx"
echo " Config: /etc/spinifex/"
echo " Data: /var/lib/spinifex/"
@@ -384,7 +454,8 @@ main() {
handle_upgrade
install_apt_deps
install_aws_cli
detect_service_user
create_service_users
install_sudoers
download_spinifex
install_files
create_directories
+7
View File
@@ -11,6 +11,11 @@
set -euo pipefail
echo "WARNING: start-dev.sh is deprecated for local development." >&2
echo " Use 'make deploy' for daily iteration or 'scripts/dev-install.sh' for first-time setup." >&2
echo " start-dev.sh is retained only for pseudo-multinode CI (e2e_pseudo_multi)." >&2
echo "" >&2
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MULGA_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
@@ -33,6 +38,8 @@ echo "Using configuration directory: $CONFIG_DIR"
LOGS_DIR="$DATA_DIR/logs"
WAL_DIR="$DATA_DIR/spinifex"
echo "⚠️ Dev mode: all services run as $(whoami) without privilege separation"
echo ""
echo "🚀 Starting Spinifex development environment..."
echo "Project root: $PROJECT_ROOT"
echo "Data directory: $DATA_DIR"
+90 -4
View File
@@ -40,6 +40,7 @@ type ConfigSettings struct {
Region string
NatsToken string
DataDir string
LogDir string
ConfigDir string
// Add more fields as needed
@@ -212,6 +213,7 @@ func CreateServiceDirectories(spxRoot string) {
filepath.Join(spxRoot, "nats"),
filepath.Join(spxRoot, "predastore"),
filepath.Join(spxRoot, "viperblock"),
filepath.Join(spxRoot, "vpcd"),
filepath.Join(spxRoot, "spinifex"),
}
@@ -241,18 +243,30 @@ func FileExists(path string) bool {
func ChownRecursive(path, username string) {
u, err := user.Lookup(username)
if err != nil {
slog.Warn("ChownRecursive: user lookup failed, skipping", "user", username, "path", path, "err", err)
return
}
uid, err := strconv.Atoi(u.Uid)
if err != nil {
slog.Warn("ChownRecursive: invalid UID, skipping", "user", username, "uid", u.Uid, "err", err)
return
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
slog.Warn("ChownRecursive: invalid GID, skipping", "user", username, "gid", u.Gid, "err", err)
return
}
uid, _ := strconv.Atoi(u.Uid)
gid, _ := strconv.Atoi(u.Gid)
// Use os.Root to scope filesystem operations and avoid symlink TOCTOU races.
// Falls back to direct chown if os.Root is not available.
root, rootErr := os.OpenRoot(path)
if rootErr != nil {
// Fallback: direct chown on the top-level path only
// Fallback: direct chown on the top-level path only — subdirectory contents
// will retain their original ownership.
slog.Warn("ChownRecursive: OpenRoot not available, only top-level directory ownership changed",
"path", path, "err", rootErr)
if chownErr := os.Chown(path, uid, gid); chownErr != nil { // #nosec G122
slog.Debug("chown failed", "path", path, "err", chownErr)
slog.Warn("chown failed", "path", path, "err", chownErr)
}
return
}
@@ -271,6 +285,78 @@ func ChownRecursive(path, username string) {
})
}
// SetServiceOwnership sets per-service ownership on data/config directories
// and shared config files to root:spinifex with correct modes.
// Keep in sync with setup.sh create_directories() and plan doc section 2.
func SetServiceOwnership() {
grp, err := user.LookupGroup("spinifex")
if err != nil {
slog.Error("SetServiceOwnership: spinifex group not found, skipping all ownership changes", "err", err)
fmt.Fprintln(os.Stderr, "WARNING: spinifex group not found — service ownership not set. Run setup.sh first or create the group manually.")
return
}
gid, err := strconv.Atoi(grp.Gid)
if err != nil {
slog.Error("SetServiceOwnership: invalid spinifex group GID", "gid", grp.Gid, "err", err)
fmt.Fprintln(os.Stderr, "WARNING: invalid spinifex group GID — service ownership not set.")
return
}
// Per-service directory trees
for path, u := range map[string]string{
"/etc/spinifex/nats": "spinifex-nats",
"/var/lib/spinifex/nats": "spinifex-nats",
"/etc/spinifex/predastore": "spinifex-storage",
"/var/lib/spinifex/predastore": "spinifex-storage",
"/var/lib/spinifex/spinifex": "spinifex-daemon",
"/var/lib/spinifex/viperblock": "spinifex-viperblock",
"/var/lib/spinifex/vpcd": "spinifex-vpcd",
} {
if _, err := os.Stat(path); err != nil {
continue
}
ChownRecursive(path, u)
}
// Shared data directories — root:spinifex 0770 so daemon + admin CLI can write
for _, dir := range []string{
"/var/lib/spinifex/images",
"/var/lib/spinifex/amis",
"/var/lib/spinifex/volumes",
"/var/lib/spinifex/state",
} {
if _, err := os.Stat(dir); err != nil {
continue
}
if err := os.Lchown(dir, 0, gid); err != nil {
slog.Warn("SetServiceOwnership: chown failed", "path", dir, "err", err)
}
if err := os.Chmod(dir, 0770); err != nil { //nolint:gosec // directories need group-write for daemon + admin CLI
slog.Warn("SetServiceOwnership: chmod failed", "path", dir, "err", err)
}
}
// Shared config files — root:spinifex, ca.key stays root:root 0600
for path, mode := range map[string]os.FileMode{
"/etc/spinifex/spinifex.toml": 0640,
"/etc/spinifex/master.key": 0640,
"/etc/spinifex/bootstrap.json": 0660, // group-write so awsgw can consume and truncate
"/etc/spinifex/server.pem": 0644,
"/etc/spinifex/server.key": 0640,
"/etc/spinifex/ca.pem": 0644,
} {
if _, err := os.Stat(path); err != nil {
continue
}
if err := os.Lchown(path, 0, gid); err != nil {
slog.Warn("SetServiceOwnership: chown failed", "path", path, "err", err)
}
if err := os.Chmod(path, mode); err != nil {
slog.Warn("SetServiceOwnership: chmod failed", "path", path, "err", err)
}
}
}
// updateAWSINIFile updates or creates an AWS INI file section with given key-value pairs
func UpdateAWSINIFile(path, section string, values map[string]string) error {
var cfg *ini.File
@@ -601,6 +601,14 @@ func launchService(cfg *Config) (err error) {
slog.Info("NBDKit started successfully and is running")
}
// NBDKit creates the socket with its own umask (typically 0755).
// The daemon (different user, same group) needs write access to connect.
if nbdSocket != "" {
if err := os.Chmod(nbdSocket, 0770); err != nil { //nolint:gosec // socket needs group-write for cross-service access
slog.Warn("Failed to chmod NBD socket", "socket", nbdSocket, "err", err)
}
}
ebsResponse.Mounted = true
ebsResponse.URI = nbdURI
+19 -28
View File
@@ -11,9 +11,6 @@ set -euo pipefail
# Ensure Go is on PATH (SSH non-interactive shells don't source .bashrc)
export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"
# Ensure we are in the spinifex project root
cd "$(dirname "$0")/../.."
# ==========================================================================
# Argument parsing
# ==========================================================================
@@ -36,14 +33,12 @@ NATS_MONITOR_PORT=8222
PREDASTORE_PORT=8443
AWSGW_PORT=9999
SSH_KEY_PATH="$HOME/.ssh/tf-user-ap-southeast-2"
SPINIFEX_DATA_DIR="$HOME/spinifex"
SPINIFEX_CONFIG="$SPINIFEX_DATA_DIR/config/spinifex.toml"
SPINIFEX_BIN="./bin/spx"
SPINIFEX_BIN="spx"
# Use Spinifex profile for AWS CLI
export AWS_PROFILE=spinifex
# Trust Spinifex CA for AWS CLI v2 (bundles its own Python/certifi, ignores system CA store)
export AWS_CA_BUNDLE="$SPINIFEX_DATA_DIR/config/ca.pem"
export AWS_CA_BUNDLE="/etc/spinifex/ca.pem"
# Track test results
TESTS_PASSED=0
@@ -106,19 +101,15 @@ dump_all_node_logs() {
local ip="${NODE_IPS[$i]}"
echo ""
echo "=== Node $((i+1)) ($ip) ==="
if [ "$ip" = "$LOCAL_IP" ]; then
for f in "$SPINIFEX_DATA_DIR/logs/"*.log; do
[ -f "$f" ] || continue
echo "--- $(basename "$f") (last 50 lines) ---"
tail -50 "$f" 2>/dev/null || echo "(not found)"
done
else
peer_ssh "$ip" 'for f in ~/spinifex/logs/*.log; do
[ -f "$f" ] || continue
echo "--- $(basename "$f") (last 50 lines) ---"
tail -50 "$f" 2>/dev/null || echo "(not found)"
done' || echo "(node unreachable)"
fi
for svc in spinifex-nats spinifex-predastore spinifex-viperblock \
spinifex-daemon spinifex-awsgw spinifex-vpcd; do
echo "--- ${svc} (last 50 lines) ---"
if [ "$ip" = "$LOCAL_IP" ]; then
sudo journalctl -u "$svc" --no-pager -n 50 2>/dev/null || echo "(not found)"
else
peer_ssh "$ip" "sudo journalctl -u $svc --no-pager -n 50" 2>/dev/null || echo "(node unreachable)"
fi
done
done
echo ""
echo "=========================================="
@@ -403,7 +394,7 @@ pass_test "Daemon readiness"
# Spinifex CLI: get nodes
echo "Checking spx get nodes..."
GET_NODES_OUTPUT=$($SPINIFEX_BIN get nodes --config "$SPINIFEX_CONFIG" --timeout 5s 2>/dev/null)
GET_NODES_OUTPUT=$($SPINIFEX_BIN get nodes --timeout 5s 2>/dev/null)
echo "$GET_NODES_OUTPUT"
READY_COUNT=$(echo "$GET_NODES_OUTPUT" | grep -c "Ready" || true)
if [ "$READY_COUNT" -ge 3 ]; then
@@ -415,7 +406,7 @@ fi
# Spinifex CLI: get vms (should be empty)
echo "Checking spx get vms (empty)..."
GET_VMS_OUTPUT=$($SPINIFEX_BIN get vms --config "$SPINIFEX_CONFIG" --timeout 5s 2>/dev/null)
GET_VMS_OUTPUT=$($SPINIFEX_BIN get vms --timeout 5s 2>/dev/null)
echo "$GET_VMS_OUTPUT"
pass_test "spx get vms (empty)"
@@ -500,7 +491,7 @@ fi
# Verify spx get vms shows all instances
echo ""
echo "Verifying spx get vms..."
GET_VMS_OUTPUT=$($SPINIFEX_BIN get vms --config "$SPINIFEX_CONFIG" --timeout 5s 2>/dev/null)
GET_VMS_OUTPUT=$($SPINIFEX_BIN get vms --timeout 5s 2>/dev/null)
echo "$GET_VMS_OUTPUT"
for instance_id in "${INSTANCE_IDS[@]}"; do
if ! echo "$GET_VMS_OUTPUT" | grep -q "$instance_id"; then
@@ -787,8 +778,8 @@ echo "Stopping services on node2 ($NODE2_IP) to simulate node failure..."
# Stop services on node2 only — SPINIFEX_FORCE_LOCAL_STOP prevents coordinated
# cluster shutdown which would kill all nodes via NATS
peer_ssh "$NODE2_IP" "cd ~/Development/mulga/spinifex && SPINIFEX_FORCE_LOCAL_STOP=1 ./scripts/stop-dev.sh" || {
echo " WARNING: stop-dev.sh returned non-zero (may be expected)"
peer_ssh "$NODE2_IP" "sudo systemctl stop spinifex.target" || {
echo " WARNING: systemctl stop returned non-zero (may be expected)"
}
# Wait for NATS cluster to detect the failure and reform
@@ -848,7 +839,7 @@ echo "Phase 9: Node Recovery"
echo "========================================"
echo "Restarting services on node2 ($NODE2_IP)..."
peer_ssh "$NODE2_IP" "cd ~/Development/mulga/spinifex && ./scripts/start-dev.sh" || {
peer_ssh "$NODE2_IP" "sudo systemctl start spinifex.target" || {
echo " ERROR: Failed to restart services on node2"
fail_test "Node2 restart"
}
@@ -899,7 +890,7 @@ fi
# Verify spx get nodes shows 3 Ready again
echo " Checking spx get nodes after recovery..."
GET_NODES_RECOVER=$($SPINIFEX_BIN get nodes --config "$SPINIFEX_CONFIG" --timeout 10s 2>/dev/null || echo "")
GET_NODES_RECOVER=$($SPINIFEX_BIN get nodes --timeout 10s 2>/dev/null || echo "")
echo "$GET_NODES_RECOVER"
READY_RECOVER=$(echo "$GET_NODES_RECOVER" | grep -c "Ready" || true)
if [ "$READY_RECOVER" -ge 3 ]; then
@@ -1252,7 +1243,7 @@ else
echo "Step 4: Validating spread placement across nodes..."
# Use spx get vms to check node assignment
SPX_VMS=$($SPINIFEX_BIN get vms --config "$SPINIFEX_CONFIG" --timeout 5s 2>/dev/null)
SPX_VMS=$($SPINIFEX_BIN get vms --timeout 5s 2>/dev/null)
echo "$SPX_VMS"
SPREAD_NODES=()