package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "regexp" "strings" ) // Secrets engine (DESIGN.md §5). The agent renders local templates that embed // OpenBao secrets, e.g.: // // # /etc/theta/templates/db.env.tpl // DB_USER="{{ bao "secret/data/nodes/node-42/db#username" }}" // DB_PASS="{{ bao "secret/data/nodes/node-42/db#password" }}" // // The agent parses the `{{ bao "path#key" }}` placeholders, fetches the secret // values from the SSO (which holds the OpenBao access), renders each template to // its target atomically (0600), and runs the configured reload. The agent never // holds a Vault token. var baoRe = regexp.MustCompile(`\{\{\s*bao\s+"([^"]+)"\s*\}\}`) // renderSecrets renders every configured secret template. Called on a // `render_secrets` command (signed) and on boot. func renderSecrets(cfg *Config, exec Executor) error { if len(cfg.Secrets) == 0 { return nil } // Collect the unique secret paths referenced across all templates. pathSet := map[string]bool{} var paths []string for _, t := range cfg.Secrets { content, err := os.ReadFile(t.Template) if err != nil { log.Printf("Secrets: cannot read template %s: %v", t.Template, err) continue } for _, m := range baoRe.FindAllStringSubmatch(string(content), -1) { path := refPath(m[1]) if !pathSet[path] { pathSet[path] = true paths = append(paths, path) } } } if len(paths) == 0 { return nil } secrets, err := fetchSecrets(cfg, paths) if err != nil { return err } for _, t := range cfg.Secrets { if err := renderOne(t, secrets, exec); err != nil { log.Printf("Secrets: render %s failed: %v", t.Template, err) } } return nil } func renderOne(t SecretTarget, secrets map[string]map[string]interface{}, exec Executor) error { content, err := os.ReadFile(t.Template) if err != nil { return err } out := baoRe.ReplaceAllStringFunc(string(content), func(match string) string { ref := baoRe.FindStringSubmatch(match)[1] path, key := refPath(ref), refKey(ref) if v, ok := secrets[path][key]; ok { return fmt.Sprintf("%v", v) } return "" }) // Atomic write: temp file in the target's directory, then rename. 0600 — the // rendered file holds secrets. tmp, err := os.CreateTemp(filepath.Dir(t.Target), ".theta-secret-*") if err != nil { return err } tmpName := tmp.Name() if _, err := io.WriteString(tmp, out); err != nil { tmp.Close() os.Remove(tmpName) return err } tmp.Close() os.Chmod(tmpName, 0600) if err := os.Rename(tmpName, t.Target); err != nil { os.Remove(tmpName) return err } if t.Reload != "" { if _, err := exec.Execute("sh", "-c", t.Reload); err != nil { log.Printf("Secrets: reload %q failed: %v", t.Reload, err) } } return nil } // fetchSecrets asks the SSO for the given node-scoped secret paths. func fetchSecrets(cfg *Config, paths []string) (map[string]map[string]interface{}, error) { base := strings.Replace(cfg.ServerURL, "wss://", "https://", 1) base = strings.Replace(base, "ws://", "http://", 1) url := strings.TrimRight(base, "/") + "/api/v1/agent/secrets" body, _ := json.Marshal(map[string]interface{}{"paths": paths}) req, err := http.NewRequest("POST", url, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+cfg.Credential()) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("secrets fetch failed: %s", resp.Status) } var out struct { Secrets map[string]map[string]interface{} `json:"secrets"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, err } return out.Secrets, nil } // refPath returns the secret path from a `path#key` reference. func refPath(ref string) string { path := ref if i := strings.Index(ref, "#"); i >= 0 { path = ref[:i] } if strings.HasPrefix(path, "resource/") { return "secret/data/resources/" + strings.TrimPrefix(path, "resource/") + "/conf" } if strings.HasPrefix(path, "resources/") { return "secret/data/resources/" + strings.TrimPrefix(path, "resources/") + "/conf" } if !strings.HasPrefix(path, "secret/") { return "secret/data/resources/" + path + "/conf" } return path } // refKey returns the key from a `path#key` reference. func refKey(ref string) string { if i := strings.Index(ref, "#"); i >= 0 { return ref[i+1:] } return "" }