fix: move bootstrap.json to different folder to tighten perms (#118)

This commit is contained in:
Julian Sommer
2026-04-08 16:12:25 +10:00
committed by GitHub
parent 9a7e914d28
commit ccb6ed287d
4 changed files with 49 additions and 30 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+21 -5
View File
@@ -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]
}