feat: automate restricted app-secret identities
This commit is contained in:
parent
d97bc041f9
commit
20381ebddf
|
|
@ -1,20 +1,24 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/bootstrap"
|
||||||
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
||||||
"github.com/Pingu-Studio/MaidnCLI/internal/openbao"
|
"github.com/Pingu-Studio/MaidnCLI/internal/openbao"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSecretConfigPath, appSecretFile, appSecretTokenFile, appSecretGrantEnvironment string
|
var appSecretConfigPath, appSecretFile, appSecretTokenFile, appSecretIdentity, appSecretGrantEnvironment string
|
||||||
var appSecretShared, appSecretDeleteYes bool
|
var appSecretShared, appSecretDeleteYes, appSecretGenerate bool
|
||||||
var appSecretGrantShared, appSecretGrantSecrets []string
|
var appSecretGrantShared, appSecretGrantSecrets []string
|
||||||
|
|
||||||
var loadAppSecretConfig = config.Load
|
var loadAppSecretConfig = config.Load
|
||||||
|
|
@ -70,8 +74,10 @@ func init() {
|
||||||
appSecretCmd.PersistentFlags().StringVar(&appSecretConfigPath, "config", "", "Path to private bootstrap config YAML")
|
appSecretCmd.PersistentFlags().StringVar(&appSecretConfigPath, "config", "", "Path to private bootstrap config YAML")
|
||||||
_ = appSecretCmd.MarkPersistentFlagRequired("config")
|
_ = appSecretCmd.MarkPersistentFlagRequired("config")
|
||||||
appSecretCmd.PersistentFlags().StringVar(&appSecretTokenFile, "token-file", "", "Path to restricted OpenBao token file for secret CRUD")
|
appSecretCmd.PersistentFlags().StringVar(&appSecretTokenFile, "token-file", "", "Path to restricted OpenBao token file for secret CRUD")
|
||||||
|
appSecretCmd.PersistentFlags().StringVar(&appSecretIdentity, "identity", "", "Encrypted operational identity: admin or e2e:<app>")
|
||||||
|
|
||||||
appSecretSetCmd.Flags().StringVar(&appSecretFile, "file", "", "Read the secret value from this file instead of stdin")
|
appSecretSetCmd.Flags().StringVar(&appSecretFile, "file", "", "Read the secret value from this file instead of stdin")
|
||||||
|
appSecretSetCmd.Flags().BoolVar(&appSecretGenerate, "generate", false, "Generate a random secret value without printing it")
|
||||||
for _, command := range []*cobra.Command{appSecretSetCmd, appSecretListCmd, appSecretDeleteCmd, appSecretStatusCmd} {
|
for _, command := range []*cobra.Command{appSecretSetCmd, appSecretListCmd, appSecretDeleteCmd, appSecretStatusCmd} {
|
||||||
command.Flags().BoolVar(&appSecretShared, "shared", false, "Use shared/<group>/<secret> instead of apps/<app>/<secret>")
|
command.Flags().BoolVar(&appSecretShared, "shared", false, "Use shared/<group>/<secret> instead of apps/<app>/<secret>")
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +96,12 @@ func runAppSecretSet(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := storeAppSecret(kubeconfig, appSecretTokenFile, path, value); err != nil {
|
tokenFile, cleanup, err := appSecretTokenPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
if err := storeAppSecret(kubeconfig, tokenFile, path, value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(cmd.OutOrStdout(), "stored %s\n", path)
|
fmt.Fprintf(cmd.OutOrStdout(), "stored %s\n", path)
|
||||||
|
|
@ -102,7 +113,12 @@ func runAppSecretList(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
values, err := listAppSecrets(kubeconfig, appSecretTokenFile, appSecretShared, args[0])
|
tokenFile, cleanup, err := appSecretTokenPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
values, err := listAppSecrets(kubeconfig, tokenFile, appSecretShared, args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +136,12 @@ func runAppSecretDelete(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := deleteAppSecret(kubeconfig, appSecretTokenFile, path); err != nil {
|
tokenFile, cleanup, err := appSecretTokenPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
if err := deleteAppSecret(kubeconfig, tokenFile, path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(cmd.OutOrStdout(), "deleted %s\n", path)
|
fmt.Fprintf(cmd.OutOrStdout(), "deleted %s\n", path)
|
||||||
|
|
@ -161,7 +182,12 @@ func runAppSecretStatus(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
present, err := appSecretStatus(kubeconfig, appSecretTokenFile, path)
|
tokenFile, cleanup, err := appSecretTokenPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
present, err := appSecretStatus(kubeconfig, tokenFile, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -201,6 +227,16 @@ func appSecretKubeconfigFromConfig() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAppSecretValue(cmd *cobra.Command) ([]byte, error) {
|
func readAppSecretValue(cmd *cobra.Command) ([]byte, error) {
|
||||||
|
if appSecretGenerate {
|
||||||
|
if appSecretFile != "" {
|
||||||
|
return nil, errors.New("--generate and --file cannot be used together")
|
||||||
|
}
|
||||||
|
value := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte(base64.RawURLEncoding.EncodeToString(value)), nil
|
||||||
|
}
|
||||||
if appSecretFile != "" {
|
if appSecretFile != "" {
|
||||||
value, err := os.ReadFile(appSecretFile)
|
value, err := os.ReadFile(appSecretFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -214,3 +250,46 @@ func readAppSecretValue(cmd *cobra.Command) ([]byte, error) {
|
||||||
}
|
}
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appSecretTokenPath() (string, func(), error) {
|
||||||
|
if appSecretTokenFile != "" {
|
||||||
|
return appSecretTokenFile, func() {}, nil
|
||||||
|
}
|
||||||
|
if appSecretIdentity == "" {
|
||||||
|
return "", nil, errors.New("--token-file or --identity is required")
|
||||||
|
}
|
||||||
|
cfg, err := loadRequiredAppSecretConfig()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
path := ""
|
||||||
|
if appSecretIdentity == "admin" {
|
||||||
|
path = "cicd/app-secret-admin"
|
||||||
|
} else if strings.HasPrefix(appSecretIdentity, "e2e:") {
|
||||||
|
path = "cicd/e2e-" + strings.TrimPrefix(appSecretIdentity, "e2e:")
|
||||||
|
} else {
|
||||||
|
return "", nil, errors.New("--identity must be admin or e2e:<app>")
|
||||||
|
}
|
||||||
|
secrets, err := bootstrap.ReadOperationalSecrets(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.New("read encrypted app-secret identity")
|
||||||
|
}
|
||||||
|
token := secrets[path]["token"]
|
||||||
|
if token == "" {
|
||||||
|
return "", nil, errors.New("configured app-secret identity is absent")
|
||||||
|
}
|
||||||
|
file, err := os.CreateTemp("", "maidn-openbao-token-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if _, err := file.WriteString(token + "\n"); err != nil {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(file.Name())
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
os.Remove(file.Name())
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
return file.Name(), func() { _ = os.Remove(file.Name()) }, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,16 @@ func TestAppSecretDeleteRequiresExplicitConfirmation(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAppSecretGenerateDoesNotReadOrPrintValue(t *testing.T) {
|
||||||
|
originalGenerate, originalFile := appSecretGenerate, appSecretFile
|
||||||
|
t.Cleanup(func() { appSecretGenerate, appSecretFile = originalGenerate, originalFile })
|
||||||
|
appSecretGenerate, appSecretFile = true, ""
|
||||||
|
value, err := readAppSecretValue(&cobra.Command{})
|
||||||
|
if err != nil || len(value) < 40 || strings.Contains(string(value), "\n") {
|
||||||
|
t.Fatal("generated app secret is not a bounded opaque value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAppSecretGrantOnlySavesNewDeclarativeDefinition(t *testing.T) {
|
func TestAppSecretGrantOnlySavesNewDeclarativeDefinition(t *testing.T) {
|
||||||
originalLoad, originalSave := loadAppSecretConfig, saveAppSecretConfig
|
originalLoad, originalSave := loadAppSecretConfig, saveAppSecretConfig
|
||||||
originalConfig, originalEnvironment, originalSecrets, originalShared := appSecretConfigPath, appSecretGrantEnvironment, appSecretGrantSecrets, appSecretGrantShared
|
originalConfig, originalEnvironment, originalSecrets, originalShared := appSecretConfigPath, appSecretGrantEnvironment, appSecretGrantSecrets, appSecretGrantShared
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ var bootstrapInitializeOpenBaoRecovery bool
|
||||||
var bootstrapInitializeOpenBao bool
|
var bootstrapInitializeOpenBao bool
|
||||||
var bootstrapCreateForgejoRegistryToken bool
|
var bootstrapCreateForgejoRegistryToken bool
|
||||||
var bootstrapCreateForgejoDeliveryStatusToken bool
|
var bootstrapCreateForgejoDeliveryStatusToken bool
|
||||||
|
var bootstrapProvisionAppSecretIdentities bool
|
||||||
|
var bootstrapE2EApp string
|
||||||
var bootstrapRegisterWebhook bool
|
var bootstrapRegisterWebhook bool
|
||||||
var bootstrapRotateWebhookAuthorization bool
|
var bootstrapRotateWebhookAuthorization bool
|
||||||
var bootstrapMergeBootstrapPR bool
|
var bootstrapMergeBootstrapPR bool
|
||||||
|
|
@ -55,6 +57,8 @@ func init() {
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapInitializeOpenBao, "initialize-openbao", false, "Initialize OpenBao and seed encrypted operational secrets for --config")
|
bootstrapCmd.Flags().BoolVar(&bootstrapInitializeOpenBao, "initialize-openbao", false, "Initialize OpenBao and seed encrypted operational secrets for --config")
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapCreateForgejoRegistryToken, "create-forgejo-registry-token", false, "Create a least-privilege Forgejo package registry token and seed it through OpenBao")
|
bootstrapCmd.Flags().BoolVar(&bootstrapCreateForgejoRegistryToken, "create-forgejo-registry-token", false, "Create a least-privilege Forgejo package registry token and seed it through OpenBao")
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapCreateForgejoDeliveryStatusToken, "create-forgejo-delivery-status-token", false, "Create or reuse the Forgejo delivery-status token and seed it through OpenBao")
|
bootstrapCmd.Flags().BoolVar(&bootstrapCreateForgejoDeliveryStatusToken, "create-forgejo-delivery-status-token", false, "Create or reuse the Forgejo delivery-status token and seed it through OpenBao")
|
||||||
|
bootstrapCmd.Flags().BoolVar(&bootstrapProvisionAppSecretIdentities, "provision-app-secret-identities", false, "Create restricted app-secret and E2E OpenBao identities")
|
||||||
|
bootstrapCmd.Flags().StringVar(&bootstrapE2EApp, "e2e-app", "", "Fixture app granted an E2E probe identity")
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapRegisterWebhook, "register-webhook", false, "Seed OpenBao secrets and register the Forgejo webhook")
|
bootstrapCmd.Flags().BoolVar(&bootstrapRegisterWebhook, "register-webhook", false, "Seed OpenBao secrets and register the Forgejo webhook")
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapRotateWebhookAuthorization, "rotate-webhook-authorization", false, "Replace the Forgejo webhook authorization and reconcile it through OpenBao")
|
bootstrapCmd.Flags().BoolVar(&bootstrapRotateWebhookAuthorization, "rotate-webhook-authorization", false, "Replace the Forgejo webhook authorization and reconcile it through OpenBao")
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping")
|
bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping")
|
||||||
|
|
@ -66,6 +70,19 @@ func init() {
|
||||||
func runBootstrap(cmd *cobra.Command, args []string) error {
|
func runBootstrap(cmd *cobra.Command, args []string) error {
|
||||||
var cfg config.Config
|
var cfg config.Config
|
||||||
var err error
|
var err error
|
||||||
|
if bootstrapProvisionAppSecretIdentities {
|
||||||
|
if bootstrapConfigPath == "" || bootstrapE2EApp == "" {
|
||||||
|
return errors.New("--provision-app-secret-identities requires --config and --e2e-app")
|
||||||
|
}
|
||||||
|
cfg, err = config.Load(bootstrapConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := bootstrap.EnsureTemplateRevisions(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bootstrap.ProvisionAppSecretIdentities(cfg, bootstrapE2EApp)
|
||||||
|
}
|
||||||
if bootstrapCreateForgejoRegistryToken {
|
if bootstrapCreateForgejoRegistryToken {
|
||||||
if bootstrapConfigPath == "" {
|
if bootstrapConfigPath == "" {
|
||||||
return fmt.Errorf("--create-forgejo-registry-token requires --config")
|
return fmt.Errorf("--create-forgejo-registry-token requires --config")
|
||||||
|
|
|
||||||
|
|
@ -136,9 +136,10 @@ code and therefore requires a narrowly scoped, disposable credential.
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
1. Create the least-privilege upstream credential.
|
1. Create the least-privilege upstream credential.
|
||||||
2. Write its value to the declared OpenBao path with `cicd-tool app secret set`
|
2. Write its value with `cicd-tool app secret set` using stdin, `--file`, or
|
||||||
using stdin or `--file` and a least-privilege `--token-file`. Never put a
|
`--generate`. Use a least-privilege `--token-file` or a bootstrap-provisioned
|
||||||
value or token in YAML, a URL, a command argument, output, or Git.
|
`--identity`; never put a value or token in YAML, a URL, a command argument,
|
||||||
|
output, or Git.
|
||||||
3. Add the reviewed grant and GitOps resources.
|
3. Add the reviewed grant and GitOps resources.
|
||||||
4. Bootstrap or reconcile to create the OpenBao role and policy.
|
4. Bootstrap or reconcile to create the OpenBao role and policy.
|
||||||
5. Verify the target ExternalSecret becomes Ready without printing its Secret.
|
5. Verify the target ExternalSecret becomes Ready without printing its Secret.
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,14 @@ The webhook-only path requires a complete delivery contract, an approved configu
|
||||||
- Do not revoke a previous credential until OpenBao, External Secrets, and every listed consumer have passed validation.
|
- Do not revoke a previous credential until OpenBao, External Secrets, and every listed consumer have passed validation.
|
||||||
- Use the sanitized procedure in [runbooks/credential-rotation.md](runbooks/credential-rotation.md) for any live rotation.
|
- Use the sanitized procedure in [runbooks/credential-rotation.md](runbooks/credential-rotation.md) for any live rotation.
|
||||||
- Application, artifact, and shared-secret access is documented in [secret-grants.md](secret-grants.md). Secret values remain outside that declaration.
|
- Application, artifact, and shared-secret access is documented in [secret-grants.md](secret-grants.md). Secret values remain outside that declaration.
|
||||||
|
|
||||||
|
## Automated app-secret identities
|
||||||
|
|
||||||
|
Use `bootstrap --provision-app-secret-identities --e2e-app <app>` to create a
|
||||||
|
short-lived non-root `admin` identity and an exact-path E2E probe identity.
|
||||||
|
Bootstrap reads root recovery material only through its encrypted recovery
|
||||||
|
bundle, writes the generated tokens only into encrypted operational state, and
|
||||||
|
never prints either value. `app secret --identity admin` and
|
||||||
|
`app secret --identity e2e:<app>` create a temporary local token file only for
|
||||||
|
the command lifetime. Root tokens, recovery bundles, and unseal shares are not
|
||||||
|
valid app-secret identities.
|
||||||
|
|
|
||||||
|
|
@ -985,6 +985,23 @@ func InitializeOpenBao(cfg config.Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvisionAppSecretIdentities rotates short-lived restricted tokens and
|
||||||
|
// immediately reseeds encrypted operational state through OpenBao.
|
||||||
|
func ProvisionAppSecretIdentities(cfg config.Config, app string) error {
|
||||||
|
kubeconfig := filepath.Join(cfg.Git.CloneParent, cfg.Talos.RepoDirName, cfg.Talos.GeneratedDir, "kubeconfig")
|
||||||
|
tokens, err := openbao.ProvisionAppSecretIdentities(kubeconfig, cfg.SOPS.RecoveryIdentityPath, cfg.SOPS.RecoveryBundlePath, app)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := UpsertOperationalSecret(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, "cicd/app-secret-admin", "token", tokens.Admin); err != nil {
|
||||||
|
return errors.New("save app-secret admin token")
|
||||||
|
}
|
||||||
|
if err := UpsertOperationalSecret(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, "cicd/e2e-"+app, "token", tokens.E2E); err != nil {
|
||||||
|
return errors.New("save E2E app-secret token")
|
||||||
|
}
|
||||||
|
return InitializeOpenBao(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
func NewWebhookAuthorization() (string, error) {
|
func NewWebhookAuthorization() (string, error) {
|
||||||
value := make([]byte, 32)
|
value := make([]byte, 32)
|
||||||
if _, err := rand.Read(value); err != nil {
|
if _, err := rand.Read(value); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AppSecretIdentityTokens struct {
|
||||||
|
Admin string
|
||||||
|
E2E string
|
||||||
|
}
|
||||||
|
|
||||||
type status struct {
|
type status struct {
|
||||||
Initialized bool `json:"initialized"`
|
Initialized bool `json:"initialized"`
|
||||||
Sealed bool `json:"sealed"`
|
Sealed bool `json:"sealed"`
|
||||||
|
|
@ -416,6 +421,61 @@ func ConfigureSecretGrants(kubeconfig, identityPath, bundlePath string, grants [
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvisionAppSecretIdentities creates short-lived non-root tokens for secret
|
||||||
|
// administration and one fixture probe. Tokens are returned only to be placed
|
||||||
|
// into encrypted operational state by the caller.
|
||||||
|
func ProvisionAppSecretIdentities(kubeconfig, identityPath, bundlePath, app string) (AppSecretIdentityTokens, error) {
|
||||||
|
if !managedSecretPart.MatchString(app) {
|
||||||
|
return AppSecretIdentityTokens{}, errors.New("E2E application must be a lowercase DNS label")
|
||||||
|
}
|
||||||
|
material, err := ReadRecoveryMaterial(identityPath, bundlePath)
|
||||||
|
if err != nil {
|
||||||
|
return AppSecretIdentityTokens{}, err
|
||||||
|
}
|
||||||
|
script := `read -r root_token
|
||||||
|
export BAO_TOKEN="$root_token"
|
||||||
|
cat >/tmp/maidn-app-secret-admin.hcl <<'EOF'
|
||||||
|
path "secret/data/apps/*" { capabilities = ["create", "update"] }
|
||||||
|
path "secret/metadata/apps/*" { capabilities = ["list", "read", "delete"] }
|
||||||
|
path "secret/data/shared/*" { capabilities = ["create", "update"] }
|
||||||
|
path "secret/metadata/shared/*" { capabilities = ["list", "read", "delete"] }
|
||||||
|
EOF
|
||||||
|
cat >/tmp/maidn-e2e.hcl <<'EOF'
|
||||||
|
path "secret/data/apps/` + app + `/E2E_PROBE" { capabilities = ["create", "update"] }
|
||||||
|
path "secret/metadata/apps/` + app + `" { capabilities = ["list"] }
|
||||||
|
path "secret/metadata/apps/` + app + `/E2E_PROBE" { capabilities = ["read", "delete"] }
|
||||||
|
EOF
|
||||||
|
bao policy write maidn-app-secret-admin /tmp/maidn-app-secret-admin.hcl >/dev/null
|
||||||
|
bao policy write maidn-e2e-` + app + ` /tmp/maidn-e2e.hcl >/dev/null
|
||||||
|
bao token create -orphan -policy=maidn-app-secret-admin -ttl=1h -explicit-max-ttl=1h -format=json
|
||||||
|
bao token create -orphan -policy=maidn-e2e-` + app + ` -ttl=1h -explicit-max-ttl=1h -format=json
|
||||||
|
rm -f /tmp/maidn-app-secret-admin.hcl /tmp/maidn-e2e.hcl`
|
||||||
|
output, err := execInPodMutation(kubeconfig, []byte(material.RootToken+"\n"), "sh", "-ec", script)
|
||||||
|
if err != nil {
|
||||||
|
return AppSecretIdentityTokens{}, openBaoMutationError("provision app-secret identities", err, output, material.RootToken)
|
||||||
|
}
|
||||||
|
var responses []struct {
|
||||||
|
Auth struct {
|
||||||
|
ClientToken string `json:"client_token"`
|
||||||
|
} `json:"auth"`
|
||||||
|
}
|
||||||
|
for _, line := range bytes.Split(bytes.TrimSpace(output), []byte("\n")) {
|
||||||
|
var response struct {
|
||||||
|
Auth struct {
|
||||||
|
ClientToken string `json:"client_token"`
|
||||||
|
} `json:"auth"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(line, &response); err != nil || response.Auth.ClientToken == "" {
|
||||||
|
return AppSecretIdentityTokens{}, errors.New("parse provisioned app-secret identity")
|
||||||
|
}
|
||||||
|
responses = append(responses, response)
|
||||||
|
}
|
||||||
|
if len(responses) != 2 {
|
||||||
|
return AppSecretIdentityTokens{}, errors.New("provision app-secret identities returned an incomplete result")
|
||||||
|
}
|
||||||
|
return AppSecretIdentityTokens{Admin: responses[0].Auth.ClientToken, E2E: responses[1].Auth.ClientToken}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func openBaoMutationError(action string, err error, output []byte, sensitive ...string) error {
|
func openBaoMutationError(action string, err error, output []byte, sensitive ...string) error {
|
||||||
diagnostic := redactOpenBaoDiagnostic(strings.TrimSpace(string(output)), sensitive...)
|
diagnostic := redactOpenBaoDiagnostic(strings.TrimSpace(string(output)), sensitive...)
|
||||||
if diagnostic == "" && err != nil {
|
if diagnostic == "" && err != nil {
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,25 @@ func TestConfigureSecretGrantsPreservesRedactedPolicyDiagnostics(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProvisionAppSecretIdentitiesScopesFixtureWithoutRootLeak(t *testing.T) {
|
||||||
|
originalDecrypt, originalMutation := decryptRecovery, execInPodMutation
|
||||||
|
t.Cleanup(func() { decryptRecovery, execInPodMutation = originalDecrypt, originalMutation })
|
||||||
|
decryptRecovery = func(string, string) ([]byte, error) {
|
||||||
|
return []byte(`{"root_token":"root-token","unseal_keys_b64":["share"],"unseal_threshold":1}`), nil
|
||||||
|
}
|
||||||
|
execInPodMutation = func(_ string, input []byte, args ...string) ([]byte, error) {
|
||||||
|
command := strings.Join(args, " ")
|
||||||
|
if string(input) != "root-token\n" || !strings.Contains(command, `secret/data/apps/maidn-e2e-web/E2E_PROBE`) || strings.Contains(command, `secret/data/apps/maidn-e2e-web/*`) {
|
||||||
|
t.Fatal("fixture identity policy scope is incorrect")
|
||||||
|
}
|
||||||
|
return []byte("{\"auth\":{\"client_token\":\"admin-token\"}}\n{\"auth\":{\"client_token\":\"e2e-token\"}}\n"), nil
|
||||||
|
}
|
||||||
|
tokens, err := ProvisionAppSecretIdentities("kubeconfig", "identity", "bundle", "maidn-e2e-web")
|
||||||
|
if err != nil || tokens.Admin != "admin-token" || tokens.E2E != "e2e-token" {
|
||||||
|
t.Fatalf("ProvisionAppSecretIdentities() = %#v, %v", tokens, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigureKubernetesAuthLimitsPlatformStore(t *testing.T) {
|
func TestConfigureKubernetesAuthLimitsPlatformStore(t *testing.T) {
|
||||||
original := execInPodMutation
|
original := execInPodMutation
|
||||||
t.Cleanup(func() { execInPodMutation = original })
|
t.Cleanup(func() { execInPodMutation = original })
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue