github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/e2e/vaultsecrets/vaultsecrets.go (about)

     1  package vaultsecrets
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"regexp"
    10  	"strings"
    11  	"time"
    12  
    13  	e2e "github.com/hashicorp/nomad/e2e/e2eutil"
    14  	"github.com/hashicorp/nomad/e2e/framework"
    15  	"github.com/hashicorp/nomad/helper/uuid"
    16  	"github.com/hashicorp/nomad/testutil"
    17  )
    18  
    19  const ns = ""
    20  
    21  type VaultSecretsTest struct {
    22  	framework.TC
    23  	secretsPath string
    24  	pkiPath     string
    25  	jobIDs      []string
    26  	policies    []string
    27  }
    28  
    29  func init() {
    30  	framework.AddSuites(&framework.TestSuite{
    31  		Component:   "VaultSecrets",
    32  		CanRunLocal: true,
    33  		Consul:      true,
    34  		Vault:       true,
    35  		Cases: []framework.TestCase{
    36  			new(VaultSecretsTest),
    37  		},
    38  	})
    39  }
    40  
    41  func (tc *VaultSecretsTest) BeforeAll(f *framework.F) {
    42  	e2e.WaitForLeader(f.T(), tc.Nomad())
    43  	e2e.WaitForNodesReady(f.T(), tc.Nomad(), 1)
    44  }
    45  
    46  func (tc *VaultSecretsTest) AfterEach(f *framework.F) {
    47  	if os.Getenv("NOMAD_TEST_SKIPCLEANUP") == "1" {
    48  		return
    49  	}
    50  
    51  	for _, id := range tc.jobIDs {
    52  		_, err := e2e.Command("nomad", "job", "stop", "-purge", id)
    53  		f.Assert().NoError(err, "could not clean up job", id)
    54  	}
    55  	tc.jobIDs = []string{}
    56  
    57  	for _, policy := range tc.policies {
    58  		_, err := e2e.Command("vault", "policy", "delete", policy)
    59  		f.Assert().NoError(err, "could not clean up vault policy", policy)
    60  	}
    61  	tc.policies = []string{}
    62  
    63  	// disabling the secrets engines will wipe all the secrets as well
    64  	_, err := e2e.Command("vault", "secrets", "disable", tc.secretsPath)
    65  	f.Assert().NoError(err)
    66  	_, err = e2e.Command("vault", "secrets", "disable", tc.pkiPath)
    67  	f.Assert().NoError(err)
    68  
    69  	_, err = e2e.Command("nomad", "system", "gc")
    70  	f.NoError(err)
    71  }
    72  
    73  func (tc *VaultSecretsTest) TestVaultSecrets(f *framework.F) {
    74  
    75  	// use a random suffix to encapsulate test keys, polices, etc.
    76  	// for cleanup from vault
    77  	testID := uuid.Generate()[0:8]
    78  	jobID := "test-vault-secrets-" + testID
    79  	tc.secretsPath = "secrets-" + testID
    80  	tc.pkiPath = "pki-" + testID
    81  	secretValue := uuid.Generate()
    82  	secretKey := tc.secretsPath + "/data/myapp"
    83  	pkiCertIssue := tc.pkiPath + "/issue/nomad"
    84  	policyID := "access-secrets-" + testID
    85  	index := 0
    86  	wc := &e2e.WaitConfig{Retries: 500}
    87  	interval, retries := wc.OrDefault()
    88  
    89  	setupCmds := []string{
    90  
    91  		// configure KV secrets engine
    92  		// Note: the secret key is written to 'secret-###/myapp' but the kv2 API
    93  		// for Vault implicitly turns that into 'secret-###/data/myapp' so we
    94  		// need to use the longer path for everything other than kv put/get
    95  		fmt.Sprintf("vault secrets enable -path=%s kv-v2", tc.secretsPath),
    96  		fmt.Sprintf("vault kv put %s/myapp key=%s", tc.secretsPath, secretValue),
    97  		fmt.Sprintf("vault secrets tune -max-lease-ttl=1m %s", tc.secretsPath),
    98  
    99  		// configure PKI secrets engine
   100  		fmt.Sprintf("vault secrets enable -path=%s pki", tc.pkiPath),
   101  		fmt.Sprintf("vault write %s/root/generate/internal "+
   102  			"common_name=service.consul ttl=1h", tc.pkiPath),
   103  		fmt.Sprintf("vault write %s/roles/nomad "+
   104  			"allowed_domains=service.consul "+
   105  			"allow_subdomains=true "+
   106  			"generate_lease=true "+
   107  			"max_ttl=1m", tc.pkiPath),
   108  		fmt.Sprintf("vault secrets tune -max-lease-ttl=1m %s", tc.pkiPath),
   109  	}
   110  
   111  	for _, setupCmd := range setupCmds {
   112  		cmd := strings.Split(setupCmd, " ")
   113  		out, err := e2e.Command(cmd[0], cmd[1:]...)
   114  		f.NoError(err, fmt.Sprintf("error for %q:\n%s", setupCmd, out))
   115  	}
   116  
   117  	// we can't set an empty policy in our job, so write a bogus policy that
   118  	// doesn't have access to any of the paths we're using
   119  	out, err := writePolicy(policyID, "./vaultsecrets/input/policy-bad.hcl", testID)
   120  	f.NoError(err, out)
   121  	tc.policies = append(tc.policies, policyID)
   122  
   123  	index++
   124  	err = runJob(jobID, testID, index)
   125  	f.NoError(err, "could not register job")
   126  	tc.jobIDs = append(tc.jobIDs, jobID)
   127  
   128  	// job doesn't have access to secrets, so they can't start
   129  	err = e2e.WaitForAllocStatusExpected(jobID, ns, []string{"pending"})
   130  	f.NoError(err, "expected pending allocation")
   131  
   132  	// we should get a task event about why they can't start
   133  	expect := fmt.Sprintf("Missing: vault.read(%s), vault.write(%s", secretKey, pkiCertIssue)
   134  
   135  	allocID, err := latestAllocID(jobID)
   136  	f.NoError(err)
   137  
   138  	testutil.WaitForResultRetries(retries, func() (bool, error) {
   139  		time.Sleep(interval)
   140  		out, err := e2e.Command("nomad", "alloc", "status", allocID)
   141  		f.NoError(err, "could not get allocation status")
   142  		return strings.Contains(out, expect),
   143  			fmt.Errorf("expected '%s', got\n%v", expect, out)
   144  	}, func(e error) {
   145  		f.NoError(e)
   146  	})
   147  
   148  	// write a working policy and redeploy
   149  	out, err = writePolicy(policyID, "./vaultsecrets/input/policy-good.hcl", testID)
   150  	f.NoError(err, out)
   151  	index++
   152  	err = runJob(jobID, testID, index)
   153  	f.NoError(err, "could not register job")
   154  
   155  	// record the rough start of vault token TTL window, so that we don't have
   156  	// to wait excessively later on
   157  	ttlStart := time.Now()
   158  
   159  	// job should be now unblocked
   160  	err = e2e.WaitForAllocStatusExpected(jobID, ns, []string{"running", "complete"})
   161  	f.NoError(err, "expected running allocation")
   162  
   163  	allocID, err = latestAllocID(jobID)
   164  	f.NoError(err)
   165  
   166  	renderedCert, err := waitForAllocSecret(allocID, "task", "/secrets/certificate.crt",
   167  		func(out string) bool {
   168  			return strings.Contains(out, "BEGIN CERTIFICATE")
   169  		}, wc)
   170  	f.NoError(err)
   171  
   172  	_, err = waitForAllocSecret(allocID, "task", "/secrets/access.key",
   173  		func(out string) bool {
   174  			return strings.Contains(out, secretValue)
   175  		}, wc)
   176  	f.NoError(err)
   177  
   178  	var re = regexp.MustCompile(`VAULT_TOKEN=(.*)`)
   179  
   180  	// check vault token was written and save it for later comparison
   181  	out, err = e2e.AllocExec(allocID, "task", "env", ns, nil)
   182  	f.NoError(err)
   183  	match := re.FindStringSubmatch(out)
   184  	f.NotNil(match, fmt.Errorf("could not find VAULT_TOKEN, got:%v\n", out))
   185  	taskToken := match[1]
   186  
   187  	// Update secret
   188  	out, err = e2e.Command("vault", "kv", "put",
   189  		fmt.Sprintf("%s/myapp", tc.secretsPath), "key=UPDATED")
   190  	f.NoError(err, out)
   191  
   192  	elapsed := time.Since(ttlStart)
   193  	time.Sleep((time.Second * 60) - elapsed)
   194  
   195  	// tokens will not be updated
   196  	out, err = e2e.AllocExec(allocID, "task", "env", ns, nil)
   197  	f.NoError(err)
   198  	match = re.FindStringSubmatch(out)
   199  	f.NotNil(match, fmt.Errorf("could not find VAULT_TOKEN, got:%v\n", out))
   200  	f.Equal(taskToken, match[1])
   201  
   202  	// cert will be renewed
   203  	_, err = waitForAllocSecret(allocID, "task", "/secrets/certificate.crt",
   204  		func(out string) bool {
   205  			return strings.Contains(out, "BEGIN CERTIFICATE") &&
   206  				out != renderedCert
   207  		}, wc)
   208  	f.NoError(err)
   209  
   210  	// secret will *not* be renewed because it doesn't have a lease to expire
   211  	_, err = waitForAllocSecret(allocID, "task", "/secrets/access.key",
   212  		func(out string) bool {
   213  			return strings.Contains(out, secretValue)
   214  		}, wc)
   215  	f.NoError(err)
   216  
   217  }
   218  
   219  // We need to namespace the keys in the policy, so read it in and replace the
   220  // values of the policy names
   221  func writePolicy(policyID, policyPath, testID string) (string, error) {
   222  	raw, err := ioutil.ReadFile(policyPath)
   223  	if err != nil {
   224  		return "", err
   225  	}
   226  	policyDoc := string(raw)
   227  	policyDoc = strings.ReplaceAll(policyDoc, "TESTID", testID)
   228  
   229  	cmd := exec.Command("vault", "policy", "write", policyID, "-")
   230  	stdin, err := cmd.StdinPipe()
   231  	if err != nil {
   232  		return "", err
   233  	}
   234  
   235  	go func() {
   236  		defer stdin.Close()
   237  		io.WriteString(stdin, policyDoc)
   238  	}()
   239  
   240  	out, err := cmd.CombinedOutput()
   241  	return string(out), err
   242  }
   243  
   244  // We need to namespace the vault paths in the job, so parse it
   245  // and replace the values of the template and vault fields
   246  func runJob(jobID, testID string, index int) error {
   247  
   248  	raw, err := ioutil.ReadFile("./vaultsecrets/input/secrets.nomad")
   249  	if err != nil {
   250  		return err
   251  	}
   252  	jobspec := string(raw)
   253  	jobspec = strings.ReplaceAll(jobspec, "TESTID", testID)
   254  	jobspec = strings.ReplaceAll(jobspec, "DEPLOYNUMBER", string(rune(index)))
   255  
   256  	return e2e.RegisterFromJobspec(jobID, jobspec)
   257  }
   258  
   259  // waitForAllocSecret is similar to e2e.WaitForAllocFile but uses `alloc exec`
   260  // to be able to read the secrets dir, which is not available to `alloc fs`
   261  func waitForAllocSecret(allocID, taskID, path string, test func(string) bool, wc *e2e.WaitConfig) (string, error) {
   262  	var err error
   263  	var out string
   264  	interval, retries := wc.OrDefault()
   265  
   266  	testutil.WaitForResultRetries(retries, func() (bool, error) {
   267  		time.Sleep(interval)
   268  		out, err = e2e.Command("nomad", "alloc", "exec", "-task", taskID, allocID, "cat", path)
   269  		if err != nil {
   270  			return false, fmt.Errorf("could not get file %q from allocation %q: %v",
   271  				path, allocID, err)
   272  		}
   273  		return test(out),
   274  			fmt.Errorf("test for file content failed: got\n%#v", out)
   275  	}, func(e error) {
   276  		err = e
   277  	})
   278  	return out, err
   279  }
   280  
   281  // this will always be sorted
   282  func latestAllocID(jobID string) (string, error) {
   283  	allocs, err := e2e.AllocsForJob(jobID, ns)
   284  	if err != nil {
   285  		return "", err
   286  	}
   287  	return allocs[0]["ID"], nil
   288  }