package openbao import ( "encoding/json" "errors" "os" "regexp" "strings" ) var managedSecretPart = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`) // ManagedSecretPath limits application-secret operations to the declared app // and shared OpenBao namespaces. func ManagedSecretPath(shared bool, owner, name string) (string, error) { if !managedSecretPart.MatchString(owner) || !managedSecretPart.MatchString(name) { return "", errors.New("application, shared group, and secret names must be lowercase DNS labels") } if shared { return "shared/" + owner + "/" + name, nil } return "apps/" + owner + "/" + name, nil } func managedSecretScope(shared bool, owner string) (string, error) { if !managedSecretPart.MatchString(owner) { return "", errors.New("application and shared group names must be lowercase DNS labels") } if shared { return "shared/" + owner, nil } return "apps/" + owner, nil } // StoreManagedSecret keeps the token and value on stdin all the way to OpenBao. func StoreManagedSecret(kubeconfig, tokenPath, path string, value []byte) error { if _, err := managedSecretPathParts(path); err != nil { return err } if len(value) == 0 { return errors.New("secret value must not be empty") } token, err := readRestrictedToken(tokenPath) if err != nil { return err } return writeSecret(kubeconfig, token, path, map[string]string{"value": string(value)}) } // ListManagedSecrets returns only secret names from KV metadata. func ListManagedSecrets(kubeconfig, tokenPath string, shared bool, owner string) ([]string, error) { scope, err := managedSecretScope(shared, owner) if err != nil { return nil, err } token, err := readRestrictedToken(tokenPath) if err != nil { return nil, err } output, err := execInPod(kubeconfig, []byte(token+"\n"), "sh", "-ec", "read -r token\nexport BAO_TOKEN=\"$token\"\nbao kv list -format=json secret/metadata/"+scope) if err != nil { if strings.Contains(strings.ToLower(string(output)), "no value found") { return nil, nil } return nil, errors.New("list OpenBao secret metadata") } var response struct { Data struct { Keys []string `json:"keys"` } `json:"data"` } if err := json.Unmarshal(output, &response); err != nil { return nil, errors.New("parse OpenBao secret metadata") } return response.Data.Keys, nil } // ManagedSecretStatus checks KV metadata without reading the secret value. func ManagedSecretStatus(kubeconfig, tokenPath, path string) (bool, error) { if _, err := managedSecretPathParts(path); err != nil { return false, err } token, err := readRestrictedToken(tokenPath) if err != nil { return false, err } output, err := execInPod(kubeconfig, []byte(token+"\n"), "sh", "-ec", "read -r token\nexport BAO_TOKEN=\"$token\"\nbao kv metadata get -format=json secret/"+path) if err != nil { if strings.Contains(strings.ToLower(string(output)), "no value found") { return false, nil } return false, errors.New("read OpenBao secret metadata") } var response struct { Data json.RawMessage `json:"data"` } if err := json.Unmarshal(output, &response); err != nil || len(response.Data) == 0 { return false, errors.New("parse OpenBao secret metadata") } return true, nil } func DeleteManagedSecret(kubeconfig, tokenPath, path string) error { if _, err := managedSecretPathParts(path); err != nil { return err } token, err := readRestrictedToken(tokenPath) if err != nil { return err } if _, err := execInPodMutation(kubeconfig, []byte(token+"\n"), "sh", "-ec", "read -r token\nexport BAO_TOKEN=\"$token\"\nbao kv metadata delete secret/"+path+" >/dev/null"); err != nil { return errors.New("delete OpenBao secret") } return nil } func readRestrictedToken(path string) (string, error) { if path == "" { return "", errors.New("--token-file is required") } contents, err := os.ReadFile(path) if err != nil { return "", errors.New("read OpenBao token file") } token := strings.TrimSpace(string(contents)) if token == "" || strings.ContainsAny(token, " \t\r\n") { return "", errors.New("OpenBao token file must contain one token") } return token, nil } func managedSecretPathParts(path string) ([]string, error) { parts := strings.Split(path, "/") if len(parts) != 3 || (parts[0] != "apps" && parts[0] != "shared") || !managedSecretPart.MatchString(parts[1]) || !managedSecretPart.MatchString(parts[2]) { return nil, errors.New("invalid managed OpenBao secret path") } return parts, nil }