mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
fix: move bootstrap.json to different folder to tighten perms (#118)
This commit is contained in:
@@ -23,7 +23,7 @@ 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/awsgw /var/log/spinifex /etc/spinifex/bootstrap.json /tmp
|
||||
ReadWritePaths=/var/lib/spinifex/awsgw /var/log/spinifex /tmp
|
||||
|
||||
# Hardening
|
||||
ProtectHome=yes
|
||||
|
||||
+20
-12
@@ -785,14 +785,15 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(os.Stderr, "Error generating IAM master key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
bootstrapResult, err := writeBootstrapFiles(configDir, masterKey, accessKey, secretKey, accountID)
|
||||
bootstrapDir := filepath.Join(spxRoot, "awsgw")
|
||||
bootstrapResult, err := writeBootstrapFiles(configDir, bootstrapDir, masterKey, accessKey, secretKey, accountID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing bootstrap files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("\n🔐 Generated IAM master key")
|
||||
fmt.Printf(" Master key: %s\n", filepath.Join(configDir, "master.key"))
|
||||
fmt.Printf(" Bootstrap: %s\n", filepath.Join(configDir, "bootstrap.json"))
|
||||
fmt.Printf(" Bootstrap: %s\n", filepath.Join(bootstrapDir, "bootstrap.json"))
|
||||
|
||||
fmt.Printf("\n🔑 Generated admin credentials (save these — they won't be shown again):\n")
|
||||
fmt.Printf(" Access Key: %s\n", bootstrapResult.AdminAccessKey)
|
||||
@@ -1048,13 +1049,14 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
|
||||
fmt.Fprintf(os.Stderr, "❌ Error generating IAM master key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
bootstrapResult, err := writeBootstrapFiles(configDir, masterKey, accessKey, secretKey, accountID)
|
||||
bootstrapDir := filepath.Join(spxRoot, "awsgw")
|
||||
bootstrapResult, err := writeBootstrapFiles(configDir, bootstrapDir, masterKey, accessKey, secretKey, accountID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error writing bootstrap files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("\n🔐 Generated IAM master key")
|
||||
fmt.Printf(" Bootstrap: %s\n", filepath.Join(configDir, "bootstrap.json"))
|
||||
fmt.Printf(" Bootstrap: %s\n", filepath.Join(bootstrapDir, "bootstrap.json"))
|
||||
|
||||
// Read CA cert/key for distribution to joining nodes
|
||||
caCertData, err := os.ReadFile(filepath.Join(configDir, "ca.pem"))
|
||||
@@ -1495,12 +1497,13 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error decoding master key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := writeBootstrapFilesWithAdmin(configDir, masterKeyBytes, creds.AccessKey, creds.SecretKey, creds.AccountID, creds.AdminAccessKey, creds.AdminSecretKey); err != nil {
|
||||
bootstrapDir := filepath.Join(dataDir, "awsgw")
|
||||
if err := writeBootstrapFilesWithAdmin(configDir, bootstrapDir, masterKeyBytes, creds.AccessKey, creds.SecretKey, creds.AccountID, creds.AdminAccessKey, creds.AdminSecretKey); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error writing bootstrap files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("✅ IAM master key received from leader")
|
||||
fmt.Printf("✅ Bootstrap file written: %s\n", filepath.Join(configDir, "bootstrap.json"))
|
||||
fmt.Printf("✅ Bootstrap file written: %s\n", filepath.Join(bootstrapDir, "bootstrap.json"))
|
||||
|
||||
// Generate server cert signed by CA with this node's bind IP
|
||||
if err := admin.GenerateServerCertOnly(configDir, bindIP); err != nil {
|
||||
@@ -1890,8 +1893,9 @@ type writeBootstrapResult struct {
|
||||
}
|
||||
|
||||
// writeBootstrapFiles generates new admin credentials and writes the bootstrap
|
||||
// files (master.key + bootstrap.json). Used by init flows (single and multi-node).
|
||||
func writeBootstrapFiles(configDir string, masterKey []byte, accessKey, secretKey, accountID string) (*writeBootstrapResult, error) {
|
||||
// files (master.key to configDir, bootstrap.json to bootstrapDir).
|
||||
// Used by init flows (single and multi-node).
|
||||
func writeBootstrapFiles(configDir, bootstrapDir string, masterKey []byte, accessKey, secretKey, accountID string) (*writeBootstrapResult, error) {
|
||||
adminAccessKey, err := admin.GenerateAWSAccessKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate admin access key: %w", err)
|
||||
@@ -1900,7 +1904,7 @@ func writeBootstrapFiles(configDir string, masterKey []byte, accessKey, secretKe
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate admin secret key: %w", err)
|
||||
}
|
||||
if err := writeBootstrapFilesWithAdmin(configDir, masterKey, accessKey, secretKey, accountID, adminAccessKey, adminSecretKey); err != nil {
|
||||
if err := writeBootstrapFilesWithAdmin(configDir, bootstrapDir, masterKey, accessKey, secretKey, accountID, adminAccessKey, adminSecretKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &writeBootstrapResult{
|
||||
@@ -1910,8 +1914,12 @@ func writeBootstrapFiles(configDir string, masterKey []byte, accessKey, secretKe
|
||||
}
|
||||
|
||||
// writeBootstrapFilesWithAdmin writes the bootstrap files using the provided
|
||||
// admin credentials. Used by join flows where admin creds come from the leader.
|
||||
func writeBootstrapFilesWithAdmin(configDir string, masterKey []byte, accessKey, secretKey, accountID, adminAccessKey, adminSecretKey string) error {
|
||||
// admin credentials. master.key goes to configDir, bootstrap.json goes to
|
||||
// bootstrapDir (the awsgw data directory) so it stays outside /etc/spinifex.
|
||||
func writeBootstrapFilesWithAdmin(configDir, bootstrapDir string, masterKey []byte, accessKey, secretKey, accountID, adminAccessKey, adminSecretKey string) error {
|
||||
if err := os.MkdirAll(bootstrapDir, 0700); err != nil {
|
||||
return fmt.Errorf("create bootstrap directory %s: %w", bootstrapDir, err)
|
||||
}
|
||||
if err := handlers_iam.SaveMasterKey(filepath.Join(configDir, "master.key"), masterKey); err != nil {
|
||||
return fmt.Errorf("saving master key: %w", err)
|
||||
}
|
||||
@@ -1939,7 +1947,7 @@ func writeBootstrapFilesWithAdmin(configDir string, masterKey []byte, accessKey,
|
||||
},
|
||||
}
|
||||
|
||||
return handlers_iam.SaveBootstrapData(filepath.Join(configDir, "bootstrap.json"), bd)
|
||||
return handlers_iam.SaveBootstrapData(filepath.Join(bootstrapDir, "bootstrap.json"), bd)
|
||||
}
|
||||
|
||||
// detectedWanBridge returns the OVS bridge name that OVN bridge-mappings should
|
||||
|
||||
+7
-12
@@ -338,20 +338,15 @@ func SetServiceOwnership() {
|
||||
}
|
||||
}
|
||||
|
||||
// /etc/spinifex needs group-write so awsgw (spinifex-gw) can delete
|
||||
// bootstrap.json after consuming it on first boot.
|
||||
if err := os.Chmod("/etc/spinifex", 0770); err != nil { //nolint:gosec // directory needs group-write for bootstrap.json deletion
|
||||
slog.Warn("SetServiceOwnership: chmod /etc/spinifex failed", "err", err)
|
||||
}
|
||||
|
||||
// Shared config files — root:spinifex, ca.key stays root:root 0600
|
||||
// bootstrap.json lives in the awsgw data dir (not /etc/spinifex),
|
||||
// so /etc/spinifex stays at 0750 (no group-write needed).
|
||||
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,
|
||||
"/etc/spinifex/spinifex.toml": 0640,
|
||||
"/etc/spinifex/master.key": 0640,
|
||||
"/etc/spinifex/server.pem": 0644,
|
||||
"/etc/spinifex/server.key": 0640,
|
||||
"/etc/spinifex/ca.pem": 0644,
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
continue
|
||||
|
||||
@@ -108,11 +108,9 @@ func launchService(config *config.ClusterConfig) error {
|
||||
}
|
||||
|
||||
// First boot: consume bootstrap.json → seed IAM users into NATS KV → delete file.
|
||||
// Resolve symlinks so the path matches systemd ReadWritePaths for deletion.
|
||||
bootstrapPath := filepath.Join(nodeConfig.BaseDir, "config", "bootstrap.json")
|
||||
if resolved, err := filepath.EvalSymlinks(bootstrapPath); err == nil {
|
||||
bootstrapPath = resolved
|
||||
}
|
||||
// Check data directory first (production: /var/lib/spinifex/awsgw/), then
|
||||
// awsgw subdir (dev: ~/spinifex/awsgw/), then legacy config dir.
|
||||
bootstrapPath := findBootstrapFile(nodeConfig.BaseDir)
|
||||
data, err := handlers_iam.LoadBootstrapData(bootstrapPath)
|
||||
switch {
|
||||
case err == nil:
|
||||
@@ -228,3 +226,21 @@ func initIAMService(natsConn *nats.Conn, masterKey []byte, clusterSize int) (*ha
|
||||
retryDelay = min(retryDelay*2, 10*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// findBootstrapFile returns the path to bootstrap.json, checking the data
|
||||
// directory first (production), then the awsgw subdir (dev mode), then the
|
||||
// legacy config dir. Returns the first path that exists, or the primary
|
||||
// location if none exist (so the caller gets a clean "not found" error).
|
||||
func findBootstrapFile(baseDir string) string {
|
||||
candidates := []string{
|
||||
filepath.Join(baseDir, "bootstrap.json"),
|
||||
filepath.Join(baseDir, "awsgw", "bootstrap.json"),
|
||||
filepath.Join(baseDir, "config", "bootstrap.json"),
|
||||
}
|
||||
for _, p := range candidates {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user