F8: persist controller.yaml as 0600 (holds infra secrets)
The Hub config-apply handler wrote controller.yaml 0644; it holds cf_api_token, cf_tunnel_token and hub api_key in plaintext. New writeConfig0600 helper writes 0600 atomically (tmp+rename, bind-mount fallback) and chmods to enforce 0600 even when the file pre-existed 0644 (os.WriteFile doesn't chmod existing files). Test asserts mode 0600 (Linux; skipped on Windows). Setup path already used 0600.
This commit is contained in:
@@ -1011,26 +1011,15 @@ func (r *Router) configApply(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Write config: try atomic rename first, fall back to direct write
|
||||
// (os.Rename fails on Docker bind mounts with "device or resource busy")
|
||||
tmpPath := r.configPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, body, 0644); err != nil {
|
||||
r.logger.Printf("[ERROR] [api] Config apply: failed to write temp file: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: "failed to write config"})
|
||||
// Write config 0600: it holds infra credentials (cf_api_token, cf_tunnel_token, hub api_key) in
|
||||
// plaintext (F8), so it must not be world-readable. writeConfig0600 enforces the mode even when the
|
||||
// target file already existed with looser perms (os.WriteFile does not chmod an existing file).
|
||||
if err := writeConfig0600(r.configPath, body); err != nil {
|
||||
r.logger.Printf("[ERROR] [api] Config apply: failed to write config: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: "failed to apply config"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, r.configPath); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
// Rename failed (likely Docker bind mount) — write directly
|
||||
if err := os.WriteFile(r.configPath, body, 0644); err != nil {
|
||||
r.logger.Printf("[ERROR] [api] Config apply: failed to write config: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: "failed to apply config"})
|
||||
return
|
||||
}
|
||||
r.logger.Printf("[INFO] [api] Config apply: rename failed, wrote directly (bind mount)")
|
||||
}
|
||||
|
||||
r.logger.Printf("[INFO] [api] Config applied from Hub (%d bytes), restart needed to take effect", len(body))
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Config applied. Restart controller to apply changes."})
|
||||
|
||||
@@ -1040,6 +1029,27 @@ func (r *Router) configApply(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// writeConfig0600 writes config bytes to path with mode 0600, atomically when possible (tmp+rename),
|
||||
// falling back to a direct write for Docker bind mounts (where os.Rename returns EBUSY). It ALWAYS
|
||||
// enforces 0600 on the final file — even if it already existed with looser perms — because controller.yaml
|
||||
// holds infra credentials in plaintext (F8); os.WriteFile only applies the mode when creating a new file.
|
||||
func writeConfig0600(path string, body []byte) error {
|
||||
tmpPath := path + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, body, 0600); err != nil {
|
||||
return fmt.Errorf("writing temp config: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
if err := os.WriteFile(path, body, 0600); err != nil { // bind-mount fallback
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(path, 0600); err != nil {
|
||||
return fmt.Errorf("chmod config 0600: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) configHash(w http.ResponseWriter, _ *http.Request) {
|
||||
hash, err := config.FileHash(r.configPath)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user