maidn-cli/internal/openbao/app_secret.go

139 lines
4.4 KiB
Go

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
}