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 }