139 lines
4.4 KiB
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
|
|
}
|