diff --git a/benchmarktests/target_secret_nomad.go b/benchmarktests/target_secret_nomad.go new file mode 100644 index 00000000..8629ba69 --- /dev/null +++ b/benchmarktests/target_secret_nomad.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package benchmarktests + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "strings" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/vault/api" + vegeta "github.com/tsenart/vegeta/v12/lib" +) + +// Constants for test +const ( + NomadSecretTestType = "nomad_secret" + NomadSecretTestMethod = "GET" + NomadTokenEnvVar = VaultBenchmarkEnvVarPrefix + "NOMAD_TOKEN" +) + +func init() { + // "Register" this test to the main test registry + TestList[NomadSecretTestType] = func() BenchmarkBuilder { return &NomadTest{} } +} + +type NomadTest struct { + pathPrefix string + header http.Header + roleName string + config *NomadSecretTestConfig + logger hclog.Logger +} + +type NomadSecretTestConfig struct { + NomadConfig *NomadConfig `hcl:"nomad,block"` + NomadRoleConfig *NomadRoleConfig `hcl:"role,block"` +} + +type NomadConfig struct { + Address string `hcl:"address"` + Token string `hcl:"token,optional"` + MaxTokenNameLength int `hcl:"max_token_name_length,optional"` + CaCert string `hcl:"ca_cert,optional"` + ClientCert string `hcl:"client_cert,optional"` + ClientKey string `hcl:"client_key,optional"` +} + +type NomadRoleConfig struct { + Name string `hcl:"name,optional"` + Policies []string `hcl:"policies,optional"` + Global bool `hcl:"global,optional"` + Type string `hcl:"type,optional"` +} + +func (c *NomadTest) ParseConfig(body hcl.Body) error { + testConfig := &struct { + Config *NomadSecretTestConfig `hcl:"config,block"` + }{ + Config: &NomadSecretTestConfig{ + NomadConfig: &NomadConfig{ + Token: os.Getenv(NomadTokenEnvVar), + }, + NomadRoleConfig: &NomadRoleConfig{ + Name: "benchmark-role", + }, + }, + } + + diags := gohcl.DecodeBody(body, nil, testConfig) + if diags.HasErrors() { + return fmt.Errorf("error decoding to struct: %v", diags) + } + c.config = testConfig.Config + + // Ensure that the token has been set by either the environment variable or the config + if c.config.NomadConfig.Token == "" { + return fmt.Errorf("nomad token must be set") + } + return nil +} + +func (c *NomadTest) Target(client *api.Client) vegeta.Target { + return vegeta.Target{ + Method: "GET", + URL: client.Address() + c.pathPrefix + "/creds/" + c.roleName, + Header: c.header, + } +} + +func (c *NomadTest) Cleanup(client *api.Client) error { + c.logger.Trace(cleanupLogMessage(c.pathPrefix)) + _, err := client.Logical().Delete(strings.Replace(c.pathPrefix, "/v1/", "/sys/mounts/", 1)) + if err != nil { + return fmt.Errorf("error cleaning up mount: %v", err) + } + return nil +} + +func (c *NomadTest) GetTargetInfo() TargetInfo { + return TargetInfo{ + method: NomadSecretTestMethod, + pathPrefix: c.pathPrefix, + } +} + +func (c *NomadTest) Setup(client *api.Client, randomMountName bool, mountName string) (BenchmarkBuilder, error) { + var err error + secretPath := mountName + config := c.config + c.logger = targetLogger.Named(NomadSecretTestType) + + if randomMountName { + secretPath, err = uuid.GenerateUUID() + if err != nil { + log.Fatalf("can't create UUID") + } + } + + c.logger.Trace(mountLogMessage("secrets", "nomad", secretPath)) + err = client.Sys().Mount(secretPath, &api.MountInput{ + Type: "nomad", + }) + if err != nil { + return nil, fmt.Errorf("error mounting nomad: %v", err) + } + + setupLogger := c.logger.Named(secretPath) + + // Decode Nomad Config + setupLogger.Trace(parsingConfigLogMessage("nomad")) + nomadConfigData, err := structToMap(config.NomadConfig) + if err != nil { + return nil, fmt.Errorf("error parsing nomad config from struct: %v", err) + } + + // Write Nomad config + setupLogger.Trace(writingLogMessage("nomad config")) + _, err = client.Logical().Write(secretPath+"/config/access", nomadConfigData) + if err != nil { + return nil, fmt.Errorf("error writing nomad config: %v", err) + } + + // Decode Role Config + setupLogger.Trace(parsingConfigLogMessage("role")) + nomadRoleConfigData, err := structToMap(config.NomadRoleConfig) + if err != nil { + return nil, fmt.Errorf("error parsing role config from struct: %v", err) + } + + // Create Role + setupLogger.Trace(writingLogMessage("nomad role"), "name", config.NomadRoleConfig.Name) + _, err = client.Logical().Write(secretPath+"/role/"+config.NomadRoleConfig.Name, nomadRoleConfigData) + if err != nil { + return nil, fmt.Errorf("error writing nomad role: %v", err) + } + + return &NomadTest{ + pathPrefix: "/v1/" + secretPath, + header: generateHeader(client), + roleName: config.NomadRoleConfig.Name, + logger: c.logger, + }, nil +} + +func (c *NomadTest) Flags(fs *flag.FlagSet) {} diff --git a/docs/tests/secret-nomad.md b/docs/tests/secret-nomad.md new file mode 100644 index 00000000..80c81aba --- /dev/null +++ b/docs/tests/secret-nomad.md @@ -0,0 +1,54 @@ +# Nomad Secrets Engine Benchmark + +This benchmark will test the dynamic generation of Nomad credentials. + +## Test Parameters + +### Nomad Database Configuration `nomad` + +#### NOTE: Ensure that the Nomad system has a limit high enough to support the number of roles you are creating. More information can be found in the [Nomad Documentation](https://developer.hashicorp.com/nomad/docs/configuration#limits) + +- `address` `(string: "")` – Specifies the address of the Nomad instance, provided as `"protocol://host:port"` like `"http://127.0.0.1:4646"`. +- `token` `(string: "")` – Specifies the Nomad Management token to use. This value can also be provided on individual calls with the NOMAD_TOKEN environment variable. This can also be provided via the `VAULT_BENCHMARK_NOMAD_TOKEN` environment variable. +- `max_token_name_length` `(int: )` – Specifies the maximum length to use for the name of the Nomad token generated with [Generate Credential](https://developer.hashicorp.com/vault/api-docs/secret/nomad#generate-credential). If omitted, `0` is used and ignored, defaulting to the max value allowed by the Nomad version. For Nomad versions 0.8.3 and earlier, the default is `64`. For Nomad version 0.8.4 and later, the default is `256`. +- `ca_cert` `(string: "")` - CA certificate to use when verifying Nomad server certificate, must be x509 PEM encoded. +- `client_cert` `(string: "")` - Client certificate used for Nomad's TLS communication, must be x509 PEM encoded and if this is set you need to also set client_key. +- `client_key` `(string: "")` - Client key used for Nomad's TLS communication, must be x509 PEM encoded and if this is set you need to also set client_cert. + +### Role Config `role` + +- `name` `(string: "benchmark-role")` – Specifies the name of an existing role against which to create this Nomad tokens. This is part of the request URL. +- `policies` `(string: "")` – Comma separated list of Nomad policies the token is going to be created against. These need to be created beforehand in Nomad. +- `global` `(bool: "false")` – Specifies if the token should be global, as defined in the [Nomad Documentation](https://developer.hashicorp.com/nomad/tutorials/access-control#acl-tokens). +- `type` `(string: "client")` - Specifies the type of token to create when using this role. Valid values are `"client"` or `"management"`. + +## Example Configuration + +```hcl +test "nomad_secret" "nomad_test_1" { + weight = 100 + config { + nomad { + address = "http://127.0.0.1:4646" + token = "NOMAD_TOKEN" + } + role { + global = true + type = "management" + } + } +} + +``` + +### Example Usage + +```bash +$ vault-benchmark run -config=config.hcl +2023-06-01T09:41:27.096-0500 [INFO] vault-benchmark: setting up targets +2023-06-01T09:41:27.102-0500 [INFO] vault-benchmark: starting benchmarks: duration=5s +2023-06-01T09:41:32.311-0500 [INFO] vault-benchmark: benchmark complete +Target: http://127.0.0.1:8200 +op count rate throughput mean 95th% 99th% successRatio +nomad_test_1 177 35.057995 33.990891 290.850018ms 375.292712ms 451.573602ms 100.00% +```